simp-select 1.0.4 → 1.0.5

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