vgapp 1.5.6 → 1.5.7

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,7 +3,8 @@
3
3
  * Возможности: modal, overlay и dropdown-рендер, компактный размер, Data API, AJAX и Promise-сценарии.
4
4
  */
5
5
  import BaseModule from "../../base-module";
6
- import VGModal from "../../vgmodal";
6
+ import VGModal from "../../vgmodal";
7
+ import VGSidebar from "../../vgsidebar";
7
8
  import VGDropdown from "../../vgdropdown";
8
9
 
9
10
  import { execute, isElement, isVisible, makeRandomString, mergeDeepObject, reflow } from "../../../utils/js/functions";
@@ -450,8 +451,10 @@ class VGAlert {
450
451
  };
451
452
  }
452
453
 
453
- const modal = VGModal.getOrCreateInstance(containerWrap);
454
- const container = Selectors.find('.vg-modal-content', modal._element) || containerWrap;
454
+ const parent = containerWrap.classList.contains('vg-sidebar')
455
+ ? VGSidebar.getOrCreateInstance(containerWrap)
456
+ : VGModal.getOrCreateInstance(containerWrap);
457
+ const container = Selectors.find('.vg-modal-content', containerWrap) || containerWrap;
455
458
 
456
459
  const overlay = document.createElement('div');
457
460
 
@@ -463,7 +466,7 @@ class VGAlert {
463
466
 
464
467
  return {
465
468
  element: overlay,
466
- render: modal,
469
+ render: parent,
467
470
  };
468
471
  }
469
472
 
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Описание: базовая логика выбора, валидации и отображения файлов VGFiles.
3
- * Возможности: управляет набором файлов, ошибками, списками, кастомными действиями, статистикой и скрытыми полями формы.
3
+ * Возможности: управляет файлами, валидацией, списками, миниатюрами и иконками, действиями, статистикой и скрытыми полями.
4
4
  */
5
5
  import BaseModule from "../../base-module";
6
6
  import {mergeDeepObject} from "../../../utils/js/functions";
@@ -8,7 +8,8 @@ import Html from "../../../utils/js/components/templater";
8
8
  import {lang_messages} from "../../../utils/js/components/lang";
9
9
  import {Classes, Manipulator} from "../../../utils/js/dom/manipulator";
10
10
  import Selectors from "../../../utils/js/dom/selectors";
11
- import {getSVG} from "../../module-fn";
11
+ import {getSVG} from "../../module-fn";
12
+ import {resolveFileIconName} from "../../../utils/js/components/file-icon";
12
13
  import VGFilePreview from "../../vgfilepreview";
13
14
  import {extractAudioMetadata} from "../../../utils/js/components/audio-metadata";
14
15
 
@@ -813,34 +814,28 @@ class VGFilesBase extends BaseModule {
813
814
  }
814
815
  }
815
816
 
816
- _renderUIImage(file) {
817
- const $container = this._tpl.div({ class: 'file-image' });
818
-
819
- const src = file?.src || file?.image;
820
- if (src) {
821
- $container.appendChild(this._tpl.img(src, file.name || '', { class: 'file-preview' }));
822
- return $container;
823
- }
824
-
825
- const customData = this._getFileCustomData(file);
826
- const audioCover = String(customData.audioCover || '').trim();
827
- if (audioCover) {
828
- $container.appendChild(this._tpl.img(audioCover, this._resolveDisplayName(file), { class: 'file-preview' }));
829
- return $container;
830
- }
831
-
832
- if (file?.type && file.type.startsWith('image/')) {
833
- const objectUrl = this._getFileObjectUrl(file);
834
- if (!objectUrl) {
835
- return $container;
836
- }
837
- $container.appendChild(this._tpl.img(objectUrl, file.name, { class: 'file-preview' }));
838
- return $container;
839
- }
840
-
841
- const icon = this._getIconByFileType(file);
842
- $container.appendChild(this._tpl.i({}, icon, { isHTML: true }));
843
- return $container;
817
+ _renderUIImage(file) {
818
+ const $container = this._tpl.div({ class: 'file-image' });
819
+ const renderIcon = () => {
820
+ const icon = this._getIconByFileType(file);
821
+ $container.replaceChildren(this._tpl.i({}, icon, { isHTML: true }));
822
+ };
823
+ const customData = this._getFileCustomData(file);
824
+ let imageSrc = String(file?.image || '').trim() || String(customData.audioCover || '').trim();
825
+
826
+ if (!imageSrc && resolveFileIconName({ type: file?.type, name: file?.name || file?.src }) === 'file-image') {
827
+ imageSrc = String(file?.src || '').trim() || this._getFileObjectUrl(file);
828
+ }
829
+
830
+ if (imageSrc) {
831
+ const image = this._tpl.img(imageSrc, this._resolveDisplayName(file), { class: 'file-preview' });
832
+ image.addEventListener('error', renderIcon, { once: true });
833
+ $container.appendChild(image);
834
+ } else {
835
+ renderIcon();
836
+ }
837
+
838
+ return $container;
844
839
  }
845
840
 
846
841
  _resolveDisplayName(file) {
@@ -1,4 +1,8 @@
1
- import BaseModule from "../../base-module";
1
+ /**
2
+ * Описание: модальные окна VGModal с декларативным и программным управлением.
3
+ * Возможности: AJAX, backdrop, геометрия окна, сворачивание и синхронизация доступности.
4
+ */
5
+ import BaseModule from "../../base-module";
2
6
  import ScrollBarHelper from "../../../utils/js/components/scrollbar";
3
7
  import Backdrop from "../../../utils/js/components/backdrop";
4
8
  import Selectors from "../../../utils/js/dom/selectors";
@@ -346,7 +350,8 @@ class VGModal extends BaseModule {
346
350
  this._saveInteractionState();
347
351
  this._disableInteractionHandlers();
348
352
  this._minimized.reset();
349
- this._element.style.display = 'none';
353
+ this._element.style.display = 'none';
354
+ this._element.setAttribute('aria-hidden', 'true');
350
355
  this._element.removeAttribute('aria-modal');
351
356
  this._element.removeAttribute('role');
352
357
  this._isTransitioning = false;
@@ -395,7 +400,8 @@ class VGModal extends BaseModule {
395
400
  document.body.append(this._element);
396
401
  }
397
402
 
398
- this._element.style.display = 'block';
403
+ this._element.style.display = 'block';
404
+ this._element.removeAttribute('aria-hidden');
399
405
  this._element.setAttribute('aria-modal', true);
400
406
  this._element.setAttribute('role', 'dialog');
401
407
  this._element.scrollTop = 0;
@@ -1,8 +1,11 @@
1
- import VGSelect from "./vgselect";
1
+ /**
2
+ * Описание: делегированные обработчики интерфейса VGSelect.
3
+ * Возможности: открытие списка, выбор опций, теги, клавиатура и сброс формы.
4
+ */
5
+ import VGSelect from "./vgselect";
2
6
  import EventHandler from "../../../utils/js/dom/event";
3
7
  import Selectors from "../../../utils/js/dom/selectors";
4
8
  import {Manipulator} from "../../../utils/js/dom/manipulator";
5
- import {transliterate} from "../../../utils/js/functions";
6
9
 
7
10
  const NAME_KEY = 'vg.select';
8
11
 
@@ -100,56 +103,6 @@ const _handlersVGSelect = () => {
100
103
  }
101
104
  });
102
105
 
103
- EventHandler.on(document, EVENT_KEY_UP_DATA_API, SELECTOR_SEARCH_TOGGLE, function(e) {
104
- const input = e.target;
105
- const dropdown = input.closest(SELECTOR_DROPDOWN);
106
- const list = dropdown?.querySelector(`.${CLASS_NAME_LIST}`);
107
- if (!list) return;
108
-
109
- const container = input.closest(`.${CLASS_NAME_CONTAINER}`);
110
- const instance = VGSelect.getInstance(container);
111
-
112
- const options = list.querySelectorAll(`.${CLASS_NAME_OPTION}`);
113
- const groups = list.querySelectorAll(`.${CLASS_NAME_OPTGROUP}`);
114
- const value = input.value.trim().toLowerCase();
115
- const search = [value, transliterate(value), transliterate(value, true)];
116
-
117
- options.forEach(el => { el.hidden = false; el.style.display = ''; });
118
- groups.forEach(el => { el.hidden = false; el.style.display = ''; });
119
-
120
- if (value) {
121
- let visibleCount = 0;
122
- groups.length ? groups.forEach(group => {
123
- const items = group.querySelectorAll(`.${CLASS_NAME_OPTION}`);
124
- const visible = Array.from(items).some(item => {
125
- const t = item.textContent.toLowerCase();
126
- return search.some(s => t.includes(s));
127
- });
128
- group.hidden = !visible;
129
- group.style.display = visible ? '' : 'none';
130
- items.forEach(item => {
131
- const t = item.textContent.toLowerCase();
132
- const match = search.some(s => t.includes(s));
133
- item.hidden = !match;
134
- item.style.display = match ? '' : 'none';
135
- if (match) visibleCount++;
136
- });
137
- }) : options.forEach(option => {
138
- const t = option.textContent.toLowerCase();
139
- const match = search.some(s => t.includes(s));
140
- option.hidden = !match;
141
- option.style.display = match ? '' : 'none';
142
- if (match) visibleCount++;
143
- });
144
-
145
- instance?._triggerEvent(EVENT_KEY_SEARCH, { query: value, results: visibleCount });
146
- instance?._callCallback('onSearch', { query: value, results: visibleCount });
147
- } else {
148
- instance?._triggerEvent(EVENT_KEY_SEARCH, { query: '', results: options.length });
149
- instance?._callCallback('onSearch', { query: '', results: options.length });
150
- }
151
- });
152
-
153
106
  EventHandler.on(document, EVENT_RESET_DATA_API, 'form', function() {
154
107
  Selectors.findAll('select[data-inited="true"]', this).forEach(select => VGSelect.build(select, true));
155
108
  });
@@ -236,4 +189,4 @@ const _handlersVGSelect = () => {
236
189
  });
237
190
  }
238
191
 
239
- export default _handlersVGSelect;
192
+ export default _handlersVGSelect;
@@ -1,9 +1,14 @@
1
- import BaseModule from "../../base-module";
1
+ /**
2
+ * Описание: кастомный select с синхронизацией исходного поля и выпадающего списка.
3
+ * Возможности: поиск, группы, дерево, теги, AJAX-пагинация, события и освобождение ресурсов.
4
+ */
5
+ import BaseModule from "../../base-module";
2
6
  import {
3
7
  isDisabled,
4
8
  isEmptyObj,
5
9
  mergeDeepObject,
6
- normalizeData,
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";
@@ -73,10 +78,10 @@ class VGSelect extends BaseModule {
73
78
  autosearch: true,
74
79
  // Dropdown placement behavior:
75
80
  // - none: default CSS positioning (no JS)
76
- // - auto: choose top/bottom based on available space in overflow ancestor/viewport
81
+ // - auto (default): choose top/bottom based on available space in overflow ancestor/viewport
77
82
  // - top: force open upwards
78
83
  // - bottom: force open downwards
79
- position: 'none',
84
+ position: 'auto',
80
85
  search: {
81
86
  enabled: false,
82
87
  route: '',
@@ -104,7 +109,9 @@ class VGSelect extends BaseModule {
104
109
  }, params));
105
110
 
106
111
  this._observer = null;
107
- this._observerTimeout = null;
112
+ this._observerTimeout = null;
113
+ this._searchTimeout = null;
114
+ this._disposed = false;
108
115
  this._visibilityTransitionId = 0;
109
116
  this._drop = Selectors.find(SELECTOR_DROPDOWN, this._element);
110
117
  this._searchTerm = '';
@@ -152,8 +159,9 @@ class VGSelect extends BaseModule {
152
159
  * @param {HTMLElement} drop - Контейнер выпадающего списка
153
160
  * @returns {HTMLElement} - Обновлённый список
154
161
  */
155
- static buildListOptions(selector, drop, params = {}) {
156
- let list = drop.querySelector(`.${CLASS_NAME_LIST}`);
162
+ static buildListOptions(selector, drop, params = {}) {
163
+ let list = drop.querySelector(`.${CLASS_NAME_LIST}`);
164
+ const controls = list ? [...list.querySelectorAll(`.${CLASS_NAME_LOAD_MORE}, .${CLASS_NAME_LOADING}`)] : [];
157
165
  if (!list) {
158
166
  list = document.createElement('ul');
159
167
  Classes.add(list, CLASS_NAME_LIST);
@@ -190,7 +198,8 @@ class VGSelect extends BaseModule {
190
198
  });
191
199
  }
192
200
 
193
- return list;
201
+ controls.forEach(control => list.appendChild(control));
202
+ return list;
194
203
  }
195
204
 
196
205
  /**
@@ -459,17 +468,16 @@ class VGSelect extends BaseModule {
459
468
  search.appendChild(searchInput);
460
469
  this._drop.insertBefore(search, this._drop.firstChild);
461
470
 
462
- let searchTimeout;
463
- searchInput.addEventListener('input', (e) => {
471
+ searchInput.addEventListener('input', (e) => {
464
472
  const term = e.target.value.trim();
465
473
  const params = this._params;
466
474
 
467
475
  this._callCallback('onSearch', { term });
468
476
  if (params.search.remote && params.search.route) {
469
- if (term.length < (params.search.minterm || 1)) return;
470
-
471
- clearTimeout(searchTimeout);
472
- searchTimeout = setTimeout(() => {
477
+ clearTimeout(this._searchTimeout);
478
+ this._remoteSearchAbortController?.abort();
479
+ if (term.length < (params.search.minterm || 1)) return;
480
+ this._searchTimeout = setTimeout(() => {
473
481
  this._fetchRemoteData(term);
474
482
  }, params.search.delay || 300);
475
483
  } else {
@@ -520,7 +528,7 @@ class VGSelect extends BaseModule {
520
528
  if (searchInput) searchInput.focus();
521
529
 
522
530
  return this._queueCallback(() => {
523
- if (transitionId !== this._visibilityTransitionId || !this._isShown()) return;
531
+ if (this._disposed || transitionId !== this._visibilityTransitionId || !this._isShown()) return;
524
532
  this._updateDropdownPlacement();
525
533
  EventHandler.trigger(this._element, EVENT_KEY_SHOWN, { relatedTarget });
526
534
  this._triggerEvent(EVENT_KEY_OPEN);
@@ -552,7 +560,7 @@ class VGSelect extends BaseModule {
552
560
  this._element.querySelector(SELECTOR_DATA_TOGGLE).setAttribute('aria-expanded', 'false');
553
561
 
554
562
  this._queueCallback(() => {
555
- if (transitionId !== this._visibilityTransitionId) return;
563
+ if (this._disposed || transitionId !== this._visibilityTransitionId) return;
556
564
  this._element.classList.remove(CLASS_NAME_SHOW);
557
565
  EventHandler.trigger(this._element, EVENT_KEY_HIDDEN, relatedTarget);
558
566
  this._triggerEvent(EVENT_KEY_CLOSE);
@@ -646,16 +654,21 @@ class VGSelect extends BaseModule {
646
654
  /**
647
655
  * Освобождает ресурсы (отключает observer, очищает таймеры)
648
656
  */
649
- dispose() {
657
+ dispose() {
658
+ if (this._disposed) return;
650
659
  this._visibilityTransitionId++;
651
660
  if (this._observer) {
652
661
  this._observer.disconnect();
653
662
  this._observer = null;
654
663
  }
655
664
  clearTimeout(this._observerTimeout);
656
- this._observerTimeout = null;
657
- this._teardownDropdownPlacement();
658
- super.dispose();
665
+ this._observerTimeout = null;
666
+ clearTimeout(this._searchTimeout);
667
+ this._remoteSearchAbortController?.abort();
668
+ if (this._isShown() && 'ontouchstart' in document.documentElement) document.body.style.pointerEvents = '';
669
+ this._teardownDropdownPlacement();
670
+ super.dispose();
671
+ this._disposed = true;
659
672
  }
660
673
 
661
674
  _setupDropdownPlacement() {
@@ -691,9 +704,9 @@ class VGSelect extends BaseModule {
691
704
 
692
705
  _getPositionMode() {
693
706
  const raw = this._params?.position;
694
- const mode = raw == null ? 'none' : String(raw).trim().toLowerCase();
707
+ const mode = raw == null ? 'auto' : String(raw).trim().toLowerCase();
695
708
  if (mode === 'auto' || mode === 'top' || mode === 'bottom' || mode === 'none') return mode;
696
- return 'none';
709
+ return 'auto';
697
710
  }
698
711
 
699
712
  _getOverflowAncestor(startEl) {
@@ -764,23 +777,28 @@ class VGSelect extends BaseModule {
764
777
  */
765
778
  static destroy(select) {
766
779
  const container = select.nextElementSibling;
767
- if (container && container.classList.contains(CLASS_NAME_CONTAINER)) {
768
- container.remove();
769
- }
780
+ if (container && container.classList.contains(CLASS_NAME_CONTAINER)) {
781
+ VGSelect.getInstance(container)?.dispose();
782
+ container.remove();
783
+ }
784
+ delete select.dataset.inited;
770
785
  }
771
786
 
772
787
  /**
773
788
  * Обновляет отображаемое значение (текст, теги)
774
789
  * @param {HTMLSelectElement} select - Исходный <select>
775
790
  */
776
- static updateUI(select) {
791
+ static updateUI(select) {
777
792
  const container = select.nextElementSibling;
778
793
  if (!container || !container.classList.contains(CLASS_NAME_CONTAINER)) return;
779
794
 
780
795
  const current = container.querySelector(SELECTOR_CURRENT);
781
796
  const placeholder = select.dataset.placeholder || '';
782
797
  const isMultiple = select.multiple;
783
- const instance = VGSelect.getInstance(container);
798
+ const instance = VGSelect.getInstance(container);
799
+ container.querySelectorAll(`.${CLASS_NAME_OPTION}`).forEach(item => {
800
+ item.classList.toggle('selected', !!select.options[Number(item.dataset.index)]?.selected);
801
+ });
784
802
 
785
803
  if (isMultiple) {
786
804
  const tags = current.querySelector(`.${CLASS_NAME_TAGS}`);
@@ -797,8 +815,12 @@ class VGSelect extends BaseModule {
797
815
  instance?._callCallback('onClear');
798
816
  }
799
817
 
800
- if (selected.length === 0) {
801
- input.placeholder = placeholder;
818
+ if (selected.length === 0) {
819
+ input.placeholder = placeholder;
820
+ const label = document.createElement('span');
821
+ label.className = CLASS_NAME_PLACEHOLDER;
822
+ label.textContent = placeholder;
823
+ tags.insertBefore(label, input);
802
824
  } else {
803
825
  input.placeholder = '';
804
826
  selected.forEach(opt => {
@@ -983,15 +1005,16 @@ class VGSelect extends BaseModule {
983
1005
  _initLoadMoreButton() {
984
1006
  if (!this._params.search?.pagination || !this._params.search.remote) return;
985
1007
 
986
- const list = this._element.querySelector(SELECTOR_LIST);
987
- if (!list) return;
1008
+ const list = this._element.querySelector(SELECTOR_LIST);
1009
+ if (!list) return;
1010
+ if (list.querySelector(SELECTOR_LOAD_MORE_BTN)) return;
988
1011
 
989
1012
  const btn = document.createElement('li');
990
1013
  btn.className = CLASS_NAME_LOAD_MORE;
991
1014
  btn.style.textAlign = 'center';
992
1015
  btn.style.padding = '8px';
993
1016
  btn.style.cursor = 'pointer';
994
- btn.style.color = '#007bff';
1017
+ btn.style.color = 'var(--vg-primary-color, #007bff)';
995
1018
  btn.style.fontSize = '14px';
996
1019
  btn.style.fontWeight = '500';
997
1020
  btn.textContent = this._params.search.loadMoreText;
@@ -1042,9 +1065,10 @@ class VGSelect extends BaseModule {
1042
1065
  * Загружает следующую страницу данных по клику
1043
1066
  * @private
1044
1067
  */
1045
- async _loadNextPage() {
1068
+ async _loadNextPage() {
1046
1069
  const { route, pageParam = 'page', termParam = 'q', perpage = 20 } = this._params.search;
1047
- const nextPage = this._currentPage + 1;
1070
+ const nextPage = this._currentPage + 1;
1071
+ const requestId = this._remoteSearchRequestId;
1048
1072
 
1049
1073
  const url = new URL(route, window.location.origin);
1050
1074
  url.searchParams.set(termParam, this._searchTerm);
@@ -1055,9 +1079,11 @@ class VGSelect extends BaseModule {
1055
1079
  this._showLoading(true);
1056
1080
  this._hideLoadMoreButton(true);
1057
1081
 
1058
- try {
1059
- const res = await fetch(url, { headers: { 'Content-Type': 'application/json' } });
1060
- const data = await res.json();
1082
+ try {
1083
+ const res = await fetch(url, { headers: { 'Content-Type': 'application/json' } });
1084
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
1085
+ const data = await res.json();
1086
+ if (this._disposed || requestId !== this._remoteSearchRequestId) return;
1061
1087
 
1062
1088
  if (Array.isArray(data.results)) {
1063
1089
  VGSelect.addOptions(this._element.previousElementSibling, data, { preserve: true });
@@ -1072,13 +1098,16 @@ class VGSelect extends BaseModule {
1072
1098
  this._callCallback('onLoadNext', { page: this._currentPage, data });
1073
1099
  this._triggerEvent(EVENT_KEY_LOAD_NEXT, { page: this._currentPage, term: this._searchTerm });
1074
1100
  }
1075
- } catch (err) {
1101
+ } catch (err) {
1102
+ if (this._disposed || requestId !== this._remoteSearchRequestId) return;
1076
1103
  console.error('VGSelect: Failed to load next page', err);
1077
1104
  this._triggerEvent(EVENT_KEY_ERROR, { error: 'Pagination fetch failed', term: this._searchTerm });
1078
1105
  this._hideLoadMoreButton(false); // оставить кнопку при ошибке
1079
- } finally {
1080
- this._showLoading(false);
1081
- this._loading = false;
1106
+ } finally {
1107
+ if (!this._disposed && requestId === this._remoteSearchRequestId) {
1108
+ this._showLoading(false);
1109
+ this._loading = false;
1110
+ }
1082
1111
  }
1083
1112
  }
1084
1113
 
@@ -1100,7 +1129,8 @@ class VGSelect extends BaseModule {
1100
1129
  // Обновляем текущее "желательное" состояние
1101
1130
  this._searchTerm = term;
1102
1131
  this._currentPage = 1;
1103
- this._totalPages = null;
1132
+ this._totalPages = null;
1133
+ this._loading = false;
1104
1134
 
1105
1135
  // Отменяем предыдущий запрос (если ещё летит)
1106
1136
  if (this._remoteSearchAbortController) {
@@ -1120,10 +1150,14 @@ class VGSelect extends BaseModule {
1120
1150
  headers: { 'Content-Type': 'application/json' },
1121
1151
  signal
1122
1152
  })
1123
- .then(res => res.json())
1153
+ .then(res => {
1154
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
1155
+ return res.json();
1156
+ })
1124
1157
  .then(data => {
1125
1158
  // Если прилетел не самый свежий ответ — игнорируем
1126
- if (requestId !== this._remoteSearchRequestId) return;
1159
+ if (this._disposed || requestId !== this._remoteSearchRequestId) return;
1160
+ if (!Array.isArray(data.results)) throw new Error('Invalid results');
1127
1161
 
1128
1162
  // Если пользователь уже ввёл другой текст — тоже игнорируем
1129
1163
  const liveTerm = searchInput ? searchInput.value.trim() : '';
@@ -1172,7 +1206,8 @@ class VGSelect extends BaseModule {
1172
1206
  this._element.classList.add(CLASS_NAME_SHOW, CLASS_NAME_ACTIVE);
1173
1207
  }
1174
1208
  })
1175
- .catch(err => {
1209
+ .catch(err => {
1210
+ if (this._disposed || requestId !== this._remoteSearchRequestId) return;
1176
1211
  // Abort — нормальная ситуация при быстром вводе
1177
1212
  if (err && (err.name === 'AbortError')) return;
1178
1213
 
@@ -1182,7 +1217,7 @@ class VGSelect extends BaseModule {
1182
1217
  })
1183
1218
  .finally(() => {
1184
1219
  // Лоадер убираем только для последнего актуального запроса
1185
- if (requestId === this._remoteSearchRequestId) {
1220
+ if (!this._disposed && requestId === this._remoteSearchRequestId) {
1186
1221
  this._showLoading(false);
1187
1222
  }
1188
1223
  });
@@ -1281,8 +1316,9 @@ class VGSelect extends BaseModule {
1281
1316
 
1282
1317
  if (isRebuild) {
1283
1318
  const drop = container.querySelector(`.${CLASS_NAME_DROPDOWN}`);
1284
- VGSelect.buildListOptions(select, drop, instance?._params || {});
1285
- instance?._syncSearch();
1319
+ VGSelect.buildListOptions(select, drop, instance?._params || {});
1320
+ instance?._syncSearch();
1321
+ this.updateUI(select);
1286
1322
  instance?._triggerEvent(EVENT_KEY_REBUILD);
1287
1323
  } else {
1288
1324
  this.updateUI(select);
@@ -1294,21 +1330,29 @@ class VGSelect extends BaseModule {
1294
1330
  * @param {string} term - Поисковый запрос
1295
1331
  * @private
1296
1332
  */
1297
- _filterLocalOptions(term) {
1298
- const list = this._drop.querySelector(`.${CLASS_NAME_LIST}`);
1299
- const options = list.querySelectorAll(`.${CLASS_NAME_OPTION}`);
1300
-
1301
- if (!term) {
1302
- options.forEach(el => el.hidden = false);
1303
- return;
1304
- }
1305
-
1306
- term = term.toLowerCase();
1307
- options.forEach(el => {
1308
- const text = el.textContent.toLowerCase();
1309
- el.hidden = !text.includes(term);
1310
- });
1311
- }
1333
+ _filterLocalOptions(term) {
1334
+ const list = this._drop.querySelector(`.${CLASS_NAME_LIST}`);
1335
+ const options = list.querySelectorAll(`.${CLASS_NAME_OPTION}`);
1336
+ term = term.toLowerCase();
1337
+ const search = [term, transliterate(term), transliterate(term, true)];
1338
+ let results = 0;
1339
+ options.forEach(el => {
1340
+ const text = el.textContent.toLowerCase();
1341
+ const option = this._element.previousElementSibling.options[Number(el.dataset.index)];
1342
+ const empty = option?.value === '' && text.trim() === '';
1343
+ const visible = !empty && (!term || search.some(value => text.includes(value)));
1344
+ el.hidden = !visible;
1345
+ el.style.display = visible ? '' : 'none';
1346
+ if (visible) results++;
1347
+ });
1348
+ list.querySelectorAll(`.${CLASS_NAME_OPTGROUP}`).forEach(group => {
1349
+ const visible = [...group.querySelectorAll(`.${CLASS_NAME_OPTION}`)].some(option => !option.hidden);
1350
+ group.hidden = !visible;
1351
+ group.style.display = visible ? '' : 'none';
1352
+ });
1353
+ this._triggerEvent(`${NAME_KEY}.search`, {query: term, results});
1354
+ this._callCallback('onSearch', {query: term, results});
1355
+ }
1312
1356
  }
1313
1357
 
1314
1358
  _handlersVGSelect();
@@ -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,4 +1,8 @@
1
- import BaseModule from "../../base-module";
1
+ /**
2
+ * Описание: боковые панели VGSidebar с декларативным и программным управлением.
3
+ * Возможности: четыре стороны экрана, backdrop, прокрутка, URL-хэш, AJAX, события и доступность.
4
+ */
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";
4
8
  import { dismissTrigger } from "../../module-fn";
@@ -211,7 +215,8 @@ class VGSidebar extends BaseModule {
211
215
  EventHandler.on(window, EVENT_KEYS.POPSTATE_DATA_API, this._showPopstateHandler);
212
216
  }
213
217
 
214
- this._element.classList.add(CLASS_NAME_SHOW);
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 = () => {
@@ -237,7 +242,8 @@ class VGSidebar extends BaseModule {
237
242
  const hideEvent = EventHandler.trigger(this._element, EVENT_KEYS.HIDE);
238
243
  if (hideEvent.defaultPrevented) return;
239
244
 
240
- this._element.classList.remove(CLASS_NAME_SHOW);
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);
@@ -288,13 +294,17 @@ class VGSidebar extends BaseModule {
288
294
  * Очищает ресурсы модуля.
289
295
  * @override
290
296
  */
291
- dispose() {
292
- super.dispose();
293
- EventHandler.off(this._element, EVENT_KEYS.HIDE);
297
+ dispose() {
298
+ if (!this._element) return;
299
+ EventHandler.off(document, EVENT_KEYS.KEYDOWN_DISMISS, this._keydownHandler);
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