categoryz3_forms 0.0.1

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