vgapp 0.8.0 → 0.8.2
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.
- package/CHANGELOG.md +16 -1
- package/app/langs/en/buttons.json +14 -0
- package/app/langs/en/messages.json +3 -0
- package/app/langs/en/titles.json +3 -0
- package/app/langs/ru/buttons.json +14 -0
- package/app/langs/ru/messages.json +3 -0
- package/app/langs/ru/titles.json +3 -0
- package/app/modules/vgloadmore/js/vgloadmore.js +363 -112
- package/app/modules/vgloadmore/readme.md +145 -0
- package/app/modules/vgrollup/js/vgrollup.js +328 -160
- package/app/modules/vgrollup/readme.md +196 -0
- package/app/modules/vgselect/js/handlers.js +220 -0
- package/app/modules/vgselect/js/vgselect.js +783 -298
- package/app/modules/vgselect/readme.md +180 -0
- package/app/modules/vgselect/scss/_variables.scss +20 -0
- package/app/modules/vgselect/scss/vgselect.scss +42 -2
- package/app/modules/vgsidebar/js/vgsidebar.js +194 -84
- package/app/modules/vgsidebar/readme.md +157 -0
- package/app/modules/vgspy/js/vgspy.js +236 -132
- package/app/modules/vgspy/readme.md +105 -0
- package/app/modules/vgtabs/js/vgtabs.js +290 -182
- package/app/modules/vgtabs/readme.md +156 -0
- package/app/modules/vgtoast/js/vgtoast.js +260 -156
- package/app/modules/vgtoast/readme.md +145 -0
- package/build/vgapp.css +1 -1
- package/build/vgapp.css.map +1 -1
- package/package.json +1 -1
|
@@ -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
|
-
*
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
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(
|
|
71
|
-
this._main_parent = this._parent
|
|
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} не имеет родителя ${
|
|
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
|
-
|
|
176
|
+
if (this._elemIsActive(innerElem)) return;
|
|
101
177
|
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
null
|
|
178
|
+
const activeElem = this._getActiveElem();
|
|
179
|
+
const relatedTarget = innerElem;
|
|
105
180
|
|
|
106
|
-
|
|
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(
|
|
113
|
-
this._activate(innerElem,
|
|
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
|
|
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
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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(
|
|
216
|
+
element.classList.add(CLASS_NAME.ACTIVE);
|
|
130
217
|
|
|
131
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
147
|
-
|
|
148
|
-
relatedTarget: relatedElem
|
|
149
|
-
})
|
|
150
|
-
}
|
|
235
|
+
EventHandler.trigger(element, EVENT_SHOWN, { relatedTarget }); // ← теперь relatedTarget определён
|
|
236
|
+
};
|
|
151
237
|
|
|
152
|
-
this._queueCallback(complete, element, element.classList.contains(
|
|
238
|
+
this._queueCallback(complete, element, element.classList.contains(CLASS_NAME.FADE));
|
|
153
239
|
}
|
|
154
240
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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(
|
|
249
|
+
element.classList.remove(CLASS_NAME.ACTIVE);
|
|
161
250
|
element.blur();
|
|
162
251
|
|
|
163
|
-
|
|
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(
|
|
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
|
-
|
|
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 (!
|
|
182
|
-
return
|
|
183
|
-
}
|
|
276
|
+
if (!NAV_KEYS.includes(event.key)) return;
|
|
184
277
|
|
|
185
|
-
event.stopPropagation()
|
|
186
|
-
event.preventDefault()
|
|
278
|
+
event.stopPropagation();
|
|
279
|
+
event.preventDefault();
|
|
187
280
|
|
|
188
|
-
const children = this._getChildren().filter(
|
|
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({
|
|
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
|
-
|
|
303
|
+
const url = document.location.toString();
|
|
304
|
+
if (!url.includes('#')) return;
|
|
210
305
|
|
|
211
|
-
|
|
212
|
-
|
|
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
|
-
|
|
215
|
-
|
|
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(
|
|
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(
|
|
325
|
+
slider.classList.add(CLASS_NAME.SLIDER);
|
|
230
326
|
this._main_parent.prepend(slider);
|
|
231
327
|
}
|
|
232
328
|
|
|
233
|
-
this._main_parent.classList.add(
|
|
329
|
+
this._main_parent.classList.add(CLASS_NAME.WITH_SLIDER);
|
|
234
330
|
|
|
235
|
-
|
|
236
|
-
|
|
331
|
+
const activeLink = Selectors.find(`.${CLASS_NAME.ACTIVE}`, this._parent);
|
|
332
|
+
if (!activeLink) return;
|
|
237
333
|
|
|
238
|
-
|
|
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 =
|
|
339
|
+
slider.style.left = `${activeLink.offsetLeft}px`;
|
|
243
340
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
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
|
-
|
|
249
|
-
|
|
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 =
|
|
354
|
+
slider.style.left = `${target.offsetLeft}px`;
|
|
261
355
|
});
|
|
262
356
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
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
|
-
|
|
269
|
-
|
|
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
|
|
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.
|
|
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',
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
349
|
-
if (
|
|
350
|
-
|
|
351
|
-
}
|
|
352
|
-
}
|
|
463
|
+
const el = Selectors.find(selector, outerElem);
|
|
464
|
+
if (el) el.classList.toggle(className, open);
|
|
465
|
+
};
|
|
353
466
|
|
|
354
|
-
toggle(
|
|
355
|
-
toggle(
|
|
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
|
-
*
|
|
474
|
+
* Обработка кликов по вкладкам
|
|
362
475
|
*/
|
|
363
|
-
EventHandler.on(document, EVENT_CLICK_DATA_API,
|
|
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
|
-
*
|
|
485
|
+
* Инициализация активных вкладок при загрузке страницы
|
|
377
486
|
*/
|
|
378
487
|
EventHandler.on(window, EVENT_LOAD_DATA_API, () => {
|
|
379
|
-
|
|
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;
|