[ Index ]

PHP Cross Reference of vtigercrm-6.1.0

title

Body

[close]

/libraries/jquery/select2/ -> select2.js (source)

   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, "&amp;");
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));


Generated: Fri Nov 28 20:08:37 2014 Cross-referenced by PHPXref 0.7.1