govuk_publishing_components 58.1.1 → 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.
Files changed (143) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/images/select-with-search/cross-icon.svg +6 -0
  3. data/app/assets/javascripts/govuk_publishing_components/analytics-ga4/ga4-search-tracker.js +4 -0
  4. data/app/assets/javascripts/govuk_publishing_components/components/select-with-search.js +57 -0
  5. data/app/assets/stylesheets/govuk_publishing_components/_all_components.scss +1 -0
  6. data/app/assets/stylesheets/govuk_publishing_components/components/_select-with-search.scss +168 -0
  7. data/app/assets/stylesheets/govuk_publishing_components/components/_select.scss +6 -0
  8. data/app/views/govuk_publishing_components/components/_select.html.erb +22 -23
  9. data/app/views/govuk_publishing_components/components/_select_with_search.html.erb +14 -0
  10. data/app/views/govuk_publishing_components/components/docs/select.yml +11 -0
  11. data/app/views/govuk_publishing_components/components/docs/select_with_search.yml +196 -0
  12. data/lib/govuk_publishing_components/presenters/select_helper.rb +8 -5
  13. data/lib/govuk_publishing_components/presenters/select_with_search_helper.rb +92 -0
  14. data/lib/govuk_publishing_components/version.rb +1 -1
  15. data/lib/govuk_publishing_components.rb +1 -0
  16. data/node_modules/choices.js/LICENSE +21 -0
  17. data/node_modules/choices.js/README.md +1360 -0
  18. data/node_modules/choices.js/package.json +173 -0
  19. data/node_modules/choices.js/public/assets/scripts/choices.js +5230 -0
  20. data/node_modules/choices.js/public/assets/scripts/choices.min.js +2 -0
  21. data/node_modules/choices.js/public/assets/scripts/choices.mjs +5222 -0
  22. data/node_modules/choices.js/public/assets/scripts/choices.search-basic.js +4748 -0
  23. data/node_modules/choices.js/public/assets/scripts/choices.search-basic.min.js +2 -0
  24. data/node_modules/choices.js/public/assets/scripts/choices.search-basic.mjs +4740 -0
  25. data/node_modules/choices.js/public/assets/scripts/choices.search-kmp.js +3631 -0
  26. data/node_modules/choices.js/public/assets/scripts/choices.search-kmp.min.js +2 -0
  27. data/node_modules/choices.js/public/assets/scripts/choices.search-kmp.mjs +3623 -0
  28. data/node_modules/choices.js/public/assets/scripts/choices.search-prefix.js +3590 -0
  29. data/node_modules/choices.js/public/assets/scripts/choices.search-prefix.min.js +2 -0
  30. data/node_modules/choices.js/public/assets/scripts/choices.search-prefix.mjs +3582 -0
  31. data/node_modules/choices.js/public/assets/styles/base.css +180 -0
  32. data/node_modules/choices.js/public/assets/styles/base.css.map +1 -0
  33. data/node_modules/choices.js/public/assets/styles/base.min.css +1 -0
  34. data/node_modules/choices.js/public/assets/styles/choices.css +338 -0
  35. data/node_modules/choices.js/public/assets/styles/choices.css.map +1 -0
  36. data/node_modules/choices.js/public/assets/styles/choices.min.css +1 -0
  37. data/node_modules/choices.js/public/types/src/index.d.ts +6 -0
  38. data/node_modules/choices.js/public/types/src/scripts/actions/choices.d.ts +30 -0
  39. data/node_modules/choices.js/public/types/src/scripts/actions/groups.d.ts +8 -0
  40. data/node_modules/choices.js/public/types/src/scripts/actions/items.d.ts +17 -0
  41. data/node_modules/choices.js/public/types/src/scripts/choices.d.ts +210 -0
  42. data/node_modules/choices.js/public/types/src/scripts/components/container.d.ts +36 -0
  43. data/node_modules/choices.js/public/types/src/scripts/components/dropdown.d.ts +21 -0
  44. data/node_modules/choices.js/public/types/src/scripts/components/index.d.ts +7 -0
  45. data/node_modules/choices.js/public/types/src/scripts/components/input.d.ts +37 -0
  46. data/node_modules/choices.js/public/types/src/scripts/components/list.d.ts +14 -0
  47. data/node_modules/choices.js/public/types/src/scripts/components/wrapped-element.d.ts +21 -0
  48. data/node_modules/choices.js/public/types/src/scripts/components/wrapped-input.d.ts +3 -0
  49. data/node_modules/choices.js/public/types/src/scripts/components/wrapped-select.d.ts +20 -0
  50. data/node_modules/choices.js/public/types/src/scripts/constants.d.ts +1 -0
  51. data/node_modules/choices.js/public/types/src/scripts/defaults.d.ts +4 -0
  52. data/node_modules/choices.js/public/types/src/scripts/interfaces/action-type.d.ts +13 -0
  53. data/node_modules/choices.js/public/types/src/scripts/interfaces/build-flags.d.ts +11 -0
  54. data/node_modules/choices.js/public/types/src/scripts/interfaces/choice-full.d.ts +23 -0
  55. data/node_modules/choices.js/public/types/src/scripts/interfaces/class-names.d.ts +61 -0
  56. data/node_modules/choices.js/public/types/src/scripts/interfaces/event-choice.d.ts +7 -0
  57. data/node_modules/choices.js/public/types/src/scripts/interfaces/event-type.d.ts +14 -0
  58. data/node_modules/choices.js/public/types/src/scripts/interfaces/group-full.d.ts +10 -0
  59. data/node_modules/choices.js/public/types/src/scripts/interfaces/index.d.ts +14 -0
  60. data/node_modules/choices.js/public/types/src/scripts/interfaces/input-choice.d.ts +15 -0
  61. data/node_modules/choices.js/public/types/src/scripts/interfaces/input-group.d.ts +10 -0
  62. data/node_modules/choices.js/public/types/src/scripts/interfaces/item.d.ts +17 -0
  63. data/node_modules/choices.js/public/types/src/scripts/interfaces/keycode-map.d.ts +13 -0
  64. data/node_modules/choices.js/public/types/src/scripts/interfaces/options.d.ts +566 -0
  65. data/node_modules/choices.js/public/types/src/scripts/interfaces/passed-element-type.d.ts +7 -0
  66. data/node_modules/choices.js/public/types/src/scripts/interfaces/passed-element.d.ts +95 -0
  67. data/node_modules/choices.js/public/types/src/scripts/interfaces/position-options-type.d.ts +1 -0
  68. data/node_modules/choices.js/public/types/src/scripts/interfaces/search.d.ts +11 -0
  69. data/node_modules/choices.js/public/types/src/scripts/interfaces/state.d.ts +10 -0
  70. data/node_modules/choices.js/public/types/src/scripts/interfaces/store.d.ts +64 -0
  71. data/node_modules/choices.js/public/types/src/scripts/interfaces/string-pre-escaped.d.ts +3 -0
  72. data/node_modules/choices.js/public/types/src/scripts/interfaces/string-untrusted.d.ts +4 -0
  73. data/node_modules/choices.js/public/types/src/scripts/interfaces/templates.d.ts +29 -0
  74. data/node_modules/choices.js/public/types/src/scripts/interfaces/types.d.ts +18 -0
  75. data/node_modules/choices.js/public/types/src/scripts/lib/choice-input.d.ts +9 -0
  76. data/node_modules/choices.js/public/types/src/scripts/lib/html-guard-statements.d.ts +4 -0
  77. data/node_modules/choices.js/public/types/src/scripts/lib/utils.d.ts +31 -0
  78. data/node_modules/choices.js/public/types/src/scripts/reducers/choices.d.ts +8 -0
  79. data/node_modules/choices.js/public/types/src/scripts/reducers/groups.d.ts +8 -0
  80. data/node_modules/choices.js/public/types/src/scripts/reducers/items.d.ts +9 -0
  81. data/node_modules/choices.js/public/types/src/scripts/search/fuse.d.ts +14 -0
  82. data/node_modules/choices.js/public/types/src/scripts/search/index.d.ts +3 -0
  83. data/node_modules/choices.js/public/types/src/scripts/search/kmp.d.ts +11 -0
  84. data/node_modules/choices.js/public/types/src/scripts/search/prefix-filter.d.ts +11 -0
  85. data/node_modules/choices.js/public/types/src/scripts/store/store.d.ts +59 -0
  86. data/node_modules/choices.js/public/types/src/scripts/templates.d.ts +8 -0
  87. data/node_modules/choices.js/src/entry.js +3 -0
  88. data/node_modules/choices.js/src/icons/cross-inverse.svg +1 -0
  89. data/node_modules/choices.js/src/icons/cross.svg +1 -0
  90. data/node_modules/choices.js/src/index.ts +8 -0
  91. data/node_modules/choices.js/src/scripts/actions/choices.ts +59 -0
  92. data/node_modules/choices.js/src/scripts/actions/groups.ts +14 -0
  93. data/node_modules/choices.js/src/scripts/actions/items.ts +34 -0
  94. data/node_modules/choices.js/src/scripts/choices.ts +2364 -0
  95. data/node_modules/choices.js/src/scripts/components/container.ts +157 -0
  96. data/node_modules/choices.js/src/scripts/components/dropdown.ts +50 -0
  97. data/node_modules/choices.js/src/scripts/components/index.ts +8 -0
  98. data/node_modules/choices.js/src/scripts/components/input.ts +146 -0
  99. data/node_modules/choices.js/src/scripts/components/list.ts +89 -0
  100. data/node_modules/choices.js/src/scripts/components/wrapped-element.ts +89 -0
  101. data/node_modules/choices.js/src/scripts/components/wrapped-input.ts +3 -0
  102. data/node_modules/choices.js/src/scripts/components/wrapped-select.ts +115 -0
  103. data/node_modules/choices.js/src/scripts/constants.ts +1 -0
  104. data/node_modules/choices.js/src/scripts/defaults.ts +93 -0
  105. data/node_modules/choices.js/src/scripts/interfaces/action-type.ts +15 -0
  106. data/node_modules/choices.js/src/scripts/interfaces/build-flags.ts +17 -0
  107. data/node_modules/choices.js/src/scripts/interfaces/choice-full.ts +30 -0
  108. data/node_modules/choices.js/src/scripts/interfaces/class-names.ts +61 -0
  109. data/node_modules/choices.js/src/scripts/interfaces/event-choice.ts +9 -0
  110. data/node_modules/choices.js/src/scripts/interfaces/event-type.ts +16 -0
  111. data/node_modules/choices.js/src/scripts/interfaces/group-full.ts +12 -0
  112. data/node_modules/choices.js/src/scripts/interfaces/index.ts +14 -0
  113. data/node_modules/choices.js/src/scripts/interfaces/input-choice.ts +17 -0
  114. data/node_modules/choices.js/src/scripts/interfaces/input-group.ts +11 -0
  115. data/node_modules/choices.js/src/scripts/interfaces/item.ts +17 -0
  116. data/node_modules/choices.js/src/scripts/interfaces/keycode-map.ts +13 -0
  117. data/node_modules/choices.js/src/scripts/interfaces/options.ts +619 -0
  118. data/node_modules/choices.js/src/scripts/interfaces/passed-element-type.ts +9 -0
  119. data/node_modules/choices.js/src/scripts/interfaces/passed-element.ts +96 -0
  120. data/node_modules/choices.js/src/scripts/interfaces/position-options-type.ts +1 -0
  121. data/node_modules/choices.js/src/scripts/interfaces/search.ts +12 -0
  122. data/node_modules/choices.js/src/scripts/interfaces/state.ts +12 -0
  123. data/node_modules/choices.js/src/scripts/interfaces/store.ts +84 -0
  124. data/node_modules/choices.js/src/scripts/interfaces/string-pre-escaped.ts +3 -0
  125. data/node_modules/choices.js/src/scripts/interfaces/string-untrusted.ts +5 -0
  126. data/node_modules/choices.js/src/scripts/interfaces/templates.ts +66 -0
  127. data/node_modules/choices.js/src/scripts/interfaces/types.ts +21 -0
  128. data/node_modules/choices.js/src/scripts/lib/choice-input.ts +88 -0
  129. data/node_modules/choices.js/src/scripts/lib/html-guard-statements.ts +7 -0
  130. data/node_modules/choices.js/src/scripts/lib/utils.ts +230 -0
  131. data/node_modules/choices.js/src/scripts/reducers/choices.ts +86 -0
  132. data/node_modules/choices.js/src/scripts/reducers/groups.ts +32 -0
  133. data/node_modules/choices.js/src/scripts/reducers/items.ts +86 -0
  134. data/node_modules/choices.js/src/scripts/search/fuse.ts +59 -0
  135. data/node_modules/choices.js/src/scripts/search/index.ts +17 -0
  136. data/node_modules/choices.js/src/scripts/search/kmp.ts +87 -0
  137. data/node_modules/choices.js/src/scripts/search/prefix-filter.ts +42 -0
  138. data/node_modules/choices.js/src/scripts/store/store.ts +184 -0
  139. data/node_modules/choices.js/src/scripts/templates.ts +409 -0
  140. data/node_modules/choices.js/src/styles/base.scss +189 -0
  141. data/node_modules/choices.js/src/styles/choices.scss +414 -0
  142. data/node_modules/choices.js/src/tsconfig.json +22 -0
  143. 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;