[ Index ] |
PHP Cross Reference of vtigercrm-6.1.0 |
[Summary view] [Print] [Text view]
1 /* 2 Copyright 2012 Igor Vaynberg 3 4 Version: 3.2 Timestamp: Mon Sep 10 10:38:04 PDT 2012 5 6 Licensed under the Apache License, Version 2.0 (the "License"); you may not use this work except in 7 compliance with the License. You may obtain a copy of the License in the LICENSE file, or at: 8 9 http://www.apache.org/licenses/LICENSE-2.0 10 11 Unless required by applicable law or agreed to in writing, software distributed under the License is 12 distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and limitations under the License. 14 */ 15 (function ($) { 16 if(typeof $.fn.each2 == "undefined"){ 17 $.fn.extend({ 18 /* 19 * 4-10 times faster .each replacement 20 * use it carefully, as it overrides jQuery context of element on each iteration 21 */ 22 each2 : function (c) { 23 var j = $([0]), i = -1, l = this.length; 24 while ( 25 ++i < l 26 && (j.context = j[0] = this[i]) 27 && c.call(j[0], i, j) !== false //"this"=DOM, i=index, j=jQuery object 28 ); 29 return this; 30 } 31 }); 32 } 33 })(jQuery); 34 35 (function ($, undefined) { 36 "use strict"; 37 /*global document, window, jQuery, console */ 38 39 if (window.Select2 !== undefined) { 40 return; 41 } 42 43 var KEY, AbstractSelect2, SingleSelect2, MultiSelect2, nextUid, sizer; 44 45 KEY = { 46 TAB: 9, 47 ENTER: 13, 48 ESC: 27, 49 SPACE: 32, 50 LEFT: 37, 51 UP: 38, 52 RIGHT: 39, 53 DOWN: 40, 54 SHIFT: 16, 55 CTRL: 17, 56 ALT: 18, 57 PAGE_UP: 33, 58 PAGE_DOWN: 34, 59 HOME: 36, 60 END: 35, 61 BACKSPACE: 8, 62 DELETE: 46, 63 isArrow: function (k) { 64 k = k.which ? k.which : k; 65 switch (k) { 66 case KEY.LEFT: 67 case KEY.RIGHT: 68 case KEY.UP: 69 case KEY.DOWN: 70 return true; 71 } 72 return false; 73 }, 74 isControl: function (e) { 75 var k = e.which; 76 switch (k) { 77 case KEY.SHIFT: 78 case KEY.CTRL: 79 case KEY.ALT: 80 return true; 81 } 82 83 if (e.metaKey) return true; 84 85 return false; 86 }, 87 isFunctionKey: function (k) { 88 k = k.which ? k.which : k; 89 return k >= 112 && k <= 123; 90 } 91 }; 92 93 nextUid=(function() { var counter=1; return function() { return counter++; }; }()); 94 95 function indexOf(value, array) { 96 var i = 0, l = array.length, v; 97 98 if (typeof value === "undefined") { 99 return -1; 100 } 101 102 if (value.constructor === String) { 103 for (; i < l; i = i + 1) if (value.localeCompare(array[i]) === 0) return i; 104 } else { 105 for (; i < l; i = i + 1) { 106 v = array[i]; 107 if (v.constructor === String) { 108 if (v.localeCompare(value) === 0) return i; 109 } else { 110 if (v === value) return i; 111 } 112 } 113 } 114 return -1; 115 } 116 117 /** 118 * Compares equality of a and b taking into account that a and b may be strings, in which case localeCompare is used 119 * @param a 120 * @param b 121 */ 122 function equal(a, b) { 123 if (a === b) return true; 124 if (a === undefined || b === undefined) return false; 125 if (a === null || b === null) return false; 126 if (a.constructor === String) return a.localeCompare(b) === 0; 127 if (b.constructor === String) return b.localeCompare(a) === 0; 128 return false; 129 } 130 131 /** 132 * Splits the string into an array of values, trimming each value. An empty array is returned for nulls or empty 133 * strings 134 * @param string 135 * @param separator 136 */ 137 function splitVal(string, separator) { 138 var val, i, l; 139 if (string === null || string.length < 1) return []; 140 val = string.split(separator); 141 for (i = 0, l = val.length; i < l; i = i + 1) val[i] = $.trim(val[i]); 142 return val; 143 } 144 145 function getSideBorderPadding(element) { 146 return element.outerWidth() - element.width(); 147 } 148 149 function installKeyUpChangeEvent(element) { 150 var key="keyup-change-value"; 151 element.bind("keydown", function () { 152 if ($.data(element, key) === undefined) { 153 $.data(element, key, element.val()); 154 } 155 }); 156 element.bind("keyup", function () { 157 var val= $.data(element, key); 158 if (val !== undefined && element.val() !== val) { 159 $.removeData(element, key); 160 element.trigger("keyup-change"); 161 } 162 }); 163 } 164 165 $(document).delegate("body", "mousemove", function (e) { 166 $.data(document, "select2-lastpos", {x: e.pageX, y: e.pageY}); 167 }); 168 169 /** 170 * filters mouse events so an event is fired only if the mouse moved. 171 * 172 * filters out mouse events that occur when mouse is stationary but 173 * the elements under the pointer are scrolled. 174 */ 175 function installFilteredMouseMove(element) { 176 element.bind("mousemove", function (e) { 177 var lastpos = $.data(document, "select2-lastpos"); 178 if (lastpos === undefined || lastpos.x !== e.pageX || lastpos.y !== e.pageY) { 179 $(e.target).trigger("mousemove-filtered", e); 180 } 181 }); 182 } 183 184 /** 185 * Debounces a function. Returns a function that calls the original fn function only if no invocations have been made 186 * within the last quietMillis milliseconds. 187 * 188 * @param quietMillis number of milliseconds to wait before invoking fn 189 * @param fn function to be debounced 190 * @param ctx object to be used as this reference within fn 191 * @return debounced version of fn 192 */ 193 function debounce(quietMillis, fn, ctx) { 194 ctx = ctx || undefined; 195 var timeout; 196 return function () { 197 var args = arguments; 198 window.clearTimeout(timeout); 199 timeout = window.setTimeout(function() { 200 fn.apply(ctx, args); 201 }, quietMillis); 202 }; 203 } 204 205 /** 206 * A simple implementation of a thunk 207 * @param formula function used to lazily initialize the thunk 208 * @return {Function} 209 */ 210 function thunk(formula) { 211 var evaluated = false, 212 value; 213 return function() { 214 if (evaluated === false) { value = formula(); evaluated = true; } 215 return value; 216 }; 217 }; 218 219 function installDebouncedScroll(threshold, element) { 220 var notify = debounce(threshold, function (e) { element.trigger("scroll-debounced", e);}); 221 element.bind("scroll", function (e) { 222 if (indexOf(e.target, element.get()) >= 0) notify(e); 223 }); 224 } 225 226 function killEvent(event) { 227 event.preventDefault(); 228 event.stopPropagation(); 229 } 230 231 function measureTextWidth(e) { 232 if (!sizer){ 233 var style = e[0].currentStyle || window.getComputedStyle(e[0], null); 234 sizer = $("<div></div>").css({ 235 position: "absolute", 236 left: "-10000px", 237 top: "-10000px", 238 display: "none", 239 fontSize: style.fontSize, 240 fontFamily: style.fontFamily, 241 fontStyle: style.fontStyle, 242 fontWeight: style.fontWeight, 243 letterSpacing: style.letterSpacing, 244 textTransform: style.textTransform, 245 whiteSpace: "nowrap" 246 }); 247 $("body").append(sizer); 248 } 249 sizer.text(e.val()); 250 return sizer.width(); 251 } 252 253 function markMatch(text, term, markup) { 254 var match=text.toUpperCase().indexOf(term.toUpperCase()), 255 tl=term.length; 256 257 if (match<0) { 258 markup.push(text); 259 return; 260 } 261 262 markup.push(text.substring(0, match)); 263 markup.push("<span class='select2-match'>"); 264 markup.push(text.substring(match, match + tl)); 265 markup.push("</span>"); 266 markup.push(text.substring(match + tl, text.length)); 267 } 268 269 /** 270 * Produces an ajax-based query function 271 * 272 * @param options object containing configuration paramters 273 * @param options.transport function that will be used to execute the ajax request. must be compatible with parameters supported by $.ajax 274 * @param options.url url for the data 275 * @param options.data a function(searchTerm, pageNumber, context) that should return an object containing query string parameters for the above url. 276 * @param options.dataType request data type: ajax, jsonp, other datatatypes supported by jQuery's $.ajax function or the transport function if specified 277 * @param options.traditional a boolean flag that should be true if you wish to use the traditional style of param serialization for the ajax request 278 * @param options.quietMillis (optional) milliseconds to wait before making the ajaxRequest, helps debounce the ajax function if invoked too often 279 * @param options.results a function(remoteData, pageNumber) that converts data returned form the remote request to the format expected by Select2. 280 * The expected format is an object containing the following keys: 281 * results array of objects that will be used as choices 282 * more (optional) boolean indicating whether there are more results available 283 * Example: {results:[{id:1, text:'Red'},{id:2, text:'Blue'}], more:true} 284 */ 285 function ajax(options) { 286 var timeout, // current scheduled but not yet executed request 287 requestSequence = 0, // sequence used to drop out-of-order responses 288 handler = null, 289 quietMillis = options.quietMillis || 100; 290 291 return function (query) { 292 window.clearTimeout(timeout); 293 timeout = window.setTimeout(function () { 294 requestSequence += 1; // increment the sequence 295 var requestNumber = requestSequence, // this request's sequence number 296 data = options.data, // ajax data function 297 transport = options.transport || $.ajax, 298 traditional = options.traditional || false, 299 type = options.type || 'GET'; // set type of request (GET or POST) 300 301 data = data.call(this, query.term, query.page, query.context); 302 303 if( null !== handler) { handler.abort(); } 304 305 handler = transport.call(null, { 306 url: options.url, 307 dataType: options.dataType, 308 data: data, 309 type: type, 310 traditional: traditional, 311 success: function (data) { 312 if (requestNumber < requestSequence) { 313 return; 314 } 315 // TODO 3.0 - replace query.page with query so users have access to term, page, etc. 316 var results = options.results(data, query.page); 317 query.callback(results); 318 } 319 }); 320 }, quietMillis); 321 }; 322 } 323 324 /** 325 * Produces a query function that works with a local array 326 * 327 * @param options object containing configuration parameters. The options parameter can either be an array or an 328 * object. 329 * 330 * If the array form is used it is assumed that it contains objects with 'id' and 'text' keys. 331 * 332 * If the object form is used ti is assumed that it contains 'data' and 'text' keys. The 'data' key should contain 333 * an array of objects that will be used as choices. These objects must contain at least an 'id' key. The 'text' 334 * key can either be a String in which case it is expected that each element in the 'data' array has a key with the 335 * value of 'text' which will be used to match choices. Alternatively, text can be a function(item) that can extract 336 * the text. 337 */ 338 function local(options) { 339 var data = options, // data elements 340 dataText, 341 text = function (item) { return ""+item.text; }; // function used to retrieve the text portion of a data item that is matched against the search 342 343 if (!$.isArray(data)) { 344 text = data.text; 345 // if text is not a function we assume it to be a key name 346 if (!$.isFunction(text)) { 347 dataText = data.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 348 text = function (item) { return item[dataText]; }; 349 } 350 data = data.results; 351 } 352 353 return function (query) { 354 var t = query.term, filtered = { results: [] }, process; 355 if (t === "") { 356 query.callback({results: data}); 357 return; 358 } 359 360 process = function(datum, collection) { 361 var group, attr; 362 datum = datum[0]; 363 if (datum.children) { 364 group = {}; 365 for (attr in datum) { 366 if (datum.hasOwnProperty(attr)) group[attr]=datum[attr]; 367 } 368 group.children=[]; 369 $(datum.children).each2(function(i, childDatum) { process(childDatum, group.children); }); 370 if (group.children.length) { 371 collection.push(group); 372 } 373 } else { 374 if (query.matcher(t, text(datum))) { 375 collection.push(datum); 376 } 377 } 378 }; 379 380 $(data).each2(function(i, datum) { process(datum, filtered.results); }); 381 query.callback(filtered); 382 }; 383 } 384 385 // TODO javadoc 386 function tags(data) { 387 // TODO even for a function we should probably return a wrapper that does the same object/string check as 388 // the function for arrays. otherwise only functions that return objects are supported. 389 if ($.isFunction(data)) { 390 return data; 391 } 392 393 // if not a function we assume it to be an array 394 395 return function (query) { 396 var t = query.term, filtered = {results: []}; 397 $(data).each(function () { 398 var isObject = this.text !== undefined, 399 text = isObject ? this.text : this; 400 if (t === "" || query.matcher(t, text)) { 401 filtered.results.push(isObject ? this : {id: this, text: this}); 402 } 403 }); 404 query.callback(filtered); 405 }; 406 } 407 408 /** 409 * Checks if the formatter function should be used. 410 * 411 * Throws an error if it is not a function. Returns true if it should be used, 412 * false if no formatting should be performed. 413 * 414 * @param formatter 415 */ 416 function checkFormatter(formatter, formatterName) { 417 if ($.isFunction(formatter)) return true; 418 if (!formatter) return false; 419 throw new Error("formatterName must be a function or a falsy value"); 420 } 421 422 function evaluate(val) { 423 return $.isFunction(val) ? val() : val; 424 } 425 426 function countResults(results) { 427 var count = 0; 428 $.each(results, function(i, item) { 429 if (item.children) { 430 count += countResults(item.children); 431 } else { 432 count++; 433 } 434 }); 435 return count; 436 } 437 438 /** 439 * Default tokenizer. This function uses breaks the input on substring match of any string from the 440 * opts.tokenSeparators array and uses opts.createSearchChoice to create the choice object. Both of those 441 * two options have to be defined in order for the tokenizer to work. 442 * 443 * @param input text user has typed so far or pasted into the search field 444 * @param selection currently selected choices 445 * @param selectCallback function(choice) callback tho add the choice to selection 446 * @param opts select2's opts 447 * @return undefined/null to leave the current input unchanged, or a string to change the input to the returned value 448 */ 449 function defaultTokenizer(input, selection, selectCallback, opts) { 450 var original = input, // store the original so we can compare and know if we need to tell the search to update its text 451 dupe = false, // check for whether a token we extracted represents a duplicate selected choice 452 token, // token 453 index, // position at which the separator was found 454 i, l, // looping variables 455 separator; // the matched separator 456 457 if (!opts.createSearchChoice || !opts.tokenSeparators || opts.tokenSeparators.length < 1) return undefined; 458 459 while (true) { 460 index = -1; 461 462 for (i = 0, l = opts.tokenSeparators.length; i < l; i++) { 463 separator = opts.tokenSeparators[i]; 464 index = input.indexOf(separator); 465 if (index >= 0) break; 466 } 467 468 if (index < 0) break; // did not find any token separator in the input string, bail 469 470 token = input.substring(0, index); 471 input = input.substring(index + separator.length); 472 473 if (token.length > 0) { 474 token = opts.createSearchChoice(token, selection); 475 if (token !== undefined && token !== null && opts.id(token) !== undefined && opts.id(token) !== null) { 476 dupe = false; 477 for (i = 0, l = selection.length; i < l; i++) { 478 if (equal(opts.id(token), opts.id(selection[i]))) { 479 dupe = true; break; 480 } 481 } 482 483 if (!dupe) selectCallback(token); 484 } 485 } 486 } 487 488 if (original.localeCompare(input) != 0) return input; 489 } 490 491 /** 492 * blurs any Select2 container that has focus when an element outside them was clicked or received focus 493 * 494 * also takes care of clicks on label tags that point to the source element 495 */ 496 $(document).ready(function () { 497 $(document).delegate("body", "mousedown touchend", function (e) { 498 var target = $(e.target).closest("div.select2-container").get(0), attr; 499 if (target) { 500 $(document).find("div.select2-container-active").each(function () { 501 if (this !== target) $(this).data("select2").blur(); 502 }); 503 } else { 504 target = $(e.target).closest("div.select2-drop").get(0); 505 $(document).find("div.select2-drop-active").each(function () { 506 if (this !== target) $(this).data("select2").blur(); 507 }); 508 } 509 510 target=$(e.target); 511 attr = target.attr("for"); 512 if ("LABEL" === e.target.tagName && attr && attr.length > 0) { 513 target = $("#"+attr); 514 target = target.data("select2"); 515 if (target !== undefined) { target.focus(); e.preventDefault();} 516 } 517 }); 518 }); 519 520 /** 521 * Creates a new class 522 * 523 * @param superClass 524 * @param methods 525 */ 526 function clazz(SuperClass, methods) { 527 var constructor = function () {}; 528 constructor.prototype = new SuperClass; 529 constructor.prototype.constructor = constructor; 530 constructor.prototype.parent = SuperClass.prototype; 531 constructor.prototype = $.extend(constructor.prototype, methods); 532 return constructor; 533 } 534 535 AbstractSelect2 = clazz(Object, { 536 537 // abstract 538 bind: function (func) { 539 var self = this; 540 return function () { 541 func.apply(self, arguments); 542 }; 543 }, 544 545 // abstract 546 init: function (opts) { 547 var results, search, resultsSelector = ".select2-results"; 548 549 // prepare options 550 this.opts = opts = this.prepareOpts(opts); 551 552 this.id=opts.id; 553 554 // destroy if called on an existing component 555 if (opts.element.data("select2") !== undefined && 556 opts.element.data("select2") !== null) { 557 this.destroy(); 558 } 559 560 this.enabled=true; 561 this.container = this.createContainer(); 562 563 this.containerId="s2id_"+(opts.element.attr("id") || "autogen"+nextUid()); 564 this.containerSelector="#"+this.containerId.replace(/([;&,\.\+\*\~':"\!\^#$%@\[\]\(\)=>\|])/g, '\\$1'); 565 this.container.attr("id", this.containerId); 566 567 // cache the body so future lookups are cheap 568 this.body = thunk(function() { return opts.element.closest("body"); }); 569 570 if (opts.element.attr("class") !== undefined) { 571 this.container.addClass(opts.element.attr("class").replace(/validate\[[\S ]+] ?/, '')); 572 } 573 574 this.container.css(evaluate(opts.containerCss)); 575 this.container.addClass(evaluate(opts.containerCssClass)); 576 577 // swap container for the element 578 this.opts.element 579 .data("select2", this) 580 .hide() 581 .before(this.container); 582 this.container.data("select2", this); 583 584 this.dropdown = this.container.find(".select2-drop"); 585 this.dropdown.addClass(evaluate(opts.dropdownCssClass)); 586 this.dropdown.data("select2", this); 587 588 this.results = results = this.container.find(resultsSelector); 589 this.search = search = this.container.find("input.select2-input"); 590 591 search.attr("tabIndex", this.opts.element.attr("tabIndex")); 592 593 this.resultsPage = 0; 594 this.context = null; 595 596 // initialize the container 597 this.initContainer(); 598 this.initContainerWidth(); 599 600 installFilteredMouseMove(this.results); 601 this.dropdown.delegate(resultsSelector, "mousemove-filtered", this.bind(this.highlightUnderEvent)); 602 603 installDebouncedScroll(80, this.results); 604 this.dropdown.delegate(resultsSelector, "scroll-debounced", this.bind(this.loadMoreIfNeeded)); 605 606 // if jquery.mousewheel plugin is installed we can prevent out-of-bounds scrolling of results via mousewheel 607 if ($.fn.mousewheel) { 608 results.mousewheel(function (e, delta, deltaX, deltaY) { 609 var top = results.scrollTop(), height; 610 if (deltaY > 0 && top - deltaY <= 0) { 611 results.scrollTop(0); 612 killEvent(e); 613 } else if (deltaY < 0 && results.get(0).scrollHeight - results.scrollTop() + deltaY <= results.height()) { 614 results.scrollTop(results.get(0).scrollHeight - results.height()); 615 killEvent(e); 616 } 617 }); 618 } 619 620 installKeyUpChangeEvent(search); 621 search.bind("keyup-change", this.bind(this.updateResults)); 622 search.bind("focus", function () { search.addClass("select2-focused"); if (search.val() === " ") search.val(""); }); 623 search.bind("blur", function () { search.removeClass("select2-focused");}); 624 625 this.dropdown.delegate(resultsSelector, "mouseup", this.bind(function (e) { 626 if ($(e.target).closest(".select2-result-selectable:not(.select2-disabled)").length > 0) { 627 this.highlightUnderEvent(e); 628 this.selectHighlighted(e); 629 } else { 630 this.focusSearch(); 631 } 632 killEvent(e); 633 })); 634 635 // trap all mouse events from leaving the dropdown. sometimes there may be a modal that is listening 636 // for mouse events outside of itself so it can close itself. since the dropdown is now outside the select2's 637 // dom it will trigger the popup close, which is not what we want 638 this.dropdown.bind("click mouseup mousedown", function (e) { e.stopPropagation(); }); 639 640 if ($.isFunction(this.opts.initSelection)) { 641 // initialize selection based on the current value of the source element 642 this.initSelection(); 643 644 // if the user has provided a function that can set selection based on the value of the source element 645 // we monitor the change event on the element and trigger it, allowing for two way synchronization 646 this.monitorSource(); 647 } 648 649 if (opts.element.is(":disabled") || opts.element.is("[readonly='readonly']")) this.disable(); 650 }, 651 652 // abstract 653 destroy: function () { 654 var select2 = this.opts.element.data("select2"); 655 if (select2 !== undefined) { 656 select2.container.remove(); 657 select2.dropdown.remove(); 658 select2.opts.element 659 .removeData("select2") 660 .unbind(".select2") 661 .show(); 662 } 663 }, 664 665 // abstract 666 prepareOpts: function (opts) { 667 var element, select, idKey, ajaxUrl; 668 669 element = opts.element; 670 671 if (element.get(0).tagName.toLowerCase() === "select") { 672 this.select = select = opts.element; 673 } 674 675 if (select) { 676 // these options are not allowed when attached to a select because they are picked up off the element itself 677 $.each(["id", "multiple", "ajax", "query", "createSearchChoice", "initSelection", "data", "tags"], function () { 678 if (this in opts) { 679 throw new Error("Option '" + this + "' is not allowed for Select2 when attached to a <select> element."); 680 } 681 }); 682 } 683 684 opts = $.extend({}, { 685 populateResults: function(container, results, query) { 686 var populate, data, result, children, id=this.opts.id, self=this; 687 688 populate=function(results, container, depth) { 689 690 var i, l, result, selectable, compound, node, label, innerContainer, formatted; 691 for (i = 0, l = results.length; i < l; i = i + 1) { 692 693 result=results[i]; 694 selectable=id(result) !== undefined; 695 compound=result.children && result.children.length > 0; 696 697 node=$("<li></li>"); 698 node.addClass("select2-results-dept-"+depth); 699 node.addClass("select2-result"); 700 node.addClass(selectable ? "select2-result-selectable" : "select2-result-unselectable"); 701 if (compound) { node.addClass("select2-result-with-children"); } 702 node.addClass(self.opts.formatResultCssClass(result)); 703 704 label=$("<div></div>"); 705 label.addClass("select2-result-label"); 706 707 formatted=opts.formatResult(result, label, query); 708 if (formatted!==undefined) { 709 label.html(self.opts.escapeMarkup(formatted)); 710 } 711 712 node.append(label); 713 714 if (compound) { 715 716 innerContainer=$("<ul></ul>"); 717 innerContainer.addClass("select2-result-sub"); 718 populate(result.children, innerContainer, depth+1); 719 node.append(innerContainer); 720 } 721 722 node.data("select2-data", result); 723 container.append(node); 724 } 725 }; 726 727 populate(results, container, 0); 728 } 729 }, $.fn.select2.defaults, opts); 730 731 if (typeof(opts.id) !== "function") { 732 idKey = opts.id; 733 opts.id = function (e) { return e[idKey]; }; 734 } 735 736 if (select) { 737 opts.query = this.bind(function (query) { 738 var data = { results: [], more: false }, 739 term = query.term, 740 children, firstChild, process; 741 742 process=function(element, collection) { 743 var group; 744 if (element.is("option")) { 745 if (query.matcher(term, element.text(), element)) { 746 collection.push({id:element.attr("value"), text:element.text(), element: element.get(), css: element.attr("class")}); 747 } 748 } else if (element.is("optgroup")) { 749 group={text:element.attr("label"), children:[], element: element.get(), css: element.attr("class")}; 750 element.children().each2(function(i, elm) { process(elm, group.children); }); 751 if (group.children.length>0) { 752 collection.push(group); 753 } 754 } 755 }; 756 757 children=element.children(); 758 759 // ignore the placeholder option if there is one 760 if (this.getPlaceholder() !== undefined && children.length > 0) { 761 firstChild = children[0]; 762 if ($(firstChild).text() === "") { 763 children=children.not(firstChild); 764 } 765 } 766 767 children.each2(function(i, elm) { process(elm, data.results); }); 768 769 query.callback(data); 770 }); 771 // this is needed because inside val() we construct choices from options and there id is hardcoded 772 opts.id=function(e) { return e.id; }; 773 opts.formatResultCssClass = function(data) { return data.css; } 774 } else { 775 if (!("query" in opts)) { 776 if ("ajax" in opts) { 777 ajaxUrl = opts.element.data("ajax-url"); 778 if (ajaxUrl && ajaxUrl.length > 0) { 779 opts.ajax.url = ajaxUrl; 780 } 781 opts.query = ajax(opts.ajax); 782 } else if ("data" in opts) { 783 opts.query = local(opts.data); 784 } else if ("tags" in opts) { 785 opts.query = tags(opts.tags); 786 opts.createSearchChoice = function (term) { return {id: term, text: term}; }; 787 opts.initSelection = function (element, callback) { 788 var data = []; 789 $(splitVal(element.val(), opts.separator)).each(function () { 790 var id = this, text = this, tags=opts.tags; 791 if ($.isFunction(tags)) tags=tags(); 792 $(tags).each(function() { if (equal(this.id, id)) { text = this.text; return false; } }); 793 data.push({id: id, text: text}); 794 }); 795 796 callback(data); 797 }; 798 } 799 } 800 } 801 if (typeof(opts.query) !== "function") { 802 throw "query function not defined for Select2 " + opts.element.attr("id"); 803 } 804 805 return opts; 806 }, 807 808 /** 809 * Monitor the original element for changes and update select2 accordingly 810 */ 811 // abstract 812 monitorSource: function () { 813 this.opts.element.bind("change.select2", this.bind(function (e) { 814 if (this.opts.element.data("select2-change-triggered") !== true) { 815 this.initSelection(); 816 } 817 })); 818 }, 819 820 /** 821 * Triggers the change event on the source element 822 */ 823 // abstract 824 triggerChange: function (details) { 825 826 details = details || {}; 827 details= $.extend({}, details, { type: "change", val: this.val() }); 828 // prevents recursive triggering 829 this.opts.element.data("select2-change-triggered", true); 830 this.opts.element.trigger(details); 831 this.opts.element.data("select2-change-triggered", false); 832 833 // some validation frameworks ignore the change event and listen instead to keyup, click for selects 834 // so here we trigger the click event manually 835 this.opts.element.click(); 836 837 // ValidationEngine ignorea the change event and listens instead to blur 838 // so here we trigger the blur event manually if so desired 839 if (this.opts.blurOnChange) 840 this.opts.element.blur(); 841 }, 842 843 844 // abstract 845 enable: function() { 846 if (this.enabled) return; 847 848 this.enabled=true; 849 this.container.removeClass("select2-container-disabled"); 850 }, 851 852 // abstract 853 disable: function() { 854 if (!this.enabled) return; 855 856 this.close(); 857 858 this.enabled=false; 859 this.container.addClass("select2-container-disabled"); 860 }, 861 862 // abstract 863 opened: function () { 864 return this.container.hasClass("select2-dropdown-open"); 865 }, 866 867 // abstract 868 positionDropdown: function() { 869 var offset = this.container.offset(), 870 height = this.container.outerHeight(), 871 width = this.container.outerWidth(), 872 dropHeight = this.dropdown.outerHeight(), 873 viewportBottom = $(window).scrollTop() + document.documentElement.clientHeight, 874 dropTop = offset.top + height, 875 dropLeft = offset.left, 876 enoughRoomBelow = dropTop + dropHeight <= viewportBottom, 877 enoughRoomAbove = (offset.top - dropHeight) >= this.body().scrollTop(), 878 aboveNow = this.dropdown.hasClass("select2-drop-above"), 879 bodyOffset, 880 above, 881 css; 882 883 // console.log("below/ droptop:", dropTop, "dropHeight", dropHeight, "sum", (dropTop+dropHeight)+" viewport bottom", viewportBottom, "enough?", enoughRoomBelow); 884 // console.log("above/ offset.top", offset.top, "dropHeight", dropHeight, "top", (offset.top-dropHeight), "scrollTop", this.body().scrollTop(), "enough?", enoughRoomAbove); 885 886 // fix positioning when body has an offset and is not position: static 887 888 if (this.body().css('position') !== 'static') { 889 bodyOffset = this.body().offset(); 890 dropTop -= bodyOffset.top; 891 dropLeft -= bodyOffset.left; 892 } 893 894 // always prefer the current above/below alignment, unless there is not enough room 895 896 if (aboveNow) { 897 above = true; 898 if (!enoughRoomAbove && enoughRoomBelow) above = false; 899 } else { 900 above = false; 901 if (!enoughRoomBelow && enoughRoomAbove) above = true; 902 } 903 904 if (above) { 905 dropTop = offset.top - dropHeight; 906 this.container.addClass("select2-drop-above"); 907 this.dropdown.addClass("select2-drop-above"); 908 } 909 else { 910 this.container.removeClass("select2-drop-above"); 911 this.dropdown.removeClass("select2-drop-above"); 912 } 913 914 css = $.extend({ 915 top: dropTop, 916 left: dropLeft, 917 width: width 918 }, evaluate(this.opts.dropdownCss)); 919 920 this.dropdown.css(css); 921 }, 922 923 // abstract 924 shouldOpen: function() { 925 var event; 926 927 if (this.opened()) return false; 928 929 event = $.Event("open"); 930 this.opts.element.trigger(event); 931 return !event.isDefaultPrevented(); 932 }, 933 934 // abstract 935 clearDropdownAlignmentPreference: function() { 936 // clear the classes used to figure out the preference of where the dropdown should be opened 937 this.container.removeClass("select2-drop-above"); 938 this.dropdown.removeClass("select2-drop-above"); 939 }, 940 941 /** 942 * Opens the dropdown 943 * 944 * @return {Boolean} whether or not dropdown was opened. This method will return false if, for example, 945 * the dropdown is already open, or if the 'open' event listener on the element called preventDefault(). 946 */ 947 // abstract 948 open: function () { 949 950 if (!this.shouldOpen()) return false; 951 952 window.setTimeout(this.bind(this.opening), 1); 953 954 return true; 955 }, 956 957 /** 958 * Performs the opening of the dropdown 959 */ 960 // abstract 961 opening: function() { 962 var cid = this.containerId, selector = this.containerSelector, 963 scroll = "scroll." + cid, resize = "resize." + cid; 964 965 this.container.parents().each(function() { 966 $(this).bind(scroll, function() { 967 var s2 = $(selector); 968 if (s2.length == 0) { 969 $(this).unbind(scroll); 970 } 971 s2.select2("close"); 972 }); 973 }); 974 975 $(window).bind(resize, function() { 976 var s2 = $(selector); 977 if (s2.length == 0) { 978 $(window).unbind(resize); 979 } 980 s2.select2("close"); 981 }); 982 983 this.clearDropdownAlignmentPreference(); 984 985 if (this.search.val() === " ") { this.search.val(""); } 986 987 this.container.addClass("select2-dropdown-open").addClass("select2-container-active"); 988 989 this.updateResults(true); 990 991 if(this.dropdown[0] !== this.body().children().last()[0]) { 992 this.dropdown.detach().appendTo(this.body()); 993 } 994 995 this.dropdown.show(); 996 997 this.positionDropdown(); 998 this.dropdown.addClass("select2-drop-active"); 999 1000 this.ensureHighlightVisible(); 1001 1002 this.focusSearch(); 1003 }, 1004 1005 // abstract 1006 close: function () { 1007 if (!this.opened()) return; 1008 1009 var self = this; 1010 1011 this.container.parents().each(function() { 1012 $(this).unbind("scroll." + self.containerId); 1013 }); 1014 $(window).unbind("resize." + this.containerId); 1015 1016 this.clearDropdownAlignmentPreference(); 1017 1018 this.dropdown.hide(); 1019 this.container.removeClass("select2-dropdown-open").removeClass("select2-container-active"); 1020 this.results.empty(); 1021 this.clearSearch(); 1022 1023 this.opts.element.trigger($.Event("close")); 1024 }, 1025 1026 // abstract 1027 clearSearch: function () { 1028 1029 }, 1030 1031 // abstract 1032 ensureHighlightVisible: function () { 1033 var results = this.results, children, index, child, hb, rb, y, more; 1034 1035 index = this.highlight(); 1036 1037 if (index < 0) return; 1038 1039 if (index == 0) { 1040 1041 // if the first element is highlighted scroll all the way to the top, 1042 // that way any unselectable headers above it will also be scrolled 1043 // into view 1044 1045 results.scrollTop(0); 1046 return; 1047 } 1048 1049 children = results.find(".select2-result-selectable"); 1050 1051 child = $(children[index]); 1052 1053 hb = child.offset().top + child.outerHeight(); 1054 1055 // if this is the last child lets also make sure select2-more-results is visible 1056 if (index === children.length - 1) { 1057 more = results.find("li.select2-more-results"); 1058 if (more.length > 0) { 1059 hb = more.offset().top + more.outerHeight(); 1060 } 1061 } 1062 1063 rb = results.offset().top + results.outerHeight(); 1064 if (hb > rb) { 1065 results.scrollTop(results.scrollTop() + (hb - rb)); 1066 } 1067 y = child.offset().top - results.offset().top; 1068 1069 // make sure the top of the element is visible 1070 if (y < 0) { 1071 results.scrollTop(results.scrollTop() + y); // y is negative 1072 } 1073 }, 1074 1075 // abstract 1076 moveHighlight: function (delta) { 1077 var choices = this.results.find(".select2-result-selectable"), 1078 index = this.highlight(); 1079 1080 while (index > -1 && index < choices.length) { 1081 index += delta; 1082 var choice = $(choices[index]); 1083 if (choice.hasClass("select2-result-selectable") && !choice.hasClass("select2-disabled")) { 1084 this.highlight(index); 1085 break; 1086 } 1087 } 1088 }, 1089 1090 // abstract 1091 highlight: function (index) { 1092 var choices = this.results.find(".select2-result-selectable").not(".select2-disabled"); 1093 1094 if (arguments.length === 0) { 1095 return indexOf(choices.filter(".select2-highlighted")[0], choices.get()); 1096 } 1097 1098 if (index >= choices.length) index = choices.length - 1; 1099 if (index < 0) index = 0; 1100 1101 choices.removeClass("select2-highlighted"); 1102 1103 $(choices[index]).addClass("select2-highlighted"); 1104 this.ensureHighlightVisible(); 1105 1106 }, 1107 1108 // abstract 1109 countSelectableResults: function() { 1110 return this.results.find(".select2-result-selectable").not(".select2-disabled").length; 1111 }, 1112 1113 // abstract 1114 highlightUnderEvent: function (event) { 1115 var el = $(event.target).closest(".select2-result-selectable"); 1116 if (el.length > 0 && !el.is(".select2-highlighted")) { 1117 var choices = this.results.find('.select2-result-selectable'); 1118 this.highlight(choices.index(el)); 1119 } else if (el.length == 0) { 1120 // if we are over an unselectable item remove al highlights 1121 this.results.find(".select2-highlighted").removeClass("select2-highlighted"); 1122 } 1123 }, 1124 1125 // abstract 1126 loadMoreIfNeeded: function () { 1127 var results = this.results, 1128 more = results.find("li.select2-more-results"), 1129 below, // pixels the element is below the scroll fold, below==0 is when the element is starting to be visible 1130 offset = -1, // index of first element without data 1131 page = this.resultsPage + 1, 1132 self=this, 1133 term=this.search.val(), 1134 context=this.context; 1135 1136 if (more.length === 0) return; 1137 below = more.offset().top - results.offset().top - results.height(); 1138 1139 if (below <= 0) { 1140 more.addClass("select2-active"); 1141 this.opts.query({ 1142 term: term, 1143 page: page, 1144 context: context, 1145 matcher: this.opts.matcher, 1146 callback: this.bind(function (data) { 1147 1148 // ignore a response if the select2 has been closed before it was received 1149 if (!self.opened()) return; 1150 1151 1152 self.opts.populateResults.call(this, results, data.results, {term: term, page: page, context:context}); 1153 1154 if (data.more===true) { 1155 more.detach().appendTo(results).text(self.opts.formatLoadMore(page+1)); 1156 window.setTimeout(function() { self.loadMoreIfNeeded(); }, 10); 1157 } else { 1158 more.remove(); 1159 } 1160 self.positionDropdown(); 1161 self.resultsPage = page; 1162 })}); 1163 } 1164 }, 1165 1166 /** 1167 * Default tokenizer function which does nothing 1168 */ 1169 tokenize: function() { 1170 1171 }, 1172 1173 /** 1174 * @param initial whether or not this is the call to this method right after the dropdown has been opened 1175 */ 1176 // abstract 1177 updateResults: function (initial) { 1178 var search = this.search, results = this.results, opts = this.opts, data, self=this, input; 1179 1180 // if the search is currently hidden we do not alter the results 1181 if (initial !== true && (this.showSearchInput === false || !this.opened())) { 1182 return; 1183 } 1184 1185 search.addClass("select2-active"); 1186 1187 function postRender() { 1188 results.scrollTop(0); 1189 search.removeClass("select2-active"); 1190 self.positionDropdown(); 1191 } 1192 1193 function render(html) { 1194 results.html(self.opts.escapeMarkup(html)); 1195 postRender(); 1196 } 1197 1198 if (opts.maximumSelectionSize >=1) { 1199 data = this.data(); 1200 if ($.isArray(data) && data.length >= opts.maximumSelectionSize && checkFormatter(opts.formatSelectionTooBig, "formatSelectionTooBig")) { 1201 render("<li class='select2-selection-limit'>" + opts.formatSelectionTooBig(opts.maximumSelectionSize) + "</li>"); 1202 return; 1203 } 1204 } 1205 1206 if (search.val().length < opts.minimumInputLength && checkFormatter(opts.formatInputTooShort, "formatInputTooShort")) { 1207 render("<li class='select2-no-results'>" + opts.formatInputTooShort(search.val(), opts.minimumInputLength) + "</li>"); 1208 return; 1209 } 1210 else { 1211 render("<li class='select2-searching'>" + opts.formatSearching() + "</li>"); 1212 } 1213 1214 // give the tokenizer a chance to pre-process the input 1215 input = this.tokenize(); 1216 if (input != undefined && input != null) { 1217 search.val(input); 1218 } 1219 1220 this.resultsPage = 1; 1221 opts.query({ 1222 term: search.val(), 1223 page: this.resultsPage, 1224 context: null, 1225 matcher: opts.matcher, 1226 callback: this.bind(function (data) { 1227 var def; // default choice 1228 1229 // ignore a response if the select2 has been closed before it was received 1230 if (!this.opened()) return; 1231 1232 // save context, if any 1233 this.context = (data.context===undefined) ? null : data.context; 1234 1235 // create a default choice and prepend it to the list 1236 if (this.opts.createSearchChoice && search.val() !== "") { 1237 def = this.opts.createSearchChoice.call(null, search.val(), data.results); 1238 if (def !== undefined && def !== null && self.id(def) !== undefined && self.id(def) !== null) { 1239 if ($(data.results).filter( 1240 function () { 1241 return equal(self.id(this), self.id(def)); 1242 }).length === 0) { 1243 data.results.unshift(def); 1244 } 1245 } 1246 } 1247 1248 if (data.results.length === 0 && checkFormatter(opts.formatNoMatches, "formatNoMatches")) { 1249 render("<li class='select2-no-results'>" + opts.formatNoMatches(search.val()) + "</li>"); 1250 return; 1251 } 1252 1253 results.empty(); 1254 self.opts.populateResults.call(this, results, data.results, {term: search.val(), page: this.resultsPage, context:null}); 1255 1256 if (data.more === true && checkFormatter(opts.formatLoadMore, "formatLoadMore")) { 1257 results.append("<li class='select2-more-results'>" + self.opts.escapeMarkup(opts.formatLoadMore(this.resultsPage)) + "</li>"); 1258 window.setTimeout(function() { self.loadMoreIfNeeded(); }, 10); 1259 } 1260 1261 this.postprocessResults(data, initial); 1262 1263 postRender(); 1264 })}); 1265 }, 1266 1267 // abstract 1268 cancel: function () { 1269 this.close(); 1270 }, 1271 1272 // abstract 1273 blur: function () { 1274 this.close(); 1275 this.container.removeClass("select2-container-active"); 1276 this.dropdown.removeClass("select2-drop-active"); 1277 // synonymous to .is(':focus'), which is available in jquery >= 1.6 1278 if (this.search[0] === document.activeElement) { this.search.blur(); } 1279 this.clearSearch(); 1280 this.selection.find(".select2-search-choice-focus").removeClass("select2-search-choice-focus"); 1281 }, 1282 1283 // abstract 1284 focusSearch: function () { 1285 // need to do it here as well as in timeout so it works in IE 1286 this.search.show(); 1287 this.search.focus(); 1288 1289 /* we do this in a timeout so that current event processing can complete before this code is executed. 1290 this makes sure the search field is focussed even if the current event would blur it */ 1291 window.setTimeout(this.bind(function () { 1292 // reset the value so IE places the cursor at the end of the input box 1293 this.search.show(); 1294 this.search.focus(); 1295 this.search.val(this.search.val()); 1296 }), 10); 1297 }, 1298 1299 // abstract 1300 selectHighlighted: function () { 1301 var index=this.highlight(), 1302 highlighted=this.results.find(".select2-highlighted").not(".select2-disabled"), 1303 data = highlighted.closest('.select2-result-selectable').data("select2-data"); 1304 if (data) { 1305 highlighted.addClass("select2-disabled"); 1306 this.highlight(index); 1307 this.onSelect(data); 1308 } 1309 }, 1310 1311 // abstract 1312 getPlaceholder: function () { 1313 return this.opts.element.attr("placeholder") || 1314 this.opts.element.attr("data-placeholder") || // jquery 1.4 compat 1315 this.opts.element.data("placeholder") || 1316 this.opts.placeholder; 1317 }, 1318 1319 /** 1320 * Get the desired width for the container element. This is 1321 * derived first from option `width` passed to select2, then 1322 * the inline 'style' on the original element, and finally 1323 * falls back to the jQuery calculated element width. 1324 */ 1325 // abstract 1326 initContainerWidth: function () { 1327 function resolveContainerWidth() { 1328 var style, attrs, matches, i, l; 1329 1330 if (this.opts.width === "off") { 1331 return null; 1332 } else if (this.opts.width === "element"){ 1333 return this.opts.element.outerWidth() === 0 ? 'auto' : this.opts.element.outerWidth() + 'px'; 1334 } else if (this.opts.width === "copy" || this.opts.width === "resolve") { 1335 // check if there is inline style on the element that contains width 1336 style = this.opts.element.attr('style'); 1337 if (style !== undefined) { 1338 attrs = style.split(';'); 1339 for (i = 0, l = attrs.length; i < l; i = i + 1) { 1340 matches = attrs[i].replace(/\s/g, '') 1341 .match(/width:(([-+]?([0-9]*\.)?[0-9]+)(px|em|ex|%|in|cm|mm|pt|pc))/); 1342 if (matches !== null && matches.length >= 1) 1343 return matches[1]; 1344 } 1345 } 1346 1347 if (this.opts.width === "resolve") { 1348 // next check if css('width') can resolve a width that is percent based, this is sometimes possible 1349 // when attached to input type=hidden or elements hidden via css 1350 style = this.opts.element.css('width'); 1351 if (style.indexOf("%") > 0) return style; 1352 1353 // finally, fallback on the calculated width of the element 1354 return (this.opts.element.outerWidth() === 0 ? 'auto' : this.opts.element.outerWidth() + 'px'); 1355 } 1356 1357 return null; 1358 } else if ($.isFunction(this.opts.width)) { 1359 return this.opts.width(); 1360 } else { 1361 return this.opts.width; 1362 } 1363 }; 1364 1365 var width = resolveContainerWidth.call(this); 1366 if (width !== null) { 1367 this.container.attr("style", "width: "+width); 1368 } 1369 } 1370 }); 1371 1372 SingleSelect2 = clazz(AbstractSelect2, { 1373 1374 // single 1375 1376 createContainer: function () { 1377 var container = $("<div></div>", { 1378 "class": "select2-container" 1379 }).html([ 1380 " <a href='#' onclick='return false;' class='select2-choice'>", 1381 " <span></span><abbr class='select2-search-choice-close' style='display:none;'></abbr>", 1382 " <div><b></b></div>" , 1383 "</a>", 1384 " <div class='select2-drop select2-offscreen'>" , 1385 " <div class='select2-search'>" , 1386 " <input type='text' autocomplete='off' class='select2-input'/>" , 1387 " </div>" , 1388 " <ul class='select2-results'>" , 1389 " </ul>" , 1390 "</div>"].join("")); 1391 return container; 1392 }, 1393 1394 // single 1395 opening: function () { 1396 this.search.show(); 1397 this.parent.opening.apply(this, arguments); 1398 this.dropdown.removeClass("select2-offscreen"); 1399 }, 1400 1401 // single 1402 close: function () { 1403 if (!this.opened()) return; 1404 this.parent.close.apply(this, arguments); 1405 this.dropdown.removeAttr("style").addClass("select2-offscreen").insertAfter(this.selection).show(); 1406 }, 1407 1408 // single 1409 focus: function () { 1410 this.close(); 1411 this.selection.focus(); 1412 }, 1413 1414 // single 1415 isFocused: function () { 1416 return this.selection[0] === document.activeElement; 1417 }, 1418 1419 // single 1420 cancel: function () { 1421 this.parent.cancel.apply(this, arguments); 1422 this.selection.focus(); 1423 }, 1424 1425 // single 1426 initContainer: function () { 1427 1428 var selection, 1429 container = this.container, 1430 dropdown = this.dropdown, 1431 clickingInside = false; 1432 1433 this.selection = selection = container.find(".select2-choice"); 1434 1435 this.search.bind("keydown", this.bind(function (e) { 1436 if (!this.enabled) return; 1437 1438 if (e.which === KEY.PAGE_UP || e.which === KEY.PAGE_DOWN) { 1439 // prevent the page from scrolling 1440 killEvent(e); 1441 return; 1442 } 1443 1444 if (this.opened()) { 1445 switch (e.which) { 1446 case KEY.UP: 1447 case KEY.DOWN: 1448 this.moveHighlight((e.which === KEY.UP) ? -1 : 1); 1449 killEvent(e); 1450 return; 1451 case KEY.TAB: 1452 case KEY.ENTER: 1453 this.selectHighlighted(); 1454 killEvent(e); 1455 return; 1456 case KEY.ESC: 1457 this.cancel(e); 1458 killEvent(e); 1459 return; 1460 } 1461 } else { 1462 1463 if (e.which === KEY.TAB || KEY.isControl(e) || KEY.isFunctionKey(e) || e.which === KEY.ESC) { 1464 return; 1465 } 1466 1467 if (this.opts.openOnEnter === false && e.which === KEY.ENTER) { 1468 return; 1469 } 1470 1471 this.open(); 1472 1473 if (e.which === KEY.ENTER) { 1474 // do not propagate the event otherwise we open, and propagate enter which closes 1475 return; 1476 } 1477 } 1478 })); 1479 1480 this.search.bind("focus", this.bind(function() { 1481 this.selection.attr("tabIndex", "-1"); 1482 })); 1483 this.search.bind("blur", this.bind(function() { 1484 if (!this.opened()) this.container.removeClass("select2-container-active"); 1485 window.setTimeout(this.bind(function() { this.selection.attr("tabIndex", this.opts.element.attr("tabIndex")); }), 10); 1486 })); 1487 1488 selection.bind("mousedown", this.bind(function (e) { 1489 clickingInside = true; 1490 1491 if (this.opened()) { 1492 this.close(); 1493 this.selection.focus(); 1494 } else if (this.enabled) { 1495 this.open(); 1496 } 1497 1498 clickingInside = false; 1499 })); 1500 1501 dropdown.bind("mousedown", this.bind(function() { this.search.focus(); })); 1502 1503 selection.bind("focus", this.bind(function() { 1504 this.container.addClass("select2-container-active"); 1505 // hide the search so the tab key does not focus on it 1506 this.search.attr("tabIndex", "-1"); 1507 })); 1508 1509 selection.bind("blur", this.bind(function() { 1510 if (!this.opened()) { 1511 this.container.removeClass("select2-container-active"); 1512 } 1513 window.setTimeout(this.bind(function() { this.search.attr("tabIndex", this.opts.element.attr("tabIndex")); }), 10); 1514 })); 1515 1516 selection.bind("keydown", this.bind(function(e) { 1517 if (!this.enabled) return; 1518 1519 if (e.which === KEY.PAGE_UP || e.which === KEY.PAGE_DOWN) { 1520 // prevent the page from scrolling 1521 killEvent(e); 1522 return; 1523 } 1524 1525 if (e.which === KEY.TAB || KEY.isControl(e) || KEY.isFunctionKey(e) 1526 || e.which === KEY.ESC) { 1527 return; 1528 } 1529 1530 if (this.opts.openOnEnter === false && e.which === KEY.ENTER) { 1531 return; 1532 } 1533 1534 if (e.which == KEY.DELETE) { 1535 if (this.opts.allowClear) { 1536 this.clear(); 1537 } 1538 return; 1539 } 1540 1541 this.open(); 1542 1543 if (e.which === KEY.ENTER) { 1544 // do not propagate the event otherwise we open, and propagate enter which closes 1545 killEvent(e); 1546 return; 1547 } 1548 1549 // do not set the search input value for non-alpha-numeric keys 1550 // otherwise pressing down results in a '(' being set in the search field 1551 if (e.which < 48 ) { // '0' == 48 1552 killEvent(e); 1553 return; 1554 } 1555 1556 var keyWritten = String.fromCharCode(e.which).toLowerCase(); 1557 1558 if (e.shiftKey) { 1559 keyWritten = keyWritten.toUpperCase(); 1560 } 1561 1562 // focus the field before calling val so the cursor ends up after the value instead of before 1563 this.search.focus(); 1564 this.search.val(keyWritten); 1565 1566 // prevent event propagation so it doesnt replay on the now focussed search field and result in double key entry 1567 killEvent(e); 1568 })); 1569 1570 selection.delegate("abbr", "mousedown", this.bind(function (e) { 1571 if (!this.enabled) return; 1572 this.clear(); 1573 killEvent(e); 1574 this.close(); 1575 this.triggerChange(); 1576 this.selection.focus(); 1577 })); 1578 1579 this.setPlaceholder(); 1580 1581 this.search.bind("focus", this.bind(function() { 1582 this.container.addClass("select2-container-active"); 1583 })); 1584 }, 1585 1586 // single 1587 clear: function() { 1588 this.opts.element.val(""); 1589 this.selection.find("span").empty(); 1590 this.selection.removeData("select2-data"); 1591 this.setPlaceholder(); 1592 }, 1593 1594 /** 1595 * Sets selection based on source element's value 1596 */ 1597 // single 1598 initSelection: function () { 1599 var selected; 1600 if (this.opts.element.val() === "") { 1601 this.close(); 1602 this.setPlaceholder(); 1603 } else { 1604 var self = this; 1605 this.opts.initSelection.call(null, this.opts.element, function(selected){ 1606 if (selected !== undefined && selected !== null) { 1607 self.updateSelection(selected); 1608 self.close(); 1609 self.setPlaceholder(); 1610 } 1611 }); 1612 } 1613 }, 1614 1615 // single 1616 prepareOpts: function () { 1617 var opts = this.parent.prepareOpts.apply(this, arguments); 1618 1619 if (opts.element.get(0).tagName.toLowerCase() === "select") { 1620 // install the selection initializer 1621 opts.initSelection = function (element, callback) { 1622 var selected = element.find(":selected"); 1623 // a single select box always has a value, no need to null check 'selected' 1624 if ($.isFunction(callback)) 1625 callback({id: selected.attr("value"), text: selected.text()}); 1626 }; 1627 } 1628 1629 return opts; 1630 }, 1631 1632 // single 1633 setPlaceholder: function () { 1634 var placeholder = this.getPlaceholder(); 1635 1636 if (this.opts.element.val() === "" && placeholder !== undefined) { 1637 1638 // check for a first blank option if attached to a select 1639 if (this.select && this.select.find("option:first").text() !== "") return; 1640 1641 this.selection.find("span").html(this.opts.escapeMarkup(placeholder)); 1642 1643 this.selection.addClass("select2-default"); 1644 1645 this.selection.find("abbr").hide(); 1646 } 1647 }, 1648 1649 // single 1650 postprocessResults: function (data, initial) { 1651 var selected = 0, self = this, showSearchInput = true; 1652 1653 // find the selected element in the result list 1654 1655 this.results.find(".select2-result-selectable").each2(function (i, elm) { 1656 if (equal(self.id(elm.data("select2-data")), self.opts.element.val())) { 1657 selected = i; 1658 return false; 1659 } 1660 }); 1661 1662 // and highlight it 1663 1664 this.highlight(selected); 1665 1666 // hide the search box if this is the first we got the results and there are a few of them 1667 1668 if (initial === true) { 1669 showSearchInput = this.showSearchInput = countResults(data.results) >= this.opts.minimumResultsForSearch; 1670 this.dropdown.find(".select2-search")[showSearchInput ? "removeClass" : "addClass"]("select2-search-hidden"); 1671 1672 //add "select2-with-searchbox" to the container if search box is shown 1673 $(this.dropdown, this.container)[showSearchInput ? "addClass" : "removeClass"]("select2-with-searchbox"); 1674 } 1675 1676 }, 1677 1678 // single 1679 onSelect: function (data) { 1680 var old = this.opts.element.val(); 1681 1682 this.opts.element.val(this.id(data)); 1683 this.updateSelection(data); 1684 this.close(); 1685 this.selection.focus(); 1686 1687 if (!equal(old, this.id(data))) { this.triggerChange(); } 1688 }, 1689 1690 // single 1691 updateSelection: function (data) { 1692 1693 var container=this.selection.find("span"), formatted; 1694 1695 this.selection.data("select2-data", data); 1696 1697 container.empty(); 1698 formatted=this.opts.formatSelection(data, container); 1699 if (formatted !== undefined) { 1700 container.append(this.opts.escapeMarkup(formatted)); 1701 } 1702 1703 this.selection.removeClass("select2-default"); 1704 1705 if (this.opts.allowClear && this.getPlaceholder() !== undefined) { 1706 this.selection.find("abbr").show(); 1707 } 1708 }, 1709 1710 // single 1711 val: function () { 1712 var val, data = null, self = this; 1713 1714 if (arguments.length === 0) { 1715 return this.opts.element.val(); 1716 } 1717 1718 val = arguments[0]; 1719 1720 if (this.select) { 1721 this.select 1722 .val(val) 1723 .find(":selected").each2(function (i, elm) { 1724 data = {id: elm.attr("value"), text: elm.text()}; 1725 return false; 1726 }); 1727 this.updateSelection(data); 1728 this.setPlaceholder(); 1729 } else { 1730 if (this.opts.initSelection === undefined) { 1731 throw new Error("cannot call val() if initSelection() is not defined"); 1732 } 1733 // val is an id. !val is true for [undefined,null,''] 1734 if (!val) { 1735 this.clear(); 1736 return; 1737 } 1738 this.opts.element.val(val); 1739 this.opts.initSelection(this.opts.element, function(data){ 1740 self.opts.element.val(!data ? "" : self.id(data)); 1741 self.updateSelection(data); 1742 self.setPlaceholder(); 1743 }); 1744 } 1745 }, 1746 1747 // single 1748 clearSearch: function () { 1749 this.search.val(""); 1750 }, 1751 1752 // single 1753 data: function(value) { 1754 var data; 1755 1756 if (arguments.length === 0) { 1757 data = this.selection.data("select2-data"); 1758 if (data == undefined) data = null; 1759 return data; 1760 } else { 1761 if (!value || value === "") { 1762 this.clear(); 1763 } else { 1764 this.opts.element.val(!value ? "" : this.id(value)); 1765 this.updateSelection(value); 1766 } 1767 } 1768 } 1769 }); 1770 1771 MultiSelect2 = clazz(AbstractSelect2, { 1772 1773 // multi 1774 createContainer: function () { 1775 var container = $("<div></div>", { 1776 "class": "select2-container select2-container-multi" 1777 }).html([ 1778 " <ul class='select2-choices'>", 1779 //"<li class='select2-search-choice'><span>California</span><a href="javascript:void(0)" class="select2-search-choice-close"></a></li>" , 1780 " <li class='select2-search-field'>" , 1781 " <input type='text' autocomplete='off' class='select2-input'>" , 1782 " </li>" , 1783 "</ul>" , 1784 "<div class='select2-drop select2-drop-multi' style='display:none;'>" , 1785 " <ul class='select2-results'>" , 1786 " </ul>" , 1787 "</div>"].join("")); 1788 return container; 1789 }, 1790 1791 // multi 1792 prepareOpts: function () { 1793 var opts = this.parent.prepareOpts.apply(this, arguments); 1794 1795 // TODO validate placeholder is a string if specified 1796 1797 if (opts.element.get(0).tagName.toLowerCase() === "select") { 1798 // install sthe selection initializer 1799 opts.initSelection = function (element,callback) { 1800 1801 var data = []; 1802 element.find(":selected").each2(function (i, elm) { 1803 data.push({id: elm.attr("value"), text: elm.text()}); 1804 }); 1805 1806 if ($.isFunction(callback)) 1807 callback(data); 1808 }; 1809 } 1810 1811 return opts; 1812 }, 1813 1814 // multi 1815 initContainer: function () { 1816 1817 var selector = ".select2-choices", selection; 1818 1819 this.searchContainer = this.container.find(".select2-search-field"); 1820 this.selection = selection = this.container.find(selector); 1821 1822 this.search.bind("keydown", this.bind(function (e) { 1823 if (!this.enabled) return; 1824 1825 if (e.which === KEY.BACKSPACE && this.search.val() === "") { 1826 this.close(); 1827 1828 var choices, 1829 selected = selection.find(".select2-search-choice-focus"); 1830 if (selected.length > 0) { 1831 this.unselect(selected.first()); 1832 this.search.width(10); 1833 killEvent(e); 1834 return; 1835 } 1836 1837 choices = selection.find(".select2-search-choice"); 1838 if (choices.length > 0) { 1839 choices.last().addClass("select2-search-choice-focus"); 1840 } 1841 } else { 1842 selection.find(".select2-search-choice-focus").removeClass("select2-search-choice-focus"); 1843 } 1844 1845 if (this.opened()) { 1846 switch (e.which) { 1847 case KEY.UP: 1848 case KEY.DOWN: 1849 this.moveHighlight((e.which === KEY.UP) ? -1 : 1); 1850 killEvent(e); 1851 return; 1852 case KEY.ENTER: 1853 case KEY.TAB: 1854 this.selectHighlighted(); 1855 killEvent(e); 1856 return; 1857 case KEY.ESC: 1858 this.cancel(e); 1859 killEvent(e); 1860 return; 1861 } 1862 } 1863 1864 if (e.which === KEY.TAB || KEY.isControl(e) || KEY.isFunctionKey(e) 1865 || e.which === KEY.BACKSPACE || e.which === KEY.ESC) { 1866 return; 1867 } 1868 1869 if (this.opts.openOnEnter === false && e.which === KEY.ENTER) { 1870 return; 1871 } 1872 1873 this.open(); 1874 1875 if (e.which === KEY.PAGE_UP || e.which === KEY.PAGE_DOWN) { 1876 // prevent the page from scrolling 1877 killEvent(e); 1878 } 1879 })); 1880 1881 this.search.bind("keyup", this.bind(this.resizeSearch)); 1882 1883 this.search.bind("blur", this.bind(function(e) { 1884 this.container.removeClass("select2-container-active"); 1885 this.search.removeClass("select2-focused"); 1886 this.clearSearch(); 1887 e.stopImmediatePropagation(); 1888 })); 1889 1890 this.container.delegate(selector, "mousedown", this.bind(function (e) { 1891 if (!this.enabled) return; 1892 if ($(e.target).closest(".select2-search-choice").length > 0) { 1893 // clicked inside a select2 search choice, do not open 1894 return; 1895 } 1896 this.clearPlaceholder(); 1897 this.open(); 1898 this.focusSearch(); 1899 e.preventDefault(); 1900 })); 1901 1902 this.container.delegate(selector, "focus", this.bind(function () { 1903 if (!this.enabled) return; 1904 this.container.addClass("select2-container-active"); 1905 this.dropdown.addClass("select2-drop-active"); 1906 this.clearPlaceholder(); 1907 })); 1908 1909 // set the placeholder if necessary 1910 this.clearSearch(); 1911 }, 1912 1913 // multi 1914 enable: function() { 1915 if (this.enabled) return; 1916 1917 this.parent.enable.apply(this, arguments); 1918 1919 this.search.removeAttr("disabled"); 1920 }, 1921 1922 // multi 1923 disable: function() { 1924 if (!this.enabled) return; 1925 1926 this.parent.disable.apply(this, arguments); 1927 1928 this.search.attr("disabled", true); 1929 }, 1930 1931 // multi 1932 initSelection: function () { 1933 var data; 1934 if (this.opts.element.val() === "") { 1935 this.updateSelection([]); 1936 this.close(); 1937 // set the placeholder if necessary 1938 this.clearSearch(); 1939 } 1940 if (this.select || this.opts.element.val() !== "") { 1941 var self = this; 1942 this.opts.initSelection.call(null, this.opts.element, function(data){ 1943 if (data !== undefined && data !== null) { 1944 self.updateSelection(data); 1945 self.close(); 1946 // set the placeholder if necessary 1947 self.clearSearch(); 1948 } 1949 }); 1950 } 1951 }, 1952 1953 // multi 1954 clearSearch: function () { 1955 var placeholder = this.getPlaceholder(); 1956 1957 if (placeholder !== undefined && this.getVal().length === 0 && this.search.hasClass("select2-focused") === false) { 1958 this.search.val(placeholder).addClass("select2-default"); 1959 // stretch the search box to full width of the container so as much of the placeholder is visible as possible 1960 this.resizeSearch(); 1961 } else { 1962 // we set this to " " instead of "" and later clear it on focus() because there is a firefox bug 1963 // that does not properly render the caret when the field starts out blank 1964 this.search.val(" ").width(10); 1965 } 1966 }, 1967 1968 // multi 1969 clearPlaceholder: function () { 1970 if (this.search.hasClass("select2-default")) { 1971 this.search.val("").removeClass("select2-default"); 1972 } else { 1973 // work around for the space character we set to avoid firefox caret bug 1974 if (this.search.val() === " ") this.search.val(""); 1975 } 1976 }, 1977 1978 // multi 1979 opening: function () { 1980 this.parent.opening.apply(this, arguments); 1981 1982 this.clearPlaceholder(); 1983 this.resizeSearch(); 1984 this.focusSearch(); 1985 }, 1986 1987 // multi 1988 close: function () { 1989 if (!this.opened()) return; 1990 this.parent.close.apply(this, arguments); 1991 }, 1992 1993 // multi 1994 focus: function () { 1995 this.close(); 1996 this.search.focus(); 1997 }, 1998 1999 // multi 2000 isFocused: function () { 2001 return this.search.hasClass("select2-focused"); 2002 }, 2003 2004 // multi 2005 updateSelection: function (data) { 2006 var ids = [], filtered = [], self = this; 2007 2008 // filter out duplicates 2009 $(data).each(function () { 2010 if (indexOf(self.id(this), ids) < 0) { 2011 ids.push(self.id(this)); 2012 filtered.push(this); 2013 } 2014 }); 2015 data = filtered; 2016 2017 this.selection.find(".select2-search-choice").remove(); 2018 $(data).each(function () { 2019 self.addSelectedChoice(this); 2020 }); 2021 self.postprocessResults(); 2022 }, 2023 2024 tokenize: function() { 2025 var input = this.search.val(); 2026 input = this.opts.tokenizer(input, this.data(), this.bind(this.onSelect), this.opts); 2027 if (input != null && input != undefined) { 2028 this.search.val(input); 2029 if (input.length > 0) { 2030 this.open(); 2031 } 2032 } 2033 2034 }, 2035 2036 // multi 2037 onSelect: function (data) { 2038 this.addSelectedChoice(data); 2039 if (this.select) { this.postprocessResults(); } 2040 2041 if (this.opts.closeOnSelect) { 2042 this.close(); 2043 this.search.width(10); 2044 } else { 2045 if (this.countSelectableResults()>0) { 2046 this.search.width(10); 2047 this.resizeSearch(); 2048 this.positionDropdown(); 2049 } else { 2050 // if nothing left to select close 2051 this.close(); 2052 } 2053 } 2054 2055 // since its not possible to select an element that has already been 2056 // added we do not need to check if this is a new element before firing change 2057 this.triggerChange({ added: data }); 2058 2059 this.focusSearch(); 2060 }, 2061 2062 // multi 2063 cancel: function () { 2064 this.close(); 2065 this.focusSearch(); 2066 }, 2067 2068 // multi 2069 addSelectedChoice: function (data) { 2070 var choice=$( 2071 "<li class='select2-search-choice'>" + 2072 " <div></div>" + 2073 " <a href='#' onclick='return false;' class='select2-search-choice-close' tabindex='-1'></a>" + 2074 "</li>"), 2075 id = this.id(data), 2076 val = this.getVal(), 2077 formatted; 2078 2079 formatted=this.opts.formatSelection(data, choice); 2080 choice.find("div").replaceWith("<div>"+this.opts.escapeMarkup(formatted)+"</div>"); 2081 choice.find(".select2-search-choice-close") 2082 .bind("mousedown", killEvent) 2083 .bind("click dblclick", this.bind(function (e) { 2084 if (!this.enabled) return; 2085 2086 $(e.target).closest(".select2-search-choice").fadeOut('fast', this.bind(function(){ 2087 this.unselect($(e.target)); 2088 this.selection.find(".select2-search-choice-focus").removeClass("select2-search-choice-focus"); 2089 this.close(); 2090 this.focusSearch(); 2091 })).dequeue(); 2092 killEvent(e); 2093 })).bind("focus", this.bind(function () { 2094 if (!this.enabled) return; 2095 this.container.addClass("select2-container-active"); 2096 this.dropdown.addClass("select2-drop-active"); 2097 })); 2098 2099 choice.data("select2-data", data); 2100 choice.insertBefore(this.searchContainer); 2101 2102 val.push(id); 2103 this.setVal(val); 2104 }, 2105 2106 // multi 2107 unselect: function (selected) { 2108 var val = this.getVal(), 2109 data, 2110 index; 2111 2112 selected = selected.closest(".select2-search-choice"); 2113 2114 if (selected.length === 0) { 2115 throw "Invalid argument: " + selected + ". Must be .select2-search-choice"; 2116 } 2117 2118 data = selected.data("select2-data"); 2119 2120 index = indexOf(this.id(data), val); 2121 2122 if (index >= 0) { 2123 val.splice(index, 1); 2124 this.setVal(val); 2125 if (this.select) this.postprocessResults(); 2126 } 2127 selected.remove(); 2128 this.triggerChange({ removed: data }); 2129 }, 2130 2131 // multi 2132 postprocessResults: function () { 2133 var val = this.getVal(), 2134 choices = this.results.find(".select2-result-selectable"), 2135 compound = this.results.find(".select2-result-with-children"), 2136 self = this; 2137 2138 choices.each2(function (i, choice) { 2139 var id = self.id(choice.data("select2-data")); 2140 if (indexOf(id, val) >= 0) { 2141 choice.addClass("select2-disabled").removeClass("select2-result-selectable"); 2142 } else { 2143 choice.removeClass("select2-disabled").addClass("select2-result-selectable"); 2144 } 2145 }); 2146 2147 compound.each2(function(i, e) { 2148 if (e.find(".select2-result-selectable").length==0) { 2149 e.addClass("select2-disabled"); 2150 } else { 2151 e.removeClass("select2-disabled"); 2152 } 2153 }); 2154 2155 choices.each2(function (i, choice) { 2156 if (!choice.hasClass("select2-disabled") && choice.hasClass("select2-result-selectable")) { 2157 self.highlight(0); 2158 return false; 2159 } 2160 }); 2161 2162 }, 2163 2164 // multi 2165 resizeSearch: function () { 2166 2167 var minimumWidth, left, maxWidth, containerLeft, searchWidth, 2168 sideBorderPadding = getSideBorderPadding(this.search); 2169 2170 minimumWidth = measureTextWidth(this.search) + 10; 2171 2172 left = this.search.offset().left; 2173 2174 maxWidth = this.selection.width(); 2175 containerLeft = this.selection.offset().left; 2176 2177 searchWidth = maxWidth - (left - containerLeft) - sideBorderPadding; 2178 if (searchWidth < minimumWidth) { 2179 searchWidth = maxWidth - sideBorderPadding; 2180 } 2181 2182 if (searchWidth < 40) { 2183 searchWidth = maxWidth - sideBorderPadding; 2184 } 2185 this.search.width(searchWidth); 2186 }, 2187 2188 // multi 2189 getVal: function () { 2190 var val; 2191 if (this.select) { 2192 val = this.select.val(); 2193 return val === null ? [] : val; 2194 } else { 2195 val = this.opts.element.val(); 2196 return splitVal(val, this.opts.separator); 2197 } 2198 }, 2199 2200 // multi 2201 setVal: function (val) { 2202 var unique; 2203 if (this.select) { 2204 this.select.val(val); 2205 } else { 2206 unique = []; 2207 // filter out duplicates 2208 $(val).each(function () { 2209 if (indexOf(this, unique) < 0) unique.push(this); 2210 }); 2211 this.opts.element.val(unique.length === 0 ? "" : unique.join(this.opts.separator)); 2212 } 2213 }, 2214 2215 // multi 2216 val: function () { 2217 var val, data = [], self=this; 2218 2219 if (arguments.length === 0) { 2220 return this.getVal(); 2221 } 2222 2223 val = arguments[0]; 2224 2225 if (!val) { 2226 this.opts.element.val(""); 2227 this.updateSelection([]); 2228 this.clearSearch(); 2229 return; 2230 } 2231 2232 // val is a list of ids 2233 this.setVal(val); 2234 2235 if (this.select) { 2236 this.select.find(":selected").each(function () { 2237 data.push({id: $(this).attr("value"), text: $(this).text()}); 2238 }); 2239 this.updateSelection(data); 2240 } else { 2241 if (this.opts.initSelection === undefined) { 2242 throw new Error("val() cannot be called if initSelection() is not defined") 2243 } 2244 2245 this.opts.initSelection(this.opts.element, function(data){ 2246 var ids=$(data).map(self.id); 2247 self.setVal(ids); 2248 self.updateSelection(data); 2249 self.clearSearch(); 2250 }); 2251 } 2252 this.clearSearch(); 2253 }, 2254 2255 // multi 2256 onSortStart: function() { 2257 if (this.select) { 2258 throw new Error("Sorting of elements is not supported when attached to <select>. Attach to <input type='hidden'/> instead."); 2259 } 2260 2261 // collapse search field into 0 width so its container can be collapsed as well 2262 this.search.width(0); 2263 // hide the container 2264 this.searchContainer.hide(); 2265 }, 2266 2267 // multi 2268 onSortEnd:function() { 2269 2270 var val=[], self=this; 2271 2272 // show search and move it to the end of the list 2273 this.searchContainer.show(); 2274 // make sure the search container is the last item in the list 2275 this.searchContainer.appendTo(this.searchContainer.parent()); 2276 // since we collapsed the width in dragStarted, we resize it here 2277 this.resizeSearch(); 2278 2279 // update selection 2280 2281 this.selection.find(".select2-search-choice").each(function() { 2282 val.push(self.opts.id($(this).data("select2-data"))); 2283 }); 2284 this.setVal(val); 2285 this.triggerChange(); 2286 }, 2287 2288 // multi 2289 data: function(values) { 2290 var self=this, ids; 2291 if (arguments.length === 0) { 2292 return this.selection 2293 .find(".select2-search-choice") 2294 .map(function() { return $(this).data("select2-data"); }) 2295 .get(); 2296 } else { 2297 if (!values) { values = []; } 2298 ids = $.map(values, function(e) { return self.opts.id(e)}); 2299 this.setVal(ids); 2300 this.updateSelection(values); 2301 this.clearSearch(); 2302 } 2303 } 2304 }); 2305 2306 $.fn.select2 = function () { 2307 2308 var args = Array.prototype.slice.call(arguments, 0), 2309 opts, 2310 select2, 2311 value, multiple, allowedMethods = ["val", "destroy", "opened", "open", "close", "focus", "isFocused", "container", "onSortStart", "onSortEnd", "enable", "disable", "positionDropdown", "data"]; 2312 2313 this.each(function () { 2314 if (args.length === 0 || typeof(args[0]) === "object") { 2315 opts = args.length === 0 ? {} : $.extend({}, args[0]); 2316 opts.element = $(this); 2317 2318 if (opts.element.get(0).tagName.toLowerCase() === "select") { 2319 multiple = opts.element.attr("multiple"); 2320 } else { 2321 multiple = opts.multiple || false; 2322 if ("tags" in opts) {opts.multiple = multiple = true;} 2323 } 2324 2325 select2 = multiple ? new MultiSelect2() : new SingleSelect2(); 2326 select2.init(opts); 2327 } else if (typeof(args[0]) === "string") { 2328 2329 if (indexOf(args[0], allowedMethods) < 0) { 2330 throw "Unknown method: " + args[0]; 2331 } 2332 2333 value = undefined; 2334 select2 = $(this).data("select2"); 2335 if (select2 === undefined) return; 2336 if (args[0] === "container") { 2337 value=select2.container; 2338 } else { 2339 value = select2[args[0]].apply(select2, args.slice(1)); 2340 } 2341 if (value !== undefined) {return false;} 2342 } else { 2343 throw "Invalid arguments to select2 plugin: " + args; 2344 } 2345 }); 2346 return (value === undefined) ? this : value; 2347 }; 2348 2349 // plugin defaults, accessible to users 2350 $.fn.select2.defaults = { 2351 width: "copy", 2352 closeOnSelect: true, 2353 openOnEnter: true, 2354 containerCss: {}, 2355 dropdownCss: {}, 2356 containerCssClass: "", 2357 dropdownCssClass: "", 2358 formatResult: function(result, container, query) { 2359 var markup=[]; 2360 markMatch(result.text, query.term, markup); 2361 return markup.join(""); 2362 }, 2363 formatSelection: function (data, container) { 2364 return data ? data.text : undefined; 2365 }, 2366 formatResultCssClass: function(data) {return undefined;}, 2367 formatNoMatches: function () { return "No matches found"; }, 2368 formatInputTooShort: function (input, min) { return "Please enter " + (min - input.length) + " more characters"; }, 2369 formatSelectionTooBig: function (limit) { return "You can only select " + limit + " item" + (limit == 1 ? "" : "s"); }, 2370 formatLoadMore: function (pageNumber) { return "Loading more results..."; }, 2371 formatSearching: function () { return "Searching..."; }, 2372 minimumResultsForSearch: 0, 2373 minimumInputLength: 0, 2374 maximumSelectionSize: 0, 2375 id: function (e) { return e.id; }, 2376 matcher: function(term, text) { 2377 return text.toUpperCase().indexOf(term.toUpperCase()) >= 0; 2378 }, 2379 separator: ",", 2380 tokenSeparators: [], 2381 tokenizer: defaultTokenizer, 2382 escapeMarkup: function (markup) { 2383 if (markup && typeof(markup) === "string") { 2384 return markup.replace(/&/g, "&"); 2385 } 2386 return markup; 2387 }, 2388 blurOnChange: false 2389 }; 2390 2391 // exports 2392 window.Select2 = { 2393 query: { 2394 ajax: ajax, 2395 local: local, 2396 tags: tags 2397 }, util: { 2398 debounce: debounce, 2399 markMatch: markMatch 2400 }, "class": { 2401 "abstract": AbstractSelect2, 2402 "single": SingleSelect2, 2403 "multi": MultiSelect2 2404 } 2405 }; 2406 2407 }(jQuery));
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Fri Nov 28 20:08:37 2014 | Cross-referenced by PHPXref 0.7.1 |