@drecchia/tom-select 2.5.2-virtual-scroll.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.
- package/LICENSE +202 -0
- package/README.md +104 -0
- package/dist/css/tom-select.bootstrap4.css +573 -0
- package/dist/css/tom-select.bootstrap5.css +612 -0
- package/dist/css/tom-select.css +405 -0
- package/dist/css/tom-select.default.css +497 -0
- package/dist/css/tom-select.default.css.map +1 -0
- package/dist/esm/plugins/caret_position/plugin.js +163 -0
- package/dist/esm/plugins/caret_position/plugin.js.map +1 -0
- package/dist/esm/plugins/change_listener/plugin.js +51 -0
- package/dist/esm/plugins/change_listener/plugin.js.map +1 -0
- package/dist/esm/plugins/checkbox_options/plugin.js +179 -0
- package/dist/esm/plugins/checkbox_options/plugin.js.map +1 -0
- package/dist/esm/plugins/clear_button/plugin.js +76 -0
- package/dist/esm/plugins/clear_button/plugin.js.map +1 -0
- package/dist/esm/plugins/drag_drop/plugin.js +220 -0
- package/dist/esm/plugins/drag_drop/plugin.js.map +1 -0
- package/dist/esm/plugins/dropdown_header/plugin.js +102 -0
- package/dist/esm/plugins/dropdown_header/plugin.js.map +1 -0
- package/dist/esm/plugins/dropdown_input/plugin.js +224 -0
- package/dist/esm/plugins/dropdown_input/plugin.js.map +1 -0
- package/dist/esm/plugins/input_autogrow/plugin.js +74 -0
- package/dist/esm/plugins/input_autogrow/plugin.js.map +1 -0
- package/dist/esm/plugins/local_virtual_scroll/plugin.js +305 -0
- package/dist/esm/plugins/local_virtual_scroll/plugin.js.map +1 -0
- package/dist/esm/plugins/no_active_items/plugin.js +26 -0
- package/dist/esm/plugins/no_active_items/plugin.js.map +1 -0
- package/dist/esm/plugins/no_backspace_delete/plugin.js +32 -0
- package/dist/esm/plugins/no_backspace_delete/plugin.js.map +1 -0
- package/dist/esm/plugins/optgroup_columns/plugin.js +86 -0
- package/dist/esm/plugins/optgroup_columns/plugin.js.map +1 -0
- package/dist/esm/plugins/remove_button/plugin.js +134 -0
- package/dist/esm/plugins/remove_button/plugin.js.map +1 -0
- package/dist/esm/plugins/restore_on_backspace/plugin.js +42 -0
- package/dist/esm/plugins/restore_on_backspace/plugin.js.map +1 -0
- package/dist/esm/plugins/virtual_scroll/plugin.js +272 -0
- package/dist/esm/plugins/virtual_scroll/plugin.js.map +1 -0
- package/dist/js/plugins/caret_position.js +171 -0
- package/dist/js/plugins/caret_position.js.map +1 -0
- package/dist/js/plugins/change_listener.js +59 -0
- package/dist/js/plugins/change_listener.js.map +1 -0
- package/dist/js/plugins/checkbox_options.js +187 -0
- package/dist/js/plugins/checkbox_options.js.map +1 -0
- package/dist/js/plugins/clear_button.js +84 -0
- package/dist/js/plugins/clear_button.js.map +1 -0
- package/dist/js/plugins/drag_drop.js +228 -0
- package/dist/js/plugins/drag_drop.js.map +1 -0
- package/dist/js/plugins/dropdown_header.js +110 -0
- package/dist/js/plugins/dropdown_header.js.map +1 -0
- package/dist/js/plugins/dropdown_input.js +232 -0
- package/dist/js/plugins/dropdown_input.js.map +1 -0
- package/dist/js/plugins/input_autogrow.js +82 -0
- package/dist/js/plugins/input_autogrow.js.map +1 -0
- package/dist/js/plugins/local_virtual_scroll.js +313 -0
- package/dist/js/plugins/local_virtual_scroll.js.map +1 -0
- package/dist/js/plugins/no_active_items.js +34 -0
- package/dist/js/plugins/no_active_items.js.map +1 -0
- package/dist/js/plugins/no_backspace_delete.js +40 -0
- package/dist/js/plugins/no_backspace_delete.js.map +1 -0
- package/dist/js/plugins/optgroup_columns.js +94 -0
- package/dist/js/plugins/optgroup_columns.js.map +1 -0
- package/dist/js/plugins/remove_button.js +142 -0
- package/dist/js/plugins/remove_button.js.map +1 -0
- package/dist/js/plugins/restore_on_backspace.js +50 -0
- package/dist/js/plugins/restore_on_backspace.js.map +1 -0
- package/dist/js/plugins/virtual_scroll.js +280 -0
- package/dist/js/plugins/virtual_scroll.js.map +1 -0
- package/dist/js/tom-select.base.js +4167 -0
- package/dist/js/tom-select.base.js.map +1 -0
- package/dist/js/tom-select.base.min.js +373 -0
- package/dist/js/tom-select.base.min.js.map +1 -0
- package/dist/js/tom-select.complete.js +5364 -0
- package/dist/js/tom-select.complete.js.map +1 -0
- package/dist/js/tom-select.complete.min.js +489 -0
- package/dist/js/tom-select.complete.min.js.map +1 -0
- package/dist/js/tom-select.popular.js +4436 -0
- package/dist/js/tom-select.popular.js.map +1 -0
- package/dist/js/tom-select.popular.min.js +396 -0
- package/dist/js/tom-select.popular.min.js.map +1 -0
- package/dist/types/constants.d.ts +12 -0
- package/dist/types/contrib/highlight.d.ts +13 -0
- package/dist/types/contrib/microevent.d.ts +20 -0
- package/dist/types/contrib/microplugin.d.ts +71 -0
- package/dist/types/defaults.d.ts +53 -0
- package/dist/types/getSettings.d.ts +3 -0
- package/dist/types/plugins/caret_position/plugin.d.ts +16 -0
- package/dist/types/plugins/change_listener/plugin.d.ts +16 -0
- package/dist/types/plugins/checkbox_options/plugin.d.ts +17 -0
- package/dist/types/plugins/checkbox_options/types.d.ts +14 -0
- package/dist/types/plugins/clear_button/plugin.d.ts +17 -0
- package/dist/types/plugins/clear_button/types.d.ts +7 -0
- package/dist/types/plugins/drag_drop/plugin.d.ts +16 -0
- package/dist/types/plugins/dropdown_header/plugin.d.ts +17 -0
- package/dist/types/plugins/dropdown_header/types.d.ts +8 -0
- package/dist/types/plugins/dropdown_input/plugin.d.ts +16 -0
- package/dist/types/plugins/input_autogrow/plugin.d.ts +15 -0
- package/dist/types/plugins/local_virtual_scroll/plugin.d.ts +19 -0
- package/dist/types/plugins/local_virtual_scroll/types.d.ts +14 -0
- package/dist/types/plugins/no_active_items/plugin.d.ts +15 -0
- package/dist/types/plugins/no_backspace_delete/plugin.d.ts +15 -0
- package/dist/types/plugins/optgroup_columns/plugin.d.ts +16 -0
- package/dist/types/plugins/remove_button/plugin.d.ts +17 -0
- package/dist/types/plugins/remove_button/types.d.ts +6 -0
- package/dist/types/plugins/restore_on_backspace/plugin.d.ts +21 -0
- package/dist/types/plugins/virtual_scroll/plugin.d.ts +16 -0
- package/dist/types/tom-select.complete.d.ts +2 -0
- package/dist/types/tom-select.d.ts +594 -0
- package/dist/types/tom-select.popular.d.ts +2 -0
- package/dist/types/types/core.d.ts +50 -0
- package/dist/types/types/index.d.ts +2 -0
- package/dist/types/types/settings.d.ts +81 -0
- package/dist/types/utils.d.ts +95 -0
- package/dist/types/vanilla.d.ts +76 -0
- package/package.json +156 -0
- package/src/constants.ts +13 -0
- package/src/contrib/highlight.ts +81 -0
- package/src/contrib/microevent.ts +73 -0
- package/src/contrib/microplugin.ts +137 -0
- package/src/defaults.ts +95 -0
- package/src/getSettings.ts +176 -0
- package/src/plugins/caret_position/plugin.ts +73 -0
- package/src/plugins/change_listener/plugin.ts +23 -0
- package/src/plugins/checkbox_options/plugin.scss +11 -0
- package/src/plugins/checkbox_options/plugin.ts +130 -0
- package/src/plugins/checkbox_options/types.ts +15 -0
- package/src/plugins/clear_button/plugin.scss +33 -0
- package/src/plugins/clear_button/plugin.ts +54 -0
- package/src/plugins/clear_button/types.ts +8 -0
- package/src/plugins/drag_drop/plugin.scss +10 -0
- package/src/plugins/drag_drop/plugin.ts +143 -0
- package/src/plugins/dropdown_header/plugin.scss +24 -0
- package/src/plugins/dropdown_header/plugin.ts +57 -0
- package/src/plugins/dropdown_header/types.ts +9 -0
- package/src/plugins/dropdown_input/plugin.scss +43 -0
- package/src/plugins/dropdown_input/plugin.ts +97 -0
- package/src/plugins/input_autogrow/plugin.scss +15 -0
- package/src/plugins/input_autogrow/plugin.ts +56 -0
- package/src/plugins/local_virtual_scroll/plugin.ts +309 -0
- package/src/plugins/local_virtual_scroll/types.ts +9 -0
- package/src/plugins/no_active_items/plugin.ts +20 -0
- package/src/plugins/no_backspace_delete/plugin.ts +30 -0
- package/src/plugins/optgroup_columns/plugin.scss +25 -0
- package/src/plugins/optgroup_columns/plugin.ts +59 -0
- package/src/plugins/remove_button/plugin.scss +70 -0
- package/src/plugins/remove_button/plugin.ts +78 -0
- package/src/plugins/remove_button/types.ts +7 -0
- package/src/plugins/restore_on_backspace/plugin.ts +44 -0
- package/src/plugins/virtual_scroll/plugin.ts +219 -0
- package/src/scss/-tom-select.bootstrap4.scss +4 -0
- package/src/scss/-tom-select.bootstrap5.scss +4 -0
- package/src/scss/_dropdown.scss +99 -0
- package/src/scss/_items.scss +114 -0
- package/src/scss/tom-select.bootstrap4.scss +218 -0
- package/src/scss/tom-select.bootstrap5.scss +270 -0
- package/src/scss/tom-select.default.scss +89 -0
- package/src/scss/tom-select.scss +179 -0
- package/src/tom-select.complete.ts +35 -0
- package/src/tom-select.popular.ts +15 -0
- package/src/tom-select.ts +2807 -0
- package/src/types/core.ts +68 -0
- package/src/types/index.ts +3 -0
- package/src/types/settings.ts +98 -0
- package/src/utils.ts +230 -0
- package/src/vanilla.ts +210 -0
|
@@ -0,0 +1,2807 @@
|
|
|
1
|
+
|
|
2
|
+
import MicroEvent from './contrib/microevent.ts';
|
|
3
|
+
import MicroPlugin from './contrib/microplugin.ts';
|
|
4
|
+
import { Sifter } from '@orchidjs/sifter';
|
|
5
|
+
import { escape_regex } from '@orchidjs/unicode-variants';
|
|
6
|
+
import { TomInput, TomArgObject, TomOption, TomOptions, TomCreateFilter, TomCreateCallback, TomItem, TomSettings, TomTemplateNames, TomClearFilter, RecursivePartial } from './types/index.ts';
|
|
7
|
+
import {highlight, removeHighlight} from './contrib/highlight.ts';
|
|
8
|
+
import * as constants from './constants.ts';
|
|
9
|
+
import getSettings from './getSettings.ts';
|
|
10
|
+
import {
|
|
11
|
+
hash_key,
|
|
12
|
+
get_hash,
|
|
13
|
+
escape_html,
|
|
14
|
+
debounce_events,
|
|
15
|
+
getSelection,
|
|
16
|
+
preventDefault,
|
|
17
|
+
addEvent,
|
|
18
|
+
loadDebounce,
|
|
19
|
+
timeout,
|
|
20
|
+
isKeyDown,
|
|
21
|
+
getId,
|
|
22
|
+
addSlashes,
|
|
23
|
+
append,
|
|
24
|
+
iterate
|
|
25
|
+
} from './utils.ts';
|
|
26
|
+
|
|
27
|
+
import {
|
|
28
|
+
getDom,
|
|
29
|
+
isHtmlString,
|
|
30
|
+
escapeQuery,
|
|
31
|
+
triggerEvent,
|
|
32
|
+
applyCSS,
|
|
33
|
+
addClasses,
|
|
34
|
+
removeClasses,
|
|
35
|
+
parentMatch,
|
|
36
|
+
getTail,
|
|
37
|
+
isEmptyObject,
|
|
38
|
+
nodeIndex,
|
|
39
|
+
setAttr,
|
|
40
|
+
replaceNode
|
|
41
|
+
} from './vanilla.ts';
|
|
42
|
+
|
|
43
|
+
var instance_i = 0;
|
|
44
|
+
|
|
45
|
+
export default class TomSelect extends MicroPlugin(MicroEvent){
|
|
46
|
+
|
|
47
|
+
public control_input : HTMLInputElement;
|
|
48
|
+
public wrapper : HTMLElement;
|
|
49
|
+
public dropdown : HTMLElement;
|
|
50
|
+
public control : HTMLElement;
|
|
51
|
+
public dropdown_content : HTMLElement;
|
|
52
|
+
public focus_node : HTMLElement;
|
|
53
|
+
|
|
54
|
+
public order : number = 0;
|
|
55
|
+
public settings : TomSettings;
|
|
56
|
+
public input : TomInput;
|
|
57
|
+
public tabIndex : number;
|
|
58
|
+
public is_select_tag : boolean;
|
|
59
|
+
public rtl : boolean;
|
|
60
|
+
private inputId : string;
|
|
61
|
+
|
|
62
|
+
private _destroy !: () => void;
|
|
63
|
+
public sifter : Sifter;
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
public isOpen : boolean = false;
|
|
67
|
+
public isDisabled : boolean = false;
|
|
68
|
+
public isReadOnly : boolean = false;
|
|
69
|
+
public isRequired : boolean;
|
|
70
|
+
public isInvalid : boolean = false; // @deprecated 1.8
|
|
71
|
+
public isValid : boolean = true;
|
|
72
|
+
public isLocked : boolean = false;
|
|
73
|
+
public isFocused : boolean = false;
|
|
74
|
+
public isInputHidden : boolean = false;
|
|
75
|
+
public isSetup : boolean = false;
|
|
76
|
+
public ignoreFocus : boolean = false;
|
|
77
|
+
public ignoreHover : boolean = false;
|
|
78
|
+
public hasOptions : boolean = false;
|
|
79
|
+
public currentResults ?: ReturnType<Sifter['search']>;
|
|
80
|
+
public lastValue : string = '';
|
|
81
|
+
public caretPos : number = 0;
|
|
82
|
+
public loading : number = 0;
|
|
83
|
+
public loadedSearches : { [key: string]: boolean } = {};
|
|
84
|
+
|
|
85
|
+
public activeOption : null|HTMLElement = null;
|
|
86
|
+
public activeItems : TomItem[] = [];
|
|
87
|
+
|
|
88
|
+
public optgroups : TomOptions = {};
|
|
89
|
+
public options : TomOptions = {};
|
|
90
|
+
public userOptions : {[key:string]:boolean} = {};
|
|
91
|
+
public items : string[] = [];
|
|
92
|
+
|
|
93
|
+
private refreshTimeout : null|number = null;
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
constructor( input_arg: string|TomInput, user_settings:RecursivePartial<TomSettings> ){
|
|
97
|
+
super();
|
|
98
|
+
|
|
99
|
+
instance_i++;
|
|
100
|
+
|
|
101
|
+
var dir;
|
|
102
|
+
var input = getDom( input_arg ) as TomInput;
|
|
103
|
+
|
|
104
|
+
if( input.tomselect ){
|
|
105
|
+
throw new Error('Tom Select already initialized on this element');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
input.tomselect = this;
|
|
109
|
+
|
|
110
|
+
// detect rtl environment
|
|
111
|
+
var computedStyle = window.getComputedStyle && window.getComputedStyle(input, null);
|
|
112
|
+
dir = computedStyle.getPropertyValue('direction');
|
|
113
|
+
|
|
114
|
+
// setup default state
|
|
115
|
+
const settings = getSettings( input, user_settings );
|
|
116
|
+
this.settings = settings;
|
|
117
|
+
this.input = input;
|
|
118
|
+
this.tabIndex = input.tabIndex || 0;
|
|
119
|
+
this.is_select_tag = input.tagName.toLowerCase() === 'select';
|
|
120
|
+
this.rtl = /rtl/i.test(dir);
|
|
121
|
+
this.inputId = getId(input, 'tomselect-'+instance_i);
|
|
122
|
+
this.isRequired = input.required;
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
// search system
|
|
126
|
+
this.sifter = new Sifter(this.options, {diacritics: settings.diacritics});
|
|
127
|
+
|
|
128
|
+
// option-dependent defaults
|
|
129
|
+
settings.mode = settings.mode || (settings.maxItems === 1 ? 'single' : 'multi');
|
|
130
|
+
if (typeof settings.hideSelected !== 'boolean') {
|
|
131
|
+
settings.hideSelected = settings.mode === 'multi';
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if( typeof settings.hidePlaceholder !== 'boolean' ){
|
|
135
|
+
settings.hidePlaceholder = settings.mode !== 'multi';
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// set up createFilter callback
|
|
139
|
+
var filter = settings.createFilter;
|
|
140
|
+
if( typeof filter !== 'function' ){
|
|
141
|
+
|
|
142
|
+
if( typeof filter === 'string' ){
|
|
143
|
+
filter = new RegExp(filter);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if( filter instanceof RegExp ){
|
|
147
|
+
settings.createFilter = (input: string) => (filter as RegExp).test(input);
|
|
148
|
+
}else{
|
|
149
|
+
settings.createFilter = (value: string) => {
|
|
150
|
+
return this.settings.duplicates || !this.options[value];
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
this.initializePlugins(settings.plugins);
|
|
157
|
+
this.setupCallbacks();
|
|
158
|
+
this.setupTemplates();
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
// Create all elements
|
|
162
|
+
const wrapper = getDom('<div>');
|
|
163
|
+
const control = getDom('<div>');
|
|
164
|
+
const dropdown = this._render('dropdown');
|
|
165
|
+
const dropdown_content = getDom(`<div role="listbox" tabindex="-1">`);
|
|
166
|
+
|
|
167
|
+
const classes = this.input.getAttribute('class') || '';
|
|
168
|
+
const inputMode = settings.mode;
|
|
169
|
+
|
|
170
|
+
var control_input: HTMLInputElement;
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
addClasses( wrapper, settings.wrapperClass, classes, inputMode);
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
addClasses(control,settings.controlClass);
|
|
177
|
+
append( wrapper, control );
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
addClasses(dropdown, settings.dropdownClass, inputMode);
|
|
181
|
+
if( settings.copyClassesToDropdown ){
|
|
182
|
+
addClasses( dropdown, classes);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
addClasses(dropdown_content, settings.dropdownContentClass);
|
|
187
|
+
append( dropdown, dropdown_content );
|
|
188
|
+
|
|
189
|
+
getDom( settings.dropdownParent || wrapper ).appendChild( dropdown );
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
// default controlInput
|
|
193
|
+
if( isHtmlString(settings.controlInput) ){
|
|
194
|
+
control_input = getDom(settings.controlInput ) as HTMLInputElement;
|
|
195
|
+
|
|
196
|
+
// set attributes
|
|
197
|
+
var attrs = ['autocorrect','autocapitalize','autocomplete','spellcheck','aria-label'];
|
|
198
|
+
iterate(attrs,(attr:string) => {
|
|
199
|
+
if( input.getAttribute(attr) ){
|
|
200
|
+
setAttr(control_input,{[attr]:input.getAttribute(attr)});
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
control_input.tabIndex = -1;
|
|
205
|
+
control.appendChild( control_input );
|
|
206
|
+
this.focus_node = control_input;
|
|
207
|
+
|
|
208
|
+
// dom element
|
|
209
|
+
}else if( settings.controlInput ){
|
|
210
|
+
control_input = getDom( settings.controlInput ) as HTMLInputElement;
|
|
211
|
+
this.focus_node = control_input;
|
|
212
|
+
|
|
213
|
+
}else{
|
|
214
|
+
control_input = getDom('<input/>') as HTMLInputElement;
|
|
215
|
+
this.focus_node = control;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
this.wrapper = wrapper;
|
|
219
|
+
this.dropdown = dropdown;
|
|
220
|
+
this.dropdown_content = dropdown_content;
|
|
221
|
+
this.control = control;
|
|
222
|
+
this.control_input = control_input;
|
|
223
|
+
|
|
224
|
+
this.setup();
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* set up event bindings.
|
|
229
|
+
*
|
|
230
|
+
*/
|
|
231
|
+
setup(){
|
|
232
|
+
|
|
233
|
+
const self = this;
|
|
234
|
+
const settings = self.settings;
|
|
235
|
+
const control_input = self.control_input;
|
|
236
|
+
const dropdown = self.dropdown;
|
|
237
|
+
const dropdown_content = self.dropdown_content;
|
|
238
|
+
const wrapper = self.wrapper;
|
|
239
|
+
const control = self.control;
|
|
240
|
+
const input = self.input;
|
|
241
|
+
const focus_node = self.focus_node;
|
|
242
|
+
const passive_event = { passive: true };
|
|
243
|
+
const listboxId = self.inputId +'-ts-dropdown';
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
setAttr(dropdown_content,{
|
|
247
|
+
id: listboxId
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
setAttr(focus_node,{
|
|
251
|
+
role:'combobox',
|
|
252
|
+
'aria-haspopup':'listbox',
|
|
253
|
+
'aria-expanded':'false',
|
|
254
|
+
'aria-controls':listboxId
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
const control_id = getId(focus_node,self.inputId + '-ts-control');
|
|
258
|
+
const query = "label[for='"+escapeQuery(self.inputId)+"']";
|
|
259
|
+
const label = document.querySelector(query);
|
|
260
|
+
const label_click = self.focus.bind(self);
|
|
261
|
+
if( label ){
|
|
262
|
+
addEvent(label,'click', label_click );
|
|
263
|
+
setAttr(label,{for:control_id});
|
|
264
|
+
const label_id = getId(label,self.inputId+'-ts-label');
|
|
265
|
+
setAttr(focus_node,{'aria-labelledby':label_id});
|
|
266
|
+
setAttr(dropdown_content,{'aria-labelledby':label_id});
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
wrapper.style.width = input.style.width;
|
|
270
|
+
wrapper.style.minWidth = input.style.minWidth;
|
|
271
|
+
wrapper.style.maxWidth = input.style.maxWidth;
|
|
272
|
+
|
|
273
|
+
if (self.plugins.names.length) {
|
|
274
|
+
const classes_plugins = 'plugin-' + self.plugins.names.join(' plugin-');
|
|
275
|
+
addClasses( [wrapper,dropdown], classes_plugins);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if ((settings.maxItems === null || settings.maxItems > 1) && self.is_select_tag ){
|
|
279
|
+
setAttr(input,{multiple:'multiple'});
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (settings.placeholder) {
|
|
283
|
+
setAttr(control_input,{placeholder:settings.placeholder});
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// if splitOn was not passed in, construct it from the delimiter to allow pasting universally
|
|
287
|
+
if (!settings.splitOn && settings.delimiter) {
|
|
288
|
+
settings.splitOn = new RegExp('\\s*' + escape_regex(settings.delimiter) + '+\\s*');
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// debounce user defined load() if loadThrottle > 0
|
|
292
|
+
// after initializePlugins() so plugins can create/modify user defined loaders
|
|
293
|
+
if( settings.load && settings.loadThrottle ){
|
|
294
|
+
settings.load = loadDebounce(settings.load,settings.loadThrottle)
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
addEvent(dropdown,'mousemove', () => {
|
|
298
|
+
self.ignoreHover = false;
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
addEvent(dropdown,'mouseenter', (e) => {
|
|
302
|
+
|
|
303
|
+
var target_match = parentMatch(e.target as HTMLElement, '[data-selectable]', dropdown);
|
|
304
|
+
if( target_match ) self.onOptionHover( e as MouseEvent, target_match );
|
|
305
|
+
|
|
306
|
+
}, {capture:true});
|
|
307
|
+
|
|
308
|
+
// clicking on an option should select it
|
|
309
|
+
addEvent(dropdown,'click',(evt) => {
|
|
310
|
+
const option = parentMatch(evt.target as HTMLElement, '[data-selectable]');
|
|
311
|
+
if( option ){
|
|
312
|
+
self.onOptionSelect( evt as MouseEvent, option );
|
|
313
|
+
preventDefault(evt,true);
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
addEvent(control,'click', (evt) => {
|
|
318
|
+
|
|
319
|
+
var target_match = parentMatch( evt.target as HTMLElement, '[data-ts-item]', control);
|
|
320
|
+
if( target_match && self.onItemSelect(evt as MouseEvent, target_match as TomItem) ){
|
|
321
|
+
preventDefault(evt,true);
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// retain focus (see control_input mousedown)
|
|
326
|
+
if( control_input.value != '' ){
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
self.onClick();
|
|
331
|
+
preventDefault(evt,true);
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
// keydown on focus_node for arrow_down/arrow_up
|
|
336
|
+
addEvent(focus_node,'keydown', (e) => self.onKeyDown(e as KeyboardEvent) );
|
|
337
|
+
|
|
338
|
+
// keypress and input/keyup
|
|
339
|
+
addEvent(control_input,'keypress', (e) => self.onKeyPress(e as KeyboardEvent) );
|
|
340
|
+
addEvent(control_input,'input', (e) => self.onInput(e as KeyboardEvent) );
|
|
341
|
+
addEvent(focus_node,'blur', (e) => self.onBlur(e as FocusEvent) );
|
|
342
|
+
addEvent(focus_node,'focus', (e) => self.onFocus(e as MouseEvent) );
|
|
343
|
+
addEvent(control_input,'paste', (e) => self.onPaste(e as MouseEvent) );
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
const doc_mousedown = (evt:Event) => {
|
|
347
|
+
|
|
348
|
+
// blur if target is outside of this instance
|
|
349
|
+
// dropdown is not always inside wrapper
|
|
350
|
+
const target = evt.composedPath()[0];
|
|
351
|
+
if( !wrapper.contains(target as HTMLElement) && !dropdown.contains(target as HTMLElement) ){
|
|
352
|
+
if (self.isFocused) {
|
|
353
|
+
self.blur();
|
|
354
|
+
}
|
|
355
|
+
self.inputState();
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
// retain focus by preventing native handling. if the
|
|
361
|
+
// event target is the input it should not be modified.
|
|
362
|
+
// otherwise, text selection within the input won't work.
|
|
363
|
+
// Fixes bug #212 which is no covered by tests
|
|
364
|
+
if( target == control_input && self.isOpen ){
|
|
365
|
+
evt.stopPropagation();
|
|
366
|
+
|
|
367
|
+
// clicking anywhere in the control should not blur the control_input (which would close the dropdown)
|
|
368
|
+
}else{
|
|
369
|
+
preventDefault(evt,true);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
const win_scroll = () => {
|
|
375
|
+
if (self.isOpen) {
|
|
376
|
+
self.positionDropdown();
|
|
377
|
+
}
|
|
378
|
+
};
|
|
379
|
+
|
|
380
|
+
const input_invalid = () => {
|
|
381
|
+
if( self.isValid ){
|
|
382
|
+
self.isValid = false;
|
|
383
|
+
self.isInvalid = true;
|
|
384
|
+
self.refreshState();
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
addEvent(input,'invalid', input_invalid);
|
|
389
|
+
addEvent(document,'mousedown', doc_mousedown);
|
|
390
|
+
addEvent(window,'scroll', win_scroll, passive_event);
|
|
391
|
+
addEvent(window,'resize', win_scroll, passive_event);
|
|
392
|
+
|
|
393
|
+
this._destroy = () => {
|
|
394
|
+
input.removeEventListener('invalid',input_invalid);
|
|
395
|
+
document.removeEventListener('mousedown',doc_mousedown);
|
|
396
|
+
window.removeEventListener('scroll',win_scroll);
|
|
397
|
+
window.removeEventListener('resize',win_scroll);
|
|
398
|
+
if( label ) label.removeEventListener('click',label_click);
|
|
399
|
+
};
|
|
400
|
+
|
|
401
|
+
// store original html and tab index so that they can be
|
|
402
|
+
// restored when the destroy() method is called.
|
|
403
|
+
this.revertSettings = {
|
|
404
|
+
innerHTML : input.innerHTML,
|
|
405
|
+
tabIndex : input.tabIndex
|
|
406
|
+
};
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
input.tabIndex = -1;
|
|
410
|
+
input.insertAdjacentElement('afterend', self.wrapper);
|
|
411
|
+
|
|
412
|
+
self.sync(false);
|
|
413
|
+
settings.items = [];
|
|
414
|
+
delete settings.optgroups;
|
|
415
|
+
delete settings.options;
|
|
416
|
+
|
|
417
|
+
self.refreshItems();
|
|
418
|
+
self.close(false);
|
|
419
|
+
self.inputState();
|
|
420
|
+
self.isSetup = true;
|
|
421
|
+
|
|
422
|
+
if( input.disabled ){
|
|
423
|
+
self.disable();
|
|
424
|
+
}else if( input.readOnly ){
|
|
425
|
+
self.setReadOnly(true);
|
|
426
|
+
}else{
|
|
427
|
+
self.enable(); //sets tabIndex
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
self.on('change', this.onChange);
|
|
431
|
+
|
|
432
|
+
addClasses(input,'tomselected','ts-hidden-accessible');
|
|
433
|
+
self.trigger('initialize');
|
|
434
|
+
|
|
435
|
+
// preload options
|
|
436
|
+
if (settings.preload === true) {
|
|
437
|
+
self.preload();
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* Register options and optgroups
|
|
445
|
+
*
|
|
446
|
+
*/
|
|
447
|
+
setupOptions(options:TomOption[] = [], optgroups:TomOption[] = []){
|
|
448
|
+
|
|
449
|
+
// build options table
|
|
450
|
+
this.addOptions(options);
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
// build optgroup table
|
|
454
|
+
iterate( optgroups, (optgroup:TomOption) => {
|
|
455
|
+
this.registerOptionGroup(optgroup);
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Sets up default rendering functions.
|
|
461
|
+
*/
|
|
462
|
+
setupTemplates() {
|
|
463
|
+
var self = this;
|
|
464
|
+
var field_label = self.settings.labelField;
|
|
465
|
+
var field_optgroup = self.settings.optgroupLabelField;
|
|
466
|
+
|
|
467
|
+
var templates = {
|
|
468
|
+
'optgroup': (data:TomOption) => {
|
|
469
|
+
let optgroup = document.createElement('div');
|
|
470
|
+
optgroup.className = 'optgroup';
|
|
471
|
+
optgroup.appendChild(data.options);
|
|
472
|
+
return optgroup;
|
|
473
|
+
|
|
474
|
+
},
|
|
475
|
+
'optgroup_header': (data:TomOption, escape:typeof escape_html) => {
|
|
476
|
+
return '<div class="optgroup-header">' + escape(data[field_optgroup]) + '</div>';
|
|
477
|
+
},
|
|
478
|
+
'option': (data:TomOption, escape:typeof escape_html) => {
|
|
479
|
+
return '<div>' + escape(data[field_label]) + '</div>';
|
|
480
|
+
},
|
|
481
|
+
'item': (data:TomOption, escape:typeof escape_html) => {
|
|
482
|
+
return '<div>' + escape(data[field_label]) + '</div>';
|
|
483
|
+
},
|
|
484
|
+
'option_create': (data:TomOption, escape:typeof escape_html) => {
|
|
485
|
+
return '<div class="create">Add <strong>' + escape(data.input) + '</strong>…</div>';
|
|
486
|
+
},
|
|
487
|
+
'no_results':() => {
|
|
488
|
+
return '<div class="no-results">No results found</div>';
|
|
489
|
+
},
|
|
490
|
+
'loading':() => {
|
|
491
|
+
return '<div class="spinner"></div>';
|
|
492
|
+
},
|
|
493
|
+
'not_loading':() => {},
|
|
494
|
+
'dropdown':() => {
|
|
495
|
+
return '<div></div>';
|
|
496
|
+
}
|
|
497
|
+
};
|
|
498
|
+
|
|
499
|
+
|
|
500
|
+
self.settings.render = Object.assign({}, templates, self.settings.render);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/**
|
|
504
|
+
* Maps fired events to callbacks provided
|
|
505
|
+
* in the settings used when creating the control.
|
|
506
|
+
*/
|
|
507
|
+
setupCallbacks() {
|
|
508
|
+
var key, fn;
|
|
509
|
+
var callbacks:{[key:string]:string} = {
|
|
510
|
+
'initialize' : 'onInitialize',
|
|
511
|
+
'change' : 'onChange',
|
|
512
|
+
'item_add' : 'onItemAdd',
|
|
513
|
+
'item_remove' : 'onItemRemove',
|
|
514
|
+
'item_select' : 'onItemSelect',
|
|
515
|
+
'clear' : 'onClear',
|
|
516
|
+
'option_add' : 'onOptionAdd',
|
|
517
|
+
'option_remove' : 'onOptionRemove',
|
|
518
|
+
'option_clear' : 'onOptionClear',
|
|
519
|
+
'optgroup_add' : 'onOptionGroupAdd',
|
|
520
|
+
'optgroup_remove' : 'onOptionGroupRemove',
|
|
521
|
+
'optgroup_clear' : 'onOptionGroupClear',
|
|
522
|
+
'dropdown_open' : 'onDropdownOpen',
|
|
523
|
+
'dropdown_close' : 'onDropdownClose',
|
|
524
|
+
'type' : 'onType',
|
|
525
|
+
'load' : 'onLoad',
|
|
526
|
+
'focus' : 'onFocus',
|
|
527
|
+
'blur' : 'onBlur'
|
|
528
|
+
};
|
|
529
|
+
|
|
530
|
+
for (key in callbacks) {
|
|
531
|
+
|
|
532
|
+
fn = this.settings[callbacks[key] as (keyof TomSettings)];
|
|
533
|
+
if (fn) this.on(key, fn);
|
|
534
|
+
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
/**
|
|
539
|
+
* Sync the Tom Select instance with the original input or select
|
|
540
|
+
*
|
|
541
|
+
*/
|
|
542
|
+
sync(get_settings:boolean=true):void{
|
|
543
|
+
const self = this;
|
|
544
|
+
const settings = get_settings ? getSettings( self.input, {delimiter:self.settings.delimiter,allowEmptyOption:self.settings.allowEmptyOption} as RecursivePartial<TomSettings> ) : self.settings;
|
|
545
|
+
|
|
546
|
+
self.setupOptions(settings.options,settings.optgroups);
|
|
547
|
+
|
|
548
|
+
self.setValue(settings.items||[],true); // silent prevents recursion
|
|
549
|
+
|
|
550
|
+
self.lastQuery = null; // so updated options will be displayed in dropdown
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
/**
|
|
554
|
+
* Triggered when the main control element
|
|
555
|
+
* has a click event.
|
|
556
|
+
*
|
|
557
|
+
*/
|
|
558
|
+
onClick():void {
|
|
559
|
+
var self = this;
|
|
560
|
+
|
|
561
|
+
if( self.activeItems.length > 0 ){
|
|
562
|
+
self.clearActiveItems();
|
|
563
|
+
self.focus();
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
if( self.isFocused && self.isOpen ){
|
|
568
|
+
self.blur();
|
|
569
|
+
} else {
|
|
570
|
+
self.focus();
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
/**
|
|
575
|
+
* @deprecated v1.7
|
|
576
|
+
*
|
|
577
|
+
*/
|
|
578
|
+
onMouseDown():void {}
|
|
579
|
+
|
|
580
|
+
/**
|
|
581
|
+
* Triggered when the value of the control has been changed.
|
|
582
|
+
* This should propagate the event to the original DOM
|
|
583
|
+
* input / select element.
|
|
584
|
+
*/
|
|
585
|
+
onChange() {
|
|
586
|
+
triggerEvent(this.input, 'input');
|
|
587
|
+
triggerEvent(this.input, 'change');
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
/**
|
|
591
|
+
* Triggered on <input> paste.
|
|
592
|
+
*
|
|
593
|
+
*/
|
|
594
|
+
onPaste(e:MouseEvent|KeyboardEvent):void {
|
|
595
|
+
var self = this;
|
|
596
|
+
|
|
597
|
+
if( self.isInputHidden || self.isLocked ){
|
|
598
|
+
preventDefault(e);
|
|
599
|
+
return;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// If a regex or string is included, this will split the pasted
|
|
603
|
+
// input and create Items for each separate value
|
|
604
|
+
if( !self.settings.splitOn ){
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// Wait for pasted text to be recognized in value
|
|
609
|
+
setTimeout(() => {
|
|
610
|
+
var pastedText = self.inputValue();
|
|
611
|
+
if( !pastedText.match(self.settings.splitOn)){
|
|
612
|
+
return
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
var splitInput = pastedText.trim().split(self.settings.splitOn);
|
|
616
|
+
iterate( splitInput, (piece:string) => {
|
|
617
|
+
|
|
618
|
+
const hash = hash_key(piece);
|
|
619
|
+
if( hash ){
|
|
620
|
+
if( this.options[piece] ){
|
|
621
|
+
self.addItem(piece);
|
|
622
|
+
}else{
|
|
623
|
+
self.createItem(piece);
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
});
|
|
627
|
+
}, 0);
|
|
628
|
+
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
/**
|
|
632
|
+
* Triggered on <input> keypress.
|
|
633
|
+
*
|
|
634
|
+
*/
|
|
635
|
+
onKeyPress(e:KeyboardEvent):void {
|
|
636
|
+
var self = this;
|
|
637
|
+
if(self.isLocked){
|
|
638
|
+
preventDefault(e);
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
var character = String.fromCharCode(e.keyCode || e.which);
|
|
642
|
+
if (self.settings.create && self.settings.mode === 'multi' && character === self.settings.delimiter) {
|
|
643
|
+
self.createItem();
|
|
644
|
+
preventDefault(e);
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
/**
|
|
650
|
+
* Triggered on <input> keydown.
|
|
651
|
+
*
|
|
652
|
+
*/
|
|
653
|
+
onKeyDown(e:KeyboardEvent):void {
|
|
654
|
+
var self = this;
|
|
655
|
+
|
|
656
|
+
self.ignoreHover = true;
|
|
657
|
+
|
|
658
|
+
if (self.isLocked) {
|
|
659
|
+
if (e.keyCode !== constants.KEY_TAB) {
|
|
660
|
+
preventDefault(e);
|
|
661
|
+
}
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
switch (e.keyCode) {
|
|
666
|
+
|
|
667
|
+
// ctrl+A: select all
|
|
668
|
+
case constants.KEY_A:
|
|
669
|
+
if( isKeyDown(constants.KEY_SHORTCUT,e) ){
|
|
670
|
+
if( self.control_input.value == '' ){
|
|
671
|
+
preventDefault(e);
|
|
672
|
+
self.selectAll();
|
|
673
|
+
return;
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
break;
|
|
677
|
+
|
|
678
|
+
// esc: close dropdown
|
|
679
|
+
case constants.KEY_ESC:
|
|
680
|
+
if (self.isOpen) {
|
|
681
|
+
preventDefault(e,true);
|
|
682
|
+
self.close();
|
|
683
|
+
}
|
|
684
|
+
self.clearActiveItems();
|
|
685
|
+
return;
|
|
686
|
+
|
|
687
|
+
// down: open dropdown or move selection down
|
|
688
|
+
case constants.KEY_DOWN:
|
|
689
|
+
if (!self.isOpen && self.hasOptions) {
|
|
690
|
+
self.open();
|
|
691
|
+
} else if (self.activeOption) {
|
|
692
|
+
let next = self.getAdjacent(self.activeOption, 1);
|
|
693
|
+
if (next) self.setActiveOption(next);
|
|
694
|
+
}
|
|
695
|
+
preventDefault(e);
|
|
696
|
+
return;
|
|
697
|
+
|
|
698
|
+
// up: move selection up
|
|
699
|
+
case constants.KEY_UP:
|
|
700
|
+
if (self.activeOption) {
|
|
701
|
+
let prev = self.getAdjacent(self.activeOption, -1);
|
|
702
|
+
if (prev) self.setActiveOption(prev);
|
|
703
|
+
}
|
|
704
|
+
preventDefault(e);
|
|
705
|
+
return;
|
|
706
|
+
|
|
707
|
+
// return: select active option
|
|
708
|
+
case constants.KEY_RETURN:
|
|
709
|
+
if( self.canSelect(self.activeOption) ){
|
|
710
|
+
self.onOptionSelect(e,self.activeOption!);
|
|
711
|
+
preventDefault(e);
|
|
712
|
+
|
|
713
|
+
// if the option_create=null, the dropdown might be closed
|
|
714
|
+
}else if (self.settings.create && self.createItem()) {
|
|
715
|
+
preventDefault(e);
|
|
716
|
+
|
|
717
|
+
// don't submit form when searching for a value
|
|
718
|
+
}else if( document.activeElement == self.control_input && self.isOpen ){
|
|
719
|
+
preventDefault(e);
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
return;
|
|
723
|
+
|
|
724
|
+
// left: modifiy item selection to the left
|
|
725
|
+
case constants.KEY_LEFT:
|
|
726
|
+
self.advanceSelection(-1, e);
|
|
727
|
+
return;
|
|
728
|
+
|
|
729
|
+
// right: modifiy item selection to the right
|
|
730
|
+
case constants.KEY_RIGHT:
|
|
731
|
+
self.advanceSelection(1, e);
|
|
732
|
+
return;
|
|
733
|
+
|
|
734
|
+
// tab: select active option and/or create item
|
|
735
|
+
case constants.KEY_TAB:
|
|
736
|
+
|
|
737
|
+
if( self.settings.selectOnTab ){
|
|
738
|
+
if( self.canSelect(self.activeOption) ){
|
|
739
|
+
self.onOptionSelect(e,self.activeOption!);
|
|
740
|
+
|
|
741
|
+
// prevent default [tab] behaviour of jump to the next field
|
|
742
|
+
// if select isFull, then the dropdown won't be open and [tab] will work normally
|
|
743
|
+
preventDefault(e);
|
|
744
|
+
}
|
|
745
|
+
else if(self.settings.create && self.createItem()) {
|
|
746
|
+
preventDefault(e);
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
return;
|
|
750
|
+
|
|
751
|
+
// delete|backspace: delete items
|
|
752
|
+
case constants.KEY_BACKSPACE:
|
|
753
|
+
case constants.KEY_DELETE:
|
|
754
|
+
self.deleteSelection(e);
|
|
755
|
+
return;
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
// don't enter text in the control_input when active items are selected
|
|
759
|
+
if( self.isInputHidden && !isKeyDown(constants.KEY_SHORTCUT,e) ){
|
|
760
|
+
preventDefault(e);
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
/**
|
|
765
|
+
* Triggered on <input> keyup.
|
|
766
|
+
*
|
|
767
|
+
*/
|
|
768
|
+
onInput(e:MouseEvent|KeyboardEvent):void {
|
|
769
|
+
|
|
770
|
+
if( this.isLocked ){
|
|
771
|
+
return;
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
const value = this.inputValue();
|
|
775
|
+
if( this.lastValue === value ) return;
|
|
776
|
+
this.lastValue = value;
|
|
777
|
+
|
|
778
|
+
if( value == '' ){
|
|
779
|
+
this._onInput();
|
|
780
|
+
return;
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
if( this.refreshTimeout ){
|
|
784
|
+
window.clearTimeout(this.refreshTimeout);
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
this.refreshTimeout = timeout(()=> {
|
|
788
|
+
this.refreshTimeout = null;
|
|
789
|
+
this._onInput();
|
|
790
|
+
}, this.settings.refreshThrottle);
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
_onInput():void {
|
|
794
|
+
const value = this.lastValue;
|
|
795
|
+
|
|
796
|
+
if( this.settings.shouldLoad.call(this,value) ){
|
|
797
|
+
this.load(value);
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
this.refreshOptions();
|
|
801
|
+
this.trigger('type', value);
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
/**
|
|
805
|
+
* Triggered when the user rolls over
|
|
806
|
+
* an option in the autocomplete dropdown menu.
|
|
807
|
+
*
|
|
808
|
+
*/
|
|
809
|
+
onOptionHover( evt:MouseEvent|KeyboardEvent, option:HTMLElement ):void{
|
|
810
|
+
if( this.ignoreHover ) return;
|
|
811
|
+
this.setActiveOption(option, false);
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
/**
|
|
815
|
+
* Triggered on <input> focus.
|
|
816
|
+
*
|
|
817
|
+
*/
|
|
818
|
+
onFocus(e?:MouseEvent|KeyboardEvent):void {
|
|
819
|
+
var self = this;
|
|
820
|
+
var wasFocused = self.isFocused;
|
|
821
|
+
|
|
822
|
+
if( self.isDisabled || self.isReadOnly ){
|
|
823
|
+
self.blur();
|
|
824
|
+
preventDefault(e);
|
|
825
|
+
return;
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
if (self.ignoreFocus) return;
|
|
829
|
+
self.isFocused = true;
|
|
830
|
+
if( self.settings.preload === 'focus' ) self.preload();
|
|
831
|
+
|
|
832
|
+
if (!wasFocused) self.trigger('focus');
|
|
833
|
+
|
|
834
|
+
if (!self.activeItems.length) {
|
|
835
|
+
self.inputState();
|
|
836
|
+
self.refreshOptions(!!self.settings.openOnFocus);
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
self.refreshState();
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
/**
|
|
843
|
+
* Triggered on <input> blur.
|
|
844
|
+
*
|
|
845
|
+
*/
|
|
846
|
+
onBlur(e?:FocusEvent):void {
|
|
847
|
+
|
|
848
|
+
if( document.hasFocus() === false ) return;
|
|
849
|
+
|
|
850
|
+
var self = this;
|
|
851
|
+
if (!self.isFocused) return;
|
|
852
|
+
self.isFocused = false;
|
|
853
|
+
self.ignoreFocus = false;
|
|
854
|
+
|
|
855
|
+
var deactivate = () => {
|
|
856
|
+
self.close();
|
|
857
|
+
self.setActiveItem();
|
|
858
|
+
self.setCaret(self.items.length);
|
|
859
|
+
self.trigger('blur');
|
|
860
|
+
};
|
|
861
|
+
|
|
862
|
+
if (self.settings.create && self.settings.createOnBlur) {
|
|
863
|
+
self.createItem(null, deactivate);
|
|
864
|
+
} else {
|
|
865
|
+
deactivate();
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
|
|
870
|
+
/**
|
|
871
|
+
* Triggered when the user clicks on an option
|
|
872
|
+
* in the autocomplete dropdown menu.
|
|
873
|
+
*
|
|
874
|
+
*/
|
|
875
|
+
onOptionSelect( evt:MouseEvent|KeyboardEvent, option:HTMLElement ){
|
|
876
|
+
var value, self = this;
|
|
877
|
+
|
|
878
|
+
|
|
879
|
+
// should not be possible to trigger a option under a disabled optgroup
|
|
880
|
+
if( option.parentElement && option.parentElement.matches('[data-disabled]') ){
|
|
881
|
+
return;
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
|
|
885
|
+
if( option.classList.contains('create') ){
|
|
886
|
+
self.createItem(null, () => {
|
|
887
|
+
if (self.settings.closeAfterSelect) {
|
|
888
|
+
self.close();
|
|
889
|
+
} else if(self.settings.clearAfterSelect) {
|
|
890
|
+
self.setTextboxValue();
|
|
891
|
+
}
|
|
892
|
+
});
|
|
893
|
+
} else {
|
|
894
|
+
value = option.dataset.value;
|
|
895
|
+
if (typeof value !== 'undefined') {
|
|
896
|
+
self.lastQuery = null;
|
|
897
|
+
self.addItem(value);
|
|
898
|
+
if (self.settings.closeAfterSelect) {
|
|
899
|
+
self.close();
|
|
900
|
+
} else if(self.settings.clearAfterSelect) {
|
|
901
|
+
self.setTextboxValue();
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
if( !self.settings.hideSelected && evt.type && /click/.test(evt.type) ){
|
|
905
|
+
self.setActiveOption(option);
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
/**
|
|
912
|
+
* Return true if the given option can be selected
|
|
913
|
+
*
|
|
914
|
+
*/
|
|
915
|
+
canSelect(option:HTMLElement|null):boolean{
|
|
916
|
+
|
|
917
|
+
if( this.isOpen && option && this.dropdown_content.contains(option) ) {
|
|
918
|
+
return true;
|
|
919
|
+
}
|
|
920
|
+
return false;
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
/**
|
|
924
|
+
* Triggered when the user clicks on an item
|
|
925
|
+
* that has been selected.
|
|
926
|
+
*
|
|
927
|
+
*/
|
|
928
|
+
onItemSelect( evt?:MouseEvent, item?:TomItem ):boolean{
|
|
929
|
+
var self = this;
|
|
930
|
+
|
|
931
|
+
if( !self.isLocked && self.settings.mode === 'multi' ){
|
|
932
|
+
preventDefault(evt);
|
|
933
|
+
self.setActiveItem(item, evt);
|
|
934
|
+
return true;
|
|
935
|
+
}
|
|
936
|
+
return false;
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
/**
|
|
940
|
+
* Determines whether or not to invoke
|
|
941
|
+
* the user-provided option provider / loader
|
|
942
|
+
*
|
|
943
|
+
* Note, there is a subtle difference between
|
|
944
|
+
* this.canLoad() and this.settings.shouldLoad();
|
|
945
|
+
*
|
|
946
|
+
* - settings.shouldLoad() is a user-input validator.
|
|
947
|
+
* When false is returned, the not_loading template
|
|
948
|
+
* will be added to the dropdown
|
|
949
|
+
*
|
|
950
|
+
* - canLoad() is lower level validator that checks
|
|
951
|
+
* the Tom Select instance. There is no inherent user
|
|
952
|
+
* feedback when canLoad returns false
|
|
953
|
+
*
|
|
954
|
+
*/
|
|
955
|
+
canLoad(value:string):boolean{
|
|
956
|
+
|
|
957
|
+
if( !this.settings.load ) return false;
|
|
958
|
+
if( this.loadedSearches.hasOwnProperty(value) ) return false;
|
|
959
|
+
|
|
960
|
+
return true;
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
/**
|
|
964
|
+
* Invokes the user-provided option provider / loader.
|
|
965
|
+
*
|
|
966
|
+
*/
|
|
967
|
+
load(value:string):void {
|
|
968
|
+
const self = this;
|
|
969
|
+
|
|
970
|
+
if( !self.canLoad(value) ) return;
|
|
971
|
+
|
|
972
|
+
addClasses(self.wrapper,self.settings.loadingClass);
|
|
973
|
+
self.loading++;
|
|
974
|
+
|
|
975
|
+
const callback = self.loadCallback.bind(self);
|
|
976
|
+
self.settings.load.call(self, value, callback);
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
/**
|
|
980
|
+
* Invoked by the user-provided option provider
|
|
981
|
+
*
|
|
982
|
+
*/
|
|
983
|
+
loadCallback( options:TomOption[], optgroups:TomOption[] ):void{
|
|
984
|
+
const self = this;
|
|
985
|
+
self.loading = Math.max(self.loading - 1, 0);
|
|
986
|
+
self.lastQuery = null;
|
|
987
|
+
|
|
988
|
+
self.clearActiveOption(); // when new results load, focus should be on first option
|
|
989
|
+
self.setupOptions(options,optgroups);
|
|
990
|
+
|
|
991
|
+
self.refreshOptions(self.isFocused && !self.isInputHidden);
|
|
992
|
+
|
|
993
|
+
if (!self.loading) {
|
|
994
|
+
removeClasses(self.wrapper,self.settings.loadingClass);
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
self.trigger('load', options, optgroups);
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
preload():void{
|
|
1001
|
+
var classList = this.wrapper.classList;
|
|
1002
|
+
if( classList.contains('preloaded') ) return;
|
|
1003
|
+
classList.add('preloaded');
|
|
1004
|
+
this.load('');
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
|
|
1008
|
+
/**
|
|
1009
|
+
* Sets the input field of the control to the specified value.
|
|
1010
|
+
*
|
|
1011
|
+
*/
|
|
1012
|
+
setTextboxValue(value:string = '') {
|
|
1013
|
+
var input = this.control_input;
|
|
1014
|
+
var changed = input.value !== value;
|
|
1015
|
+
if (changed) {
|
|
1016
|
+
input.value = value;
|
|
1017
|
+
triggerEvent(input,'update');
|
|
1018
|
+
this.lastValue = value;
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
/**
|
|
1023
|
+
* Returns the value of the control. If multiple items
|
|
1024
|
+
* can be selected (e.g. <select multiple>), this returns
|
|
1025
|
+
* an array. If only one item can be selected, this
|
|
1026
|
+
* returns a string.
|
|
1027
|
+
*
|
|
1028
|
+
*/
|
|
1029
|
+
getValue():string|string[] {
|
|
1030
|
+
|
|
1031
|
+
if( this.is_select_tag && this.input.hasAttribute('multiple')) {
|
|
1032
|
+
return this.items;
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
return this.items.join(this.settings.delimiter);
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
/**
|
|
1039
|
+
* Resets the selected items to the given value.
|
|
1040
|
+
*
|
|
1041
|
+
*/
|
|
1042
|
+
setValue( value:string|string[], silent?:boolean ):void{
|
|
1043
|
+
var events = silent ? [] : ['change'];
|
|
1044
|
+
|
|
1045
|
+
debounce_events(this, events,() => {
|
|
1046
|
+
this.clear(silent);
|
|
1047
|
+
this.addItems(value, silent);
|
|
1048
|
+
});
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
|
|
1052
|
+
/**
|
|
1053
|
+
* Resets the number of max items to the given value
|
|
1054
|
+
*
|
|
1055
|
+
*/
|
|
1056
|
+
setMaxItems(value:null|number){
|
|
1057
|
+
if(value === 0) value = null; //reset to unlimited items.
|
|
1058
|
+
this.settings.maxItems = value;
|
|
1059
|
+
this.refreshState();
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
/**
|
|
1063
|
+
* Sets the selected item.
|
|
1064
|
+
*
|
|
1065
|
+
*/
|
|
1066
|
+
setActiveItem( item?:TomItem, e?:MouseEvent|KeyboardEvent ){
|
|
1067
|
+
var self = this;
|
|
1068
|
+
var eventName;
|
|
1069
|
+
var i, begin, end, swap;
|
|
1070
|
+
var last;
|
|
1071
|
+
|
|
1072
|
+
if (self.settings.mode === 'single') return;
|
|
1073
|
+
|
|
1074
|
+
// clear the active selection
|
|
1075
|
+
if( !item ){
|
|
1076
|
+
self.clearActiveItems();
|
|
1077
|
+
if (self.isFocused) {
|
|
1078
|
+
self.inputState();
|
|
1079
|
+
}
|
|
1080
|
+
return;
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
// modify selection
|
|
1084
|
+
eventName = e && e.type.toLowerCase();
|
|
1085
|
+
|
|
1086
|
+
if (eventName === 'click' && isKeyDown('shiftKey',e) && self.activeItems.length) {
|
|
1087
|
+
last = self.getLastActive();
|
|
1088
|
+
begin = Array.prototype.indexOf.call(self.control.children, last);
|
|
1089
|
+
end = Array.prototype.indexOf.call(self.control.children, item);
|
|
1090
|
+
|
|
1091
|
+
if (begin > end) {
|
|
1092
|
+
swap = begin;
|
|
1093
|
+
begin = end;
|
|
1094
|
+
end = swap;
|
|
1095
|
+
}
|
|
1096
|
+
for (i = begin; i <= end; i++) {
|
|
1097
|
+
item = self.control.children[i] as TomItem;
|
|
1098
|
+
if (self.activeItems.indexOf(item) === -1) {
|
|
1099
|
+
self.setActiveItemClass(item);
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
preventDefault(e);
|
|
1103
|
+
} else if ((eventName === 'click' && isKeyDown(constants.KEY_SHORTCUT,e) ) || (eventName === 'keydown' && isKeyDown('shiftKey',e))) {
|
|
1104
|
+
if( item.classList.contains('active') ){
|
|
1105
|
+
self.removeActiveItem( item );
|
|
1106
|
+
} else {
|
|
1107
|
+
self.setActiveItemClass(item);
|
|
1108
|
+
}
|
|
1109
|
+
} else {
|
|
1110
|
+
self.clearActiveItems();
|
|
1111
|
+
self.setActiveItemClass(item);
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
// ensure control has focus
|
|
1115
|
+
self.inputState();
|
|
1116
|
+
if (!self.isFocused) {
|
|
1117
|
+
self.focus();
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
/**
|
|
1122
|
+
* Set the active and last-active classes
|
|
1123
|
+
*
|
|
1124
|
+
*/
|
|
1125
|
+
setActiveItemClass( item:TomItem ){
|
|
1126
|
+
const self = this;
|
|
1127
|
+
const last_active = self.control.querySelector('.last-active');
|
|
1128
|
+
if( last_active ) removeClasses(last_active as HTMLElement,'last-active');
|
|
1129
|
+
|
|
1130
|
+
addClasses(item,'active last-active');
|
|
1131
|
+
self.trigger('item_select', item);
|
|
1132
|
+
if( self.activeItems.indexOf(item) == -1 ){
|
|
1133
|
+
self.activeItems.push( item );
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
/**
|
|
1138
|
+
* Remove active item
|
|
1139
|
+
*
|
|
1140
|
+
*/
|
|
1141
|
+
removeActiveItem( item:TomItem ){
|
|
1142
|
+
var idx = this.activeItems.indexOf(item);
|
|
1143
|
+
this.activeItems.splice(idx, 1);
|
|
1144
|
+
removeClasses(item,'active');
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
/**
|
|
1148
|
+
* Clears all the active items
|
|
1149
|
+
*
|
|
1150
|
+
*/
|
|
1151
|
+
clearActiveItems(){
|
|
1152
|
+
removeClasses(this.activeItems,'active');
|
|
1153
|
+
this.activeItems = [];
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
/**
|
|
1157
|
+
* Sets the selected item in the dropdown menu
|
|
1158
|
+
* of available options.
|
|
1159
|
+
*
|
|
1160
|
+
*/
|
|
1161
|
+
setActiveOption( option:null|HTMLElement,scroll:boolean=true ):void{
|
|
1162
|
+
|
|
1163
|
+
if( option === this.activeOption ){
|
|
1164
|
+
return;
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
this.clearActiveOption();
|
|
1168
|
+
if( !option ) return;
|
|
1169
|
+
|
|
1170
|
+
this.activeOption = option;
|
|
1171
|
+
setAttr(this.focus_node,{'aria-activedescendant':option.getAttribute('id')});
|
|
1172
|
+
setAttr(option,{'aria-selected':'true'});
|
|
1173
|
+
addClasses(option,'active');
|
|
1174
|
+
if( scroll ) this.scrollToOption(option);
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
/**
|
|
1178
|
+
* Sets the dropdown_content scrollTop to display the option
|
|
1179
|
+
*
|
|
1180
|
+
*/
|
|
1181
|
+
scrollToOption( option:null|HTMLElement, behavior?:string ):void{
|
|
1182
|
+
|
|
1183
|
+
if( !option ) return;
|
|
1184
|
+
|
|
1185
|
+
const content = this.dropdown_content;
|
|
1186
|
+
const height_menu = content.clientHeight;
|
|
1187
|
+
const scrollTop = content.scrollTop || 0;
|
|
1188
|
+
const height_item = option.offsetHeight;
|
|
1189
|
+
const y = option.getBoundingClientRect().top - content.getBoundingClientRect().top + scrollTop;
|
|
1190
|
+
|
|
1191
|
+
if (y + height_item > height_menu + scrollTop) {
|
|
1192
|
+
this.scroll(y - height_menu + height_item, behavior);
|
|
1193
|
+
|
|
1194
|
+
} else if (y < scrollTop) {
|
|
1195
|
+
this.scroll(y, behavior);
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
/**
|
|
1200
|
+
* Scroll the dropdown to the given position
|
|
1201
|
+
*
|
|
1202
|
+
*/
|
|
1203
|
+
scroll( scrollTop:number, behavior?:string ):void{
|
|
1204
|
+
const content = this.dropdown_content;
|
|
1205
|
+
if( behavior ){
|
|
1206
|
+
content.style.scrollBehavior = behavior;
|
|
1207
|
+
}
|
|
1208
|
+
content.scrollTop = scrollTop;
|
|
1209
|
+
content.style.scrollBehavior = '';
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
/**
|
|
1213
|
+
* Clears the active option
|
|
1214
|
+
*
|
|
1215
|
+
*/
|
|
1216
|
+
clearActiveOption(){
|
|
1217
|
+
if( this.activeOption ){
|
|
1218
|
+
removeClasses(this.activeOption,'active');
|
|
1219
|
+
setAttr(this.activeOption,{'aria-selected':null});
|
|
1220
|
+
}
|
|
1221
|
+
this.activeOption = null;
|
|
1222
|
+
setAttr(this.focus_node,{'aria-activedescendant':null});
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
|
|
1226
|
+
/**
|
|
1227
|
+
* Selects all items (CTRL + A).
|
|
1228
|
+
*/
|
|
1229
|
+
selectAll() {
|
|
1230
|
+
const self = this;
|
|
1231
|
+
|
|
1232
|
+
if (self.settings.mode === 'single') return;
|
|
1233
|
+
|
|
1234
|
+
const activeItems = self.controlChildren();
|
|
1235
|
+
|
|
1236
|
+
if( !activeItems.length ) return;
|
|
1237
|
+
|
|
1238
|
+
self.inputState();
|
|
1239
|
+
self.close();
|
|
1240
|
+
|
|
1241
|
+
self.activeItems = activeItems;
|
|
1242
|
+
iterate( activeItems, (item:TomItem) => {
|
|
1243
|
+
self.setActiveItemClass(item);
|
|
1244
|
+
});
|
|
1245
|
+
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
/**
|
|
1249
|
+
* Determines if the control_input should be in a hidden or visible state
|
|
1250
|
+
*
|
|
1251
|
+
*/
|
|
1252
|
+
inputState(){
|
|
1253
|
+
var self = this;
|
|
1254
|
+
|
|
1255
|
+
if( !self.control.contains(self.control_input) ) return;
|
|
1256
|
+
|
|
1257
|
+
setAttr(self.control_input,{placeholder:self.settings.placeholder});
|
|
1258
|
+
|
|
1259
|
+
if( self.activeItems.length > 0 || (!self.isFocused && self.settings.hidePlaceholder && self.items.length > 0) ){
|
|
1260
|
+
self.setTextboxValue();
|
|
1261
|
+
self.isInputHidden = true;
|
|
1262
|
+
|
|
1263
|
+
}else{
|
|
1264
|
+
|
|
1265
|
+
if( self.settings.hidePlaceholder && self.items.length > 0 ){
|
|
1266
|
+
setAttr(self.control_input,{placeholder:''});
|
|
1267
|
+
}
|
|
1268
|
+
self.isInputHidden = false;
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
self.wrapper.classList.toggle('input-hidden', self.isInputHidden );
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
/**
|
|
1275
|
+
* Get the input value
|
|
1276
|
+
*/
|
|
1277
|
+
inputValue(){
|
|
1278
|
+
return this.control_input.value.trim();
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
/**
|
|
1282
|
+
* Gives the control focus.
|
|
1283
|
+
*/
|
|
1284
|
+
focus() {
|
|
1285
|
+
var self = this;
|
|
1286
|
+
if( self.isDisabled || self.isReadOnly) return;
|
|
1287
|
+
|
|
1288
|
+
self.ignoreFocus = true;
|
|
1289
|
+
|
|
1290
|
+
if( self.control_input.offsetWidth ){
|
|
1291
|
+
self.control_input.focus();
|
|
1292
|
+
}else{
|
|
1293
|
+
self.focus_node.focus();
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
setTimeout(() => {
|
|
1297
|
+
self.ignoreFocus = false;
|
|
1298
|
+
self.onFocus();
|
|
1299
|
+
}, 0);
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
/**
|
|
1303
|
+
* Forces the control out of focus.
|
|
1304
|
+
*
|
|
1305
|
+
*/
|
|
1306
|
+
blur():void {
|
|
1307
|
+
this.focus_node.blur();
|
|
1308
|
+
this.onBlur();
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
/**
|
|
1312
|
+
* Returns a function that scores an object
|
|
1313
|
+
* to show how good of a match it is to the
|
|
1314
|
+
* provided query.
|
|
1315
|
+
*
|
|
1316
|
+
* @return {function}
|
|
1317
|
+
*/
|
|
1318
|
+
getScoreFunction(query:string) {
|
|
1319
|
+
return this.sifter.getScoreFunction(query, this.getSearchOptions());
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
/**
|
|
1323
|
+
* Returns search options for sifter (the system
|
|
1324
|
+
* for scoring and sorting results).
|
|
1325
|
+
*
|
|
1326
|
+
* @see https://github.com/orchidjs/sifter.js
|
|
1327
|
+
* @return {object}
|
|
1328
|
+
*/
|
|
1329
|
+
getSearchOptions() {
|
|
1330
|
+
var settings = this.settings;
|
|
1331
|
+
var sort = settings.sortField;
|
|
1332
|
+
if (typeof settings.sortField === 'string') {
|
|
1333
|
+
sort = [{field: settings.sortField}];
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
return {
|
|
1337
|
+
fields : settings.searchField,
|
|
1338
|
+
conjunction : settings.searchConjunction,
|
|
1339
|
+
sort : sort,
|
|
1340
|
+
nesting : settings.nesting
|
|
1341
|
+
};
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
/**
|
|
1345
|
+
* Searches through available options and returns
|
|
1346
|
+
* a sorted array of matches.
|
|
1347
|
+
*
|
|
1348
|
+
*/
|
|
1349
|
+
search(query:string) : ReturnType<Sifter['search']>{
|
|
1350
|
+
var result, calculateScore;
|
|
1351
|
+
var self = this;
|
|
1352
|
+
var options = this.getSearchOptions();
|
|
1353
|
+
|
|
1354
|
+
// validate user-provided result scoring function
|
|
1355
|
+
if ( self.settings.score ){
|
|
1356
|
+
calculateScore = self.settings.score.call(self,query);
|
|
1357
|
+
if (typeof calculateScore !== 'function') {
|
|
1358
|
+
throw new Error('Tom Select "score" setting must be a function that returns a function');
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
// perform search
|
|
1363
|
+
if (query !== self.lastQuery) {
|
|
1364
|
+
self.lastQuery = query;
|
|
1365
|
+
// temp fix for https://github.com/orchidjs/tom-select/issues/987
|
|
1366
|
+
// UI crashed when more than 30 same chars in a row, prevent search and return empt result
|
|
1367
|
+
if (/(.)\1{15,}/.test(query)) {
|
|
1368
|
+
query = '';
|
|
1369
|
+
}
|
|
1370
|
+
result = self.sifter.search(query, Object.assign(options, { score: calculateScore }));
|
|
1371
|
+
self.currentResults = result;
|
|
1372
|
+
} else {
|
|
1373
|
+
result = Object.assign( {}, self.currentResults);
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
// filter out selected items
|
|
1377
|
+
if( self.settings.hideSelected ){
|
|
1378
|
+
result.items = result.items.filter((item) => {
|
|
1379
|
+
let hashed = hash_key(item.id);
|
|
1380
|
+
return !(hashed !== null && self.items.indexOf(hashed) !== -1 );
|
|
1381
|
+
});
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
return result;
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
/**
|
|
1388
|
+
* Refreshes the list of available options shown
|
|
1389
|
+
* in the autocomplete dropdown menu.
|
|
1390
|
+
*
|
|
1391
|
+
*/
|
|
1392
|
+
refreshOptions( triggerDropdown:boolean = true ){
|
|
1393
|
+
var i, j, k, n, optgroup, optgroups, html:DocumentFragment, has_create_option, active_group;
|
|
1394
|
+
var create;
|
|
1395
|
+
|
|
1396
|
+
type Group = {fragment:DocumentFragment,order:number,optgroup:string}
|
|
1397
|
+
const groups: {[key:string]:number} = {};
|
|
1398
|
+
const groups_order:Group[] = [];
|
|
1399
|
+
|
|
1400
|
+
var self = this;
|
|
1401
|
+
var query = self.inputValue();
|
|
1402
|
+
const same_query = query === self.lastQuery || (query == '' && self.lastQuery == null);
|
|
1403
|
+
var results = self.search(query);
|
|
1404
|
+
var active_option:HTMLElement|null = null;
|
|
1405
|
+
var show_dropdown = self.settings.shouldOpen || false;
|
|
1406
|
+
var dropdown_content = self.dropdown_content;
|
|
1407
|
+
|
|
1408
|
+
|
|
1409
|
+
if( same_query ){
|
|
1410
|
+
active_option = self.activeOption;
|
|
1411
|
+
|
|
1412
|
+
if( active_option ){
|
|
1413
|
+
active_group = active_option.closest('[data-group]') as HTMLElement;
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
// build markup
|
|
1418
|
+
n = results.items.length;
|
|
1419
|
+
if (typeof self.settings.maxOptions === 'number') {
|
|
1420
|
+
n = Math.min(n, self.settings.maxOptions);
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
if( n > 0 ){
|
|
1424
|
+
show_dropdown = true;
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1427
|
+
// get fragment for group and the position of the group in group_order
|
|
1428
|
+
const getGroupFragment = (optgroup:string,order:number):[number,DocumentFragment] => {
|
|
1429
|
+
|
|
1430
|
+
let group_order_i = groups[optgroup];
|
|
1431
|
+
|
|
1432
|
+
if( group_order_i !== undefined ){
|
|
1433
|
+
let order_group = groups_order[group_order_i];
|
|
1434
|
+
if( order_group !== undefined ){
|
|
1435
|
+
return [group_order_i,order_group.fragment];
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
let group_fragment = document.createDocumentFragment();
|
|
1440
|
+
group_order_i = groups_order.length;
|
|
1441
|
+
groups_order.push({fragment:group_fragment,order,optgroup});
|
|
1442
|
+
|
|
1443
|
+
return [group_order_i,group_fragment]
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
// render and group available options individually
|
|
1447
|
+
for (i = 0; i < n; i++) {
|
|
1448
|
+
|
|
1449
|
+
// get option dom element
|
|
1450
|
+
let item = results.items[i];
|
|
1451
|
+
if( !item ) continue;
|
|
1452
|
+
|
|
1453
|
+
let opt_value = item.id;
|
|
1454
|
+
let option = self.options[opt_value];
|
|
1455
|
+
|
|
1456
|
+
if( option === undefined ) continue;
|
|
1457
|
+
|
|
1458
|
+
let opt_hash = get_hash(opt_value);
|
|
1459
|
+
let option_el = self.getOption(opt_hash,true) as HTMLElement;
|
|
1460
|
+
|
|
1461
|
+
// toggle 'selected' class
|
|
1462
|
+
if( !self.settings.hideSelected ){
|
|
1463
|
+
option_el.classList.toggle('selected', self.items.includes(opt_hash) );
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
optgroup = option[self.settings.optgroupField] || '';
|
|
1467
|
+
optgroups = Array.isArray(optgroup) ? optgroup : [optgroup];
|
|
1468
|
+
|
|
1469
|
+
|
|
1470
|
+
for (j = 0, k = optgroups && optgroups.length; j < k; j++) {
|
|
1471
|
+
optgroup = optgroups[j];
|
|
1472
|
+
|
|
1473
|
+
let order = option.$order;
|
|
1474
|
+
let self_optgroup = self.optgroups[optgroup];
|
|
1475
|
+
if (self_optgroup === undefined && typeof self.settings.optionGroupRegister === 'function') {
|
|
1476
|
+
var regGroup;
|
|
1477
|
+
if (regGroup = self.settings.optionGroupRegister.apply(self, [optgroup])) {
|
|
1478
|
+
self.registerOptionGroup(regGroup);
|
|
1479
|
+
}
|
|
1480
|
+
}
|
|
1481
|
+
self_optgroup = self.optgroups[optgroup];
|
|
1482
|
+
if( self_optgroup === undefined ){
|
|
1483
|
+
optgroup = '';
|
|
1484
|
+
}else{
|
|
1485
|
+
order = self_optgroup.$order;
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
const [group_order_i,group_fragment] = getGroupFragment(optgroup,order);
|
|
1489
|
+
|
|
1490
|
+
|
|
1491
|
+
// nodes can only have one parent, so if the option is in mutple groups, we need a clone
|
|
1492
|
+
if( j > 0 ){
|
|
1493
|
+
option_el = option_el.cloneNode(true) as HTMLElement;
|
|
1494
|
+
setAttr(option_el,{id: option.$id+'-clone-'+j,'aria-selected':null});
|
|
1495
|
+
option_el.classList.add('ts-cloned');
|
|
1496
|
+
removeClasses(option_el,'active');
|
|
1497
|
+
|
|
1498
|
+
|
|
1499
|
+
// make sure we keep the activeOption in the same group
|
|
1500
|
+
if( self.activeOption && self.activeOption.dataset.value == opt_value ){
|
|
1501
|
+
if( active_group && active_group.dataset.group === optgroup.toString() ){
|
|
1502
|
+
active_option = option_el;
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
group_fragment.appendChild(option_el);
|
|
1508
|
+
if( optgroup != '' ){
|
|
1509
|
+
groups[optgroup] = group_order_i;
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
|
|
1514
|
+
// sort optgroups
|
|
1515
|
+
if( self.settings.lockOptgroupOrder ){
|
|
1516
|
+
groups_order.sort((a, b) => {
|
|
1517
|
+
return a.order - b.order;
|
|
1518
|
+
});
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
// render optgroup headers & join groups
|
|
1522
|
+
html = document.createDocumentFragment();
|
|
1523
|
+
iterate( groups_order, (group_order:Group) => {
|
|
1524
|
+
|
|
1525
|
+
let group_fragment = group_order.fragment;
|
|
1526
|
+
let optgroup = group_order.optgroup
|
|
1527
|
+
|
|
1528
|
+
if( !group_fragment || !group_fragment.children.length ) return;
|
|
1529
|
+
|
|
1530
|
+
let group_heading = self.optgroups[optgroup];
|
|
1531
|
+
|
|
1532
|
+
if( group_heading !== undefined ){
|
|
1533
|
+
|
|
1534
|
+
let group_options = document.createDocumentFragment();
|
|
1535
|
+
let header = self.render('optgroup_header', group_heading);
|
|
1536
|
+
append( group_options, header );
|
|
1537
|
+
append( group_options, group_fragment );
|
|
1538
|
+
|
|
1539
|
+
let group_html = self.render('optgroup', {group:group_heading,options:group_options} );
|
|
1540
|
+
|
|
1541
|
+
append( html, group_html );
|
|
1542
|
+
|
|
1543
|
+
} else {
|
|
1544
|
+
append( html, group_fragment );
|
|
1545
|
+
}
|
|
1546
|
+
});
|
|
1547
|
+
|
|
1548
|
+
dropdown_content.innerHTML = '';
|
|
1549
|
+
append( dropdown_content, html );
|
|
1550
|
+
|
|
1551
|
+
// highlight matching terms inline
|
|
1552
|
+
if (self.settings.highlight) {
|
|
1553
|
+
removeHighlight( dropdown_content );
|
|
1554
|
+
if (results.query.length && results.tokens.length) {
|
|
1555
|
+
iterate( results.tokens, (tok) => {
|
|
1556
|
+
highlight( dropdown_content, tok.regex);
|
|
1557
|
+
});
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
|
|
1561
|
+
// helper method for adding templates to dropdown
|
|
1562
|
+
var add_template = (template:TomTemplateNames) => {
|
|
1563
|
+
let content = self.render(template,{input:query});
|
|
1564
|
+
if( content ){
|
|
1565
|
+
show_dropdown = true;
|
|
1566
|
+
dropdown_content.insertBefore(content, dropdown_content.firstChild);
|
|
1567
|
+
}
|
|
1568
|
+
return content;
|
|
1569
|
+
};
|
|
1570
|
+
|
|
1571
|
+
|
|
1572
|
+
// add loading message
|
|
1573
|
+
if( self.loading ){
|
|
1574
|
+
add_template('loading');
|
|
1575
|
+
|
|
1576
|
+
// invalid query
|
|
1577
|
+
}else if( !self.settings.shouldLoad.call(self,query) ){
|
|
1578
|
+
add_template('not_loading');
|
|
1579
|
+
|
|
1580
|
+
// add no_results message
|
|
1581
|
+
}else if( results.items.length === 0 ){
|
|
1582
|
+
add_template('no_results');
|
|
1583
|
+
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1586
|
+
|
|
1587
|
+
|
|
1588
|
+
// add create option
|
|
1589
|
+
has_create_option = self.canCreate(query);
|
|
1590
|
+
if (has_create_option) {
|
|
1591
|
+
create = add_template('option_create');
|
|
1592
|
+
}
|
|
1593
|
+
|
|
1594
|
+
|
|
1595
|
+
// activate
|
|
1596
|
+
self.hasOptions = results.items.length > 0 || has_create_option;
|
|
1597
|
+
if( show_dropdown ){
|
|
1598
|
+
|
|
1599
|
+
if (results.items.length > 0) {
|
|
1600
|
+
|
|
1601
|
+
if( !active_option && self.settings.mode === 'single' && self.items[0] != undefined ){
|
|
1602
|
+
active_option = self.getOption(self.items[0]);
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1605
|
+
if( !dropdown_content.contains(active_option) ){
|
|
1606
|
+
|
|
1607
|
+
let active_index = 0;
|
|
1608
|
+
if( create && !self.settings.addPrecedence ){
|
|
1609
|
+
active_index = 1;
|
|
1610
|
+
}
|
|
1611
|
+
active_option = self.selectable()[active_index] as HTMLElement;
|
|
1612
|
+
}
|
|
1613
|
+
|
|
1614
|
+
}else if( create ){
|
|
1615
|
+
active_option = create;
|
|
1616
|
+
}
|
|
1617
|
+
|
|
1618
|
+
if( triggerDropdown && !self.isOpen ){
|
|
1619
|
+
self.open();
|
|
1620
|
+
self.scrollToOption(active_option,'auto');
|
|
1621
|
+
}
|
|
1622
|
+
self.setActiveOption(active_option);
|
|
1623
|
+
|
|
1624
|
+
}else{
|
|
1625
|
+
self.clearActiveOption();
|
|
1626
|
+
if( triggerDropdown && self.isOpen ){
|
|
1627
|
+
self.close(false); // if create_option=null, we want the dropdown to close but not reset the textbox value
|
|
1628
|
+
}
|
|
1629
|
+
}
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1632
|
+
/**
|
|
1633
|
+
* Return list of selectable options
|
|
1634
|
+
*
|
|
1635
|
+
*/
|
|
1636
|
+
selectable():NodeList{
|
|
1637
|
+
return this.dropdown_content.querySelectorAll('[data-selectable]');
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1640
|
+
|
|
1641
|
+
|
|
1642
|
+
/**
|
|
1643
|
+
* Adds an available option. If it already exists,
|
|
1644
|
+
* nothing will happen. Note: this does not refresh
|
|
1645
|
+
* the options list dropdown (use `refreshOptions`
|
|
1646
|
+
* for that).
|
|
1647
|
+
*
|
|
1648
|
+
* Usage:
|
|
1649
|
+
*
|
|
1650
|
+
* this.addOption(data)
|
|
1651
|
+
*
|
|
1652
|
+
*/
|
|
1653
|
+
addOption( data:TomOption, user_created = false ):false|string {
|
|
1654
|
+
const self = this;
|
|
1655
|
+
|
|
1656
|
+
// @deprecated 1.7.7
|
|
1657
|
+
// use addOptions( array, user_created ) for adding multiple options
|
|
1658
|
+
if( Array.isArray(data) ){
|
|
1659
|
+
self.addOptions( data, user_created);
|
|
1660
|
+
return false;
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1663
|
+
const key = hash_key(data[self.settings.valueField]);
|
|
1664
|
+
if( key === null || self.options.hasOwnProperty(key) ){
|
|
1665
|
+
return false;
|
|
1666
|
+
}
|
|
1667
|
+
|
|
1668
|
+
data.$order = data.$order || ++self.order;
|
|
1669
|
+
data.$id = self.inputId + '-opt-' + data.$order;
|
|
1670
|
+
self.options[key] = data;
|
|
1671
|
+
self.lastQuery = null;
|
|
1672
|
+
|
|
1673
|
+
if( user_created ){
|
|
1674
|
+
self.userOptions[key] = user_created;
|
|
1675
|
+
self.trigger('option_add', key, data);
|
|
1676
|
+
}
|
|
1677
|
+
|
|
1678
|
+
return key;
|
|
1679
|
+
}
|
|
1680
|
+
|
|
1681
|
+
/**
|
|
1682
|
+
* Add multiple options
|
|
1683
|
+
*
|
|
1684
|
+
*/
|
|
1685
|
+
addOptions( data:TomOption[], user_created = false ):void{
|
|
1686
|
+
iterate( data, (dat:TomOption) => {
|
|
1687
|
+
this.addOption(dat, user_created);
|
|
1688
|
+
});
|
|
1689
|
+
}
|
|
1690
|
+
|
|
1691
|
+
/**
|
|
1692
|
+
* @deprecated 1.7.7
|
|
1693
|
+
*/
|
|
1694
|
+
registerOption( data:TomOption ):false|string {
|
|
1695
|
+
return this.addOption(data);
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1698
|
+
/**
|
|
1699
|
+
* Registers an option group to the pool of option groups.
|
|
1700
|
+
*
|
|
1701
|
+
* @return {boolean|string}
|
|
1702
|
+
*/
|
|
1703
|
+
registerOptionGroup(data:TomOption) {
|
|
1704
|
+
var key = hash_key(data[this.settings.optgroupValueField]);
|
|
1705
|
+
|
|
1706
|
+
if ( key === null ) return false;
|
|
1707
|
+
|
|
1708
|
+
data.$order = data.$order || ++this.order;
|
|
1709
|
+
this.optgroups[key] = data;
|
|
1710
|
+
return key;
|
|
1711
|
+
}
|
|
1712
|
+
|
|
1713
|
+
/**
|
|
1714
|
+
* Registers a new optgroup for options
|
|
1715
|
+
* to be bucketed into.
|
|
1716
|
+
*
|
|
1717
|
+
*/
|
|
1718
|
+
addOptionGroup(id:string, data:TomOption) {
|
|
1719
|
+
var hashed_id;
|
|
1720
|
+
data[this.settings.optgroupValueField] = id;
|
|
1721
|
+
|
|
1722
|
+
if( hashed_id = this.registerOptionGroup(data) ){
|
|
1723
|
+
this.trigger('optgroup_add', hashed_id, data);
|
|
1724
|
+
}
|
|
1725
|
+
}
|
|
1726
|
+
|
|
1727
|
+
/**
|
|
1728
|
+
* Removes an existing option group.
|
|
1729
|
+
*
|
|
1730
|
+
*/
|
|
1731
|
+
removeOptionGroup(id:string) {
|
|
1732
|
+
if (this.optgroups.hasOwnProperty(id)) {
|
|
1733
|
+
delete this.optgroups[id];
|
|
1734
|
+
this.clearCache();
|
|
1735
|
+
this.trigger('optgroup_remove', id);
|
|
1736
|
+
}
|
|
1737
|
+
}
|
|
1738
|
+
|
|
1739
|
+
/**
|
|
1740
|
+
* Clears all existing option groups.
|
|
1741
|
+
*/
|
|
1742
|
+
clearOptionGroups() {
|
|
1743
|
+
this.optgroups = {};
|
|
1744
|
+
this.clearCache();
|
|
1745
|
+
this.trigger('optgroup_clear');
|
|
1746
|
+
}
|
|
1747
|
+
|
|
1748
|
+
/**
|
|
1749
|
+
* Updates an option available for selection. If
|
|
1750
|
+
* it is visible in the selected items or options
|
|
1751
|
+
* dropdown, it will be re-rendered automatically.
|
|
1752
|
+
*
|
|
1753
|
+
*/
|
|
1754
|
+
updateOption(value:string, data:TomOption) {
|
|
1755
|
+
const self = this;
|
|
1756
|
+
var item_new;
|
|
1757
|
+
var index_item;
|
|
1758
|
+
|
|
1759
|
+
const value_old = hash_key(value);
|
|
1760
|
+
const value_new = hash_key(data[self.settings.valueField]);
|
|
1761
|
+
|
|
1762
|
+
// sanity checks
|
|
1763
|
+
if( value_old === null ) return;
|
|
1764
|
+
|
|
1765
|
+
const data_old = self.options[value_old];
|
|
1766
|
+
|
|
1767
|
+
if( data_old == undefined ) return;
|
|
1768
|
+
if( typeof value_new !== 'string' ) throw new Error('Value must be set in option data');
|
|
1769
|
+
|
|
1770
|
+
|
|
1771
|
+
const option = self.getOption(value_old);
|
|
1772
|
+
const item = self.getItem(value_old);
|
|
1773
|
+
|
|
1774
|
+
|
|
1775
|
+
data.$order = data.$order || data_old.$order;
|
|
1776
|
+
delete self.options[value_old];
|
|
1777
|
+
|
|
1778
|
+
// invalidate render cache
|
|
1779
|
+
// don't remove existing node yet, we'll remove it after replacing it
|
|
1780
|
+
self.uncacheValue(value_new);
|
|
1781
|
+
|
|
1782
|
+
self.options[value_new] = data;
|
|
1783
|
+
|
|
1784
|
+
// update the option if it's in the dropdown
|
|
1785
|
+
if( option ){
|
|
1786
|
+
if( self.dropdown_content.contains(option) ){
|
|
1787
|
+
|
|
1788
|
+
const option_new = self._render('option', data);
|
|
1789
|
+
replaceNode(option, option_new);
|
|
1790
|
+
|
|
1791
|
+
if( self.activeOption === option ){
|
|
1792
|
+
self.setActiveOption(option_new);
|
|
1793
|
+
}
|
|
1794
|
+
}
|
|
1795
|
+
option.remove();
|
|
1796
|
+
}
|
|
1797
|
+
|
|
1798
|
+
// update the item if we have one
|
|
1799
|
+
if( item ){
|
|
1800
|
+
index_item = self.items.indexOf(value_old);
|
|
1801
|
+
if (index_item !== -1) {
|
|
1802
|
+
self.items.splice(index_item, 1, value_new);
|
|
1803
|
+
}
|
|
1804
|
+
|
|
1805
|
+
item_new = self._render('item', data);
|
|
1806
|
+
|
|
1807
|
+
if( item.classList.contains('active') ) addClasses(item_new,'active');
|
|
1808
|
+
|
|
1809
|
+
replaceNode( item, item_new);
|
|
1810
|
+
}
|
|
1811
|
+
|
|
1812
|
+
// invalidate last query because we might have updated the sortField
|
|
1813
|
+
self.lastQuery = null;
|
|
1814
|
+
}
|
|
1815
|
+
|
|
1816
|
+
/**
|
|
1817
|
+
* Removes a single option.
|
|
1818
|
+
*
|
|
1819
|
+
*/
|
|
1820
|
+
removeOption(value:string, silent?:boolean):void {
|
|
1821
|
+
const self = this;
|
|
1822
|
+
value = get_hash(value);
|
|
1823
|
+
|
|
1824
|
+
self.uncacheValue(value);
|
|
1825
|
+
|
|
1826
|
+
delete self.userOptions[value];
|
|
1827
|
+
delete self.options[value];
|
|
1828
|
+
self.lastQuery = null;
|
|
1829
|
+
self.trigger('option_remove', value);
|
|
1830
|
+
self.removeItem(value, silent);
|
|
1831
|
+
}
|
|
1832
|
+
|
|
1833
|
+
/**
|
|
1834
|
+
* Clears all options.
|
|
1835
|
+
*/
|
|
1836
|
+
clearOptions(filter?:TomClearFilter ) {
|
|
1837
|
+
|
|
1838
|
+
const boundFilter = (filter || this.clearFilter).bind(this);
|
|
1839
|
+
|
|
1840
|
+
this.loadedSearches = {};
|
|
1841
|
+
this.userOptions = {};
|
|
1842
|
+
this.clearCache();
|
|
1843
|
+
|
|
1844
|
+
const selected:TomOptions = {};
|
|
1845
|
+
iterate(this.options,(option:TomOption,key:string)=>{
|
|
1846
|
+
if( boundFilter(option,key as string) ){
|
|
1847
|
+
selected[key] = option;
|
|
1848
|
+
}
|
|
1849
|
+
});
|
|
1850
|
+
|
|
1851
|
+
this.options = this.sifter.items = selected;
|
|
1852
|
+
this.lastQuery = null;
|
|
1853
|
+
this.trigger('option_clear');
|
|
1854
|
+
}
|
|
1855
|
+
|
|
1856
|
+
/**
|
|
1857
|
+
* Used by clearOptions() to decide whether or not an option should be removed
|
|
1858
|
+
* Return true to keep an option, false to remove
|
|
1859
|
+
*
|
|
1860
|
+
*/
|
|
1861
|
+
clearFilter(option:TomOption,value:string){
|
|
1862
|
+
if( this.items.indexOf(value) >= 0 ){
|
|
1863
|
+
return true;
|
|
1864
|
+
}
|
|
1865
|
+
return false;
|
|
1866
|
+
}
|
|
1867
|
+
|
|
1868
|
+
/**
|
|
1869
|
+
* Returns the dom element of the option
|
|
1870
|
+
* matching the given value.
|
|
1871
|
+
*
|
|
1872
|
+
*/
|
|
1873
|
+
getOption(value:undefined|null|boolean|string|number, create:boolean=false):null|HTMLElement {
|
|
1874
|
+
|
|
1875
|
+
const hashed = hash_key(value);
|
|
1876
|
+
if( hashed === null ) return null;
|
|
1877
|
+
|
|
1878
|
+
const option = this.options[hashed];
|
|
1879
|
+
if( option != undefined ){
|
|
1880
|
+
|
|
1881
|
+
if( option.$div ){
|
|
1882
|
+
return option.$div;
|
|
1883
|
+
}
|
|
1884
|
+
|
|
1885
|
+
if( create ){
|
|
1886
|
+
return this._render('option', option);
|
|
1887
|
+
}
|
|
1888
|
+
}
|
|
1889
|
+
|
|
1890
|
+
return null;
|
|
1891
|
+
}
|
|
1892
|
+
|
|
1893
|
+
/**
|
|
1894
|
+
* Returns the dom element of the next or previous dom element of the same type
|
|
1895
|
+
* Note: adjacent options may not be adjacent DOM elements (optgroups)
|
|
1896
|
+
*
|
|
1897
|
+
*/
|
|
1898
|
+
getAdjacent( option:null|HTMLElement, direction:number, type:string = 'option' ) : HTMLElement|null{
|
|
1899
|
+
var self = this, all;
|
|
1900
|
+
|
|
1901
|
+
if( !option ){
|
|
1902
|
+
return null;
|
|
1903
|
+
}
|
|
1904
|
+
|
|
1905
|
+
if( type == 'item' ){
|
|
1906
|
+
all = self.controlChildren();
|
|
1907
|
+
}else{
|
|
1908
|
+
all = self.dropdown_content.querySelectorAll('[data-selectable]');
|
|
1909
|
+
}
|
|
1910
|
+
|
|
1911
|
+
for( let i = 0; i < all.length; i++ ){
|
|
1912
|
+
if( all[i] != option ){
|
|
1913
|
+
continue;
|
|
1914
|
+
}
|
|
1915
|
+
|
|
1916
|
+
if( direction > 0 ){
|
|
1917
|
+
return all[i+1] as HTMLElement;
|
|
1918
|
+
}
|
|
1919
|
+
|
|
1920
|
+
return all[i-1] as HTMLElement;
|
|
1921
|
+
}
|
|
1922
|
+
return null;
|
|
1923
|
+
}
|
|
1924
|
+
|
|
1925
|
+
|
|
1926
|
+
/**
|
|
1927
|
+
* Returns the dom element of the item
|
|
1928
|
+
* matching the given value.
|
|
1929
|
+
*
|
|
1930
|
+
*/
|
|
1931
|
+
getItem(item:string|TomItem|null):null|TomItem {
|
|
1932
|
+
|
|
1933
|
+
if( typeof item == 'object' ){
|
|
1934
|
+
return item;
|
|
1935
|
+
}
|
|
1936
|
+
|
|
1937
|
+
var value = hash_key(item);
|
|
1938
|
+
return value !== null
|
|
1939
|
+
? this.control.querySelector(`[data-value="${addSlashes(value)}"]`)
|
|
1940
|
+
: null;
|
|
1941
|
+
}
|
|
1942
|
+
|
|
1943
|
+
/**
|
|
1944
|
+
* "Selects" multiple items at once. Adds them to the list
|
|
1945
|
+
* at the current caret position.
|
|
1946
|
+
*
|
|
1947
|
+
*/
|
|
1948
|
+
addItems( values:string|string[], silent?:boolean ):void{
|
|
1949
|
+
var self = this;
|
|
1950
|
+
|
|
1951
|
+
var items = Array.isArray(values) ? values : [values];
|
|
1952
|
+
items = items.filter(x => self.items.indexOf(x) === -1);
|
|
1953
|
+
const last_item = items[items.length - 1];
|
|
1954
|
+
items.forEach(item => {
|
|
1955
|
+
self.isPending = (item !== last_item);
|
|
1956
|
+
self.addItem(item, silent);
|
|
1957
|
+
});
|
|
1958
|
+
}
|
|
1959
|
+
|
|
1960
|
+
/**
|
|
1961
|
+
* "Selects" an item. Adds it to the list
|
|
1962
|
+
* at the current caret position.
|
|
1963
|
+
*
|
|
1964
|
+
*/
|
|
1965
|
+
addItem( value:string, silent?:boolean ):void{
|
|
1966
|
+
var events = silent ? [] : ['change','dropdown_close'];
|
|
1967
|
+
|
|
1968
|
+
debounce_events(this, events, () => {
|
|
1969
|
+
var item, wasFull;
|
|
1970
|
+
const self = this;
|
|
1971
|
+
const inputMode = self.settings.mode;
|
|
1972
|
+
const hashed = hash_key(value);
|
|
1973
|
+
|
|
1974
|
+
if( hashed && self.items.indexOf(hashed) !== -1 ){
|
|
1975
|
+
|
|
1976
|
+
if( inputMode === 'single' ){
|
|
1977
|
+
self.close();
|
|
1978
|
+
}
|
|
1979
|
+
|
|
1980
|
+
if( inputMode === 'single' || !self.settings.duplicates ){
|
|
1981
|
+
return;
|
|
1982
|
+
}
|
|
1983
|
+
}
|
|
1984
|
+
|
|
1985
|
+
if (hashed === null || !self.options.hasOwnProperty(hashed)) return;
|
|
1986
|
+
if (inputMode === 'single') self.clear(silent);
|
|
1987
|
+
if (inputMode === 'multi' && self.isFull()) return;
|
|
1988
|
+
|
|
1989
|
+
item = self._render('item', self.options[hashed]);
|
|
1990
|
+
|
|
1991
|
+
if( self.control.contains(item) ){ // duplicates
|
|
1992
|
+
item = item.cloneNode(true) as HTMLElement;
|
|
1993
|
+
}
|
|
1994
|
+
|
|
1995
|
+
wasFull = self.isFull();
|
|
1996
|
+
self.items.splice(self.caretPos, 0, hashed);
|
|
1997
|
+
self.insertAtCaret(item);
|
|
1998
|
+
|
|
1999
|
+
if (self.isSetup) {
|
|
2000
|
+
|
|
2001
|
+
// update menu / remove the option (if this is not one item being added as part of series)
|
|
2002
|
+
if( !self.isPending && self.settings.hideSelected ){
|
|
2003
|
+
let option = self.getOption(hashed);
|
|
2004
|
+
let next = self.getAdjacent(option, 1);
|
|
2005
|
+
if( next ){
|
|
2006
|
+
self.setActiveOption(next);
|
|
2007
|
+
}
|
|
2008
|
+
}
|
|
2009
|
+
|
|
2010
|
+
//remove input value when enabled
|
|
2011
|
+
if(self.settings.clearAfterSelect) {
|
|
2012
|
+
self.setTextboxValue();
|
|
2013
|
+
}
|
|
2014
|
+
|
|
2015
|
+
// refreshOptions after setActiveOption(),
|
|
2016
|
+
// otherwise setActiveOption() will be called by refreshOptions() with the wrong value
|
|
2017
|
+
if( !self.isPending && !self.settings.closeAfterSelect ){
|
|
2018
|
+
self.refreshOptions(self.isFocused && inputMode !== 'single');
|
|
2019
|
+
}
|
|
2020
|
+
|
|
2021
|
+
// hide the menu if the maximum number of items have been selected or no options are left
|
|
2022
|
+
if( self.settings.closeAfterSelect != false && self.isFull() ){
|
|
2023
|
+
self.close();
|
|
2024
|
+
} else if (!self.isPending) {
|
|
2025
|
+
self.positionDropdown();
|
|
2026
|
+
}
|
|
2027
|
+
|
|
2028
|
+
self.trigger('item_add', hashed, item);
|
|
2029
|
+
|
|
2030
|
+
if (!self.isPending) {
|
|
2031
|
+
self.updateOriginalInput({silent: silent});
|
|
2032
|
+
}
|
|
2033
|
+
}
|
|
2034
|
+
|
|
2035
|
+
if (!self.isPending || (!wasFull && self.isFull())) {
|
|
2036
|
+
self.inputState();
|
|
2037
|
+
self.refreshState();
|
|
2038
|
+
}
|
|
2039
|
+
|
|
2040
|
+
});
|
|
2041
|
+
}
|
|
2042
|
+
|
|
2043
|
+
/**
|
|
2044
|
+
* Removes the selected item matching
|
|
2045
|
+
* the provided value.
|
|
2046
|
+
*
|
|
2047
|
+
*/
|
|
2048
|
+
removeItem( item:string|TomItem|null=null, silent?:boolean ){
|
|
2049
|
+
const self = this;
|
|
2050
|
+
item = self.getItem(item);
|
|
2051
|
+
|
|
2052
|
+
if( !item ) return;
|
|
2053
|
+
|
|
2054
|
+
var i,idx;
|
|
2055
|
+
const value = item.dataset.value;
|
|
2056
|
+
i = nodeIndex(item);
|
|
2057
|
+
|
|
2058
|
+
item.remove();
|
|
2059
|
+
if( item.classList.contains('active') ){
|
|
2060
|
+
idx = self.activeItems.indexOf(item);
|
|
2061
|
+
self.activeItems.splice(idx, 1);
|
|
2062
|
+
removeClasses(item,'active');
|
|
2063
|
+
}
|
|
2064
|
+
|
|
2065
|
+
self.items.splice(i, 1);
|
|
2066
|
+
self.lastQuery = null;
|
|
2067
|
+
if (!self.settings.persist && self.userOptions.hasOwnProperty(value)) {
|
|
2068
|
+
self.removeOption(value, silent);
|
|
2069
|
+
}
|
|
2070
|
+
|
|
2071
|
+
if (i < self.caretPos) {
|
|
2072
|
+
self.setCaret(self.caretPos - 1);
|
|
2073
|
+
}
|
|
2074
|
+
|
|
2075
|
+
self.updateOriginalInput({silent: silent});
|
|
2076
|
+
self.refreshState();
|
|
2077
|
+
self.positionDropdown();
|
|
2078
|
+
self.trigger('item_remove', value, item);
|
|
2079
|
+
|
|
2080
|
+
}
|
|
2081
|
+
|
|
2082
|
+
/**
|
|
2083
|
+
* Invokes the `create` method provided in the
|
|
2084
|
+
* TomSelect options that should provide the data
|
|
2085
|
+
* for the new item, given the user input.
|
|
2086
|
+
*
|
|
2087
|
+
* Once this completes, it will be added
|
|
2088
|
+
* to the item list.
|
|
2089
|
+
*
|
|
2090
|
+
*/
|
|
2091
|
+
createItem( input:null|string=null, callback:TomCreateCallback = ()=>{} ):boolean{
|
|
2092
|
+
|
|
2093
|
+
// triggerDropdown parameter @deprecated 2.1.1
|
|
2094
|
+
if( arguments.length === 3 ){
|
|
2095
|
+
callback = arguments[2];
|
|
2096
|
+
}
|
|
2097
|
+
if( typeof callback != 'function' ){
|
|
2098
|
+
callback = () => {};
|
|
2099
|
+
}
|
|
2100
|
+
|
|
2101
|
+
var self = this;
|
|
2102
|
+
var caret = self.caretPos;
|
|
2103
|
+
var output;
|
|
2104
|
+
input = input || self.inputValue();
|
|
2105
|
+
|
|
2106
|
+
if (!self.canCreate(input)) {
|
|
2107
|
+
const hash = hash_key(input);
|
|
2108
|
+
if( hash ){
|
|
2109
|
+
if( this.options[input] ){
|
|
2110
|
+
self.addItem(input);
|
|
2111
|
+
}
|
|
2112
|
+
}
|
|
2113
|
+
callback();
|
|
2114
|
+
return false;
|
|
2115
|
+
}
|
|
2116
|
+
|
|
2117
|
+
self.lock();
|
|
2118
|
+
|
|
2119
|
+
var created = false;
|
|
2120
|
+
var create = (data?:boolean|TomOption) => {
|
|
2121
|
+
self.unlock();
|
|
2122
|
+
|
|
2123
|
+
if (!data || typeof data !== 'object') return callback();
|
|
2124
|
+
var value = hash_key(data[self.settings.valueField]);
|
|
2125
|
+
if( typeof value !== 'string' ){
|
|
2126
|
+
return callback();
|
|
2127
|
+
}
|
|
2128
|
+
|
|
2129
|
+
self.setTextboxValue();
|
|
2130
|
+
self.addOption(data,true);
|
|
2131
|
+
self.setCaret(caret);
|
|
2132
|
+
self.addItem(value);
|
|
2133
|
+
callback(data);
|
|
2134
|
+
created = true;
|
|
2135
|
+
};
|
|
2136
|
+
|
|
2137
|
+
if( typeof self.settings.create === 'function' ){
|
|
2138
|
+
output = self.settings.create.call(this, input, create);
|
|
2139
|
+
}else{
|
|
2140
|
+
output = {
|
|
2141
|
+
[self.settings.labelField]: input,
|
|
2142
|
+
[self.settings.valueField]: input,
|
|
2143
|
+
};
|
|
2144
|
+
}
|
|
2145
|
+
|
|
2146
|
+
if( !created ){
|
|
2147
|
+
create(output);
|
|
2148
|
+
}
|
|
2149
|
+
|
|
2150
|
+
return true;
|
|
2151
|
+
}
|
|
2152
|
+
|
|
2153
|
+
/**
|
|
2154
|
+
* Re-renders the selected item lists.
|
|
2155
|
+
*/
|
|
2156
|
+
refreshItems() {
|
|
2157
|
+
var self = this;
|
|
2158
|
+
self.lastQuery = null;
|
|
2159
|
+
|
|
2160
|
+
if (self.isSetup) {
|
|
2161
|
+
self.addItems(self.items);
|
|
2162
|
+
}
|
|
2163
|
+
|
|
2164
|
+
self.updateOriginalInput();
|
|
2165
|
+
self.refreshState();
|
|
2166
|
+
}
|
|
2167
|
+
|
|
2168
|
+
/**
|
|
2169
|
+
* Updates all state-dependent attributes
|
|
2170
|
+
* and CSS classes.
|
|
2171
|
+
*/
|
|
2172
|
+
refreshState() {
|
|
2173
|
+
const self = this;
|
|
2174
|
+
|
|
2175
|
+
self.refreshValidityState();
|
|
2176
|
+
|
|
2177
|
+
const isFull = self.isFull();
|
|
2178
|
+
const isLocked = self.isLocked;
|
|
2179
|
+
|
|
2180
|
+
self.wrapper.classList.toggle('rtl',self.rtl);
|
|
2181
|
+
|
|
2182
|
+
|
|
2183
|
+
const wrap_classList = self.wrapper.classList;
|
|
2184
|
+
|
|
2185
|
+
wrap_classList.toggle('focus', self.isFocused)
|
|
2186
|
+
wrap_classList.toggle('disabled', self.isDisabled)
|
|
2187
|
+
wrap_classList.toggle('readonly', self.isReadOnly)
|
|
2188
|
+
wrap_classList.toggle('required', self.isRequired)
|
|
2189
|
+
wrap_classList.toggle('invalid', !self.isValid)
|
|
2190
|
+
wrap_classList.toggle('locked', isLocked)
|
|
2191
|
+
wrap_classList.toggle('full', isFull)
|
|
2192
|
+
wrap_classList.toggle('input-active', self.isFocused && !self.isInputHidden)
|
|
2193
|
+
wrap_classList.toggle('dropdown-active', self.isOpen)
|
|
2194
|
+
wrap_classList.toggle('has-options', isEmptyObject(self.options) )
|
|
2195
|
+
wrap_classList.toggle('has-items', self.items.length > 0);
|
|
2196
|
+
}
|
|
2197
|
+
|
|
2198
|
+
/**
|
|
2199
|
+
* Update the `required` attribute of both input and control input.
|
|
2200
|
+
*
|
|
2201
|
+
* The `required` property needs to be activated on the control input
|
|
2202
|
+
* for the error to be displayed at the right place. `required` also
|
|
2203
|
+
* needs to be temporarily deactivated on the input since the input is
|
|
2204
|
+
* hidden and can't show errors.
|
|
2205
|
+
*/
|
|
2206
|
+
refreshValidityState() {
|
|
2207
|
+
var self = this;
|
|
2208
|
+
|
|
2209
|
+
if( !self.input.validity ){
|
|
2210
|
+
return;
|
|
2211
|
+
}
|
|
2212
|
+
|
|
2213
|
+
self.isValid = self.input.validity.valid;
|
|
2214
|
+
self.isInvalid = !self.isValid;
|
|
2215
|
+
}
|
|
2216
|
+
|
|
2217
|
+
/**
|
|
2218
|
+
* Determines whether or not more items can be added
|
|
2219
|
+
* to the control without exceeding the user-defined maximum.
|
|
2220
|
+
*
|
|
2221
|
+
* @returns {boolean}
|
|
2222
|
+
*/
|
|
2223
|
+
isFull() {
|
|
2224
|
+
return this.settings.maxItems !== null && this.items.length >= this.settings.maxItems;
|
|
2225
|
+
}
|
|
2226
|
+
|
|
2227
|
+
/**
|
|
2228
|
+
* Refreshes the original <select> or <input>
|
|
2229
|
+
* element to reflect the current state.
|
|
2230
|
+
*
|
|
2231
|
+
*/
|
|
2232
|
+
updateOriginalInput( opts:TomArgObject = {} ){
|
|
2233
|
+
const self = this;
|
|
2234
|
+
var option, label;
|
|
2235
|
+
|
|
2236
|
+
const empty_option = self.input.querySelector('option[value=""]') as HTMLOptionElement;
|
|
2237
|
+
|
|
2238
|
+
if( self.is_select_tag ){
|
|
2239
|
+
|
|
2240
|
+
const selected:HTMLOptionElement[] = [];
|
|
2241
|
+
const has_selected:number = self.input.querySelectorAll('option:checked').length;
|
|
2242
|
+
|
|
2243
|
+
function AddSelected(option_el:HTMLOptionElement|null, value:string, label:string):HTMLOptionElement{
|
|
2244
|
+
|
|
2245
|
+
if( !option_el ){
|
|
2246
|
+
option_el = getDom('<option value="' + escape_html(value) + '">' + escape_html(label) + '</option>') as HTMLOptionElement;
|
|
2247
|
+
}
|
|
2248
|
+
|
|
2249
|
+
// don't move empty option from top of list
|
|
2250
|
+
// fixes bug in firefox https://bugzilla.mozilla.org/show_bug.cgi?id=1725293
|
|
2251
|
+
if( option_el != empty_option ){
|
|
2252
|
+
self.input.append(option_el);
|
|
2253
|
+
}
|
|
2254
|
+
|
|
2255
|
+
selected.push(option_el);
|
|
2256
|
+
|
|
2257
|
+
// marking empty option as selected can break validation
|
|
2258
|
+
// fixes https://github.com/orchidjs/tom-select/issues/303
|
|
2259
|
+
if( option_el != empty_option || has_selected > 0 ){
|
|
2260
|
+
option_el.selected = true;
|
|
2261
|
+
}
|
|
2262
|
+
|
|
2263
|
+
return option_el;
|
|
2264
|
+
}
|
|
2265
|
+
|
|
2266
|
+
// unselect all selected options
|
|
2267
|
+
self.input.querySelectorAll('option:checked').forEach((option_el:Element) => {
|
|
2268
|
+
(<HTMLOptionElement>option_el).selected = false;
|
|
2269
|
+
});
|
|
2270
|
+
|
|
2271
|
+
|
|
2272
|
+
// nothing selected?
|
|
2273
|
+
if( self.items.length == 0 && self.settings.mode == 'single' ){
|
|
2274
|
+
|
|
2275
|
+
AddSelected(empty_option, "", "");
|
|
2276
|
+
|
|
2277
|
+
// order selected <option> tags for values in self.items
|
|
2278
|
+
}else{
|
|
2279
|
+
|
|
2280
|
+
self.items.forEach((value)=>{
|
|
2281
|
+
option = self.options[value]!;
|
|
2282
|
+
label = option[self.settings.labelField] || '';
|
|
2283
|
+
|
|
2284
|
+
if( selected.includes(option.$option) ){
|
|
2285
|
+
const reuse_opt = self.input.querySelector(`option[value="${addSlashes(value)}"]:not(:checked)`) as HTMLOptionElement;
|
|
2286
|
+
AddSelected(reuse_opt, value, label);
|
|
2287
|
+
}else{
|
|
2288
|
+
option.$option = AddSelected(option.$option, value, label);
|
|
2289
|
+
}
|
|
2290
|
+
});
|
|
2291
|
+
|
|
2292
|
+
}
|
|
2293
|
+
|
|
2294
|
+
} else {
|
|
2295
|
+
self.input.value = self.getValue() as string;
|
|
2296
|
+
}
|
|
2297
|
+
|
|
2298
|
+
if (self.isSetup) {
|
|
2299
|
+
if (!opts.silent) {
|
|
2300
|
+
self.trigger('change', self.getValue() );
|
|
2301
|
+
}
|
|
2302
|
+
}
|
|
2303
|
+
}
|
|
2304
|
+
|
|
2305
|
+
/**
|
|
2306
|
+
* Shows the autocomplete dropdown containing
|
|
2307
|
+
* the available options.
|
|
2308
|
+
*/
|
|
2309
|
+
open() {
|
|
2310
|
+
var self = this;
|
|
2311
|
+
|
|
2312
|
+
if (self.isLocked || self.isOpen || (self.settings.mode === 'multi' && self.isFull())) return;
|
|
2313
|
+
self.isOpen = true;
|
|
2314
|
+
setAttr(self.focus_node,{'aria-expanded': 'true'});
|
|
2315
|
+
self.refreshState();
|
|
2316
|
+
applyCSS(self.dropdown,{visibility: 'hidden', display: 'block'});
|
|
2317
|
+
self.positionDropdown();
|
|
2318
|
+
applyCSS(self.dropdown,{visibility: 'visible', display: 'block'});
|
|
2319
|
+
self.focus();
|
|
2320
|
+
self.trigger('dropdown_open', self.dropdown);
|
|
2321
|
+
}
|
|
2322
|
+
|
|
2323
|
+
/**
|
|
2324
|
+
* Closes the autocomplete dropdown menu.
|
|
2325
|
+
*/
|
|
2326
|
+
close(setTextboxValue=true) {
|
|
2327
|
+
var self = this;
|
|
2328
|
+
var trigger = self.isOpen;
|
|
2329
|
+
|
|
2330
|
+
if( setTextboxValue ){
|
|
2331
|
+
|
|
2332
|
+
// before blur() to prevent form onchange event
|
|
2333
|
+
self.setTextboxValue();
|
|
2334
|
+
|
|
2335
|
+
if (self.settings.mode === 'single' && self.items.length) {
|
|
2336
|
+
self.inputState();
|
|
2337
|
+
}
|
|
2338
|
+
}
|
|
2339
|
+
|
|
2340
|
+
self.isOpen = false;
|
|
2341
|
+
setAttr(self.focus_node,{'aria-expanded': 'false'});
|
|
2342
|
+
applyCSS(self.dropdown,{display: 'none'});
|
|
2343
|
+
if( self.settings.hideSelected ){
|
|
2344
|
+
self.clearActiveOption();
|
|
2345
|
+
}
|
|
2346
|
+
self.refreshState();
|
|
2347
|
+
|
|
2348
|
+
if (trigger) self.trigger('dropdown_close', self.dropdown);
|
|
2349
|
+
}
|
|
2350
|
+
|
|
2351
|
+
/**
|
|
2352
|
+
* Calculates and applies the appropriate
|
|
2353
|
+
* position of the dropdown if dropdownParent = 'body'.
|
|
2354
|
+
* Otherwise, position is determined by css
|
|
2355
|
+
*/
|
|
2356
|
+
positionDropdown(){
|
|
2357
|
+
|
|
2358
|
+
if( this.settings.dropdownParent !== 'body' ){
|
|
2359
|
+
return;
|
|
2360
|
+
}
|
|
2361
|
+
|
|
2362
|
+
var context = this.control;
|
|
2363
|
+
var rect = context.getBoundingClientRect();
|
|
2364
|
+
var top = context.offsetHeight + rect.top + window.scrollY;
|
|
2365
|
+
var left = rect.left + window.scrollX;
|
|
2366
|
+
|
|
2367
|
+
|
|
2368
|
+
applyCSS(this.dropdown,{
|
|
2369
|
+
width : rect.width + 'px',
|
|
2370
|
+
top : top + 'px',
|
|
2371
|
+
left : left + 'px'
|
|
2372
|
+
});
|
|
2373
|
+
|
|
2374
|
+
}
|
|
2375
|
+
|
|
2376
|
+
/**
|
|
2377
|
+
* Resets / clears all selected items
|
|
2378
|
+
* from the control.
|
|
2379
|
+
*
|
|
2380
|
+
*/
|
|
2381
|
+
clear(silent?:boolean) {
|
|
2382
|
+
var self = this;
|
|
2383
|
+
|
|
2384
|
+
if (!self.items.length) return;
|
|
2385
|
+
|
|
2386
|
+
var items = self.controlChildren();
|
|
2387
|
+
iterate(items,(item:TomItem)=>{
|
|
2388
|
+
self.removeItem(item,true);
|
|
2389
|
+
});
|
|
2390
|
+
|
|
2391
|
+
self.inputState();
|
|
2392
|
+
if( !silent ) self.updateOriginalInput();
|
|
2393
|
+
self.trigger('clear');
|
|
2394
|
+
}
|
|
2395
|
+
|
|
2396
|
+
/**
|
|
2397
|
+
* A helper method for inserting an element
|
|
2398
|
+
* at the current caret position.
|
|
2399
|
+
*
|
|
2400
|
+
*/
|
|
2401
|
+
insertAtCaret(el:HTMLElement) {
|
|
2402
|
+
const self = this;
|
|
2403
|
+
const caret = self.caretPos;
|
|
2404
|
+
const target = self.control;
|
|
2405
|
+
|
|
2406
|
+
target.insertBefore(el, target.children[caret] || null);
|
|
2407
|
+
self.setCaret(caret + 1);
|
|
2408
|
+
}
|
|
2409
|
+
|
|
2410
|
+
/**
|
|
2411
|
+
* Removes the current selected item(s).
|
|
2412
|
+
*
|
|
2413
|
+
*/
|
|
2414
|
+
deleteSelection(e:KeyboardEvent):boolean {
|
|
2415
|
+
var direction, selection, caret, tail;
|
|
2416
|
+
var self = this;
|
|
2417
|
+
|
|
2418
|
+
direction = (e && e.keyCode === constants.KEY_BACKSPACE) ? -1 : 1;
|
|
2419
|
+
selection = getSelection(self.control_input);
|
|
2420
|
+
|
|
2421
|
+
|
|
2422
|
+
// determine items that will be removed
|
|
2423
|
+
const rm_items:TomItem[] = [];
|
|
2424
|
+
|
|
2425
|
+
if (self.activeItems.length) {
|
|
2426
|
+
|
|
2427
|
+
tail = getTail(self.activeItems, direction);
|
|
2428
|
+
caret = nodeIndex(tail);
|
|
2429
|
+
|
|
2430
|
+
if (direction > 0) { caret++; }
|
|
2431
|
+
|
|
2432
|
+
iterate(self.activeItems, (item:TomItem) => rm_items.push(item) );
|
|
2433
|
+
|
|
2434
|
+
} else if ((self.isFocused || self.settings.mode === 'single') && self.items.length) {
|
|
2435
|
+
const items = self.controlChildren();
|
|
2436
|
+
let rm_item;
|
|
2437
|
+
if( direction < 0 && selection.start === 0 && selection.length === 0 ){
|
|
2438
|
+
rm_item = items[self.caretPos - 1];
|
|
2439
|
+
|
|
2440
|
+
}else if( direction > 0 && selection.start === self.inputValue().length ){
|
|
2441
|
+
rm_item = items[self.caretPos];
|
|
2442
|
+
}
|
|
2443
|
+
|
|
2444
|
+
if( rm_item !== undefined ){
|
|
2445
|
+
rm_items.push( rm_item );
|
|
2446
|
+
}
|
|
2447
|
+
}
|
|
2448
|
+
|
|
2449
|
+
if( !self.shouldDelete(rm_items,e) ){
|
|
2450
|
+
return false;
|
|
2451
|
+
}
|
|
2452
|
+
|
|
2453
|
+
preventDefault(e,true);
|
|
2454
|
+
|
|
2455
|
+
// perform removal
|
|
2456
|
+
if (typeof caret !== 'undefined') {
|
|
2457
|
+
self.setCaret(caret);
|
|
2458
|
+
}
|
|
2459
|
+
|
|
2460
|
+
while( rm_items.length ){
|
|
2461
|
+
self.removeItem(rm_items.pop());
|
|
2462
|
+
}
|
|
2463
|
+
|
|
2464
|
+
self.inputState();
|
|
2465
|
+
self.positionDropdown();
|
|
2466
|
+
self.refreshOptions(false);
|
|
2467
|
+
|
|
2468
|
+
return true;
|
|
2469
|
+
}
|
|
2470
|
+
|
|
2471
|
+
/**
|
|
2472
|
+
* Return true if the items should be deleted
|
|
2473
|
+
*/
|
|
2474
|
+
shouldDelete(items:TomItem[],evt:MouseEvent|KeyboardEvent){
|
|
2475
|
+
|
|
2476
|
+
const values = items.map(item => item.dataset.value);
|
|
2477
|
+
|
|
2478
|
+
// allow the callback to abort
|
|
2479
|
+
if( !values.length || (typeof this.settings.onDelete === 'function' && this.settings.onDelete.call(this,values,evt) === false) ){
|
|
2480
|
+
return false;
|
|
2481
|
+
}
|
|
2482
|
+
|
|
2483
|
+
return true;
|
|
2484
|
+
}
|
|
2485
|
+
|
|
2486
|
+
/**
|
|
2487
|
+
* Selects the previous / next item (depending on the `direction` argument).
|
|
2488
|
+
*
|
|
2489
|
+
* > 0 - right
|
|
2490
|
+
* < 0 - left
|
|
2491
|
+
*
|
|
2492
|
+
*/
|
|
2493
|
+
advanceSelection(direction:number, e?:MouseEvent|KeyboardEvent) {
|
|
2494
|
+
var last_active, adjacent, self = this;
|
|
2495
|
+
|
|
2496
|
+
if (self.rtl) direction *= -1;
|
|
2497
|
+
if( self.inputValue().length ) return;
|
|
2498
|
+
|
|
2499
|
+
|
|
2500
|
+
// add or remove to active items
|
|
2501
|
+
if( isKeyDown(constants.KEY_SHORTCUT,e) || isKeyDown('shiftKey',e) ){
|
|
2502
|
+
|
|
2503
|
+
last_active = self.getLastActive(direction);
|
|
2504
|
+
if( last_active ){
|
|
2505
|
+
|
|
2506
|
+
if( !last_active.classList.contains('active') ){
|
|
2507
|
+
adjacent = last_active;
|
|
2508
|
+
}else{
|
|
2509
|
+
adjacent = self.getAdjacent(last_active,direction,'item');
|
|
2510
|
+
}
|
|
2511
|
+
|
|
2512
|
+
// if no active item, get items adjacent to the control input
|
|
2513
|
+
}else if( direction > 0 ){
|
|
2514
|
+
adjacent = self.control_input.nextElementSibling;
|
|
2515
|
+
}else{
|
|
2516
|
+
adjacent = self.control_input.previousElementSibling;
|
|
2517
|
+
}
|
|
2518
|
+
|
|
2519
|
+
|
|
2520
|
+
if( adjacent ){
|
|
2521
|
+
if( adjacent.classList.contains('active') ){
|
|
2522
|
+
self.removeActiveItem(last_active);
|
|
2523
|
+
}
|
|
2524
|
+
self.setActiveItemClass(adjacent); // mark as last_active !! after removeActiveItem() on last_active
|
|
2525
|
+
}
|
|
2526
|
+
|
|
2527
|
+
// move caret to the left or right
|
|
2528
|
+
}else{
|
|
2529
|
+
self.moveCaret(direction);
|
|
2530
|
+
}
|
|
2531
|
+
}
|
|
2532
|
+
|
|
2533
|
+
moveCaret(direction:number){}
|
|
2534
|
+
|
|
2535
|
+
/**
|
|
2536
|
+
* Get the last active item
|
|
2537
|
+
*
|
|
2538
|
+
*/
|
|
2539
|
+
getLastActive(direction?:number){
|
|
2540
|
+
|
|
2541
|
+
let last_active = this.control.querySelector('.last-active');
|
|
2542
|
+
if( last_active ){
|
|
2543
|
+
return last_active;
|
|
2544
|
+
}
|
|
2545
|
+
|
|
2546
|
+
|
|
2547
|
+
var result = this.control.querySelectorAll('.active');
|
|
2548
|
+
if( result ){
|
|
2549
|
+
return getTail(result,direction);
|
|
2550
|
+
}
|
|
2551
|
+
}
|
|
2552
|
+
|
|
2553
|
+
|
|
2554
|
+
/**
|
|
2555
|
+
* Moves the caret to the specified index.
|
|
2556
|
+
*
|
|
2557
|
+
* The input must be moved by leaving it in place and moving the
|
|
2558
|
+
* siblings, due to the fact that focus cannot be restored once lost
|
|
2559
|
+
* on mobile webkit devices
|
|
2560
|
+
*
|
|
2561
|
+
*/
|
|
2562
|
+
setCaret(new_pos:number) {
|
|
2563
|
+
this.caretPos = this.items.length;
|
|
2564
|
+
}
|
|
2565
|
+
|
|
2566
|
+
/**
|
|
2567
|
+
* Return list of item dom elements
|
|
2568
|
+
*
|
|
2569
|
+
*/
|
|
2570
|
+
controlChildren():TomItem[]{
|
|
2571
|
+
return Array.from( this.control.querySelectorAll('[data-ts-item]') ) as TomItem[];
|
|
2572
|
+
}
|
|
2573
|
+
|
|
2574
|
+
/**
|
|
2575
|
+
* Disables user input on the control. Used while
|
|
2576
|
+
* items are being asynchronously created.
|
|
2577
|
+
*/
|
|
2578
|
+
lock() {
|
|
2579
|
+
this.setLocked(true);
|
|
2580
|
+
}
|
|
2581
|
+
|
|
2582
|
+
/**
|
|
2583
|
+
* Re-enables user input on the control.
|
|
2584
|
+
*/
|
|
2585
|
+
unlock() {
|
|
2586
|
+
this.setLocked(false);
|
|
2587
|
+
}
|
|
2588
|
+
|
|
2589
|
+
/**
|
|
2590
|
+
* Disable or enable user input on the control
|
|
2591
|
+
*/
|
|
2592
|
+
setLocked( lock:boolean = this.isReadOnly || this.isDisabled ){
|
|
2593
|
+
this.isLocked = lock;
|
|
2594
|
+
this.refreshState();
|
|
2595
|
+
}
|
|
2596
|
+
|
|
2597
|
+
/**
|
|
2598
|
+
* Disables user input on the control completely.
|
|
2599
|
+
* While disabled, it cannot receive focus.
|
|
2600
|
+
*/
|
|
2601
|
+
disable() {
|
|
2602
|
+
this.setDisabled(true);
|
|
2603
|
+
this.close();
|
|
2604
|
+
}
|
|
2605
|
+
|
|
2606
|
+
/**
|
|
2607
|
+
* Enables the control so that it can respond
|
|
2608
|
+
* to focus and user input.
|
|
2609
|
+
*/
|
|
2610
|
+
enable() {
|
|
2611
|
+
this.setDisabled(false);
|
|
2612
|
+
}
|
|
2613
|
+
|
|
2614
|
+
setDisabled(disabled:boolean){
|
|
2615
|
+
this.focus_node.tabIndex = disabled ? -1 : this.tabIndex;
|
|
2616
|
+
this.isDisabled = disabled;
|
|
2617
|
+
this.input.disabled = disabled;
|
|
2618
|
+
this.control_input.disabled = disabled;
|
|
2619
|
+
this.setLocked();
|
|
2620
|
+
}
|
|
2621
|
+
|
|
2622
|
+
setReadOnly(isReadOnly:boolean){
|
|
2623
|
+
this.isReadOnly = isReadOnly;
|
|
2624
|
+
this.input.readOnly = isReadOnly;
|
|
2625
|
+
this.control_input.readOnly = isReadOnly;
|
|
2626
|
+
this.setLocked();
|
|
2627
|
+
}
|
|
2628
|
+
|
|
2629
|
+
/**
|
|
2630
|
+
* Completely destroys the control and
|
|
2631
|
+
* unbinds all event listeners so that it can
|
|
2632
|
+
* be garbage collected.
|
|
2633
|
+
*/
|
|
2634
|
+
destroy() {
|
|
2635
|
+
var self = this;
|
|
2636
|
+
var revertSettings = self.revertSettings;
|
|
2637
|
+
|
|
2638
|
+
self.trigger('destroy');
|
|
2639
|
+
self.off();
|
|
2640
|
+
self.wrapper.remove();
|
|
2641
|
+
self.dropdown.remove();
|
|
2642
|
+
|
|
2643
|
+
self.input.innerHTML = revertSettings.innerHTML;
|
|
2644
|
+
self.input.tabIndex = revertSettings.tabIndex;
|
|
2645
|
+
|
|
2646
|
+
removeClasses(self.input,'tomselected','ts-hidden-accessible');
|
|
2647
|
+
|
|
2648
|
+
self._destroy();
|
|
2649
|
+
|
|
2650
|
+
delete self.input.tomselect;
|
|
2651
|
+
}
|
|
2652
|
+
|
|
2653
|
+
/**
|
|
2654
|
+
* A helper method for rendering "item" and
|
|
2655
|
+
* "option" templates, given the data.
|
|
2656
|
+
*
|
|
2657
|
+
*/
|
|
2658
|
+
render( templateName:TomTemplateNames, data?:any ):null|HTMLElement{
|
|
2659
|
+
var id, html;
|
|
2660
|
+
const self = this;
|
|
2661
|
+
|
|
2662
|
+
if( typeof this.settings.render[templateName] !== 'function' ){
|
|
2663
|
+
return null;
|
|
2664
|
+
}
|
|
2665
|
+
|
|
2666
|
+
// render markup
|
|
2667
|
+
html = self.settings.render[templateName].call(this, data, escape_html);
|
|
2668
|
+
|
|
2669
|
+
if( !html ){
|
|
2670
|
+
return null;
|
|
2671
|
+
}
|
|
2672
|
+
|
|
2673
|
+
html = getDom( html );
|
|
2674
|
+
|
|
2675
|
+
// add mandatory attributes
|
|
2676
|
+
if (templateName === 'option' || templateName === 'option_create') {
|
|
2677
|
+
|
|
2678
|
+
if( data[self.settings.disabledField] ){
|
|
2679
|
+
setAttr(html,{'aria-disabled':'true'});
|
|
2680
|
+
}else{
|
|
2681
|
+
setAttr(html,{'data-selectable': ''});
|
|
2682
|
+
}
|
|
2683
|
+
|
|
2684
|
+
}else if (templateName === 'optgroup') {
|
|
2685
|
+
id = data.group[self.settings.optgroupValueField];
|
|
2686
|
+
setAttr(html,{'data-group': id});
|
|
2687
|
+
if(data.group[self.settings.disabledField]) {
|
|
2688
|
+
setAttr(html,{'data-disabled': ''});
|
|
2689
|
+
}
|
|
2690
|
+
}
|
|
2691
|
+
|
|
2692
|
+
if (templateName === 'option' || templateName === 'item') {
|
|
2693
|
+
const value = get_hash(data[self.settings.valueField]);
|
|
2694
|
+
setAttr(html,{'data-value': value });
|
|
2695
|
+
|
|
2696
|
+
|
|
2697
|
+
// make sure we have some classes if a template is overwritten
|
|
2698
|
+
if( templateName === 'item' ){
|
|
2699
|
+
addClasses(html,self.settings.itemClass);
|
|
2700
|
+
setAttr(html,{'data-ts-item':''});
|
|
2701
|
+
}else{
|
|
2702
|
+
addClasses(html,self.settings.optionClass);
|
|
2703
|
+
setAttr(html,{
|
|
2704
|
+
role:'option',
|
|
2705
|
+
id:data.$id
|
|
2706
|
+
});
|
|
2707
|
+
|
|
2708
|
+
// update cache
|
|
2709
|
+
data.$div = html;
|
|
2710
|
+
self.options[value] = data;
|
|
2711
|
+
}
|
|
2712
|
+
|
|
2713
|
+
|
|
2714
|
+
}
|
|
2715
|
+
|
|
2716
|
+
return html;
|
|
2717
|
+
|
|
2718
|
+
}
|
|
2719
|
+
|
|
2720
|
+
|
|
2721
|
+
/**
|
|
2722
|
+
* Type guarded rendering
|
|
2723
|
+
*
|
|
2724
|
+
*/
|
|
2725
|
+
_render( templateName:TomTemplateNames, data?:any ):HTMLElement{
|
|
2726
|
+
const html = this.render(templateName, data);
|
|
2727
|
+
|
|
2728
|
+
if( html == null ){
|
|
2729
|
+
throw 'HTMLElement expected';
|
|
2730
|
+
}
|
|
2731
|
+
return html;
|
|
2732
|
+
}
|
|
2733
|
+
|
|
2734
|
+
|
|
2735
|
+
/**
|
|
2736
|
+
* Clears the render cache for a template. If
|
|
2737
|
+
* no template is given, clears all render
|
|
2738
|
+
* caches.
|
|
2739
|
+
*
|
|
2740
|
+
*/
|
|
2741
|
+
clearCache():void{
|
|
2742
|
+
|
|
2743
|
+
iterate(this.options, (option:TomOption)=>{
|
|
2744
|
+
if( option.$div ){
|
|
2745
|
+
option.$div.remove();
|
|
2746
|
+
delete option.$div;
|
|
2747
|
+
}
|
|
2748
|
+
});
|
|
2749
|
+
|
|
2750
|
+
}
|
|
2751
|
+
|
|
2752
|
+
/**
|
|
2753
|
+
* Removes a value from item and option caches
|
|
2754
|
+
*
|
|
2755
|
+
*/
|
|
2756
|
+
uncacheValue(value:string){
|
|
2757
|
+
|
|
2758
|
+
const option_el = this.getOption(value);
|
|
2759
|
+
if( option_el ) option_el.remove();
|
|
2760
|
+
|
|
2761
|
+
}
|
|
2762
|
+
|
|
2763
|
+
/**
|
|
2764
|
+
* Determines whether or not to display the
|
|
2765
|
+
* create item prompt, given a user input.
|
|
2766
|
+
*
|
|
2767
|
+
*/
|
|
2768
|
+
canCreate( input:string ):boolean {
|
|
2769
|
+
return this.settings.create && (input.length > 0) && (this.settings.createFilter as TomCreateFilter ).call(this, input);
|
|
2770
|
+
}
|
|
2771
|
+
|
|
2772
|
+
|
|
2773
|
+
/**
|
|
2774
|
+
* Wraps this.`method` so that `new_fn` can be invoked 'before', 'after', or 'instead' of the original method
|
|
2775
|
+
*
|
|
2776
|
+
* this.hook('instead','onKeyDown',function( arg1, arg2 ...){
|
|
2777
|
+
*
|
|
2778
|
+
* });
|
|
2779
|
+
*/
|
|
2780
|
+
hook( when:string, method:string, new_fn:any ){
|
|
2781
|
+
var self = this;
|
|
2782
|
+
var orig_method = self[method];
|
|
2783
|
+
|
|
2784
|
+
|
|
2785
|
+
self[method] = function(){
|
|
2786
|
+
var result, result_new;
|
|
2787
|
+
|
|
2788
|
+
if( when === 'after' ){
|
|
2789
|
+
result = orig_method.apply(self, arguments);
|
|
2790
|
+
}
|
|
2791
|
+
|
|
2792
|
+
result_new = new_fn.apply(self, arguments );
|
|
2793
|
+
|
|
2794
|
+
if( when === 'instead' ){
|
|
2795
|
+
return result_new;
|
|
2796
|
+
}
|
|
2797
|
+
|
|
2798
|
+
if( when === 'before' ){
|
|
2799
|
+
result = orig_method.apply(self, arguments);
|
|
2800
|
+
}
|
|
2801
|
+
|
|
2802
|
+
return result;
|
|
2803
|
+
};
|
|
2804
|
+
|
|
2805
|
+
}
|
|
2806
|
+
|
|
2807
|
+
};
|