vgapp 1.5.6 → 1.5.8

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 (109) hide show
  1. package/app/langs/en/buttons.json +17 -17
  2. package/app/langs/en/messages.json +36 -36
  3. package/app/langs/ru/buttons.json +17 -17
  4. package/app/langs/ru/messages.json +1 -1
  5. package/app/modules/vgalert/js/vgalert.js +6 -3
  6. package/app/modules/vgalert/scss/_variables.scss +16 -16
  7. package/app/modules/vgcollapse/scss/_variables.scss +7 -7
  8. package/app/modules/vgcollapse/scss/vgcollapse.scss +5 -5
  9. package/app/modules/vgdropdown/scss/_variables.scss +14 -14
  10. package/app/modules/vgdropdown/scss/vgdropdown.scss +5 -5
  11. package/app/modules/vgfilepreview/js/i18n.js +56 -56
  12. package/app/modules/vgfilepreview/js/renderers/image-modal.js +145 -145
  13. package/app/modules/vgfilepreview/js/renderers/image.js +92 -92
  14. package/app/modules/vgfilepreview/js/renderers/index.js +19 -19
  15. package/app/modules/vgfilepreview/js/renderers/office-modal.js +168 -168
  16. package/app/modules/vgfilepreview/js/renderers/office.js +79 -79
  17. package/app/modules/vgfilepreview/js/renderers/pdf-modal.js +260 -260
  18. package/app/modules/vgfilepreview/js/renderers/pdf.js +76 -76
  19. package/app/modules/vgfilepreview/js/renderers/playlist.js +71 -71
  20. package/app/modules/vgfilepreview/js/renderers/text-modal.js +343 -343
  21. package/app/modules/vgfilepreview/js/renderers/text.js +83 -83
  22. package/app/modules/vgfilepreview/js/renderers/zip-modal.js +522 -522
  23. package/app/modules/vgfilepreview/js/renderers/zip.js +89 -89
  24. package/app/modules/vgfilepreview/readme.md +68 -68
  25. package/app/modules/vgfiles/js/base.js +189 -194
  26. package/app/modules/vgfiles/js/droppable.js +17 -17
  27. package/app/modules/vgfiles/js/render.js +153 -153
  28. package/app/modules/vgfiles/js/sortable.js +52 -52
  29. package/app/modules/vgfiles/js/vgfiles.js +43 -43
  30. package/app/modules/vgfiles/readme.md +123 -123
  31. package/app/modules/vgfiles/scss/_mixins.scss +13 -13
  32. package/app/modules/vgfiles/scss/_variables.scss +10 -10
  33. package/app/modules/vgfiles/scss/vgfiles.scss +28 -28
  34. package/app/modules/vgformsender/js/hideshowpass.js +22 -22
  35. package/app/modules/vgformsender/readme.md +1 -1
  36. package/app/modules/vgformsender/scss/_variables.scss +5 -5
  37. package/app/modules/vgformsender/scss/vgformsender.scss +42 -42
  38. package/app/modules/vglawcookie/js/vglawcookie.js +19 -19
  39. package/app/modules/vglawcookie/scss/_variables.scss +13 -13
  40. package/app/modules/vglawcookie/scss/vglawcookie.scss +5 -5
  41. package/app/modules/vgmodal/js/vgmodal.interaction.js +216 -216
  42. package/app/modules/vgmodal/js/vgmodal.js +6 -0
  43. package/app/modules/vgmodal/js/vgmodal.minimized.js +227 -227
  44. package/app/modules/vgmodal/scss/_variables.scss +17 -17
  45. package/app/modules/vgmodal/scss/vgmodal.scss +77 -77
  46. package/app/modules/vgnav/js/vgnav.js +62 -62
  47. package/app/modules/vgnav/scss/_breakpoints.scss +36 -36
  48. package/app/modules/vgnav/scss/_hamburger.scss +5 -5
  49. package/app/modules/vgnav/scss/_placement.scss +5 -5
  50. package/app/modules/vgnav/scss/_toggle.scss +5 -5
  51. package/app/modules/vgnav/scss/_variables.scss +26 -26
  52. package/app/modules/vgnav/scss/vgnav.scss +41 -41
  53. package/app/modules/vgnestable/README.md +39 -39
  54. package/app/modules/vgrangeslider/js/skins.js +8 -8
  55. package/app/modules/vgrangeslider/js/vgrangeslider.js +29 -29
  56. package/app/modules/vgrangeslider/scss/_variables.scss +55 -55
  57. package/app/modules/vgrangeslider/scss/vgrangeslider.scss +239 -239
  58. package/app/modules/vgrollup/js/vgrollup.js +15 -15
  59. package/app/modules/vgrollup/scss/_variables.scss +15 -15
  60. package/app/modules/vgrollup/scss/vgrollup.scss +6 -6
  61. package/app/modules/vgselect/js/handlers.js +5 -52
  62. package/app/modules/vgselect/js/vgselect.js +128 -40
  63. package/app/modules/vgselect/readme.md +11 -11
  64. package/app/modules/vgselect/scss/_variables.scss +51 -51
  65. package/app/modules/vgselect/scss/vgselect.scss +1 -1
  66. package/app/modules/vgsidebar/js/vgsidebar.js +16 -5
  67. package/app/modules/vgsidebar/scss/_variables.scss +10 -10
  68. package/app/modules/vgsidebar/scss/vgsidebar.scss +5 -5
  69. package/app/modules/vgspy/js/vgspy.js +28 -7
  70. package/app/modules/vgtable/js/_filters.js +10 -6
  71. package/app/modules/vgtable/js/_options.js +16 -16
  72. package/app/modules/vgtable/js/_pagination.js +118 -118
  73. package/app/modules/vgtable/js/_responsive.js +79 -79
  74. package/app/modules/vgtable/js/_sticky-header.js +349 -280
  75. package/app/modules/vgtable/js/vgtable.js +98 -98
  76. package/app/modules/vgtable/scss/_sorting.scss +6 -6
  77. package/app/modules/vgtable/scss/_sticky-header.scss +31 -31
  78. package/app/modules/vgtable/scss/_variables.scss +6 -4
  79. package/app/modules/vgtable/scss/vgtable.scss +8 -4
  80. package/app/modules/vgtabs/js/vgtabs.js +82 -57
  81. package/app/modules/vgtabs/scss/_variables.scss +23 -23
  82. package/app/modules/vgtabs/scss/vgtabs.scss +5 -5
  83. package/app/modules/vgtoast/js/vgtoast.js +11 -5
  84. package/app/modules/vgtoast/scss/_variables.scss +13 -13
  85. package/app/modules/vgtoast/scss/vgtoast.scss +5 -5
  86. package/app/modules/vgtooltip/js/vgtooltip.js +121 -111
  87. package/app/modules/vgtooltip/scss/_variables.scss +10 -10
  88. package/app/modules/vgtooltip/scss/vgtooltip.scss +5 -5
  89. package/app/utils/js/components/audio-metadata.js +240 -240
  90. package/app/utils/js/components/backdrop.js +116 -116
  91. package/app/utils/js/components/file-icon.js +5 -0
  92. package/app/utils/js/components/file-preview.js +304 -304
  93. package/app/utils/js/components/responsive.js +165 -165
  94. package/app/utils/js/components/sanitize.js +150 -150
  95. package/app/utils/js/components/video-metadata.js +140 -140
  96. package/app/utils/scss/_backdrop.scss +5 -5
  97. package/app/utils/scss/_theme-default.scss +12 -12
  98. package/app/utils/scss/_theme.scss +7 -7
  99. package/app/utils/scss/default.scss +9 -9
  100. package/app/utils/scss/functions.scss +21 -21
  101. package/app/utils/scss/index.scss +4 -4
  102. package/app/utils/scss/mixin.scss +16 -16
  103. package/app/utils/scss/variables.scss +147 -147
  104. package/build/vgapp.css +1 -1
  105. package/build/vgapp.css.map +1 -1
  106. package/build/vgapp.js +1 -1
  107. package/build/vgapp.js.map +1 -1
  108. package/index.js +7 -7
  109. package/package.json +1 -1
@@ -1,9 +1,14 @@
1
+ /**
2
+ * Описание: кастомный select с синхронизацией исходного поля и выпадающего списка.
3
+ * Возможности: поиск, группы, дерево, теги, ограничение высоты, AJAX-пагинация, события и освобождение ресурсов.
4
+ */
1
5
  import BaseModule from "../../base-module";
2
6
  import {
3
7
  isDisabled,
4
8
  isEmptyObj,
5
9
  mergeDeepObject,
6
10
  normalizeData,
11
+ transliterate,
7
12
  } from "../../../utils/js/functions";
8
13
  import {Classes, Manipulator} from "../../../utils/js/dom/manipulator";
9
14
  import EventHandler from "../../../utils/js/dom/event";
@@ -30,7 +35,9 @@ const CLASS_NAME_TAG = 'vg-select-tag';
30
35
  const CLASS_NAME_TAG_REMOVE = 'vg-select-tag-remove';
31
36
  const CLASS_NAME_LOAD_MORE = 'vg-select-load-more';
32
37
  const CLASS_NAME_LOADING = 'vg-select-loading';
33
- const CLASS_NAME_DROP_UP = 'drop-up';
38
+ const CLASS_NAME_DROP_UP = 'drop-up';
39
+
40
+ const DEFAULT_LIST_MAX_HEIGHT = 300;
34
41
 
35
42
  const EVENT_KEY_CHANGE = `${NAME_KEY}.change`;
36
43
  const EVENT_KEY_HIDE = `${NAME_KEY}.hide`;
@@ -73,11 +80,12 @@ class VGSelect extends BaseModule {
73
80
  autosearch: true,
74
81
  // Dropdown placement behavior:
75
82
  // - none: default CSS positioning (no JS)
76
- // - auto: choose top/bottom based on available space in overflow ancestor/viewport
83
+ // - auto (default): choose top/bottom based on available space in overflow ancestor/viewport
77
84
  // - top: force open upwards
78
85
  // - bottom: force open downwards
79
- position: 'none',
80
- search: {
86
+ position: 'auto',
87
+ height: true,
88
+ search: {
81
89
  enabled: false,
82
90
  route: '',
83
91
  remote: false,
@@ -105,8 +113,11 @@ class VGSelect extends BaseModule {
105
113
 
106
114
  this._observer = null;
107
115
  this._observerTimeout = null;
116
+ this._searchTimeout = null;
117
+ this._disposed = false;
108
118
  this._visibilityTransitionId = 0;
109
- this._drop = Selectors.find(SELECTOR_DROPDOWN, this._element);
119
+ this._drop = Selectors.find(SELECTOR_DROPDOWN, this._element);
120
+ this._applyListMaxHeight();
110
121
  this._searchTerm = '';
111
122
  this._currentPage = 1;
112
123
  this._totalPages = null;
@@ -154,6 +165,7 @@ class VGSelect extends BaseModule {
154
165
  */
155
166
  static buildListOptions(selector, drop, params = {}) {
156
167
  let list = drop.querySelector(`.${CLASS_NAME_LIST}`);
168
+ const controls = list ? [...list.querySelectorAll(`.${CLASS_NAME_LOAD_MORE}, .${CLASS_NAME_LOADING}`)] : [];
157
169
  if (!list) {
158
170
  list = document.createElement('ul');
159
171
  Classes.add(list, CLASS_NAME_LIST);
@@ -190,6 +202,7 @@ class VGSelect extends BaseModule {
190
202
  });
191
203
  }
192
204
 
205
+ controls.forEach(control => list.appendChild(control));
193
206
  return list;
194
207
  }
195
208
 
@@ -228,9 +241,6 @@ class VGSelect extends BaseModule {
228
241
  li.style.paddingLeft = `${16 + (level * 16)}px`;
229
242
  }
230
243
 
231
- // Раньше подсветка зависела от "явно выбранных" option (атрибут selected),
232
- // из-за этого UI мог показывать placeholder, но DOM считал, что выбрана 1-я опция.
233
- // Подсвечиваем по фактическому состоянию option.selected.
234
244
  if (option.selected) {
235
245
  li.classList.add('selected');
236
246
  }
@@ -459,17 +469,16 @@ class VGSelect extends BaseModule {
459
469
  search.appendChild(searchInput);
460
470
  this._drop.insertBefore(search, this._drop.firstChild);
461
471
 
462
- let searchTimeout;
463
472
  searchInput.addEventListener('input', (e) => {
464
473
  const term = e.target.value.trim();
465
474
  const params = this._params;
466
475
 
467
476
  this._callCallback('onSearch', { term });
468
477
  if (params.search.remote && params.search.route) {
478
+ clearTimeout(this._searchTimeout);
479
+ this._remoteSearchAbortController?.abort();
469
480
  if (term.length < (params.search.minterm || 1)) return;
470
-
471
- clearTimeout(searchTimeout);
472
- searchTimeout = setTimeout(() => {
481
+ this._searchTimeout = setTimeout(() => {
473
482
  this._fetchRemoteData(term);
474
483
  }, params.search.delay || 300);
475
484
  } else {
@@ -520,7 +529,7 @@ class VGSelect extends BaseModule {
520
529
  if (searchInput) searchInput.focus();
521
530
 
522
531
  return this._queueCallback(() => {
523
- if (transitionId !== this._visibilityTransitionId || !this._isShown()) return;
532
+ if (this._disposed || transitionId !== this._visibilityTransitionId || !this._isShown()) return;
524
533
  this._updateDropdownPlacement();
525
534
  EventHandler.trigger(this._element, EVENT_KEY_SHOWN, { relatedTarget });
526
535
  this._triggerEvent(EVENT_KEY_OPEN);
@@ -552,7 +561,7 @@ class VGSelect extends BaseModule {
552
561
  this._element.querySelector(SELECTOR_DATA_TOGGLE).setAttribute('aria-expanded', 'false');
553
562
 
554
563
  this._queueCallback(() => {
555
- if (transitionId !== this._visibilityTransitionId) return;
564
+ if (this._disposed || transitionId !== this._visibilityTransitionId) return;
556
565
  this._element.classList.remove(CLASS_NAME_SHOW);
557
566
  EventHandler.trigger(this._element, EVENT_KEY_HIDDEN, relatedTarget);
558
567
  this._triggerEvent(EVENT_KEY_CLOSE);
@@ -647,6 +656,7 @@ class VGSelect extends BaseModule {
647
656
  * Освобождает ресурсы (отключает observer, очищает таймеры)
648
657
  */
649
658
  dispose() {
659
+ if (this._disposed) return;
650
660
  this._visibilityTransitionId++;
651
661
  if (this._observer) {
652
662
  this._observer.disconnect();
@@ -654,8 +664,12 @@ class VGSelect extends BaseModule {
654
664
  }
655
665
  clearTimeout(this._observerTimeout);
656
666
  this._observerTimeout = null;
667
+ clearTimeout(this._searchTimeout);
668
+ this._remoteSearchAbortController?.abort();
669
+ if (this._isShown() && 'ontouchstart' in document.documentElement) document.body.style.pointerEvents = '';
657
670
  this._teardownDropdownPlacement();
658
671
  super.dispose();
672
+ this._disposed = true;
659
673
  }
660
674
 
661
675
  _setupDropdownPlacement() {
@@ -675,7 +689,7 @@ class VGSelect extends BaseModule {
675
689
  this._updateDropdownPlacement();
676
690
  }
677
691
 
678
- _teardownDropdownPlacement() {
692
+ _teardownDropdownPlacement() {
679
693
  window.removeEventListener('resize', this._boundUpdateDropdownPlacement);
680
694
  window.removeEventListener('scroll', this._boundUpdateDropdownPlacement);
681
695
 
@@ -684,16 +698,63 @@ class VGSelect extends BaseModule {
684
698
  }
685
699
  this._positioningScrollParent = null;
686
700
 
687
- // Reset to stylesheet defaults.
688
- this._element.classList.remove(CLASS_NAME_DROP_UP);
689
- this._element.style.removeProperty('--vg-select-list-max-height');
690
- }
701
+ // Restore the configured limit after a temporary viewport clamp.
702
+ this._element.classList.remove(CLASS_NAME_DROP_UP);
703
+ this._applyListMaxHeight();
704
+ }
705
+
706
+ /**
707
+ * Возвращает пользовательское ограничение списка в пикселях.
708
+ * true использует стандартные 300px, false снимает ограничение.
709
+ * @returns {number|null}
710
+ * @private
711
+ */
712
+ _getListMaxHeight() {
713
+ const raw = this._params?.height;
714
+ if (raw === false || raw === null) return null;
715
+ if (raw === true || typeof raw === 'undefined' || raw === '') {
716
+ const cssHeight = parseFloat(window.getComputedStyle(this._element)
717
+ .getPropertyValue('--vg-select-list-max-height'));
718
+ return Number.isFinite(cssHeight) && cssHeight > 0 ? cssHeight : DEFAULT_LIST_MAX_HEIGHT;
719
+ }
720
+
721
+ const height = Number(raw);
722
+ return Number.isFinite(height) && height > 0 ? height : DEFAULT_LIST_MAX_HEIGHT;
723
+ }
724
+
725
+ /**
726
+ * Применяет базовое либо уменьшенное под доступное пространство ограничение.
727
+ * @param {number|null} [available=null] - Доступная высота дропа
728
+ * @private
729
+ */
730
+ _applyListMaxHeight(available = null) {
731
+ if (!this._element) return;
732
+
733
+ const raw = this._params?.height;
734
+ const usesStylesheetDefault = raw === true || typeof raw === 'undefined' || raw === '';
735
+ if (usesStylesheetDefault) {
736
+ this._element.style.removeProperty('--vg-select-list-max-height');
737
+ if (!Number.isFinite(available)) return;
738
+ }
739
+
740
+ const configured = this._getListMaxHeight();
741
+ if (configured === null) {
742
+ this._element.style.setProperty('--vg-select-list-max-height', 'none');
743
+ return;
744
+ }
745
+
746
+ const availableHeight = Number.isFinite(available)
747
+ ? Math.max(80, Math.floor(available - 12))
748
+ : configured;
749
+ const maxHeight = Math.min(configured, availableHeight);
750
+ this._element.style.setProperty('--vg-select-list-max-height', `${maxHeight}px`);
751
+ }
691
752
 
692
753
  _getPositionMode() {
693
754
  const raw = this._params?.position;
694
- const mode = raw == null ? 'none' : String(raw).trim().toLowerCase();
755
+ const mode = raw == null ? 'auto' : String(raw).trim().toLowerCase();
695
756
  if (mode === 'auto' || mode === 'top' || mode === 'bottom' || mode === 'none') return mode;
696
- return 'none';
757
+ return 'auto';
697
758
  }
698
759
 
699
760
  _getOverflowAncestor(startEl) {
@@ -751,12 +812,8 @@ class VGSelect extends BaseModule {
751
812
 
752
813
  const available = openUp ? spaceAbove : spaceBelow;
753
814
 
754
- // Give the dropdown some breathing room (borders/margins) and clamp.
755
- const listMax = Math.max(80, Math.floor(available - 12));
756
- if (Number.isFinite(listMax) && listMax > 0) {
757
- this._element.style.setProperty('--vg-select-list-max-height', `${listMax}px`);
758
- }
759
- }
815
+ this._applyListMaxHeight(available);
816
+ }
760
817
 
761
818
  /**
762
819
  * Удаляет кастомный UI
@@ -765,8 +822,10 @@ class VGSelect extends BaseModule {
765
822
  static destroy(select) {
766
823
  const container = select.nextElementSibling;
767
824
  if (container && container.classList.contains(CLASS_NAME_CONTAINER)) {
825
+ VGSelect.getInstance(container)?.dispose();
768
826
  container.remove();
769
827
  }
828
+ delete select.dataset.inited;
770
829
  }
771
830
 
772
831
  /**
@@ -781,6 +840,9 @@ class VGSelect extends BaseModule {
781
840
  const placeholder = select.dataset.placeholder || '';
782
841
  const isMultiple = select.multiple;
783
842
  const instance = VGSelect.getInstance(container);
843
+ container.querySelectorAll(`.${CLASS_NAME_OPTION}`).forEach(item => {
844
+ item.classList.toggle('selected', !!select.options[Number(item.dataset.index)]?.selected);
845
+ });
784
846
 
785
847
  if (isMultiple) {
786
848
  const tags = current.querySelector(`.${CLASS_NAME_TAGS}`);
@@ -799,6 +861,10 @@ class VGSelect extends BaseModule {
799
861
 
800
862
  if (selected.length === 0) {
801
863
  input.placeholder = placeholder;
864
+ const label = document.createElement('span');
865
+ label.className = CLASS_NAME_PLACEHOLDER;
866
+ label.textContent = placeholder;
867
+ tags.insertBefore(label, input);
802
868
  } else {
803
869
  input.placeholder = '';
804
870
  selected.forEach(opt => {
@@ -985,13 +1051,14 @@ class VGSelect extends BaseModule {
985
1051
 
986
1052
  const list = this._element.querySelector(SELECTOR_LIST);
987
1053
  if (!list) return;
1054
+ if (list.querySelector(SELECTOR_LOAD_MORE_BTN)) return;
988
1055
 
989
1056
  const btn = document.createElement('li');
990
1057
  btn.className = CLASS_NAME_LOAD_MORE;
991
1058
  btn.style.textAlign = 'center';
992
1059
  btn.style.padding = '8px';
993
1060
  btn.style.cursor = 'pointer';
994
- btn.style.color = '#007bff';
1061
+ btn.style.color = 'var(--vg-primary-color, #007bff)';
995
1062
  btn.style.fontSize = '14px';
996
1063
  btn.style.fontWeight = '500';
997
1064
  btn.textContent = this._params.search.loadMoreText;
@@ -1045,6 +1112,7 @@ class VGSelect extends BaseModule {
1045
1112
  async _loadNextPage() {
1046
1113
  const { route, pageParam = 'page', termParam = 'q', perpage = 20 } = this._params.search;
1047
1114
  const nextPage = this._currentPage + 1;
1115
+ const requestId = this._remoteSearchRequestId;
1048
1116
 
1049
1117
  const url = new URL(route, window.location.origin);
1050
1118
  url.searchParams.set(termParam, this._searchTerm);
@@ -1057,7 +1125,9 @@ class VGSelect extends BaseModule {
1057
1125
 
1058
1126
  try {
1059
1127
  const res = await fetch(url, { headers: { 'Content-Type': 'application/json' } });
1128
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
1060
1129
  const data = await res.json();
1130
+ if (this._disposed || requestId !== this._remoteSearchRequestId) return;
1061
1131
 
1062
1132
  if (Array.isArray(data.results)) {
1063
1133
  VGSelect.addOptions(this._element.previousElementSibling, data, { preserve: true });
@@ -1073,12 +1143,15 @@ class VGSelect extends BaseModule {
1073
1143
  this._triggerEvent(EVENT_KEY_LOAD_NEXT, { page: this._currentPage, term: this._searchTerm });
1074
1144
  }
1075
1145
  } catch (err) {
1146
+ if (this._disposed || requestId !== this._remoteSearchRequestId) return;
1076
1147
  console.error('VGSelect: Failed to load next page', err);
1077
1148
  this._triggerEvent(EVENT_KEY_ERROR, { error: 'Pagination fetch failed', term: this._searchTerm });
1078
1149
  this._hideLoadMoreButton(false); // оставить кнопку при ошибке
1079
1150
  } finally {
1080
- this._showLoading(false);
1081
- this._loading = false;
1151
+ if (!this._disposed && requestId === this._remoteSearchRequestId) {
1152
+ this._showLoading(false);
1153
+ this._loading = false;
1154
+ }
1082
1155
  }
1083
1156
  }
1084
1157
 
@@ -1101,6 +1174,7 @@ class VGSelect extends BaseModule {
1101
1174
  this._searchTerm = term;
1102
1175
  this._currentPage = 1;
1103
1176
  this._totalPages = null;
1177
+ this._loading = false;
1104
1178
 
1105
1179
  // Отменяем предыдущий запрос (если ещё летит)
1106
1180
  if (this._remoteSearchAbortController) {
@@ -1120,10 +1194,14 @@ class VGSelect extends BaseModule {
1120
1194
  headers: { 'Content-Type': 'application/json' },
1121
1195
  signal
1122
1196
  })
1123
- .then(res => res.json())
1197
+ .then(res => {
1198
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
1199
+ return res.json();
1200
+ })
1124
1201
  .then(data => {
1125
1202
  // Если прилетел не самый свежий ответ — игнорируем
1126
- if (requestId !== this._remoteSearchRequestId) return;
1203
+ if (this._disposed || requestId !== this._remoteSearchRequestId) return;
1204
+ if (!Array.isArray(data.results)) throw new Error('Invalid results');
1127
1205
 
1128
1206
  // Если пользователь уже ввёл другой текст — тоже игнорируем
1129
1207
  const liveTerm = searchInput ? searchInput.value.trim() : '';
@@ -1173,6 +1251,7 @@ class VGSelect extends BaseModule {
1173
1251
  }
1174
1252
  })
1175
1253
  .catch(err => {
1254
+ if (this._disposed || requestId !== this._remoteSearchRequestId) return;
1176
1255
  // Abort — нормальная ситуация при быстром вводе
1177
1256
  if (err && (err.name === 'AbortError')) return;
1178
1257
 
@@ -1182,7 +1261,7 @@ class VGSelect extends BaseModule {
1182
1261
  })
1183
1262
  .finally(() => {
1184
1263
  // Лоадер убираем только для последнего актуального запроса
1185
- if (requestId === this._remoteSearchRequestId) {
1264
+ if (!this._disposed && requestId === this._remoteSearchRequestId) {
1186
1265
  this._showLoading(false);
1187
1266
  }
1188
1267
  });
@@ -1283,6 +1362,7 @@ class VGSelect extends BaseModule {
1283
1362
  const drop = container.querySelector(`.${CLASS_NAME_DROPDOWN}`);
1284
1363
  VGSelect.buildListOptions(select, drop, instance?._params || {});
1285
1364
  instance?._syncSearch();
1365
+ this.updateUI(select);
1286
1366
  instance?._triggerEvent(EVENT_KEY_REBUILD);
1287
1367
  } else {
1288
1368
  this.updateUI(select);
@@ -1297,17 +1377,25 @@ class VGSelect extends BaseModule {
1297
1377
  _filterLocalOptions(term) {
1298
1378
  const list = this._drop.querySelector(`.${CLASS_NAME_LIST}`);
1299
1379
  const options = list.querySelectorAll(`.${CLASS_NAME_OPTION}`);
1300
-
1301
- if (!term) {
1302
- options.forEach(el => el.hidden = false);
1303
- return;
1304
- }
1305
-
1306
1380
  term = term.toLowerCase();
1381
+ const search = [term, transliterate(term), transliterate(term, true)];
1382
+ let results = 0;
1307
1383
  options.forEach(el => {
1308
1384
  const text = el.textContent.toLowerCase();
1309
- el.hidden = !text.includes(term);
1385
+ const option = this._element.previousElementSibling.options[Number(el.dataset.index)];
1386
+ const empty = option?.value === '' && text.trim() === '';
1387
+ const visible = !empty && (!term || search.some(value => text.includes(value)));
1388
+ el.hidden = !visible;
1389
+ el.style.display = visible ? '' : 'none';
1390
+ if (visible) results++;
1391
+ });
1392
+ list.querySelectorAll(`.${CLASS_NAME_OPTGROUP}`).forEach(group => {
1393
+ const visible = [...group.querySelectorAll(`.${CLASS_NAME_OPTION}`)].some(option => !option.hidden);
1394
+ group.hidden = !visible;
1395
+ group.style.display = visible ? '' : 'none';
1310
1396
  });
1397
+ this._triggerEvent(`${NAME_KEY}.search`, {query: term, results});
1398
+ this._callCallback('onSearch', {query: term, results});
1311
1399
  }
1312
1400
  }
1313
1401
 
@@ -38,11 +38,11 @@
38
38
  ```js
39
39
  js import VGSelect from './app/modules/vgselect/js/vgselect';
40
40
  // Инициализация
41
- VGSelect.init(document.getElementById('mySelect'), {
42
- lang: 'ru',
43
- placeholder: 'Выберите значение',
44
- autosearch: true,
45
- search: {
41
+ VGSelect.init(document.getElementById('mySelect'), {
42
+ lang: 'ru',
43
+ placeholder: 'Выберите значение',
44
+ autosearch: true,
45
+ search: {
46
46
  enabled: true,
47
47
  remote: true,
48
48
  route: '/api/search',
@@ -62,11 +62,11 @@ VGSelect.init(document.getElementById('mySelect'), {
62
62
 
63
63
  | Параметр | Тип | Описание |
64
64
  |--------|-----|---------|
65
- | `lang` | `string` | Язык интерфейса (поддерживается i18n). По умолчанию: `ru` |
66
- | `placeholder` | `string` | Текст плейсхолдера |
67
- | `autosearch` | `boolean \| number` | Автоматически включить поиск, если опций больше порога. `true` — порог 7 (по умолчанию), число — собственный порог, `false` — отключить автоматику |
68
- | `search.enabled` | `boolean` | Включить поле поиска вручную при любом количестве опций |
69
- | `search.remote` | `boolean` | Поиск через AJAX; при `true` поле поиска включается независимо от количества локальных опций |
65
+ | `lang` | `string` | Язык интерфейса (поддерживается i18n). По умолчанию: `ru` |
66
+ | `placeholder` | `string` | Текст плейсхолдера |
67
+ | `autosearch` | `boolean \| number` | Автоматически включить поиск, если опций больше порога. `true` — порог 7 (по умолчанию), число — собственный порог, `false` — отключить автоматику |
68
+ | `search.enabled` | `boolean` | Включить поле поиска вручную при любом количестве опций |
69
+ | `search.remote` | `boolean` | Поиск через AJAX; при `true` поле поиска включается независимо от количества локальных опций |
70
70
  | `search.route` | `string` | URL для удалённого поиска |
71
71
  | `search.delay` | `number` | Задержка перед запросом (мс) |
72
72
  | `search.minTerm` | `number` | Минимальная длина запроса |
@@ -179,4 +179,4 @@ MIT. Свободно использовать и модифицировать.
179
179
 
180
180
  📌 *Разработано в рамках фронтенд-системы VG Modules.*
181
181
  > 🚀 Автор: VEGAS STUDIO (vegas-dev.com)
182
- > 📍 Поддерживается в проектах VEGAS
182
+ > 📍 Поддерживается в проектах VEGAS
@@ -1,11 +1,11 @@
1
- @use "../../../utils/scss" as vg;
2
-
3
- $select-current-map: (
4
- background-color: var(--vg-control-bg, #ffffff),
5
- color: var(--vg-text-color, #{vg.$color}),
6
- border-width: vg.$border-width,
7
- border-style: vg.$border-style,
8
- border-color: var(--vg-control-border-color, #dddddd),
1
+ @use "../../../utils/scss" as vg;
2
+
3
+ $select-current-map: (
4
+ background-color: var(--vg-control-bg, #ffffff),
5
+ color: var(--vg-text-color, #{vg.$color}),
6
+ border-width: vg.$border-width,
7
+ border-style: vg.$border-style,
8
+ border-color: var(--vg-control-border-color, #dddddd),
9
9
  border-radius: 5px,
10
10
  padding-left: 16px,
11
11
  padding-right: 40px,
@@ -16,69 +16,69 @@ $select-current-map: (
16
16
  z-index: 10,
17
17
  ) !default;
18
18
 
19
- $select-placeholder-map: (
20
- placehoder-color: var(--vg-text-placeholder-color, rgba(0, 0, 0, .32)),
21
- placehoder-font-size: 16px,
22
- ) !default;
19
+ $select-placeholder-map: (
20
+ placehoder-color: var(--vg-text-placeholder-color, rgba(0, 0, 0, .32)),
21
+ placehoder-font-size: 16px,
22
+ ) !default;
23
23
 
24
24
  $select-current-svg: '<svg aria-hidden="true" focusable="false" data-prefix="fa-light" data-icon="chevron-down" class="svg-inline--fa fa-chevron-down fa-w-14" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path d="M4 181C7 178 12 176 16 176C20 176 24 177 27 180L224 362L421 180C428 174 438 175 444 181C450 188 449 198 443 204L235 396C229 401 219 401 213 396L5 204C-1 198 -2 188 4 181Z" fill="gray"/></svg>' !default;
25
25
 
26
- $select-dropdown-map: (
27
- background-color: var(--vg-control-bg, #ffffff),
28
- color: var(--vg-text-color, #{vg.$color}),
29
- border-width: vg.$border-width,
30
- border-style: vg.$border-style,
31
- border-color: var(--vg-control-border-color, #dddddd),
32
- border-radius: vg.$border-radius,
33
- box-shadow: 0 7px 12px 1px var(--vg-shadow-soft-color, rgba(68, 68, 68, .11)),
26
+ $select-dropdown-map: (
27
+ background-color: var(--vg-control-bg, #ffffff),
28
+ color: var(--vg-text-color, #{vg.$color}),
29
+ border-width: vg.$border-width,
30
+ border-style: vg.$border-style,
31
+ border-color: var(--vg-control-border-color, #dddddd),
32
+ border-radius: vg.$border-radius,
33
+ box-shadow: 0 7px 12px 1px var(--vg-shadow-soft-color, rgba(68, 68, 68, .11)),
34
34
  z-index: 15,
35
35
  ) !default;
36
36
 
37
- $select-list-map: (
38
- background-color: transparent,
39
- color: var(--vg-text-color, #{vg.$color}),
37
+ $select-list-map: (
38
+ background-color: transparent,
39
+ color: var(--vg-text-color, #{vg.$color}),
40
40
  padding-left: 8px,
41
41
  padding-right: 8px,
42
42
  padding-top: 8px,
43
43
  padding-bottom: 8px,
44
- border-bottom-width: vg.$border-width,
45
- border-bottom-style: vg.$border-style,
46
- border-bottom-color: var(--vg-border-disabled-color, #f5f5f5),
44
+ border-bottom-width: vg.$border-width,
45
+ border-bottom-style: vg.$border-style,
46
+ border-bottom-color: var(--vg-border-disabled-color, #f5f5f5),
47
47
  ) !default;
48
48
 
49
- $select-optgroup-map: (
50
- background-color: transparent,
51
- color: var(--vg-text-color, #{vg.$color}),
49
+ $select-optgroup-map: (
50
+ background-color: transparent,
51
+ color: var(--vg-text-color, #{vg.$color}),
52
52
  padding-left: 8px,
53
53
  padding-right: 8px,
54
54
  padding-top: 8px,
55
55
  padding-bottom: 8px,
56
- border-bottom-width: vg.$border-width,
57
- border-bottom-style: vg.$border-style,
58
- border-bottom-color: var(--vg-border-strong-color, #838383),
56
+ border-bottom-width: vg.$border-width,
57
+ border-bottom-style: vg.$border-style,
58
+ border-bottom-color: var(--vg-border-strong-color, #838383),
59
59
  font-weight: 500,
60
60
  ) !default;
61
61
 
62
- $select-list-max-height: 286px !default;
62
+ $select-list-max-height: 300px !default;
63
63
  $select-list-scrollbar-width: 6px !default;
64
- $select-list-scrollbar-bg: var(--vg-surface-muted-bg, #f5f5f5) !default;
65
- $select-list-scrollbar-thumb: var(--vg-text-muted-color, #999999) !default;
64
+ $select-list-scrollbar-bg: var(--vg-surface-muted-bg, #f5f5f5) !default;
65
+ $select-list-scrollbar-thumb: var(--vg-text-muted-color, #999999) !default;
66
66
 
67
- $select-list-hover-map: (
68
- background-color: var(--vg-surface-hover-bg, #f7f7f7),
69
- color: var(--vg-text-color, #{vg.$color}),
70
- border-bottom-color: var(--vg-border-disabled-color, #f5f5f5),
67
+ $select-list-hover-map: (
68
+ background-color: var(--vg-surface-hover-bg, #f7f7f7),
69
+ color: var(--vg-text-color, #{vg.$color}),
70
+ border-bottom-color: var(--vg-border-disabled-color, #f5f5f5),
71
71
  ) !default;
72
72
 
73
- $select-optgroup-hover-map: (
74
- background-color: var(--vg-surface-hover-bg, #f7f7f7),
75
- color: var(--vg-text-color, #{vg.$color}),
76
- border-bottom-color: var(--vg-border-disabled-color, #f5f5f5),
73
+ $select-optgroup-hover-map: (
74
+ background-color: var(--vg-surface-hover-bg, #f7f7f7),
75
+ color: var(--vg-text-color, #{vg.$color}),
76
+ border-bottom-color: var(--vg-border-disabled-color, #f5f5f5),
77
77
  ) !default;
78
78
 
79
- $select-search-map: (
80
- background-color: var(--vg-control-muted-bg, #f2f2f2),
81
- color: var(--vg-text-color, #{vg.$color})
79
+ $select-search-map: (
80
+ background-color: var(--vg-control-muted-bg, #f2f2f2),
81
+ color: var(--vg-text-color, #{vg.$color})
82
82
  ) !default;
83
83
 
84
84
  $select-tags-map: (
@@ -88,15 +88,15 @@ $select-tags-map: (
88
88
  padding: 2px 0
89
89
  ) !default;
90
90
 
91
- $select-tag-map: (
92
- gap: 4px,
93
- background: var(--vg-primary-color, #0d6efd),
94
- color: var(--vg-primary-contrast-color, #ffffff),
91
+ $select-tag-map: (
92
+ gap: 4px,
93
+ background: var(--vg-primary-color, #0d6efd),
94
+ color: var(--vg-primary-contrast-color, #ffffff),
95
95
  padding: 2px 6px,
96
96
  border-radius: 4px,
97
97
  font-size: 14px,
98
98
  remove-width: 12px,
99
99
  remove-height: 12px,
100
- remove-stroke: var(--vg-primary-contrast-color, #ffffff),
100
+ remove-stroke: var(--vg-primary-contrast-color, #ffffff),
101
101
  remove-stroke-width: 2
102
102
  ) !default;
@@ -11,7 +11,7 @@
11
11
  @use "variables" as vars;
12
12
 
13
13
  select {
14
- &.vg-select {
14
+ &.vg-select[data-inited="true"] {
15
15
  position: absolute;
16
16
  left: 0;
17
17
  top: 0;
@@ -1,3 +1,7 @@
1
+ /**
2
+ * Описание: боковые панели VGSidebar с декларативным и программным управлением.
3
+ * Возможности: четыре стороны экрана, backdrop, прокрутка, URL-хэш, AJAX, события и доступность.
4
+ */
1
5
  import BaseModule from "../../base-module";
2
6
  import { isDisabled, isVisible, mergeDeepObject } from "../../../utils/js/functions";
3
7
  import EventHandler from "../../../utils/js/dom/event";
@@ -212,6 +216,7 @@ class VGSidebar extends BaseModule {
212
216
  }
213
217
 
214
218
  this._element.classList.add(CLASS_NAME_SHOW);
219
+ this._element.removeAttribute('aria-hidden');
215
220
  document.body.classList.add(CLASS_NAME_OPEN);
216
221
 
217
222
  const completeCallback = () => {
@@ -238,6 +243,7 @@ class VGSidebar extends BaseModule {
238
243
  if (hideEvent.defaultPrevented) return;
239
244
 
240
245
  this._element.classList.remove(CLASS_NAME_SHOW);
246
+ this._element.setAttribute('aria-hidden', 'true');
241
247
  const remainingOpenSidebars = VGSidebar.getOpenSidebars(this._element);
242
248
  if (!remainingOpenSidebars.length) {
243
249
  document.body.classList.remove(CLASS_NAME_OPEN);
@@ -289,12 +295,16 @@ class VGSidebar extends BaseModule {
289
295
  * @override
290
296
  */
291
297
  dispose() {
292
- super.dispose();
298
+ if (!this._element) return;
299
+ EventHandler.off(document, EVENT_KEYS.KEYDOWN_DISMISS, this._keydownHandler);
293
300
  EventHandler.off(this._element, EVENT_KEYS.HIDE);
294
301
  if (this._showPopstateHandler) {
295
302
  EventHandler.off(window, EVENT_KEYS.POPSTATE_DATA_API, this._showPopstateHandler);
296
303
  }
297
- this._scrollBar.reset();
304
+ if (!VGSidebar.getOpenSidebars(this._element).length && !Backdrop.isActive()) {
305
+ this._scrollBar.reset();
306
+ }
307
+ super.dispose();
298
308
  }
299
309
 
300
310
  /**
@@ -311,15 +321,16 @@ class VGSidebar extends BaseModule {
311
321
  * @private
312
322
  */
313
323
  _addEventListeners() {
314
- EventHandler.on(document, EVENT_KEYS.KEYDOWN_DISMISS, (event) => {
315
- if (event.key !== 'Escape') return;
324
+ this._keydownHandler = (event) => {
325
+ if (event.key !== 'Escape' || !this._isShown()) return;
316
326
 
317
327
  if (this._params.keyboard) {
318
328
  this.hide();
319
329
  } else {
320
330
  EventHandler.trigger(this._element, EVENT_KEYS.HIDE_PREVENTED);
321
331
  }
322
- });
332
+ };
333
+ EventHandler.on(document, EVENT_KEYS.KEYDOWN_DISMISS, this._keydownHandler);
323
334
  }
324
335
  }
325
336