1 2/* 3Copyright 2012 Igor Vaynberg 4 5Version: 3.4.1 Timestamp: Thu Jun 27 18:02:10 PDT 2013 6 7This software is licensed under the Apache License, Version 2.0 (the "Apache License") or the GNU 8General Public License version 2 (the "GPL License"). You may choose either license to govern your 9use of this software only upon the condition that you accept all of the terms of either the Apache 10License or the GPL License. 11 12You may obtain a copy of the Apache License and the GPL License at: 13 14 http://www.apache.org/licenses/LICENSE-2.0 15 http://www.gnu.org/licenses/gpl-2.0.html 16 17Unless required by applicable law or agreed to in writing, software distributed under the 18Apache License or the GPL Licesnse is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR 19CONDITIONS OF ANY KIND, either express or implied. See the Apache License and the GPL License for 20the specific language governing permissions and limitations under the Apache License and the GPL License. 21*/ 22(function ($) { 23 if(typeof $.fn.each2 == "undefined") { 24 $.fn.extend({ 25 /* 26 * 4-10 times faster .each replacement 27 * use it carefully, as it overrides jQuery context of element on each iteration 28 */ 29 each2 : function (c) { 30 var j = $([0]), i = -1, l = this.length; 31 while ( 32 ++i < l 33 && (j.context = j[0] = this[i]) 34 && c.call(j[0], i, j) !== false //"this"=DOM, i=index, j=jQuery object 35 ); 36 return this; 37 } 38 }); 39 } 40})(jQuery); 41 42(function ($, undefined) { 43 "use strict"; 44 /*global document, window, jQuery, console */ 45 46 if (window.Select2 !== undefined) { 47 return; 48 } 49 50 var KEY, AbstractSelect2, SingleSelect2, MultiSelect2, nextUid, sizer, 51 lastMousePosition={x:0,y:0}, $document, scrollBarDimensions, 52 53 KEY = { 54 TAB: 9, 55 ENTER: 13, 56 ESC: 27, 57 SPACE: 32, 58 LEFT: 37, 59 UP: 38, 60 RIGHT: 39, 61 DOWN: 40, 62 SHIFT: 16, 63 CTRL: 17, 64 ALT: 18, 65 PAGE_UP: 33, 66 PAGE_DOWN: 34, 67 HOME: 36, 68 END: 35, 69 BACKSPACE: 8, 70 DELETE: 46, 71 isArrow: function (k) { 72 k = k.which ? k.which : k; 73 switch (k) { 74 case KEY.LEFT: 75 case KEY.RIGHT: 76 case KEY.UP: 77 case KEY.DOWN: 78 return true; 79 } 80 return false; 81 }, 82 isControl: function (e) { 83 var k = e.which; 84 switch (k) { 85 case KEY.SHIFT: 86 case KEY.CTRL: 87 case KEY.ALT: 88 return true; 89 } 90 91 if (e.metaKey) return true; 92 93 return false; 94 }, 95 isFunctionKey: function (k) { 96 k = k.which ? k.which : k; 97 return k >= 112 && k <= 123; 98 } 99 }, 100 MEASURE_SCROLLBAR_TEMPLATE = "<div class='select2-measure-scrollbar'></div>"; 101 102 $document = $(document); 103 104 nextUid=(function() { var counter=1; return function() { return counter++; }; }()); 105 106 function indexOf(value, array) { 107 var i = 0, l = array.length; 108 for (; i < l; i = i + 1) { 109 if (equal(value, array[i])) return i; 110 } 111 return -1; 112 } 113 114 function measureScrollbar () { 115 var $template = $( MEASURE_SCROLLBAR_TEMPLATE ); 116 $template.appendTo('body'); 117 118 var dim = { 119 width: $template.width() - $template[0].clientWidth, 120 height: $template.height() - $template[0].clientHeight 121 }; 122 $template.remove(); 123 124 return dim; 125 } 126 127 /** 128 * Compares equality of a and b 129 * @param a 130 * @param b 131 */ 132 function equal(a, b) { 133 if (a === b) return true; 134 if (a === undefined || b === undefined) return false; 135 if (a === null || b === null) return false; 136 // Check whether 'a' or 'b' is a string (primitive or object). 137 // The concatenation of an empty string (+'') converts its argument to a string's primitive. 138 if (a.constructor === String) return a+'' === b+''; // a+'' - in case 'a' is a String object 139 if (b.constructor === String) return b+'' === a+''; // b+'' - in case 'b' is a String object 140 return false; 141 } 142 143 /** 144 * Splits the string into an array of values, trimming each value. An empty array is returned for nulls or empty 145 * strings 146 * @param string 147 * @param separator 148 */ 149 function splitVal(string, separator) { 150 var val, i, l; 151 if (string === null || string.length < 1) return []; 152 val = string.split(separator); 153 for (i = 0, l = val.length; i < l; i = i + 1) val[i] = $.trim(val[i]); 154 return val; 155 } 156 157 function getSideBorderPadding(element) { 158 return element.outerWidth(false) - element.width(); 159 } 160 161 function installKeyUpChangeEvent(element) { 162 var key="keyup-change-value";
163 element.on("keydown", function () { 164 if ($.data(element, key) === undefined) { 165 $.data(element, key, element.val()); 166 } 167 }); 168 element.on("keyup", function () { 169 var val= $.data(element, key); 170 if (val !== undefined && element.val() !== val) { 171 $.removeData(element, key); 172 element.trigger("keyup-change"); 173 } 174 }); 175 } 176 177 $document.on("mousemove", function (e) { 178 lastMousePosition.x = e.pageX; 179 lastMousePosition.y = e.pageY; 180 }); 181 182 /** 183 * filters mouse events so an event is fired only if the mouse moved. 184 * 185 * filters out mouse events that occur when mouse is stationary but 186 * the elements under the pointer are scrolled. 187 */ 188 function installFilteredMouseMove(element) { 189 element.on("mousemove", function (e) { 190 var lastpos = lastMousePosition; 191 if (lastpos === undefined || lastpos.x !== e.pageX || lastpos.y !== e.pageY) { 192 $(e.target).trigger("mousemove-filtered", e); 193 } 194 }); 195 } 196 197 /** 198 * Debounces a function. Returns a function that calls the original fn function only if no invocations have been made 199 * within the last quietMillis milliseconds. 200 * 201 * @param quietMillis number of milliseconds to wait before invoking fn 202 * @param fn function to be debounced 203 * @param ctx object to be used as this reference within fn 204 * @return debounced version of fn 205 */ 206 function debounce(quietMillis, fn, ctx) { 207 ctx = ctx || undefined; 208 var timeout; 209 return function () { 210 var args = arguments; 211 window.clearTimeout(timeout); 212 timeout = window.setTimeout(function() { 213 fn.apply(ctx, args); 214 }, quietMillis); 215 }; 216 } 217 218 /** 219 * A simple implementation of a thunk 220 * @param formula function used to lazily initialize the thunk 221 * @return {Function} 222 */ 223 function thunk(formula) { 224 var evaluated = false, 225 value; 226 return function() { 227 if (evaluated === false) { value = formula(); evaluated = true; } 228 return value; 229 }; 230 }; 231 232 function installDebouncedScroll(threshold, element) { 233 var notify = debounce(threshold, function (e) { element.trigger("scroll-debounced", e);}); 234 element.on("scroll", function (e) { 235 if (indexOf(e.target, element.get()) >= 0) notify(e); 236 }); 237 } 238 239 function focus($el) { 240 if ($el[0] === document.activeElement) return; 241 242 /* set the focus in a 0 timeout - that way the focus is set after the processing 243 of the current event has finished - which seems like the only reliable way 244 to set focus */ 245 window.setTimeout(function() { 246 var el=$el[0], pos=$el.val().length, range; 247 248 $el.focus(); 249 250 /* make sure el received focus so we do not error out when trying to manipulate the caret. 251 sometimes modals or others listeners may steal it after its set */ 252 if ($el.is(":visible") && el === document.activeElement) { 253 254 /* after the focus is set move the caret to the end, necessary when we val() 255 just before setting focus */ 256 if(el.setSelectionRange) 257 { 258 el.setSelectionRange(pos, pos); 259 } 260 else if (el.createTextRange) { 261 range = el.createTextRange(); 262 range.collapse(false); 263 range.select(); 264 } 265 } 266 }, 0); 267 } 268 269 function getCursorInfo(el) { 270 el = $(el)[0]; 271 var offset = 0; 272 var length = 0; 273 if ('selectionStart' in el) { 274 offset = el.selectionStart; 275 length = el.selectionEnd - offset; 276 } else if ('selection' in document) { 277 el.focus(); 278 var sel = document.selection.createRange(); 279 length = document.selection.createRange().text.length; 280 sel.moveStart('character', -el.value.length); 281 offset = sel.text.length - length; 282 } 283 return { offset: offset, length: length }; 284 } 285 286 function killEvent(event) { 287 event.preventDefault(); 288 event.stopPropagation(); 289 } 290 function killEventImmediately(event) { 291 event.preventDefault(); 292 event.stopImmediatePropagation(); 293 } 294 295 function measureTextWidth(e) { 296 if (!sizer){ 297 var style = e[0].currentStyle || window.getComputedStyle(e[0], null); 298 sizer = $(document.createElement("div")).css({ 299 position: "absolute",
300 left: "-10000px", 301 top: "-10000px", 302 display: "none", 303 fontSize: style.fontSize, 304 fontFamily: style.fontFamily, 305 fontStyle: style.fontStyle, 306 fontWeight: style.fontWeight, 307 letterSpacing: style.letterSpacing, 308 textTransform: style.textTransform, 309 whiteSpace: "nowrap" 310 }); 311 sizer.attr("class","select2-sizer"); 312 $("body").append(sizer); 313 } 314 sizer.text(e.val()); 315 return sizer.width(); 316 } 317 318 function syncCssClasses(dest, src, adapter) { 319 var classes, replacements = [], adapted; 320 321 classes = dest.attr("class"); 322 if (classes) { 323 classes = '' + classes; // for IE which returns object 324 $(classes.split(" ")).each2(function() { 325 if (this.indexOf("select2-") === 0) { 326 replacements.push(this); 327 } 328 }); 329 } 330 classes = src.attr("class"); 331 if (classes) { 332 classes = '' + classes; // for IE which returns object 333 $(classes.split(" ")).each2(function() { 334 if (this.indexOf("select2-") !== 0) { 335 adapted = adapter(this); 336 if (adapted) { 337 replacements.push(this); 338 } 339 } 340 }); 341 } 342 dest.attr("class", replacements.join(" ")); 343 } 344 345 346 function markMatch(text, term, markup, escapeMarkup) { 347 var match=text.toUpperCase().indexOf(term.toUpperCase()), 348 tl=term.length; 349 350 if (match<0) { 351 markup.push(escapeMarkup(text)); 352 return; 353 } 354 355 markup.push(escapeMarkup(text.substring(0, match))); 356 markup.push("<span class='select2-match'>"); 357 markup.push(escapeMarkup(text.substring(match, match + tl))); 358 markup.push("</span>"); 359 markup.push(escapeMarkup(text.substring(match + tl, text.length))); 360 } 361 362 function defaultEscapeMarkup(markup) { 363 var replace_map = { 364 '\\': '\', 365 '&': '&', 366 '<': '<', 367 '>': '>', 368 '"': '"', 369 "'": ''', 370 "/": '/' 371 }; 372 373 return String(markup).replace(/[&<>"'\/\\]/g, function (match) { 374 return replace_map[match]; 375 }); 376 } 377 378 /** 379 * Produces an ajax-based query function 380 * 381 * @param options object containing configuration paramters 382 * @param options.params parameter map for the transport ajax call, can contain such options as cache, jsonpCallback, etc. see $.ajax 383 * @param options.transport function that will be used to execute the ajax request. must be compatible with parameters supported by $.ajax 384 * @param options.url url for the data 385 * @param options.data a function(searchTerm, pageNumber, context) that should return an object c
385ontaining query string parameters for the above url. 386 * @param options.dataType request data type: ajax, jsonp, other datatatypes supported by jQuery's $.ajax function or the transport function if specified 387 * @param options.quietMillis (optional) milliseconds to wait before making the ajaxRequest, helps debounce the ajax function if invoked too often 388 * @param options.results a function(remoteData, pageNumber) that converts data returned form the remote request to the format expected by Select2. 389 * The expected format is an object containing the following keys: 390 * results array of objects that will be used as choices 391 * more (optional) boolean indicating whether there are more results available 392 * Example: {results:[{id:1, text:'Red'},{id:2, text:'Blue'}], more:true} 393 */ 394 function ajax(options) { 395 var timeout, // current scheduled but not yet executed request 396 requestSequence = 0, // sequence used to drop out-of-order responses 397 handler = null, 398 quietMillis = options.quietMillis || 100, 399 ajaxUrl = options.url, 400 self = this; 401 402 return function (query) { 403 window.clearTimeout(timeout); 404 timeout = window.setTimeout(function () { 405 requestSequence += 1; // increment the sequence 406 var requestNumber = requestSequence, // this request's sequence number 407 data = options.data, // ajax data function 408 url = ajaxUrl, // ajax url string or function 409 transport = options.transport || $.fn.select2.ajaxDefaults.transport, 410 // deprecated - to be removed in 4.0 - use params instead 411 deprecated = { 412 type: options.type || 'GET', // set type of request (GET or POST) 413 cache: options.cache || false, 414 jsonpCallback: options.jsonpCallback||undefined, 415 dataType: options.dataType||"json" 416 }, 417 params = $.extend({}, $.fn.select2.ajaxDefaults.params, deprecated); 418 419 data = data ? data.call(self, query.term, query.page, query.context) : null; 420 url = (typeof url === 'function') ? url.call(self, query.term, query.page, query.context) : url; 421 422 if (handler) { handler.abort(); } 423 424 if (options.params) { 425 if ($.isFunction(options.params)) { 426 $.extend(params, options.params.call(self)); 427 } else { 428 $.extend(params, options.params); 429 } 430 } 431 432 $.extend(params, { 433 url: url, 434 dataType: options.dataType, 435 data: data, 436 success: function (data) { 437 if (requestNumber < requestSequence) { 438 return; 439 } 440 // TODO - replace query.page with query so users have access to term, page, etc. 441 var results = options.results(data, query.page); 442 query.callback(results); 443 } 444 }); 445 handler = transport.call(self, params); 446 }, quietMillis); 447 }; 448 } 449 450 /** 451 * Produces a query function that works with a local array 452 * 453 * @param options object containing configuration parameters. The options parameter can either be an array or an 454 * object. 455 * 456 * If the array form is used it is assumed that it contains objects with 'id' and 'text' keys. 457 * 458 * If the object form is used ti is assumed that it contains 'data' and 'text' keys. The 'data' key should contain 459 * an array of objects that will be used as choices. These objects must contain at least an 'id' key. The 'text' 460 * key can either be a String in which case it is expected that each element in the 'data' array has a key with the 461 * value of 'text' which will be used to match choices. Alternatively, text can be a function(item) that can extract 462 * the text. 463 */ 464 function local(options) { 465 var data = options, // data elements 466 dataText, 467 tmp, 468 text = function (item) { return ""+item.text; }; // function used to retrieve the text portion of a data item that is matched against the search 469 470 if ($.isArray(data)) { 471 tmp = data; 472 data = { results: tmp }; 473 } 474 475 if ($.isFunction(data) === false) { 476 tmp = data; 477 data = function() { return tmp; }; 478 } 479 480 var dataItem = data(); 481 if (dataItem.text) { 482 text = dataItem.text; 483 // if text is not a function we assume it to be a key name 484 if (!$.isFunction(text)) { 485 dataText = dataItem.text; // we need to store this in a separate variable because in the next step data gets reset and data.text is no longer available 486 text = function (item) { return item[dataText]; }; 487 } 488 } 489 490 return function (query) { 491 var t = query.term, filtered = { results: [] }, process; 492 if (t === "") { 493 query.callback(data()); 494 return; 495 } 496 497 process = function(datum, collection) { 498 var group, attr; 499 datum = datum[0]; 500 if (datum.children) { 501 group = {}; 502 for (attr in datum) { 503 if (datum.hasOwnProperty(attr)) group[attr]=datum[attr]; 504 } 505 group.children=[]; 506 $(datum.children).each2(function(i, childDatum) { process(childDatum, group.children); }); 507 if (group.children.length || query.matcher(t, text(group), datum)) { 508 collection.push(group); 509 } 510 } else { 511 if (query.matcher(t, text(datum), datum)) { 512 collection.push(datum); 513 } 514 } 515 }; 516 517 $(data().results).each2(function(i, datum) { process(datum, filtered.results); }); 518 query.callback(filtered); 519 }; 520 } 521 522 // TODO javadoc 523 function tags(data) { 524 var isFunc = $.isFunction(data); 525 return function (query) { 526 var t = query.term, filtered = {results: []}; 527 $(isFunc ? data() : data).each(function () { 528 var isObject = this.text !== undefined, 529 text = isObject ? this.text : this; 530 if (t === "" || query.matcher(t, text)) { 531 filtered.results.push(isObject ? this : {id: this, text: this}); 532 } 533 }); 534 query.callback(filtered); 535 }; 536 } 537 538 /** 539 * Checks if the formatter function should be used. 540 * 541 * Throws an error if it is not a function. Returns true if it should be used, 542 * false if no formatting should be performed. 543 * 544 * @param formatter 545 */ 546 function checkFormatter(formatter, formatterName) { 547 if ($.isFunction(formatter)) return true; 548 if (!formatter) return false; 549 throw new Error(formatterName +" must be a function or a falsy value"); 550 } 551 552 function evaluate(val) { 553 return $.isFunction(val) ? val() : val; 554 } 555 556 function countResults(results) { 557 var count = 0; 558 $.each(results, function(i, item) { 559 if (item.children) { 560 count += countResults(item.children); 561 } else { 562 count++; 563 } 564 }); 565 return count; 566 } 567 568 /** 569 * Default tokenizer. This function uses breaks the input on substring match of any string from the 570 * opts.tokenSeparators array and uses opts.createSearchChoice to create the choice object. Both of those 571 * two options have to be defined in order for the tokenizer to work. 572 * 573 * @param input text user has typed so far or pasted into the search f
573ield 574 * @param selection currently selected choices 575 * @param selectCallback function(choice) callback tho add the choice to selection 576 * @param opts select2's opts 577 * @return undefined/null to leave the current input unchanged, or a string to change the input to the returned value 578 */ 579 function defaultTokenizer(input, selection, selectCallback, opts) { 580 var original = input, // store the original so we can compare and know if we need to tell the search to update its text 581 dupe = false, // check for whether a token we extracted represents a duplicate selected choice 582 token, // token 583 index, // position at which the separator was found 584 i, l, // looping variables 585 separator; // the matched separator 586 587 if (!opts.createSearchChoice || !opts.tokenSeparators || opts.tokenSeparators.length < 1) return undefined; 588 589 while (true) { 590 index = -1; 591 592 for (i = 0, l = opts.tokenSeparators.length; i < l; i++) { 593 separator = opts.tokenSeparators[i]; 594 index = input.indexOf(separator); 595 if (index >= 0) break; 596 } 597 598 if (index < 0) break; // did not find any token separator in the input string, bail 599 600 token = input.substring(0, index); 601 input = input.substring(index + separator.length); 602 603 if (token.length > 0) { 604 token = opts.createSearchChoice.call(this, token, selection); 605 if (token !== undefined && token !== null && opts.id(token) !== undefined && opts.id(token) !== null) { 606 dupe = false; 607 for (i = 0, l = selection.length; i < l; i++) { 608 if (equal(opts.id(token), opts.id(selection[i]))) { 609 dupe = true; break; 610 } 611 } 612 613 if (!dupe) selectCallback(token); 614 } 615 } 616 } 617
618 if (original!==input) return input; 619 } 620 621 /** 622 * Creates a new class 623 * 624 * @param superClass 625 * @param methods 626 */ 627 function clazz(SuperClass, methods) { 628 var constructor = function () {}; 629 constructor.prototype = new SuperClass; 630 constructor.prototype.constructor = constructor; 631 constructor.prototype.parent = SuperClass.prototype; 632 constructor.prototype = $.extend(constructor.prototype, methods); 633 return constructor; 634 } 635 636 AbstractSelect2 = clazz(Object, { 637 638 // abstract 639 bind: function (func) { 640 var self = this; 641 return function () { 642 func.apply(self, arguments); 643 }; 644 }, 645 646 // abstract 647 init: function (opts) { 648 var results, search, resultsSelector = ".select2-results", disabled, readonly; 649 650 // prepare options 651 this.opts = opts = this.prepareOpts(opts); 652 653 this.id=opts.id; 654 655 // destroy if called on an existing component 656 if (opts.element.data("select2") !== undefined && 657 opts.element.data("select2") !== null) { 658 opts.element.data("select2").destroy(); 659 } 660 661 this.container = this.createContainer(); 662 663 this.containerId="s2id_"+(opts.element.attr("id") || "autogen"+nextUid()); 664 this.containerSelector="#"+this.containerId.replace(/([;&,\.\+\*\~':"\!\^#$%@\[\]\(\)=>\|])/g, '\\$1'); 665 this.container.attr("id", this.containerId); 666 667 // cache the body so future lookups are cheap 668 this.body = thunk(function() { return opts.element.closest("body"); }); 669 670 syncCssClasses(this.container, this.opts.element, this.opts.adaptContainerCssClass); 671 672 this.container.css(evaluate(opts.containerCss)); 673 this.container.addClass(evaluate(opts.containerCssClass)); 674 675 this.elementTabIndex = this.opts.element.attr("tabindex"); 676 677 // swap container for the element 678 this.opts.element 679 .data("select2", this) 680 .attr("tabindex", "-1") 681 .before(this.container); 682 this.container.data("select2", this); 683 684 this.dropdown = this.container.find(".select2-drop"); 685 this.dropdown.addClass(evaluate(opts.dropdownCssClass)); 686 this.dropdown.data("select2", this); 687 688 this.results = results = this.container.find(resultsSelector); 689 this.search = search = this.container.find("input.select2-input"); 690 691 this.resultsPage = 0; 692 this.context = null; 693 694 // initialize the container 695 this.initContainer(); 696 697 installFilteredMouseMove(this.results); 698 this.dropdown.on("mousemove-filtered touchstart touchmove touchend", resultsSelector, this.bind(this.highlightUnderEvent)); 699 700 installDebouncedScroll(80, this.results); 701 this.dropdown.on("scroll-debounced", resultsSelector, this.bind(this.loadMoreIfNeeded)); 702 703 // do not propagate change event from the search field out of the component 704 $(this.container).on("change", ".select2-input", function(e) {e.stopPropagation();}); 705 $(this.dropdown).on("change", ".select2-input", function(e) {e.stopPropagation();}); 706 707 // if jquery.mousewheel plugin is installed we can prevent out-of-bounds scrolling of results via mousewheel 708 if ($.fn.mousewheel) { 709 results.mousewheel(function (e, delta, deltaX, deltaY) { 710 var top = results.scrollTop(), height; 711 if (deltaY > 0 && top - deltaY <= 0) { 712 results.scrollTop(0); 713 killEvent(e); 714 } else if (deltaY < 0 && results.get(0).scrollHeight - results.scrollTop() + deltaY <= results.height()) { 715 results.scrollTop(results.get(0).scrollHeight - results.height()); 716 killEvent(e); 717 } 718 }); 719 } 720 721 installKeyUpChangeEvent(search); 722 search.on("keyup-change input paste", this.bind(this.updateResults)); 723 search.on("focus", function () { search.addClass("select2-focused"); }); 724 search.on("blur", function () { search.removeClass("select2-focused");}); 725 726 this.dropdown.on("mouseup", resultsSelector, this.bind(function (e) { 727 if ($(e.target).closest(".select2-result-selectable").length > 0) { 728 this.highlightUnderEvent(e); 729 this.selectHighlighted(e); 730 } 731 })); 732 733 // trap all mouse events from leaving the dropdown. sometimes there may be a modal that is listening 734 // for mouse events outside of itself so it can close itself. since the dropdown is now outside the select2's 735 // dom it will trigger the popup close, which is not what we want 736 this.dropdown.on("click mouseup mousedown", function (e) { e.stopPropagation(); }); 737 738 if ($.isFunction(this.opts.initSelection)) { 739 // initialize selection based on the current value of the source element 740 this.initSelection(); 741 742 // if the user has provided a function that can set selection based on the value of the source element 743 // we monitor the change event on the element and trigger it, allowing for two way synchronization 744 this.monitorSource(); 745 } 746 747 if (opts.maximumInputLength !== null) { 748 this.search.attr("maxlength", opts.maximumInputLength); 749 } 750 751 var disabled = opts.element.prop("disabled"); 752 if (disabled === undefined) disabled = false; 753 this.enable(!disabled); 754 755 var readonly = opts.element.prop("readonly"); 756 if (readonly === undefined) readonly = false; 757 this.readonly(readonly); 758 759 // Calculate size of scrollbar 760 scrollBarDimensions = scrollBarDimensions || measureScrollbar(); 761 762 this.autofocus = opts.element.prop("autofocus") 763 opts.element.prop("autofocus", false); 764 if (this.autofocus) this.focus(); 765 }, 766 767 // abstract 768 destroy: function () { 769 var element=this.opts.element, select2 = element.data("select2"); 770 771 if (this.propertyObserver) { delete this.propertyObserver; this.propertyObserver = null; } 772 773 if (select2 !== undefined) { 774 select2.container.remove(); 775 select2.dropdown.remove(); 776 element 777 .removeClass("select2-offscreen") 778 .removeData("select2") 779 .off(".select2") 780 .prop("autofocus", this.autofocus || false); 781 if (this.elementTabIndex) { 782 element.attr({tabindex: this.elementTabIndex}); 783 } else { 784 element.removeAttr("tabindex"); 785 } 786 element.show(); 787 } 788 }, 789 790 // abstract 791 optionToData: function(element) { 792 if (element.is("option")) { 793 return { 794 id:element.prop("value"), 795 text:element.text(), 796 element: element.get(), 797 css: element.attr("class"), 798 disabled: element.prop("disabled"), 799 locked: equal(element.attr("locked"), "locked") || equal(element.data("locked"), true) 800 }; 801 } else if (element.is("optgroup")) { 802 return { 803 text:element.attr("label"), 804 children:[], 805 element: element.get(), 806 css: element.attr("class") 807 }; 808 } 809 }, 810 811 // abstract 812 prepareOpts: function (opts) { 813 var element, select, idKey, ajaxUrl, self = this; 814 815 element = opts.element; 816 817 if (element.get(0).tagName.toLowerCase() === "select") { 818 this.select = select = opts.element; 819 } 820 821 if (select) { 822 // these options are not allowed when attached to a select because they are picked up off the element itself 823 $.each(["id", "multiple", "ajax", "query", "createSearchChoice", "initSelection", "data", "tags"], function () { 824 if (this in opts) { 825 throw new Error("Option '" + this + "' is not allowed for Select2 when attached to a <select> element."); 826 } 827 }); 828 } 829 830 opts = $.extend({}, { 831 populateResults: function(container, results, query
831) { 832 var populate, data, result, children, id=this.opts.id; 833 834 populate=function(results, container, depth) { 835 836 var i, l, result, selectable, disabled, compound, node, label, innerContainer, formatted; 837 838 results = opts.sortResults(results, container, query); 839 840 for (i = 0, l = results.length; i < l; i = i + 1) { 841 842 result=results[i]; 843 844 disabled = (result.disabled === true); 845 selectable = (!disabled) && (id(result) !== undefined); 846 847 compound=result.children && result.children.length > 0; 848 849 node=$("<li></li>"); 850 node.addClass("select2-results-dept-"+depth); 851 node.addClass("select2-result"); 852 node.addClass(selectable ? "select2-result-selectable" : "select2-result-unselectable"); 853 if (disabled) { node.addClass("select2-disabled"); } 854 if (compound) { node.addClass("select2-result-with-children"); } 855 node.addClass(self.opts.formatResultCssClass(result)); 856 857 label=$(document.createElement("div")); 858 label.addClass("select2-result-label"); 859 860 formatted=opts.formatResult(result, label, query, self.opts.escapeMarkup); 861 if (formatted!==undefined) { 862 label.html(formatted); 863 } 864 865 node.append(label); 866 867 if (compound) { 868 869 innerContainer=$("<ul></ul>"); 870 innerContainer.addClass("select2-result-sub"); 871 populate(result.children, innerContainer, depth+1); 872 node.append(innerContainer); 873 } 874 875 node.data("select2-data", result); 876 container.append(node); 877 } 878 }; 879 880 populate(results, container, 0); 881 } 882 }, $.fn.select2.defaults, opts); 883 884 if (typeof(opts.id) !== "function") { 885 idKey = opts.id; 886 opts.id = function (e) { return e[idKey]; }; 887 } 888 889 if ($.isArray(opts.element.data("select2Tags"))) { 890 if ("tags" in opts) { 891 throw "tags specified as both an attribute 'data-select2-tags' and in options of Select2 " + opts.element.attr("id"); 892 } 893 opts.tags=opts.element.data("select2Tags"); 894 } 895 896 if (select) { 897 opts.query = this.bind(function (query) { 898 var data = { results: [], more: false }, 899 term = query.term, 900 children, placeholderOption, process; 901 902 process=function(element, collection) { 903 var group; 904 if (element.is("option")) { 905 if (query.matcher(term, element.text(), element)) { 906 collection.push(self.optionToData(element)); 907 } 908 } else if (element.is("optgroup")) { 909 group=self.optionToData(element); 910 element.children().each2(function(i, elm) { process(elm, group.children); }); 911 if (group.children.length>0) { 912 collection.push(group); 913 } 914 } 915 }; 916 917 children=element.children(); 918 919 // ignore the placeholder option if there is one 920 if (this.getPlaceholder() !== undefined && children.length > 0) { 921 placeholderOption = this.getPlaceholderOption(); 922 if (placeholderOption) { 923 children=children.not(placeholderOption); 924 } 925 } 926 927 children.each2(function(i, elm) { process(elm, data.results); }); 928 929 query.callback(data); 930 }); 931 // this is needed because inside val() we construct choices from options and there id is hardcoded 932 opts.id=function(e) { return e.id; }; 933 opts.formatResultCssClass = function(data) { return data.css; }; 934 } else {
935 if (!("query" in opts)) { 936 937 if ("ajax" in opts) { 938 ajaxUrl = opts.element.data("ajax-url"); 939 if (ajaxUrl && ajaxUrl.length > 0) { 940 opts.ajax.url = ajaxUrl; 941 } 942 opts.query = ajax.call(opts.element, opts.ajax); 943 } else if ("data" in opts) { 944 opts.query = local(opts.data); 945 } else if ("tags" in opts) { 946 opts.query = tags(opts.tags); 947 if (opts.createSearchChoice === undefined) { 948 opts.createSearchChoice = function (term) { return {id: term, text: term}; }; 949 } 950 if (opts.initSelection === undefined) { 951 opts.initSelection = function (element, callback) { 952 var data = []; 953 $(splitVal(element.val(), opts.separator)).each(function () { 954 var id = this, text = this, tags=opts.tags; 955 if ($.isFunction(tags)) tags=tags(); 956 $(tags).each(function() { if (equal(this.id, id)) { text = this.text; return false; } }); 957 data.push({id: id, text: text}); 958 }); 959 960 callback(data); 961 }; 962 } 963 } 964 } 965 } 966 if (typeof(opts.query) !== "function") { 967 throw "query function not defined for Select2 " + opts.element.attr("id"); 968 } 969 970 return opts; 971 }, 972 973 /** 974 * Monitor the original element for changes and update select2 accordingly 975 */ 976 // abstract 977 monitorSource: function () { 978 var el = this.opts.element, sync; 979 980 el.on("change.select2", this.bind(function (e) { 981 if (this.opts.element.data("select2-change-triggered") !== true) { 982 this.initSelection(); 983 } 984 })); 985 986 sync = this.bind(function () { 987 988 var enabled, readonly, self = this; 989 990 // sync enabled state 991 var disabled = el.prop("disabled"); 992 if (disabled === undefined) disabled = false; 993 this.enable(!disabled); 994 995 var readonly = el.prop("readonly"); 996 if (readonly === undefined) readonly = false; 997 this.readonly(readonly); 998 999 syncCssClasses(this.container, this.opts.element, this.opts.adaptContainerCssClass); 1000 this.container.addClass(evaluate(this.opts.containerCssClass)); 1001 1002 syncCssClasses(this.dropdown, this.opts.element, this.opts.adaptDropdownCssClass); 1003 this.dropdown.addClass(evaluate(this.opts.dropdownCssClass)); 1004 1005 }); 1006 1007 // mozilla and IE 1008 el.on("propertychange.select2 DOMAttrModified.select2", sync); 1009 1010 1011 // hold onto a reference of the callback to work around a chromium bug 1012 if (this.mutationCallback === undefined) { 1013 this.mutationCallback = function (mutations) { 1014 mutations.forEach(sync); 1015 } 1016 } 1017 1018 // safari and chrome 1019 if (typeof WebKitMutationObserver !== "undefined") { 1020 if (this.propertyObserver) { delete this.propertyObserver; this.propertyObserver = null; } 1021 this.propertyObserver = new WebKitMutationObserver(this.mutationCallback); 1022 this.propertyObserver.observe(el.get(0), { attributes:true, subtree:false }); 1023 } 1024 }, 1025 1026 // abstract 1027 triggerSelect: function(data) { 1028 var evt = $.Event("select2-selecting", { val: this.id(data), object: data }); 1029 this.opts.element.trigger(evt); 1030 return !evt.isDefaultPrevented(); 1031 }, 1032 1033 /** 1034 * Triggers the change event on the source element 1035 */ 1036 // abstract 1037 triggerChange: function (details) { 1038 1039 details = details || {}; 1040 details= $.extend({}, details, { type: "change", val: this.val() }); 1041 // prevents recursive triggering 1042 this.opts.element.data("select2-change-triggered", true); 1043 this.opts.element.trigger(details); 1044 this.opts.element.data("select2-change-triggered", false); 1045 1046 // some validation frameworks ignore the change event and listen instead to keyup, click for selects 1047 // so here we trigger the click event manually 1048 this.opts.element.click(); 1049 1050 // ValidationEngine ignorea the change event and listens instead to blur 1051 // so here we trigger the blur event manually if so desired 1052 if (this.opts.blurOnChange) 1053 this.opts.element.blur(); 1054 }, 1055 1056 //abstract 1057 isInterfaceEnabled: function() 1058 { 1059 return this.enabledInterface === true; 1060 }, 1061 1062 // abstract 1063 enableInterface: function() { 1064 var enabled = this._enabled && !this._readonly, 1065 disabled = !enabled; 1066 1067 if (enabled === this.enabledInterface) return false; 1068 1069 this.container.toggleClass("select2-container-disabled", disabled); 1070 this.close(); 1071 this.enabledInterface = enabled; 1072 1073 return true; 1074 }, 1075 1076 // abstract 1077 enable: function(enabled) { 1078 if (enabled === undefined) enabled = true; 1079 if (this._enabled === enabled) return false; 1080 this._enabled = enabled; 1081 1082 this.opts.element.prop("disabled", !enabled); 1083 this.enableInterface(); 1084 return true; 1085 }, 1086 1087 // abstract 1088 readonly: function(enabled) { 1089 if (enabled === undefined) enabled = false; 1090 if (this._readonly === enabled) return false; 1091 this._readonly = enabled; 1092 1093 this.opts.element.prop("readonly", enabled); 1094 this.enableInterface(); 1095 return true; 1096 }, 1097 1098 // abstract 1099 opened: function () { 1100 return this.container.hasClass("select2-dropdown-open"); 1101 }, 1102 1103 // abstract 1104 positionDropdown: function() { 1105 var $dropdown = this.dropdown, 1106 offset = this.container.offset(), 1107 height = this.container.outerHeight(false), 1108 width = this.container.outerWidth(false), 1109 dropHeight = $dropdown.outerHeight(false), 1110 viewPortRight = $(window).scrollLeft() + $(window).width(), 1111 viewportBottom = $(window).scrollTop() + $(window).height(), 1112 dropTop = offset.top + height, 1113 dropLeft = offset.left, 1114 enoughRoomBelow = dropTop + dropHeight <= viewportBottom, 1115 enoughRoomAbove = (offset.top - dropHeight) >= this.body().scrollTop(), 1116 dropWidth = $dropdown.outerWidth(false), 1117 enoughRoomOnRight = dropLeft + dropWidth <= viewPortRight, 1118 aboveNow = $dropdown.hasClass("select2-drop-above"), 1119 bodyOffset, 1120 above, 1121 css, 1122 resultsListNode; 1123 1124 if (this.opts.dropdownAutoWidth) { 1125 resultsListNode = $('.select2-results', $dropdown)[0]; 1126 $dropdown.addClass('select2-drop-auto-width'); 1127 $dropdown.css('width', ''); 1128 // Add scrollbar width to dropdown if vertical scrollbar is present 1129 dropWidth = $dropdown.outerWidth(false) + (resultsListNode.scrollHeight === resultsListNode.clientHeight ? 0 : scrollBarDimensions.width); 1130 dropWidth > width ? width = dropWidth : dropWidth = width; 1131 enoughRoomOnRight = dropLeft + dropWidth <= viewPortRight; 1132 } 1133 else { 1134 this.container.removeClass('select2-drop-auto-width'); 1135 } 1136 1137 //console.log("below/ droptop:", dropTop, "dropHeight", dropHeight, "sum", (dropTop+dropHeight)+" viewport bottom", viewportBottom, "enough?", enoughRoomBelow); 1138 //console.log("above/ offset.top", offset.top, "dropHeight", dropHeight, "top", (offset.top-dropHeight), "scrollTop", this.body().scrollTop(), "enough?", enoughRoomAbove); 1139 1140 // fix positioning when body has an offset and is not position: static 1141 if (this.body().css('position') !== 'static') { 1142 bodyOffset = this.body().offset(); 1143 dropTop -= bodyOffset.top; 1144 dropLeft -= bodyOffset.left; 1145 } 1146 1147 // always prefer the current above/below alignment, unless there is not enough room 1148 if (aboveNow) { 1149 above = true; 1150 if (!enoughRoomAbove && enoughRoomBelow) above = false; 1151 } else { 1152 above = false; 1153 if (!enoughRoomBelow && enoughRoomAbove) above = true; 1154 } 1155 1156 if (!enoughRoomOnRight) { 1157 dropLeft = offset.left + width - dropWidth; 1158 } 1159 1160 if (above) { 1161 dropTop = offset.top - dropHeight; 1162 this.container.addClass("select2-drop-above"); 1163 $dropdown.addClass("select2-drop-above"); 1164 } 1165 else { 1166 this.container.removeClass("select2-drop-above"); 1167 $dropdown.removeClass("select2-drop-above"); 1168 } 1169 1170 css = $.extend({ 1171 top: dropTop, 1172 left: dropLeft, 1173 width: width 1174 }, evaluate(this.opts.dropdownCss)); 1175 1176 $dropdown.css(css); 1177 }, 1178 1179 // abstract 1180 shouldOpen: function() { 1181 var event; 1182 1183 if (this.opened()) return false; 1184 1185 if (this._enabled === false || this._readonly === true) return false; 1186 1187 event = $.Event("select2-opening"); 1188 this.opts.element.trigger(event); 1189 return !event.isDefaultPrevented(); 1190 }, 1191 1192 // abstract 1193 clearDropdownAlignmentPreference: function() { 1194 // clear the classes used to figure out the preference of where the dropdown should be opened 1195 this.container.removeClass("select2-drop-above"); 1196 this.dropdown.removeClass("select2-drop-above"); 1197 }, 1198 1199 /** 1200 * Opens the dropdown 1201 * 1202 * @return {Boolean} whether or not dropdown was opened. This method will return false if, for example, 1203 * the dropdown is already open, or if the 'open' event listener on the element called preventDefault(). 1204 */ 1205 // abstract 1206 open: function () { 1207 1208 if (!this.shouldOpen()) return false; 1209 1210 this.opening(); 1211 1212 return true; 1213 }, 1214 1215 /** 1216 * Performs the opening of the dropdown 1217 */ 1218 // abstract 1219 opening: function() { 1220 var cid = this.containerId, 1221 scroll = "scroll." + cid, 1222 resize = "resize."+cid, 1223 orient = "orientationchange."+cid, 1224 mask, maskCss; 1225 1226 this.container.addClass("select2-dropdown-open").addClass("select2-container-active"); 1227 1228 this.clearDropdownAlignmentPreference(); 1229 1230 if(this.dropdown[0] !== this.body().children().last(
1230)[0]) { 1231 this.dropdown.detach().appendTo(this.body()); 1232 } 1233 1234 // create the dropdown mask if doesnt already exist 1235 mask = $("#select2-drop-mask"); 1236 if (mask.length == 0) { 1237 mask = $(document.createElement("div")); 1238 mask.attr("id","select2-drop-mask").attr("class","select2-drop-mask"); 1239 mask.hide(); 1240 mask.appendTo(this.body()); 1241 mask.on("mousedown touchstart click", function (e) { 1242 var dropdown = $("#select2-drop"), self; 1243 if (dropdown.length > 0) { 1244 self=dropdown.data("select2"); 1245 if (self.opts.selectOnBlur) { 1246 self.selectHighlighted({noFocus: true}); 1247 } 1248 self.close(); 1249 e.preventDefault(); 1250 e.stopPropagation(); 1251 } 1252 }); 1253 } 1254 1255 // ensure the mask is always right before the dropdown 1256 if (this.dropdown.prev()[0] !== mask[0]) { 1257 this.dropdown.before(mask); 1258 } 1259 1260 // move the global id to the correct dropdown 1261 $("#select2-drop").removeAttr("id"); 1262 this.dropdown.attr("id", "select2-drop"); 1263 1264 // show the elements 1265 maskCss=_makeMaskCss(); 1266 1267 mask.css(maskCss).show(); 1268 1269 this.dropdown.show(); 1270 this.positionDropdown(); 1271 1272 this.dropdown.addClass("select2-drop-active"); 1273 1274 // attach listeners to events that can change the position of the container and thus require 1275 // the position of the dropdown to be updated as well so it does not come unglued from the container 1276 var that = this; 1277 this.container.parents().add(window).each(function () { 1278 $(this).on(resize+" "+scroll+" "+orient, function (e) { 1279 var maskCss=_makeMaskCss(); 1280 $("#select2-drop-mask").css(maskCss); 1281 that.positionDropdown(); 1282 }); 1283 }); 1284 1285 function _makeMaskCss() { 1286 return { 1287 width : Math.max(document.documentElement.scrollWidth, $(window).width()), 1288 height : Math.max(document.documentElement.scrollHeight, $(window).height()) 1289 } 1290 } 1291 }, 1292 1293 // abstract 1294 close: function () { 1295 if (!this.opened()) return; 1296 1297 var cid = this.containerId, 1298 scroll = "scroll." + cid, 1299 resize = "resize."+cid, 1300 orient = "orientationchange."+cid; 1301 1302 // unbind event listeners 1303 this.container.parents().add(window).each(function () { $(this).off(scroll).off(resize).off(orient); }); 1304 1305 this.clearDropdownAlignmentPreference(); 1306 1307 $("#select2-drop-mask").hide(); 1308 this.dropdown.removeAttr("id"); // only the active dropdown has the select2-drop id 1309 this.dropdown.hide(); 1310 this.container.removeClass("select2-dropdown-open"); 1311 this.results.empty(); 1312 1313 1314 this.clearSearch(); 1315 this.search.removeClass("select2-active"); 1316 this.opts.element.trigger($.Event("select2-close")); 1317 }, 1318 1319 /** 1320 * Opens control, sets input value, and updates results. 1321 */ 1322 // abstract 1323 externalSearch: function (term) { 1324 this.open(); 1325 this.search.val(term); 1326 this.updateResults(false); 1327 }, 1328 1329 // abstract 1330 clearSearch: function () { 1331 1332 }, 1333 1334 //abstract 1335 getMaximumSelectionSize: function() { 1336 return evaluate(this.opts.maximumSelectionSize); 1337 }, 1338 1339 // abstract 1340 ensureHighlightVisible: function () { 1341 var results = this.results, children, index, child, hb, rb, y, more; 1342 1343 index = this.highlight(); 1344 1345 if (index < 0) return; 1346 1347 if (index == 0) { 1348 1349 // if the first element is highlighted scroll all the way to the top, 1350 // that way any unselectable headers above it will also be scrolled 1351 // into view 1352 1353 results.scrollTop(0); 1354 return; 1355 } 1356 1357 children = this.findHighlightableChoices().find('.select2-result-label'); 1358 1359 child = $(children[index]); 1360 1361 hb = child.offset().top + child.outerHeight(true); 1362 1363 // if this is the last child lets also make sure select2-more-results is visible 1364 if (index === children.length - 1) { 1365 more = results.find("li.select2-more-results"); 1366 if (more.length > 0) { 1367 hb = more.offset().top + more.outerHeight(true); 1368 } 1369 } 1370 1371 rb = results.offset().top + results.outerHeight(true); 1372 if (hb > rb) { 1373 results.scrollTop(results.scrollTop() + (hb - rb)); 1374 } 1375 y = child.offset().top - results.offset().top; 1376 1377 // make sure the top of the element is visible 1378 if (y < 0 && child.css('display') != 'none' ) { 1379 results.scrollTop(results.scrollTop() + y); // y is negative 1380 } 1381 }, 1382 1383 // abstract 1384 findHighlightableChoices: function() { 1385 return this.results.find(".select2-result-selectable:not(.select2-selected):not(.select2-disabled)"); 1386 }, 1387 1388 // abstract 1389 moveHighlight: function (delta) { 1390 var choices = this.findHighlightableChoices(), 1391 index = this.highlight(); 1392 1393 while (index > -1 && index < choices.length) { 1394 index += delta; 1395 var choice = $(choices[index]); 1396 if (choice.hasClass("select2-result-selectable") && !c
1396hoice.hasClass("select2-disabled") && !choice.hasClass("select2-selected")) { 1397 this.highlight(index); 1398 break; 1399 } 1400 } 1401 }, 1402 1403 // abstract 1404 highlight: function (index) { 1405 var choices = this.findHighlightableChoices(), 1406 choice, 1407 data; 1408 1409 if (arguments.length === 0) { 1410 return indexOf(choices.filter(".select2-highlighted")[0], choices.get()); 1411 } 1412 1413 if (index >= choices.length) index = choices.length - 1; 1414 if (index < 0) index = 0; 1415 1416 this.results.find(".select2-highlighted").removeClass("select2-highlighted"); 1417 1418 choice = $(choices[index]); 1419 choice.addClass("select2-highlighted"); 1420 1421 this.ensureHighlightVisible(); 1422 1423 data = choice.data("select2-data"); 1424 if (data) { 1425 this.opts.element.trigger({ type: "select2-highlight", val: this.id(data), choice: data }); 1426 } 1427 }, 1428 1429 // abstract 1430 countSelectableResults: function() { 1431 return this.findHighlightableChoices().length; 1432 }, 1433 1434 // abstract 1435 highlightUnderEvent: function (event) { 1436 var el = $(event.target).closest(".select2-result-selectable"); 1437 if (el.length > 0 && !el.is(".select2-highlighted")) { 1438 var choices = this.findHighlightableChoices(); 1439 this.highlight(choices.index(el)); 1440 } else if (el.length == 0) { 1441 // if we are over an unselectable item remove al highlights 1442 this.results.find(".select2-highlighted").removeClass("select2-highlighted"); 1443 } 1444 }, 1445 1446 // abstract 1447 loadMoreIfNeeded: function () { 1448 var results = this.results, 1449 more = results.find("li.select2-more-results"), 1450 below, // pixels the element is below the scroll fold, below==0 is when the element is starting to be visible 1451 offset = -1, // index of first element without data 1452 page = this.resultsPage + 1, 1453 self=this, 1454 term=this.search.val(), 1455 context=this.context; 1456 1457 if (more.length === 0) return; 1458 below = more.offset().top - results.offset().top - results.height(); 1459 1460 if (below <= this.opts.loadMorePadding) { 1461 more.addClass("select2-active"); 1462 this.opts.query({ 1463 element: this.opts.element, 1464 term: term, 1465 page: page, 1466 context: context, 1467 matcher: this.opts.matcher, 1468 callback: this.bind(function (data) { 1469 1470 // ignore a response if the select2 has been closed before it was received 1471 if (!self.opened()) return; 1472 1473 1474 self.opts.populateResults.call(this, results, data.results, {term: term, page: page, context:context}); 1475 self.postprocessResults(data, false, false); 1476 1477 if (data.more===true) { 1478 more.detach().appendTo(results).text(self.opts.formatLoadMore(page+1)); 1479 window.setTimeout(function() { self.loadMoreIfNeeded(); }, 10); 1480 } else { 1481 more.remove(); 1482 } 1483 self.positionDropdown(); 1484 self.resultsPage = page; 1485 self.context = data.context; 1486 })}); 1487 } 1488 }, 1489 1490 /** 1491 * Default tokenizer function which does nothing 1492 */ 1493 tokenize: function() { 1494 1495 }, 1496 1497 /** 1498 * @param initial whether or not this is the call to this method right after the dropdown has been opened 1499 */ 1500 // abstract 1501 updateResults: function (initial) { 1502 var search = this.search, 1503 results = this.results, 1504 opts = this.opts, 1505 data, 1506 self = this, 1507 input, 1508 term = search.val(), 1509 lastTerm=$.data(this.container, "select2-last-term"); 1510 1511 // prevent duplicate queries against the same term 1512 if (initial !== true && lastTerm && equal(term, lastTerm)) return; 1513 1514 $.data(this.container, "select2-last-term", term); 1515 1516 // if the search is currently hidden we do not alter the results 1517 if (initial !== true && (this.showSearchInput === false || !this.opened())) { 1518 return; 1519 } 1520 1521 function postRender() { 1522 search.removeClass("select2-active"); 1523 self.positionDropdown(); 1524 } 1525 1526 function render(html) { 1527 results.html(html); 1528 postRender(); 1529 } 1530 1531 var maxSelSize = this.getMaximumSelectionSize(); 1532 if (maxSelSize >=1) { 1533 data = this.data(); 1534 if ($.isArray(data) && data.length >= maxSelSize && checkFormatter(opts.formatSelectionTooBig, "formatSelectionTooBig")) {
1535 render("<li class='select2-selection-limit'>" + opts.formatSelectionTooBig(maxSelSize) + "</li>"); 1536 return; 1537 } 1538 } 1539 1540 if (search.val().length < opts.minimumInputLength) { 1541 if (checkFormatter(opts.formatInputTooShort, "formatInputTooShort")) { 1542 render("<li class='select2-no-results'>" + opts.formatInputTooShort(search.val(), opts.minimumInputLength) + "</li>"); 1543 } else { 1544 render(""); 1545 } 1546 if (initial && this.showSearch) this.showSearch(true); 1547 return; 1548 } 1549 1550 if (opts.maximumInputLength && search.val().length > opts.maximumInputLength) { 1551 if (checkFormatter(opts.formatInputTooLong, "formatInputTooLong")) { 1552 render("<li class='select2-no-results'>" + opts.formatInputTooLong(search.val(), opts.maximumInputLength) + "</li>"); 1553 } else { 1554 render(""); 1555 } 1556 return; 1557 } 1558 1559 if (opts.formatSearching && this.findHighlightableChoices().length === 0) { 1560 render("<li class='select2-searching'>" + opts.formatSearching() + "</li>"); 1561 } 1562 1563 search.addClass("select2-active"); 1564 1565 // give the tokenizer a chance to pre-process the input 1566 input = this.tokenize(); 1567 if (input != undefined && input != null) { 1568 search.val(input); 1569 } 1570 1571 this.resultsPage = 1; 1572 1573 opts.query({ 1574 element: opts.element, 1575 term: search.val(), 1576 page: this.resultsPage, 1577 context: null, 1578 matcher: opts.matcher, 1579 callback: this.bind(function (data) { 1580 var def; // default choice 1581 1582 // ignore a response if the select2 has been closed before it was received 1583 if (!this.opened()) { 1584 this.search.removeClass("select2-active"); 1585 return; 1586 } 1587 1588 // save context, if any 1589 this.context = (data.context===undefined) ? null : data.context; 1590 // create a default choice and prepend it to the list 1591 if (this.opts.createSearchChoice && search.val() !== "") { 1592 def = this.opts.createSearchChoice.call(self, search.val(), data.results); 1593 if (def !== undefined && def !== null && self.id(def) !== undefined && self.id(def) !== null) { 1594 if ($(data.results).filter( 1595 function () { 1596 return equal(self.id(this), self.id(def)); 1597 }).length === 0) { 1598 data.results.unshift(def); 1599 } 1600 } 1601 } 1602 1603 if (data.results.length === 0 && checkFormatter(opts.formatNoMatches, "formatNoMatches")) { 1604 render("<li class='select2-no-results'>" + opts.formatNoMatches(search.val()) + "</li>"); 1605 return; 1606 } 1607 1608 results.empty(); 1609 self.opts.populateResults.call(this, results, data.results, {term: search.val(), page: this.resultsPage, context:null}); 1610 1611 if (data.more === true && checkFormatter(opts.formatLoadMore, "formatLoadMore")) { 1612 results.append("<li class='select2-more-results'>" + self.opts.escapeMarkup(opts.formatLoadMore(this.resultsPage)) + "</li>"); 1613 window.setTimeout(function() { self.loadMoreIfNeeded(); }, 10); 1614 } 1615 1616 this.postprocessResults(data, initial); 1617 1618 postRender(); 1619 1620 this.opts.element.trigger({ type: "select2-loaded", items: data }); 1621 })}); 1622 }, 1623 1624 // abstract 1625 cancel: function () { 1626 this.close(); 1627 }, 1628 1629 // abstract 1630 blur: function () { 1631 // if selectOnBlur == true, select the currently highlighted option 1632 if (this.opts.selectOnBlur) 1633 this.selectHighlighted({noFocus: true}); 1634 1635 this.close(); 1636 this.container.removeClass("select2-container-active"); 1637 // synonymous to .is(':focus'), which is available in jquery >= 1.6 1638 if (this.search[0] === document.activeElement) { this.search.blur(); } 1639 this.clearSearch(); 1640 this.selection.find(".select2-search-choice-focus").removeClass("select2-search-choice-focus"); 1641 }, 1642 1643 // abstract 1644 focusSearch: function () { 1645 focus(this.search); 1646 }, 1647 1648 // abstract 1649 selectHighlighted: function (options) { 1650 var index=this.highlight(), 1651 highlighted=this.results.find(".select2-highlighted"), 1652 data = highlighted.closest('.select2-result').data("select2-data"); 1653 1654 if (data) { 1655 this.highlight(index); 1656 this.onSelect(data, options); 1657 } else if (options && options.noFocus) { 1658 this.close(); 1659 } 1660 }, 1661 1662 // abstract 1663 getPlaceholder: function () { 1664 var placeholderOption; 1665 return this.opts.element.attr("placeholder") || 1666 this.opts.element.attr("data-placeholder") || // jquery 1.4 compat 1667 this.opts.element.data("placeholder") || 1668 this.opts.placeholder || 1669 ((placeholderOption = this.getPlaceholderOption()) !== undefined ? placeholderOption.text() : undefined); 1670 }, 1671 1672 // abstract 1673 getPlaceholderOption: function() { 1674 if (this.select) { 1675 var firstOption = this.select.children().first(); 1676 if (this.opts.placeholderOption !== undefined ) { 1677 //Determine the placeholder option based on the specified placeholderOption setting 1678 return (this.opts.placeholderOption === "first" && firstOption) || 1679 (typeof this.opts.placeholderOption === "function" && this.opts.placeholderOption(this.select)); 1680 } else if (firstOption.text() === "" && firstOption.val() === "") { 1681 //No explicit placeholder option specified, use the first if it's blank 1682 return firstOption; 1683 } 1684 } 1685 }, 1686 1687 /** 1688 * Get the desired width for the container element. This is 1689 * derived first from option `width` passed to select2, then 1690 * the inline 'style' on the original element, and finally 1691 * falls back to the jQuery calculated element width. 1692 */ 1693 // abstract 1694 initContainerWidth: function () {
1695 function resolveContainerWidth() { 1696 var style, attrs, matches, i, l; 1697 1698 if (this.opts.width === "off") { 1699 return null; 1700 } else if (this.opts.width === "element"){ 1701 return this.opts.element.outerWidth(false) === 0 ? 'auto' : this.opts.element.outerWidth(false) + 'px'; 1702 } else if (this.opts.width === "copy" || this.opts.width === "resolve") { 1703 // check if there is inline style on the element that contains width 1704 style = this.opts.element.attr('style'); 1705 if (style !== undefined) { 1706 attrs = style.split(';'); 1707 for (i = 0, l = attrs.length; i < l; i = i + 1) { 1708 matches = attrs[i].replace(/\s/g, '') 1709 .match(/width:(([-+]?([0-9]*\.)?[0-9]+)(px|em|ex|%|in|cm|mm|pt|pc))/i); 1710 if (matches !== null && matches.length >= 1) 1711 return matches[1]; 1712 } 1713 } 1714 1715 if (this.opts.width === "resolve") { 1716 // next check if css('width') can resolve a width that is percent based, this is sometimes possible 1717 // when attached to input type=hidden or elements hidden via css 1718 style = this.opts.element.css('width'); 1719 if (style.indexOf("%") > 0) return style; 1720 1721 // finally, fallback on the calculated width of the element 1722 return (this.opts.element.outerWidth(false) === 0 ? 'auto' : this.opts.element.outerWidth(false) + 'px'); 1723 } 1724 1725 return null; 1726 } else if ($.isFunction(this.opts.width)) { 1727 return this.opts.width(); 1728 } else { 1729 return this.opts.width; 1730 } 1731 }; 1732 1733 var width = resolveContainerWidth.call(this); 1734 if (width !== null) { 1735 this.container.css("width", width); 1736 } 1737 } 1738 }); 1739 1740 SingleSelect2 = clazz(AbstractSelect2, { 1741 1742 // single 1743 1744 createContainer: function () { 1745 var container = $(document.createElement("div")).attr({ 1746 "class": "select2-container" 1747 }).html([ 1748 "<a href='javascript:void(0)' onclick='return false;' class='select2-choice' tabindex='-1'>", 1749 " <span class='select2-chosen'> </span><abbr class='select2-search-choice-close'></abbr>", 1750 " <span class='select2-arrow'><b></b></span>", 1751 "</a>", 1752 "<input class='select2-focusser select2-offscreen' type='text'/>", 1753 "<div class='select2-drop select2-display-none'>", 1754 " <div class='select2-search'>", 1755 " <input type='text' autocomplete='off' autocorrect='off' autocapitalize='off' spellcheck='false' class='select2-input'/>", 1756 " </div>", 1757 " <ul class='select2-results'>", 1758 " </ul>", 1759 "</div>"].join("")); 1760 return container; 1761 }, 1762 1763 // single 1764 enableInterface: function() { 1765 if (this.parent.enableInterface.apply(this, arguments)) { 1766 this.focusser.prop("disabled", !this.isInterfaceEnabled()); 1767 } 1768 }, 1769 1770 // single 1771 opening: function () { 1772 var el, range, len; 1773 1774 if (this.opts.minimumResultsForSearch >= 0) { 1775 this.showSearch(true); 1776 } 1777 1778 this.parent.opening.apply(this, arguments); 1779 1780 if (this.showSearchInput !== false) { 1781 // IE appends focusser.val() at the end of field :/ so we manually insert it at the beginning using a range 1782 // all other browsers handle this just fine 1783 1784 this.search.val(this.focusser.val()); 1785 } 1786 this.search.focus(); 1787 // move the cursor to the end after focussing, otherwise it will be at the beginning and 1788 // new text will appear *before* focusser.val() 1789 el = this.search.get(0); 1790 if (el.createTextRange) { 1791 range = el.createTextRange(); 1792 range.collapse(false); 1793 range.select(); 1794 } else if (el.setSelectionRange) { 1795 len = this.search.val().length; 1796 el.setSelectionRange(len, len); 1797 } 1798 1799 this.focusser.prop("disabled", true).val(""); 1800 this.updateResults(true); 1801 this.opts.element.trigger($.Event("select2-open")); 1802 }, 1803 1804 // single 1805 close: function () { 1806 if (!this.opened()) return; 1807 this.parent.close.apply(this, arguments); 1808 this.focusser.removeAttr("disabled"); 1809 this.focusser.focus(); 1810 }, 1811 1812 // single 1813 focus: function () { 1814 if (this.opened()) { 1815 this.close(); 1816 } else { 1817 this.focusser.removeAttr("disabled"); 1818 this.focusser.focus(); 1819 } 1820 }, 1821 1822 // single 1823 isFocused: function () { 1824 return this.container.hasClass("select2-container-active"); 1825 }, 1826 1827 // single 1828 cancel: function () { 1829 this.parent.cancel.apply(this, arguments); 1830 this.focusser.removeAttr("disabled"); 1831 this.focusser.focus(); 1832 }, 1833 1834 // single 1835 initContainer: function () { 1836 1837 var selection, 1838 container = this.container, 1839 dropdown = this.dropdown; 1840 1841 if (this.opts.minimumResultsForSearch < 0) { 1842 this.showSearch(false); 1843 } else { 1844 this.showSearch(true); 1845 } 1846 1847 this.selection = selection = container.find(".select2-choice"); 1848 1849 this.focusser = container.find(".select2-focusser"); 1850 1851 // rewrite labels from original element to focusser 1852 this.focusser.attr("id", "s2id_autogen"+nextUid()); 1853 1854 $("label[for='" + this.opts.element.attr("id") + "']") 1855 .attr('for', this.focusser.attr('id')); 1856 1857 this.focusser.attr("tabindex", this.elementTabIndex); 1858 1859 this.search.on("keydown", this.bind(function (e) { 1860 if (!this.isInterfaceEnabled()) return; 1861 1862 if (e.which === KEY.PAGE_UP || e.which === KEY.PAGE_DOWN) { 1863 // prevent the page from scrolling 1864 killEvent(e); 1865 return; 1866 } 1867 1868 switch (e.which) { 1869 case KEY.UP: 1870 case KEY.DOWN: 1871 this.moveHighlight((e.which === KEY.UP) ? -1 : 1); 1872 killEvent(e); 1873 return; 1874 case KEY.ENTER: 1875 this.selectHighlighted(); 1876 killEvent(e); 1877 return; 1878 case KEY.TAB: 1879 this.selectHighlighted({noFocus: true}); 1880 return; 1881 case KEY.ESC: 1882 this.cancel(e); 1883 killEvent(e); 1884 return; 1885 } 1886 })); 1887 1888 this.search.on("blur", this.bind(function(e) {
1889 // a workaround for chrome to keep the search field focussed when the scroll bar is used to scroll the dropdown. 1890 // without this the search field loses focus which is annoying 1891 if (document.activeElement === this.body().get(0)) { 1892 window.setTimeout(this.bind(function() { 1893 this.search.focus(); 1894 }), 0); 1895 } 1896 })); 1897 1898 this.focusser.on("keydown", this.bind(function (e) { 1899 if (!this.isInterfaceEnabled()) return; 1900 1901 if (e.which === KEY.TAB || KEY.isControl(e) || KEY.isFunctionKey(e) || e.which === KEY.ESC) { 1902 return; 1903 } 1904 1905 if (this.opts.openOnEnter === false && e.which === KEY.ENTER) { 1906 killEvent(e); 1907 return; 1908 } 1909 1910 if (e.which == KEY.DOWN || e.which == KEY.UP 1911 || (e.which == KEY.ENTER && this.opts.openOnEnter)) { 1912 1913 if (e.altKey || e.ctrlKey || e.shiftKey || e.metaKey) return; 1914 1915 this.open(); 1916 killEvent(e); 1917 return; 1918 } 1919 1920 if (e.which == KEY.DELETE || e.which == KEY.BACKSPACE) { 1921 if (this.opts.allowClear) { 1922 this.clear(); 1923 } 1924 killEvent(e); 1925 return; 1926 } 1927 })); 1928 1929 1930 installKeyUpChangeEvent(this.focusser); 1931 this.focusser.on("keyup-change input", this.bind(function(e) { 1932 if (this.opts.minimumResultsForSearch >= 0) { 1933 e.stopPropagation(); 1934 if (this.opened()) return; 1935 this.open(); 1936 } 1937 })); 1938 1939 selection.on("mousedown", "abbr", this.bind(function (e) { 1940 if (!this.isInterfaceEnabled()) return; 1941 this.clear(); 1942 killEventImmediately(e); 1943 this.close(); 1944 this.selection.focus(); 1945 })); 1946 1947 selection.on("mousedown", this.bind(function (e) { 1948 1949 if (!this.container.hasClass("select2-container-active")) { 1950 this.opts.element.trigger($.Event("select2-focus")); 1951 } 1952 1953 if (this.opened()) { 1954 this.close(); 1955 } else if (this.isInterfaceEnabled()) { 1956 this.open(); 1957 } 1958 1959 killEvent(e); 1960 })); 1961 1962 dropdown.on("mousedown", this.bind(function() { this.search.focus(); })); 1963 1964 selection.on("focus", this.bind(function(e) { 1965 killEvent(e); 1966 })); 1967 1968 this.focusser.on("focus", this.bind(function(){ 1969 if (!this.container.hasClass("select2-container-active")) { 1970 this.opts.element.trigger($.Event("select2-focus")); 1971 } 1972 this.container.addClass("select2-container-active"); 1973 })).on("blur", this.bind(function() { 1974 if (!this.opened()) { 1975 this.container.removeClass("select2-container-active"); 1976 this.opts.element.trigger($.Event("select2-blur")); 1977 } 1978 })); 1979 this.search.on("focus", this.bind(function(){ 1980 if (!this.container.hasClass("select2-container-active")) { 1981 this.opts.element.trigger($.Event("select2-focus")); 1982 } 1983 this.container.addClass("select2-container-active"); 1984 })); 1985 1986 this.initContainerWidth(); 1987 this.opts.element.addClass("select2-offscreen"); 1988 this.setPlaceholder(); 1989 1990 }, 1991 1992 // single 1993 clear: function(triggerChange) { 1994 var data=this.selection.data("select2-data"); 1995 if (data) { // guard against queued quick consecutive clicks 1996 var placeholderOption = this.getPlaceholderOption(); 1997 this.opts.element.val(placeholderOption ? placeholderOption.val() : ""); 1998 this.selection.find(".select2-chosen").empty(); 1999 this.selection.removeData("select2-data"); 2000 this.setPlaceholder(); 2001 2002 if (triggerChange !== false){ 2003 this.opts.element.trigger({ type: "select2-removed", val: this.id(data), choice: data }); 2004 this.triggerChange({removed:data}); 2005 } 2006 } 2007 }, 2008 2009 /** 2010 * Sets selection based on source element's value 2011 */ 2012 // single 2013 initSelection: function () { 2014 var selected; 2015 if (this.isPlaceholderOptionSelected()) { 2016 this.updateSelection([]); 2017 this.close(); 2018 this.setPlaceholder(); 2019 } else { 2020 var self = this; 2021 this.opts.initSelection.call(null, this.opts.element, function(selected){ 2022 if (selected !== undefined && selected !== null) { 2023 self.updateSelection(selected); 2024 self.close(); 2025 self.setPlaceholder(); 2026 } 2027 }); 2028 } 2029 }, 2030 2031 isPlaceholderOptionSelected: function() { 2032 var placeholderOption; 2033 return ((placeholderOption = this.getPlaceholderOption()) !== undefined && placeholderOption.is(':selected')) || 2034 (this.opts.element.val() === "") || 2035 (this.opts.element.val() === undefined) || 2036 (this.opts.element.val() === null); 2037 }, 2038 2039 // single 2040 prepareOpts: function () { 2041 var opts = this.parent.prepareOpts.apply(this, arguments), 2042 self=this; 2043 2044 if (opts.element.get(0).tagName.toLowerCase() === "select") { 2045 // install the selection initializer 2046 opts.initSelection = function (element, callback) { 2047 var selected = element.find(":selected"); 2048 // a single select box always has a value, no need to null check 'selected' 2049 callback(self.optionToData(selected)); 2050 }; 2051 } else if ("data" in opts) { 2052 // install default initSelection when applied to hidden input and data is local 2053 opts.initSelection = opts.initSelection || function (element, callback) { 2054 var id = element.val(); 2055 //search in data by id, storing the actual matching item 2056 var match = null; 2057 opts.query({ 2058 matcher: function(term, text, el){ 2059 var is_match = equal(id, opts.id(el)); 2060 if (is_match) { 2061 match = el; 2062 } 2063 return is_match; 2064 },
2065 callback: !$.isFunction(callback) ? $.noop : function() { 2066 callback(match); 2067 } 2068 }); 2069 }; 2070 } 2071 2072 return opts; 2073 }, 2074 2075 // single 2076 getPlaceholder: function() { 2077 // if a placeholder is specified on a single select without a valid placeholder option ignore it 2078 if (this.select) { 2079 if (this.getPlaceholderOption() === undefined) { 2080 return undefined; 2081 } 2082 } 2083 2084 return this.parent.getPlaceholder.apply(this, arguments); 2085 }, 2086 2087 // single 2088 setPlaceholder: function () { 2089 var placeholder = this.getPlaceholder(); 2090 2091 if (this.isPlaceholderOptionSelected() && placeholder !== undefined) { 2092 2093 // check for a placeholder option if attached to a select 2094 if (this.select && this.getPlaceholderOption() === undefined) return; 2095 2096 this.selection.find(".select2-chosen").html(this.opts.escapeMarkup(placeholder)); 2097 2098 this.selection.addClass("select2-default"); 2099 2100 this.container.removeClass("select2-allowclear"); 2101 } 2102 }, 2103 2104 // single 2105 postprocessResults: function (data, initial, noHighlightUpdate) { 2106 var selected = 0, self = this, showSearchInput = true; 2107 2108 // find the selected element in the result list 2109 2110 this.findHighlightableChoices().each2(function (i, elm) { 2111 if (equal(self.id(elm.data("select2-data")), self.opts.element.val())) { 2112 selected = i; 2113 return false; 2114 } 2115 }); 2116 2117 // and highlight it 2118 if (noHighlightUpdate !== false) { 2119 if (initial === true && selected >= 0) { 2120 this.highlight(selected); 2121 } else { 2122 this.highlight(0); 2123 } 2124 } 2125 2126 // hide the search box if this is the first we got the results and there are enough of them for search 2127 2128 if (initial === true) { 2129 var min = this.opts.minimumResultsForSearch; 2130 if (min >= 0) { 2131 this.showSearch(countResults(data.results) >= min); 2132 } 2133 } 2134 }, 2135 2136 // single 2137 showSearch: function(showSearchInput) { 2138 if (this.showSearchInput === showSearchInput) return; 2139 2140 this.showSearchInput = showSearchInput; 2141 2142 this.dropdown.find(".select2-search").toggleClass("select2-search-hidden", !showSearchInput); 2143 this.dropdown.find(".select2-search").toggleClass("select2-offscreen", !showSearchInput); 2144 //add "select2-with-searchbox" to the container if search box is shown 2145 $(this.dropdown, this.container).toggleClass("select2-with-searchbox", showSearchInput); 2146 }, 2147 2148 // single 2149 onSelect: function (data, options) { 2150 2151 if (!this.triggerSelect(data)) { return; } 2152 2153 var old = this.opts.element.val(), 2154 oldData = this.data(); 2155 2156 this.opts.element.val(this.id(data)); 2157 this.updateSelection(data); 2158 2159 this.opts.element.trigger({ type: "select2-selected", val: this.id(data), choice: data }); 2160 2161 this.close(); 2162 2163 if (!options || !options.noFocus) 2164 this.selection.focus(); 2165 2166 if (!equal(old, this.id(data))) { this.triggerChange({added:data,removed:oldData}); } 2167 }, 2168 2169 // single 2170 updateSelection: function (data) { 2171 2172 var container=this.selection.find(".select2-chosen"), formatted, cssClass; 2173 2174 this.selection.data("select2-data", data); 2175 2176 container.empty(); 2177 formatted=this.opts.formatSelection(data, container, this.opts.escapeMarkup); 2178 if (formatted !== undefined) { 2179 container.append(formatted); 2180 } 2181 cssClass=this.opts.formatSelectionCssClass(data, container); 2182 if (cssClass !== undefined) { 2183 container.addClass(cssClass); 2184 } 2185 2186 this.selection.removeClass("select2-default"); 2187 2188 if (this.opts.allowClear && this.getPlaceholder() !== undefined) { 2189 this.container.addClass("select2-allowclear"); 2190 } 2191 }, 2192 2193 // single 2194 val: function () { 2195 var val, 2196 triggerChange = false, 2197 data = null, 2198 self = this, 2199 oldData = this.data(); 2200 2201 if (arguments.length === 0) { 2202 return this.opts.element.val(); 2203 } 2204 2205 val = arguments[0]; 2206 2207 if (arguments.length > 1) { 2208 triggerChange = arguments[1]; 2209 } 2210 2211 if (this.select) { 2212 this.select 2213 .val(val) 2214 .find(":selected").each2(function (i, elm) { 2215 data = self.optionToData(elm); 2216 return false; 2217 }); 2218 this.updateSelection(data); 2219 this.setPlaceholder(); 2220 if (triggerChange) { 2221 this.triggerChange({added: data, removed:oldData}
2221); 2222 } 2223 } else { 2224 // val is an id. !val is true for [undefined,null,'',0] - 0 is legal 2225 if (!val && val !== 0) { 2226 this.clear(triggerChange); 2227 return; 2228 } 2229 if (this.opts.initSelection === undefined) { 2230 throw new Error("cannot call val() if initSelection() is not defined"); 2231 } 2232 this.opts.element.val(val); 2233 this.opts.initSelection(this.opts.element, function(data){ 2234 self.opts.element.val(!data ? "" : self.id(data)); 2235 self.updateSelection(data); 2236 self.setPlaceholder(); 2237 if (triggerChange) { 2238 self.triggerChange({added: data, removed:oldData}); 2239 } 2240 }); 2241 } 2242 }, 2243 2244 // single 2245 clearSearch: function () { 2246 this.search.val(""); 2247 this.focusser.val(""); 2248 }, 2249 2250 // single 2251 data: function(value, triggerChange) { 2252 var data; 2253 2254 if (arguments.length === 0) { 2255 data = this.selection.data("select2-data"); 2256 if (data == undefined) data = null; 2257 return data; 2258 } else { 2259 if (!value || value === "") { 2260 this.clear(triggerChange); 2261 } else { 2262 data = this.data(); 2263 this.opts.element.val(!value ? "" : this.id(value)); 2264 this.updateSelection(value); 2265 if (triggerChange) { 2266 this.triggerChange({added: value, removed:data}); 2267 } 2268 } 2269 } 2270 } 2271 }); 2272 2273 MultiSelect2 = clazz(AbstractSelect2, { 2274 2275 // multi 2276 createContainer: function () { 2277 var container = $(document.createElement("div")).attr({ 2278 "class": "select2-container select2-container-multi" 2279 }).html([ 2280 "<ul class='select2-choices'>", 2281 " <li class='select2-search-field'>", 2282 " <input type='text' autocomplete='off' autocorrect='off' autocapitilize='off' spellcheck='false' class='select2-input'>", 2283 " </li>", 2284 "</ul>", 2285 "<div class='select2-drop select2-drop-multi select2-display-none'>", 2286 " <ul class='select2-results'>", 2287 " </ul>", 2288 "</div>"].join("")); 2289 return container; 2290 }, 2291 2292 // multi 2293 prepareOpts: function () { 2294 var opts = this.parent.prepareOpts.apply(this, arguments), 2295 self=this; 2296 2297 // TODO validate placeholder is a string if specified 2298 2299 if (opts.element.get(0).tagName.toLowerCase() === "select") { 2300 // install sthe selection initializer 2301 opts.initSelection = function (element, callback) { 2302 2303 var data = []; 2304 2305 element.find(":selected").each2(function (i, elm) { 2306 data.push(self.optionToData(elm)); 2307 }); 2308 callback(data); 2309 }; 2310 } else if ("data" in opts) { 2311 // install default initSelection when applied to hidden input and data is local 2312 opts.initSelection = opts.initSelection || function (element, callback) { 2313 var ids = splitVal(element.val(), opts.separator); 2314 //search in data by array of ids, storing matching items in a list 2315 var matches = []; 2316 opts.query({ 2317 matcher: function(term, text, el){ 2318 var is_match = $.grep(ids, function(id) { 2319 return equal(id, opts.id(el)); 2320 }).length; 2321 if (is_match) { 2322 matches.push(el); 2323 } 2324 return is_match; 2325 },
2326 callback: !$.isFunction(callback) ? $.noop : function() { 2327 // reorder matches based on the order they appear in the ids array because right now 2328 // they are in the order in which they appear in data array 2329 var ordered = []; 2330 for (var i = 0; i < ids.length; i++) { 2331 var id = ids[i]; 2332 for (var j = 0; j < matches.length; j++) { 2333 var match = matches[j]; 2334 if (equal(id, opts.id(match))) { 2335 ordered.push(match); 2336 matches.splice(j, 1); 2337 break; 2338 } 2339 } 2340 } 2341 callback(ordered); 2342 } 2343 }); 2344 }; 2345 } 2346 2347 return opts; 2348 }, 2349 2350 selectChoice: function (choice) { 2351 2352 var selected = this.container.find(".select2-search-choice-focus"); 2353 if (selected.length && choice && choice[0] == selected[0]) { 2354 2355 } else { 2356 if (selected.length) { 2357 this.opts.element.trigger("choice-deselected", selected); 2358 } 2359 selected.removeClass("select2-search-choice-focus"); 2360 if (choice && choice.length) { 2361 this.close(); 2362 choice.addClass("select2-search-choice-focus"); 2363 this.opts.element.trigger("choice-selected", choice); 2364 } 2365 } 2366 }, 2367 2368 // multi 2369 initContainer: function () { 2370 2371 var selector = ".select2-choices", selection; 2372 2373 this.searchContainer = this.container.find(".select2-search-field"); 2374 this.selection = selection = this.container.find(selector); 2375 2376 var _this = this; 2377 this.selection.on("mousedown", ".select2-search-choice", function (e) { 2378 //killEvent(e); 2379 _this.search[0].focus(); 2380 _this.selectChoice($(this)); 2381 }) 2382 2383 // rewrite labels from original element to focusser 2384 this.search.attr("id", "s2id_autogen"+nextUid()); 2385 $("label[for='" + this.opts.element.attr("id") + "']") 2386 .attr('for', this.search.attr('id')); 2387 2388 this.search.on("input paste", this.bind(function() { 2389 if (!this.isInterfaceEnabled()) return; 2390 if (!this.opened()) { 2391 this.open(); 2392 } 2393 })); 2394 2395 this.search.attr("tabindex", this.elementTabIndex); 2396 2397 this.keydowns = 0; 2398 this.search.on("keydown", this.bind(function (e) { 2399 if (!this.isInterfaceEnabled()) return; 2400 2401 ++this.keydowns; 2402 var selected = selection.find(".select2-search-choice-focus"); 2403 var prev = selected.prev(".select2-search-choice:not(.select2-locked)"); 2404 var next = selected.next(".select2-search-choice:not(.select2-locked)"); 2405 var pos = getCursorInfo(this.search); 2406 2407 if (selected.length && 2408 (e.which == KEY.LEFT || e.which == KEY.RIGHT || e.which == KEY.BACKSPACE || e.which == KEY.DELETE || e.which == KEY.ENTER)) { 2409 var selectedChoice = selected; 2410 if (e.which == KEY.LEFT && prev.length) { 2411 selectedChoice = prev; 2412 } 2413 else if (e.which == KEY.RIGHT) { 2414 selectedChoice = next.length ? next : null; 2415 } 2416 else if (e.which === KEY.BACKSPACE) { 2417 this.unselect(selected.first()); 2418 this.search.width(10); 2419 selectedChoice = prev.length ? prev : next; 2420 } else if (e.which == KEY.DELETE) { 2421 this.unselect(selected.first()); 2422 this.search.width(10); 2423 selectedChoice = next.length ? next : null; 2424 } else if (e.which == KEY.ENTER) { 2425 selectedChoice = null; 2426 } 2427 2428 this.selectChoice(selectedChoice); 2429 killEvent(e); 2430 if (!selectedChoice || !selectedChoice.length) { 2431 this.open(); 2432 } 2433 return; 2434 } else if (((e.which === KEY.BACKSPACE && this.keydowns == 1) 2435 || e.which == KEY.LEFT) && (pos.offset == 0 && !pos.length)) { 2436 2437 this.selectChoice(selection.find(".select2-search-choice:not(.select2-locked)").last()); 2438 killEvent(e); 2439 return; 2440 } else { 2441 this.selectChoice(null); 2442 } 2443 2444 if (this.opened()) { 2445 switch (e.which) { 2446 case KEY.UP: 2447 case KEY.DOWN: 2448 this.moveHighlight((e.which === KEY.UP) ? -1 : 1); 2449 killEvent(e); 2450 return; 2451 case KEY.ENTER: 2452 this.selectHighlighted(); 2453 killEvent(e); 2454 return; 2455 case KEY.TAB: 2456 this.selectHighlighted({noFocus:true}); 2457 this.close(); 2458 return; 2459 case KEY.ESC: 2460 this.cancel(e); 2461 killEvent(e); 2462 return; 2463 } 2464 } 2465 2466 if (e.which === KEY.TAB || KEY.isControl(e) || KEY.isFunctionKey(e) 2467 || e.which === KEY.BACKSPACE || e.which === KEY.ESC) { 2468 return; 2469 } 2470 2471 if (e.which === KEY.ENTER) { 2472 if (this.opts.openOnEnter === false) { 2473 return; 2474 } else if (e.altKey || e.ctrlKey || e.shiftKey || e.metaKey) { 2475 return; 2476 } 2477 } 2478 2479 this.open(); 2480 2481 if (e.which === KEY.PAGE_UP || e.which === KEY.PAGE_DOWN) { 2482 // prevent the page from scrolling 2483 killEvent(e); 2484 } 2485 2486 if (e.which === KEY.ENTER) { 2487 // prevent form from being submitted 2488 killEvent(e); 2489 } 2490 2491 })); 2492 2493 this.search.on("keyup", this.bind(function (e) { 2494 this.keydowns = 0; 2495 this.resizeSearch(); 2496 }) 2497 ); 2498 2499 this.search.on("blur", this.bind(function(e) { 2500 this.container.removeClass("select2-container-active"); 2501 this.search.removeClass("select2-focused"); 2502 this.selectChoice(null); 2503 if (!this.opened()) this.clearSearch(); 2504 e.stopImmediatePropagation(); 2505 this.opts.element.trigger($.Event("select2-blur")); 2506 })); 2507 2508 this.container.on("click", selector, this.bind(function (e) { 2509 if (!this.isInterfaceEnabled()) return; 2510 if ($(e.target).closest(".select2-search-choice").length > 0) { 2511 // clicked inside a select2 search choice, do not open 2512 return; 2513 } 2514 this.selectChoice(null); 2515 this.clearPlaceholder(); 2516 if (!this.container.hasClass("select2-container-active")) { 2517 this.opts.element.trigger($.Event("select2-focus")); 2518 } 2519 this.open(); 2520 this.focusSearch(); 2521 e.preventDefault(); 2522 })); 2523 2524 this.container.on("focus", selector, this.bind(function () { 2525 if (!this.isInterfaceEnabled()) return; 2526 if (!this.container.hasClass("select2-container-active")) { 2527 this.opts.element.trigger($.Event("select2-focus")); 2528 } 2529 this.container.addClass("select2-container-active"); 2530 this.dropdown.addClass("select2-drop-active"); 2531 this.clearPlaceholder(); 2532 })); 2533 2534 this.initContainerWidth(); 2535 this.opts.element.addClass("select2-offscreen"); 2536 2537 // set the placeholder if necessary 2538 this.clearSearch(); 2539 }, 2540 2541 // multi 2542 enableInterface: function() { 2543 if (this.parent.enableInterface.apply(this, arguments)) { 2544 this.search.prop("disabled", !this.isInterfaceEnabled()); 2545 } 2546 }, 2547 2548 // multi 2549 initSelection: function () { 2550 var data; 2551 if (this.opts.element.val() === "" && this.opts.element.text() === "") { 2552 this.updateSelection([]); 2553 this.close(); 2554 // set the placeholder if necessary 2555 this.clearSearch(); 2556 } 2557 if (this.select || this.opts.element.val() !== "") { 2558 var self = this; 2559 this.opts.initSelection.call(null, this.opts.element, function(data){ 2560 if (data !== undefined && data !== null) { 2561 self.updateSelection(data); 2562 self.close(); 2563 // set the placeholder if necessary 2564 self.clearSearch(); 2565 } 2566 }); 2567 } 2568 }, 2569 2570 // multi 2571 clearSearch: function () { 2572 var placeholder = this.getPlaceholder(), 2573 maxWidth = this.getMaxSearchWidth(); 2574 2575 if (placeholder !== undefined && this.getVal().length === 0 && this.search.hasClass("select2-focused") === false) { 2576 this.search.val(placeholder).addClass("select2-default"); 2577 // stretch the search box to full width of the container so as much of the placeholder is visible as possible 2578 // we could call this.resizeSearch(), but we do not because that requires a sizer and we do not want to create one so early because of a firefox bug, see #944 2579 this.search.width(maxWidth > 0 ? maxWidth : this.container.css("width")); 2580 } else { 2581 this.search.val("").width(10); 2582 } 2583 }, 2584 2585 // multi 2586 clearPlaceholder: function () { 2587 if (this.search.hasClass("select2-default")) { 2588 this.search.val("").removeClass("select2-default"); 2589 } 2590 }, 2591 2592 // multi 2593 opening: function () { 2594 this.clearPlaceholder(); // should be done before super so placeholder is not used to search 2595 this.resizeSearch(); 2596 2597 this.parent.opening.apply(this, arguments); 2598 2599 this.focusSearch(); 2600 2601 this.updateResults(true); 2602 this.search.focus(); 2603 this.opts.element.trigger($.Event("select2-open")); 2604 }, 2605 2606 // multi 2607 close: function () { 2608 if (!this.opened()) return; 2609 this.parent.close.apply(this, arguments); 2610 }, 2611 2612 // multi 2613 focus: function () { 2614 this.close(); 2615 this.search.focus(); 2616 }, 2617 2618 // multi 2619 isFocused: function () { 2620 return this.search.hasClass("select2-focused"); 2621 }, 2622 2623 // multi 2624 updateSelection: function (data) { 2625 var ids = [], filtered = [], self = this; 2626 2627 // filter out duplicates 2628 $(data).each(function () { 2629 if (indexOf(self.id(this), ids) < 0) { 2630 ids.push(self.id(this)); 2631 filtered.push(this); 2632 } 2633 }); 2634 data = filtered; 2635 2636 this.selection.find(".select2-search-choice").remove(); 2637 $(data).each(function () { 2638 self.addSelectedChoice(this); 2639 }); 2640 self.postprocessResults(); 2641 }, 2642 2643 // multi 2644 tokenize: function() { 2645 var input = this.search.val(); 2646 input = this.opts.tokenizer.call(this, input, this.data(), this.bind(this.onSelect), this.opts); 2647 if (input != null && input != undefined) { 2648 this.search.val(input); 2649 if (input.length > 0) { 2650 this.open(); 2651 } 2652 } 2653 2654 }, 2655 2656 // multi 2657 onSelect: function (data, options) { 2658 2659 if (!this.triggerSelect(data)) { return; } 2660 2661 this.addSelectedChoice(data); 2662 2663 this.opts.element.trigger({ type: "selected", val: this.id(data), choice: data }); 2664 2665 if (this.select || !this.opts.closeOnSelect) this.postprocessResults(); 2666 2667 if (this.opts.closeOnSelect) { 2668 this.close(); 2669 this.search.width(10); 2670 } else { 2671 if (this.countSelectableResults()>0) { 2672 this.search.width(10); 2673 this.resizeSearch(); 2674 if (this.getMaximumSelectionSize() > 0 && this.val().length >= this.getMaximumSelectionSize()) { 2675 // if we reached max selection size repaint the results so choices 2676 // are replaced with the max selection reached message 2677 this.updateResults(true); 2678 } 2679 this.positionDropdown(); 2680 } else { 2681 // if nothing left to select close 2682 this.close(); 2683 this.search.width(10); 2684 } 2685 } 2686 2687 // since its not possible to select an element that has already been 2688 // added we do not need to check if this is a new element before firing change 2689 this.triggerChange({ added: data }); 2690 2691 if (!options || !options.noFocus) 2692 this.focusSearch(); 2693 }, 2694 2695 // multi 2696 cancel: function () { 2697 this.close(); 2698 this.focusSearch(); 2699 }, 2700 2701 addSelectedChoice: function (data) { 2702 var enableChoice = !data.locked, 2703 enabledItem = $( 2704 "<li class='select2-search-choice'>" + 2705 " <div></div>" + 2706 " <a href='#' onclick='return false;' class='select2-search-choice-close' tabindex='-1'></a>" + 2707 "</li>"), 2708 disabledItem = $( 2709 "<li class='select2-search-choice select2-locked'>" + 2710 "<div></div>" + 2711 "</li>"); 2712 var choice = enableChoice ? enabledItem : disabledItem, 2713 id = this.id(data), 2714 val = this.getVal(), 2715 formatted, 2716 cssClass; 2717 2718 formatted=this.opts.formatSelection(data, choice.find("div"), this.opts.escapeMarkup); 2719 if (formatted != undefined) { 2720 choice.find("div").replaceWith("<div>"+formatted+"</div>"); 2721 } 2722 cssClass=this.opts.formatSelectionCssClass(data, choice.find("div")); 2723 if (cssClass != undefined) { 2724 choice.addClass(cssClass); 2725 } 2726 2727 if(enableChoice){ 2728 choice.find(".select2-search-choice-close") 2729 .on("mousedown", killEvent) 2730 .on("click dblclick", this.bind(function (e) { 2731 if (!this.isInterfaceEnabled()) return; 2732 2733 $(e.target).closest(".select2-search-choice").fadeOut('fast', this.bind(function(){ 2734 this.unselect($(e.target)); 2735 this.selection.find(".select2-search-choice-f
2735ocus").removeClass("select2-search-choice-focus"); 2736 this.close(); 2737 this.focusSearch(); 2738 })).dequeue(); 2739 killEvent(e); 2740 })).on("focus", this.bind(function () { 2741 if (!this.isInterfaceEnabled()) return; 2742 this.container.addClass("select2-container-active"); 2743 this.dropdown.addClass("select2-drop-active"); 2744 })); 2745 } 2746 2747 choice.data("select2-data", data); 2748 choice.insertBefore(this.searchContainer); 2749 2750 val.push(id); 2751 this.setVal(val); 2752 }, 2753 2754 // multi 2755 unselect: function (selected) { 2756 var val = this.getVal(), 2757 data, 2758 index; 2759 2760 selected = selected.closest(".select2-search-choice"); 2761 2762 if (selected.length === 0) { 2763 throw "Invalid argument: " + selected + ". Must be .select2-search-choice"; 2764 } 2765 2766 data = selected.data("select2-data"); 2767 2768 if (!data) { 2769 // prevent a race condition when the 'x' is clicked really fast repeatedly the event can be queued 2770 // and invoked on an element already removed 2771 return; 2772 } 2773 2774 index = indexOf(this.id(data), val); 2775 2776 if (index >= 0) { 2777 val.splice(index, 1); 2778 this.setVal(val); 2779 if (this.select) this.postprocessResults(); 2780 } 2781 selected.remove(); 2782 2783 this.opts.element.trigger({ type: "removed", val: this.id(data), choice: data }); 2784 this.triggerChange({ removed: data }); 2785 }, 2786 2787 // multi 2788 postprocessResults: function (data, initial, noHighlightUpdate) { 2789 var val = this.getVal(), 2790 choices = this.results.find(".select2-result"), 2791 compound = this.results.find(".select2-result-with-children"), 2792 self = this; 2793 2794 choices.each2(function (i, choice) { 2795 var id = self.id(choice.data("select2-data")); 2796 if (indexOf(id, val) >= 0) { 2797 choice.addClass("select2-selected"); 2798 // mark all children of the selected parent as selected 2799 choice.find(".select2-result-selectable").addClass("select2-selected"); 2800 } 2801 }); 2802 2803 compound.each2(function(i, choice) { 2804 // hide an optgroup if it doesnt have any selectable children 2805 if (!choice.is('.select2-result-selectable') 2806 && choice.find(".select2-result-selectable:not(.select2-selected)").length === 0) { 2807 choice.addClass("select2-selected"); 2808 } 2809 }); 2810 2811 if (this.highlight() == -1 && noHighlightUpdate !== false){ 2812 self.highlight(0); 2813 } 2814 2815 //If all results are chosen render formatNoMAtches 2816 if(!this.opts.createSearchChoice && !choices.filter('.select2-result:not(.select2-selected)').length > 0){ 2817 if(!data || data && !data.more && this.results.find(".select2-no-results").length === 0) { 2818 if (checkFormatter(self.opts.formatNoMatches, "formatNoMatches")) { 2819 this.results.append("<li class='select2-no-results'>" + self.opts.formatNoMatches(self.search.val()) + "</li>"); 2820 } 2821 } 2822 } 2823 2824 }, 2825 2826 // multi 2827 getMaxSearchWidth: function() { 2828 return this.selection.width() - getSideBorderPadding(this.search); 2829 }, 2830 2831 // multi 2832 resizeSearch: function () { 2833 var minimumWidth, left, maxWidth, containerLeft, searchWidth, 2834 sideBorderPadding = getSideBorderPadding(this.search); 2835 2836 minimumWidth = measureTextWidth(this.search) + 10; 2837 2838 left = this.search.offset().left; 2839 2840 maxWidth = this.selection.width(); 2841 containerLeft = this.selection.offset().left; 2842 2843 searchWidth = maxWidth - (left - containerLeft) - sideBorderPadding; 2844 2845 if (searchWidth < minimumWidth) { 2846 searchWidth = maxWidth - sideBorderPadding; 2847 } 2848 2849 if (searchWidth < 40) { 2850 searchWidth = maxWidth - sideBorderPadding; 2851 } 2852 2853 if (searchWidth <= 0) { 2854 searchWidth = minimumWidth; 2855 } 2856 2857 this.search.width(searchWidth); 2858 }, 2859 2860 // multi 2861 getVal: function () { 2862 var val; 2863 if (this.select) { 2864 val = this.select.val(); 2865 return val === null ? [] : val; 2866 } else { 2867 val = this.opts.element.val(); 2868 return splitVal(val, this.opts.separator); 2869 } 2870 }, 2871 2872 // multi 2873 setVal: function (val) { 2874 var unique; 2875 if (this.select) { 2876 this.select.val(val); 2877 } else { 2878 unique = []; 2879 // filter out duplicates 2880 $(val).each(function () { 2881 if (indexOf(this, unique) < 0) unique.push(this); 2882 }); 2883 this.opts.element.val(unique.length === 0 ? "" : unique.join(this.opts.separator)); 2884 } 2885 }, 2886 2887 // multi 2888 buildChangeDetails: function (old, current) { 2889 var current = current.slice(0), 2890 old = old.slice(0); 2891 2892 // remove intersection from each array 2893 for (var i = 0; i < current.length; i++) { 2894 for (var j = 0; j < old.length; j++) { 2895 if (equal(this.opts.id(current[i]), this.opts.id(old[j]))) { 2896 current.splice(i, 1); 2897 i--; 2898 old.splice(j, 1); 2899 j--; 2900 } 2901 } 2902 } 2903 2904 return {added: current, removed: old}; 2905 }, 2906 2907 2908 // multi 2909 val: function (val, triggerChange) { 2910 var oldData, self=this, changeDetails; 2911 2912 if (arguments.length === 0) { 2913 return this.getVal(); 2914 } 2915 2916 oldData=this.data(); 2917 if (!oldData.length) oldData=[]; 2918 2919 // val is an id. !val is true for [undefined,null,'',0] - 0 is legal 2920 if (!val && val !== 0) { 2921 this.opts.element.val(""); 2922 this.updateSelection([]); 2923 this.clearSearch(); 2924 if (triggerChange) { 2925 this.triggerChange({added: this.data(), removed: oldData}); 2926 } 2927 return; 2928 } 2929 2930 // val is a list of ids 2931 this.setVal(val); 2932 2933 if (this.select) { 2934 this.opts.initSelection(this.select, this.bind(this.updateSelection)); 2935 if (triggerChange) { 2936 this.triggerChange(this.buildChangeDetails(oldData, this.data())); 2937 } 2938 } else { 2939 if (this.opts.initSelection === undefined) { 2940 throw new Error("val() cannot be called if initSelection() is not defined"); 2941 } 2942 2943 this.opts.initSelection(this.opts.element, function(data){ 2944 var ids=$.map(data, self.id); 2945 self.setVal(ids); 2946 self.updateSelection(data); 2947 self.clearSearch(); 2948 if (triggerChange) { 2949 self.triggerChange(this.buildChangeDetails(oldData, this.data())); 2950 } 2951 }); 2952 } 2953 this.clearSearch(); 2954 }, 2955 2956 // multi 2957 onSortStart: function() { 2958 if (this.select) { 2959 throw new Error("Sorting of elements is not supported when attached to <select>. Attach to <input type='hidden'/> instead."); 2960 } 2961
2962 // collapse search field into 0 width so its container can be collapsed as well 2963 this.search.width(0); 2964 // hide the container 2965 this.searchContainer.hide(); 2966 }, 2967 2968 // multi 2969 onSortEnd:function() { 2970 2971 var val=[], self=this; 2972 2973 // show search and move it to the end of the list 2974 this.searchContainer.show(); 2975 // make sure the search container is the last item in the list 2976 this.searchContainer.appendTo(this.searchContainer.parent()); 2977 // since we collapsed the width in dragStarted, we resize it here 2978 this.resizeSearch(); 2979 2980 // update selection 2981 this.selection.find(".select2-search-choice").each(function() { 2982 val.push(self.opts.id($(this).data("select2-data"))); 2983 }); 2984 this.setVal(val); 2985 this.triggerChange(); 2986 }, 2987 2988 // multi 2989 data: function(values, triggerChange) { 2990 var self=this, ids, old; 2991 if (arguments.length === 0) { 2992 return this.selection 2993 .find(".select2-search-choice") 2994 .map(function() { return $(this).data("select2-data"); }) 2995 .get(); 2996 } else { 2997 old = this.data(); 2998 if (!values) { values = []; } 2999 ids = $.map(values, function(e) { return self.opts.id(e); }); 3000 this.setVal(ids); 3001 this.updateSelection(values); 3002 this.clearSearch(); 3003 if (triggerChange) { 3004 this.triggerChange(this.buildChangeDetails(old, this.data())); 3005 } 3006 } 3007 } 3008 }); 3009 3010 $.fn.select2 = function () { 3011 3012 var args = Array.prototype.slice.call(arguments, 0), 3013 opts, 3014 select2, 3015 method, value, multiple, 3016 allowedMethods = ["val", "destroy", "opened", "open", "close", "focu
3016s", "isFocused", "container", "dropdown", "onSortStart", "onSortEnd", "enable", "readonly", "positionDropdown", "data", "search"], 3017 valueMethods = ["val", "opened", "isFocused", "container", "data"], 3018 methodsMap = { search: "externalSearch" }; 3019 3020 this.each(function () { 3021 if (args.length === 0 || typeof(args[0]) === "object") { 3022 opts = args.length === 0 ? {} : $.extend({}, args[0]); 3023 opts.element = $(this); 3024 3025 if (opts.element.get(0).tagName.toLowerCase() === "select") { 3026 multiple = opts.element.prop("multiple"); 3027 } else { 3028 multiple = opts.multiple || false; 3029 if ("tags" in opts) {opts.multiple = multiple = true;} 3030 } 3031 3032 select2 = multiple ? new MultiSelect2() : new SingleSelect2(); 3033 select2.init(opts); 3034 } else if (typeof(args[0]) === "string") { 3035 3036 if (indexOf(args[0], allowedMethods) < 0) { 3037 throw "Unknown method: " + args[0]; 3038 } 3039 3040 value = undefined; 3041 select2 = $(this).data("select2"); 3042 if (select2 === undefined) return; 3043 3044 method=args[0]; 3045 3046 if (method === "container") { 3047 value = select2.container; 3048 } else if (method === "dropdown") { 3049 value = select2.dropdown; 3050 } else { 3051 if (methodsMap[method]) method = methodsMap[method]; 3052 3053 value = select2[method].apply(select2, args.slice(1)); 3054 } 3055 if (indexOf(args[0], valueMethods) >= 0) { 3056 return false; 3057 } 3058 } else { 3059 throw "Invalid arguments to select2 plugin: " + args; 3060 } 3061 }); 3062 return (value === undefined) ? this : value; 3063 }; 3064 3065 // plugin defaults, accessible to users 3066 $.fn.select2.defaults = { 3067 width: "copy", 3068 loadMorePadding: 0, 3069 closeOnSelect: true, 3070 openOnEnter: true, 3071 containerCss: {}, 3072 dropdownCss: {}, 3073 containerCssClass: "", 3074 dropdownCssClass: "", 3075 formatResult: function(result, container, query, escapeMarkup) { 3076 var markup=[]; 3077 markMatch(result.text, query.term, markup, escapeMarkup); 3078 return markup.join(""); 3079 }, 3080 formatSelection: function (data, container, escapeMarkup) { 3081 return data ? escapeMarkup(data.text) : undefined; 3082 }, 3083 sortResults: function (results, container, query) { 3084 return results; 3085 }, 3086 formatResultCssClass: function(data) {return undefined;}, 3087 formatSelectionCssClass: function(data, container) {return undefined;}, 3088 formatNoMatches: function () { return "No matches found"; }, 3089 formatInputTooShort: function (input, min) { var n = min - input.length; return "Please enter " + n + " more character" + (n == 1? "" : "s"); }, 3090 formatInputTooLong: function (input, max) { var n = input.length - max; return "Please delete " + n + " character" + (n == 1? "" : "s"); }, 3091 formatSelectionTooBig: function (limit) { return "You can only select " + limit + " item" + (limit == 1 ? "" : "s"); }, 3092 formatLoadMore: function (pageNumber) { return "Loading more results..."; }, 3093 formatSearching: function () { return "Searching..."; }, 3094 minimumResultsForSearch: 0, 3095 minimumInputLength: 0, 3096 maximumInputLength: null, 3097 maximumSelectionSize: 0, 3098 id: function (e) { return e.id; }, 3099 matcher: function(term, text) { 3100 return (''+text).toUpperCase().indexOf((''+term).toUpperCase()) >= 0; 3101 }, 3102 separator: ",", 3103 tokenSeparators: [], 3104 tokenizer: defaultTokenizer, 3105 escapeMarkup: defaultEscapeMarkup, 3106 blurOnChange: false, 3107 selectOnBlur: false, 3108 adaptContainerCssClass: function(c) { return c; }, 3109 adaptDropdownCssClass: function(c) { return null; } 3110 }; 3111 3112 $.fn.select2.ajaxDefaults = { 3113 transport: $.ajax, 3114 params: { 3115 type: "GET", 3116 cache: false, 3117 dataType: "json" 3118 } 3119 }; 3120 3121 // exports 3122 window.Select2 = { 3123 query: { 3124 ajax: ajax, 3125 local: local, 3126 tags: tags 3127 }, util: { 3128 debounce: debounce, 3129 markMatch: markMatch, 3130 escapeMarkup: defaultEscapeMarkup 3131 }, "class": { 3132 "abstract": AbstractSelect2, 3133 "single": SingleSelect2, 3134 "multi": MultiSelect2 3135 } 3136 }; 3137 3138}(jQuery)); 3139 3140$(document).ready(function(){$(".cart-icon").click(function(){$(".cart_items_wrapper").toggleClass("shown")});$("#close-btn").click(function(){$(".cart_items_wrapper").toggleClass("shown")})});
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.