simp-select 1.2.3 → 1.2.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.
- package/dist/demo/index.html +1 -1
- package/dist/simpleSelect.js +1 -1
- package/dist/simpleSelectItemDOM.d.ts +2 -0
- package/dist/style.css +1 -1
- package/dist/utils/simpleSelection.utils.d.ts +4 -2
- package/package.json +1 -1
- package/src/demo/index.html +8 -2
- package/src/simpleSelectItem.ts +19 -9
- package/src/simpleSelectItemDOM.ts +47 -15
- package/src/style.css +16 -1
- package/src/utils/simpleSelection.utils.ts +34 -12
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
cloneObj, compareObj,
|
|
4
4
|
createButton, decodeHtmlEntities,
|
|
5
5
|
getClass,
|
|
6
|
+
getNativeOptions,
|
|
6
7
|
ifTrueDataAttr,
|
|
7
8
|
removeExtraSpaces,
|
|
8
9
|
toCamelCase,
|
|
@@ -332,10 +333,10 @@ export class SimpleSelectItemDOM {
|
|
|
332
333
|
}
|
|
333
334
|
resClassesWrap += ` ${this.isMulti ? getClass('multi', true) : getClass('single', true)}`;
|
|
334
335
|
if (this.options.isLabelMode) {
|
|
335
|
-
resClassesWrap +=
|
|
336
|
+
resClassesWrap += ` ${getClass('label_mode', true)}`;
|
|
336
337
|
}
|
|
337
338
|
this.elemWrap.className = resClassesWrap;
|
|
338
|
-
this.elemWrap.dataset.countAll = this.$select.
|
|
339
|
+
this.elemWrap.dataset.countAll = getNativeOptions(this.$select).length.toString();
|
|
339
340
|
|
|
340
341
|
this.elemTop.className = getClass('top');
|
|
341
342
|
|
|
@@ -430,7 +431,16 @@ export class SimpleSelectItemDOM {
|
|
|
430
431
|
}
|
|
431
432
|
/** end MultiSelect debounce animate status */
|
|
432
433
|
|
|
434
|
+
const classOpened = getClass('opened', true);
|
|
433
435
|
this.elemDropDownWrap.append(this.elemDropDown);
|
|
436
|
+
this.elemDropDownWrap.addEventListener('transitionend', (e) => {
|
|
437
|
+
if (e.propertyName === 'max-height') {
|
|
438
|
+
if (this.state.getState('isOpen')) {
|
|
439
|
+
this.elemWrap.classList.add(classOpened);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
});
|
|
443
|
+
|
|
434
444
|
this.elemWrap.appendChild(this.elemDropDownWrap);
|
|
435
445
|
this.elemDropDown.appendChild(this.elemListBody);
|
|
436
446
|
|
|
@@ -501,7 +511,6 @@ export class SimpleSelectItemDOM {
|
|
|
501
511
|
this.elemAlwaysLabel = document.createElement('div');
|
|
502
512
|
let classesTitle = getClass('label');
|
|
503
513
|
if (this.options.isOnlyPlaceholder) {
|
|
504
|
-
alert('111');
|
|
505
514
|
classesTitle += ` ${getClass('always-placeholder', true, classesTitle)}`;
|
|
506
515
|
}
|
|
507
516
|
this.elemAlwaysLabel.className = classesTitle;
|
|
@@ -553,7 +562,7 @@ export class SimpleSelectItemDOM {
|
|
|
553
562
|
if (itemsChecked.length > maxShow) {
|
|
554
563
|
title = `${this.options.locale.selected} ${itemsChecked.length}`;
|
|
555
564
|
|
|
556
|
-
if (this.$select
|
|
565
|
+
if (getNativeOptions(this.$select).length === itemsChecked.length) {
|
|
557
566
|
title += ` (${this.options.locale.all})`;
|
|
558
567
|
}
|
|
559
568
|
} else if (attrTitle) {
|
|
@@ -563,8 +572,6 @@ export class SimpleSelectItemDOM {
|
|
|
563
572
|
|
|
564
573
|
this.elemTitle.innerHTML = title;
|
|
565
574
|
|
|
566
|
-
|
|
567
|
-
|
|
568
575
|
if (isPlaceholder) {
|
|
569
576
|
this.elemTitle.classList.add('SimpleSel__title--placeholder');
|
|
570
577
|
this.elemTitle.classList.remove('SimpleSel__title--fill');
|
|
@@ -612,6 +619,8 @@ export class SimpleSelectItemDOM {
|
|
|
612
619
|
let countCheckedFullItems = 0;
|
|
613
620
|
|
|
614
621
|
const items = isFilter ? this.filterList() : this.state.getState('items');
|
|
622
|
+
// один раз на весь проход: копирование списка option заметно на больших select
|
|
623
|
+
const nativeOptions = getNativeOptions(this.$select);
|
|
615
624
|
const dataForCompare: IDataForCompareOptions[] = [];
|
|
616
625
|
items.forEach((group:IOptionItems) => {
|
|
617
626
|
group.items.forEach((i) => {
|
|
@@ -625,7 +634,7 @@ export class SimpleSelectItemDOM {
|
|
|
625
634
|
if (!group.isGroup) {
|
|
626
635
|
const {
|
|
627
636
|
result, countShow, countChecked, countCheckedFull,
|
|
628
|
-
} = this.createLi(group);
|
|
637
|
+
} = this.createLi(group, nativeOptions);
|
|
629
638
|
resBodyList += result;
|
|
630
639
|
countShowItem += countShow;
|
|
631
640
|
countCheckedItems += countChecked;
|
|
@@ -633,7 +642,7 @@ export class SimpleSelectItemDOM {
|
|
|
633
642
|
} else {
|
|
634
643
|
const {
|
|
635
644
|
result, countShow, countChecked, countCheckedFull,
|
|
636
|
-
} = this.createLi(group);
|
|
645
|
+
} = this.createLi(group, nativeOptions);
|
|
637
646
|
let groupAttrs = `data-count-show="${countShow}" `;
|
|
638
647
|
groupAttrs += `data-count-checked="${countChecked}" `;
|
|
639
648
|
let classGroup = getClass('group_items');
|
|
@@ -661,7 +670,7 @@ export class SimpleSelectItemDOM {
|
|
|
661
670
|
this.elemWrap.dataset.countChecked = countCheckedItems.toString();
|
|
662
671
|
this.elemWrap.dataset.countCheckedFull = countCheckedFullItems.toString();
|
|
663
672
|
if (this.isMulti) {
|
|
664
|
-
this.elemWrap.dataset.checkAllMulti = (
|
|
673
|
+
this.elemWrap.dataset.checkAllMulti = (nativeOptions.length === countCheckedItems) ? 'yes' : 'no';
|
|
665
674
|
}
|
|
666
675
|
|
|
667
676
|
resBodyList = removeExtraSpaces(resBodyList);
|
|
@@ -739,7 +748,7 @@ export class SimpleSelectItemDOM {
|
|
|
739
748
|
return res;
|
|
740
749
|
}
|
|
741
750
|
|
|
742
|
-
private createLi(data: IOptionItems): ICreateLiReturn {
|
|
751
|
+
private createLi(data: IOptionItems, nativeOptions: HTMLOptionElement[]): ICreateLiReturn {
|
|
743
752
|
let result = '';
|
|
744
753
|
let countShow = 0;
|
|
745
754
|
let countChecked = 0;
|
|
@@ -805,7 +814,7 @@ export class SimpleSelectItemDOM {
|
|
|
805
814
|
}
|
|
806
815
|
|
|
807
816
|
result += `<li class="${classLi}" ${dataAttr}>`;
|
|
808
|
-
const createLiBodyRes = this.createLiBody(option, this
|
|
817
|
+
const createLiBodyRes = this.createLiBody(option, this.findNativeOption(option, nativeOptions));
|
|
809
818
|
result += typeof createLiBodyRes === 'string' ? createLiBodyRes : createLiBodyRes.outerHTML;
|
|
810
819
|
result += '</li>';
|
|
811
820
|
});
|
|
@@ -817,7 +826,30 @@ export class SimpleSelectItemDOM {
|
|
|
817
826
|
};
|
|
818
827
|
}
|
|
819
828
|
|
|
820
|
-
|
|
829
|
+
// нативного option может не быть на своей позиции: DOM select могли поменять после построения списка
|
|
830
|
+
// (авто-перевод браузера, внешний скрипт) - тогда ищем по value
|
|
831
|
+
private findNativeOption(option: IOptionItem, nativeOptions: HTMLOptionElement[]): HTMLOptionElement | null {
|
|
832
|
+
const byPosition = nativeOptions[option.position];
|
|
833
|
+
if (byPosition && byPosition.getAttribute('value') === option.value) {
|
|
834
|
+
return byPosition;
|
|
835
|
+
}
|
|
836
|
+
return nativeOptions.find((native) => native.getAttribute('value') === option.value) || byPosition || null;
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
// запасной option, чтобы changeBodyLi всегда получал элемент, даже если нативный не нашёлся
|
|
840
|
+
private createFakeOption(option: IOptionItem): HTMLOptionElement {
|
|
841
|
+
const fake = document.createElement('option');
|
|
842
|
+
if (option.value !== null) {
|
|
843
|
+
fake.value = option.value;
|
|
844
|
+
}
|
|
845
|
+
fake.innerHTML = option.title;
|
|
846
|
+
fake.selected = option.checked;
|
|
847
|
+
fake.disabled = option.disabled;
|
|
848
|
+
|
|
849
|
+
return fake;
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
private createLiBody(option: IOptionItem, optionNative: HTMLOptionElement | null): HTMLElement | string {
|
|
821
853
|
const item = document.createElement('div');
|
|
822
854
|
|
|
823
855
|
item.className = getClass('list_item_body');
|
|
@@ -831,7 +863,7 @@ export class SimpleSelectItemDOM {
|
|
|
831
863
|
res += this.bodyLiHTMLBeforeFromSelect;
|
|
832
864
|
}
|
|
833
865
|
|
|
834
|
-
if (optionNative.hasAttribute('data-simple-html-before')) {
|
|
866
|
+
if (optionNative && optionNative.hasAttribute('data-simple-html-before')) {
|
|
835
867
|
res += optionNative.getAttribute('data-simple-html-before');
|
|
836
868
|
}
|
|
837
869
|
|
|
@@ -839,14 +871,14 @@ export class SimpleSelectItemDOM {
|
|
|
839
871
|
if (this.bodyLiHTMLAfterFromSelect) {
|
|
840
872
|
res += this.bodyLiHTMLAfterFromSelect;
|
|
841
873
|
}
|
|
842
|
-
if (optionNative.hasAttribute('data-simple-html-after')) {
|
|
874
|
+
if (optionNative && optionNative.hasAttribute('data-simple-html-after')) {
|
|
843
875
|
res += optionNative.getAttribute('data-simple-html-after');
|
|
844
876
|
}
|
|
845
877
|
|
|
846
878
|
item.innerHTML = res;
|
|
847
879
|
|
|
848
880
|
if (this.options.changeBodyLi) {
|
|
849
|
-
return this.options.changeBodyLi(item, optionNative);
|
|
881
|
+
return this.options.changeBodyLi(item, optionNative || this.createFakeOption(option));
|
|
850
882
|
}
|
|
851
883
|
return item;
|
|
852
884
|
}
|
package/src/style.css
CHANGED
|
@@ -49,6 +49,16 @@
|
|
|
49
49
|
.SimpleSel * {
|
|
50
50
|
box-sizing: border-box;
|
|
51
51
|
}
|
|
52
|
+
|
|
53
|
+
/*
|
|
54
|
+
* Авто-перевод браузера оборачивает текст в <font> и пересоздаёт эти узлы.
|
|
55
|
+
* Если узел, получивший mousedown, удалён из DOM до mouseup, Chrome не генерирует click
|
|
56
|
+
* и селект не открывается. Делегирование не помогает: события click нет вообще.
|
|
57
|
+
* Убираем <font> из hit-testing, чтобы целью события был стабильный родитель
|
|
58
|
+
*/
|
|
59
|
+
.SimpleSel font {
|
|
60
|
+
pointer-events: none;
|
|
61
|
+
}
|
|
52
62
|
.SimpleSel :after,
|
|
53
63
|
.SimpleSel :before {
|
|
54
64
|
box-sizing: border-box;
|
|
@@ -111,6 +121,7 @@
|
|
|
111
121
|
.SimpleSel__list_item_icon,
|
|
112
122
|
.SimpleSel__reset_all__icon {
|
|
113
123
|
width: var(--simpS_size_check);
|
|
124
|
+
min-width: var(--simpS_size_check);
|
|
114
125
|
height: var(--simpS_size_check);
|
|
115
126
|
position: relative;
|
|
116
127
|
border: 1px solid currentColor;
|
|
@@ -219,7 +230,6 @@
|
|
|
219
230
|
opacity: 0;
|
|
220
231
|
visibility: hidden;
|
|
221
232
|
overflow: hidden;
|
|
222
|
-
overflow-y: auto;
|
|
223
233
|
position: absolute;
|
|
224
234
|
transition: .25s all;
|
|
225
235
|
width: 100%;
|
|
@@ -228,6 +238,7 @@
|
|
|
228
238
|
border-radius: var(--simpS_radius);
|
|
229
239
|
background-color: var(--simpS_dropdownBg);
|
|
230
240
|
overscroll-behavior: contain;
|
|
241
|
+
|
|
231
242
|
}
|
|
232
243
|
.SimpleSel--body-always_open {
|
|
233
244
|
max-height: none;
|
|
@@ -235,6 +246,7 @@
|
|
|
235
246
|
top: auto;
|
|
236
247
|
opacity: 1;
|
|
237
248
|
visibility: visible;
|
|
249
|
+
overflow-y: auto;
|
|
238
250
|
}
|
|
239
251
|
|
|
240
252
|
.SimpleSel:not(.SimpleSel--up) .SimpleSel--multi {
|
|
@@ -247,6 +259,9 @@
|
|
|
247
259
|
margin-bottom: 2px;
|
|
248
260
|
}
|
|
249
261
|
|
|
262
|
+
.SimpleSel--opened .SimpleSel__body {
|
|
263
|
+
overflow-y: auto;
|
|
264
|
+
}
|
|
250
265
|
.SimpleSel--open .SimpleSel__body {
|
|
251
266
|
max-height: 230px;
|
|
252
267
|
opacity: 1;
|
|
@@ -48,15 +48,32 @@ export function triggerInputEvent(element: HTMLElement, type = 'change') {
|
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
});
|
|
51
|
+
// select.options может не содержать option, если браузер (авто-перевод страницы) обернул их в свой тег.
|
|
52
|
+
// querySelectorAll находит option в любом случае, порядок тот же (порядок в документе)
|
|
53
|
+
export const getNativeOptions = (select: HTMLSelectElement): HTMLOptionElement[] => Array.from(select.querySelectorAll('option'));
|
|
54
|
+
|
|
55
|
+
export const getOptionsPositions = (select: HTMLSelectElement): Map<HTMLOptionElement, number> => {
|
|
56
|
+
const positions = new Map<HTMLOptionElement, number>();
|
|
57
|
+
getNativeOptions(select).forEach((option, ind) => {
|
|
58
|
+
positions.set(option, ind);
|
|
59
|
+
});
|
|
60
|
+
return positions;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export const getCreateItem = (option: HTMLOptionElement, index: number, positions?: Map<HTMLOptionElement, number>) => {
|
|
64
|
+
const position = positions ? positions.get(option) : undefined;
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
id: (index + 1).toString(),
|
|
68
|
+
// option.index возвращает 0, если option вне списка опций select (обёрнут другим тегом)
|
|
69
|
+
position: position === undefined ? option.index : position,
|
|
70
|
+
title: option.innerHTML,
|
|
71
|
+
value: option.getAttribute('value'),
|
|
72
|
+
checked: option.selected,
|
|
73
|
+
disabled: option.disabled,
|
|
74
|
+
isShowFilter: true,
|
|
75
|
+
};
|
|
76
|
+
};
|
|
60
77
|
|
|
61
78
|
export const createOptionItems = (isGroup: boolean, idGroup: string, items:IOptionItem[], isShowFilter = true): IOptionItems => {
|
|
62
79
|
return {
|
|
@@ -67,20 +84,25 @@ export const createOptionItems = (isGroup: boolean, idGroup: string, items:IOpti
|
|
|
67
84
|
};
|
|
68
85
|
};
|
|
69
86
|
|
|
70
|
-
export const getCreateListItem = (
|
|
87
|
+
export const getCreateListItem = (
|
|
88
|
+
item: HTMLSelectElement | HTMLOptGroupElement | HTMLOptionElement,
|
|
89
|
+
idGroup: string,
|
|
90
|
+
isGroup: boolean,
|
|
91
|
+
positions?: Map<HTMLOptionElement, number>,
|
|
92
|
+
) => {
|
|
71
93
|
// Если есть группы, но может быть option вне группы
|
|
72
94
|
if (item instanceof HTMLOptionElement) {
|
|
73
95
|
return createOptionItems(
|
|
74
96
|
isGroup,
|
|
75
97
|
idGroup,
|
|
76
|
-
[getCreateItem(item, 1)],
|
|
98
|
+
[getCreateItem(item, 1, positions)],
|
|
77
99
|
true,
|
|
78
100
|
);
|
|
79
101
|
}
|
|
80
102
|
const options = item.querySelectorAll('option');
|
|
81
103
|
const items:IOptionItem[] = [];
|
|
82
104
|
options.forEach((option, ind) => {
|
|
83
|
-
items.push(getCreateItem(option, ind));
|
|
105
|
+
items.push(getCreateItem(option, ind, positions));
|
|
84
106
|
});
|
|
85
107
|
const newItem: IOptionItems = createOptionItems(
|
|
86
108
|
isGroup,
|