selectize-rails 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +18 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +53 -0
- data/Rakefile +1 -0
- data/lib/selectize-rails.rb +12 -0
- data/lib/selectize-rails/engine.rb +5 -0
- data/lib/selectize-rails/railtie.rb +5 -0
- data/lib/selectize-rails/version.rb +5 -0
- data/selectize-rails.gemspec +23 -0
- data/vendor/assets/javascripts/selectize.js +1823 -0
- data/vendor/assets/stylesheets/selectize.css +227 -0
- metadata +84 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 5f5f5589d1ad2624b7e8dd134e6da8febf3d5e34
|
4
|
+
data.tar.gz: 59832eaa0ba270ff1df87a7e4d46ac97c9943d93
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 193daee9a81ac47beb089eb6bfa3a314505c0113f5bd92c14495c80b5c038fcc3a4654c40546e1c504e12686e9f0751f2d3158fa50c39be100031edd06cd0650
|
7
|
+
data.tar.gz: 3979aa2049b5d43e9569a142db01c2c453df4fef9544598b857dc286e1a1396a14bd989458e048af25647f39a38030591baf6e3c90b457291792bb692de94673
|
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2013 Manuel van Rijn
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
# selectize-rails [](http://badge.fury.io/rb/selectize-rails)
|
2
|
+
|
3
|
+
selectize-rails provides the [selectize.js](http://brianreavis.github.io/selectize.js/)
|
4
|
+
plugin as a Rails engine to use it within the asset pipeline.
|
5
|
+
|
6
|
+
## Installation
|
7
|
+
|
8
|
+
Add this to your Gemfile:
|
9
|
+
|
10
|
+
```ruby
|
11
|
+
gem "selectize-rails"
|
12
|
+
```
|
13
|
+
|
14
|
+
and run `bundle install`.
|
15
|
+
|
16
|
+
## Usage
|
17
|
+
|
18
|
+
In your `application.js`, include the following:
|
19
|
+
|
20
|
+
```js
|
21
|
+
//= require selectize
|
22
|
+
```
|
23
|
+
|
24
|
+
In your `application.css`, include the following:
|
25
|
+
|
26
|
+
```css
|
27
|
+
*= require selectize
|
28
|
+
```
|
29
|
+
|
30
|
+
## Examples
|
31
|
+
|
32
|
+
See the [demo page of Brian Reavis](http://brianreavis.github.io/selectize.js/) for examples how to use the plugin
|
33
|
+
|
34
|
+
## Changes
|
35
|
+
|
36
|
+
| Version | Notes |
|
37
|
+
|---------+---------------------------------------------------------------------------|
|
38
|
+
| 0.1.0 | Initial release with plugin version 0.1.5 |
|
39
|
+
|
40
|
+
## License
|
41
|
+
|
42
|
+
* The [selectize.js](http://brianreavis.github.io/selectize.js/) plugin is licensed under the
|
43
|
+
[Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0)
|
44
|
+
* The [selectize-rails](https://github.com/manuelvanrijn/selectize-rails) project is
|
45
|
+
licensed under the [MIT License](http://opensource.org/licenses/mit-license.html)
|
46
|
+
|
47
|
+
## Contributing
|
48
|
+
|
49
|
+
1. Fork it
|
50
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
51
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
52
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
53
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'selectize-rails/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "selectize-rails"
|
8
|
+
spec.version = Selectize::Rails::VERSION
|
9
|
+
spec.authors = ["Manuel van Rijn"]
|
10
|
+
spec.email = ["manuel@manuelles.nl"]
|
11
|
+
spec.description = %q{A small gem for putting selectize.js into the Rails asset pipeline}
|
12
|
+
spec.summary = %q{an asset gemification of the selectize.js plugin}
|
13
|
+
spec.homepage = "https://github.com/manuelvanrijn/selectize-rails"
|
14
|
+
spec.license = "MIT, Apache License v2.0"
|
15
|
+
|
16
|
+
spec.files = `git ls-files`.split($/)
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_development_dependency "bundler", "~> 1.3"
|
22
|
+
spec.add_development_dependency "rake"
|
23
|
+
end
|
@@ -0,0 +1,1823 @@
|
|
1
|
+
/*! selectize.js | https://github.com/brianreavis/selectize.js | Apache License (v2) */
|
2
|
+
|
3
|
+
(function (factory) {
|
4
|
+
if (typeof exports === 'object') {
|
5
|
+
factory(require('jquery'));
|
6
|
+
} else if (typeof define === 'function' && define.amd) {
|
7
|
+
define(['jquery'], factory);
|
8
|
+
} else {
|
9
|
+
factory(jQuery);
|
10
|
+
}
|
11
|
+
}(function ($) {
|
12
|
+
"use strict";
|
13
|
+
|
14
|
+
// --- src/contrib/highlight.js ---
|
15
|
+
|
16
|
+
/**
|
17
|
+
* highlight v3 | MIT license | Johann Burkard <jb@eaio.com>
|
18
|
+
* Highlights arbitrary terms in a node.
|
19
|
+
*
|
20
|
+
* - Modified by Marshal <beatgates@gmail.com> 2011-6-24 (added regex)
|
21
|
+
* - Modified by Brian Reavis <brian@thirdroute.com> 2012-8-27 (cleanup)
|
22
|
+
*/
|
23
|
+
|
24
|
+
var highlight = function($element, pattern) {
|
25
|
+
if (typeof pattern === 'string' && !pattern.length) return;
|
26
|
+
var regex = (typeof pattern === 'string') ? new RegExp(pattern, 'i') : pattern;
|
27
|
+
|
28
|
+
var highlight = function(node) {
|
29
|
+
var skip = 0;
|
30
|
+
if (node.nodeType === 3) {
|
31
|
+
var pos = node.data.search(regex);
|
32
|
+
if (pos >= 0 && node.data.length > 0) {
|
33
|
+
var match = node.data.match(regex);
|
34
|
+
var spannode = document.createElement('span');
|
35
|
+
spannode.className = 'highlight';
|
36
|
+
var middlebit = node.splitText(pos);
|
37
|
+
var endbit = middlebit.splitText(match[0].length);
|
38
|
+
var middleclone = middlebit.cloneNode(true);
|
39
|
+
spannode.appendChild(middleclone);
|
40
|
+
middlebit.parentNode.replaceChild(spannode, middlebit);
|
41
|
+
skip = 1;
|
42
|
+
}
|
43
|
+
} else if (node.nodeType === 1 && node.childNodes && !/(script|style)/i.test(node.tagName)) {
|
44
|
+
for (var i = 0; i < node.childNodes.length; ++i) {
|
45
|
+
i += highlight(node.childNodes[i]);
|
46
|
+
}
|
47
|
+
}
|
48
|
+
return skip;
|
49
|
+
};
|
50
|
+
|
51
|
+
return $element.each(function() {
|
52
|
+
highlight(this);
|
53
|
+
});
|
54
|
+
};
|
55
|
+
|
56
|
+
var unhighlight = function($element) {
|
57
|
+
return $element.find('span.highlight').each(function() {
|
58
|
+
var parent = this.parentNode;
|
59
|
+
parent.replaceChild(parent.firstChild, parent);
|
60
|
+
parent.normalize();
|
61
|
+
}).end();
|
62
|
+
};
|
63
|
+
|
64
|
+
// --- src/constants.js ---
|
65
|
+
|
66
|
+
/**
|
67
|
+
* selectize - A highly customizable select control with autocomplete.
|
68
|
+
* Copyright (c) 2013 Brian Reavis & contributors
|
69
|
+
*
|
70
|
+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this
|
71
|
+
* file except in compliance with the License. You may obtain a copy of the License at:
|
72
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
73
|
+
*
|
74
|
+
* Unless required by applicable law or agreed to in writing, software distributed under
|
75
|
+
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
|
76
|
+
* ANY KIND, either express or implied. See the License for the specific language
|
77
|
+
* governing permissions and limitations under the License.
|
78
|
+
*
|
79
|
+
* @author Brian Reavis <brian@thirdroute.com>
|
80
|
+
*/
|
81
|
+
|
82
|
+
var IS_MAC = /Mac/.test(navigator.userAgent);
|
83
|
+
|
84
|
+
var KEY_COMMA = 188;
|
85
|
+
var KEY_RETURN = 13;
|
86
|
+
var KEY_ESC = 27;
|
87
|
+
var KEY_LEFT = 37;
|
88
|
+
var KEY_UP = 38;
|
89
|
+
var KEY_RIGHT = 39;
|
90
|
+
var KEY_DOWN = 40;
|
91
|
+
var KEY_BACKSPACE = 8;
|
92
|
+
var KEY_DELETE = 46;
|
93
|
+
var KEY_SHIFT = 16;
|
94
|
+
var KEY_CTRL = IS_MAC ? 18 : 17;
|
95
|
+
var KEY_TAB = 9;
|
96
|
+
|
97
|
+
var TAG_SELECT = 1;
|
98
|
+
var TAG_INPUT = 2;
|
99
|
+
|
100
|
+
var DIACRITICS = {
|
101
|
+
'a': '[aÀÁÂÃÄÅàáâãäå]',
|
102
|
+
'c': '[cÇç]',
|
103
|
+
'e': '[eÈÉÊËèéêë]',
|
104
|
+
'i': '[iÌÍÎÏìíîï]',
|
105
|
+
'n': '[nÑñ]',
|
106
|
+
'o': '[oÒÓÔÕÕÖØòóôõöø]',
|
107
|
+
's': '[sŠš]',
|
108
|
+
'u': '[uÙÚÛÜùúûü]',
|
109
|
+
'y': '[yŸÿý]',
|
110
|
+
'z': '[zŽž]'
|
111
|
+
};
|
112
|
+
|
113
|
+
// --- src/selectize.jquery.js ---
|
114
|
+
|
115
|
+
var defaults = {
|
116
|
+
delimiter: ',',
|
117
|
+
persist: true,
|
118
|
+
diacritics: true,
|
119
|
+
create: false,
|
120
|
+
highlight: true,
|
121
|
+
openOnFocus: true,
|
122
|
+
maxOptions: 1000,
|
123
|
+
maxItems: null,
|
124
|
+
hideSelected: null,
|
125
|
+
|
126
|
+
scrollDuration: 60,
|
127
|
+
loadThrottle: 300,
|
128
|
+
|
129
|
+
dataAttr: 'data-data',
|
130
|
+
sortField: null,
|
131
|
+
sortDirection: 'asc',
|
132
|
+
valueField: 'value',
|
133
|
+
labelField: 'text',
|
134
|
+
searchField: ['text'],
|
135
|
+
|
136
|
+
mode: null,
|
137
|
+
theme: 'default',
|
138
|
+
wrapperClass: 'selectize-control',
|
139
|
+
inputClass: 'selectize-input',
|
140
|
+
dropdownClass: 'selectize-dropdown',
|
141
|
+
|
142
|
+
load : null, // function(query, callback)
|
143
|
+
score : null, // function(search)
|
144
|
+
onChange : null, // function(value)
|
145
|
+
onItemAdd : null, // function(value, $item) { ... }
|
146
|
+
onItemRemove : null, // function(value) { ... }
|
147
|
+
onClear : null, // function() { ... }
|
148
|
+
onOptionAdd : null, // function(value, data) { ... }
|
149
|
+
onOptionRemove : null, // function(value) { ... }
|
150
|
+
onDropdownOpen : null, // function($dropdown) { ... }
|
151
|
+
onDropdownClose : null, // function($dropdown) { ... }
|
152
|
+
onType : null, // function(str) { ... }
|
153
|
+
|
154
|
+
render: {
|
155
|
+
item: null,
|
156
|
+
option: null,
|
157
|
+
option_create: null
|
158
|
+
}
|
159
|
+
};
|
160
|
+
|
161
|
+
$.fn.selectize = function (settings) {
|
162
|
+
var defaults = $.fn.selectize.defaults;
|
163
|
+
settings = settings || {};
|
164
|
+
|
165
|
+
return this.each(function() {
|
166
|
+
var instance, value, values, i, n, data, dataAttr, settings_element, tagName;
|
167
|
+
var $options, $option, $input = $(this);
|
168
|
+
|
169
|
+
tagName = $input[0].tagName.toLowerCase();
|
170
|
+
|
171
|
+
if (typeof settings === 'string') {
|
172
|
+
instance = $input.data('selectize');
|
173
|
+
instance[settings].apply(instance, Array.prototype.splice.apply(arguments, 1));
|
174
|
+
} else {
|
175
|
+
dataAttr = settings.dataAttr || defaults.dataAttr;
|
176
|
+
settings_element = {};
|
177
|
+
settings_element.placeholder = $input.attr('placeholder');
|
178
|
+
settings_element.options = {};
|
179
|
+
settings_element.items = [];
|
180
|
+
|
181
|
+
if (tagName === 'select') {
|
182
|
+
settings_element.maxItems = !!$input.attr('multiple') ? null : 1;
|
183
|
+
$options = $input.children();
|
184
|
+
for (i = 0, n = $options.length; i < n; i++) {
|
185
|
+
$option = $($options[i]);
|
186
|
+
value = $option.attr('value') || '';
|
187
|
+
if (!value.length) continue;
|
188
|
+
data = (dataAttr && $option.attr(dataAttr)) || {
|
189
|
+
'text' : $option.html(),
|
190
|
+
'value' : value
|
191
|
+
};
|
192
|
+
|
193
|
+
if (typeof data === 'string') data = JSON.parse(data);
|
194
|
+
settings_element.options[value] = data;
|
195
|
+
if ($option.is(':selected')) {
|
196
|
+
settings_element.items.push(value);
|
197
|
+
}
|
198
|
+
}
|
199
|
+
} else {
|
200
|
+
value = $.trim($input.val() || '');
|
201
|
+
if (value.length) {
|
202
|
+
values = value.split(settings.delimiter || defaults.delimiter);
|
203
|
+
for (i = 0, n = values.length; i < n; i++) {
|
204
|
+
settings_element.options[values[i]] = {
|
205
|
+
'text' : values[i],
|
206
|
+
'value' : values[i]
|
207
|
+
};
|
208
|
+
}
|
209
|
+
settings_element.items = values;
|
210
|
+
}
|
211
|
+
}
|
212
|
+
|
213
|
+
instance = new Selectize($input, $.extend(true, {}, defaults, settings_element, settings));
|
214
|
+
$input.data('selectize', instance);
|
215
|
+
$input.addClass('selectized');
|
216
|
+
}
|
217
|
+
});
|
218
|
+
};
|
219
|
+
|
220
|
+
$.fn.selectize.defaults = defaults;
|
221
|
+
|
222
|
+
// --- src/utils.js ---
|
223
|
+
|
224
|
+
var isset = function(object) {
|
225
|
+
return typeof object !== 'undefined';
|
226
|
+
};
|
227
|
+
|
228
|
+
var htmlEntities = function(str) {
|
229
|
+
return String(str).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
230
|
+
};
|
231
|
+
|
232
|
+
var quoteRegExp = function(str) {
|
233
|
+
return (str + '').replace(/([.?*+^$[\]\\(){}|-])/g, '\\$1');
|
234
|
+
};
|
235
|
+
|
236
|
+
var once = function(fn) {
|
237
|
+
var called = false;
|
238
|
+
return function() {
|
239
|
+
if (called) return;
|
240
|
+
called = true;
|
241
|
+
fn.apply(this, arguments);
|
242
|
+
};
|
243
|
+
};
|
244
|
+
|
245
|
+
var debounce = function(fn, delay) {
|
246
|
+
var timeout;
|
247
|
+
return function() {
|
248
|
+
var self = this;
|
249
|
+
var args = arguments;
|
250
|
+
window.clearTimeout(timeout);
|
251
|
+
timeout = window.setTimeout(function() {
|
252
|
+
fn.apply(self, args);
|
253
|
+
}, delay);
|
254
|
+
};
|
255
|
+
};
|
256
|
+
|
257
|
+
/**
|
258
|
+
* A workaround for http://bugs.jquery.com/ticket/6696
|
259
|
+
*
|
260
|
+
* @param {object} $parent - Parent element to listen on.
|
261
|
+
* @param {string} event - Event name.
|
262
|
+
* @param {string} selector - Descendant selector to filter by.
|
263
|
+
* @param {function} fn - Event handler.
|
264
|
+
*/
|
265
|
+
var watchChildEvent = function($parent, event, selector, fn) {
|
266
|
+
$parent.on(event, selector, function(e) {
|
267
|
+
var child = e.target;
|
268
|
+
while (child && child.parentNode !== $parent[0]) {
|
269
|
+
child = child.parentNode;
|
270
|
+
}
|
271
|
+
e.currentTarget = child;
|
272
|
+
return fn.apply(this, [e]);
|
273
|
+
});
|
274
|
+
};
|
275
|
+
|
276
|
+
var getSelection = function(input) {
|
277
|
+
var result = {};
|
278
|
+
if ('selectionStart' in input) {
|
279
|
+
result.start = input.selectionStart;
|
280
|
+
result.length = input.selectionEnd - result.start;
|
281
|
+
} else if (document.selection) {
|
282
|
+
input.focus();
|
283
|
+
var sel = document.selection.createRange();
|
284
|
+
var selLen = document.selection.createRange().text.length;
|
285
|
+
sel.moveStart('character', -input.value.length);
|
286
|
+
result.start = sel.text.length - selLen;
|
287
|
+
result.length = selLen;
|
288
|
+
}
|
289
|
+
return result;
|
290
|
+
};
|
291
|
+
|
292
|
+
var transferStyles = function($from, $to, properties) {
|
293
|
+
var styles = {};
|
294
|
+
if (properties) {
|
295
|
+
for (var i = 0; i < properties.length; i++) {
|
296
|
+
styles[properties[i]] = $from.css(properties[i]);
|
297
|
+
}
|
298
|
+
} else {
|
299
|
+
styles = $from.css();
|
300
|
+
}
|
301
|
+
$to.css(styles);
|
302
|
+
return $to;
|
303
|
+
};
|
304
|
+
|
305
|
+
var measureString = function(str, $parent) {
|
306
|
+
var $test = $('<test>').css({
|
307
|
+
position: 'absolute',
|
308
|
+
top: -99999,
|
309
|
+
left: -99999,
|
310
|
+
width: 'auto',
|
311
|
+
padding: 0,
|
312
|
+
whiteSpace: 'nowrap'
|
313
|
+
}).text(str).appendTo('body');
|
314
|
+
|
315
|
+
transferStyles($parent, $test, [
|
316
|
+
'letterSpacing',
|
317
|
+
'fontSize',
|
318
|
+
'fontFamily',
|
319
|
+
'fontWeight',
|
320
|
+
'textTransform'
|
321
|
+
]);
|
322
|
+
|
323
|
+
var width = $test.width();
|
324
|
+
$test.remove();
|
325
|
+
|
326
|
+
return width;
|
327
|
+
};
|
328
|
+
|
329
|
+
var autoGrow = function($input) {
|
330
|
+
var update = function(e) {
|
331
|
+
var value, keyCode, printable, placeholder, width;
|
332
|
+
var shift, character;
|
333
|
+
|
334
|
+
e = e || window.event;
|
335
|
+
value = $input.val();
|
336
|
+
if (e.type && e.type.toLowerCase() === 'keydown') {
|
337
|
+
keyCode = e.keyCode;
|
338
|
+
printable = (
|
339
|
+
(keyCode >= 97 && keyCode <= 122) || // a-z
|
340
|
+
(keyCode >= 65 && keyCode <= 90) || // A-Z
|
341
|
+
(keyCode >= 48 && keyCode <= 57) || // 0-9
|
342
|
+
keyCode == 32 // space
|
343
|
+
);
|
344
|
+
|
345
|
+
if (printable) {
|
346
|
+
shift = e.shiftKey;
|
347
|
+
character = String.fromCharCode(e.keyCode);
|
348
|
+
if (shift) character = character.toUpperCase();
|
349
|
+
else character = character.toLowerCase();
|
350
|
+
value += character;
|
351
|
+
}
|
352
|
+
}
|
353
|
+
|
354
|
+
placeholder = $input.attr('placeholder') || '';
|
355
|
+
if (!value.length && placeholder.length) {
|
356
|
+
value = placeholder;
|
357
|
+
}
|
358
|
+
|
359
|
+
width = measureString(value, $input) + 4;
|
360
|
+
if (width !== $input.width()) {
|
361
|
+
$input.width(width);
|
362
|
+
$input.triggerHandler('resize');
|
363
|
+
}
|
364
|
+
};
|
365
|
+
$input.on('keydown keyup update blur', update);
|
366
|
+
update({});
|
367
|
+
};
|
368
|
+
|
369
|
+
// --- src/selectize.js ---
|
370
|
+
|
371
|
+
/**
|
372
|
+
* selectize.js
|
373
|
+
* Copyright (c) 2013 Brian Reavis & contributors
|
374
|
+
*
|
375
|
+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this
|
376
|
+
* file except in compliance with the License. You may obtain a copy of the License at:
|
377
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
378
|
+
*
|
379
|
+
* Unless required by applicable law or agreed to in writing, software distributed under
|
380
|
+
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
|
381
|
+
* ANY KIND, either express or implied. See the License for the specific language
|
382
|
+
* governing permissions and limitations under the License.
|
383
|
+
*
|
384
|
+
* @author Brian Reavis <brian@thirdroute.com>
|
385
|
+
*/
|
386
|
+
|
387
|
+
var Selectize = function($input, settings) {
|
388
|
+
$input[0].selectize = this;
|
389
|
+
|
390
|
+
this.$input = $input;
|
391
|
+
this.tagType = $input[0].tagName.toLowerCase() === 'select' ? TAG_SELECT : TAG_INPUT;
|
392
|
+
this.settings = settings;
|
393
|
+
|
394
|
+
this.highlightedValue = null;
|
395
|
+
this.isOpen = false;
|
396
|
+
this.isLocked = false;
|
397
|
+
this.isFocused = false;
|
398
|
+
this.isInputFocused = false;
|
399
|
+
this.isSetup = false;
|
400
|
+
this.isShiftDown = false;
|
401
|
+
this.isCtrlDown = false;
|
402
|
+
this.ignoreFocus = false;
|
403
|
+
this.hasOptions = false;
|
404
|
+
this.currentResults = null;
|
405
|
+
this.lastValue = '';
|
406
|
+
this.caretPos = 0;
|
407
|
+
this.loading = 0;
|
408
|
+
this.loadedSearches = {};
|
409
|
+
|
410
|
+
this.$activeOption = null;
|
411
|
+
this.$activeItems = [];
|
412
|
+
|
413
|
+
this.options = {};
|
414
|
+
this.userOptions = {};
|
415
|
+
this.items = [];
|
416
|
+
this.renderCache = {};
|
417
|
+
this.onSearchChange = debounce(this.onSearchChange, this.settings.loadThrottle);
|
418
|
+
|
419
|
+
if ($.isArray(settings.options)) {
|
420
|
+
var key = settings.valueField;
|
421
|
+
for (var i = 0; i < settings.options.length; i++) {
|
422
|
+
if (settings.options[i].hasOwnProperty(key)) {
|
423
|
+
this.options[settings.options[i][key]] = settings.options[i];
|
424
|
+
}
|
425
|
+
}
|
426
|
+
} else if (typeof settings.options === 'object') {
|
427
|
+
$.extend(this.options, settings.options);
|
428
|
+
delete this.settings.options;
|
429
|
+
}
|
430
|
+
|
431
|
+
// option-dependent defaults
|
432
|
+
this.settings.mode = this.settings.mode || (this.settings.maxItems === 1 ? 'single' : 'multi');
|
433
|
+
if (typeof this.settings.hideSelected !== 'boolean') {
|
434
|
+
this.settings.hideSelected = this.settings.mode === 'multi';
|
435
|
+
}
|
436
|
+
|
437
|
+
this.setup();
|
438
|
+
};
|
439
|
+
|
440
|
+
/**
|
441
|
+
* Creates all elements and sets up event bindings.
|
442
|
+
*/
|
443
|
+
Selectize.prototype.setup = function() {
|
444
|
+
var self = this;
|
445
|
+
var $wrapper;
|
446
|
+
var $control;
|
447
|
+
var $control_input;
|
448
|
+
var $dropdown;
|
449
|
+
var inputMode;
|
450
|
+
var displayMode;
|
451
|
+
var timeout_blur;
|
452
|
+
var timeout_focus;
|
453
|
+
|
454
|
+
$wrapper = $('<div>').addClass(this.settings.theme).addClass(this.settings.wrapperClass);
|
455
|
+
$control = $('<div>').addClass(this.settings.inputClass).addClass('items').toggleClass('has-options', !$.isEmptyObject(this.options)).appendTo($wrapper);
|
456
|
+
$control_input = $('<input type="text">').appendTo($control);
|
457
|
+
$dropdown = $('<div>').addClass(this.settings.dropdownClass).hide().appendTo($wrapper);
|
458
|
+
|
459
|
+
displayMode = this.$input.css('display');
|
460
|
+
$wrapper.css({
|
461
|
+
width: this.$input[0].style.width,
|
462
|
+
display: displayMode
|
463
|
+
});
|
464
|
+
|
465
|
+
inputMode = this.settings.mode;
|
466
|
+
$wrapper.toggleClass('single', inputMode === 'single');
|
467
|
+
$wrapper.toggleClass('multi', inputMode === 'multi');
|
468
|
+
|
469
|
+
if ((this.settings.maxItems === null || this.settings.maxItems > 1) && this.tagType === TAG_SELECT) {
|
470
|
+
this.$input.attr('multiple', 'multiple');
|
471
|
+
}
|
472
|
+
|
473
|
+
if (this.settings.placeholder) {
|
474
|
+
$control_input.attr('placeholder', this.settings.placeholder);
|
475
|
+
}
|
476
|
+
|
477
|
+
this.$wrapper = $wrapper;
|
478
|
+
this.$control = $control;
|
479
|
+
this.$control_input = $control_input;
|
480
|
+
this.$dropdown = $dropdown;
|
481
|
+
|
482
|
+
$control.on('mousedown', function(e) {
|
483
|
+
if (e.currentTarget === self.$control[0]) {
|
484
|
+
$control_input.trigger('focus');
|
485
|
+
} else {
|
486
|
+
self.focus(true);
|
487
|
+
}
|
488
|
+
e.preventDefault();
|
489
|
+
});
|
490
|
+
|
491
|
+
watchChildEvent($dropdown, 'mouseenter', '*', function() { return self.onOptionHover.apply(self, arguments); });
|
492
|
+
watchChildEvent($dropdown, 'mousedown', '*', function() { return self.onOptionSelect.apply(self, arguments); });
|
493
|
+
watchChildEvent($control, 'mousedown', '*:not(input)', function() { return self.onItemSelect.apply(self, arguments); });
|
494
|
+
|
495
|
+
autoGrow($control_input);
|
496
|
+
|
497
|
+
$control_input.on({
|
498
|
+
mousedown : function(e) { e.stopPropagation(); },
|
499
|
+
keydown : function() { return self.onKeyDown.apply(self, arguments); },
|
500
|
+
keyup : function() { return self.onKeyUp.apply(self, arguments); },
|
501
|
+
keypress : function() { return self.onKeyPress.apply(self, arguments); },
|
502
|
+
resize : function() { self.positionDropdown.apply(self, []); },
|
503
|
+
blur : function() { return self.onBlur.apply(self, arguments); },
|
504
|
+
focus : function() { return self.onFocus.apply(self, arguments); }
|
505
|
+
});
|
506
|
+
|
507
|
+
$(document).on({
|
508
|
+
keydown: function(e) {
|
509
|
+
self.isCtrlDown = e[IS_MAC ? 'altKey' : 'ctrlKey'];
|
510
|
+
self.isShiftDown = e.shiftKey;
|
511
|
+
if (self.isFocused && !self.isLocked) {
|
512
|
+
var tagName = (e.target.tagName || '').toLowerCase();
|
513
|
+
if (tagName === 'input' || tagName === 'textarea') return;
|
514
|
+
if ([KEY_SHIFT, KEY_BACKSPACE, KEY_DELETE, KEY_ESC, KEY_LEFT, KEY_RIGHT, KEY_TAB].indexOf(e.keyCode) !== -1) {
|
515
|
+
return self.onKeyDown.apply(self, arguments);
|
516
|
+
}
|
517
|
+
}
|
518
|
+
},
|
519
|
+
keyup: function(e) {
|
520
|
+
if (e.keyCode === KEY_CTRL) self.isCtrlDown = false;
|
521
|
+
else if (e.keyCode === KEY_SHIFT) self.isShiftDown = false;
|
522
|
+
},
|
523
|
+
mousedown: function(e) {
|
524
|
+
if (self.isFocused && !self.isLocked) {
|
525
|
+
// prevent events on the dropdown scrollbar from causing the control to blur
|
526
|
+
if (e.target === self.$dropdown[0]) {
|
527
|
+
var ignoreFocus = self.ignoreFocus;
|
528
|
+
self.ignoreFocus = true;
|
529
|
+
window.setTimeout(function() {
|
530
|
+
self.ignoreFocus = ignoreFocus;
|
531
|
+
self.focus(false);
|
532
|
+
}, 0);
|
533
|
+
return;
|
534
|
+
}
|
535
|
+
// blur on click outside
|
536
|
+
if (!self.$control.has(e.target).length && e.target !== self.$control[0]) {
|
537
|
+
self.blur();
|
538
|
+
}
|
539
|
+
}
|
540
|
+
}
|
541
|
+
});
|
542
|
+
|
543
|
+
$(window).on({
|
544
|
+
resize: function() {
|
545
|
+
if (self.isOpen) {
|
546
|
+
self.positionDropdown.apply(self, arguments);
|
547
|
+
}
|
548
|
+
}
|
549
|
+
});
|
550
|
+
|
551
|
+
this.$input.hide().after(this.$wrapper);
|
552
|
+
|
553
|
+
if ($.isArray(this.settings.items)) {
|
554
|
+
this.setValue(this.settings.items);
|
555
|
+
delete this.settings.items;
|
556
|
+
}
|
557
|
+
|
558
|
+
this.updateOriginalInput();
|
559
|
+
this.refreshItems();
|
560
|
+
this.updatePlaceholder();
|
561
|
+
this.isSetup = true;
|
562
|
+
};
|
563
|
+
|
564
|
+
/**
|
565
|
+
* Triggers a callback defined in the user-provided settings.
|
566
|
+
* Events: onItemAdd, onOptionAdd, etc
|
567
|
+
*
|
568
|
+
* @param {string} event
|
569
|
+
*/
|
570
|
+
Selectize.prototype.trigger = function(event) {
|
571
|
+
var args;
|
572
|
+
if (typeof this.settings[event] === 'function') {
|
573
|
+
args = Array.prototype.slice.apply(arguments, [1]);
|
574
|
+
this.settings.event.apply(this, args);
|
575
|
+
}
|
576
|
+
};
|
577
|
+
|
578
|
+
/**
|
579
|
+
* Triggered on <input> keypress.
|
580
|
+
*
|
581
|
+
* @param {object} e
|
582
|
+
* @returns {boolean}
|
583
|
+
*/
|
584
|
+
Selectize.prototype.onKeyPress = function(e) {
|
585
|
+
if (this.isLocked) return;
|
586
|
+
var character = String.fromCharCode(e.keyCode || e.which);
|
587
|
+
if (this.settings.create && character === this.settings.delimiter) {
|
588
|
+
this.createItem();
|
589
|
+
e.preventDefault();
|
590
|
+
return false;
|
591
|
+
}
|
592
|
+
};
|
593
|
+
|
594
|
+
/**
|
595
|
+
* Triggered on <input> keydown.
|
596
|
+
*
|
597
|
+
* @param {object} e
|
598
|
+
* @returns {boolean}
|
599
|
+
*/
|
600
|
+
Selectize.prototype.onKeyDown = function(e) {
|
601
|
+
if (this.isLocked) return;
|
602
|
+
var isInput = e.target === this.$control_input[0];
|
603
|
+
|
604
|
+
switch (e.keyCode || e.which) {
|
605
|
+
case KEY_ESC:
|
606
|
+
this.blur();
|
607
|
+
return;
|
608
|
+
case KEY_DOWN:
|
609
|
+
if (!this.isOpen && this.hasOptions && this.isInputFocused) {
|
610
|
+
this.open();
|
611
|
+
} else if (this.$activeOption) {
|
612
|
+
var $next = this.$activeOption.next();
|
613
|
+
if ($next.length) this.setActiveOption($next, true, true);
|
614
|
+
}
|
615
|
+
e.preventDefault();
|
616
|
+
break;
|
617
|
+
case KEY_UP:
|
618
|
+
if (this.$activeOption) {
|
619
|
+
var $prev = this.$activeOption.prev();
|
620
|
+
if ($prev.length) this.setActiveOption($prev, true, true);
|
621
|
+
}
|
622
|
+
e.preventDefault();
|
623
|
+
break;
|
624
|
+
case KEY_RETURN:
|
625
|
+
if (this.$activeOption) {
|
626
|
+
this.onOptionSelect({currentTarget: this.$activeOption});
|
627
|
+
}
|
628
|
+
e.preventDefault();
|
629
|
+
break;
|
630
|
+
case KEY_LEFT:
|
631
|
+
this.advanceSelection(-1, e);
|
632
|
+
break;
|
633
|
+
case KEY_RIGHT:
|
634
|
+
this.advanceSelection(1, e);
|
635
|
+
break;
|
636
|
+
case KEY_TAB:
|
637
|
+
if (this.settings.create && $.trim(this.$control_input.val()).length) {
|
638
|
+
this.createItem();
|
639
|
+
e.preventDefault();
|
640
|
+
}
|
641
|
+
break;
|
642
|
+
case KEY_BACKSPACE:
|
643
|
+
case KEY_DELETE:
|
644
|
+
this.deleteSelection(e);
|
645
|
+
break;
|
646
|
+
default:
|
647
|
+
if (this.isFull()) {
|
648
|
+
e.preventDefault();
|
649
|
+
return;
|
650
|
+
}
|
651
|
+
}
|
652
|
+
if (!this.isFull()) {
|
653
|
+
this.focus(true);
|
654
|
+
}
|
655
|
+
};
|
656
|
+
|
657
|
+
/**
|
658
|
+
* Triggered on <input> keyup.
|
659
|
+
*
|
660
|
+
* @param {object} e
|
661
|
+
* @returns {boolean}
|
662
|
+
*/
|
663
|
+
Selectize.prototype.onKeyUp = function(e) {
|
664
|
+
if (this.isLocked) return;
|
665
|
+
var value = this.$control_input.val() || '';
|
666
|
+
if (this.lastValue !== value) {
|
667
|
+
this.lastValue = value;
|
668
|
+
this.onSearchChange(value);
|
669
|
+
this.refreshOptions();
|
670
|
+
this.trigger('onType', value);
|
671
|
+
}
|
672
|
+
};
|
673
|
+
|
674
|
+
/**
|
675
|
+
* Invokes the user-provide option provider / loader.
|
676
|
+
*
|
677
|
+
* Note: this function is debounced in the Selectize
|
678
|
+
* constructor (by `settings.loadDelay` milliseconds)
|
679
|
+
*
|
680
|
+
* @param {string} value
|
681
|
+
*/
|
682
|
+
Selectize.prototype.onSearchChange = function(value) {
|
683
|
+
if (!this.settings.load) return;
|
684
|
+
if (this.loadedSearches.hasOwnProperty(value)) return;
|
685
|
+
var self = this;
|
686
|
+
var $wrapper = this.$wrapper.addClass('loading');
|
687
|
+
|
688
|
+
this.loading++;
|
689
|
+
this.loadedSearches[value] = true;
|
690
|
+
this.settings.load.apply(this, [value, function(results) {
|
691
|
+
self.loading = Math.max(self.loading - 1, 0);
|
692
|
+
if (results && results.length) {
|
693
|
+
self.addOption(results);
|
694
|
+
self.refreshOptions(false);
|
695
|
+
if (self.isInputFocused) self.open();
|
696
|
+
}
|
697
|
+
if (!self.loading) {
|
698
|
+
$wrapper.removeClass('loading');
|
699
|
+
}
|
700
|
+
}]);
|
701
|
+
};
|
702
|
+
|
703
|
+
/**
|
704
|
+
* Triggered on <input> focus.
|
705
|
+
*
|
706
|
+
* @param {object} e
|
707
|
+
* @returns {boolean}
|
708
|
+
*/
|
709
|
+
Selectize.prototype.onFocus = function(e) {
|
710
|
+
this.showInput();
|
711
|
+
this.isInputFocused = true;
|
712
|
+
this.isFocused = true;
|
713
|
+
if (this.ignoreFocus) return;
|
714
|
+
|
715
|
+
this.setActiveItem(null);
|
716
|
+
this.$control.addClass('focus');
|
717
|
+
this.refreshOptions(!!this.settings.openOnFocus);
|
718
|
+
};
|
719
|
+
|
720
|
+
/**
|
721
|
+
* Triggered on <input> blur.
|
722
|
+
*
|
723
|
+
* @param {object} e
|
724
|
+
* @returns {boolean}
|
725
|
+
*/
|
726
|
+
Selectize.prototype.onBlur = function(e) {
|
727
|
+
this.isInputFocused = false;
|
728
|
+
if (this.ignoreFocus) return;
|
729
|
+
|
730
|
+
this.close();
|
731
|
+
this.setTextboxValue('');
|
732
|
+
this.setActiveOption(null);
|
733
|
+
this.setCaret(this.items.length, false);
|
734
|
+
if (!this.$activeItems.length) {
|
735
|
+
this.$control.removeClass('focus');
|
736
|
+
this.isFocused = false;
|
737
|
+
}
|
738
|
+
};
|
739
|
+
|
740
|
+
/**
|
741
|
+
* Triggered when the user rolls over
|
742
|
+
* an option in the autocomplete dropdown menu.
|
743
|
+
*
|
744
|
+
* @param {object} e
|
745
|
+
* @returns {boolean}
|
746
|
+
*/
|
747
|
+
Selectize.prototype.onOptionHover = function(e) {
|
748
|
+
this.setActiveOption(e.currentTarget, false);
|
749
|
+
};
|
750
|
+
|
751
|
+
/**
|
752
|
+
* Triggered when the user clicks on an option
|
753
|
+
* in the autocomplete dropdown menu.
|
754
|
+
*
|
755
|
+
* @param {object} e
|
756
|
+
* @returns {boolean}
|
757
|
+
*/
|
758
|
+
Selectize.prototype.onOptionSelect = function(e) {
|
759
|
+
var $target = $(e.currentTarget);
|
760
|
+
if ($target.hasClass('create')) {
|
761
|
+
this.createItem();
|
762
|
+
} else {
|
763
|
+
var value = $target.attr('data-value');
|
764
|
+
if (value) {
|
765
|
+
this.addItem(value);
|
766
|
+
this.setTextboxValue('');
|
767
|
+
}
|
768
|
+
}
|
769
|
+
};
|
770
|
+
|
771
|
+
/**
|
772
|
+
* Triggered when the user clicks on an item
|
773
|
+
* that has been selected.
|
774
|
+
*
|
775
|
+
* @param {object} e
|
776
|
+
* @returns {boolean}
|
777
|
+
*/
|
778
|
+
Selectize.prototype.onItemSelect = function(e) {
|
779
|
+
if (this.settings.mode === 'multi') {
|
780
|
+
this.$control_input.trigger('blur');
|
781
|
+
this.setActiveItem(e.currentTarget, e);
|
782
|
+
e.stopPropagation();
|
783
|
+
}
|
784
|
+
};
|
785
|
+
|
786
|
+
/**
|
787
|
+
* Sets the input field of the control to the specified value.
|
788
|
+
*
|
789
|
+
* @param {string} value
|
790
|
+
*/
|
791
|
+
Selectize.prototype.setTextboxValue = function(value) {
|
792
|
+
this.$control_input.val(value);
|
793
|
+
this.lastValue = value;
|
794
|
+
};
|
795
|
+
|
796
|
+
/**
|
797
|
+
* Returns the value of the control. If multiple items
|
798
|
+
* can be selected (e.g. <select multiple>), this returns
|
799
|
+
* an array. If only one item can be selected, this
|
800
|
+
* returns a string.
|
801
|
+
*
|
802
|
+
* @returns {mixed}
|
803
|
+
*/
|
804
|
+
Selectize.prototype.getValue = function() {
|
805
|
+
if (this.tagType === TAG_SELECT && this.$input.attr('multiple')) {
|
806
|
+
return this.items;
|
807
|
+
} else {
|
808
|
+
return this.items.join(this.settings.delimiter);
|
809
|
+
}
|
810
|
+
};
|
811
|
+
|
812
|
+
/**
|
813
|
+
* Resets the selected items to the given value.
|
814
|
+
*
|
815
|
+
* @param {mixed} value
|
816
|
+
*/
|
817
|
+
Selectize.prototype.setValue = function(value) {
|
818
|
+
this.clear();
|
819
|
+
var items = $.isArray(value) ? value : [value];
|
820
|
+
for (var i = 0, n = items.length; i < n; i++) {
|
821
|
+
this.addItem(items[i]);
|
822
|
+
}
|
823
|
+
};
|
824
|
+
|
825
|
+
/**
|
826
|
+
* Sets the selected item.
|
827
|
+
*
|
828
|
+
* @param {object} $item
|
829
|
+
* @param {object} e (optional)
|
830
|
+
*/
|
831
|
+
Selectize.prototype.setActiveItem = function($item, e) {
|
832
|
+
var eventName;
|
833
|
+
var i, idx, begin, end, item, swap;
|
834
|
+
var $last;
|
835
|
+
|
836
|
+
$item = $($item);
|
837
|
+
|
838
|
+
// clear the active selection
|
839
|
+
if (!$item.length) {
|
840
|
+
$(this.$activeItems).removeClass('active');
|
841
|
+
this.$activeItems = [];
|
842
|
+
this.isFocused = this.isInputFocused;
|
843
|
+
return;
|
844
|
+
}
|
845
|
+
|
846
|
+
// modify selection
|
847
|
+
eventName = e && e.type.toLowerCase();
|
848
|
+
|
849
|
+
if (eventName === 'mousedown' && this.isShiftDown && this.$activeItems.length) {
|
850
|
+
$last = this.$control.children('.active:last');
|
851
|
+
begin = Array.prototype.indexOf.apply(this.$control[0].childNodes, [$last[0]]);
|
852
|
+
end = Array.prototype.indexOf.apply(this.$control[0].childNodes, [$item[0]]);
|
853
|
+
if (begin > end) {
|
854
|
+
swap = begin;
|
855
|
+
begin = end;
|
856
|
+
end = swap;
|
857
|
+
}
|
858
|
+
for (i = begin; i <= end; i++) {
|
859
|
+
item = this.$control[0].childNodes[i];
|
860
|
+
if (this.$activeItems.indexOf(item) === -1) {
|
861
|
+
$(item).addClass('active');
|
862
|
+
this.$activeItems.push(item);
|
863
|
+
}
|
864
|
+
}
|
865
|
+
e.preventDefault();
|
866
|
+
} else if ((eventName === 'mousedown' && this.isCtrlDown) || (eventName === 'keydown' && this.isShiftDown)) {
|
867
|
+
if ($item.hasClass('active')) {
|
868
|
+
idx = this.$activeItems.indexOf($item[0]);
|
869
|
+
this.$activeItems.splice(idx, 1);
|
870
|
+
$item.removeClass('active');
|
871
|
+
} else {
|
872
|
+
this.$activeItems.push($item.addClass('active')[0]);
|
873
|
+
}
|
874
|
+
} else {
|
875
|
+
$(this.$activeItems).removeClass('active');
|
876
|
+
this.$activeItems = [$item.addClass('active')[0]];
|
877
|
+
}
|
878
|
+
|
879
|
+
this.isFocused = !!this.$activeItems.length || this.isInputFocused;
|
880
|
+
};
|
881
|
+
|
882
|
+
/**
|
883
|
+
* Sets the selected item in the dropdown menu
|
884
|
+
* of available options.
|
885
|
+
*
|
886
|
+
* @param {object} $object
|
887
|
+
* @param {boolean} scroll
|
888
|
+
* @param {boolean} animate
|
889
|
+
*/
|
890
|
+
Selectize.prototype.setActiveOption = function($option, scroll, animate) {
|
891
|
+
var height_menu, height_item, y;
|
892
|
+
var scroll_top, scroll_bottom;
|
893
|
+
|
894
|
+
if (this.$activeOption) this.$activeOption.removeClass('active');
|
895
|
+
this.$activeOption = null;
|
896
|
+
|
897
|
+
$option = $($option);
|
898
|
+
if (!$option.length) return;
|
899
|
+
|
900
|
+
this.$activeOption = $option.addClass('active');
|
901
|
+
|
902
|
+
if (scroll || !isset(scroll)) {
|
903
|
+
|
904
|
+
height_menu = this.$dropdown.height();
|
905
|
+
height_item = this.$activeOption.outerHeight(true);
|
906
|
+
scroll = this.$dropdown.scrollTop() || 0;
|
907
|
+
y = this.$activeOption.offset().top - this.$dropdown.offset().top + scroll;
|
908
|
+
scroll_top = y;
|
909
|
+
scroll_bottom = y - height_menu + height_item;
|
910
|
+
|
911
|
+
if (y + height_item > height_menu - scroll) {
|
912
|
+
this.$dropdown.stop().animate({scrollTop: scroll_bottom}, animate ? this.settings.scrollDuration : 0);
|
913
|
+
} else if (y < scroll) {
|
914
|
+
this.$dropdown.stop().animate({scrollTop: scroll_top}, animate ? this.settings.scrollDuration : 0);
|
915
|
+
}
|
916
|
+
|
917
|
+
}
|
918
|
+
};
|
919
|
+
|
920
|
+
/**
|
921
|
+
* Hides the input element out of view, while
|
922
|
+
* retaining its focus.
|
923
|
+
*/
|
924
|
+
Selectize.prototype.hideInput = function() {
|
925
|
+
this.$control_input.css({opacity: 0});
|
926
|
+
this.isInputFocused = false;
|
927
|
+
};
|
928
|
+
|
929
|
+
/**
|
930
|
+
* Restores input visibility.
|
931
|
+
*/
|
932
|
+
Selectize.prototype.showInput = function() {
|
933
|
+
this.$control_input.css({opacity: 1});
|
934
|
+
};
|
935
|
+
|
936
|
+
/**
|
937
|
+
* Gives the control focus. If "trigger" is falsy,
|
938
|
+
* focus handlers won't be fired--causing the focus
|
939
|
+
* to happen silently in the background.
|
940
|
+
*
|
941
|
+
* @param {boolean} trigger
|
942
|
+
*/
|
943
|
+
Selectize.prototype.focus = function(trigger) {
|
944
|
+
var ignoreFocus = this.ignoreFocus;
|
945
|
+
this.ignoreFocus = !trigger;
|
946
|
+
this.$control_input[0].focus();
|
947
|
+
this.ignoreFocus = ignoreFocus;
|
948
|
+
};
|
949
|
+
|
950
|
+
/**
|
951
|
+
* Forces the control out of focus.
|
952
|
+
*/
|
953
|
+
Selectize.prototype.blur = function() {
|
954
|
+
this.$control_input.trigger('blur');
|
955
|
+
this.setActiveItem(null);
|
956
|
+
};
|
957
|
+
|
958
|
+
/**
|
959
|
+
* Splits a search string into an array of
|
960
|
+
* individual regexps to be used to match results.
|
961
|
+
*
|
962
|
+
* @param {string} query
|
963
|
+
* @returns {array}
|
964
|
+
*/
|
965
|
+
Selectize.prototype.parseSearchTokens = function(query) {
|
966
|
+
query = $.trim(String(query || '').toLowerCase());
|
967
|
+
if (!query || !query.length) return [];
|
968
|
+
|
969
|
+
var i, n, regex, letter;
|
970
|
+
var tokens = [];
|
971
|
+
var words = query.split(/ +/);
|
972
|
+
|
973
|
+
for (i = 0, n = words.length; i < n; i++) {
|
974
|
+
regex = quoteRegExp(words[i]);
|
975
|
+
if (this.settings.diacritics) {
|
976
|
+
for (letter in DIACRITICS) {
|
977
|
+
if (DIACRITICS.hasOwnProperty(letter)) {
|
978
|
+
regex = regex.replace(new RegExp(letter, 'g'), DIACRITICS[letter]);
|
979
|
+
}
|
980
|
+
}
|
981
|
+
}
|
982
|
+
tokens.push({
|
983
|
+
string : words[i],
|
984
|
+
regex : new RegExp(regex, 'i')
|
985
|
+
});
|
986
|
+
}
|
987
|
+
|
988
|
+
return tokens;
|
989
|
+
};
|
990
|
+
|
991
|
+
/**
|
992
|
+
* Returns a function to be used to score individual results.
|
993
|
+
* Results will be sorted by the score (descending). Scores less
|
994
|
+
* than or equal to zero (no match) will not be included in the results.
|
995
|
+
*
|
996
|
+
* @param {object} data
|
997
|
+
* @param {object} search
|
998
|
+
* @returns {function}
|
999
|
+
*/
|
1000
|
+
Selectize.prototype.getScoreFunction = function(search) {
|
1001
|
+
var self = this;
|
1002
|
+
var tokens = search.tokens;
|
1003
|
+
|
1004
|
+
var calculateFieldScore = (function() {
|
1005
|
+
if (!tokens.length) {
|
1006
|
+
return function() { return 0; };
|
1007
|
+
} else if (tokens.length === 1) {
|
1008
|
+
return function(value) {
|
1009
|
+
var score, pos;
|
1010
|
+
|
1011
|
+
value = String(value || '').toLowerCase();
|
1012
|
+
pos = value.search(tokens[0].regex);
|
1013
|
+
if (pos === -1) return 0;
|
1014
|
+
score = tokens[0].string.length / value.length;
|
1015
|
+
if (pos === 0) score += 0.5;
|
1016
|
+
return score;
|
1017
|
+
};
|
1018
|
+
} else {
|
1019
|
+
return function(value) {
|
1020
|
+
var score, pos, i, j;
|
1021
|
+
|
1022
|
+
value = String(value || '').toLowerCase();
|
1023
|
+
score = 0;
|
1024
|
+
for (i = 0, j = tokens.length; i < j; i++) {
|
1025
|
+
pos = value.search(tokens[i].regex);
|
1026
|
+
if (pos === -1) return 0;
|
1027
|
+
if (pos === 0) score += 0.5;
|
1028
|
+
score += tokens[i].string.length / value.length;
|
1029
|
+
}
|
1030
|
+
return score / tokens.length;
|
1031
|
+
};
|
1032
|
+
}
|
1033
|
+
})();
|
1034
|
+
|
1035
|
+
var calculateScore = (function() {
|
1036
|
+
var fields = self.settings.searchField;
|
1037
|
+
if (typeof fields === 'string') {
|
1038
|
+
fields = [fields];
|
1039
|
+
}
|
1040
|
+
if (!fields || !fields.length) {
|
1041
|
+
return function() { return 0; };
|
1042
|
+
} else if (fields.length === 1) {
|
1043
|
+
var field = fields[0];
|
1044
|
+
return function(data) {
|
1045
|
+
if (!data.hasOwnProperty(field)) return 0;
|
1046
|
+
return calculateFieldScore(data[field]);
|
1047
|
+
};
|
1048
|
+
} else {
|
1049
|
+
return function(data) {
|
1050
|
+
var n = 0;
|
1051
|
+
var score = 0;
|
1052
|
+
for (var i = 0, j = fields.length; i < j; i++) {
|
1053
|
+
if (data.hasOwnProperty(fields[i])) {
|
1054
|
+
score += calculateFieldScore(data[fields[i]]);
|
1055
|
+
n++;
|
1056
|
+
}
|
1057
|
+
}
|
1058
|
+
return score / n;
|
1059
|
+
};
|
1060
|
+
}
|
1061
|
+
})();
|
1062
|
+
|
1063
|
+
return calculateScore;
|
1064
|
+
};
|
1065
|
+
|
1066
|
+
/**
|
1067
|
+
* Searches through available options and returns
|
1068
|
+
* a sorted array of matches. Includes options that
|
1069
|
+
* have already been selected.
|
1070
|
+
*
|
1071
|
+
* The `settings` parameter can contain:
|
1072
|
+
*
|
1073
|
+
* - searchField
|
1074
|
+
* - sortField
|
1075
|
+
* - sortDirection
|
1076
|
+
*
|
1077
|
+
* Returns an object containing:
|
1078
|
+
*
|
1079
|
+
* - query {string}
|
1080
|
+
* - tokens {array}
|
1081
|
+
* - total {int}
|
1082
|
+
* - items {array}
|
1083
|
+
*
|
1084
|
+
* @param {string} query
|
1085
|
+
* @param {object} settings
|
1086
|
+
* @returns {object}
|
1087
|
+
*/
|
1088
|
+
Selectize.prototype.search = function(query, settings) {
|
1089
|
+
var self = this;
|
1090
|
+
var value, score, search, calculateScore;
|
1091
|
+
|
1092
|
+
settings = settings || {};
|
1093
|
+
query = $.trim(String(query || '').toLowerCase());
|
1094
|
+
|
1095
|
+
if (query !== this.lastQuery) {
|
1096
|
+
this.lastQuery = query;
|
1097
|
+
|
1098
|
+
search = {
|
1099
|
+
query : query,
|
1100
|
+
tokens : this.parseSearchTokens(query),
|
1101
|
+
total : 0,
|
1102
|
+
items : []
|
1103
|
+
};
|
1104
|
+
|
1105
|
+
// generate result scoring function
|
1106
|
+
if (this.settings.score) {
|
1107
|
+
calculateScore = this.settings.score.apply(this, [search]);
|
1108
|
+
if (typeof calculateScore !== 'function') {
|
1109
|
+
throw new Error('Selectize "score" setting must be a function that returns a function');
|
1110
|
+
}
|
1111
|
+
} else {
|
1112
|
+
calculateScore = this.getScoreFunction(search);
|
1113
|
+
}
|
1114
|
+
|
1115
|
+
// perform search and sort
|
1116
|
+
if (query.length) {
|
1117
|
+
for (value in this.options) {
|
1118
|
+
if (this.options.hasOwnProperty(value)) {
|
1119
|
+
score = calculateScore(this.options[value]);
|
1120
|
+
if (score > 0) {
|
1121
|
+
search.items.push({
|
1122
|
+
score: score,
|
1123
|
+
value: value
|
1124
|
+
});
|
1125
|
+
}
|
1126
|
+
}
|
1127
|
+
}
|
1128
|
+
search.items.sort(function(a, b) {
|
1129
|
+
return b.score - a.score;
|
1130
|
+
});
|
1131
|
+
} else {
|
1132
|
+
for (value in this.options) {
|
1133
|
+
if (this.options.hasOwnProperty(value)) {
|
1134
|
+
search.items.push({
|
1135
|
+
score: 1,
|
1136
|
+
value: value
|
1137
|
+
});
|
1138
|
+
}
|
1139
|
+
}
|
1140
|
+
if (this.settings.sortField) {
|
1141
|
+
search.items.sort((function() {
|
1142
|
+
var field = self.settings.sortField;
|
1143
|
+
var multiplier = self.settings.sortDirection === 'desc' ? -1 : 1;
|
1144
|
+
return function(a, b) {
|
1145
|
+
a = a && String(self.options[a.value][field] || '').toLowerCase();
|
1146
|
+
b = b && String(self.options[b.value][field] || '').toLowerCase();
|
1147
|
+
if (a > b) return 1 * multiplier;
|
1148
|
+
if (b > a) return -1 * multiplier;
|
1149
|
+
return 0;
|
1150
|
+
};
|
1151
|
+
})());
|
1152
|
+
}
|
1153
|
+
}
|
1154
|
+
this.currentResults = search;
|
1155
|
+
} else {
|
1156
|
+
search = $.extend(true, {}, this.currentResults);
|
1157
|
+
}
|
1158
|
+
|
1159
|
+
// apply limits and return
|
1160
|
+
return this.prepareResults(search, settings);
|
1161
|
+
};
|
1162
|
+
|
1163
|
+
/**
|
1164
|
+
* Filters out any items that have already been selected
|
1165
|
+
* and applies search limits.
|
1166
|
+
*
|
1167
|
+
* @param {object} results
|
1168
|
+
* @param {object} settings
|
1169
|
+
* @returns {object}
|
1170
|
+
*/
|
1171
|
+
Selectize.prototype.prepareResults = function(search, settings) {
|
1172
|
+
if (this.settings.hideSelected) {
|
1173
|
+
for (var i = search.items.length - 1; i >= 0; i--) {
|
1174
|
+
if (this.items.indexOf(String(search.items[i].value)) !== -1) {
|
1175
|
+
search.items.splice(i, 1);
|
1176
|
+
}
|
1177
|
+
}
|
1178
|
+
}
|
1179
|
+
|
1180
|
+
search.total = search.items.length;
|
1181
|
+
if (typeof settings.limit === 'number') {
|
1182
|
+
search.items = search.items.slice(0, settings.limit);
|
1183
|
+
}
|
1184
|
+
|
1185
|
+
return search;
|
1186
|
+
};
|
1187
|
+
|
1188
|
+
/**
|
1189
|
+
* Refreshes the list of available options shown
|
1190
|
+
* in the autocomplete dropdown menu.
|
1191
|
+
*
|
1192
|
+
* @param {boolean} triggerDropdown
|
1193
|
+
*/
|
1194
|
+
Selectize.prototype.refreshOptions = function(triggerDropdown) {
|
1195
|
+
if (typeof triggerDropdown === 'undefined') {
|
1196
|
+
triggerDropdown = true;
|
1197
|
+
}
|
1198
|
+
|
1199
|
+
var i, n;
|
1200
|
+
var hasCreateOption;
|
1201
|
+
var query = this.$control_input.val();
|
1202
|
+
var results = this.search(query, {});
|
1203
|
+
var html = [];
|
1204
|
+
|
1205
|
+
// build markup
|
1206
|
+
n = results.items.length;
|
1207
|
+
if (typeof this.settings.maxOptions === 'number') {
|
1208
|
+
n = Math.min(n, this.settings.maxOptions);
|
1209
|
+
}
|
1210
|
+
for (i = 0; i < n; i++) {
|
1211
|
+
html.push(this.render('option', this.options[results.items[i].value]));
|
1212
|
+
}
|
1213
|
+
|
1214
|
+
this.$dropdown.html(html.join(''));
|
1215
|
+
|
1216
|
+
// highlight matching terms inline
|
1217
|
+
if (this.settings.highlight && results.query.length && results.tokens.length) {
|
1218
|
+
for (i = 0, n = results.tokens.length; i < n; i++) {
|
1219
|
+
highlight(this.$dropdown, results.tokens[i].regex);
|
1220
|
+
}
|
1221
|
+
}
|
1222
|
+
|
1223
|
+
// add "selected" class to selected options
|
1224
|
+
if (!this.settings.hideSelected) {
|
1225
|
+
for (i = 0, n = this.items.length; i < n; i++) {
|
1226
|
+
this.getOption(this.items[i]).addClass('selected');
|
1227
|
+
}
|
1228
|
+
}
|
1229
|
+
|
1230
|
+
// add create option
|
1231
|
+
hasCreateOption = this.settings.create && results.query.length;
|
1232
|
+
if (hasCreateOption) {
|
1233
|
+
this.$dropdown.prepend(this.render('option_create', {input: query}));
|
1234
|
+
}
|
1235
|
+
|
1236
|
+
// activate
|
1237
|
+
this.hasOptions = results.items.length > 0 || hasCreateOption;
|
1238
|
+
if (this.hasOptions) {
|
1239
|
+
this.setActiveOption(this.$dropdown[0].childNodes[hasCreateOption && results.items.length > 0 ? 1 : 0]);
|
1240
|
+
if (triggerDropdown && !this.isOpen) { this.open(); }
|
1241
|
+
} else {
|
1242
|
+
this.setActiveOption(null);
|
1243
|
+
if (triggerDropdown && this.isOpen) { this.close(); }
|
1244
|
+
}
|
1245
|
+
};
|
1246
|
+
|
1247
|
+
/**
|
1248
|
+
* Adds an available option. If it already exists,
|
1249
|
+
* nothing will happen. Note: this does not refresh
|
1250
|
+
* the options list dropdown (use `refreshOptions`
|
1251
|
+
* for that).
|
1252
|
+
*
|
1253
|
+
* @param {string} value
|
1254
|
+
* @param {object} data
|
1255
|
+
*/
|
1256
|
+
Selectize.prototype.addOption = function(value, data) {
|
1257
|
+
if ($.isArray(value)) {
|
1258
|
+
for (var i = 0, n = value.length; i < n; i++) {
|
1259
|
+
this.addOption(value[i][this.settings.valueField], value[i]);
|
1260
|
+
}
|
1261
|
+
return;
|
1262
|
+
}
|
1263
|
+
|
1264
|
+
if (this.options.hasOwnProperty(value)) return;
|
1265
|
+
value = String(value);
|
1266
|
+
this.userOptions[value] = true;
|
1267
|
+
this.options[value] = data;
|
1268
|
+
this.lastQuery = null;
|
1269
|
+
this.trigger('onOptionAdd', value, data);
|
1270
|
+
};
|
1271
|
+
|
1272
|
+
/**
|
1273
|
+
* Updates an option available for selection. If
|
1274
|
+
* it is visible in the selected items or options
|
1275
|
+
* dropdown, it will be re-rendered automatically.
|
1276
|
+
*
|
1277
|
+
* @param {string} value
|
1278
|
+
* @param {object} data
|
1279
|
+
*/
|
1280
|
+
Selectize.prototype.updateOption = function(value, data) {
|
1281
|
+
value = String(value);
|
1282
|
+
this.options[value] = data;
|
1283
|
+
if (isset(this.renderCache['item'])) delete this.renderCache['item'][value];
|
1284
|
+
if (isset(this.renderCache['option'])) delete this.renderCache['option'][value];
|
1285
|
+
|
1286
|
+
if (this.items.indexOf(value) !== -1) {
|
1287
|
+
var $item = this.getItem(value);
|
1288
|
+
var $item_new = $(this.render('item', data));
|
1289
|
+
if ($item.hasClass('active')) $item_new.addClass('active');
|
1290
|
+
$item.replaceWith($item_new);
|
1291
|
+
}
|
1292
|
+
|
1293
|
+
if (this.isOpen) {
|
1294
|
+
this.refreshOptions(false);
|
1295
|
+
}
|
1296
|
+
};
|
1297
|
+
|
1298
|
+
/**
|
1299
|
+
* Removes an option.
|
1300
|
+
*
|
1301
|
+
* @param {string} value
|
1302
|
+
*/
|
1303
|
+
Selectize.prototype.removeOption = function(value) {
|
1304
|
+
value = String(value);
|
1305
|
+
delete this.userOptions[value];
|
1306
|
+
delete this.options[value];
|
1307
|
+
this.lastQuery = null;
|
1308
|
+
this.trigger('onOptionRemove', value);
|
1309
|
+
};
|
1310
|
+
|
1311
|
+
/**
|
1312
|
+
* Returns the jQuery element of the option
|
1313
|
+
* matching the given value.
|
1314
|
+
*
|
1315
|
+
* @param {string} value
|
1316
|
+
* @returns {object}
|
1317
|
+
*/
|
1318
|
+
Selectize.prototype.getOption = function(value) {
|
1319
|
+
return this.$dropdown.children('[data-value="' + value.replace(/(['"])/g, '\\$1') + '"]:first');
|
1320
|
+
};
|
1321
|
+
|
1322
|
+
/**
|
1323
|
+
* Returns the jQuery element of the item
|
1324
|
+
* matching the given value.
|
1325
|
+
*
|
1326
|
+
* @param {string} value
|
1327
|
+
* @returns {object}
|
1328
|
+
*/
|
1329
|
+
Selectize.prototype.getItem = function(value) {
|
1330
|
+
var i = this.items.indexOf(value);
|
1331
|
+
if (i !== -1) {
|
1332
|
+
if (i >= this.caretPos) i++;
|
1333
|
+
var $el = $(this.$control[0].childNodes[i]);
|
1334
|
+
if ($el.attr('data-value') === value) {
|
1335
|
+
return $el;
|
1336
|
+
}
|
1337
|
+
}
|
1338
|
+
return $();
|
1339
|
+
};
|
1340
|
+
|
1341
|
+
/**
|
1342
|
+
* "Selects" an item. Adds it to the list
|
1343
|
+
* at the current caret position.
|
1344
|
+
*
|
1345
|
+
* @param {string} value
|
1346
|
+
*/
|
1347
|
+
Selectize.prototype.addItem = function(value) {
|
1348
|
+
var $item;
|
1349
|
+
var self = this;
|
1350
|
+
var inputMode = this.settings.mode;
|
1351
|
+
var isFull = this.isFull();
|
1352
|
+
value = String(value);
|
1353
|
+
|
1354
|
+
if (inputMode === 'single') this.clear();
|
1355
|
+
if (inputMode === 'multi' && isFull) return;
|
1356
|
+
if (this.items.indexOf(value) !== -1) return;
|
1357
|
+
if (!this.options.hasOwnProperty(value)) return;
|
1358
|
+
|
1359
|
+
$item = $(this.render('item', this.options[value]));
|
1360
|
+
this.items.splice(this.caretPos, 0, value);
|
1361
|
+
this.insertAtCaret($item);
|
1362
|
+
|
1363
|
+
isFull = this.isFull();
|
1364
|
+
this.$control.toggleClass('has-items', true);
|
1365
|
+
this.$control.toggleClass('full', isFull).toggleClass('not-full', !isFull);
|
1366
|
+
|
1367
|
+
if (this.isSetup) {
|
1368
|
+
// remove the option from the menu
|
1369
|
+
var options = this.$dropdown[0].childNodes;
|
1370
|
+
for (var i = 0; i < options.length; i++) {
|
1371
|
+
var $option = $(options[i]);
|
1372
|
+
if ($option.attr('data-value') === value) {
|
1373
|
+
$option.remove();
|
1374
|
+
if ($option[0] === this.$activeOption[0]) {
|
1375
|
+
this.setActiveOption(options.length ? $(options[0]).addClass('active') : null);
|
1376
|
+
}
|
1377
|
+
break;
|
1378
|
+
}
|
1379
|
+
}
|
1380
|
+
|
1381
|
+
// hide the menu if the maximum number of items have been selected or no options are left
|
1382
|
+
if (!options.length || (this.settings.maxItems !== null && this.items.length >= this.settings.maxItems)) {
|
1383
|
+
this.close();
|
1384
|
+
} else {
|
1385
|
+
this.positionDropdown();
|
1386
|
+
}
|
1387
|
+
|
1388
|
+
// restore focus to input
|
1389
|
+
if (this.isFocused) {
|
1390
|
+
window.setTimeout(function() {
|
1391
|
+
if (inputMode === 'single') {
|
1392
|
+
self.blur();
|
1393
|
+
self.focus(false);
|
1394
|
+
self.hideInput();
|
1395
|
+
} else {
|
1396
|
+
self.focus(false);
|
1397
|
+
}
|
1398
|
+
}, 0);
|
1399
|
+
}
|
1400
|
+
|
1401
|
+
this.updatePlaceholder();
|
1402
|
+
this.updateOriginalInput();
|
1403
|
+
this.trigger('onItemAdd', value, $item);
|
1404
|
+
}
|
1405
|
+
};
|
1406
|
+
|
1407
|
+
/**
|
1408
|
+
* Removes the selected item matching
|
1409
|
+
* the provided value.
|
1410
|
+
*
|
1411
|
+
* @param {string} value
|
1412
|
+
*/
|
1413
|
+
Selectize.prototype.removeItem = function(value) {
|
1414
|
+
var $item, i, idx;
|
1415
|
+
|
1416
|
+
$item = (typeof value === 'object') ? value : this.getItem(value);
|
1417
|
+
value = String($item.attr('data-value'));
|
1418
|
+
i = this.items.indexOf(value);
|
1419
|
+
|
1420
|
+
if (i !== -1) {
|
1421
|
+
$item.remove();
|
1422
|
+
if ($item.hasClass('active')) {
|
1423
|
+
idx = this.$activeItems.indexOf($item[0]);
|
1424
|
+
this.$activeItems.splice(idx, 1);
|
1425
|
+
}
|
1426
|
+
|
1427
|
+
this.items.splice(i, 1);
|
1428
|
+
this.$control.toggleClass('has-items', this.items.length > 0);
|
1429
|
+
this.$control.removeClass('full').addClass('not-full');
|
1430
|
+
this.lastQuery = null;
|
1431
|
+
if (!this.settings.persist && this.userOptions.hasOwnProperty(value)) {
|
1432
|
+
this.removeOption(value);
|
1433
|
+
}
|
1434
|
+
this.setCaret(i);
|
1435
|
+
this.positionDropdown();
|
1436
|
+
this.refreshOptions(false);
|
1437
|
+
|
1438
|
+
if (!this.hasOptions) { this.close(); }
|
1439
|
+
else if (this.isInputFocused) { this.open(); }
|
1440
|
+
|
1441
|
+
this.updatePlaceholder();
|
1442
|
+
this.updateOriginalInput();
|
1443
|
+
this.trigger('onItemRemove', value);
|
1444
|
+
}
|
1445
|
+
};
|
1446
|
+
|
1447
|
+
/**
|
1448
|
+
* Invokes the `create` method provided in the
|
1449
|
+
* selectize options that should provide the data
|
1450
|
+
* for the new item, given the user input.
|
1451
|
+
*
|
1452
|
+
* Once this completes, it will be added
|
1453
|
+
* to the item list.
|
1454
|
+
*/
|
1455
|
+
Selectize.prototype.createItem = function() {
|
1456
|
+
var self = this;
|
1457
|
+
var input = $.trim(this.$control_input.val() || '');
|
1458
|
+
var caret = this.caretPos;
|
1459
|
+
if (!input.length) return;
|
1460
|
+
this.lock();
|
1461
|
+
this.$control_input[0].blur();
|
1462
|
+
|
1463
|
+
var setup = (typeof this.settings.create === 'function') ? this.settings.create : function(input) {
|
1464
|
+
var data = {};
|
1465
|
+
data[self.settings.labelField] = input;
|
1466
|
+
data[self.settings.valueField] = input;
|
1467
|
+
return data;
|
1468
|
+
};
|
1469
|
+
|
1470
|
+
var create = once(function(data) {
|
1471
|
+
self.unlock();
|
1472
|
+
self.focus(false);
|
1473
|
+
|
1474
|
+
var value = data && data[self.settings.valueField];
|
1475
|
+
if (!value) return;
|
1476
|
+
|
1477
|
+
self.addOption(value, data);
|
1478
|
+
self.setCaret(caret, false);
|
1479
|
+
self.addItem(value);
|
1480
|
+
self.refreshOptions(false);
|
1481
|
+
self.setTextboxValue('');
|
1482
|
+
});
|
1483
|
+
|
1484
|
+
var output = setup(input, create);
|
1485
|
+
if (typeof output === 'object') {
|
1486
|
+
create(output);
|
1487
|
+
}
|
1488
|
+
};
|
1489
|
+
|
1490
|
+
/**
|
1491
|
+
* Re-renders the selected item lists.
|
1492
|
+
*/
|
1493
|
+
Selectize.prototype.refreshItems = function() {
|
1494
|
+
var isFull = this.isFull();
|
1495
|
+
this.lastQuery = null;
|
1496
|
+
this.$control.toggleClass('full', isFull).toggleClass('not-full', !isFull);
|
1497
|
+
this.$control.toggleClass('has-items', this.items.length > 0);
|
1498
|
+
|
1499
|
+
if (this.isSetup) {
|
1500
|
+
for (var i = 0; i < this.items.length; i++) {
|
1501
|
+
this.addItem(this.items);
|
1502
|
+
}
|
1503
|
+
}
|
1504
|
+
|
1505
|
+
this.updateOriginalInput();
|
1506
|
+
};
|
1507
|
+
|
1508
|
+
/**
|
1509
|
+
* Determines whether or not more items can be added
|
1510
|
+
* to the control without exceeding the user-defined maximum.
|
1511
|
+
*
|
1512
|
+
* @returns {boolean}
|
1513
|
+
*/
|
1514
|
+
Selectize.prototype.isFull = function() {
|
1515
|
+
return this.settings.maxItems !== null && this.items.length >= this.settings.maxItems;
|
1516
|
+
};
|
1517
|
+
|
1518
|
+
/**
|
1519
|
+
* Refreshes the original <select> or <input>
|
1520
|
+
* element to reflect the current state.
|
1521
|
+
*/
|
1522
|
+
Selectize.prototype.updateOriginalInput = function() {
|
1523
|
+
var i, n, options;
|
1524
|
+
|
1525
|
+
if (this.$input[0].tagName.toLowerCase() === 'select') {
|
1526
|
+
options = [];
|
1527
|
+
for (i = 0, n = this.items.length; i < n; i++) {
|
1528
|
+
options.push('<option value="' + htmlEntities(this.items[i]) + '" selected="selected"></option>');
|
1529
|
+
}
|
1530
|
+
if (!options.length && !this.$input.attr('multiple')) {
|
1531
|
+
options.push('<option value="" selected="selected"></option>');
|
1532
|
+
}
|
1533
|
+
this.$input.html(options.join(''));
|
1534
|
+
} else {
|
1535
|
+
this.$input.val(this.getValue());
|
1536
|
+
}
|
1537
|
+
|
1538
|
+
this.$input.trigger('change');
|
1539
|
+
if (this.isSetup) {
|
1540
|
+
this.trigger('onChange', this.$input.val());
|
1541
|
+
}
|
1542
|
+
};
|
1543
|
+
|
1544
|
+
/**
|
1545
|
+
* Shows/hide the input placeholder depending
|
1546
|
+
* on if there items in the list already.
|
1547
|
+
*/
|
1548
|
+
Selectize.prototype.updatePlaceholder = function() {
|
1549
|
+
if (!this.settings.placeholder) return;
|
1550
|
+
var $input = this.$control_input;
|
1551
|
+
|
1552
|
+
if (this.items.length) {
|
1553
|
+
$input.removeAttr('placeholder');
|
1554
|
+
} else {
|
1555
|
+
$input.attr('placeholder', this.settings.placeholder);
|
1556
|
+
}
|
1557
|
+
$input.triggerHandler('update');
|
1558
|
+
};
|
1559
|
+
|
1560
|
+
/**
|
1561
|
+
* Shows the autocomplete dropdown containing
|
1562
|
+
* the available options.
|
1563
|
+
*/
|
1564
|
+
Selectize.prototype.open = function() {
|
1565
|
+
if (this.isOpen || (this.settings.mode === 'multi' && this.isFull())) return;
|
1566
|
+
this.isOpen = true;
|
1567
|
+
this.positionDropdown();
|
1568
|
+
this.$control.addClass('dropdown-active');
|
1569
|
+
this.$dropdown.show();
|
1570
|
+
this.trigger('onDropdownOpen', this.$dropdown);
|
1571
|
+
};
|
1572
|
+
|
1573
|
+
/**
|
1574
|
+
* Closes the autocomplete dropdown menu.
|
1575
|
+
*/
|
1576
|
+
Selectize.prototype.close = function() {
|
1577
|
+
if (!this.isOpen) return;
|
1578
|
+
this.$dropdown.hide();
|
1579
|
+
this.$control.removeClass('dropdown-active');
|
1580
|
+
this.isOpen = false;
|
1581
|
+
this.trigger('onDropdownClose', this.$dropdown);
|
1582
|
+
};
|
1583
|
+
|
1584
|
+
/**
|
1585
|
+
* Calculates and applies the appropriate
|
1586
|
+
* position of the dropdown.
|
1587
|
+
*/
|
1588
|
+
Selectize.prototype.positionDropdown = function() {
|
1589
|
+
var $control = this.$control;
|
1590
|
+
var offset = $control.position();
|
1591
|
+
offset.top += $control.outerHeight(true);
|
1592
|
+
|
1593
|
+
this.$dropdown.css({
|
1594
|
+
width : $control.outerWidth(),
|
1595
|
+
top : offset.top,
|
1596
|
+
left : offset.left
|
1597
|
+
});
|
1598
|
+
};
|
1599
|
+
|
1600
|
+
/**
|
1601
|
+
* Resets / clears all selected items
|
1602
|
+
* from the control.
|
1603
|
+
*/
|
1604
|
+
Selectize.prototype.clear = function() {
|
1605
|
+
if (!this.items.length) return;
|
1606
|
+
this.$control.removeClass('has-items');
|
1607
|
+
this.$control.children(':not(input)').remove();
|
1608
|
+
this.items = [];
|
1609
|
+
this.setCaret(0);
|
1610
|
+
this.updatePlaceholder();
|
1611
|
+
this.updateOriginalInput();
|
1612
|
+
this.trigger('onClear');
|
1613
|
+
};
|
1614
|
+
|
1615
|
+
/**
|
1616
|
+
* A helper method for inserting an element
|
1617
|
+
* at the current caret position.
|
1618
|
+
*
|
1619
|
+
* @param {object} $el
|
1620
|
+
*/
|
1621
|
+
Selectize.prototype.insertAtCaret = function($el) {
|
1622
|
+
var caret = Math.min(this.caretPos, this.items.length);
|
1623
|
+
if (caret === 0) {
|
1624
|
+
this.$control.prepend($el);
|
1625
|
+
} else {
|
1626
|
+
$(this.$control[0].childNodes[caret]).before($el);
|
1627
|
+
}
|
1628
|
+
this.setCaret(caret + 1);
|
1629
|
+
};
|
1630
|
+
|
1631
|
+
/**
|
1632
|
+
* Removes the current selected item(s).
|
1633
|
+
*
|
1634
|
+
* @param {object} e (optional)
|
1635
|
+
*/
|
1636
|
+
Selectize.prototype.deleteSelection = function(e) {
|
1637
|
+
var i, n, direction, selection, values, caret, $tail;
|
1638
|
+
|
1639
|
+
direction = (e.keyCode === KEY_BACKSPACE) ? -1 : 1;
|
1640
|
+
selection = getSelection(this.$control_input[0]);
|
1641
|
+
if (this.$activeItems.length) {
|
1642
|
+
$tail = this.$control.children('.active:' + (direction > 0 ? 'last' : 'first'));
|
1643
|
+
caret = Array.prototype.indexOf.apply(this.$control[0].childNodes, [$tail[0]]);
|
1644
|
+
if (this.$activeItems.length > 1 && direction > 0) { caret--; }
|
1645
|
+
|
1646
|
+
values = [];
|
1647
|
+
for (i = 0, n = this.$activeItems.length; i < n; i++) {
|
1648
|
+
values.push($(this.$activeItems[i]).attr('data-value'));
|
1649
|
+
}
|
1650
|
+
while (values.length) {
|
1651
|
+
this.removeItem(values.pop());
|
1652
|
+
}
|
1653
|
+
|
1654
|
+
this.setCaret(caret);
|
1655
|
+
e.preventDefault();
|
1656
|
+
e.stopPropagation();
|
1657
|
+
} else if ((this.isInputFocused || this.settings.mode === 'single') && this.items.length) {
|
1658
|
+
if (direction < 0 && selection.start === 0 && selection.length === 0) {
|
1659
|
+
this.removeItem(this.items[this.caretPos - 1]);
|
1660
|
+
} else if (direction > 0 && selection.start === this.$control_input.val().length) {
|
1661
|
+
this.removeItem(this.items[this.caretPos]);
|
1662
|
+
}
|
1663
|
+
}
|
1664
|
+
};
|
1665
|
+
|
1666
|
+
/**
|
1667
|
+
* Selects the previous / next item (depending
|
1668
|
+
* on the `direction` argument).
|
1669
|
+
*
|
1670
|
+
* > 0 - right
|
1671
|
+
* < 0 - left
|
1672
|
+
*
|
1673
|
+
* @param {int} direction
|
1674
|
+
* @param {object} e (optional)
|
1675
|
+
*/
|
1676
|
+
Selectize.prototype.advanceSelection = function(direction, e) {
|
1677
|
+
if (direction === 0) return;
|
1678
|
+
var tail = direction > 0 ? 'last' : 'first';
|
1679
|
+
var selection = getSelection(this.$control_input[0]);
|
1680
|
+
|
1681
|
+
if (this.isInputFocused) {
|
1682
|
+
var valueLength = this.$control_input.val().length;
|
1683
|
+
var cursorAtEdge = direction < 0
|
1684
|
+
? selection.start === 0 && selection.length === 0
|
1685
|
+
: selection.start === valueLength;
|
1686
|
+
|
1687
|
+
if (cursorAtEdge && !valueLength) {
|
1688
|
+
this.advanceCaret(direction, e);
|
1689
|
+
}
|
1690
|
+
} else {
|
1691
|
+
var $tail = this.$control.children('.active:' + tail);
|
1692
|
+
if ($tail.length) {
|
1693
|
+
var idx = Array.prototype.indexOf.apply(this.$control[0].childNodes, [$tail[0]]);
|
1694
|
+
this.setCaret(direction > 0 ? idx + 1 : idx);
|
1695
|
+
}
|
1696
|
+
}
|
1697
|
+
};
|
1698
|
+
|
1699
|
+
/**
|
1700
|
+
* Moves the caret left / right.
|
1701
|
+
*
|
1702
|
+
* @param {int} direction
|
1703
|
+
* @param {object} e (optional)
|
1704
|
+
*/
|
1705
|
+
Selectize.prototype.advanceCaret = function(direction, e) {
|
1706
|
+
if (direction === 0) return;
|
1707
|
+
var fn = direction > 0 ? 'next' : 'prev';
|
1708
|
+
if (this.isShiftDown) {
|
1709
|
+
var $adj = this.$control_input[fn]();
|
1710
|
+
if ($adj.length) {
|
1711
|
+
this.blur();
|
1712
|
+
this.setActiveItem($adj);
|
1713
|
+
e && e.preventDefault();
|
1714
|
+
}
|
1715
|
+
} else {
|
1716
|
+
this.setCaret(this.caretPos + direction);
|
1717
|
+
}
|
1718
|
+
};
|
1719
|
+
|
1720
|
+
/**
|
1721
|
+
* Moves the caret to the specified index.
|
1722
|
+
*
|
1723
|
+
* @param {int} i
|
1724
|
+
* @param {boolean} focus
|
1725
|
+
*/
|
1726
|
+
Selectize.prototype.setCaret = function(i, focus) {
|
1727
|
+
if (this.settings.mode === 'single' || this.isFull()) {
|
1728
|
+
i = this.items.length;
|
1729
|
+
} else {
|
1730
|
+
i = Math.max(0, Math.min(this.items.length, i));
|
1731
|
+
}
|
1732
|
+
|
1733
|
+
this.ignoreFocus = true;
|
1734
|
+
this.$control_input.detach();
|
1735
|
+
if (i === this.items.length) {
|
1736
|
+
this.$control.append(this.$control_input);
|
1737
|
+
} else {
|
1738
|
+
this.$control_input.insertBefore(this.$control.children(':not(input)')[i]);
|
1739
|
+
}
|
1740
|
+
this.ignoreFocus = false;
|
1741
|
+
if (focus && this.isSetup) {
|
1742
|
+
this.focus(true);
|
1743
|
+
}
|
1744
|
+
|
1745
|
+
this.caretPos = i;
|
1746
|
+
};
|
1747
|
+
|
1748
|
+
/**
|
1749
|
+
* Disables user input on the control. Used while
|
1750
|
+
* items are being asynchronously created.
|
1751
|
+
*/
|
1752
|
+
Selectize.prototype.lock = function() {
|
1753
|
+
this.isLocked = true;
|
1754
|
+
this.$control.addClass('locked');
|
1755
|
+
};
|
1756
|
+
|
1757
|
+
/**
|
1758
|
+
* Re-enables user input on the control.
|
1759
|
+
*/
|
1760
|
+
Selectize.prototype.unlock = function() {
|
1761
|
+
this.isLocked = false;
|
1762
|
+
this.$control.removeClass('locked');
|
1763
|
+
};
|
1764
|
+
|
1765
|
+
/**
|
1766
|
+
* A helper method for rendering "item" and
|
1767
|
+
* "option" templates, given the data.
|
1768
|
+
*
|
1769
|
+
* @param {string} templateName
|
1770
|
+
* @param {object} data
|
1771
|
+
* @returns {string}
|
1772
|
+
*/
|
1773
|
+
Selectize.prototype.render = function(templateName, data) {
|
1774
|
+
cache = isset(cache) ? cache : true;
|
1775
|
+
|
1776
|
+
var value, label;
|
1777
|
+
var html = '';
|
1778
|
+
var cache = false;
|
1779
|
+
|
1780
|
+
if (['option', 'item'].indexOf(templateName) !== -1) {
|
1781
|
+
value = data[this.settings.valueField];
|
1782
|
+
cache = isset(value);
|
1783
|
+
}
|
1784
|
+
|
1785
|
+
if (cache) {
|
1786
|
+
if (!isset(this.renderCache[templateName])) {
|
1787
|
+
this.renderCache[templateName] = {};
|
1788
|
+
}
|
1789
|
+
if (this.renderCache[templateName].hasOwnProperty(value)) {
|
1790
|
+
return this.renderCache[templateName][value];
|
1791
|
+
}
|
1792
|
+
}
|
1793
|
+
|
1794
|
+
if (this.settings.render && typeof this.settings.render[templateName] === 'function') {
|
1795
|
+
html = this.settings.render[templateName].apply(this, [data]);
|
1796
|
+
} else {
|
1797
|
+
label = data[this.settings.labelField];
|
1798
|
+
switch (templateName) {
|
1799
|
+
case 'option':
|
1800
|
+
html = '<div class="option">' + label + '</div>';
|
1801
|
+
break;
|
1802
|
+
case 'item':
|
1803
|
+
html = '<div class="item">' + label + '</div>';
|
1804
|
+
break;
|
1805
|
+
case 'option_create':
|
1806
|
+
html = '<div class="create">Create <strong>' + htmlEntities(data.input) + '</strong>…</div>';
|
1807
|
+
break;
|
1808
|
+
}
|
1809
|
+
}
|
1810
|
+
|
1811
|
+
if (isset(value)) {
|
1812
|
+
html = html.replace(/^[\ ]*<([a-z][a-z0-9\-_]*(?:\:[a-z][a-z0-9\-_]*)?)/i, '<$1 data-value="' + value + '"');
|
1813
|
+
}
|
1814
|
+
if (cache) {
|
1815
|
+
this.renderCache[templateName][value] = html;
|
1816
|
+
}
|
1817
|
+
|
1818
|
+
return html;
|
1819
|
+
};
|
1820
|
+
|
1821
|
+
return Selectize;
|
1822
|
+
|
1823
|
+
}));
|