vgapp 1.5.5 → 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.
@@ -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
 
@@ -1,5 +1,9 @@
1
- import BaseModule from "../../base-module";
2
- import { mergeDeepObject, getElement, isDisabled, isVisible } from "../../../utils/js/functions";
1
+ /**
2
+ * Описание: отслеживание активных секций и навигация по якорям.
3
+ * Возможности: Data API, нативная и виртуальная прокрутка, вложенные меню и обновление секций.
4
+ */
5
+ import BaseModule from "../../base-module";
6
+ import { getElement, isDisabled, isVisible, normalizeData } from "../../../utils/js/functions";
3
7
  import EventHandler from "../../../utils/js/dom/event";
4
8
  import Selectors from "../../../utils/js/dom/selectors";
5
9
 
@@ -52,7 +56,7 @@ class VGSpy extends BaseModule {
52
56
  * @property {number[]|string} threshold - пороги видимости (0.1, 0.5, 1)
53
57
  */
54
58
  this._params = this._configAfterMerge(
55
- mergeDeepObject(
59
+ Object.assign(
56
60
  {
57
61
  offset: null, // Устаревшее, для обратной совместимости
58
62
  rootMargin: '0px 0px -25%',
@@ -60,7 +64,8 @@ class VGSpy extends BaseModule {
60
64
  target: this._element,
61
65
  threshold: [0.1, 0.5, 1],
62
66
  },
63
- params
67
+ params,
68
+ this._getDataOptions()
64
69
  )
65
70
  );
66
71
 
@@ -130,15 +135,27 @@ class VGSpy extends BaseModule {
130
135
  * Ключ модуля (для хранения в data)
131
136
  * @returns {string}
132
137
  */
133
- static get NAME_KEY() {
134
- return NAME_KEY;
135
- }
138
+ static get NAME_KEY() {
139
+ return NAME_KEY;
140
+ }
141
+
142
+ _getDataOptions() {
143
+ const options = {};
144
+ for (const name of ['target', 'offset', 'rootMargin', 'smoothScroll', 'threshold']) {
145
+ const attribute = `data-${name.replace(/[A-Z]/g, letter => `-${letter.toLowerCase()}`)}`;
146
+ if (this._element.hasAttribute(attribute)) {
147
+ options[name] = normalizeData(this._element.getAttribute(attribute));
148
+ }
149
+ }
150
+ return options;
151
+ }
136
152
 
137
153
  /**
138
154
  * Инициализирует или перезапускает модуль: находит ссылки и секции, создаёт observer
139
155
  */
140
- refresh() {
141
- this._initializeTargetsAndObservables();
156
+ refresh() {
157
+ this._process(null);
158
+ this._initializeTargetsAndObservables();
142
159
  this._updateRootElement();
143
160
  this._maybeEnableSmoothScroll();
144
161
 
@@ -163,8 +180,8 @@ class VGSpy extends BaseModule {
163
180
 
164
181
  // Smooth Scrollbar can be initialized after the spy; retry once on the next tick.
165
182
  if (this._rootElement && !this._isScrollableSelf(this._rootElement) && window.Scrollbar) {
166
- setTimeout(() => {
167
- if (this._scrollbar) return;
183
+ setTimeout(() => {
184
+ if (!this._element || this._scrollbar) return;
168
185
  this._updateSmoothScrollbar();
169
186
  if (this._scrollbar) this._setupScrollbarTracking();
170
187
  }, 0);
@@ -356,7 +373,9 @@ class VGSpy extends BaseModule {
356
373
  return window.scrollY || document.documentElement.scrollTop || 0;
357
374
  }
358
375
 
359
- dispose() {
376
+ dispose() {
377
+ if (!this._element) return;
378
+ this._process(null);
360
379
  if (this._observer) {
361
380
  this._observer.disconnect();
362
381
  }
@@ -415,8 +434,9 @@ class VGSpy extends BaseModule {
415
434
  // If the target section is currently hidden (tabs/collapses/lazy layout),
416
435
  // don't hijack the click; allow UI to reveal it, then retry.
417
436
  if (!isVisible(section)) {
418
- setTimeout(() => {
419
- const revealed = Selectors.findID(id);
437
+ setTimeout(() => {
438
+ if (!this._element) return;
439
+ const revealed = Selectors.findID(id);
420
440
  if (!revealed || !isVisible(revealed)) return;
421
441
 
422
442
  this._targetLinks.set(revealed.id, link);
@@ -505,10 +525,10 @@ class VGSpy extends BaseModule {
505
525
  const userScrollsDown = parentScrollTop >= this._previousScrollData.parentScrollTop;
506
526
  this._previousScrollData.parentScrollTop = parentScrollTop;
507
527
 
508
- for (const entry of entries) {
509
- if (!entry.isIntersecting) {
510
- this._clearActiveClass(getTargetLink(entry));
511
- continue;
528
+ for (const entry of entries) {
529
+ if (!entry.isIntersecting) {
530
+ if (getTargetLink(entry) === this._activeTarget) this._process(null);
531
+ continue;
512
532
  }
513
533
 
514
534
  const entryTop = this._getSectionTop(entry.target);
@@ -516,7 +536,7 @@ class VGSpy extends BaseModule {
516
536
  const shouldActivate =
517
537
  (userScrollsDown && isEntryBelow) || (!userScrollsDown && !isEntryBelow);
518
538
 
519
- if (shouldActivate) {
539
+ if (shouldActivate || !this._activeTarget) {
520
540
  this._previousScrollData.visibleEntryTop = entryTop;
521
541
  this._process(getTargetLink(entry));
522
542
  }
@@ -595,7 +615,7 @@ class VGSpy extends BaseModule {
595
615
  parent.classList.remove(CLASS_NAME_ACTIVE);
596
616
 
597
617
  const activeLinks = Selectors.findAll(
598
- `[href].${CLASS_NAME_ACTIVE}, [data-vg-target].${CLASS_NAME_ACTIVE}`,
618
+ `[href].${CLASS_NAME_ACTIVE}, [data-vg-target].${CLASS_NAME_ACTIVE}, ${SELECTOR_DROPDOWN_TOGGLE}.${CLASS_NAME_ACTIVE}`,
599
619
  parent
600
620
  );
601
621
  for (const link of activeLinks) {
@@ -603,8 +623,9 @@ class VGSpy extends BaseModule {
603
623
  }
604
624
  }
605
625
 
606
- _getTargetIdFromTrigger(trigger) {
607
- if (!trigger || typeof trigger.getAttribute !== 'function') return null;
626
+ _getTargetIdFromTrigger(trigger) {
627
+ if (!trigger || typeof trigger.getAttribute !== 'function') return null;
628
+ if (isDisabled(trigger) || trigger.matches(SELECTOR_DROPDOWN_TOGGLE)) return null;
608
629
 
609
630
  const dataTarget = (trigger.getAttribute('data-vg-target') || '').trim();
610
631
  const href = (trigger.getAttribute('href') || '').trim();
@@ -1,13 +1,13 @@
1
1
  /**
2
2
  * Описание: константы и параметры по умолчанию базовой таблицы VGTable.
3
- * Возможности: единая настройка Data API, i18n, wrapper, remote-запросов, фильтров, sticky-заголовка, колонок, строк, сортировки, выбора и пагинации.
4
- */
3
+ * Возможности: единая настройка Data API, i18n, wrapper, remote, фильтров, sticky, колонок, строк, выбора и адаптивной пагинации.
4
+ */
5
5
 
6
6
  /**
7
7
  * Служебные константы модуля.
8
8
  */
9
9
  // Имя компонента в публичном API. Варианты: фиксированное значение 'table'.
10
- const NAME = 'table';
10
+ const NAME = 'table';
11
11
  // Ключ экземпляра в Data API. Варианты: фиксированное значение 'vg.table'.
12
12
  const NAME_KEY = `vg.${NAME}`;
13
13
  // Селектор автоматической инициализации. Варианты: элементы с атрибутом data-vg-table.
@@ -21,7 +21,16 @@ const TABLE_CONTAINER_SELECTOR = '.vg-table-container';
21
21
  // Маркер container, созданного компонентом. Варианты: пустой data-атрибут.
22
22
  const GENERATED_TABLE_CONTAINER_ATTRIBUTE = 'data-vg-table-generated-container';
23
23
 
24
- const DEFAULT_OPTIONS = {
24
+ const DEFAULT_OPTIONS = {
25
+ /** Профили представления: базовые параметры + все достигнутые границы xs–xxl. */
26
+ responsive: {
27
+ // Включается явно; Data API: data-responsive-enabled.
28
+ enabled: false,
29
+ // Локальные переопределения границ общего Responsive; пустой объект сохраняет глобальные значения.
30
+ breakpoints: {},
31
+ // Профили принимают pagination: maxButtons, align, position, size.enabled/label, quick.enabled.
32
+ xs: {}, sm: {}, md: {}, lg: {}, xl: {}, xxl: {},
33
+ },
25
34
  // Активная локаль встроенного интерфейса. Data API: data-locale. Варианты: 'ru' | 'en' | региональный BCP 47 код.
26
35
  locale: 'ru',
27
36
  // Пользовательские словари, объединяемые со встроенными ru/en. Data API отсутствует; варианты: объект {locale: {group: {key: value}}}.
@@ -359,7 +368,9 @@ const DEFAULT_OPTIONS = {
359
368
  // Задаёт число страниц, после которого разрешены многоточие и quick='auto'. Варианты: целое число >= 1.
360
369
  threshold: 5,
361
370
  // Задаёт максимальное число соседних страниц между первой и последней. Варианты: целое число >= 1.
362
- visible: 5,
371
+ visible: 5,
372
+ // Лимит номеров и многоточий без prev/next; null сохраняет visible/threshold. Data API: data-pagination-max-buttons.
373
+ maxButtons: null,
363
374
 
364
375
  /** Поле и VGDropdown для выбора количества строк. */
365
376
  size: {