vgapp 0.8.1 → 0.8.3

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.
@@ -4,23 +4,60 @@ import EventHandler from "../../../utils/js/dom/event";
4
4
  import {getNextActiveElement, isDisabled, mergeDeepObject} from "../../../utils/js/functions";
5
5
 
6
6
  /**
7
- * Constants
7
+ * @constant {string} NAME - Имя модуля (используется для событий и идентификации)
8
8
  */
9
9
  const NAME = 'tabs';
10
+ /**
11
+ * @constant {string} NAME_KEY - Полное пространство имён модуля (с префиксом)
12
+ */
10
13
  const NAME_KEY = 'vg.tabs';
11
14
 
15
+ /**
16
+ * @event VGTabs#hide - Срабатывает перед скрытием вкладки
17
+ */
12
18
  const EVENT_HIDE = `${NAME_KEY}.hide`;
19
+ /**
20
+ * @event VGTabs#hidden - Срабатывает после скрытия вкладки
21
+ */
13
22
  const EVENT_HIDDEN = `${NAME_KEY}.hidden`;
23
+ /**
24
+ * @event VGTabs#show - Срабатывает перед показом вкладки
25
+ */
14
26
  const EVENT_SHOW = `${NAME_KEY}.show`;
27
+ /**
28
+ * @event VGTabs#shown - Срабатывает после показа вкладки
29
+ */
15
30
  const EVENT_SHOWN = `${NAME_KEY}.shown`;
31
+ /**
32
+ * @event VGTabs#loaded - Срабатывает после загрузки контента (AJAX)
33
+ */
16
34
  const EVENT_LOADED = `${NAME_KEY}.loaded`;
17
35
 
36
+ /**
37
+ * @constant {string} EVENT_KEYDOWN - Событие клавиатуры для навигации по вкладкам
38
+ */
18
39
  const EVENT_KEYDOWN = `keydown.${NAME_KEY}`;
40
+ /**
41
+ * @constant {string} EVENT_LOAD_DATA_API - Событие загрузки страницы
42
+ */
19
43
  const EVENT_LOAD_DATA_API = `load.${NAME_KEY}`;
44
+ /**
45
+ * @constant {string} EVENT_CLICK_DATA_API - Событие клика для активации вкладки
46
+ */
20
47
  const EVENT_CLICK_DATA_API = `click.${NAME_KEY}`;
48
+ /**
49
+ * @constant {string} EVENT_MOUSEOVER_DATA_API - Событие наведения для слайдера
50
+ */
21
51
  const EVENT_MOUSEOVER_DATA_API = `mouseover.${NAME_KEY}`;
52
+ /**
53
+ * @constant {string} EVENT_MOUSEOUT_DATA_API - Событие ухода курсора для слайдера
54
+ */
22
55
  const EVENT_MOUSEOUT_DATA_API = `mouseout.${NAME_KEY}`;
23
56
 
57
+ /**
58
+ * @constant {string[]} NAV_KEYS - Клавиши для навигации между вкладками
59
+ */
60
+ const NAV_KEYS = ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Home', 'End'];
24
61
  const ARROW_LEFT_KEY = 'ArrowLeft';
25
62
  const ARROW_RIGHT_KEY = 'ArrowRight';
26
63
  const ARROW_UP_KEY = 'ArrowUp';
@@ -28,29 +65,59 @@ const ARROW_DOWN_KEY = 'ArrowDown';
28
65
  const HOME_KEY = 'Home';
29
66
  const END_KEY = 'End';
30
67
 
31
- const CLASS_NAME_ACTIVE = 'active';
32
- const CLASS_NAME_HOVER = 'hover';
33
- const CLASS_NAME_FADE = 'fade';
34
- const CLASS_NAME_SHOW = 'show';
35
- const CLASS_DROPDOWN = 'dropdown';
36
- const CLASS_SLIDER = 'vg-tabs-slider';
37
- const CLASS_WITH_SLIDER = 'vg-tabs-with-slider';
38
-
39
- const SELECTOR_DROPDOWN_TOGGLE = '[data-vg-toggle="dropdown"]';
40
- const SELECTOR_DROPDOWN_MENU = '.dropdown-content';
41
- const NOT_SELECTOR_DROPDOWN_TOGGLE = `:not(${SELECTOR_DROPDOWN_TOGGLE})`;
42
-
43
- const SELECTOR_TAB_CLASS = '.vg-tabs';
44
- const SELECTOR_TAB_PANEL = '.list-group, .vg-tabs-panel, [role="tablist"]';
45
- const SELECTOR_OUTER = '.vg-tabs-item, .list-group-item';
46
- const SELECTOR_INNER = `.vg-tabs-link${NOT_SELECTOR_DROPDOWN_TOGGLE}, .list-group-item${NOT_SELECTOR_DROPDOWN_TOGGLE}, [role="tab"]${NOT_SELECTOR_DROPDOWN_TOGGLE}`;
47
- const SELECTOR_DATA_TOGGLE = '[data-vg-toggle="tab"]';
48
- const SELECTOR_INNER_ELEM = `${SELECTOR_INNER}, ${SELECTOR_DATA_TOGGLE}`;
68
+ /**
69
+ * @constant {Object} CLASS_NAME - Классы, используемые в компоненте
70
+ */
71
+ const CLASS_NAME = {
72
+ ACTIVE: 'active',
73
+ HOVER: 'hover',
74
+ FADE: 'fade',
75
+ SHOW: 'show',
76
+ DROPDOWN: 'dropdown',
77
+ SLIDER: 'vg-tabs-slider',
78
+ WITH_SLIDER: 'vg-tabs-with-slider'
79
+ };
49
80
 
50
- const SELECTOR_DATA_TOGGLE_ACTIVE = `.${CLASS_NAME_ACTIVE}[data-vg-toggle="tab"]`;
81
+ /**
82
+ * @constant {Object} SELECTOR - CSS-селекторы, используемые в компоненте
83
+ */
84
+ const INNER_SELECTOR = `.vg-tabs-link:not([data-vg-toggle="dropdown"]), .list-group-item:not([data-vg-toggle="dropdown"]), [role="tab"]:not([data-vg-toggle="dropdown"])`;
85
+ const DATA_TOGGLE = '[data-vg-toggle="tab"]';
86
+
87
+ const SELECTOR = {
88
+ DROPDOWN_TOGGLE: '[data-vg-toggle="dropdown"]',
89
+ DROPDOWN_MENU: '.dropdown-content',
90
+ TAB_CLASS: '.vg-tabs',
91
+ TAB_PANEL: '.list-group, .vg-tabs-panel, [role="tablist"]',
92
+ OUTER: '.vg-tabs-item, .list-group-item',
93
+ INNER: INNER_SELECTOR,
94
+ DATA_TOGGLE: DATA_TOGGLE,
95
+ INNER_ELEM: `${INNER_SELECTOR}, ${DATA_TOGGLE}`,
96
+ DATA_TOGGLE_ACTIVE: `.active[data-vg-toggle="tab"]`
97
+ };
51
98
 
99
+ /**
100
+ * Компонент вкладок (Tabs)
101
+ * Поддерживает: навигацию с клавиатуры, хеш-роутинг, AJAX-загрузку, анимацию, слайдер-индикатор.
102
+ *
103
+ * @extends BaseModule
104
+ */
52
105
  class VGTabs extends BaseModule {
53
-
106
+ /**
107
+ * Создаёт экземпляр VGTabs
108
+ *
109
+ * @param {HTMLElement} element - Элемент вкладки (например, ссылка)
110
+ * @param {Object} params - Параметры инициализации
111
+ * @param {boolean} [params.slide=false] - Показывать ли индикатор-слайдер
112
+ * @param {boolean} [params.hash=false] - Активировать вкладку по хешу в URL
113
+ * @param {Object} [params.ajax] - Настройки AJAX
114
+ * @param {string} [params.ajax.route=''] - URL для загрузки
115
+ * @param {string} [params.ajax.target=''] - Селектор цели загрузки
116
+ * @param {string} [params.ajax.method='get'] - HTTP-метод
117
+ * @param {boolean} [params.ajax.loader=false] - Показывать ли лоадер
118
+ * @param {boolean} [params.ajax.once=true] - Загружать один раз
119
+ * @param {boolean} [params.ajax.output=true] - Выводить ли ответ в DOM
120
+ */
54
121
  constructor(element, params) {
55
122
  super(element, params);
56
123
 
@@ -67,319 +134,360 @@ class VGTabs extends BaseModule {
67
134
  },
68
135
  }, this._params);
69
136
 
70
- this._parent = this._element.closest(SELECTOR_TAB_PANEL);
71
- this._main_parent = this._parent.closest(SELECTOR_TAB_CLASS);
72
- this._params = this._getParams(this._main_parent, this._params);
73
- this._params = this._getParams(this._element, this._params);
137
+ this._parent = this._element.closest(SELECTOR.TAB_PANEL);
138
+ this._main_parent = this._parent?.closest(SELECTOR.TAB_CLASS) || null;
74
139
 
75
140
  if (!this._parent) {
76
- throw new TypeError(`${element.outerHTML} не имеет родителя ${SELECTOR_INNER_ELEM}`)
141
+ throw new TypeError(`${element.outerHTML} не имеет родителя с селектором ${SELECTOR.INNER_ELEM}`);
77
142
  }
78
143
 
144
+ this._params = this._getParams(this._main_parent, this._params);
145
+ this._params = this._getParams(this._element, this._params);
146
+
79
147
  this._setInitialAttributes(this._parent, this._getChildren());
80
148
  this._setInitialSlider();
81
149
  this._setTabHash();
82
150
 
83
- EventHandler.on(this._element, EVENT_KEYDOWN, event => this._keydown(event))
151
+ EventHandler.on(this._element, EVENT_KEYDOWN, event => this._keydown(event));
84
152
  }
85
153
 
154
+ /**
155
+ * Возвращает имя компонента
156
+ * @returns {string}
157
+ */
86
158
  static get NAME() {
87
159
  return NAME;
88
160
  }
89
161
 
162
+ /**
163
+ * Возвращает ключ компонента (с префиксом)
164
+ * @returns {string}
165
+ */
90
166
  static get NAME_KEY() {
91
- return NAME_KEY
167
+ return NAME_KEY;
92
168
  }
93
169
 
170
+ /**
171
+ * Активирует вкладку
172
+ */
94
173
  show() {
95
- const innerElem = this._element
96
- if (this._elemIsActive(innerElem)) {
97
- return
98
- }
174
+ const innerElem = this._element;
99
175
 
100
- const active = this._getActiveElem()
176
+ if (this._elemIsActive(innerElem)) return;
101
177
 
102
- const hideEvent = active ?
103
- EventHandler.trigger(active, EVENT_HIDE, { relatedTarget: innerElem }) :
104
- null
178
+ const activeElem = this._getActiveElem();
179
+ const relatedTarget = innerElem;
105
180
 
106
- const showEvent = EventHandler.trigger(innerElem, EVENT_SHOW, { relatedTarget: active })
181
+ // События hide и show
182
+ const hideEvent = activeElem ? EventHandler.trigger(activeElem, EVENT_HIDE, {relatedTarget}) : null;
183
+ const showEvent = EventHandler.trigger(innerElem, EVENT_SHOW, {relatedTarget});
107
184
 
108
- if (showEvent.defaultPrevented || (hideEvent && hideEvent.defaultPrevented)) {
109
- return
110
- }
185
+ if (showEvent.defaultPrevented || (hideEvent && hideEvent.defaultPrevented)) return;
111
186
 
112
- this._deactivate(active, innerElem)
113
- this._activate(innerElem, active)
187
+ this._deactivate(activeElem, innerElem);
188
+ this._activate(innerElem, relatedTarget);
114
189
  }
115
190
 
191
+ /**
192
+ * Проверяет, активен ли элемент
193
+ * @param {HTMLElement} elem - Элемент для проверки
194
+ * @returns {boolean}
195
+ */
116
196
  _elemIsActive(elem) {
117
- return elem.classList.contains(CLASS_NAME_ACTIVE)
197
+ return elem?.classList.contains(CLASS_NAME.ACTIVE) || false;
118
198
  }
119
199
 
200
+ /**
201
+ * Получает активный элемент во вкладках
202
+ * @returns {HTMLElement|null}
203
+ */
120
204
  _getActiveElem() {
121
- return this._getChildren().find(child => this._elemIsActive(child)) || null
205
+ return this._getChildren().find(child => this._elemIsActive(child)) || null;
122
206
  }
123
207
 
124
- _activate(element, relatedElem) {
125
- if (!element) {
126
- return
127
- }
208
+ /**
209
+ * Активирует элемент и его целевой панель
210
+ * @param {HTMLElement} element - Активируемый элемент
211
+ * @param {HTMLElement} relatedTarget - Элемент, вызвавший активацию
212
+ */
213
+ _activate(element, relatedTarget) {
214
+ if (!element) return;
128
215
 
129
- element.classList.add(CLASS_NAME_ACTIVE);
216
+ element.classList.add(CLASS_NAME.ACTIVE);
130
217
 
131
- this._activate(Selectors.getElementFromSelector(element));
218
+ const target = Selectors.getElementFromSelector(element);
219
+ if (target) this._activate(target, relatedTarget);
132
220
 
133
221
  const complete = () => {
134
222
  if (element.getAttribute('role') !== 'tab') {
135
- element.classList.add(CLASS_NAME_SHOW)
136
- return
223
+ element.classList.add(CLASS_NAME.SHOW);
224
+ return;
137
225
  }
138
226
 
139
227
  this._route((status, data) => {
140
- EventHandler.trigger(this._element, EVENT_LOADED, {stats: status, data: data});
228
+ EventHandler.trigger(this._element, EVENT_LOADED, { stats: status, data });
141
229
  });
142
230
 
143
- element.removeAttribute('tabindex')
144
- element.setAttribute('aria-selected', true);
231
+ element.removeAttribute('tabindex');
232
+ element.setAttribute('aria-selected', 'true');
233
+ this._toggleDropDown(element, true);
145
234
 
146
- this._toggleDropDown(element, true)
147
- EventHandler.trigger(element, EVENT_SHOWN, {
148
- relatedTarget: relatedElem
149
- })
150
- }
235
+ EventHandler.trigger(element, EVENT_SHOWN, { relatedTarget }); // ← теперь relatedTarget определён
236
+ };
151
237
 
152
- this._queueCallback(complete, element, element.classList.contains(CLASS_NAME_FADE))
238
+ this._queueCallback(complete, element, element.classList.contains(CLASS_NAME.FADE));
153
239
  }
154
240
 
155
- _deactivate(element, relatedElem) {
156
- if (!element) {
157
- return;
158
- }
241
+ /**
242
+ * Деактивирует элемент
243
+ * @param {HTMLElement} element - Деактивируемый элемент
244
+ * @param {HTMLElement} relatedTarget - Новый активный элемент
245
+ */
246
+ _deactivate(element, relatedTarget) {
247
+ if (!element) return;
159
248
 
160
- element.classList.remove(CLASS_NAME_ACTIVE);
249
+ element.classList.remove(CLASS_NAME.ACTIVE);
161
250
  element.blur();
162
251
 
163
- this._deactivate(Selectors.getElementFromSelector(element));
252
+ const target = Selectors.getElementFromSelector(element);
253
+ if (target) this._deactivate(target, relatedTarget);
164
254
 
165
255
  const complete = () => {
166
256
  if (element.getAttribute('role') !== 'tab') {
167
- element.classList.remove(CLASS_NAME_SHOW);
257
+ element.classList.remove(CLASS_NAME.SHOW);
168
258
  return;
169
259
  }
170
260
 
171
- element.setAttribute('aria-selected', false);
261
+ element.setAttribute('aria-selected', 'false');
172
262
  element.setAttribute('tabindex', '-1');
173
263
  this._toggleDropDown(element, false);
174
- EventHandler.trigger(element, EVENT_HIDDEN, { relatedTarget: relatedElem });
175
- }
176
264
 
177
- this._queueCallback(complete, element, element.classList.contains(CLASS_NAME_FADE));
265
+ EventHandler.trigger(element, EVENT_HIDDEN, { relatedTarget });
266
+ };
267
+
268
+ this._queueCallback(complete, element, element.classList.contains(CLASS_NAME.FADE));
178
269
  }
179
270
 
271
+ /**
272
+ * Обработка навигации с клавиатуры
273
+ * @param {KeyboardEvent} event
274
+ */
180
275
  _keydown(event) {
181
- if (!([ARROW_LEFT_KEY, ARROW_RIGHT_KEY, ARROW_UP_KEY, ARROW_DOWN_KEY, HOME_KEY, END_KEY].includes(event.key))) {
182
- return
183
- }
276
+ if (!NAV_KEYS.includes(event.key)) return;
184
277
 
185
- event.stopPropagation()// stopPropagation/preventDefault both added to support up/down keys without scrolling the page
186
- event.preventDefault()
278
+ event.stopPropagation();
279
+ event.preventDefault();
187
280
 
188
- const children = this._getChildren().filter(element => !isDisabled(element))
189
- let nextActiveElement
281
+ const children = this._getChildren().filter(el => !isDisabled(el));
282
+ let nextActiveElement;
190
283
 
191
284
  if ([HOME_KEY, END_KEY].includes(event.key)) {
192
- nextActiveElement = children[event.key === HOME_KEY ? 0 : children.length - 1]
285
+ nextActiveElement = children[event.key === HOME_KEY ? 0 : children.length - 1];
193
286
  } else {
194
- const isNext = [ARROW_RIGHT_KEY, ARROW_DOWN_KEY].includes(event.key)
195
- nextActiveElement = getNextActiveElement(children, event.target, isNext, true)
287
+ const isNext = [ARROW_RIGHT_KEY, ARROW_DOWN_KEY].includes(event.key);
288
+ nextActiveElement = getNextActiveElement(children, event.target, isNext, true);
196
289
  }
197
290
 
198
291
  if (nextActiveElement) {
199
- nextActiveElement.focus({ preventScroll: true })
200
- VGTabs.getOrCreateInstance(nextActiveElement).show()
292
+ nextActiveElement.focus({preventScroll: true});
293
+ VGTabs.getOrCreateInstance(nextActiveElement).show();
201
294
  }
202
295
  }
203
296
 
297
+ /**
298
+ * Активация вкладки по хешу в URL
299
+ */
204
300
  _setTabHash() {
205
- if (!this._params.hash) {
206
- return;
207
- }
301
+ if (!this._params.hash) return;
208
302
 
209
- let url = document.location.toString();
303
+ const url = document.location.toString();
304
+ if (!url.includes('#')) return;
210
305
 
211
- if (url.match('#')) {
212
- let id = url.split('#')[1];
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;
213
310
 
214
- let element = Selectors.find('[href="#' + id +'"]', this._parent) || Selectors.find('[data-vg-target="#' + id +'"]', this._element) || null;
215
- if (element) {
216
- VGTabs.getOrCreateInstance(element).show();
217
- }
311
+ if (element) {
312
+ VGTabs.getOrCreateInstance(element).show();
218
313
  }
219
314
  }
220
315
 
316
+ /**
317
+ * Инициализация слайдера-индикатора под вкладками
318
+ */
221
319
  _setInitialSlider() {
222
- if (!this._params.slide) {
223
- return;
224
- }
320
+ if (!this._params.slide) return;
225
321
 
226
- let slider = Selectors.find('.' + CLASS_SLIDER, this._main_parent);
322
+ let slider = Selectors.find(`.${CLASS_NAME.SLIDER}`, this._main_parent);
227
323
  if (!slider) {
228
324
  slider = document.createElement('span');
229
- slider.classList.add(CLASS_SLIDER);
325
+ slider.classList.add(CLASS_NAME.SLIDER);
230
326
  this._main_parent.prepend(slider);
231
327
  }
232
328
 
233
- this._main_parent.classList.add(CLASS_WITH_SLIDER);
329
+ this._main_parent.classList.add(CLASS_NAME.WITH_SLIDER);
234
330
 
235
- let link_active = Selectors.find('.' + CLASS_NAME_ACTIVE, this._parent),
236
- {width, height} = window.getComputedStyle(link_active);
331
+ const activeLink = Selectors.find(`.${CLASS_NAME.ACTIVE}`, this._parent);
332
+ if (!activeLink) return;
237
333
 
238
- link_active.classList.add(CLASS_NAME_HOVER);
334
+ const {width, height} = window.getComputedStyle(activeLink);
335
+ activeLink.classList.add(CLASS_NAME.HOVER);
239
336
 
240
337
  slider.style.width = width;
241
338
  slider.style.height = height;
242
- slider.style.left = link_active.offsetLeft + 'px';
339
+ slider.style.left = `${activeLink.offsetLeft}px`;
243
340
 
244
- EventHandler.on(this._main_parent, EVENT_MOUSEOVER_DATA_API, SELECTOR_DATA_TOGGLE, (event) => {
245
- let link_target = event.target,
246
- {width, height} = window.getComputedStyle(link_target);
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;
247
346
 
248
- if (['A', 'AREA'].includes(event.target.tagName)) {
249
- event.preventDefault();
250
- }
251
-
252
- if (isDisabled(link_target)) return;
253
-
254
- let link_current_hover = Selectors.find('.' + CLASS_NAME_HOVER, this._parent);
255
- if (link_current_hover) link_current_hover.classList.remove(CLASS_NAME_HOVER);
256
- link_target.classList.add(CLASS_NAME_HOVER);
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);
257
350
 
351
+ const {width, height} = window.getComputedStyle(target);
258
352
  slider.style.width = width;
259
353
  slider.style.height = height;
260
- slider.style.left = link_target.offsetLeft + 'px';
354
+ slider.style.left = `${target.offsetLeft}px`;
261
355
  });
262
356
 
263
- EventHandler.on(this._main_parent, EVENT_MOUSEOUT_DATA_API, SELECTOR_DATA_TOGGLE, (event) => {
264
- if (['A', 'AREA'].includes(event.target.tagName)) {
265
- event.preventDefault();
266
- }
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);
267
361
 
268
- let active = Selectors.find('.' + CLASS_NAME_ACTIVE, this._parent),
269
- {width, height} = window.getComputedStyle(active);
270
-
271
- [... Selectors.findAll('.' + CLASS_NAME_HOVER, this._parent)].forEach(el => {
272
- el.classList.remove(CLASS_NAME_HOVER);
273
- });
274
-
275
- active.classList.add(CLASS_NAME_HOVER);
362
+ Selectors.findAll(`.${CLASS_NAME.HOVER}`, this._parent).forEach(el => el.classList.remove(CLASS_NAME.HOVER));
363
+ active.classList.add(CLASS_NAME.HOVER);
276
364
 
277
365
  slider.style.width = width;
278
366
  slider.style.height = height;
279
- slider.style.left = active.offsetLeft + 'px';
367
+ slider.style.left = `${active.offsetLeft}px`;
280
368
  });
281
369
  }
282
370
 
371
+ /**
372
+ * Устанавливает базовые ARIA-атрибуты родителю
373
+ * @param {HTMLElement} parent - Родительский элемент (tablist)
374
+ * @param {HTMLElement[]} children - Дочерние элементы (вкладки)
375
+ */
283
376
  _setInitialAttributes(parent, children) {
284
- this._setAttributeIfNotExists(parent, 'role', 'tablist')
285
-
286
- for (const child of children) {
287
- this._setInitialAttributesOnChild(child)
288
- }
377
+ this._setAttributeIfNotExists(parent, 'role', 'tablist');
378
+ children.forEach(child => this._setInitialAttributesOnChild(child));
289
379
  }
290
380
 
381
+ /**
382
+ * Устанавливает атрибуты для одной вкладки
383
+ * @param {HTMLElement} child - Элемент вкладки
384
+ */
291
385
  _setInitialAttributesOnChild(child) {
292
- child = this._getInnerElement(child)
293
- const isActive = this._elemIsActive(child)
294
- const outerElem = this._getOuterElement(child)
295
- child.setAttribute('aria-selected', isActive)
386
+ child = this._getInnerElement(child);
387
+ const isActive = this._elemIsActive(child);
388
+ const outerElem = this._getOuterElement(child);
296
389
 
390
+ child.setAttribute('aria-selected', isActive);
297
391
  if (outerElem !== child) {
298
- this._setAttributeIfNotExists(outerElem, 'role', 'presentation')
392
+ this._setAttributeIfNotExists(outerElem, 'role', 'presentation');
299
393
  }
300
-
301
394
  if (!isActive) {
302
- child.setAttribute('tabindex', '-1')
395
+ child.setAttribute('tabindex', '-1');
303
396
  }
304
-
305
- this._setAttributeIfNotExists(child, 'role', 'tab')
306
- this._setInitialAttributesOnTargetPanel(child)
397
+ this._setAttributeIfNotExists(child, 'role', 'tab');
398
+ this._setInitialAttributesOnTargetPanel(child);
307
399
  }
308
400
 
401
+ /**
402
+ * Устанавливает атрибуты целевой панели (tabpanel)
403
+ * @param {HTMLElement} child - Элемент вкладки
404
+ */
309
405
  _setInitialAttributesOnTargetPanel(child) {
310
- const target = Selectors.getElementFromSelector(child)
311
-
312
- if (!target) {
313
- return
314
- }
315
-
316
- this._setAttributeIfNotExists(target, 'role', 'tabpanel')
406
+ const target = Selectors.getElementFromSelector(child);
407
+ if (!target) return;
317
408
 
409
+ this._setAttributeIfNotExists(target, 'role', 'tabpanel');
318
410
  if (child.id) {
319
- this._setAttributeIfNotExists(target, 'aria-labelledby', `${child.id}`)
411
+ this._setAttributeIfNotExists(target, 'aria-labelledby', child.id);
320
412
  }
321
413
  }
322
414
 
415
+ /**
416
+ * Устанавливает атрибут, если его ещё нет
417
+ * @param {HTMLElement} element - Целевой элемент
418
+ * @param {string} attribute - Имя атрибута
419
+ * @param {string} value - Значение атрибута
420
+ */
323
421
  _setAttributeIfNotExists(element, attribute, value) {
324
422
  if (!element.hasAttribute(attribute)) {
325
- element.setAttribute(attribute, value)
423
+ element.setAttribute(attribute, value);
326
424
  }
327
425
  }
328
426
 
427
+ /**
428
+ * Получает все дочерние элементы-вкладки
429
+ * @returns {HTMLElement[]}
430
+ */
329
431
  _getChildren() {
330
- return Selectors.findAll(SELECTOR_INNER_ELEM, this._parent)
432
+ return Selectors.findAll(SELECTOR.INNER_ELEM, this._parent);
331
433
  }
332
434
 
435
+ /**
436
+ * Получает внутренний элемент вкладки (ссылку)
437
+ * @param {HTMLElement} elem - Элемент
438
+ * @returns {HTMLElement}
439
+ */
333
440
  _getInnerElement(elem) {
334
- return elem.matches(SELECTOR_INNER_ELEM) ? elem : Selectors.find(SELECTOR_INNER_ELEM, elem)
441
+ return elem.matches(SELECTOR.INNER_ELEM) ? elem : Selectors.find(SELECTOR.INNER_ELEM, elem);
335
442
  }
336
443
 
444
+ /**
445
+ * Получает внешний контейнер вкладки
446
+ * @param {HTMLElement} elem - Элемент
447
+ * @returns {HTMLElement}
448
+ */
337
449
  _getOuterElement(elem) {
338
- return elem.closest(SELECTOR_OUTER) || elem
450
+ return elem.closest(SELECTOR.OUTER) || elem;
339
451
  }
340
452
 
453
+ /**
454
+ * Управляет состоянием выпадающего меню
455
+ * @param {HTMLElement} element - Элемент вкладки
456
+ * @param {boolean} open - Открыть или закрыть
457
+ */
341
458
  _toggleDropDown(element, open) {
342
- const outerElem = this._getOuterElement(element)
343
- if (!outerElem.classList.contains(CLASS_DROPDOWN)) {
344
- return
345
- }
459
+ const outerElem = this._getOuterElement(element);
460
+ if (!outerElem.classList.contains(CLASS_NAME.DROPDOWN)) return;
346
461
 
347
462
  const toggle = (selector, className) => {
348
- const element = Selectors.find(selector, outerElem)
349
- if (element) {
350
- element.classList.toggle(className, open)
351
- }
352
- }
463
+ const el = Selectors.find(selector, outerElem);
464
+ if (el) el.classList.toggle(className, open);
465
+ };
353
466
 
354
- toggle(SELECTOR_DROPDOWN_TOGGLE, CLASS_NAME_ACTIVE)
355
- toggle(SELECTOR_DROPDOWN_MENU, CLASS_NAME_SHOW)
356
- outerElem.setAttribute('aria-expanded', open)
467
+ toggle(SELECTOR.DROPDOWN_TOGGLE, CLASS_NAME.ACTIVE);
468
+ toggle(SELECTOR.DROPDOWN_MENU, CLASS_NAME.SHOW);
469
+ outerElem.setAttribute('aria-expanded', open);
357
470
  }
358
471
  }
359
472
 
360
473
  /**
361
- * Data API implementation
474
+ * Обработка кликов по вкладкам
362
475
  */
363
- EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (event) {
476
+ EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR.DATA_TOGGLE, function (event) {
364
477
  if (['A', 'AREA'].includes(this.tagName)) {
365
478
  event.preventDefault();
366
479
  }
367
-
368
- if (isDisabled(this)) {
369
- return;
370
- }
371
-
480
+ if (isDisabled(this)) return;
372
481
  VGTabs.getOrCreateInstance(this).show();
373
- })
482
+ });
374
483
 
375
484
  /**
376
- * Initialize on focus
485
+ * Инициализация активных вкладок при загрузке страницы
377
486
  */
378
487
  EventHandler.on(window, EVENT_LOAD_DATA_API, () => {
379
- for (const element of Selectors.findAll(SELECTOR_DATA_TOGGLE_ACTIVE)) {
488
+ Selectors.findAll(SELECTOR.DATA_TOGGLE_ACTIVE).forEach(element => {
380
489
  VGTabs.getOrCreateInstance(element);
381
- }
490
+ });
382
491
  });
383
492
 
384
-
385
- export default VGTabs;
493
+ export default VGTabs;