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.
@@ -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 += ' ' + getClass('label_mode', true);
336
+ resClassesWrap += ` ${getClass('label_mode', true)}`;
336
337
  }
337
338
  this.elemWrap.className = resClassesWrap;
338
- this.elemWrap.dataset.countAll = this.$select.options.length.toString();
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.querySelectorAll('option').length === itemsChecked.length) {
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 = (this.$select.options.length === countCheckedItems) ? 'yes' : 'no';
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.$select.options[option.position]);
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
- private createLiBody(option: IOptionItem, optionNative: HTMLOptionElement): HTMLElement | string {
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
- export const getCreateItem = (option: HTMLOptionElement, index: number) => ({
52
- id: (index + 1).toString(),
53
- position: option.index,
54
- title: option.innerHTML,
55
- value: option.getAttribute('value'),
56
- checked: option.selected,
57
- disabled: option.disabled,
58
- isShowFilter: true,
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 = (item: HTMLSelectElement | HTMLOptGroupElement | HTMLOptionElement, idGroup: string, isGroup: boolean) => {
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,