govuk_publishing_components 58.1.0 → 58.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/app/assets/images/select-with-search/cross-icon.svg +6 -0
- data/app/assets/javascripts/govuk_publishing_components/analytics-ga4/ga4-search-tracker.js +4 -0
- data/app/assets/javascripts/govuk_publishing_components/components/select-with-search.js +57 -0
- data/app/assets/stylesheets/govuk_publishing_components/_all_components.scss +1 -0
- data/app/assets/stylesheets/govuk_publishing_components/components/_layout-super-navigation-header.scss +7 -2
- data/app/assets/stylesheets/govuk_publishing_components/components/_select-with-search.scss +168 -0
- data/app/assets/stylesheets/govuk_publishing_components/components/_select.scss +6 -0
- data/app/assets/stylesheets/govuk_publishing_components/components/govspeak/_contact.scss +10 -10
- data/app/assets/stylesheets/govuk_publishing_components/components/govspeak/_typography.scss +3 -16
- data/app/views/govuk_publishing_components/components/_add_another.html.erb +0 -1
- data/app/views/govuk_publishing_components/components/_layout_super_navigation_header.html.erb +1 -1
- data/app/views/govuk_publishing_components/components/_select.html.erb +22 -23
- data/app/views/govuk_publishing_components/components/_select_with_search.html.erb +14 -0
- data/app/views/govuk_publishing_components/components/docs/select.yml +11 -0
- data/app/views/govuk_publishing_components/components/docs/select_with_search.yml +196 -0
- data/lib/govuk_publishing_components/presenters/select_helper.rb +8 -5
- data/lib/govuk_publishing_components/presenters/select_with_search_helper.rb +92 -0
- data/lib/govuk_publishing_components/version.rb +1 -1
- data/lib/govuk_publishing_components.rb +1 -0
- data/node_modules/choices.js/LICENSE +21 -0
- data/node_modules/choices.js/README.md +1360 -0
- data/node_modules/choices.js/package.json +173 -0
- data/node_modules/choices.js/public/assets/scripts/choices.js +5230 -0
- data/node_modules/choices.js/public/assets/scripts/choices.min.js +2 -0
- data/node_modules/choices.js/public/assets/scripts/choices.mjs +5222 -0
- data/node_modules/choices.js/public/assets/scripts/choices.search-basic.js +4748 -0
- data/node_modules/choices.js/public/assets/scripts/choices.search-basic.min.js +2 -0
- data/node_modules/choices.js/public/assets/scripts/choices.search-basic.mjs +4740 -0
- data/node_modules/choices.js/public/assets/scripts/choices.search-kmp.js +3631 -0
- data/node_modules/choices.js/public/assets/scripts/choices.search-kmp.min.js +2 -0
- data/node_modules/choices.js/public/assets/scripts/choices.search-kmp.mjs +3623 -0
- data/node_modules/choices.js/public/assets/scripts/choices.search-prefix.js +3590 -0
- data/node_modules/choices.js/public/assets/scripts/choices.search-prefix.min.js +2 -0
- data/node_modules/choices.js/public/assets/scripts/choices.search-prefix.mjs +3582 -0
- data/node_modules/choices.js/public/assets/styles/base.css +180 -0
- data/node_modules/choices.js/public/assets/styles/base.css.map +1 -0
- data/node_modules/choices.js/public/assets/styles/base.min.css +1 -0
- data/node_modules/choices.js/public/assets/styles/choices.css +338 -0
- data/node_modules/choices.js/public/assets/styles/choices.css.map +1 -0
- data/node_modules/choices.js/public/assets/styles/choices.min.css +1 -0
- data/node_modules/choices.js/public/types/src/index.d.ts +6 -0
- data/node_modules/choices.js/public/types/src/scripts/actions/choices.d.ts +30 -0
- data/node_modules/choices.js/public/types/src/scripts/actions/groups.d.ts +8 -0
- data/node_modules/choices.js/public/types/src/scripts/actions/items.d.ts +17 -0
- data/node_modules/choices.js/public/types/src/scripts/choices.d.ts +210 -0
- data/node_modules/choices.js/public/types/src/scripts/components/container.d.ts +36 -0
- data/node_modules/choices.js/public/types/src/scripts/components/dropdown.d.ts +21 -0
- data/node_modules/choices.js/public/types/src/scripts/components/index.d.ts +7 -0
- data/node_modules/choices.js/public/types/src/scripts/components/input.d.ts +37 -0
- data/node_modules/choices.js/public/types/src/scripts/components/list.d.ts +14 -0
- data/node_modules/choices.js/public/types/src/scripts/components/wrapped-element.d.ts +21 -0
- data/node_modules/choices.js/public/types/src/scripts/components/wrapped-input.d.ts +3 -0
- data/node_modules/choices.js/public/types/src/scripts/components/wrapped-select.d.ts +20 -0
- data/node_modules/choices.js/public/types/src/scripts/constants.d.ts +1 -0
- data/node_modules/choices.js/public/types/src/scripts/defaults.d.ts +4 -0
- data/node_modules/choices.js/public/types/src/scripts/interfaces/action-type.d.ts +13 -0
- data/node_modules/choices.js/public/types/src/scripts/interfaces/build-flags.d.ts +11 -0
- data/node_modules/choices.js/public/types/src/scripts/interfaces/choice-full.d.ts +23 -0
- data/node_modules/choices.js/public/types/src/scripts/interfaces/class-names.d.ts +61 -0
- data/node_modules/choices.js/public/types/src/scripts/interfaces/event-choice.d.ts +7 -0
- data/node_modules/choices.js/public/types/src/scripts/interfaces/event-type.d.ts +14 -0
- data/node_modules/choices.js/public/types/src/scripts/interfaces/group-full.d.ts +10 -0
- data/node_modules/choices.js/public/types/src/scripts/interfaces/index.d.ts +14 -0
- data/node_modules/choices.js/public/types/src/scripts/interfaces/input-choice.d.ts +15 -0
- data/node_modules/choices.js/public/types/src/scripts/interfaces/input-group.d.ts +10 -0
- data/node_modules/choices.js/public/types/src/scripts/interfaces/item.d.ts +17 -0
- data/node_modules/choices.js/public/types/src/scripts/interfaces/keycode-map.d.ts +13 -0
- data/node_modules/choices.js/public/types/src/scripts/interfaces/options.d.ts +566 -0
- data/node_modules/choices.js/public/types/src/scripts/interfaces/passed-element-type.d.ts +7 -0
- data/node_modules/choices.js/public/types/src/scripts/interfaces/passed-element.d.ts +95 -0
- data/node_modules/choices.js/public/types/src/scripts/interfaces/position-options-type.d.ts +1 -0
- data/node_modules/choices.js/public/types/src/scripts/interfaces/search.d.ts +11 -0
- data/node_modules/choices.js/public/types/src/scripts/interfaces/state.d.ts +10 -0
- data/node_modules/choices.js/public/types/src/scripts/interfaces/store.d.ts +64 -0
- data/node_modules/choices.js/public/types/src/scripts/interfaces/string-pre-escaped.d.ts +3 -0
- data/node_modules/choices.js/public/types/src/scripts/interfaces/string-untrusted.d.ts +4 -0
- data/node_modules/choices.js/public/types/src/scripts/interfaces/templates.d.ts +29 -0
- data/node_modules/choices.js/public/types/src/scripts/interfaces/types.d.ts +18 -0
- data/node_modules/choices.js/public/types/src/scripts/lib/choice-input.d.ts +9 -0
- data/node_modules/choices.js/public/types/src/scripts/lib/html-guard-statements.d.ts +4 -0
- data/node_modules/choices.js/public/types/src/scripts/lib/utils.d.ts +31 -0
- data/node_modules/choices.js/public/types/src/scripts/reducers/choices.d.ts +8 -0
- data/node_modules/choices.js/public/types/src/scripts/reducers/groups.d.ts +8 -0
- data/node_modules/choices.js/public/types/src/scripts/reducers/items.d.ts +9 -0
- data/node_modules/choices.js/public/types/src/scripts/search/fuse.d.ts +14 -0
- data/node_modules/choices.js/public/types/src/scripts/search/index.d.ts +3 -0
- data/node_modules/choices.js/public/types/src/scripts/search/kmp.d.ts +11 -0
- data/node_modules/choices.js/public/types/src/scripts/search/prefix-filter.d.ts +11 -0
- data/node_modules/choices.js/public/types/src/scripts/store/store.d.ts +59 -0
- data/node_modules/choices.js/public/types/src/scripts/templates.d.ts +8 -0
- data/node_modules/choices.js/src/entry.js +3 -0
- data/node_modules/choices.js/src/icons/cross-inverse.svg +1 -0
- data/node_modules/choices.js/src/icons/cross.svg +1 -0
- data/node_modules/choices.js/src/index.ts +8 -0
- data/node_modules/choices.js/src/scripts/actions/choices.ts +59 -0
- data/node_modules/choices.js/src/scripts/actions/groups.ts +14 -0
- data/node_modules/choices.js/src/scripts/actions/items.ts +34 -0
- data/node_modules/choices.js/src/scripts/choices.ts +2364 -0
- data/node_modules/choices.js/src/scripts/components/container.ts +157 -0
- data/node_modules/choices.js/src/scripts/components/dropdown.ts +50 -0
- data/node_modules/choices.js/src/scripts/components/index.ts +8 -0
- data/node_modules/choices.js/src/scripts/components/input.ts +146 -0
- data/node_modules/choices.js/src/scripts/components/list.ts +89 -0
- data/node_modules/choices.js/src/scripts/components/wrapped-element.ts +89 -0
- data/node_modules/choices.js/src/scripts/components/wrapped-input.ts +3 -0
- data/node_modules/choices.js/src/scripts/components/wrapped-select.ts +115 -0
- data/node_modules/choices.js/src/scripts/constants.ts +1 -0
- data/node_modules/choices.js/src/scripts/defaults.ts +93 -0
- data/node_modules/choices.js/src/scripts/interfaces/action-type.ts +15 -0
- data/node_modules/choices.js/src/scripts/interfaces/build-flags.ts +17 -0
- data/node_modules/choices.js/src/scripts/interfaces/choice-full.ts +30 -0
- data/node_modules/choices.js/src/scripts/interfaces/class-names.ts +61 -0
- data/node_modules/choices.js/src/scripts/interfaces/event-choice.ts +9 -0
- data/node_modules/choices.js/src/scripts/interfaces/event-type.ts +16 -0
- data/node_modules/choices.js/src/scripts/interfaces/group-full.ts +12 -0
- data/node_modules/choices.js/src/scripts/interfaces/index.ts +14 -0
- data/node_modules/choices.js/src/scripts/interfaces/input-choice.ts +17 -0
- data/node_modules/choices.js/src/scripts/interfaces/input-group.ts +11 -0
- data/node_modules/choices.js/src/scripts/interfaces/item.ts +17 -0
- data/node_modules/choices.js/src/scripts/interfaces/keycode-map.ts +13 -0
- data/node_modules/choices.js/src/scripts/interfaces/options.ts +619 -0
- data/node_modules/choices.js/src/scripts/interfaces/passed-element-type.ts +9 -0
- data/node_modules/choices.js/src/scripts/interfaces/passed-element.ts +96 -0
- data/node_modules/choices.js/src/scripts/interfaces/position-options-type.ts +1 -0
- data/node_modules/choices.js/src/scripts/interfaces/search.ts +12 -0
- data/node_modules/choices.js/src/scripts/interfaces/state.ts +12 -0
- data/node_modules/choices.js/src/scripts/interfaces/store.ts +84 -0
- data/node_modules/choices.js/src/scripts/interfaces/string-pre-escaped.ts +3 -0
- data/node_modules/choices.js/src/scripts/interfaces/string-untrusted.ts +5 -0
- data/node_modules/choices.js/src/scripts/interfaces/templates.ts +66 -0
- data/node_modules/choices.js/src/scripts/interfaces/types.ts +21 -0
- data/node_modules/choices.js/src/scripts/lib/choice-input.ts +88 -0
- data/node_modules/choices.js/src/scripts/lib/html-guard-statements.ts +7 -0
- data/node_modules/choices.js/src/scripts/lib/utils.ts +230 -0
- data/node_modules/choices.js/src/scripts/reducers/choices.ts +86 -0
- data/node_modules/choices.js/src/scripts/reducers/groups.ts +32 -0
- data/node_modules/choices.js/src/scripts/reducers/items.ts +86 -0
- data/node_modules/choices.js/src/scripts/search/fuse.ts +59 -0
- data/node_modules/choices.js/src/scripts/search/index.ts +17 -0
- data/node_modules/choices.js/src/scripts/search/kmp.ts +87 -0
- data/node_modules/choices.js/src/scripts/search/prefix-filter.ts +42 -0
- data/node_modules/choices.js/src/scripts/store/store.ts +184 -0
- data/node_modules/choices.js/src/scripts/templates.ts +409 -0
- data/node_modules/choices.js/src/styles/base.scss +189 -0
- data/node_modules/choices.js/src/styles/choices.scss +414 -0
- data/node_modules/choices.js/src/tsconfig.json +22 -0
- metadata +134 -1
@@ -0,0 +1,2364 @@
|
|
1
|
+
import { activateChoices, addChoice, removeChoice, filterChoices } from './actions/choices';
|
2
|
+
import { addGroup } from './actions/groups';
|
3
|
+
import { addItem, highlightItem, removeItem } from './actions/items';
|
4
|
+
import { Container, Dropdown, Input, List, WrappedInput, WrappedSelect } from './components';
|
5
|
+
import { DEFAULT_CONFIG } from './defaults';
|
6
|
+
import { InputChoice } from './interfaces/input-choice';
|
7
|
+
import { InputGroup } from './interfaces/input-group';
|
8
|
+
import { Options, ObjectsInConfig } from './interfaces/options';
|
9
|
+
import { StateChangeSet } from './interfaces/state';
|
10
|
+
import {
|
11
|
+
addClassesToElement,
|
12
|
+
diff,
|
13
|
+
escapeForTemplate,
|
14
|
+
generateId,
|
15
|
+
getAdjacentEl,
|
16
|
+
getClassNames,
|
17
|
+
getClassNamesSelector,
|
18
|
+
isScrolledIntoView,
|
19
|
+
removeClassesFromElement,
|
20
|
+
resolveNoticeFunction,
|
21
|
+
resolveStringFunction,
|
22
|
+
sortByRank,
|
23
|
+
strToEl,
|
24
|
+
unwrapStringForEscaped,
|
25
|
+
} from './lib/utils';
|
26
|
+
import Store from './store/store';
|
27
|
+
import { coerceBool, mapInputToChoice } from './lib/choice-input';
|
28
|
+
import { ChoiceFull } from './interfaces/choice-full';
|
29
|
+
import { GroupFull } from './interfaces/group-full';
|
30
|
+
import { EventChoiceValueType, EventType, KeyCodeMap, PassedElementType, PassedElementTypes } from './interfaces';
|
31
|
+
import { EventChoice } from './interfaces/event-choice';
|
32
|
+
import { NoticeType, NoticeTypes, Templates } from './interfaces/templates';
|
33
|
+
import { isHtmlInputElement, isHtmlSelectElement } from './lib/html-guard-statements';
|
34
|
+
import { Searcher } from './interfaces/search';
|
35
|
+
import { getSearcher } from './search';
|
36
|
+
// eslint-disable-next-line import/no-named-default
|
37
|
+
import { default as defaultTemplates } from './templates';
|
38
|
+
import { canUseDom } from './interfaces/build-flags';
|
39
|
+
|
40
|
+
/** @see {@link http://browserhacks.com/#hack-acea075d0ac6954f275a70023906050c} */
|
41
|
+
const IS_IE11 =
|
42
|
+
canUseDom &&
|
43
|
+
'-ms-scroll-limit' in document.documentElement.style &&
|
44
|
+
'-ms-ime-align' in document.documentElement.style;
|
45
|
+
|
46
|
+
const USER_DEFAULTS: Partial<Options> = {};
|
47
|
+
|
48
|
+
const parseDataSetId = (element: HTMLElement | null): number | undefined => {
|
49
|
+
if (!element) {
|
50
|
+
return undefined;
|
51
|
+
}
|
52
|
+
|
53
|
+
return element.dataset.id ? parseInt(element.dataset.id, 10) : undefined;
|
54
|
+
};
|
55
|
+
|
56
|
+
const selectableChoiceIdentifier = '[data-choice-selectable]';
|
57
|
+
|
58
|
+
/**
|
59
|
+
* Choices
|
60
|
+
* @author Josh Johnson<josh@joshuajohnson.co.uk>
|
61
|
+
*/
|
62
|
+
class Choices {
|
63
|
+
static version: string = '__VERSION__';
|
64
|
+
|
65
|
+
static get defaults(): {
|
66
|
+
options: Partial<Options>;
|
67
|
+
allOptions: Options;
|
68
|
+
templates: Templates;
|
69
|
+
} {
|
70
|
+
return Object.preventExtensions({
|
71
|
+
get options(): Partial<Options> {
|
72
|
+
return USER_DEFAULTS;
|
73
|
+
},
|
74
|
+
get allOptions(): Options {
|
75
|
+
return DEFAULT_CONFIG;
|
76
|
+
},
|
77
|
+
get templates(): Templates {
|
78
|
+
return defaultTemplates;
|
79
|
+
},
|
80
|
+
});
|
81
|
+
}
|
82
|
+
|
83
|
+
initialised: boolean;
|
84
|
+
|
85
|
+
initialisedOK?: boolean = undefined;
|
86
|
+
|
87
|
+
config: Options;
|
88
|
+
|
89
|
+
passedElement: WrappedInput | WrappedSelect;
|
90
|
+
|
91
|
+
containerOuter: Container;
|
92
|
+
|
93
|
+
containerInner: Container;
|
94
|
+
|
95
|
+
choiceList: List;
|
96
|
+
|
97
|
+
itemList: List;
|
98
|
+
|
99
|
+
input: Input;
|
100
|
+
|
101
|
+
dropdown: Dropdown;
|
102
|
+
|
103
|
+
_elementType: PassedElementType;
|
104
|
+
|
105
|
+
_isTextElement: boolean;
|
106
|
+
|
107
|
+
_isSelectOneElement: boolean;
|
108
|
+
|
109
|
+
_isSelectMultipleElement: boolean;
|
110
|
+
|
111
|
+
_isSelectElement: boolean;
|
112
|
+
|
113
|
+
_hasNonChoicePlaceholder: boolean = false;
|
114
|
+
|
115
|
+
_canAddUserChoices: boolean;
|
116
|
+
|
117
|
+
_store: Store<Options>;
|
118
|
+
|
119
|
+
_templates: Templates;
|
120
|
+
|
121
|
+
_lastAddedChoiceId: number = 0;
|
122
|
+
|
123
|
+
_lastAddedGroupId: number = 0;
|
124
|
+
|
125
|
+
_currentValue: string;
|
126
|
+
|
127
|
+
_canSearch: boolean;
|
128
|
+
|
129
|
+
_isScrollingOnIe: boolean;
|
130
|
+
|
131
|
+
_highlightPosition: number;
|
132
|
+
|
133
|
+
_wasTap: boolean;
|
134
|
+
|
135
|
+
_isSearching: boolean;
|
136
|
+
|
137
|
+
_placeholderValue: string | null;
|
138
|
+
|
139
|
+
_baseId: string;
|
140
|
+
|
141
|
+
_direction: HTMLElement['dir'];
|
142
|
+
|
143
|
+
_idNames: {
|
144
|
+
itemChoice: string;
|
145
|
+
};
|
146
|
+
|
147
|
+
_presetChoices: (ChoiceFull | GroupFull)[];
|
148
|
+
|
149
|
+
_initialItems: string[];
|
150
|
+
|
151
|
+
_searcher: Searcher<ChoiceFull>;
|
152
|
+
|
153
|
+
_notice?: {
|
154
|
+
type: NoticeType;
|
155
|
+
text: string;
|
156
|
+
};
|
157
|
+
|
158
|
+
_docRoot: ShadowRoot | HTMLElement;
|
159
|
+
|
160
|
+
constructor(
|
161
|
+
element: string | Element | HTMLInputElement | HTMLSelectElement = '[data-choice]',
|
162
|
+
userConfig: Partial<Options> = {},
|
163
|
+
) {
|
164
|
+
const { defaults } = Choices;
|
165
|
+
this.config = {
|
166
|
+
...defaults.allOptions,
|
167
|
+
...defaults.options,
|
168
|
+
...userConfig,
|
169
|
+
} as Options;
|
170
|
+
ObjectsInConfig.forEach((key) => {
|
171
|
+
this.config[key] = {
|
172
|
+
...defaults.allOptions[key],
|
173
|
+
...defaults.options[key],
|
174
|
+
...userConfig[key],
|
175
|
+
};
|
176
|
+
});
|
177
|
+
|
178
|
+
const { config } = this;
|
179
|
+
if (!config.silent) {
|
180
|
+
this._validateConfig();
|
181
|
+
}
|
182
|
+
|
183
|
+
const docRoot = config.shadowRoot || document.documentElement;
|
184
|
+
this._docRoot = docRoot;
|
185
|
+
const passedElement = typeof element === 'string' ? docRoot.querySelector<HTMLElement>(element) : element;
|
186
|
+
|
187
|
+
if (
|
188
|
+
!passedElement ||
|
189
|
+
typeof passedElement !== 'object' ||
|
190
|
+
!(isHtmlInputElement(passedElement) || isHtmlSelectElement(passedElement))
|
191
|
+
) {
|
192
|
+
if (!passedElement && typeof element === 'string') {
|
193
|
+
throw TypeError(`Selector ${element} failed to find an element`);
|
194
|
+
}
|
195
|
+
throw TypeError(`Expected one of the following types text|select-one|select-multiple`);
|
196
|
+
}
|
197
|
+
|
198
|
+
let elementType = passedElement.type as PassedElementType;
|
199
|
+
const isText = elementType === PassedElementTypes.Text;
|
200
|
+
if (isText || config.maxItemCount !== 1) {
|
201
|
+
config.singleModeForMultiSelect = false;
|
202
|
+
}
|
203
|
+
if (config.singleModeForMultiSelect) {
|
204
|
+
elementType = PassedElementTypes.SelectMultiple;
|
205
|
+
}
|
206
|
+
const isSelectOne = elementType === PassedElementTypes.SelectOne;
|
207
|
+
const isSelectMultiple = elementType === PassedElementTypes.SelectMultiple;
|
208
|
+
const isSelect = isSelectOne || isSelectMultiple;
|
209
|
+
|
210
|
+
this._elementType = elementType;
|
211
|
+
this._isTextElement = isText;
|
212
|
+
this._isSelectOneElement = isSelectOne;
|
213
|
+
this._isSelectMultipleElement = isSelectMultiple;
|
214
|
+
this._isSelectElement = isSelectOne || isSelectMultiple;
|
215
|
+
this._canAddUserChoices = (isText && config.addItems) || (isSelect && config.addChoices);
|
216
|
+
|
217
|
+
if (typeof config.renderSelectedChoices !== 'boolean') {
|
218
|
+
config.renderSelectedChoices = config.renderSelectedChoices === 'always' || isSelectOne;
|
219
|
+
}
|
220
|
+
|
221
|
+
if (config.closeDropdownOnSelect === 'auto') {
|
222
|
+
config.closeDropdownOnSelect = isText || isSelectOne || config.singleModeForMultiSelect;
|
223
|
+
} else {
|
224
|
+
config.closeDropdownOnSelect = coerceBool(config.closeDropdownOnSelect);
|
225
|
+
}
|
226
|
+
|
227
|
+
if (config.placeholder) {
|
228
|
+
if (config.placeholderValue) {
|
229
|
+
this._hasNonChoicePlaceholder = true;
|
230
|
+
} else if (passedElement.dataset.placeholder) {
|
231
|
+
this._hasNonChoicePlaceholder = true;
|
232
|
+
config.placeholderValue = passedElement.dataset.placeholder;
|
233
|
+
}
|
234
|
+
}
|
235
|
+
|
236
|
+
if (userConfig.addItemFilter && typeof userConfig.addItemFilter !== 'function') {
|
237
|
+
const re =
|
238
|
+
userConfig.addItemFilter instanceof RegExp ? userConfig.addItemFilter : new RegExp(userConfig.addItemFilter);
|
239
|
+
|
240
|
+
config.addItemFilter = re.test.bind(re);
|
241
|
+
}
|
242
|
+
|
243
|
+
if (this._isTextElement) {
|
244
|
+
this.passedElement = new WrappedInput({
|
245
|
+
element: passedElement as HTMLInputElement,
|
246
|
+
classNames: config.classNames,
|
247
|
+
});
|
248
|
+
} else {
|
249
|
+
const selectEl = passedElement as HTMLSelectElement;
|
250
|
+
this.passedElement = new WrappedSelect({
|
251
|
+
element: selectEl,
|
252
|
+
classNames: config.classNames,
|
253
|
+
template: (data: ChoiceFull): HTMLOptionElement => this._templates.option(data),
|
254
|
+
extractPlaceholder: config.placeholder && !this._hasNonChoicePlaceholder,
|
255
|
+
});
|
256
|
+
}
|
257
|
+
|
258
|
+
this.initialised = false;
|
259
|
+
|
260
|
+
this._store = new Store(config);
|
261
|
+
this._currentValue = '';
|
262
|
+
config.searchEnabled = (!isText && config.searchEnabled) || isSelectMultiple;
|
263
|
+
this._canSearch = config.searchEnabled;
|
264
|
+
this._isScrollingOnIe = false;
|
265
|
+
this._highlightPosition = 0;
|
266
|
+
this._wasTap = true;
|
267
|
+
this._placeholderValue = this._generatePlaceholderValue();
|
268
|
+
this._baseId = generateId(passedElement, 'choices-');
|
269
|
+
|
270
|
+
/**
|
271
|
+
* setting direction in cases where it's explicitly set on passedElement
|
272
|
+
* or when calculated direction is different from the document
|
273
|
+
*/
|
274
|
+
this._direction = passedElement.dir;
|
275
|
+
|
276
|
+
if (canUseDom && !this._direction) {
|
277
|
+
const { direction: elementDirection } = window.getComputedStyle(passedElement);
|
278
|
+
const { direction: documentDirection } = window.getComputedStyle(document.documentElement);
|
279
|
+
if (elementDirection !== documentDirection) {
|
280
|
+
this._direction = elementDirection;
|
281
|
+
}
|
282
|
+
}
|
283
|
+
|
284
|
+
this._idNames = {
|
285
|
+
itemChoice: 'item-choice',
|
286
|
+
};
|
287
|
+
|
288
|
+
this._templates = defaults.templates;
|
289
|
+
this._render = this._render.bind(this);
|
290
|
+
this._onFocus = this._onFocus.bind(this);
|
291
|
+
this._onBlur = this._onBlur.bind(this);
|
292
|
+
this._onKeyUp = this._onKeyUp.bind(this);
|
293
|
+
this._onKeyDown = this._onKeyDown.bind(this);
|
294
|
+
this._onInput = this._onInput.bind(this);
|
295
|
+
this._onClick = this._onClick.bind(this);
|
296
|
+
this._onTouchMove = this._onTouchMove.bind(this);
|
297
|
+
this._onTouchEnd = this._onTouchEnd.bind(this);
|
298
|
+
this._onMouseDown = this._onMouseDown.bind(this);
|
299
|
+
this._onMouseOver = this._onMouseOver.bind(this);
|
300
|
+
this._onFormReset = this._onFormReset.bind(this);
|
301
|
+
this._onSelectKey = this._onSelectKey.bind(this);
|
302
|
+
this._onEnterKey = this._onEnterKey.bind(this);
|
303
|
+
this._onEscapeKey = this._onEscapeKey.bind(this);
|
304
|
+
this._onDirectionKey = this._onDirectionKey.bind(this);
|
305
|
+
this._onDeleteKey = this._onDeleteKey.bind(this);
|
306
|
+
|
307
|
+
// If element has already been initialised with Choices, fail silently
|
308
|
+
if (this.passedElement.isActive) {
|
309
|
+
if (!config.silent) {
|
310
|
+
console.warn('Trying to initialise Choices on element already initialised', { element });
|
311
|
+
}
|
312
|
+
|
313
|
+
this.initialised = true;
|
314
|
+
this.initialisedOK = false;
|
315
|
+
|
316
|
+
return;
|
317
|
+
}
|
318
|
+
|
319
|
+
// Let's go
|
320
|
+
this.init();
|
321
|
+
// preserve the selected item list after setup for form reset
|
322
|
+
this._initialItems = this._store.items.map((choice) => choice.value);
|
323
|
+
}
|
324
|
+
|
325
|
+
init(): void {
|
326
|
+
if (this.initialised || this.initialisedOK !== undefined) {
|
327
|
+
return;
|
328
|
+
}
|
329
|
+
|
330
|
+
this._searcher = getSearcher<ChoiceFull>(this.config);
|
331
|
+
this._loadChoices();
|
332
|
+
this._createTemplates();
|
333
|
+
this._createElements();
|
334
|
+
this._createStructure();
|
335
|
+
|
336
|
+
if (
|
337
|
+
(this._isTextElement && !this.config.addItems) ||
|
338
|
+
this.passedElement.element.hasAttribute('disabled') ||
|
339
|
+
!!this.passedElement.element.closest('fieldset:disabled')
|
340
|
+
) {
|
341
|
+
this.disable();
|
342
|
+
} else {
|
343
|
+
this.enable();
|
344
|
+
this._addEventListeners();
|
345
|
+
}
|
346
|
+
|
347
|
+
// should be triggered **after** disabled state to avoid additional re-draws
|
348
|
+
this._initStore();
|
349
|
+
|
350
|
+
this.initialised = true;
|
351
|
+
this.initialisedOK = true;
|
352
|
+
|
353
|
+
const { callbackOnInit } = this.config;
|
354
|
+
// Run callback if it is a function
|
355
|
+
if (typeof callbackOnInit === 'function') {
|
356
|
+
callbackOnInit.call(this);
|
357
|
+
}
|
358
|
+
}
|
359
|
+
|
360
|
+
destroy(): void {
|
361
|
+
if (!this.initialised) {
|
362
|
+
return;
|
363
|
+
}
|
364
|
+
|
365
|
+
this._removeEventListeners();
|
366
|
+
this.passedElement.reveal();
|
367
|
+
this.containerOuter.unwrap(this.passedElement.element);
|
368
|
+
|
369
|
+
this._store._listeners = []; // prevents select/input value being wiped
|
370
|
+
this.clearStore(false);
|
371
|
+
this._stopSearch();
|
372
|
+
|
373
|
+
this._templates = Choices.defaults.templates;
|
374
|
+
this.initialised = false;
|
375
|
+
this.initialisedOK = undefined;
|
376
|
+
}
|
377
|
+
|
378
|
+
enable(): this {
|
379
|
+
if (this.passedElement.isDisabled) {
|
380
|
+
this.passedElement.enable();
|
381
|
+
}
|
382
|
+
|
383
|
+
if (this.containerOuter.isDisabled) {
|
384
|
+
this._addEventListeners();
|
385
|
+
this.input.enable();
|
386
|
+
this.containerOuter.enable();
|
387
|
+
}
|
388
|
+
|
389
|
+
return this;
|
390
|
+
}
|
391
|
+
|
392
|
+
disable(): this {
|
393
|
+
if (!this.passedElement.isDisabled) {
|
394
|
+
this.passedElement.disable();
|
395
|
+
}
|
396
|
+
|
397
|
+
if (!this.containerOuter.isDisabled) {
|
398
|
+
this._removeEventListeners();
|
399
|
+
this.input.disable();
|
400
|
+
this.containerOuter.disable();
|
401
|
+
}
|
402
|
+
|
403
|
+
return this;
|
404
|
+
}
|
405
|
+
|
406
|
+
highlightItem(item: InputChoice, runEvent = true): this {
|
407
|
+
if (!item || !item.id) {
|
408
|
+
return this;
|
409
|
+
}
|
410
|
+
const choice = this._store.items.find((c) => c.id === item.id);
|
411
|
+
if (!choice || choice.highlighted) {
|
412
|
+
return this;
|
413
|
+
}
|
414
|
+
|
415
|
+
this._store.dispatch(highlightItem(choice, true));
|
416
|
+
|
417
|
+
if (runEvent) {
|
418
|
+
this.passedElement.triggerEvent(EventType.highlightItem, this._getChoiceForOutput(choice));
|
419
|
+
}
|
420
|
+
|
421
|
+
return this;
|
422
|
+
}
|
423
|
+
|
424
|
+
unhighlightItem(item: InputChoice, runEvent = true): this {
|
425
|
+
if (!item || !item.id) {
|
426
|
+
return this;
|
427
|
+
}
|
428
|
+
const choice = this._store.items.find((c) => c.id === item.id);
|
429
|
+
if (!choice || !choice.highlighted) {
|
430
|
+
return this;
|
431
|
+
}
|
432
|
+
|
433
|
+
this._store.dispatch(highlightItem(choice, false));
|
434
|
+
|
435
|
+
if (runEvent) {
|
436
|
+
this.passedElement.triggerEvent(EventType.unhighlightItem, this._getChoiceForOutput(choice));
|
437
|
+
}
|
438
|
+
|
439
|
+
return this;
|
440
|
+
}
|
441
|
+
|
442
|
+
highlightAll(): this {
|
443
|
+
this._store.withTxn(() => {
|
444
|
+
this._store.items.forEach((item) => {
|
445
|
+
if (!item.highlighted) {
|
446
|
+
this._store.dispatch(highlightItem(item, true));
|
447
|
+
|
448
|
+
this.passedElement.triggerEvent(EventType.highlightItem, this._getChoiceForOutput(item));
|
449
|
+
}
|
450
|
+
});
|
451
|
+
});
|
452
|
+
|
453
|
+
return this;
|
454
|
+
}
|
455
|
+
|
456
|
+
unhighlightAll(): this {
|
457
|
+
this._store.withTxn(() => {
|
458
|
+
this._store.items.forEach((item) => {
|
459
|
+
if (item.highlighted) {
|
460
|
+
this._store.dispatch(highlightItem(item, false));
|
461
|
+
|
462
|
+
this.passedElement.triggerEvent(EventType.highlightItem, this._getChoiceForOutput(item));
|
463
|
+
}
|
464
|
+
});
|
465
|
+
});
|
466
|
+
|
467
|
+
return this;
|
468
|
+
}
|
469
|
+
|
470
|
+
removeActiveItemsByValue(value: string): this {
|
471
|
+
this._store.withTxn(() => {
|
472
|
+
this._store.items.filter((item) => item.value === value).forEach((item) => this._removeItem(item));
|
473
|
+
});
|
474
|
+
|
475
|
+
return this;
|
476
|
+
}
|
477
|
+
|
478
|
+
removeActiveItems(excludedId?: number): this {
|
479
|
+
this._store.withTxn(() => {
|
480
|
+
this._store.items.filter(({ id }) => id !== excludedId).forEach((item) => this._removeItem(item));
|
481
|
+
});
|
482
|
+
|
483
|
+
return this;
|
484
|
+
}
|
485
|
+
|
486
|
+
removeHighlightedItems(runEvent = false): this {
|
487
|
+
this._store.withTxn(() => {
|
488
|
+
this._store.highlightedActiveItems.forEach((item) => {
|
489
|
+
this._removeItem(item);
|
490
|
+
// If this action was performed by the user
|
491
|
+
// trigger the event
|
492
|
+
if (runEvent) {
|
493
|
+
this._triggerChange(item.value);
|
494
|
+
}
|
495
|
+
});
|
496
|
+
});
|
497
|
+
|
498
|
+
return this;
|
499
|
+
}
|
500
|
+
|
501
|
+
showDropdown(preventInputFocus?: boolean): this {
|
502
|
+
if (this.dropdown.isActive) {
|
503
|
+
return this;
|
504
|
+
}
|
505
|
+
|
506
|
+
if (preventInputFocus === undefined) {
|
507
|
+
// eslint-disable-next-line no-param-reassign
|
508
|
+
preventInputFocus = !this._canSearch;
|
509
|
+
}
|
510
|
+
|
511
|
+
requestAnimationFrame(() => {
|
512
|
+
this.dropdown.show();
|
513
|
+
const rect = this.dropdown.element.getBoundingClientRect();
|
514
|
+
this.containerOuter.open(rect.bottom, rect.height);
|
515
|
+
|
516
|
+
if (!preventInputFocus) {
|
517
|
+
this.input.focus();
|
518
|
+
}
|
519
|
+
|
520
|
+
this.passedElement.triggerEvent(EventType.showDropdown);
|
521
|
+
});
|
522
|
+
|
523
|
+
return this;
|
524
|
+
}
|
525
|
+
|
526
|
+
hideDropdown(preventInputBlur?: boolean): this {
|
527
|
+
if (!this.dropdown.isActive) {
|
528
|
+
return this;
|
529
|
+
}
|
530
|
+
|
531
|
+
requestAnimationFrame(() => {
|
532
|
+
this.dropdown.hide();
|
533
|
+
this.containerOuter.close();
|
534
|
+
|
535
|
+
if (!preventInputBlur && this._canSearch) {
|
536
|
+
this.input.removeActiveDescendant();
|
537
|
+
this.input.blur();
|
538
|
+
}
|
539
|
+
|
540
|
+
this.passedElement.triggerEvent(EventType.hideDropdown);
|
541
|
+
});
|
542
|
+
|
543
|
+
return this;
|
544
|
+
}
|
545
|
+
|
546
|
+
getValue<B extends boolean = false>(valueOnly?: B): EventChoiceValueType<B> | EventChoiceValueType<B>[] {
|
547
|
+
const values = this._store.items.map((item) => {
|
548
|
+
return (valueOnly ? item.value : this._getChoiceForOutput(item)) as EventChoiceValueType<B>;
|
549
|
+
});
|
550
|
+
|
551
|
+
return this._isSelectOneElement || this.config.singleModeForMultiSelect ? values[0] : values;
|
552
|
+
}
|
553
|
+
|
554
|
+
setValue(items: string[] | InputChoice[]): this {
|
555
|
+
if (!this.initialisedOK) {
|
556
|
+
this._warnChoicesInitFailed('setValue');
|
557
|
+
|
558
|
+
return this;
|
559
|
+
}
|
560
|
+
|
561
|
+
this._store.withTxn(() => {
|
562
|
+
items.forEach((value: string | InputChoice) => {
|
563
|
+
if (value) {
|
564
|
+
this._addChoice(mapInputToChoice(value, false));
|
565
|
+
}
|
566
|
+
});
|
567
|
+
});
|
568
|
+
|
569
|
+
// @todo integrate with Store
|
570
|
+
this._searcher.reset();
|
571
|
+
|
572
|
+
return this;
|
573
|
+
}
|
574
|
+
|
575
|
+
setChoiceByValue(value: string | string[]): this {
|
576
|
+
if (!this.initialisedOK) {
|
577
|
+
this._warnChoicesInitFailed('setChoiceByValue');
|
578
|
+
|
579
|
+
return this;
|
580
|
+
}
|
581
|
+
if (this._isTextElement) {
|
582
|
+
return this;
|
583
|
+
}
|
584
|
+
this._store.withTxn(() => {
|
585
|
+
// If only one value has been passed, convert to array
|
586
|
+
const choiceValue = Array.isArray(value) ? value : [value];
|
587
|
+
|
588
|
+
// Loop through each value and
|
589
|
+
choiceValue.forEach((val) => this._findAndSelectChoiceByValue(val));
|
590
|
+
this.unhighlightAll();
|
591
|
+
});
|
592
|
+
|
593
|
+
// @todo integrate with Store
|
594
|
+
this._searcher.reset();
|
595
|
+
|
596
|
+
return this;
|
597
|
+
}
|
598
|
+
|
599
|
+
/**
|
600
|
+
* Set choices of select input via an array of objects (or function that returns array of object or promise of it),
|
601
|
+
* a value field name and a label field name.
|
602
|
+
* This behaves the same as passing items via the choices option but can be called after initialising Choices.
|
603
|
+
* This can also be used to add groups of choices (see example 2); Optionally pass a true `replaceChoices` value to remove any existing choices.
|
604
|
+
* Optionally pass a `customProperties` object to add additional data to your choices (useful when searching/filtering etc).
|
605
|
+
*
|
606
|
+
* **Input types affected:** select-one, select-multiple
|
607
|
+
*
|
608
|
+
* @example
|
609
|
+
* ```js
|
610
|
+
* const example = new Choices(element);
|
611
|
+
*
|
612
|
+
* example.setChoices([
|
613
|
+
* {value: 'One', label: 'Label One', disabled: true},
|
614
|
+
* {value: 'Two', label: 'Label Two', selected: true},
|
615
|
+
* {value: 'Three', label: 'Label Three'},
|
616
|
+
* ], 'value', 'label', false);
|
617
|
+
* ```
|
618
|
+
*
|
619
|
+
* @example
|
620
|
+
* ```js
|
621
|
+
* const example = new Choices(element);
|
622
|
+
*
|
623
|
+
* example.setChoices(async () => {
|
624
|
+
* try {
|
625
|
+
* const items = await fetch('/items');
|
626
|
+
* return items.json()
|
627
|
+
* } catch(err) {
|
628
|
+
* console.error(err)
|
629
|
+
* }
|
630
|
+
* });
|
631
|
+
* ```
|
632
|
+
*
|
633
|
+
* @example
|
634
|
+
* ```js
|
635
|
+
* const example = new Choices(element);
|
636
|
+
*
|
637
|
+
* example.setChoices([{
|
638
|
+
* label: 'Group one',
|
639
|
+
* id: 1,
|
640
|
+
* disabled: false,
|
641
|
+
* choices: [
|
642
|
+
* {value: 'Child One', label: 'Child One', selected: true},
|
643
|
+
* {value: 'Child Two', label: 'Child Two', disabled: true},
|
644
|
+
* {value: 'Child Three', label: 'Child Three'},
|
645
|
+
* ]
|
646
|
+
* },
|
647
|
+
* {
|
648
|
+
* label: 'Group two',
|
649
|
+
* id: 2,
|
650
|
+
* disabled: false,
|
651
|
+
* choices: [
|
652
|
+
* {value: 'Child Four', label: 'Child Four', disabled: true},
|
653
|
+
* {value: 'Child Five', label: 'Child Five'},
|
654
|
+
* {value: 'Child Six', label: 'Child Six', customProperties: {
|
655
|
+
* description: 'Custom description about child six',
|
656
|
+
* random: 'Another random custom property'
|
657
|
+
* }},
|
658
|
+
* ]
|
659
|
+
* }], 'value', 'label', false);
|
660
|
+
* ```
|
661
|
+
*/
|
662
|
+
setChoices(
|
663
|
+
choicesArrayOrFetcher:
|
664
|
+
| (InputChoice | InputGroup)[]
|
665
|
+
| ((instance: Choices) => (InputChoice | InputGroup)[] | Promise<(InputChoice | InputGroup)[]>) = [],
|
666
|
+
value: string | null = 'value',
|
667
|
+
label: string = 'label',
|
668
|
+
replaceChoices: boolean = false,
|
669
|
+
clearSearchFlag: boolean = true,
|
670
|
+
replaceItems: boolean = false,
|
671
|
+
): this | Promise<this> {
|
672
|
+
if (!this.initialisedOK) {
|
673
|
+
this._warnChoicesInitFailed('setChoices');
|
674
|
+
|
675
|
+
return this;
|
676
|
+
}
|
677
|
+
if (!this._isSelectElement) {
|
678
|
+
throw new TypeError(`setChoices can't be used with INPUT based Choices`);
|
679
|
+
}
|
680
|
+
|
681
|
+
if (typeof value !== 'string' || !value) {
|
682
|
+
throw new TypeError(`value parameter must be a name of 'value' field in passed objects`);
|
683
|
+
}
|
684
|
+
|
685
|
+
if (typeof choicesArrayOrFetcher === 'function') {
|
686
|
+
// it's a choices fetcher function
|
687
|
+
const fetcher = choicesArrayOrFetcher(this);
|
688
|
+
|
689
|
+
if (typeof Promise === 'function' && fetcher instanceof Promise) {
|
690
|
+
// that's a promise
|
691
|
+
// eslint-disable-next-line no-promise-executor-return
|
692
|
+
return new Promise((resolve) => requestAnimationFrame(resolve))
|
693
|
+
.then(() => this._handleLoadingState(true))
|
694
|
+
.then(() => fetcher)
|
695
|
+
.then((data: InputChoice[]) =>
|
696
|
+
this.setChoices(data, value, label, replaceChoices, clearSearchFlag, replaceItems),
|
697
|
+
)
|
698
|
+
.catch((err) => {
|
699
|
+
if (!this.config.silent) {
|
700
|
+
console.error(err);
|
701
|
+
}
|
702
|
+
})
|
703
|
+
.then(() => this._handleLoadingState(false))
|
704
|
+
.then(() => this);
|
705
|
+
}
|
706
|
+
|
707
|
+
// function returned something else than promise, let's check if it's an array of choices
|
708
|
+
if (!Array.isArray(fetcher)) {
|
709
|
+
throw new TypeError(
|
710
|
+
`.setChoices first argument function must return either array of choices or Promise, got: ${typeof fetcher}`,
|
711
|
+
);
|
712
|
+
}
|
713
|
+
|
714
|
+
// recursion with results, it's sync and choices were cleared already
|
715
|
+
return this.setChoices(fetcher, value, label, false);
|
716
|
+
}
|
717
|
+
|
718
|
+
if (!Array.isArray(choicesArrayOrFetcher)) {
|
719
|
+
throw new TypeError(
|
720
|
+
`.setChoices must be called either with array of choices with a function resulting into Promise of array of choices`,
|
721
|
+
);
|
722
|
+
}
|
723
|
+
|
724
|
+
this.containerOuter.removeLoadingState();
|
725
|
+
|
726
|
+
this._store.withTxn(() => {
|
727
|
+
if (clearSearchFlag) {
|
728
|
+
this._isSearching = false;
|
729
|
+
}
|
730
|
+
// Clear choices if needed
|
731
|
+
if (replaceChoices) {
|
732
|
+
this.clearChoices(true, replaceItems);
|
733
|
+
}
|
734
|
+
const isDefaultValue = value === 'value';
|
735
|
+
const isDefaultLabel = label === 'label';
|
736
|
+
|
737
|
+
choicesArrayOrFetcher.forEach((groupOrChoice: InputGroup | InputChoice) => {
|
738
|
+
if ('choices' in groupOrChoice) {
|
739
|
+
let group = groupOrChoice;
|
740
|
+
if (!isDefaultLabel) {
|
741
|
+
group = {
|
742
|
+
...group,
|
743
|
+
label: group[label],
|
744
|
+
} as InputGroup;
|
745
|
+
}
|
746
|
+
|
747
|
+
this._addGroup(mapInputToChoice<InputGroup>(group, true));
|
748
|
+
} else {
|
749
|
+
let choice = groupOrChoice;
|
750
|
+
if (!isDefaultLabel || !isDefaultValue) {
|
751
|
+
choice = {
|
752
|
+
...choice,
|
753
|
+
value: choice[value],
|
754
|
+
label: choice[label],
|
755
|
+
} as InputChoice;
|
756
|
+
}
|
757
|
+
const choiceFull = mapInputToChoice<InputChoice>(choice, false);
|
758
|
+
this._addChoice(choiceFull);
|
759
|
+
if (choiceFull.placeholder && !this._hasNonChoicePlaceholder) {
|
760
|
+
this._placeholderValue = unwrapStringForEscaped(choiceFull.label);
|
761
|
+
}
|
762
|
+
}
|
763
|
+
});
|
764
|
+
|
765
|
+
this.unhighlightAll();
|
766
|
+
});
|
767
|
+
|
768
|
+
// @todo integrate with Store
|
769
|
+
this._searcher.reset();
|
770
|
+
|
771
|
+
return this;
|
772
|
+
}
|
773
|
+
|
774
|
+
refresh(withEvents: boolean = false, selectFirstOption: boolean = false, deselectAll: boolean = false): this {
|
775
|
+
if (!this._isSelectElement) {
|
776
|
+
if (!this.config.silent) {
|
777
|
+
console.warn('refresh method can only be used on choices backed by a <select> element');
|
778
|
+
}
|
779
|
+
|
780
|
+
return this;
|
781
|
+
}
|
782
|
+
|
783
|
+
this._store.withTxn(() => {
|
784
|
+
const choicesFromOptions = (this.passedElement as WrappedSelect).optionsAsChoices();
|
785
|
+
|
786
|
+
// Build the list of items which require preserving
|
787
|
+
const existingItems = {};
|
788
|
+
if (!deselectAll) {
|
789
|
+
this._store.items.forEach((choice) => {
|
790
|
+
if (choice.id && choice.active && choice.selected) {
|
791
|
+
existingItems[choice.value] = true;
|
792
|
+
}
|
793
|
+
});
|
794
|
+
}
|
795
|
+
|
796
|
+
this.clearStore(false);
|
797
|
+
|
798
|
+
const updateChoice = (choice: ChoiceFull): void => {
|
799
|
+
if (deselectAll) {
|
800
|
+
this._store.dispatch(removeItem(choice));
|
801
|
+
} else if (existingItems[choice.value]) {
|
802
|
+
choice.selected = true;
|
803
|
+
}
|
804
|
+
};
|
805
|
+
|
806
|
+
choicesFromOptions.forEach((groupOrChoice) => {
|
807
|
+
if ('choices' in groupOrChoice) {
|
808
|
+
groupOrChoice.choices.forEach(updateChoice);
|
809
|
+
|
810
|
+
return;
|
811
|
+
}
|
812
|
+
updateChoice(groupOrChoice);
|
813
|
+
});
|
814
|
+
|
815
|
+
/* @todo only generate add events for the added options instead of all
|
816
|
+
if (withEvents) {
|
817
|
+
items.forEach((choice) => {
|
818
|
+
if (existingItems[choice.value]) {
|
819
|
+
this.passedElement.triggerEvent(
|
820
|
+
EventType.removeItem,
|
821
|
+
this._getChoiceForEvent(choice),
|
822
|
+
);
|
823
|
+
}
|
824
|
+
});
|
825
|
+
}
|
826
|
+
*/
|
827
|
+
|
828
|
+
// load new choices & items
|
829
|
+
this._addPredefinedChoices(choicesFromOptions, selectFirstOption, withEvents);
|
830
|
+
|
831
|
+
// re-do search if required
|
832
|
+
if (this._isSearching) {
|
833
|
+
this._searchChoices(this.input.value);
|
834
|
+
}
|
835
|
+
});
|
836
|
+
|
837
|
+
return this;
|
838
|
+
}
|
839
|
+
|
840
|
+
removeChoice(value: string): this {
|
841
|
+
const choice = this._store.choices.find((c) => c.value === value);
|
842
|
+
if (!choice) {
|
843
|
+
return this;
|
844
|
+
}
|
845
|
+
this._clearNotice();
|
846
|
+
this._store.dispatch(removeChoice(choice));
|
847
|
+
// @todo integrate with Store
|
848
|
+
this._searcher.reset();
|
849
|
+
|
850
|
+
if (choice.selected) {
|
851
|
+
this.passedElement.triggerEvent(EventType.removeItem, this._getChoiceForOutput(choice));
|
852
|
+
}
|
853
|
+
|
854
|
+
return this;
|
855
|
+
}
|
856
|
+
|
857
|
+
clearChoices(clearOptions: boolean = true, clearItems: boolean = false): this {
|
858
|
+
if (clearOptions) {
|
859
|
+
if (clearItems) {
|
860
|
+
this.passedElement.element.replaceChildren('');
|
861
|
+
} else {
|
862
|
+
this.passedElement.element.querySelectorAll(':not([selected])').forEach((el): void => {
|
863
|
+
el.remove();
|
864
|
+
});
|
865
|
+
}
|
866
|
+
}
|
867
|
+
this.itemList.element.replaceChildren('');
|
868
|
+
this.choiceList.element.replaceChildren('');
|
869
|
+
this._clearNotice();
|
870
|
+
this._store.withTxn(() => {
|
871
|
+
const items = clearItems ? [] : this._store.items;
|
872
|
+
this._store.reset();
|
873
|
+
items.forEach((item: ChoiceFull): void => {
|
874
|
+
this._store.dispatch(addChoice(item));
|
875
|
+
this._store.dispatch(addItem(item));
|
876
|
+
});
|
877
|
+
});
|
878
|
+
// @todo integrate with Store
|
879
|
+
this._searcher.reset();
|
880
|
+
|
881
|
+
return this;
|
882
|
+
}
|
883
|
+
|
884
|
+
clearStore(clearOptions: boolean = true): this {
|
885
|
+
this.clearChoices(clearOptions, true);
|
886
|
+
this._stopSearch();
|
887
|
+
this._lastAddedChoiceId = 0;
|
888
|
+
this._lastAddedGroupId = 0;
|
889
|
+
|
890
|
+
return this;
|
891
|
+
}
|
892
|
+
|
893
|
+
clearInput(): this {
|
894
|
+
const shouldSetInputWidth = !this._isSelectOneElement;
|
895
|
+
this.input.clear(shouldSetInputWidth);
|
896
|
+
this._stopSearch();
|
897
|
+
|
898
|
+
return this;
|
899
|
+
}
|
900
|
+
|
901
|
+
_validateConfig(): void {
|
902
|
+
const { config } = this;
|
903
|
+
const invalidConfigOptions = diff(config, DEFAULT_CONFIG);
|
904
|
+
if (invalidConfigOptions.length) {
|
905
|
+
console.warn('Unknown config option(s) passed', invalidConfigOptions.join(', '));
|
906
|
+
}
|
907
|
+
|
908
|
+
if (config.allowHTML && config.allowHtmlUserInput) {
|
909
|
+
if (config.addItems) {
|
910
|
+
console.warn(
|
911
|
+
'Warning: allowHTML/allowHtmlUserInput/addItems all being true is strongly not recommended and may lead to XSS attacks',
|
912
|
+
);
|
913
|
+
}
|
914
|
+
if (config.addChoices) {
|
915
|
+
console.warn(
|
916
|
+
'Warning: allowHTML/allowHtmlUserInput/addChoices all being true is strongly not recommended and may lead to XSS attacks',
|
917
|
+
);
|
918
|
+
}
|
919
|
+
}
|
920
|
+
}
|
921
|
+
|
922
|
+
_render(changes: StateChangeSet = { choices: true, groups: true, items: true }): void {
|
923
|
+
if (this._store.inTxn()) {
|
924
|
+
return;
|
925
|
+
}
|
926
|
+
|
927
|
+
if (this._isSelectElement) {
|
928
|
+
if (changes.choices || changes.groups) {
|
929
|
+
this._renderChoices();
|
930
|
+
}
|
931
|
+
}
|
932
|
+
|
933
|
+
if (changes.items) {
|
934
|
+
this._renderItems();
|
935
|
+
}
|
936
|
+
}
|
937
|
+
|
938
|
+
_renderChoices(): void {
|
939
|
+
if (!this._canAddItems()) {
|
940
|
+
return; // block rendering choices if the input limit is reached.
|
941
|
+
}
|
942
|
+
|
943
|
+
const { config, _isSearching: isSearching } = this;
|
944
|
+
const { activeGroups, activeChoices } = this._store;
|
945
|
+
|
946
|
+
let renderLimit = 0;
|
947
|
+
if (isSearching && config.searchResultLimit > 0) {
|
948
|
+
renderLimit = config.searchResultLimit;
|
949
|
+
} else if (config.renderChoiceLimit > 0) {
|
950
|
+
renderLimit = config.renderChoiceLimit;
|
951
|
+
}
|
952
|
+
|
953
|
+
if (this._isSelectElement) {
|
954
|
+
const backingOptions = activeChoices.filter((choice) => !choice.element);
|
955
|
+
if (backingOptions.length) {
|
956
|
+
(this.passedElement as WrappedSelect).addOptions(backingOptions);
|
957
|
+
}
|
958
|
+
}
|
959
|
+
|
960
|
+
const fragment = document.createDocumentFragment();
|
961
|
+
const renderableChoices = (choices: ChoiceFull[]): ChoiceFull[] =>
|
962
|
+
choices.filter(
|
963
|
+
(choice) =>
|
964
|
+
!choice.placeholder && (isSearching ? !!choice.rank : config.renderSelectedChoices || !choice.selected),
|
965
|
+
);
|
966
|
+
|
967
|
+
let selectableChoices = false;
|
968
|
+
const renderChoices = (choices: ChoiceFull[], withinGroup: boolean, groupLabel?: string): void => {
|
969
|
+
if (isSearching) {
|
970
|
+
// sortByRank is used to ensure stable sorting, as scores are non-unique
|
971
|
+
// this additionally ensures fuseOptions.sortFn is not ignored
|
972
|
+
choices.sort(sortByRank);
|
973
|
+
} else if (config.shouldSort) {
|
974
|
+
choices.sort(config.sorter);
|
975
|
+
}
|
976
|
+
|
977
|
+
let choiceLimit = choices.length;
|
978
|
+
choiceLimit = !withinGroup && renderLimit && choiceLimit > renderLimit ? renderLimit : choiceLimit;
|
979
|
+
choiceLimit--;
|
980
|
+
|
981
|
+
choices.every((choice, index) => {
|
982
|
+
// choiceEl being empty signals the contents has probably significantly changed
|
983
|
+
const dropdownItem =
|
984
|
+
choice.choiceEl || this._templates.choice(config, choice, config.itemSelectText, groupLabel);
|
985
|
+
choice.choiceEl = dropdownItem;
|
986
|
+
fragment.appendChild(dropdownItem);
|
987
|
+
if (isSearching || !choice.selected) {
|
988
|
+
selectableChoices = true;
|
989
|
+
}
|
990
|
+
|
991
|
+
return index < choiceLimit;
|
992
|
+
});
|
993
|
+
};
|
994
|
+
|
995
|
+
if (activeChoices.length) {
|
996
|
+
if (config.resetScrollPosition) {
|
997
|
+
requestAnimationFrame(() => this.choiceList.scrollToTop());
|
998
|
+
}
|
999
|
+
|
1000
|
+
if (!this._hasNonChoicePlaceholder && !isSearching && this._isSelectOneElement) {
|
1001
|
+
// If we have a placeholder choice along with groups
|
1002
|
+
renderChoices(
|
1003
|
+
activeChoices.filter((choice) => choice.placeholder && !choice.group),
|
1004
|
+
false,
|
1005
|
+
undefined,
|
1006
|
+
);
|
1007
|
+
}
|
1008
|
+
|
1009
|
+
// If we have grouped options
|
1010
|
+
if (activeGroups.length && !isSearching) {
|
1011
|
+
if (config.shouldSort) {
|
1012
|
+
activeGroups.sort(config.sorter);
|
1013
|
+
}
|
1014
|
+
// render Choices without group first, regardless of sort, otherwise they won't be distinguishable
|
1015
|
+
// from the last group
|
1016
|
+
renderChoices(
|
1017
|
+
activeChoices.filter((choice) => !choice.placeholder && !choice.group),
|
1018
|
+
false,
|
1019
|
+
undefined,
|
1020
|
+
);
|
1021
|
+
|
1022
|
+
activeGroups.forEach((group) => {
|
1023
|
+
const groupChoices = renderableChoices(group.choices);
|
1024
|
+
if (groupChoices.length) {
|
1025
|
+
if (group.label) {
|
1026
|
+
const dropdownGroup = group.groupEl || this._templates.choiceGroup(this.config, group);
|
1027
|
+
group.groupEl = dropdownGroup;
|
1028
|
+
dropdownGroup.remove();
|
1029
|
+
fragment.appendChild(dropdownGroup);
|
1030
|
+
}
|
1031
|
+
renderChoices(groupChoices, true, config.appendGroupInSearch && isSearching ? group.label : undefined);
|
1032
|
+
}
|
1033
|
+
});
|
1034
|
+
} else {
|
1035
|
+
renderChoices(renderableChoices(activeChoices), false, undefined);
|
1036
|
+
}
|
1037
|
+
}
|
1038
|
+
|
1039
|
+
if (!selectableChoices && (isSearching || !fragment.children.length || !config.renderSelectedChoices)) {
|
1040
|
+
if (!this._notice) {
|
1041
|
+
this._notice = {
|
1042
|
+
text: resolveStringFunction(isSearching ? config.noResultsText : config.noChoicesText),
|
1043
|
+
type: isSearching ? NoticeTypes.noResults : NoticeTypes.noChoices,
|
1044
|
+
};
|
1045
|
+
}
|
1046
|
+
fragment.replaceChildren('');
|
1047
|
+
}
|
1048
|
+
|
1049
|
+
this._renderNotice(fragment);
|
1050
|
+
this.choiceList.element.replaceChildren(fragment);
|
1051
|
+
|
1052
|
+
if (selectableChoices) {
|
1053
|
+
this._highlightChoice();
|
1054
|
+
}
|
1055
|
+
}
|
1056
|
+
|
1057
|
+
_renderItems(): void {
|
1058
|
+
const items = this._store.items || [];
|
1059
|
+
const itemList = this.itemList.element;
|
1060
|
+
const { config } = this;
|
1061
|
+
const fragment: DocumentFragment = document.createDocumentFragment();
|
1062
|
+
|
1063
|
+
const itemFromList = (item: ChoiceFull): HTMLElement | null =>
|
1064
|
+
itemList.querySelector<HTMLElement>(`[data-item][data-id="${item.id}"]`);
|
1065
|
+
|
1066
|
+
const addItemToFragment = (item: ChoiceFull): void => {
|
1067
|
+
let el = item.itemEl;
|
1068
|
+
if (el && el.parentElement) {
|
1069
|
+
return;
|
1070
|
+
}
|
1071
|
+
el = itemFromList(item) || this._templates.item(config, item, config.removeItemButton);
|
1072
|
+
item.itemEl = el;
|
1073
|
+
fragment.appendChild(el);
|
1074
|
+
};
|
1075
|
+
|
1076
|
+
// new items
|
1077
|
+
items.forEach(addItemToFragment);
|
1078
|
+
|
1079
|
+
let addedItems = !!fragment.childNodes.length;
|
1080
|
+
if (this._isSelectOneElement) {
|
1081
|
+
const existingItems = itemList.children.length;
|
1082
|
+
if (addedItems || existingItems > 1) {
|
1083
|
+
const placeholder = itemList.querySelector<HTMLElement>(getClassNamesSelector(config.classNames.placeholder));
|
1084
|
+
if (placeholder) {
|
1085
|
+
placeholder.remove();
|
1086
|
+
}
|
1087
|
+
} else if (!addedItems && !existingItems && this._placeholderValue) {
|
1088
|
+
addedItems = true;
|
1089
|
+
addItemToFragment(
|
1090
|
+
mapInputToChoice<InputChoice>(
|
1091
|
+
{
|
1092
|
+
selected: true,
|
1093
|
+
value: '',
|
1094
|
+
label: this._placeholderValue,
|
1095
|
+
placeholder: true,
|
1096
|
+
},
|
1097
|
+
false,
|
1098
|
+
),
|
1099
|
+
);
|
1100
|
+
}
|
1101
|
+
}
|
1102
|
+
|
1103
|
+
if (addedItems) {
|
1104
|
+
itemList.append(fragment);
|
1105
|
+
|
1106
|
+
if (config.shouldSortItems && !this._isSelectOneElement) {
|
1107
|
+
items.sort(config.sorter);
|
1108
|
+
|
1109
|
+
// push sorting into the DOM
|
1110
|
+
items.forEach((item) => {
|
1111
|
+
const el = itemFromList(item);
|
1112
|
+
if (el) {
|
1113
|
+
el.remove();
|
1114
|
+
fragment.append(el);
|
1115
|
+
}
|
1116
|
+
});
|
1117
|
+
|
1118
|
+
itemList.append(fragment);
|
1119
|
+
}
|
1120
|
+
}
|
1121
|
+
|
1122
|
+
if (this._isTextElement) {
|
1123
|
+
// Update the value of the hidden input
|
1124
|
+
this.passedElement.value = items.map(({ value }) => value).join(config.delimiter);
|
1125
|
+
}
|
1126
|
+
}
|
1127
|
+
|
1128
|
+
_displayNotice(text: string, type: NoticeType, openDropdown: boolean = true): void {
|
1129
|
+
const oldNotice = this._notice;
|
1130
|
+
if (
|
1131
|
+
oldNotice &&
|
1132
|
+
((oldNotice.type === type && oldNotice.text === text) ||
|
1133
|
+
(oldNotice.type === NoticeTypes.addChoice &&
|
1134
|
+
(type === NoticeTypes.noResults || type === NoticeTypes.noChoices)))
|
1135
|
+
) {
|
1136
|
+
if (openDropdown) {
|
1137
|
+
this.showDropdown(true);
|
1138
|
+
}
|
1139
|
+
|
1140
|
+
return;
|
1141
|
+
}
|
1142
|
+
|
1143
|
+
this._clearNotice();
|
1144
|
+
|
1145
|
+
this._notice = text
|
1146
|
+
? {
|
1147
|
+
text,
|
1148
|
+
type,
|
1149
|
+
}
|
1150
|
+
: undefined;
|
1151
|
+
|
1152
|
+
this._renderNotice();
|
1153
|
+
|
1154
|
+
if (openDropdown && text) {
|
1155
|
+
this.showDropdown(true);
|
1156
|
+
}
|
1157
|
+
}
|
1158
|
+
|
1159
|
+
_clearNotice(): void {
|
1160
|
+
if (!this._notice) {
|
1161
|
+
return;
|
1162
|
+
}
|
1163
|
+
|
1164
|
+
const noticeElement = this.choiceList.element.querySelector<HTMLElement>(
|
1165
|
+
getClassNamesSelector(this.config.classNames.notice),
|
1166
|
+
);
|
1167
|
+
if (noticeElement) {
|
1168
|
+
noticeElement.remove();
|
1169
|
+
}
|
1170
|
+
|
1171
|
+
this._notice = undefined;
|
1172
|
+
}
|
1173
|
+
|
1174
|
+
_renderNotice(fragment?: DocumentFragment): void {
|
1175
|
+
const noticeConf = this._notice;
|
1176
|
+
if (noticeConf) {
|
1177
|
+
const notice = this._templates.notice(this.config, noticeConf.text, noticeConf.type);
|
1178
|
+
if (fragment) {
|
1179
|
+
fragment.append(notice);
|
1180
|
+
} else {
|
1181
|
+
this.choiceList.prepend(notice);
|
1182
|
+
}
|
1183
|
+
}
|
1184
|
+
}
|
1185
|
+
|
1186
|
+
// eslint-disable-next-line class-methods-use-this
|
1187
|
+
_getChoiceForOutput(choice: ChoiceFull, keyCode?: number): EventChoice {
|
1188
|
+
return {
|
1189
|
+
id: choice.id,
|
1190
|
+
highlighted: choice.highlighted,
|
1191
|
+
labelClass: choice.labelClass,
|
1192
|
+
labelDescription: choice.labelDescription,
|
1193
|
+
customProperties: choice.customProperties,
|
1194
|
+
disabled: choice.disabled,
|
1195
|
+
active: choice.active,
|
1196
|
+
label: choice.label,
|
1197
|
+
placeholder: choice.placeholder,
|
1198
|
+
value: choice.value,
|
1199
|
+
groupValue: choice.group ? choice.group.label : undefined,
|
1200
|
+
element: choice.element,
|
1201
|
+
keyCode,
|
1202
|
+
};
|
1203
|
+
}
|
1204
|
+
|
1205
|
+
_triggerChange(value): void {
|
1206
|
+
if (value === undefined || value === null) {
|
1207
|
+
return;
|
1208
|
+
}
|
1209
|
+
|
1210
|
+
this.passedElement.triggerEvent(EventType.change, {
|
1211
|
+
value,
|
1212
|
+
});
|
1213
|
+
}
|
1214
|
+
|
1215
|
+
_handleButtonAction(element: HTMLElement): void {
|
1216
|
+
const { items } = this._store;
|
1217
|
+
if (!items.length || !this.config.removeItems || !this.config.removeItemButton) {
|
1218
|
+
return;
|
1219
|
+
}
|
1220
|
+
|
1221
|
+
const id = element && parseDataSetId(element.parentElement);
|
1222
|
+
const itemToRemove = id && items.find((item) => item.id === id);
|
1223
|
+
if (!itemToRemove) {
|
1224
|
+
return;
|
1225
|
+
}
|
1226
|
+
|
1227
|
+
this._store.withTxn(() => {
|
1228
|
+
// Remove item associated with button
|
1229
|
+
this._removeItem(itemToRemove);
|
1230
|
+
this._triggerChange(itemToRemove.value);
|
1231
|
+
|
1232
|
+
if (this._isSelectOneElement && !this._hasNonChoicePlaceholder) {
|
1233
|
+
const placeholderChoice = (this.config.shouldSort ? this._store.choices.reverse() : this._store.choices).find(
|
1234
|
+
(choice) => choice.placeholder,
|
1235
|
+
);
|
1236
|
+
if (placeholderChoice) {
|
1237
|
+
this._addItem(placeholderChoice);
|
1238
|
+
this.unhighlightAll();
|
1239
|
+
if (placeholderChoice.value) {
|
1240
|
+
this._triggerChange(placeholderChoice.value);
|
1241
|
+
}
|
1242
|
+
}
|
1243
|
+
}
|
1244
|
+
});
|
1245
|
+
}
|
1246
|
+
|
1247
|
+
_handleItemAction(element: HTMLElement, hasShiftKey = false): void {
|
1248
|
+
const { items } = this._store;
|
1249
|
+
if (!items.length || !this.config.removeItems || this._isSelectOneElement) {
|
1250
|
+
return;
|
1251
|
+
}
|
1252
|
+
|
1253
|
+
const id = parseDataSetId(element);
|
1254
|
+
if (!id) {
|
1255
|
+
return;
|
1256
|
+
}
|
1257
|
+
|
1258
|
+
// We only want to select one item with a click
|
1259
|
+
// so we deselect any items that aren't the target
|
1260
|
+
// unless shift is being pressed
|
1261
|
+
items.forEach((item) => {
|
1262
|
+
if (item.id === id && !item.highlighted) {
|
1263
|
+
this.highlightItem(item);
|
1264
|
+
} else if (!hasShiftKey && item.highlighted) {
|
1265
|
+
this.unhighlightItem(item);
|
1266
|
+
}
|
1267
|
+
});
|
1268
|
+
|
1269
|
+
// Focus input as without focus, a user cannot do anything with a
|
1270
|
+
// highlighted item
|
1271
|
+
this.input.focus();
|
1272
|
+
}
|
1273
|
+
|
1274
|
+
_handleChoiceAction(element: HTMLElement): boolean {
|
1275
|
+
// If we are clicking on an option
|
1276
|
+
const id = parseDataSetId(element);
|
1277
|
+
const choice = id && this._store.getChoiceById(id);
|
1278
|
+
if (!choice || choice.disabled) {
|
1279
|
+
return false;
|
1280
|
+
}
|
1281
|
+
|
1282
|
+
const hasActiveDropdown = this.dropdown.isActive;
|
1283
|
+
|
1284
|
+
if (!choice.selected) {
|
1285
|
+
if (!this._canAddItems()) {
|
1286
|
+
return true; // causes _onEnterKey to early out
|
1287
|
+
}
|
1288
|
+
|
1289
|
+
this._store.withTxn(() => {
|
1290
|
+
this._addItem(choice, true, true);
|
1291
|
+
|
1292
|
+
this.clearInput();
|
1293
|
+
this.unhighlightAll();
|
1294
|
+
});
|
1295
|
+
|
1296
|
+
this._triggerChange(choice.value);
|
1297
|
+
}
|
1298
|
+
|
1299
|
+
// We want to close the dropdown if we are dealing with a single select box
|
1300
|
+
if (hasActiveDropdown && this.config.closeDropdownOnSelect) {
|
1301
|
+
this.hideDropdown(true);
|
1302
|
+
this.containerOuter.element.focus();
|
1303
|
+
}
|
1304
|
+
|
1305
|
+
return true;
|
1306
|
+
}
|
1307
|
+
|
1308
|
+
_handleBackspace(items: ChoiceFull[]): void {
|
1309
|
+
const { config } = this;
|
1310
|
+
if (!config.removeItems || !items.length) {
|
1311
|
+
return;
|
1312
|
+
}
|
1313
|
+
|
1314
|
+
const lastItem = items[items.length - 1];
|
1315
|
+
const hasHighlightedItems = items.some((item) => item.highlighted);
|
1316
|
+
|
1317
|
+
// If editing the last item is allowed and there are not other selected items,
|
1318
|
+
// we can edit the item value. Otherwise if we can remove items, remove all selected items
|
1319
|
+
if (config.editItems && !hasHighlightedItems && lastItem) {
|
1320
|
+
this.input.value = lastItem.value;
|
1321
|
+
this.input.setWidth();
|
1322
|
+
this._removeItem(lastItem);
|
1323
|
+
this._triggerChange(lastItem.value);
|
1324
|
+
} else {
|
1325
|
+
if (!hasHighlightedItems) {
|
1326
|
+
// Highlight last item if none already highlighted
|
1327
|
+
this.highlightItem(lastItem, false);
|
1328
|
+
}
|
1329
|
+
this.removeHighlightedItems(true);
|
1330
|
+
}
|
1331
|
+
}
|
1332
|
+
|
1333
|
+
_loadChoices(): void {
|
1334
|
+
const { config } = this;
|
1335
|
+
if (this._isTextElement) {
|
1336
|
+
// Assign preset items from passed object first
|
1337
|
+
this._presetChoices = config.items.map((e: InputChoice | string) => mapInputToChoice(e, false));
|
1338
|
+
// Add any values passed from attribute
|
1339
|
+
if (this.passedElement.value) {
|
1340
|
+
const elementItems: ChoiceFull[] = this.passedElement.value
|
1341
|
+
.split(config.delimiter)
|
1342
|
+
.map((e: string) => mapInputToChoice<string>(e, false, this.config.allowHtmlUserInput));
|
1343
|
+
this._presetChoices = this._presetChoices.concat(elementItems);
|
1344
|
+
}
|
1345
|
+
this._presetChoices.forEach((choice: ChoiceFull) => {
|
1346
|
+
choice.selected = true;
|
1347
|
+
});
|
1348
|
+
} else if (this._isSelectElement) {
|
1349
|
+
// Assign preset choices from passed object
|
1350
|
+
this._presetChoices = config.choices.map((e: InputChoice) => mapInputToChoice(e, true));
|
1351
|
+
// Create array of choices from option elements
|
1352
|
+
const choicesFromOptions = (this.passedElement as WrappedSelect).optionsAsChoices();
|
1353
|
+
if (choicesFromOptions) {
|
1354
|
+
this._presetChoices.push(...choicesFromOptions);
|
1355
|
+
}
|
1356
|
+
}
|
1357
|
+
}
|
1358
|
+
|
1359
|
+
_handleLoadingState(setLoading = true): void {
|
1360
|
+
const el = this.itemList.element;
|
1361
|
+
if (setLoading) {
|
1362
|
+
this.disable();
|
1363
|
+
this.containerOuter.addLoadingState();
|
1364
|
+
if (this._isSelectOneElement) {
|
1365
|
+
el.replaceChildren(this._templates.placeholder(this.config, this.config.loadingText));
|
1366
|
+
} else {
|
1367
|
+
this.input.placeholder = this.config.loadingText;
|
1368
|
+
}
|
1369
|
+
} else {
|
1370
|
+
this.enable();
|
1371
|
+
this.containerOuter.removeLoadingState();
|
1372
|
+
|
1373
|
+
if (this._isSelectOneElement) {
|
1374
|
+
el.replaceChildren('');
|
1375
|
+
this._render();
|
1376
|
+
} else {
|
1377
|
+
this.input.placeholder = this._placeholderValue || '';
|
1378
|
+
}
|
1379
|
+
}
|
1380
|
+
}
|
1381
|
+
|
1382
|
+
_handleSearch(value?: string): void {
|
1383
|
+
if (!this.input.isFocussed) {
|
1384
|
+
return;
|
1385
|
+
}
|
1386
|
+
|
1387
|
+
// Check that we have a value to search and the input was an alphanumeric character
|
1388
|
+
if (value !== null && typeof value !== 'undefined' && value.length >= this.config.searchFloor) {
|
1389
|
+
const resultCount = this.config.searchChoices ? this._searchChoices(value) : 0;
|
1390
|
+
if (resultCount !== null) {
|
1391
|
+
// Trigger search event
|
1392
|
+
this.passedElement.triggerEvent(EventType.search, {
|
1393
|
+
value,
|
1394
|
+
resultCount,
|
1395
|
+
});
|
1396
|
+
}
|
1397
|
+
} else if (this._store.choices.some((option) => !option.active)) {
|
1398
|
+
this._stopSearch();
|
1399
|
+
}
|
1400
|
+
}
|
1401
|
+
|
1402
|
+
_canAddItems(): boolean {
|
1403
|
+
const { config } = this;
|
1404
|
+
const { maxItemCount, maxItemText } = config;
|
1405
|
+
|
1406
|
+
if (!config.singleModeForMultiSelect && maxItemCount > 0 && maxItemCount <= this._store.items.length) {
|
1407
|
+
this.choiceList.element.replaceChildren('');
|
1408
|
+
this._notice = undefined;
|
1409
|
+
this._displayNotice(
|
1410
|
+
typeof maxItemText === 'function' ? maxItemText(maxItemCount) : maxItemText,
|
1411
|
+
NoticeTypes.addChoice,
|
1412
|
+
);
|
1413
|
+
|
1414
|
+
return false;
|
1415
|
+
}
|
1416
|
+
|
1417
|
+
if (this._notice && this._notice.type === NoticeTypes.addChoice) {
|
1418
|
+
this._clearNotice();
|
1419
|
+
}
|
1420
|
+
|
1421
|
+
return true;
|
1422
|
+
}
|
1423
|
+
|
1424
|
+
_canCreateItem(value: string): boolean {
|
1425
|
+
const { config } = this;
|
1426
|
+
let canAddItem = true;
|
1427
|
+
let notice = '';
|
1428
|
+
|
1429
|
+
if (canAddItem && typeof config.addItemFilter === 'function' && !config.addItemFilter(value)) {
|
1430
|
+
canAddItem = false;
|
1431
|
+
notice = resolveNoticeFunction(config.customAddItemText, value);
|
1432
|
+
}
|
1433
|
+
|
1434
|
+
if (canAddItem) {
|
1435
|
+
const foundChoice = this._store.choices.find((choice) => config.valueComparer(choice.value, value));
|
1436
|
+
if (foundChoice) {
|
1437
|
+
if (this._isSelectElement) {
|
1438
|
+
// for exact matches, do not prompt to add it as a custom choice
|
1439
|
+
this._displayNotice('', NoticeTypes.addChoice);
|
1440
|
+
|
1441
|
+
return false;
|
1442
|
+
}
|
1443
|
+
if (!config.duplicateItemsAllowed) {
|
1444
|
+
canAddItem = false;
|
1445
|
+
notice = resolveNoticeFunction(config.uniqueItemText, value);
|
1446
|
+
}
|
1447
|
+
}
|
1448
|
+
}
|
1449
|
+
|
1450
|
+
if (canAddItem) {
|
1451
|
+
notice = resolveNoticeFunction(config.addItemText, value);
|
1452
|
+
}
|
1453
|
+
|
1454
|
+
if (notice) {
|
1455
|
+
this._displayNotice(notice, NoticeTypes.addChoice);
|
1456
|
+
}
|
1457
|
+
|
1458
|
+
return canAddItem;
|
1459
|
+
}
|
1460
|
+
|
1461
|
+
_searchChoices(value: string): number | null {
|
1462
|
+
const newValue = value.trim().replace(/\s{2,}/, ' ');
|
1463
|
+
|
1464
|
+
// signal input didn't change search
|
1465
|
+
if (!newValue.length || newValue === this._currentValue) {
|
1466
|
+
return null;
|
1467
|
+
}
|
1468
|
+
|
1469
|
+
const searcher = this._searcher;
|
1470
|
+
if (searcher.isEmptyIndex()) {
|
1471
|
+
searcher.index(this._store.searchableChoices);
|
1472
|
+
}
|
1473
|
+
// If new value matches the desired length and is not the same as the current value with a space
|
1474
|
+
const results = searcher.search(newValue);
|
1475
|
+
|
1476
|
+
this._currentValue = newValue;
|
1477
|
+
this._highlightPosition = 0;
|
1478
|
+
this._isSearching = true;
|
1479
|
+
|
1480
|
+
const notice = this._notice;
|
1481
|
+
const noticeType = notice && notice.type;
|
1482
|
+
if (noticeType !== NoticeTypes.addChoice) {
|
1483
|
+
if (!results.length) {
|
1484
|
+
this._displayNotice(resolveStringFunction(this.config.noResultsText), NoticeTypes.noResults);
|
1485
|
+
} else {
|
1486
|
+
this._clearNotice();
|
1487
|
+
}
|
1488
|
+
}
|
1489
|
+
|
1490
|
+
this._store.dispatch(filterChoices(results));
|
1491
|
+
|
1492
|
+
return results.length;
|
1493
|
+
}
|
1494
|
+
|
1495
|
+
_stopSearch(): void {
|
1496
|
+
if (this._isSearching) {
|
1497
|
+
this._currentValue = '';
|
1498
|
+
this._isSearching = false;
|
1499
|
+
this._clearNotice();
|
1500
|
+
this._store.dispatch(activateChoices(true));
|
1501
|
+
|
1502
|
+
this.passedElement.triggerEvent(EventType.search, {
|
1503
|
+
value: '',
|
1504
|
+
resultCount: 0,
|
1505
|
+
});
|
1506
|
+
}
|
1507
|
+
}
|
1508
|
+
|
1509
|
+
_addEventListeners(): void {
|
1510
|
+
const documentElement = this._docRoot;
|
1511
|
+
const outerElement = this.containerOuter.element;
|
1512
|
+
const inputElement = this.input.element;
|
1513
|
+
|
1514
|
+
// capture events - can cancel event processing or propagation
|
1515
|
+
documentElement.addEventListener('touchend', this._onTouchEnd, true);
|
1516
|
+
outerElement.addEventListener('keydown', this._onKeyDown, true);
|
1517
|
+
outerElement.addEventListener('mousedown', this._onMouseDown, true);
|
1518
|
+
|
1519
|
+
// passive events - doesn't call `preventDefault` or `stopPropagation`
|
1520
|
+
documentElement.addEventListener('click', this._onClick, { passive: true });
|
1521
|
+
documentElement.addEventListener('touchmove', this._onTouchMove, {
|
1522
|
+
passive: true,
|
1523
|
+
});
|
1524
|
+
this.dropdown.element.addEventListener('mouseover', this._onMouseOver, {
|
1525
|
+
passive: true,
|
1526
|
+
});
|
1527
|
+
|
1528
|
+
if (this._isSelectOneElement) {
|
1529
|
+
outerElement.addEventListener('focus', this._onFocus, {
|
1530
|
+
passive: true,
|
1531
|
+
});
|
1532
|
+
outerElement.addEventListener('blur', this._onBlur, {
|
1533
|
+
passive: true,
|
1534
|
+
});
|
1535
|
+
}
|
1536
|
+
|
1537
|
+
inputElement.addEventListener('keyup', this._onKeyUp, {
|
1538
|
+
passive: true,
|
1539
|
+
});
|
1540
|
+
inputElement.addEventListener('input', this._onInput, {
|
1541
|
+
passive: true,
|
1542
|
+
});
|
1543
|
+
|
1544
|
+
inputElement.addEventListener('focus', this._onFocus, {
|
1545
|
+
passive: true,
|
1546
|
+
});
|
1547
|
+
inputElement.addEventListener('blur', this._onBlur, {
|
1548
|
+
passive: true,
|
1549
|
+
});
|
1550
|
+
|
1551
|
+
if (inputElement.form) {
|
1552
|
+
inputElement.form.addEventListener('reset', this._onFormReset, {
|
1553
|
+
passive: true,
|
1554
|
+
});
|
1555
|
+
}
|
1556
|
+
|
1557
|
+
this.input.addEventListeners();
|
1558
|
+
}
|
1559
|
+
|
1560
|
+
_removeEventListeners(): void {
|
1561
|
+
const documentElement = this._docRoot;
|
1562
|
+
const outerElement = this.containerOuter.element;
|
1563
|
+
const inputElement = this.input.element;
|
1564
|
+
|
1565
|
+
documentElement.removeEventListener('touchend', this._onTouchEnd, true);
|
1566
|
+
outerElement.removeEventListener('keydown', this._onKeyDown, true);
|
1567
|
+
outerElement.removeEventListener('mousedown', this._onMouseDown, true);
|
1568
|
+
|
1569
|
+
documentElement.removeEventListener('click', this._onClick);
|
1570
|
+
documentElement.removeEventListener('touchmove', this._onTouchMove);
|
1571
|
+
this.dropdown.element.removeEventListener('mouseover', this._onMouseOver);
|
1572
|
+
|
1573
|
+
if (this._isSelectOneElement) {
|
1574
|
+
outerElement.removeEventListener('focus', this._onFocus);
|
1575
|
+
outerElement.removeEventListener('blur', this._onBlur);
|
1576
|
+
}
|
1577
|
+
|
1578
|
+
inputElement.removeEventListener('keyup', this._onKeyUp);
|
1579
|
+
inputElement.removeEventListener('input', this._onInput);
|
1580
|
+
inputElement.removeEventListener('focus', this._onFocus);
|
1581
|
+
inputElement.removeEventListener('blur', this._onBlur);
|
1582
|
+
|
1583
|
+
if (inputElement.form) {
|
1584
|
+
inputElement.form.removeEventListener('reset', this._onFormReset);
|
1585
|
+
}
|
1586
|
+
|
1587
|
+
this.input.removeEventListeners();
|
1588
|
+
}
|
1589
|
+
|
1590
|
+
_onKeyDown(event: KeyboardEvent): void {
|
1591
|
+
const { keyCode } = event;
|
1592
|
+
const hasActiveDropdown = this.dropdown.isActive;
|
1593
|
+
/*
|
1594
|
+
See:
|
1595
|
+
https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key
|
1596
|
+
https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_key_values
|
1597
|
+
https://en.wikipedia.org/wiki/UTF-16#Code_points_from_U+010000_to_U+10FFFF - UTF-16 surrogate pairs
|
1598
|
+
https://stackoverflow.com/a/70866532 - "Unidentified" for mobile
|
1599
|
+
http://www.unicode.org/versions/Unicode5.2.0/ch16.pdf#G19635 - U+FFFF is reserved (Section 16.7)
|
1600
|
+
|
1601
|
+
Logic: when a key event is sent, `event.key` represents its printable value _or_ one
|
1602
|
+
of a large list of special values indicating meta keys/functionality. In addition,
|
1603
|
+
key events for compose functionality contain a value of `Dead` when mid-composition.
|
1604
|
+
|
1605
|
+
I can't quite verify it, but non-English IMEs may also be able to generate key codes
|
1606
|
+
for code points in the surrogate-pair range, which could potentially be seen as having
|
1607
|
+
key.length > 1. Since `Fn` is one of the special keys, we can't distinguish by that
|
1608
|
+
alone.
|
1609
|
+
|
1610
|
+
Here, key.length === 1 means we know for sure the input was printable and not a special
|
1611
|
+
`key` value. When the length is greater than 1, it could be either a printable surrogate
|
1612
|
+
pair or a special `key` value. We can tell the difference by checking if the _character
|
1613
|
+
code_ value (not code point!) is in the "surrogate pair" range or not.
|
1614
|
+
|
1615
|
+
We don't use .codePointAt because an invalid code point would return 65535, which wouldn't
|
1616
|
+
pass the >= 0x10000 check we would otherwise use.
|
1617
|
+
|
1618
|
+
> ...The Unicode Standard sets aside 66 noncharacter code points. The last two code points
|
1619
|
+
> of each plane are noncharacters: U+FFFE and U+FFFF on the BMP...
|
1620
|
+
*/
|
1621
|
+
const wasPrintableChar =
|
1622
|
+
event.key.length === 1 ||
|
1623
|
+
(event.key.length === 2 && event.key.charCodeAt(0) >= 0xd800) ||
|
1624
|
+
event.key === 'Unidentified';
|
1625
|
+
|
1626
|
+
/*
|
1627
|
+
We do not show the dropdown if focusing out with esc or navigating through input fields.
|
1628
|
+
An activated search can still be opened with any other key.
|
1629
|
+
*/
|
1630
|
+
if (
|
1631
|
+
!this._isTextElement &&
|
1632
|
+
!hasActiveDropdown &&
|
1633
|
+
keyCode !== KeyCodeMap.ESC_KEY &&
|
1634
|
+
keyCode !== KeyCodeMap.TAB_KEY &&
|
1635
|
+
keyCode !== KeyCodeMap.SHIFT_KEY
|
1636
|
+
) {
|
1637
|
+
this.showDropdown();
|
1638
|
+
|
1639
|
+
if (!this.input.isFocussed && wasPrintableChar) {
|
1640
|
+
/*
|
1641
|
+
We update the input value with the pressed key as
|
1642
|
+
the input was not focussed at the time of key press
|
1643
|
+
therefore does not have the value of the key.
|
1644
|
+
*/
|
1645
|
+
this.input.value += event.key;
|
1646
|
+
// browsers interpret a space as pagedown
|
1647
|
+
if (event.key === ' ') {
|
1648
|
+
event.preventDefault();
|
1649
|
+
}
|
1650
|
+
}
|
1651
|
+
}
|
1652
|
+
|
1653
|
+
switch (keyCode) {
|
1654
|
+
case KeyCodeMap.A_KEY:
|
1655
|
+
return this._onSelectKey(event, this.itemList.element.hasChildNodes());
|
1656
|
+
case KeyCodeMap.ENTER_KEY:
|
1657
|
+
return this._onEnterKey(event, hasActiveDropdown);
|
1658
|
+
case KeyCodeMap.ESC_KEY:
|
1659
|
+
return this._onEscapeKey(event, hasActiveDropdown);
|
1660
|
+
case KeyCodeMap.UP_KEY:
|
1661
|
+
case KeyCodeMap.PAGE_UP_KEY:
|
1662
|
+
case KeyCodeMap.DOWN_KEY:
|
1663
|
+
case KeyCodeMap.PAGE_DOWN_KEY:
|
1664
|
+
return this._onDirectionKey(event, hasActiveDropdown);
|
1665
|
+
case KeyCodeMap.DELETE_KEY:
|
1666
|
+
case KeyCodeMap.BACK_KEY:
|
1667
|
+
return this._onDeleteKey(event, this._store.items, this.input.isFocussed);
|
1668
|
+
default:
|
1669
|
+
}
|
1670
|
+
}
|
1671
|
+
|
1672
|
+
_onKeyUp(/* event: KeyboardEvent */): void {
|
1673
|
+
this._canSearch = this.config.searchEnabled;
|
1674
|
+
}
|
1675
|
+
|
1676
|
+
_onInput(/* event: InputEvent */): void {
|
1677
|
+
const { value } = this.input;
|
1678
|
+
if (!value) {
|
1679
|
+
if (this._isTextElement) {
|
1680
|
+
this.hideDropdown(true);
|
1681
|
+
} else {
|
1682
|
+
this._stopSearch();
|
1683
|
+
}
|
1684
|
+
|
1685
|
+
return;
|
1686
|
+
}
|
1687
|
+
|
1688
|
+
if (!this._canAddItems()) {
|
1689
|
+
return;
|
1690
|
+
}
|
1691
|
+
|
1692
|
+
if (this._canSearch) {
|
1693
|
+
// do the search even if the entered text can not be added
|
1694
|
+
this._handleSearch(value);
|
1695
|
+
}
|
1696
|
+
|
1697
|
+
if (!this._canAddUserChoices) {
|
1698
|
+
return;
|
1699
|
+
}
|
1700
|
+
|
1701
|
+
// determine if a notice needs to be displayed for why a search result can't be added
|
1702
|
+
this._canCreateItem(value);
|
1703
|
+
if (this._isSelectElement) {
|
1704
|
+
this._highlightPosition = 0; // reset to select the notice and/or exact match
|
1705
|
+
this._highlightChoice();
|
1706
|
+
}
|
1707
|
+
}
|
1708
|
+
|
1709
|
+
_onSelectKey(event: KeyboardEvent, hasItems: boolean): void {
|
1710
|
+
// If CTRL + A or CMD + A have been pressed and there are items to select
|
1711
|
+
if ((event.ctrlKey || event.metaKey) && hasItems) {
|
1712
|
+
this._canSearch = false;
|
1713
|
+
|
1714
|
+
const shouldHightlightAll =
|
1715
|
+
this.config.removeItems && !this.input.value && this.input.element === document.activeElement;
|
1716
|
+
|
1717
|
+
if (shouldHightlightAll) {
|
1718
|
+
this.highlightAll();
|
1719
|
+
}
|
1720
|
+
}
|
1721
|
+
}
|
1722
|
+
|
1723
|
+
_onEnterKey(event: KeyboardEvent, hasActiveDropdown: boolean): void {
|
1724
|
+
const { value } = this.input;
|
1725
|
+
const target = event.target as HTMLElement | null;
|
1726
|
+
event.preventDefault();
|
1727
|
+
|
1728
|
+
if (target && target.hasAttribute('data-button')) {
|
1729
|
+
this._handleButtonAction(target);
|
1730
|
+
|
1731
|
+
return;
|
1732
|
+
}
|
1733
|
+
|
1734
|
+
if (!hasActiveDropdown) {
|
1735
|
+
if (this._isSelectElement || this._notice) {
|
1736
|
+
this.showDropdown();
|
1737
|
+
}
|
1738
|
+
|
1739
|
+
return;
|
1740
|
+
}
|
1741
|
+
|
1742
|
+
const highlightedChoice = this.dropdown.element.querySelector<HTMLElement>(
|
1743
|
+
getClassNamesSelector(this.config.classNames.highlightedState),
|
1744
|
+
);
|
1745
|
+
|
1746
|
+
if (highlightedChoice && this._handleChoiceAction(highlightedChoice)) {
|
1747
|
+
return;
|
1748
|
+
}
|
1749
|
+
|
1750
|
+
if (!target || !value) {
|
1751
|
+
this.hideDropdown(true);
|
1752
|
+
|
1753
|
+
return;
|
1754
|
+
}
|
1755
|
+
|
1756
|
+
if (!this._canAddItems()) {
|
1757
|
+
return;
|
1758
|
+
}
|
1759
|
+
|
1760
|
+
let addedItem = false;
|
1761
|
+
this._store.withTxn(() => {
|
1762
|
+
addedItem = this._findAndSelectChoiceByValue(value, true);
|
1763
|
+
if (!addedItem) {
|
1764
|
+
if (!this._canAddUserChoices) {
|
1765
|
+
return;
|
1766
|
+
}
|
1767
|
+
|
1768
|
+
if (!this._canCreateItem(value)) {
|
1769
|
+
return;
|
1770
|
+
}
|
1771
|
+
|
1772
|
+
this._addChoice(mapInputToChoice<string>(value, false, this.config.allowHtmlUserInput), true, true);
|
1773
|
+
addedItem = true;
|
1774
|
+
}
|
1775
|
+
|
1776
|
+
this.clearInput();
|
1777
|
+
this.unhighlightAll();
|
1778
|
+
});
|
1779
|
+
|
1780
|
+
if (!addedItem) {
|
1781
|
+
return;
|
1782
|
+
}
|
1783
|
+
|
1784
|
+
this._triggerChange(value);
|
1785
|
+
|
1786
|
+
if (this.config.closeDropdownOnSelect) {
|
1787
|
+
this.hideDropdown(true);
|
1788
|
+
}
|
1789
|
+
}
|
1790
|
+
|
1791
|
+
_onEscapeKey(event: KeyboardEvent, hasActiveDropdown: boolean): void {
|
1792
|
+
if (hasActiveDropdown) {
|
1793
|
+
event.stopPropagation();
|
1794
|
+
this.hideDropdown(true);
|
1795
|
+
this._stopSearch();
|
1796
|
+
this.containerOuter.element.focus();
|
1797
|
+
}
|
1798
|
+
}
|
1799
|
+
|
1800
|
+
_onDirectionKey(event: KeyboardEvent, hasActiveDropdown: boolean): void {
|
1801
|
+
const { keyCode } = event;
|
1802
|
+
|
1803
|
+
// If up or down key is pressed, traverse through options
|
1804
|
+
if (hasActiveDropdown || this._isSelectOneElement) {
|
1805
|
+
this.showDropdown();
|
1806
|
+
this._canSearch = false;
|
1807
|
+
|
1808
|
+
const directionInt = keyCode === KeyCodeMap.DOWN_KEY || keyCode === KeyCodeMap.PAGE_DOWN_KEY ? 1 : -1;
|
1809
|
+
const skipKey = event.metaKey || keyCode === KeyCodeMap.PAGE_DOWN_KEY || keyCode === KeyCodeMap.PAGE_UP_KEY;
|
1810
|
+
|
1811
|
+
let nextEl: HTMLElement | null;
|
1812
|
+
if (skipKey) {
|
1813
|
+
if (directionInt > 0) {
|
1814
|
+
nextEl = this.dropdown.element.querySelector(`${selectableChoiceIdentifier}:last-of-type`);
|
1815
|
+
} else {
|
1816
|
+
nextEl = this.dropdown.element.querySelector(selectableChoiceIdentifier);
|
1817
|
+
}
|
1818
|
+
} else {
|
1819
|
+
const currentEl = this.dropdown.element.querySelector<HTMLElement>(
|
1820
|
+
getClassNamesSelector(this.config.classNames.highlightedState),
|
1821
|
+
);
|
1822
|
+
if (currentEl) {
|
1823
|
+
nextEl = getAdjacentEl(currentEl, selectableChoiceIdentifier, directionInt);
|
1824
|
+
} else {
|
1825
|
+
nextEl = this.dropdown.element.querySelector(selectableChoiceIdentifier);
|
1826
|
+
}
|
1827
|
+
}
|
1828
|
+
|
1829
|
+
if (nextEl) {
|
1830
|
+
// We prevent default to stop the cursor moving
|
1831
|
+
// when pressing the arrow
|
1832
|
+
if (!isScrolledIntoView(nextEl, this.choiceList.element, directionInt)) {
|
1833
|
+
this.choiceList.scrollToChildElement(nextEl, directionInt);
|
1834
|
+
}
|
1835
|
+
this._highlightChoice(nextEl);
|
1836
|
+
}
|
1837
|
+
|
1838
|
+
// Prevent default to maintain cursor position whilst
|
1839
|
+
// traversing dropdown options
|
1840
|
+
event.preventDefault();
|
1841
|
+
}
|
1842
|
+
}
|
1843
|
+
|
1844
|
+
_onDeleteKey(event: KeyboardEvent, items: ChoiceFull[], hasFocusedInput: boolean): void {
|
1845
|
+
// If backspace or delete key is pressed and the input has no value
|
1846
|
+
if (!this._isSelectOneElement && !(event.target as HTMLInputElement).value && hasFocusedInput) {
|
1847
|
+
this._handleBackspace(items);
|
1848
|
+
event.preventDefault();
|
1849
|
+
}
|
1850
|
+
}
|
1851
|
+
|
1852
|
+
_onTouchMove(): void {
|
1853
|
+
if (this._wasTap) {
|
1854
|
+
this._wasTap = false;
|
1855
|
+
}
|
1856
|
+
}
|
1857
|
+
|
1858
|
+
_onTouchEnd(event: TouchEvent): void {
|
1859
|
+
const { target } = event || (event as TouchEvent).touches[0];
|
1860
|
+
const touchWasWithinContainer = this._wasTap && this.containerOuter.element.contains(target as Node);
|
1861
|
+
|
1862
|
+
if (touchWasWithinContainer) {
|
1863
|
+
const containerWasExactTarget = target === this.containerOuter.element || target === this.containerInner.element;
|
1864
|
+
|
1865
|
+
if (containerWasExactTarget) {
|
1866
|
+
if (this._isTextElement) {
|
1867
|
+
this.input.focus();
|
1868
|
+
} else if (this._isSelectMultipleElement) {
|
1869
|
+
this.showDropdown();
|
1870
|
+
}
|
1871
|
+
}
|
1872
|
+
|
1873
|
+
// Prevents focus event firing
|
1874
|
+
event.stopPropagation();
|
1875
|
+
}
|
1876
|
+
|
1877
|
+
this._wasTap = true;
|
1878
|
+
}
|
1879
|
+
|
1880
|
+
/**
|
1881
|
+
* Handles mousedown event in capture mode for containetOuter.element
|
1882
|
+
*/
|
1883
|
+
_onMouseDown(event: MouseEvent): void {
|
1884
|
+
const { target } = event;
|
1885
|
+
if (!(target instanceof HTMLElement)) {
|
1886
|
+
return;
|
1887
|
+
}
|
1888
|
+
|
1889
|
+
// If we have our mouse down on the scrollbar and are on IE11...
|
1890
|
+
if (IS_IE11 && this.choiceList.element.contains(target)) {
|
1891
|
+
// check if click was on a scrollbar area
|
1892
|
+
const firstChoice = this.choiceList.element.firstElementChild as HTMLElement;
|
1893
|
+
|
1894
|
+
this._isScrollingOnIe =
|
1895
|
+
this._direction === 'ltr' ? event.offsetX >= firstChoice.offsetWidth : event.offsetX < firstChoice.offsetLeft;
|
1896
|
+
}
|
1897
|
+
|
1898
|
+
if (target === this.input.element) {
|
1899
|
+
return;
|
1900
|
+
}
|
1901
|
+
|
1902
|
+
const item = target.closest('[data-button],[data-item],[data-choice]');
|
1903
|
+
if (item instanceof HTMLElement) {
|
1904
|
+
if ('button' in item.dataset) {
|
1905
|
+
this._handleButtonAction(item);
|
1906
|
+
} else if ('item' in item.dataset) {
|
1907
|
+
this._handleItemAction(item, event.shiftKey);
|
1908
|
+
} else if ('choice' in item.dataset) {
|
1909
|
+
this._handleChoiceAction(item);
|
1910
|
+
}
|
1911
|
+
}
|
1912
|
+
|
1913
|
+
event.preventDefault();
|
1914
|
+
}
|
1915
|
+
|
1916
|
+
/**
|
1917
|
+
* Handles mouseover event over this.dropdown
|
1918
|
+
* @param {MouseEvent} event
|
1919
|
+
*/
|
1920
|
+
_onMouseOver({ target }: Pick<MouseEvent, 'target'>): void {
|
1921
|
+
if (target instanceof HTMLElement && 'choice' in target.dataset) {
|
1922
|
+
this._highlightChoice(target);
|
1923
|
+
}
|
1924
|
+
}
|
1925
|
+
|
1926
|
+
_onClick({ target }: Pick<MouseEvent, 'target'>): void {
|
1927
|
+
const { containerOuter } = this;
|
1928
|
+
const clickWasWithinContainer = containerOuter.element.contains(target as Node);
|
1929
|
+
|
1930
|
+
if (clickWasWithinContainer) {
|
1931
|
+
if (!this.dropdown.isActive && !containerOuter.isDisabled) {
|
1932
|
+
if (this._isTextElement) {
|
1933
|
+
if (document.activeElement !== this.input.element) {
|
1934
|
+
this.input.focus();
|
1935
|
+
}
|
1936
|
+
} else {
|
1937
|
+
this.showDropdown();
|
1938
|
+
containerOuter.element.focus();
|
1939
|
+
}
|
1940
|
+
} else if (
|
1941
|
+
this._isSelectOneElement &&
|
1942
|
+
target !== this.input.element &&
|
1943
|
+
!this.dropdown.element.contains(target as Node)
|
1944
|
+
) {
|
1945
|
+
this.hideDropdown();
|
1946
|
+
}
|
1947
|
+
} else {
|
1948
|
+
containerOuter.removeFocusState();
|
1949
|
+
this.hideDropdown(true);
|
1950
|
+
this.unhighlightAll();
|
1951
|
+
}
|
1952
|
+
}
|
1953
|
+
|
1954
|
+
_onFocus({ target }: Pick<FocusEvent, 'target'>): void {
|
1955
|
+
const { containerOuter } = this;
|
1956
|
+
const focusWasWithinContainer = target && containerOuter.element.contains(target as Node);
|
1957
|
+
|
1958
|
+
if (!focusWasWithinContainer) {
|
1959
|
+
return;
|
1960
|
+
}
|
1961
|
+
const targetIsInput = target === this.input.element;
|
1962
|
+
if (this._isTextElement) {
|
1963
|
+
if (targetIsInput) {
|
1964
|
+
containerOuter.addFocusState();
|
1965
|
+
}
|
1966
|
+
} else if (this._isSelectMultipleElement) {
|
1967
|
+
if (targetIsInput) {
|
1968
|
+
this.showDropdown(true);
|
1969
|
+
// If element is a select box, the focused element is the container and the dropdown
|
1970
|
+
// isn't already open, focus and show dropdown
|
1971
|
+
containerOuter.addFocusState();
|
1972
|
+
}
|
1973
|
+
} else {
|
1974
|
+
containerOuter.addFocusState();
|
1975
|
+
if (targetIsInput) {
|
1976
|
+
this.showDropdown(true);
|
1977
|
+
}
|
1978
|
+
}
|
1979
|
+
}
|
1980
|
+
|
1981
|
+
_onBlur({ target }: Pick<FocusEvent, 'target'>): void {
|
1982
|
+
const { containerOuter } = this;
|
1983
|
+
const blurWasWithinContainer = target && containerOuter.element.contains(target as Node);
|
1984
|
+
|
1985
|
+
if (blurWasWithinContainer && !this._isScrollingOnIe) {
|
1986
|
+
if (target === this.input.element) {
|
1987
|
+
containerOuter.removeFocusState();
|
1988
|
+
this.hideDropdown(true);
|
1989
|
+
if (this._isTextElement || this._isSelectMultipleElement) {
|
1990
|
+
this.unhighlightAll();
|
1991
|
+
}
|
1992
|
+
} else if (target === this.containerOuter.element) {
|
1993
|
+
// Remove the focus state when the past outerContainer was the target
|
1994
|
+
containerOuter.removeFocusState();
|
1995
|
+
|
1996
|
+
// Also close the dropdown if search is disabled
|
1997
|
+
if (!this._canSearch) {
|
1998
|
+
this.hideDropdown(true);
|
1999
|
+
}
|
2000
|
+
}
|
2001
|
+
} else {
|
2002
|
+
// On IE11, clicking the scollbar blurs our input and thus
|
2003
|
+
// closes the dropdown. To stop this, we refocus our input
|
2004
|
+
// if we know we are on IE *and* are scrolling.
|
2005
|
+
this._isScrollingOnIe = false;
|
2006
|
+
this.input.element.focus();
|
2007
|
+
}
|
2008
|
+
}
|
2009
|
+
|
2010
|
+
_onFormReset(): void {
|
2011
|
+
this._store.withTxn(() => {
|
2012
|
+
this.clearInput();
|
2013
|
+
this.hideDropdown();
|
2014
|
+
this.refresh(false, false, true);
|
2015
|
+
if (this._initialItems.length) {
|
2016
|
+
this.setChoiceByValue(this._initialItems);
|
2017
|
+
}
|
2018
|
+
});
|
2019
|
+
}
|
2020
|
+
|
2021
|
+
_highlightChoice(el: HTMLElement | null = null): void {
|
2022
|
+
const choices = Array.from(this.dropdown.element.querySelectorAll<HTMLElement>(selectableChoiceIdentifier));
|
2023
|
+
|
2024
|
+
if (!choices.length) {
|
2025
|
+
return;
|
2026
|
+
}
|
2027
|
+
|
2028
|
+
let passedEl = el;
|
2029
|
+
const { highlightedState } = this.config.classNames;
|
2030
|
+
const highlightedChoices = Array.from(
|
2031
|
+
this.dropdown.element.querySelectorAll<HTMLElement>(getClassNamesSelector(highlightedState)),
|
2032
|
+
);
|
2033
|
+
|
2034
|
+
// Remove any highlighted choices
|
2035
|
+
highlightedChoices.forEach((choice) => {
|
2036
|
+
removeClassesFromElement(choice, highlightedState);
|
2037
|
+
choice.setAttribute('aria-selected', 'false');
|
2038
|
+
});
|
2039
|
+
|
2040
|
+
if (passedEl) {
|
2041
|
+
this._highlightPosition = choices.indexOf(passedEl);
|
2042
|
+
} else {
|
2043
|
+
// Highlight choice based on last known highlight location
|
2044
|
+
if (choices.length > this._highlightPosition) {
|
2045
|
+
// If we have an option to highlight
|
2046
|
+
passedEl = choices[this._highlightPosition];
|
2047
|
+
} else {
|
2048
|
+
// Otherwise highlight the option before
|
2049
|
+
passedEl = choices[choices.length - 1];
|
2050
|
+
}
|
2051
|
+
|
2052
|
+
if (!passedEl) {
|
2053
|
+
passedEl = choices[0];
|
2054
|
+
}
|
2055
|
+
}
|
2056
|
+
|
2057
|
+
addClassesToElement(passedEl, highlightedState);
|
2058
|
+
passedEl.setAttribute('aria-selected', 'true');
|
2059
|
+
this.passedElement.triggerEvent(EventType.highlightChoice, {
|
2060
|
+
el: passedEl,
|
2061
|
+
});
|
2062
|
+
|
2063
|
+
if (this.dropdown.isActive) {
|
2064
|
+
// IE11 ignores aria-label and blocks virtual keyboard
|
2065
|
+
// if aria-activedescendant is set without a dropdown
|
2066
|
+
this.input.setActiveDescendant(passedEl.id);
|
2067
|
+
this.containerOuter.setActiveDescendant(passedEl.id);
|
2068
|
+
}
|
2069
|
+
}
|
2070
|
+
|
2071
|
+
_addItem(item: ChoiceFull, withEvents: boolean = true, userTriggered = false): void {
|
2072
|
+
if (!item.id) {
|
2073
|
+
throw new TypeError('item.id must be set before _addItem is called for a choice/item');
|
2074
|
+
}
|
2075
|
+
|
2076
|
+
if (this.config.singleModeForMultiSelect || this._isSelectOneElement) {
|
2077
|
+
this.removeActiveItems(item.id);
|
2078
|
+
}
|
2079
|
+
|
2080
|
+
this._store.dispatch(addItem(item));
|
2081
|
+
|
2082
|
+
if (withEvents) {
|
2083
|
+
this.passedElement.triggerEvent(EventType.addItem, this._getChoiceForOutput(item));
|
2084
|
+
|
2085
|
+
if (userTriggered) {
|
2086
|
+
this.passedElement.triggerEvent(EventType.choice, this._getChoiceForOutput(item));
|
2087
|
+
}
|
2088
|
+
}
|
2089
|
+
}
|
2090
|
+
|
2091
|
+
_removeItem(item: ChoiceFull): void {
|
2092
|
+
if (!item.id) {
|
2093
|
+
return;
|
2094
|
+
}
|
2095
|
+
|
2096
|
+
this._store.dispatch(removeItem(item));
|
2097
|
+
const notice = this._notice;
|
2098
|
+
if (notice && notice.type === NoticeTypes.noChoices) {
|
2099
|
+
this._clearNotice();
|
2100
|
+
}
|
2101
|
+
|
2102
|
+
this.passedElement.triggerEvent(EventType.removeItem, this._getChoiceForOutput(item));
|
2103
|
+
}
|
2104
|
+
|
2105
|
+
_addChoice(choice: ChoiceFull, withEvents: boolean = true, userTriggered = false): void {
|
2106
|
+
if (choice.id) {
|
2107
|
+
throw new TypeError('Can not re-add a choice which has already been added');
|
2108
|
+
}
|
2109
|
+
|
2110
|
+
const { config } = this;
|
2111
|
+
if (!config.duplicateItemsAllowed && this._store.choices.find((c) => config.valueComparer(c.value, choice.value))) {
|
2112
|
+
return;
|
2113
|
+
}
|
2114
|
+
|
2115
|
+
// Generate unique id, in-place update is required so chaining _addItem works as expected
|
2116
|
+
this._lastAddedChoiceId++;
|
2117
|
+
choice.id = this._lastAddedChoiceId;
|
2118
|
+
choice.elementId = `${this._baseId}-${this._idNames.itemChoice}-${choice.id}`;
|
2119
|
+
|
2120
|
+
const { prependValue, appendValue } = config;
|
2121
|
+
if (prependValue) {
|
2122
|
+
choice.value = prependValue + choice.value;
|
2123
|
+
}
|
2124
|
+
if (appendValue) {
|
2125
|
+
choice.value += appendValue.toString();
|
2126
|
+
}
|
2127
|
+
if ((prependValue || appendValue) && choice.element) {
|
2128
|
+
(choice.element as HTMLOptionElement).value = choice.value;
|
2129
|
+
}
|
2130
|
+
|
2131
|
+
this._clearNotice();
|
2132
|
+
this._store.dispatch(addChoice(choice));
|
2133
|
+
|
2134
|
+
if (choice.selected) {
|
2135
|
+
this._addItem(choice, withEvents, userTriggered);
|
2136
|
+
}
|
2137
|
+
}
|
2138
|
+
|
2139
|
+
_addGroup(group: GroupFull, withEvents: boolean = true): void {
|
2140
|
+
if (group.id) {
|
2141
|
+
throw new TypeError('Can not re-add a group which has already been added');
|
2142
|
+
}
|
2143
|
+
|
2144
|
+
this._store.dispatch(addGroup(group));
|
2145
|
+
|
2146
|
+
if (!group.choices) {
|
2147
|
+
return;
|
2148
|
+
}
|
2149
|
+
|
2150
|
+
// add unique id for the group(s), and do not store the full list of choices in this group
|
2151
|
+
this._lastAddedGroupId++;
|
2152
|
+
group.id = this._lastAddedGroupId;
|
2153
|
+
|
2154
|
+
group.choices.forEach((item: ChoiceFull) => {
|
2155
|
+
item.group = group;
|
2156
|
+
if (group.disabled) {
|
2157
|
+
item.disabled = true;
|
2158
|
+
}
|
2159
|
+
|
2160
|
+
this._addChoice(item, withEvents);
|
2161
|
+
});
|
2162
|
+
}
|
2163
|
+
|
2164
|
+
_createTemplates(): void {
|
2165
|
+
const { callbackOnCreateTemplates } = this.config;
|
2166
|
+
let userTemplates: Partial<Templates> = {};
|
2167
|
+
|
2168
|
+
if (typeof callbackOnCreateTemplates === 'function') {
|
2169
|
+
userTemplates = callbackOnCreateTemplates.call(this, strToEl, escapeForTemplate, getClassNames);
|
2170
|
+
}
|
2171
|
+
|
2172
|
+
const templating: Partial<Templates> = {};
|
2173
|
+
Object.keys(this._templates).forEach((name) => {
|
2174
|
+
if (name in userTemplates) {
|
2175
|
+
templating[name] = userTemplates[name].bind(this);
|
2176
|
+
} else {
|
2177
|
+
templating[name] = this._templates[name].bind(this);
|
2178
|
+
}
|
2179
|
+
});
|
2180
|
+
|
2181
|
+
this._templates = templating as Templates;
|
2182
|
+
}
|
2183
|
+
|
2184
|
+
_createElements(): void {
|
2185
|
+
const templating = this._templates;
|
2186
|
+
const { config, _isSelectOneElement: isSelectOneElement } = this;
|
2187
|
+
const { position, classNames } = config;
|
2188
|
+
const elementType = this._elementType;
|
2189
|
+
|
2190
|
+
this.containerOuter = new Container({
|
2191
|
+
element: templating.containerOuter(
|
2192
|
+
config,
|
2193
|
+
this._direction,
|
2194
|
+
this._isSelectElement,
|
2195
|
+
isSelectOneElement,
|
2196
|
+
config.searchEnabled,
|
2197
|
+
elementType,
|
2198
|
+
config.labelId,
|
2199
|
+
),
|
2200
|
+
classNames,
|
2201
|
+
type: elementType,
|
2202
|
+
position,
|
2203
|
+
});
|
2204
|
+
|
2205
|
+
this.containerInner = new Container({
|
2206
|
+
element: templating.containerInner(config),
|
2207
|
+
classNames,
|
2208
|
+
type: elementType,
|
2209
|
+
position,
|
2210
|
+
});
|
2211
|
+
|
2212
|
+
this.input = new Input({
|
2213
|
+
element: templating.input(config, this._placeholderValue),
|
2214
|
+
classNames,
|
2215
|
+
type: elementType,
|
2216
|
+
preventPaste: !config.paste,
|
2217
|
+
});
|
2218
|
+
|
2219
|
+
this.choiceList = new List({
|
2220
|
+
element: templating.choiceList(config, isSelectOneElement),
|
2221
|
+
});
|
2222
|
+
|
2223
|
+
this.itemList = new List({
|
2224
|
+
element: templating.itemList(config, isSelectOneElement),
|
2225
|
+
});
|
2226
|
+
|
2227
|
+
this.dropdown = new Dropdown({
|
2228
|
+
element: templating.dropdown(config),
|
2229
|
+
classNames,
|
2230
|
+
type: elementType,
|
2231
|
+
});
|
2232
|
+
}
|
2233
|
+
|
2234
|
+
_createStructure(): void {
|
2235
|
+
const { containerInner, containerOuter, passedElement } = this;
|
2236
|
+
const dropdownElement = this.dropdown.element;
|
2237
|
+
|
2238
|
+
// Hide original element
|
2239
|
+
passedElement.conceal();
|
2240
|
+
// Wrap input in container preserving DOM ordering
|
2241
|
+
containerInner.wrap(passedElement.element);
|
2242
|
+
// Wrapper inner container with outer container
|
2243
|
+
containerOuter.wrap(containerInner.element);
|
2244
|
+
|
2245
|
+
if (this._isSelectOneElement) {
|
2246
|
+
this.input.placeholder = this.config.searchPlaceholderValue || '';
|
2247
|
+
} else {
|
2248
|
+
if (this._placeholderValue) {
|
2249
|
+
this.input.placeholder = this._placeholderValue;
|
2250
|
+
}
|
2251
|
+
this.input.setWidth();
|
2252
|
+
}
|
2253
|
+
|
2254
|
+
containerOuter.element.appendChild(containerInner.element);
|
2255
|
+
containerOuter.element.appendChild(dropdownElement);
|
2256
|
+
containerInner.element.appendChild(this.itemList.element);
|
2257
|
+
dropdownElement.appendChild(this.choiceList.element);
|
2258
|
+
|
2259
|
+
if (!this._isSelectOneElement) {
|
2260
|
+
containerInner.element.appendChild(this.input.element);
|
2261
|
+
} else if (this.config.searchEnabled) {
|
2262
|
+
dropdownElement.insertBefore(this.input.element, dropdownElement.firstChild);
|
2263
|
+
}
|
2264
|
+
|
2265
|
+
this._highlightPosition = 0;
|
2266
|
+
this._isSearching = false;
|
2267
|
+
}
|
2268
|
+
|
2269
|
+
_initStore(): void {
|
2270
|
+
this._store.subscribe(this._render).withTxn(() => {
|
2271
|
+
this._addPredefinedChoices(
|
2272
|
+
this._presetChoices,
|
2273
|
+
this._isSelectOneElement && !this._hasNonChoicePlaceholder,
|
2274
|
+
false,
|
2275
|
+
);
|
2276
|
+
});
|
2277
|
+
|
2278
|
+
if (!this._store.choices.length || (this._isSelectOneElement && this._hasNonChoicePlaceholder)) {
|
2279
|
+
this._render();
|
2280
|
+
}
|
2281
|
+
}
|
2282
|
+
|
2283
|
+
_addPredefinedChoices(
|
2284
|
+
choices: (ChoiceFull | GroupFull)[],
|
2285
|
+
selectFirstOption: boolean = false,
|
2286
|
+
withEvents: boolean = true,
|
2287
|
+
): void {
|
2288
|
+
if (selectFirstOption) {
|
2289
|
+
/**
|
2290
|
+
* If there is a selected choice already or the choice is not the first in
|
2291
|
+
* the array, add each choice normally.
|
2292
|
+
*
|
2293
|
+
* Otherwise we pre-select the first enabled choice in the array ("select-one" only)
|
2294
|
+
*/
|
2295
|
+
const noSelectedChoices = choices.findIndex((choice: ChoiceFull) => choice.selected) === -1;
|
2296
|
+
if (noSelectedChoices) {
|
2297
|
+
choices.some((choice) => {
|
2298
|
+
if (choice.disabled || 'choices' in choice) {
|
2299
|
+
return false;
|
2300
|
+
}
|
2301
|
+
|
2302
|
+
choice.selected = true;
|
2303
|
+
|
2304
|
+
return true;
|
2305
|
+
});
|
2306
|
+
}
|
2307
|
+
}
|
2308
|
+
|
2309
|
+
choices.forEach((item) => {
|
2310
|
+
if ('choices' in item) {
|
2311
|
+
if (this._isSelectElement) {
|
2312
|
+
this._addGroup(item, withEvents);
|
2313
|
+
}
|
2314
|
+
} else {
|
2315
|
+
this._addChoice(item, withEvents);
|
2316
|
+
}
|
2317
|
+
});
|
2318
|
+
}
|
2319
|
+
|
2320
|
+
_findAndSelectChoiceByValue(value: string, userTriggered: boolean = false): boolean {
|
2321
|
+
// Check 'value' property exists and the choice isn't already selected
|
2322
|
+
const foundChoice = this._store.choices.find((choice) => this.config.valueComparer(choice.value, value));
|
2323
|
+
|
2324
|
+
if (foundChoice && !foundChoice.disabled && !foundChoice.selected) {
|
2325
|
+
this._addItem(foundChoice, true, userTriggered);
|
2326
|
+
|
2327
|
+
return true;
|
2328
|
+
}
|
2329
|
+
|
2330
|
+
return false;
|
2331
|
+
}
|
2332
|
+
|
2333
|
+
_generatePlaceholderValue(): string | null {
|
2334
|
+
const { config } = this;
|
2335
|
+
if (!config.placeholder) {
|
2336
|
+
return null;
|
2337
|
+
}
|
2338
|
+
|
2339
|
+
if (this._hasNonChoicePlaceholder) {
|
2340
|
+
return config.placeholderValue;
|
2341
|
+
}
|
2342
|
+
|
2343
|
+
if (this._isSelectElement) {
|
2344
|
+
const { placeholderOption } = this.passedElement as WrappedSelect;
|
2345
|
+
|
2346
|
+
return placeholderOption ? placeholderOption.text : null;
|
2347
|
+
}
|
2348
|
+
|
2349
|
+
return null;
|
2350
|
+
}
|
2351
|
+
|
2352
|
+
_warnChoicesInitFailed(caller: string): void {
|
2353
|
+
if (this.config.silent) {
|
2354
|
+
return;
|
2355
|
+
}
|
2356
|
+
if (!this.initialised) {
|
2357
|
+
throw new TypeError(`${caller} called on a non-initialised instance of Choices`);
|
2358
|
+
} else if (!this.initialisedOK) {
|
2359
|
+
throw new TypeError(`${caller} called for an element which has multiple instances of Choices initialised on it`);
|
2360
|
+
}
|
2361
|
+
}
|
2362
|
+
}
|
2363
|
+
|
2364
|
+
export default Choices;
|