simp-select 1.0.4 → 1.0.6

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.
@@ -0,0 +1,515 @@
1
+ import { IItemLocalOptions, ISimpleSelectOptions } from './types/simpleSelect.types';
2
+ import { IOptionItems } from './types/item.types';
3
+ import {
4
+ cloneObj,
5
+ compareObj,
6
+ getCreateListItem,
7
+ toCamelCase,
8
+ triggerInputEvent,
9
+ } from './utils/simpleSelection.utils';
10
+ import { SimpleSelectItemDOM } from './simpleSelectItemDOM';
11
+
12
+ export class SimpleSelectItem extends SimpleSelectItemDOM {
13
+ closeOutsideHandler!: (e:MouseEvent) => void; // not native
14
+
15
+ closeEscHandler!: (e:KeyboardEvent) => void; // not native
16
+
17
+ changeListener!: (e:Event) => void; // not native
18
+
19
+ searchHandler!: (e:Event) => void; // not native
20
+
21
+ handleResize!: (e: MediaQueryList | null) => void; // not native
22
+
23
+ mql: MediaQueryList | null = null;
24
+
25
+ countOpen = 0;
26
+
27
+ multiDebounceTime = 0;
28
+
29
+ timeoutDebounceId: NodeJS.Timeout | null = null;
30
+
31
+ constructor(select: HTMLSelectElement, options: ISimpleSelectOptions, localOptions: IItemLocalOptions) {
32
+ super(select, options, localOptions);
33
+
34
+ if (!select) {
35
+ throw Error('Select is required');
36
+ }
37
+ this.init();
38
+ super.initDom();
39
+ this.initAfterDom();
40
+ }
41
+
42
+ init() {
43
+ this.changeListener = this.changeListenerInit.bind(this);
44
+ this.$select.addEventListener('change', this.changeListener);
45
+
46
+ this.searchHandler = this.searchHandlerInit.bind(this);
47
+ this.closeOutsideHandler = this.closeOutsideHandlerInit.bind(this);
48
+ this.closeEscHandler = this.closeEscHandlerInit.bind(this);
49
+
50
+ this.handleResize = this.handleResizeInit.bind(this);
51
+
52
+ if (this.options.callbackInitialization) {
53
+ this.options.callbackInitialization(this);
54
+ }
55
+
56
+ if (!this.isNative && this.options.floatWidth) {
57
+ this.mql = window.matchMedia(`screen and (max-width: ${this.options.floatWidth}px)`);
58
+ if (this.mql) {
59
+ // @ts-ignore
60
+ this.mql.onchange = this.handleResize;
61
+ this.handleResizeInit(this.mql);
62
+ }
63
+ }
64
+
65
+ this.state.subscribe('isOpen', (val: IOptionItems[]) => {
66
+ this.toggleOpenHandler();
67
+ if (!val && this.options.isConfirmInMulti) {
68
+ this.createList();
69
+ }
70
+ if (!val) {
71
+ if (this.elemInputSearch) {
72
+ this.elemInputSearch.value = '';
73
+ this.state.setState('filterStr', '');
74
+ }
75
+ }
76
+ });
77
+
78
+ this.state.subscribe('filterStr', (val: string) => {
79
+ this.filterList(val);
80
+ });
81
+
82
+ if (!this.isNative) {
83
+ this.elemTopBody.onclick = this.clickToggleOpen.bind(this);
84
+ this.elemTopBody.onkeyup = this.clickToggleOpen.bind(this);
85
+ }
86
+ }
87
+
88
+ private handleResizeInit(e: MediaQueryList | null) {
89
+ if (!e) {
90
+ return;
91
+ }
92
+
93
+ if (e.matches) {
94
+ this.state.setState('isFloat', true);
95
+ } else {
96
+ this.state.setState('isFloat', false);
97
+ }
98
+ }
99
+
100
+ private initAfterDom() {
101
+ if (this.confirmOk) {
102
+ this.confirmOk.onclick = this.confirmOkHandler.bind(this);
103
+ }
104
+ if (this.confirmNo) {
105
+ this.confirmNo.onclick = this.confirmNoHandler.bind(this);
106
+ }
107
+
108
+ if (this.options.callbackInitialized) {
109
+ this.options.callbackInitialized(this);
110
+ }
111
+
112
+ if (this.isMulti && !this.options.isConfirmInMulti) {
113
+ if (toCamelCase('simple-debounce-time') in this.$select.dataset) {
114
+ this.multiDebounceTime = Number(this.$select.dataset[toCamelCase('simple-debounce-time')]);
115
+ } else if (this.options.debounceTime || this.options.debounceTime === 0) {
116
+ this.multiDebounceTime = this.options.debounceTime;
117
+ }
118
+ }
119
+
120
+ if (this.multiDebounceTime) {
121
+ this.multiDebounceChange = this.debounce(this.multiDebounceChange.bind(this), this.multiDebounceTime);
122
+ }
123
+
124
+ if (this.elemSelectAll) {
125
+ this.elemSelectAll.onclick = this.selectAllHandler.bind(this);
126
+ }
127
+ if (this.elemResetAll) {
128
+ this.elemResetAll.onclick = this.resetAllHandler.bind(this);
129
+ }
130
+ if (this.elemDropDownClose) {
131
+ this.elemDropDownClose.onclick = this.closeHandler.bind(this);
132
+ }
133
+ }
134
+
135
+ debounce<T extends (
136
+ ...args: never[]) => void>(
137
+ func: T,
138
+ delay: number,
139
+ ): (...args: Parameters<T>) => void {
140
+ return (...args: Parameters<T>): void => {
141
+ if (this.timeoutDebounceId) {
142
+ clearTimeout(this.timeoutDebounceId);
143
+ }
144
+
145
+ this.timeoutDebounceId = setTimeout(() => {
146
+ func(...args);
147
+ this.timeoutDebounceId = null;
148
+ }, delay);
149
+ };
150
+ }
151
+
152
+ confirmOkHandler(e:MouseEvent) {
153
+ e.preventDefault();
154
+
155
+ this.confirmOkBuild();
156
+ }
157
+
158
+ confirmOkBuild() {
159
+ const { options } = this.$select;
160
+ if (!this.elemListBody) {
161
+ return;
162
+ }
163
+ const liItems: NodeListOf<HTMLLIElement> = this.elemListBody.querySelectorAll('[data-sel-position]');
164
+ liItems.forEach((item:HTMLLIElement) => {
165
+ const pos = parseInt(item.dataset[toCamelCase('sel-position')]!, 10);
166
+ if (!pos && pos !== 0) {
167
+ return;
168
+ }
169
+ const option = options[pos];
170
+ if (!option || option.disabled) {
171
+ return;
172
+ }
173
+ option.selected = item.dataset[toCamelCase('sel-opt-checked')] === 'true';
174
+ });
175
+ this.state.setState('isOpen', false);
176
+ this.triggerInit();
177
+ }
178
+
179
+ confirmNoHandler(e:MouseEvent) {
180
+ e.preventDefault();
181
+ this.state.setState('isOpen', false);
182
+ }
183
+
184
+ closeHandler(e:MouseEvent) {
185
+ e.preventDefault();
186
+ this.state.setState('isOpen', false);
187
+ }
188
+
189
+ selectAllHandler(e:MouseEvent) {
190
+ e.preventDefault();
191
+ Array.from(this.$select.options).forEach((option) => {
192
+ if (option.disabled) {
193
+ return;
194
+ }
195
+ option.selected = true;
196
+ });
197
+ this.createList();
198
+ if (this.options.selectAllAfterClose) {
199
+ this.state.setState('isOpen', false);
200
+ }
201
+ this.triggerInit();
202
+ }
203
+
204
+ resetAllHandler(e:MouseEvent) {
205
+ e.preventDefault();
206
+ Array.from(this.$select.options).forEach((option) => {
207
+ if (option.disabled) {
208
+ return;
209
+ }
210
+ option.selected = false;
211
+ });
212
+ this.createList();
213
+ if (this.options.selectAllAfterClose) {
214
+ this.state.setState('isOpen', false);
215
+ }
216
+ this.triggerInit();
217
+ }
218
+
219
+ // click for LI
220
+ triggerSetup(e:MouseEvent) {
221
+ if (e.button !== 0) return;
222
+ const target = e.target as HTMLElement;
223
+ const targetLi = target.closest('li');
224
+ if (targetLi) {
225
+ this.changeClickItem(targetLi);
226
+ }
227
+ }
228
+
229
+ changeClickItem(item: HTMLLIElement) {
230
+ if (item) {
231
+ const pos = Number(item.dataset[toCamelCase('sel-position')]) || 0;
232
+ const option = this.$select.options[pos];
233
+ if (option && !option.disabled) {
234
+ if (this.isMulti) {
235
+ if (this.options.isConfirmInMulti || this.isFloatWidth) {
236
+ if (item.dataset[toCamelCase('sel-opt-checked')] === 'true') {
237
+ item.dataset[toCamelCase('sel-opt-checked')] = 'false';
238
+ item.classList.remove('SimpleSel__list_item--checked');
239
+ } else {
240
+ item.dataset[toCamelCase('sel-opt-checked')] = 'true';
241
+ item.classList.add('SimpleSel__list_item--checked');
242
+ }
243
+ } else {
244
+ option.selected = !option.selected;
245
+ this.createList();
246
+ this.multiDebounceChange();
247
+ }
248
+ } else {
249
+ option.selected = !option.selected;
250
+ this.createList();
251
+ this.state.setState('isOpen', false);
252
+ this.triggerInit();
253
+ }
254
+ }
255
+ }
256
+ }
257
+
258
+ multiDebounceChange() {
259
+ // can be overridden for multiselect - debounce
260
+ this.triggerInit();
261
+ }
262
+
263
+ triggerInit() {
264
+ triggerInputEvent(this.$select);
265
+ }
266
+
267
+ clickToggleOpen(e:MouseEvent | KeyboardEvent) {
268
+ e.preventDefault();
269
+ if (this.isDisabled) {
270
+ return;
271
+ }
272
+ if (e.type === 'click') {
273
+ this.state.setState('isOpen', !this.state.getState('isOpen'));
274
+ return;
275
+ }
276
+ if (e instanceof KeyboardEvent) {
277
+ if (e.key === 'Enter') {
278
+ this.state.setState('isOpen', !this.state.getState('isOpen'));
279
+ }
280
+ }
281
+ }
282
+
283
+ closeOutsideHandlerInit(e: MouseEvent) {
284
+ const target: HTMLElement = e.target as HTMLElement;
285
+ if (!target) {
286
+ return;
287
+ }
288
+ if (!this.elemWrap.contains(target)) {
289
+ if (this.options.isConfirmInMulti && this.options.isConfirmInMultiOkClickOutside) {
290
+ this.confirmOkBuild();
291
+ }
292
+
293
+ this.state.setState('isOpen', false);
294
+ }
295
+ }
296
+
297
+ closeEscHandlerInit(e: KeyboardEvent) {
298
+ if (e.code === 'Escape') {
299
+ e.preventDefault();
300
+ e.stopPropagation();
301
+ this.state.setState('isOpen', false);
302
+ }
303
+ if (e.code === 'Tab') {
304
+ e.preventDefault();
305
+ e.stopPropagation();
306
+
307
+ if (!this.elemWrap.contains(e.target as HTMLElement)) {
308
+ this.state.setState('isOpen', false);
309
+ }
310
+ }
311
+ if (e.key === 'ArrowUp' || e.key === 'ArrowDown') {
312
+ e.preventDefault();
313
+ e.stopPropagation();
314
+ this.keyBoardChangeChecked(e.key === 'ArrowDown');
315
+ }
316
+ if (e.key === 'Enter') {
317
+ const target = e.target as HTMLLIElement;
318
+ if (target && toCamelCase('sel-opt-item') in target.dataset) {
319
+ e.preventDefault();
320
+ e.stopPropagation();
321
+ this.changeClickItem(target);
322
+ }
323
+ }
324
+ }
325
+
326
+ keyBoardChangeChecked(isDown: boolean) {
327
+ // eslint-disable-next-line max-len
328
+ const liItems: NodeListOf<HTMLLIElement> = this.elemListBody!.querySelectorAll('[data-sel-position]:not([data-sel-opt-disabled="true"])');
329
+ if (!liItems.length) {
330
+ return;
331
+ }
332
+
333
+ let indCurrent = 0;
334
+ let firstOption!: HTMLLIElement;
335
+ liItems.forEach((el, i) => {
336
+ if (document.activeElement === el) {
337
+ indCurrent = i;
338
+ firstOption = el;
339
+ }
340
+ el.removeAttribute('tabindex');
341
+ });
342
+
343
+ if (!firstOption) {
344
+ firstOption = isDown ? liItems[0] : liItems[liItems.length - 1];
345
+ } else if (isDown) {
346
+ firstOption = liItems[indCurrent + 1] || liItems[0];
347
+ } else {
348
+ firstOption = liItems[indCurrent - 1] || liItems[liItems.length - 1];
349
+ }
350
+
351
+ firstOption.tabIndex = 0;
352
+ firstOption.focus();
353
+ }
354
+
355
+ searchHandlerInit(e: Event) {
356
+ const target = e.target as HTMLInputElement;
357
+ if (!target) {
358
+ return;
359
+ }
360
+ const { value } = target;
361
+ this.state.setState('filterStr', value);
362
+ }
363
+
364
+ toggleOpenHandler() {
365
+ const isOpen = this.state.getState('isOpen');
366
+
367
+ if (isOpen) {
368
+ this.elemWrap.classList.add('SimpleSel--open');
369
+ document.addEventListener('click', this.closeOutsideHandler);
370
+ document.addEventListener('keyup', this.closeEscHandler);
371
+
372
+ if (this.elemInputSearch) {
373
+ this.elemInputSearch.focus();
374
+ }
375
+
376
+ if (this.options.callbackOpen) {
377
+ this.options.callbackOpen(this);
378
+ }
379
+ this.countOpen++;
380
+ if (this.elemDropDown) {
381
+ setTimeout(() => {
382
+ this.elemDropDown!.scrollTop = 0;
383
+ }, 10);
384
+ }
385
+ } else {
386
+ this.state.setState('filterList', '');
387
+ this.elemWrap.classList.remove('SimpleSel--open');
388
+ document.removeEventListener('click', this.closeOutsideHandler);
389
+ document.removeEventListener('keyup', this.closeEscHandler);
390
+
391
+ if (this.timeoutDebounceId) {
392
+ clearTimeout(this.timeoutDebounceId);
393
+ this.triggerInit();
394
+ }
395
+
396
+ if (this.options.callbackClose && this.countOpen > 0) {
397
+ this.options.callbackClose(this);
398
+ }
399
+ }
400
+ }
401
+
402
+ private changeListenerInit(e: Event) {
403
+ if (this.options.callbackChangeSelect) {
404
+ this.options.callbackChangeSelect(e, this);
405
+ }
406
+
407
+ this.createList(true);
408
+ }
409
+
410
+ public getSelect() {
411
+ return this.$select;
412
+ }
413
+
414
+ protected handlerChangeChecked() {
415
+ if (this.elemListBody) {
416
+ this.elemListBody.onmouseup = this.triggerSetup.bind(this);
417
+ }
418
+ }
419
+
420
+ protected createList(isCompare = false) {
421
+ const newItems:IOptionItems[] = [];
422
+ const group = this.$select.querySelectorAll('optgroup');
423
+ if (group && group.length) {
424
+ group.forEach((item, ind) => {
425
+ newItems.push(getCreateListItem(item, (ind + 1).toString(), true));
426
+ });
427
+ } else {
428
+ newItems.push(getCreateListItem(this.$select, '1', false));
429
+ }
430
+
431
+ if (isCompare) {
432
+ const old = this.state.getState('items');
433
+ if (!compareObj(old, newItems)) {
434
+ this.state.setState('items', newItems);
435
+ }
436
+ } else {
437
+ this.state.setState('items', newItems);
438
+ }
439
+ }
440
+
441
+ private filterList(val: string) {
442
+ val = val.toLowerCase();
443
+ const items:IOptionItems[] = cloneObj(this.state.getState('items'));
444
+
445
+ items.forEach((group) => {
446
+ let isShowGroup = false;
447
+ group.items.forEach((item) => {
448
+ if (item.title.toLowerCase().indexOf(val) >= 0) {
449
+ isShowGroup = true;
450
+ item.isShowFilter = true;
451
+ } else {
452
+ item.isShowFilter = false;
453
+ }
454
+ });
455
+ group.isShowFilter = isShowGroup;
456
+ });
457
+ this.state.setState('items', items);
458
+ }
459
+
460
+ inputSearchHandler() {
461
+ if (!this.elemInputSearch) {
462
+ return;
463
+ }
464
+ this.elemInputSearch.addEventListener('input', this.searchHandler);
465
+ }
466
+
467
+ public detachItem() {
468
+ if (this.options.callbackDestroyInit) {
469
+ this.options.callbackDestroyInit(this);
470
+ }
471
+ const parentElement = this.elemWrap.parentNode;
472
+ this.$select.removeEventListener('change', this.changeListener);
473
+
474
+ if (this.elemInputSearch) {
475
+ this.elemInputSearch.removeEventListener('input', this.searchHandler);
476
+ }
477
+
478
+ if (this.confirmOk) {
479
+ this.confirmOk.onclick = null;
480
+ }
481
+ if (this.confirmNo) {
482
+ this.confirmNo.onclick = null;
483
+ }
484
+
485
+ parentElement!.replaceChild(this.$select, this.elemWrap);
486
+ this.$select.classList.remove(this.classSelectInit);
487
+
488
+ if (this.elemTopBody) {
489
+ this.elemTopBody.onclick = null;
490
+ this.elemTopBody.onkeyup = null;
491
+ }
492
+ if (this.elemListBody) {
493
+ this.elemListBody.onmouseup = null;
494
+ }
495
+
496
+ if (this.elemSelectAll) {
497
+ this.elemSelectAll.onclick = null;
498
+ }
499
+ if (this.elemResetAll) {
500
+ this.elemResetAll.onclick = null;
501
+ }
502
+
503
+ if (this.options.callbackDestroy) {
504
+ this.options.callbackDestroy(this);
505
+ }
506
+
507
+ if (this.elemDropDownClose) {
508
+ this.elemDropDownClose.onclick = null;
509
+ }
510
+ if (this.mql) {
511
+ this.mql.onchange = null;
512
+ this.mql = null;
513
+ }
514
+ }
515
+ }