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,6 +1,6 @@
1
1
  /**
2
2
  * Описание: локальная и remote-пагинация строк базовой таблицы VGTable.
3
- * Возможности: выбор страницы и размера, серверная meta, многоточия, быстрый переход, сохранение, Fixed Header container и публичные события.
3
+ * Возможности: выбор страницы и размера, серверная meta, лимит кнопок, responsive-представление без запросов, многоточия, быстрый переход и сохранение.
4
4
  */
5
5
  import EventHandler from "../../../utils/js/dom/event";
6
6
  import VGDropdown from "../../vgdropdown";
@@ -16,7 +16,10 @@ class _pagination {
16
16
  this._options = options;
17
17
  this._host = table.closest('.vg-table-wrapper') || table.parentElement;
18
18
  this._containers = [];
19
- this._dropdowns = [];
19
+ this._dropdowns = [];
20
+ this._externalVisibility = new Map();
21
+ this._manageExternalVisibility = options.responsive === true;
22
+ this._updatingPresentation = false;
20
23
  this._page = this._positiveInt(options.page, 1);
21
24
  this._perPage = this._clampPerPage(options.per);
22
25
  this._remote = options.remote === true;
@@ -29,20 +32,24 @@ class _pagination {
29
32
  this._boundFocusOut = this._handleFocusOut.bind(this);
30
33
  }
31
34
 
32
- init() {
33
- this._restoreState();
34
- this._containers = this._resolveContainers();
35
- this._containers.forEach((container) => {
35
+ init() {
36
+ this._restoreState();
37
+ this._containers = this._resolveContainers();
38
+ this._bindContainers();
39
+ this.refresh();
40
+ }
41
+
42
+ _bindContainers() {
43
+ this._containers.forEach((container) => {
36
44
  container.addEventListener('click', this._boundClick);
37
45
  container.addEventListener('change', this._boundChange);
38
46
  container.addEventListener('keydown', this._boundKeydown);
39
47
  container.addEventListener('focusin', this._boundFocusIn);
40
48
  container.addEventListener('focusout', this._boundFocusOut);
41
49
  });
42
- this.refresh();
43
- }
44
-
45
- dispose() {
50
+ }
51
+
52
+ _releaseContainers() {
46
53
  this._disposeDropdowns();
47
54
  this._containers.forEach((container) => {
48
55
  container.removeEventListener('click', this._boundClick);
@@ -50,15 +57,69 @@ class _pagination {
50
57
  container.removeEventListener('keydown', this._boundKeydown);
51
58
  container.removeEventListener('focusin', this._boundFocusIn);
52
59
  container.removeEventListener('focusout', this._boundFocusOut);
53
- if (container.hasAttribute(GENERATED_ATTRIBUTE)) container.remove();
54
- });
60
+ if (container.hasAttribute(GENERATED_ATTRIBUTE)) container.remove();
61
+ });
62
+ this._containers = [];
63
+ }
64
+
65
+ dispose() {
66
+ this._releaseContainers();
67
+ this._externalVisibility.forEach((hidden, container) => { container.hidden = hidden; });
68
+ this._externalVisibility.clear();
55
69
  this._rows().forEach((row) => {
56
70
  row.hidden = row.getAttribute('data-vg-table-expand-hidden') === 'true'
57
71
  || row.hasAttribute('data-vg-table-filter-hidden');
58
72
  row.removeAttribute('data-vg-table-page-row');
59
73
  });
60
- this._containers = [];
61
- }
74
+ this._containers = [];
75
+ }
76
+
77
+ /** Обновляет только панели: не трогает строки, page/per, storage, scroll или onChange. */
78
+ updatePresentation(options) {
79
+ const focus = this._captureFocus();
80
+ const position = this._position();
81
+ this._updatingPresentation = true;
82
+ try {
83
+ this._options = {...this._options, ...options};
84
+ if (position !== this._position()) {
85
+ this._manageExternalVisibility = true;
86
+ this._releaseContainers();
87
+ this._containers = this._resolveContainers();
88
+ this._bindContainers();
89
+ }
90
+ this._renderControls();
91
+ } finally {
92
+ this._updatingPresentation = false;
93
+ }
94
+ this._restoreFocus(focus);
95
+ }
96
+
97
+ _captureFocus() {
98
+ const active = this._table.ownerDocument.activeElement;
99
+ const container = this._containers.find((item) => item.contains(active));
100
+ if (!container) return null;
101
+ const attribute = ['data-pagination-page', 'data-pagination-per-page', 'data-pagination-quick-input', 'data-pagination-quick-button']
102
+ .find((name) => active.hasAttribute(name));
103
+ return {
104
+ position: this._positionFor(container), attribute, value: attribute ? active.getAttribute(attribute) : null,
105
+ inputValue: active.tagName === 'INPUT' ? active.value : null,
106
+ start: active.selectionStart, end: active.selectionEnd,
107
+ };
108
+ }
109
+
110
+ _restoreFocus(focus) {
111
+ if (!focus) return;
112
+ const container = this._containers.find((item) => this._positionFor(item) === focus.position) || this._containers[0];
113
+ if (!container) return;
114
+ const matching = focus.attribute ? Array.from(container.querySelectorAll(`[${focus.attribute}]`))
115
+ .find((item) => !item.disabled && item.getAttribute(focus.attribute) === focus.value) : null;
116
+ const target = matching || container.querySelector('[aria-current="page"]');
117
+ target?.focus({preventScroll: true});
118
+ if (matching && focus.inputValue !== null) {
119
+ matching.value = focus.inputValue;
120
+ if (focus.start !== null && focus.start !== undefined) matching.setSelectionRange(focus.start, focus.end);
121
+ }
122
+ }
62
123
 
63
124
  setPage(page, emit = false, source = 'api') {
64
125
  const nextPage = this._clampPage(page);
@@ -131,7 +192,11 @@ class _pagination {
131
192
  });
132
193
  }
133
194
 
134
- const markup = this._buildMarkup();
195
+ this._renderControls();
196
+ }
197
+
198
+ _renderControls() {
199
+ const markup = this._buildMarkup();
135
200
  this._disposeDropdowns();
136
201
  this._containers.forEach((container) => {
137
202
  container.className = `vg-table-pagination vg-table-pagination--${this._positionFor(container)}`;
@@ -221,8 +286,10 @@ class _pagination {
221
286
  return `<svg class="vg-table-page__ellipsis-chevron vg-table-page__ellipsis-chevron--${direction}" viewBox="0 0 20 20" focusable="false" aria-hidden="true">${paths}</svg>`;
222
287
  }
223
288
 
224
- _buildPages() {
225
- const total = this._totalPages();
289
+ _buildPages() {
290
+ const total = this._totalPages();
291
+ const maxButtons = this._options.maxButtons;
292
+ if (Number.isInteger(maxButtons) && maxButtons >= 3) return this._buildLimitedPages(total, maxButtons);
226
293
  const visible = Math.max(1, this._positiveInt(this._options.visible, 5));
227
294
  const threshold = Math.max(1, this._positiveInt(this._options.threshold, 5));
228
295
  if (this._options.ellipsis === false || total <= threshold || total <= visible + 2) {
@@ -237,8 +304,30 @@ class _pagination {
237
304
  for (let page = start; page <= end; page += 1) pages.push(page);
238
305
  if (end < total - 1) pages.push({direction: 'next'});
239
306
  pages.push(total);
240
- return pages;
241
- }
307
+ return pages;
308
+ }
309
+
310
+ _buildLimitedPages(total, maximum) {
311
+ if (total <= maximum) return this._range(1, total);
312
+ if (maximum < 5 || this._options.ellipsis === false) {
313
+ const start = Math.max(1, Math.min(this._page - Math.floor(maximum / 2), total - maximum + 1));
314
+ return this._range(start, start + maximum - 1);
315
+ }
316
+ // Резервируем края и подбираем наибольшее окно, учитывая оба многоточия в лимите.
317
+ for (let count = maximum - 2; count >= 1; count -= 1) {
318
+ const start = Math.max(2, Math.min(this._page - Math.floor(count / 2), total - count));
319
+ const end = start + count - 1;
320
+ const pages = [1];
321
+ if (start === 3) pages.push(2);
322
+ else if (start > 3) pages.push({direction: 'prev'});
323
+ pages.push(...this._range(start, end));
324
+ if (end === total - 2) pages.push(total - 1);
325
+ else if (end < total - 2) pages.push({direction: 'next'});
326
+ pages.push(total);
327
+ if (pages.length <= maximum) return pages;
328
+ }
329
+ return [this._page];
330
+ }
242
331
 
243
332
  _handleClick(event) {
244
333
  const sizeOption = event.target.closest('[data-pagination-per-page-option]');
@@ -297,7 +386,8 @@ class _pagination {
297
386
  if (input) input.value = String(this._perPage);
298
387
  }
299
388
 
300
- _handleFocusOut(event) {
389
+ _handleFocusOut(event) {
390
+ if (this._updatingPresentation) return;
301
391
  const input = event.target.closest('[data-pagination-per-page]');
302
392
  if (!input) return;
303
393
  const related = event.relatedTarget instanceof Element ? event.relatedTarget : null;
@@ -323,10 +413,14 @@ class _pagination {
323
413
  _resolveContainers() {
324
414
  if (!this._host) return [];
325
415
  const position = this._position();
326
- const existing = Array.from(this._host.querySelectorAll('[data-vg-table-pagination]'));
327
- if (existing.length) {
328
- if (position === 'both') return existing;
329
- return [existing.find((item) => item.getAttribute('data-position') === position) || existing[0]];
416
+ const existing = Array.from(this._host.querySelectorAll('[data-vg-table-pagination]'));
417
+ if (existing.length) {
418
+ const selected = position === 'both' ? existing : [existing.find((item) => item.getAttribute('data-position') === position) || existing[0]];
419
+ if (this._manageExternalVisibility) existing.forEach((container) => {
420
+ if (!this._externalVisibility.has(container)) this._externalVisibility.set(container, container.hidden);
421
+ container.hidden = !selected.includes(container);
422
+ });
423
+ return selected;
330
424
  }
331
425
 
332
426
  const positions = position === 'both' ? ['top', 'bottom'] : [position];
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Описание: адаптер общего Responsive для представления VGTable.
3
+ * Возможности: отбор безопасных параметров пагинации и события таблицы; границы, наследование и подписки делегируются Responsive.
4
+ */
5
+ import EventHandler from "../../../utils/js/dom/event";
6
+ import {Responsive} from "../../../utils/js/components/responsive";
7
+
8
+ const object = (value) => value && typeof value === 'object' && !Array.isArray(value);
9
+
10
+ // Только представление: page/per, callbacks, storage и параметры запросов не принимаются.
11
+ const paginationProfile = (value) => {
12
+ const result = {};
13
+ if (!object(value)) return result;
14
+ if (value.maxButtons === null || (Number.isInteger(value.maxButtons) && value.maxButtons >= 3)) result.maxButtons = value.maxButtons;
15
+ if (['left', 'center', 'right', 'between'].includes(value.align)) result.align = value.align;
16
+ if (['top', 'bottom', 'both'].includes(value.position)) result.position = value.position;
17
+ if (object(value.size)) {
18
+ result.size = {};
19
+ if (typeof value.size.enabled === 'boolean') result.size.enabled = value.size.enabled;
20
+ if (value.size.label === false || typeof value.size.label === 'string') result.size.label = value.size.label;
21
+ }
22
+ if (object(value.quick) && [true, false, 'auto'].includes(value.quick.enabled)) result.quick = {enabled: value.quick.enabled};
23
+ return result;
24
+ };
25
+
26
+ class _responsive {
27
+ constructor(table, options, onChange) {
28
+ this._table = table;
29
+ this._onChange = onChange;
30
+ this._responsive = new Responsive({breakpoints: options.breakpoints, window: table.ownerDocument.defaultView});
31
+ this._profiles = Object.fromEntries(Object.keys(this._responsive.breakpoints).map((name) => [name, paginationProfile(options[name]?.pagination)]));
32
+ this._breakpoint = null;
33
+ this._pagination = {};
34
+ this._width = 0;
35
+ this._valid = this._responsive.isValid();
36
+ }
37
+
38
+ init() {
39
+ if (!this._valid) {
40
+ console.warn('VGTable: responsive.breakpoints must start at xs: 0 and increase through xxl.');
41
+ return this;
42
+ }
43
+ this.refresh();
44
+ this._responsive.subscribe(() => this.refresh());
45
+ return this;
46
+ }
47
+
48
+ refresh(force = false) {
49
+ if (!this._valid || !this._responsive) return this.getState();
50
+ this._width = this._responsive.viewport().width;
51
+ const breakpoint = this._responsive.getBreakpointKey(this._width);
52
+ if (!force && breakpoint === this._breakpoint) return this.getState();
53
+ const previous = this._breakpoint;
54
+ this._breakpoint = breakpoint;
55
+ this._pagination = this._responsive.resolve(this._profiles, {}, this._width);
56
+ this._onChange();
57
+ if (previous !== null && previous !== breakpoint) {
58
+ EventHandler.trigger(this._table, 'responsivechange.vg.table', {...this.getState(), previous});
59
+ }
60
+ return this.getState();
61
+ }
62
+
63
+ getState() {
64
+ return {
65
+ breakpoint: this._breakpoint,
66
+ width: this._responsive?.viewport().width ?? this._width,
67
+ reason: this._valid ? null : 'invalid-breakpoints',
68
+ pagination: {...this._pagination, size: {...this._pagination.size}, quick: {...this._pagination.quick}},
69
+ };
70
+ }
71
+
72
+ dispose() {
73
+ this._width = this._responsive?.viewport().width ?? this._width;
74
+ this._responsive?.dispose();
75
+ this._responsive = null;
76
+ }
77
+ }
78
+
79
+ export default _responsive;
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Описание: основной модуль базовых таблиц VGTable.
3
- * Возможности: i18n, wrapper/container, состояния и URL state, local/remote, sticky, управление колонками и строками, сортировка, дерево, пагинация, выбор и panning.
3
+ * Возможности: i18n, wrapper/container, состояния и URL state, local/remote, sticky, колонки и строки, сортировка, дерево, responsive-пагинация, выбор и panning.
4
4
  */
5
5
  import BaseModule from "../../base-module";
6
6
  import {mergeDeepObject} from "../../../utils/js/functions";
@@ -9,7 +9,8 @@ import Selectors from "../../../utils/js/dom/selectors";
9
9
  import _sorting from "./_sorting.js";
10
10
  import _panning from "./_panning.js";
11
11
  import _selection from "./_selection.js";
12
- import _pagination from "./_pagination.js";
12
+ import _pagination from "./_pagination.js";
13
+ import _responsive from "./_responsive.js";
13
14
  import _expandable from "./_expandable.js";
14
15
  import _stickyHeader from "./_sticky-header.js";
15
16
  import _fixedColumns from "./_fixed-columns.js";
@@ -63,7 +64,8 @@ class VGTable extends BaseModule {
63
64
  this._panning = null;
64
65
  this._selection = null;
65
66
  this._expandable = null;
66
- this._pagination = null;
67
+ this._pagination = null;
68
+ this._responsive = null;
67
69
  this._filters = null;
68
70
  this._search = null;
69
71
  this._skeleton = null;
@@ -187,9 +189,17 @@ class VGTable extends BaseModule {
187
189
  this._expandable.init();
188
190
  }
189
191
 
190
- // Включаем локальную или серверную пагинацию
191
- if (!this._pagination && this._params.pagination.enabled === true) {
192
- this._pagination = new _pagination(this._element, Object.assign({}, this._params.pagination, {
192
+ if (!this._responsive && this._params.responsive?.enabled === true) {
193
+ this._responsive = new _responsive(this._element, this._params.responsive, () => {
194
+ this._pagination?.updatePresentation(this._paginationOptions());
195
+ });
196
+ this._responsive.init();
197
+ }
198
+
199
+ // Включаем локальную или серверную пагинацию
200
+ if (!this._pagination && this._params.pagination.enabled === true) {
201
+ this._pagination = new _pagination(this._element, Object.assign({}, this._paginationOptions(), {
202
+ responsive: this._responsive?.getState().reason === null,
193
203
  remote: this._isRemote,
194
204
  onChange: (state) => this._handlePaginationChange(state),
195
205
  }));
@@ -300,15 +310,27 @@ class VGTable extends BaseModule {
300
310
  this._normalizeColumnsDataOptions();
301
311
  this._normalizeRowReorderDataOptions();
302
312
  if (this._remote) this._remote._options.labels = this._dictionary.remote || {};
303
- this._pagination?.refresh?.();
313
+ this._pagination?.updatePresentation(this._paginationOptions());
304
314
  this._expandable?.refresh?.();
305
315
  this._columns?.refresh?.();
306
316
  this._rowReorder?.refresh?.();
307
317
  const state = this._states?.getState?.();
308
318
  if (state) this._states.render(state.type);
309
319
  EventHandler.trigger(this._element, 'localechange.vg.table', {locale: normalized});
310
- return normalized;
311
- }
320
+ return normalized;
321
+ }
322
+
323
+ /** Текущий брейкпоинт и накопленные переопределения; null при выключенном responsive. */
324
+ getResponsiveState() { return this._responsive?.getState() || null; }
325
+
326
+ /** Принудительно обновляет только адаптивное представление, без загрузки данных. */
327
+ refreshResponsive() { return this._responsive?.refresh(true) || null; }
328
+
329
+ _paginationOptions() {
330
+ const base = this._params.pagination;
331
+ const profile = this._responsive?.getState().pagination || {};
332
+ return {...base, ...profile, size: {...base.size, ...profile.size}, quick: {...base.quick, ...profile.quick}};
333
+ }
312
334
 
313
335
  _mergeLocale(target, source) {
314
336
  if (!target || !source || typeof source !== 'object') return target;
@@ -524,7 +546,11 @@ class VGTable extends BaseModule {
524
546
  assign('ellipsis', (value) => { pagination.ellipsis = boolean(value); });
525
547
  assign('ellipsis-hover', (value) => { pagination.ellipsisHover = boolean(value); });
526
548
  assign('ellipsis-after', (value) => { pagination.threshold = number(value, pagination.threshold); });
527
- assign('max-visible-pages', (value) => { pagination.visible = number(value, pagination.visible); });
549
+ assign('max-visible-pages', (value) => { pagination.visible = number(value, pagination.visible); });
550
+ assign('max-buttons', (value) => {
551
+ const parsed = Number(value);
552
+ pagination.maxButtons = Number.isInteger(parsed) && parsed >= 3 ? parsed : null;
553
+ });
528
554
  assign('show-per-page', (value) => { pagination.size.enabled = boolean(value); });
529
555
  assign('show-per-page-label', (value) => { pagination.size.label = boolean(value) ? 'Строк на странице' : false; });
530
556
  assign('per-page-label', (value) => { pagination.size.label = value; });
@@ -1436,7 +1462,8 @@ class VGTable extends BaseModule {
1436
1462
  /**
1437
1463
  * Очистка ресурсов
1438
1464
  */
1439
- dispose() {
1465
+ dispose() {
1466
+ this._responsive?.dispose();
1440
1467
  if (this._remote) this._remote.dispose();
1441
1468
  if (this._skeleton) this._skeleton.dispose();
1442
1469
  if (this._urlState) this._urlState.dispose();
@@ -1,4 +1,8 @@
1
- import BaseModule from "../../base-module";
1
+ /**
2
+ * Описание: вкладки VGTabs с декларативной и ручной инициализацией.
3
+ * Возможности: клавиатура, начальный hash, AJAX, отменяемые события и адаптивный индикатор.
4
+ */
5
+ import BaseModule from "../../base-module";
2
6
  import Selectors from "../../../utils/js/dom/selectors";
3
7
  import EventHandler from "../../../utils/js/dom/event";
4
8
  import {getNextActiveElement, isDisabled, mergeDeepObject} from "../../../utils/js/functions";
@@ -132,7 +136,7 @@ class VGTabs extends BaseModule {
132
136
  once: true,
133
137
  output: true,
134
138
  },
135
- }, this._params);
139
+ }, params || {});
136
140
 
137
141
  this._parent = this._element.closest(SELECTOR.TAB_PANEL);
138
142
  this._main_parent = this._parent?.closest(SELECTOR.TAB_CLASS) || null;
@@ -173,19 +177,19 @@ class VGTabs extends BaseModule {
173
177
  show() {
174
178
  const innerElem = this._element;
175
179
 
176
- if (this._elemIsActive(innerElem)) return;
180
+ if (!innerElem || isDisabled(innerElem) || this._elemIsActive(innerElem)) return;
177
181
 
178
182
  const activeElem = this._getActiveElem();
179
- const relatedTarget = innerElem;
180
183
 
181
184
  // События hide и show
182
- const hideEvent = activeElem ? EventHandler.trigger(activeElem, EVENT_HIDE, {relatedTarget}) : null;
183
- const showEvent = EventHandler.trigger(innerElem, EVENT_SHOW, {relatedTarget});
185
+ const hideEvent = activeElem ? EventHandler.trigger(activeElem, EVENT_HIDE, {relatedTarget: innerElem}) : null;
186
+ const showEvent = EventHandler.trigger(innerElem, EVENT_SHOW, {relatedTarget: activeElem});
184
187
 
185
188
  if (showEvent.defaultPrevented || (hideEvent && hideEvent.defaultPrevented)) return;
186
189
 
187
190
  this._deactivate(activeElem, innerElem);
188
- this._activate(innerElem, relatedTarget);
191
+ this._activate(innerElem, activeElem);
192
+ this._updateSlider(innerElem);
189
193
  }
190
194
 
191
195
  /**
@@ -218,21 +222,28 @@ class VGTabs extends BaseModule {
218
222
  const target = Selectors.getElementFromSelector(element);
219
223
  if (target) this._activate(target, relatedTarget);
220
224
 
221
- const complete = () => {
225
+ const complete = () => {
226
+ if (!this._element || !element.classList.contains(CLASS_NAME.ACTIVE)) return;
222
227
  if (element.getAttribute('role') !== 'tab') {
223
228
  element.classList.add(CLASS_NAME.SHOW);
224
229
  return;
225
230
  }
226
231
 
227
- this._route((status, data) => {
228
- EventHandler.trigger(this._element, EVENT_LOADED, { stats: status, data });
229
- });
232
+ if (this._params.ajax.route && !this._isLoaded && !this._isLoading) {
233
+ this._isLoading = true;
234
+ const loaded = (status, data) => {
235
+ if (!this._element) return;
236
+ this._isLoading = false;
237
+ EventHandler.trigger(this._element, EVENT_LOADED, { stats: status, data });
238
+ };
239
+ this._route(loaded, error => loaded('error', error));
240
+ }
230
241
 
231
242
  element.removeAttribute('tabindex');
232
243
  element.setAttribute('aria-selected', 'true');
233
244
  this._toggleDropDown(element, true);
234
245
 
235
- EventHandler.trigger(element, EVENT_SHOWN, { relatedTarget }); // ← теперь relatedTarget определён
246
+ EventHandler.trigger(element, EVENT_SHOWN, { relatedTarget });
236
247
  };
237
248
 
238
249
  this._queueCallback(complete, element, element.classList.contains(CLASS_NAME.FADE));
@@ -252,7 +263,8 @@ class VGTabs extends BaseModule {
252
263
  const target = Selectors.getElementFromSelector(element);
253
264
  if (target) this._deactivate(target, relatedTarget);
254
265
 
255
- const complete = () => {
266
+ const complete = () => {
267
+ if (!this._element || element.classList.contains(CLASS_NAME.ACTIVE)) return;
256
268
  if (element.getAttribute('role') !== 'tab') {
257
269
  element.classList.remove(CLASS_NAME.SHOW);
258
270
  return;
@@ -300,13 +312,12 @@ class VGTabs extends BaseModule {
300
312
  _setTabHash() {
301
313
  if (!this._params.hash) return;
302
314
 
303
- const url = document.location.toString();
304
- if (!url.includes('#')) return;
305
-
306
- const id = url.split('#')[1];
307
- const element = Selectors.find(`[href="#${id}"]`, this._parent) ||
308
- Selectors.find(`[data-vg-target="#${id}"]`, this._element) ||
309
- null;
315
+ let hash = document.location.hash;
316
+ if (!hash) return;
317
+ try { hash = decodeURIComponent(hash); } catch { return; }
318
+ const element = this._getChildren().find(child =>
319
+ !isDisabled(child) && (child.getAttribute('href') === hash || child.getAttribute('data-vg-target') === hash)
320
+ );
310
321
 
311
322
  if (element) {
312
323
  VGTabs.getOrCreateInstance(element).show();
@@ -316,8 +327,8 @@ class VGTabs extends BaseModule {
316
327
  /**
317
328
  * Инициализация слайдера-индикатора под вкладками
318
329
  */
319
- _setInitialSlider() {
320
- if (!this._params.slide) return;
330
+ _setInitialSlider() {
331
+ if (!this._params.slide || !this._main_parent) return;
321
332
 
322
333
  let slider = Selectors.find(`.${CLASS_NAME.SLIDER}`, this._main_parent);
323
334
  if (!slider) {
@@ -328,45 +339,58 @@ class VGTabs extends BaseModule {
328
339
 
329
340
  this._main_parent.classList.add(CLASS_NAME.WITH_SLIDER);
330
341
 
331
- const activeLink = Selectors.find(`.${CLASS_NAME.ACTIVE}`, this._parent);
332
- if (!activeLink) return;
333
-
334
- const {width, height} = window.getComputedStyle(activeLink);
335
- activeLink.classList.add(CLASS_NAME.HOVER);
336
-
337
- slider.style.width = width;
338
- slider.style.height = height;
339
- slider.style.left = `${activeLink.offsetLeft}px`;
340
-
341
- // Наведение
342
- EventHandler.on(this._main_parent, EVENT_MOUSEOVER_DATA_API, SELECTOR.DATA_TOGGLE, (event) => {
343
- const target = event.target;
344
- if (['A', 'AREA'].includes(target.tagName)) event.preventDefault();
345
- if (isDisabled(target)) return;
346
-
347
- const hover = Selectors.find(`.${CLASS_NAME.HOVER}`, this._parent);
348
- if (hover) hover.classList.remove(CLASS_NAME.HOVER);
349
- target.classList.add(CLASS_NAME.HOVER);
350
-
351
- const {width, height} = window.getComputedStyle(target);
352
- slider.style.width = width;
353
- slider.style.height = height;
354
- slider.style.left = `${target.offsetLeft}px`;
355
- });
356
-
357
- // Уход курсора
358
- EventHandler.on(this._main_parent, EVENT_MOUSEOUT_DATA_API, SELECTOR.DATA_TOGGLE, () => {
359
- const active = Selectors.find(`.${CLASS_NAME.ACTIVE}`, this._parent);
360
- const {width, height} = window.getComputedStyle(active);
361
-
362
- Selectors.findAll(`.${CLASS_NAME.HOVER}`, this._parent).forEach(el => el.classList.remove(CLASS_NAME.HOVER));
363
- active.classList.add(CLASS_NAME.HOVER);
364
-
365
- slider.style.width = width;
366
- slider.style.height = height;
367
- slider.style.left = `${active.offsetLeft}px`;
368
- });
369
- }
342
+ this._updateSlider(this._getActiveElem());
343
+ this._sliderOver = event => {
344
+ const target = event.delegateTarget;
345
+ if (target.closest(SELECTOR.TAB_PANEL) === this._parent && !isDisabled(target)) this._updateSlider(target);
346
+ };
347
+ this._sliderOut = () => this._updateSlider(this._getActiveElem());
348
+ // Только один владелец общих обработчиков на группу вкладок.
349
+ const owner = this._getChildren().some(child => VGTabs.getInstance(child)?._sliderOver && child !== this._element);
350
+ if (owner) {
351
+ this._sliderOver = null;
352
+ this._sliderOut = null;
353
+ return;
354
+ }
355
+ EventHandler.on(this._main_parent, EVENT_MOUSEOVER_DATA_API, SELECTOR.DATA_TOGGLE, this._sliderOver);
356
+ EventHandler.on(this._main_parent, EVENT_MOUSEOUT_DATA_API, SELECTOR.DATA_TOGGLE, this._sliderOut);
357
+ this._sliderResize = () => this._updateSlider(this._getActiveElem());
358
+ window.addEventListener('resize', this._sliderResize);
359
+ if (typeof ResizeObserver !== 'undefined') {
360
+ this._sliderObserver = new ResizeObserver(this._sliderResize);
361
+ this._sliderObserver.observe(this._parent);
362
+ this._getChildren().forEach(child => this._sliderObserver.observe(child));
363
+ }
364
+ }
365
+
366
+ _updateSlider(target) {
367
+ if (!target || !this._main_parent) return;
368
+ const slider = this._main_parent.querySelector(`.${CLASS_NAME.SLIDER}`);
369
+ if (!slider) return;
370
+ this._getChildren().forEach(child => child.classList.toggle(CLASS_NAME.HOVER, child === target));
371
+ const {width, height} = window.getComputedStyle(target);
372
+ Object.assign(slider.style, {width, height, left: `${target.offsetLeft}px`, top: `${target.offsetTop}px`});
373
+ }
374
+
375
+ dispose() {
376
+ if (!this._element) return;
377
+ const parent = this._main_parent;
378
+ const successor = this._sliderOver && this._getChildren()
379
+ .map(child => VGTabs.getInstance(child))
380
+ .find(instance => instance && instance !== this && instance._params.slide);
381
+ if (this._sliderOver) {
382
+ EventHandler.off(parent, EVENT_MOUSEOVER_DATA_API, SELECTOR.DATA_TOGGLE, this._sliderOver);
383
+ EventHandler.off(parent, EVENT_MOUSEOUT_DATA_API, SELECTOR.DATA_TOGGLE, this._sliderOut);
384
+ window.removeEventListener('resize', this._sliderResize);
385
+ this._sliderObserver?.disconnect();
386
+ }
387
+ super.dispose();
388
+ if (successor) successor._setInitialSlider();
389
+ else if (parent && !Selectors.findAll(SELECTOR.DATA_TOGGLE, parent).some(child => VGTabs.getInstance(child)?._params.slide)) {
390
+ parent.querySelector(`.${CLASS_NAME.SLIDER}`)?.remove();
391
+ parent.classList.remove(CLASS_NAME.WITH_SLIDER);
392
+ }
393
+ }
370
394
 
371
395
  /**
372
396
  * Устанавливает базовые ARIA-атрибуты родителю
@@ -391,9 +415,9 @@ class VGTabs extends BaseModule {
391
415
  if (outerElem !== child) {
392
416
  this._setAttributeIfNotExists(outerElem, 'role', 'presentation');
393
417
  }
394
- if (!isActive) {
395
- child.setAttribute('tabindex', '-1');
396
- }
418
+ if (!isActive) {
419
+ child.setAttribute('tabindex', '-1');
420
+ } else child.removeAttribute('tabindex');
397
421
  this._setAttributeIfNotExists(child, 'role', 'tab');
398
422
  this._setInitialAttributesOnTargetPanel(child);
399
423
  }
@@ -429,7 +453,8 @@ class VGTabs extends BaseModule {
429
453
  * @returns {HTMLElement[]}
430
454
  */
431
455
  _getChildren() {
432
- return Selectors.findAll(SELECTOR.INNER_ELEM, this._parent);
456
+ return Selectors.findAll(SELECTOR.INNER_ELEM, this._parent)
457
+ .filter(child => child.closest(SELECTOR.TAB_PANEL) === this._parent);
433
458
  }
434
459
 
435
460
  /**
@@ -490,4 +515,4 @@ EventHandler.on(window, EVENT_LOAD_DATA_API, () => {
490
515
  });
491
516
  });
492
517
 
493
- export default VGTabs;
518
+ export default VGTabs;