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.
@@ -0,0 +1,157 @@
1
+ # VGSidebar – Модуль боковой панели (сайдбара)
2
+
3
+ `VGSidebar` – это модуль на чистом JavaScript, реализующий интерактивную боковую панель (сайдбар) с поддержкой анимаций, backdrop, AJAX-загрузки, управления скроллом и навигации через URL-хэш. Легко интегрируется в любые проекты без зависимостей.
4
+
5
+ ---
6
+
7
+ ## ✅ Возможности
8
+
9
+ - Открытие/закрытие сайдбара по клику, хэшу URL или программно
10
+ - Поддержка затемнённого фона (backdrop)
11
+ - Блокировка скролла страницы при открытом сайдбаре
12
+ - Поддержка клавиши **Escape** для закрытия
13
+ - Анимации входа и выхода (через CSS-классы, например, с `animate.css`)
14
+ - Возможность загрузки контента через AJAX
15
+ - Поддержка открытия по `#id` в URL (хэш-роутинг)
16
+
17
+ ---
18
+
19
+ ## 🔧 Установка
20
+
21
+ HTML разметка сайдбара:
22
+ ```html
23
+ <div class="vg-sidebar left" id="sidebar-left" data-animation='{"enable": true, "in": "animate__fadeInLeft", "out": "animate__fadeOutLeft"}'>
24
+ <div class="vg-sidebar-header">
25
+ <div class="vg-sidebar-header--title">...</div>
26
+ <button type="button" class="vg-btn-close" data-vg-dismiss="sidebar" data-vg-target="#sidebar-left" aria-label="Close"></button>
27
+ </div>
28
+ <div class="vg-sidebar-body">...</div>
29
+ <div class="vg-sidebar-footer">...</div>
30
+ </div>
31
+ ```
32
+ ---
33
+
34
+ ## ⚙️ Параметры (настройки)
35
+
36
+ Параметры можно задать:
37
+ - Через `data-*` атрибуты
38
+ - Через JavaScript при инициализации
39
+ - Через объединение обоих способов
40
+
41
+ | Параметр | Тип | По умолчанию | Описание |
42
+ |--------------------|----------|--------------------|---------|
43
+ | `backdrop` | boolean | `true` | Показывать затемнённый фон |
44
+ | `overflow` | boolean | `true` | Блокировать скролл страницы |
45
+ | `keyboard` | boolean | `true` | Закрывать по нажатию `Esc` |
46
+ | `hash` | boolean | `false` | Поддержка открытия по `#id` в URL |
47
+ | `animation.enable` | boolean | `false` | Включить анимации |
48
+ | `animation.in` | string | `animate__rollIn` | CSS-класс анимации входа |
49
+ | `animation.out` | string | `animate__rollOut` | CSS-класс анимации выхода |
50
+ | `animation.delay` | number | `800` | Задержка перед закрытием (мс) |
51
+ | `ajax.route` | string | `''` | URL для AJAX-загрузки |
52
+ | `ajax.target` | string | `''` | Селектор внутри сайдбара для вставки |
53
+ | `ajax.method` | string | `'get'` | HTTP-метод (`get`, `post`) |
54
+ | `ajax.loader` | boolean | `false` | Показывать лоадер |
55
+ | `ajax.once` | boolean | `false` | Загружать контент только один раз |
56
+ | `ajax.output` | boolean | `true` | Вставлять ответ в DOM |
57
+
58
+ ---
59
+
60
+ ## 🖱️ Использование
61
+
62
+ ### 1. Через Data API (рекомендуется)
63
+ ```html
64
+ <a href="#sidebar-left" data-vg-toggle="sidebar">Открыть панель слева</a>
65
+ или
66
+ <button class="btn btn-primary" data-vg-target="#sidebar-right" data-vg-toggle="sidebar">Открыть панель справа</button>
67
+ ```
68
+
69
+ > ⚠️ Обязательно задавайте `id`, если используете `data-vg-target` или хэш.
70
+
71
+ ### 2. Через JavaScript
72
+
73
+ ```js
74
+ import VGSidebar from './modules/vgsidebar/js/vgsidebar.js';
75
+ const sidebar = new VGSidebar(document.getElementById('sidebar-left'), {
76
+ backdrop: true,
77
+ overflow: true,
78
+ keyboard: true,
79
+ hash: true,
80
+ animation: {
81
+ enable: true,
82
+ in: 'animate__fadeInLeft',
83
+ out: 'animate__fadeOutLeft',
84
+ delay: 500
85
+ }
86
+ });
87
+
88
+ sidebar.show();
89
+ sidebar.hide();
90
+ sidebar.toggle();
91
+ ```
92
+
93
+ ### 3. Открытие по хэшу URL
94
+
95
+ Включите параметр `hash: true`, и при переходе по ссылке вида:
96
+
97
+ ```html
98
+ https://example.com/page#sidebar-left
99
+ ```
100
+
101
+ Сайдбар с `id="sidebar-left"` автоматически откроется.
102
+
103
+ ---
104
+
105
+ ### 4. AJAX-загрузка контента
106
+ ```html
107
+ <div class="vg-sidebar right" id="sidebar-right" data-params='{"ajax": {"route": "/core/server.php?sidebar=right", "target": "#sidebar-ajax-content", "loader": true}}'>
108
+ ...
109
+ ```
110
+
111
+ При открытии сайдбара контент будет загружен с `/api/sidebar-content` и вставлен в `.vg-sidebar-content`.
112
+
113
+ ---
114
+
115
+ ## 🎉 События
116
+
117
+ Модуль генерирует пользовательские события:
118
+
119
+ | Событие | Описание |
120
+ |---------------------------|---------|
121
+ | `vg.sidebar.show` | Перед открытием |
122
+ | `vg.sidebar.shown` | После открытия |
123
+ | `vg.sidebar.hide` | Перед закрытием |
124
+ | `vg.sidebar.hidden` | После закрытия |
125
+ | `vg.sidebar.loaded` | После AJAX-загрузки |
126
+ | `vg.sidebar.hidePrevented`| Если закрытие отменено (например, `Esc`, но `keyboard: false`) |
127
+
128
+ ## 🧹 Очистка
129
+
130
+ При необходимости удалите экземпляр:
131
+
132
+ ```js
133
+ sidebar.dispose();
134
+ ````
135
+
136
+ ---
137
+
138
+ ## 🧩 Зависимости
139
+
140
+ - `BaseModule` – базовый класс модулей
141
+ - `Backdrop` – управление затемнением
142
+ - `ScrollBarHelper` – блокировка скролла
143
+ - `EventHandler` – гибкая система событий
144
+ - `Selectors` – безопасный поиск элементов
145
+
146
+ ---
147
+
148
+
149
+ ## 📝 Лицензия
150
+
151
+ MIT. Свободно использовать и модифицировать.
152
+
153
+ ---
154
+
155
+ 📌 *Разработано в рамках фронтенд-системы VG Modules.*
156
+ > 🚀 Автор: VEGAS STUDIO (vegas-dev.com)
157
+ > 📍 Поддерживается в проектах VEGAS
@@ -1,228 +1,332 @@
1
1
  import BaseModule from "../../base-module";
2
- import {mergeDeepObject, getElement, isDisabled, isVisible} from "../../../utils/js/functions";
2
+ import { mergeDeepObject, getElement, isDisabled, isVisible } from "../../../utils/js/functions";
3
3
  import EventHandler from "../../../utils/js/dom/event";
4
4
  import Selectors from "../../../utils/js/dom/selectors";
5
5
 
6
6
  /**
7
- * Constants
7
+ * Константы модуля VGSpy
8
8
  */
9
9
  const NAME = 'spy';
10
10
  const NAME_KEY = 'vg.spy';
11
- const EVENT_KEY = `.${NAME_KEY}`
12
- const DATA_API_KEY = '.data-api'
13
-
14
- const EVENT_ACTIVATE = `activate${EVENT_KEY}`
15
- const EVENT_CLICK = `click${EVENT_KEY}`
16
- const EVENT_LOAD_DATA_API = `load${EVENT_KEY}${DATA_API_KEY}`
17
-
18
- const CLASS_NAME_DROPDOWN_ITEM = 'vg-dropdown-item'
19
- const CLASS_NAME_ACTIVE = 'active'
20
-
21
- const SELECTOR_DATA_SPY = '[data-vg-toggle="spy"]'
22
- const SELECTOR_TARGET_LINKS = '[href]'
23
- const SELECTOR_NAV_LIST_GROUP = '.nav, .list-group'
24
- const SELECTOR_NAV_LINKS = '.nav-link'
25
- const SELECTOR_NAV_ITEMS = '.nav-item'
26
- const SELECTOR_LIST_ITEMS = '.list-group-item'
27
- const SELECTOR_LINK_ITEMS = `${SELECTOR_NAV_LINKS}, ${SELECTOR_NAV_ITEMS} > ${SELECTOR_NAV_LINKS}, ${SELECTOR_LIST_ITEMS}`
28
- const SELECTOR_DROPDOWN = '.vg-dropdown'
29
- const SELECTOR_DROPDOWN_TOGGLE = '[data-vg-toggle="dropdown"]'
30
-
11
+ const EVENT_KEY = `.${NAME_KEY}`;
12
+ const DATA_API_KEY = '.data-api';
13
+
14
+ const EVENT_ACTIVATE = `activate${EVENT_KEY}`;
15
+ const EVENT_CLICK = `click${EVENT_KEY}`;
16
+ const EVENT_LOAD_DATA_API = `load${EVENT_KEY}${DATA_API_KEY}`;
17
+
18
+ const CLASS_NAME_DROPDOWN_ITEM = 'vg-dropdown-item';
19
+ const CLASS_NAME_ACTIVE = 'active';
20
+
21
+ const SELECTOR_DATA_SPY = '[data-vg-toggle="spy"]';
22
+ const SELECTOR_TARGET_LINKS = '[href]';
23
+ const SELECTOR_NAV_LIST_GROUP = '.nav, .list-group';
24
+ const SELECTOR_NAV_LINKS = '.nav-link';
25
+ const SELECTOR_NAV_ITEMS = '.nav-item';
26
+ const SELECTOR_LIST_ITEMS = '.list-group-item';
27
+ const SELECTOR_LINK_ITEMS = `${SELECTOR_NAV_LINKS}, ${SELECTOR_NAV_ITEMS} > ${SELECTOR_NAV_LINKS}, ${SELECTOR_LIST_ITEMS}`;
28
+ const SELECTOR_DROPDOWN = '.vg-dropdown';
29
+ const SELECTOR_DROPDOWN_TOGGLE = '[data-vg-toggle="dropdown"]';
31
30
 
31
+ /**
32
+ * Модуль "Spy" — отслеживает прокрутку и активные секции на странице.
33
+ * Автоматически подсвечивает навигационные ссылки в зависимости от текущего положения скролла.
34
+ * Поддерживает плавную прокрутку и работу внутри контейнеров с overflow.
35
+ */
32
36
  class VGSpy extends BaseModule {
37
+ /**
38
+ * Создаёт экземпляр VGSpy
39
+ * @param {HTMLElement} element — корневой элемент навигации (например, .nav)
40
+ * @param {Object} params — параметры конфигурации
41
+ */
33
42
  constructor(element, params) {
34
43
  super(element, params);
35
44
 
36
- this._params = this._getParams(element, mergeDeepObject({
37
- offset: null, // TODO: v6 @deprecated, keep it for backwards compatibility reasons
38
- rootMargin: '0px 0px -25%',
39
- smoothScroll: true,
40
- target: this._element,
41
- threshold: [0.1, 0.5, 1]
42
- }, params));
43
-
44
- this._targetLinks = new Map()
45
- this._observableSections = new Map()
46
- this._rootElement = getComputedStyle(this._element).overflowY === 'visible' ? null : this._element
47
- this._activeTarget = null
48
- this._observer = null
45
+ /**
46
+ * Объединённые параметры с настройками по умолчанию
47
+ * @type {Object}
48
+ * @property {number|null} offset - смещение (устаревшее, для совместимости)
49
+ * @property {string} rootMargin - отступ для IntersectionObserver
50
+ * @property {boolean} smoothScroll - включить плавную прокрутку по якорям
51
+ * @property {HTMLElement|string} target - целевой контейнер прокрутки
52
+ * @property {number[]|string} threshold - пороги видимости (0.1, 0.5, 1)
53
+ */
54
+ this._params = this._configAfterMerge(
55
+ mergeDeepObject(
56
+ {
57
+ offset: null, // Устаревшее, для обратной совместимости
58
+ rootMargin: '0px 0px -25%',
59
+ smoothScroll: true,
60
+ target: this._element,
61
+ threshold: [0.1, 0.5, 1],
62
+ },
63
+ params
64
+ )
65
+ );
66
+
67
+ /**
68
+ * Карта: хеш-ссылка → HTML-элемент ссылки
69
+ * @type {Map<string, HTMLElement>}
70
+ */
71
+ this._targetLinks = new Map();
72
+
73
+ /**
74
+ * Карта: хеш-ссылка → HTML-элемент наблюдаемой секции
75
+ * @type {Map<string, HTMLElement>}
76
+ */
77
+ this._observableSections = new Map();
78
+
79
+ /**
80
+ * Корневой элемент для IntersectionObserver (если скролл не окно)
81
+ * @type {HTMLElement|null}
82
+ */
83
+ this._rootElement = getComputedStyle(this._element).overflowY === 'visible' ? null : this._element;
84
+
85
+ /**
86
+ * Текущая активная секция
87
+ * @type {HTMLElement|null}
88
+ */
89
+ this._activeTarget = null;
90
+
91
+ /**
92
+ * Экземпляр IntersectionObserver
93
+ * @type {IntersectionObserver|null}
94
+ */
95
+ this._observer = null;
96
+
97
+ /**
98
+ * Данные о предыдущей прокрутке для определения направления
99
+ * @type {{visibleEntryTop: number, parentScrollTop: number}}
100
+ */
49
101
  this._previousScrollData = {
50
102
  visibleEntryTop: 0,
51
- parentScrollTop: 0
52
- }
53
- this._params = this._configAfterMerge(this._params);
103
+ parentScrollTop: 0,
104
+ };
54
105
 
55
106
  this.refresh();
56
107
  }
57
108
 
109
+ /**
110
+ * Имя модуля
111
+ * @returns {string}
112
+ */
58
113
  static get NAME() {
59
114
  return NAME;
60
115
  }
61
116
 
117
+ /**
118
+ * Ключ модуля (для хранения в data)
119
+ * @returns {string}
120
+ */
62
121
  static get NAME_KEY() {
63
- return NAME_KEY
122
+ return NAME_KEY;
64
123
  }
65
124
 
125
+ /**
126
+ * Инициализирует или перезапускает модуль: находит ссылки и секции, создаёт observer
127
+ */
66
128
  refresh() {
67
- this._initializeTargetsAndObservables()
68
- this._maybeEnableSmoothScroll()
129
+ this._initializeTargetsAndObservables();
130
+ this._maybeEnableSmoothScroll();
69
131
 
70
132
  if (this._observer) {
71
- this._observer.disconnect()
133
+ this._observer.disconnect();
72
134
  } else {
73
- this._observer = this._getNewObserver()
135
+ this._observer = this._getNewObserver();
74
136
  }
75
137
 
138
+ // Подписываемся на наблюдение за секциями
76
139
  for (const section of this._observableSections.values()) {
77
- this._observer.observe(section)
140
+ this._observer.observe(section);
78
141
  }
79
142
  }
80
143
 
144
+ /**
145
+ * Очищает ресурсы (отключает observer)
146
+ */
81
147
  dispose() {
82
- this._observer.disconnect()
83
- super.dispose()
148
+ if (this._observer) {
149
+ this._observer.disconnect();
150
+ }
151
+ super.dispose();
84
152
  }
85
153
 
86
- _configAfterMerge(param) {
87
- param.target = getElement(param.target) || document.body
88
- param.rootMargin = param.offset ? `${param.offset}px 0px -30%` : param.rootMargin
154
+ /**
155
+ * Обрабатывает и нормализует параметры после слияния
156
+ * @param {Object} config
157
+ * @returns {Object}
158
+ * @private
159
+ */
160
+ _configAfterMerge(config) {
161
+ config.target = getElement(config.target) || document.body;
162
+
163
+ // Поддержка устаревшего параметра `offset`
164
+ if (config.offset != null) {
165
+ config.rootMargin = `${config.offset}px 0px -30%`;
166
+ }
89
167
 
90
- if (typeof param.threshold === 'string') {
91
- param.threshold = param.threshold.split(',').map(value => Number.parseFloat(value))
168
+ // Преобразуем строку порогов в массив чисел
169
+ if (typeof config.threshold === 'string') {
170
+ config.threshold = config.threshold
171
+ .split(',')
172
+ .map((value) => Number.parseFloat(value.trim()));
92
173
  }
93
174
 
94
- return param
175
+ return config;
95
176
  }
96
177
 
178
+ /**
179
+ * Подключает плавную прокрутку по якорным ссылкам
180
+ * @private
181
+ */
97
182
  _maybeEnableSmoothScroll() {
98
- if (!this._params.smoothScroll) {
99
- return
100
- }
183
+ if (!this._params.smoothScroll) return;
184
+
185
+ EventHandler.off(this._params.target, EVENT_CLICK);
186
+ EventHandler.on(this._params.target, EVENT_CLICK, SELECTOR_TARGET_LINKS, (event) => {
187
+ const hash = event.target.hash;
188
+ if (!hash) return;
189
+
190
+ const section = this._observableSections.get(hash);
191
+ if (!section) return;
192
+
193
+ event.preventDefault();
194
+
195
+ const root = this._rootElement || window;
196
+ const scrollTop = section.offsetTop - this._element.offsetTop;
101
197
 
102
- EventHandler.off(this._params.target, EVENT_CLICK)
103
-
104
- EventHandler.on(this._params.target, EVENT_CLICK, SELECTOR_TARGET_LINKS, event => {
105
- const observableSection = this._observableSections.get(event.target.hash)
106
- if (observableSection) {
107
- event.preventDefault()
108
- const root = this._rootElement || window
109
- const height = observableSection.offsetTop - this._element.offsetTop
110
- if (root.scrollTo) {
111
- root.scrollTo({ top: height, behavior: 'smooth' })
112
- return
113
- }
114
- root.scrollTop = height
198
+ if (root.scrollTo) {
199
+ root.scrollTo({ top: scrollTop, behavior: 'smooth' });
200
+ } else {
201
+ root.scrollTop = scrollTop;
115
202
  }
116
- })
203
+ });
117
204
  }
118
205
 
206
+ /**
207
+ * Создаёт новый экземпляр IntersectionObserver
208
+ * @returns {IntersectionObserver}
209
+ * @private
210
+ */
119
211
  _getNewObserver() {
120
212
  const options = {
121
213
  root: this._rootElement,
214
+ rootMargin: this._params.rootMargin,
122
215
  threshold: this._params.threshold,
123
- rootMargin: this._params.rootMargin
124
- }
216
+ };
125
217
 
126
- return new IntersectionObserver(entries => this._observerCallback(entries), options)
218
+ return new IntersectionObserver((entries) => this._observerCallback(entries), options);
127
219
  }
128
220
 
221
+ /**
222
+ * Обработчик пересечений (IntersectionObserver)
223
+ * @param {IntersectionObserverEntry[]} entries
224
+ * @private
225
+ */
129
226
  _observerCallback(entries) {
130
- const targetElement = entry => this._targetLinks.get(`#${entry.target.id}`);
131
-
132
- const activate = entry => {
133
- this._previousScrollData.visibleEntryTop = entry.target.offsetTop;
134
- this._process(targetElement(entry));
135
- }
136
-
137
- const parentScrollTop = (this._rootElement || document.documentElement).scrollTop
138
- const userScrollsDown = parentScrollTop >= this._previousScrollData.parentScrollTop
139
- this._previousScrollData.parentScrollTop = parentScrollTop
227
+ const getTargetLink = (entry) => this._targetLinks.get(`#${entry.target.id}`);
228
+ const parentScrollTop = (this._rootElement || document.documentElement).scrollTop;
229
+ const userScrollsDown = parentScrollTop >= this._previousScrollData.parentScrollTop;
230
+ this._previousScrollData.parentScrollTop = parentScrollTop;
140
231
 
141
232
  for (const entry of entries) {
142
233
  if (!entry.isIntersecting) {
143
- this._activeTarget = null
144
- this._clearActiveClass(targetElement(entry))
145
-
146
- continue
234
+ this._clearActiveClass(getTargetLink(entry));
235
+ continue;
147
236
  }
148
237
 
149
- const entryIsLowerThanPrevious = entry.target.offsetTop >= this._previousScrollData.visibleEntryTop
150
- if (userScrollsDown && entryIsLowerThanPrevious) {
151
- activate(entry)
152
- if (!parentScrollTop) {
153
- return
154
- }
238
+ const isEntryBelow = entry.target.offsetTop >= this._previousScrollData.visibleEntryTop;
239
+ const shouldActivate =
240
+ (userScrollsDown && isEntryBelow) || (!userScrollsDown && !isEntryBelow);
155
241
 
156
- continue
157
- }
158
-
159
- if (!userScrollsDown && !entryIsLowerThanPrevious) {
160
- activate(entry)
242
+ if (shouldActivate) {
243
+ this._previousScrollData.visibleEntryTop = entry.target.offsetTop;
244
+ this._process(getTargetLink(entry));
161
245
  }
162
246
  }
163
247
  }
164
248
 
249
+ /**
250
+ * Находит все ссылки и соответствующие им секции
251
+ * @private
252
+ */
165
253
  _initializeTargetsAndObservables() {
166
- this._targetLinks = new Map();
167
- this._observableSections = new Map();
168
-
169
- const targetLinks = Selectors.findAll(SELECTOR_TARGET_LINKS, this._params.target);
170
-
171
- for (const anchor of targetLinks) {
172
- if (!anchor.hash || isDisabled(anchor)) {
173
- continue
174
- }
175
-
176
- const observableSection = Selectors.find(decodeURI(anchor.hash));
177
-
178
- if (isVisible(observableSection)) {
179
- this._targetLinks.set(decodeURI(anchor.hash), anchor)
180
- this._observableSections.set(anchor.hash, observableSection)
254
+ this._targetLinks.clear();
255
+ this._observableSections.clear();
256
+
257
+ const links = Selectors.findAll(SELECTOR_TARGET_LINKS, this._params.target);
258
+ for (const link of links) {
259
+ const hash = link.hash;
260
+ if (!hash || isDisabled(link)) continue;
261
+
262
+ const section = Selectors.find(decodeURI(hash));
263
+ if (isVisible(section)) {
264
+ this._targetLinks.set(decodeURI(hash), link);
265
+ this._observableSections.set(hash, section);
181
266
  }
182
267
  }
183
268
  }
184
269
 
270
+ /**
271
+ * Активирует элемент и запускает событие
272
+ * @param {HTMLElement|null} target — элемент ссылки, который нужно активировать
273
+ * @private
274
+ */
185
275
  _process(target) {
186
- if (this._activeTarget === target) {
187
- return
188
- }
276
+ if (this._activeTarget === target) return;
189
277
 
190
- this._clearActiveClass(this._params.target)
191
- this._activeTarget = target
192
- target.classList.add(CLASS_NAME_ACTIVE)
193
- this._activateParents(target)
278
+ this._clearActiveClass(this._params.target);
279
+ this._activeTarget = target;
194
280
 
195
- EventHandler.trigger(this._element, EVENT_ACTIVATE, { relatedTarget: target })
281
+ if (target) {
282
+ target.classList.add(CLASS_NAME_ACTIVE);
283
+ this._activateParents(target);
284
+ EventHandler.trigger(this._element, EVENT_ACTIVATE, { relatedTarget: target });
285
+ }
196
286
  }
197
287
 
288
+ /**
289
+ * Активирует родительские элементы (навигация, dropdown)
290
+ * @param {HTMLElement} target — активная ссылка
291
+ * @private
292
+ */
198
293
  _activateParents(target) {
199
294
  if (target.classList.contains(CLASS_NAME_DROPDOWN_ITEM)) {
200
- Selectors.find(SELECTOR_DROPDOWN_TOGGLE, target.closest(SELECTOR_DROPDOWN))
201
- .classList.add(CLASS_NAME_ACTIVE)
202
- return
295
+ const dropdownToggle = Selectors.find(SELECTOR_DROPDOWN_TOGGLE, target.closest(SELECTOR_DROPDOWN));
296
+ if (dropdownToggle) dropdownToggle.classList.add(CLASS_NAME_ACTIVE);
297
+ return;
203
298
  }
204
299
 
205
- for (const listGroup of Selectors.parents(target, SELECTOR_NAV_LIST_GROUP)) {
206
- for (const item of Selectors.prev(listGroup, SELECTOR_LINK_ITEMS)) {
207
- item.classList.add(CLASS_NAME_ACTIVE)
300
+ // Активируем предыдущие элементы в nav/list-group
301
+ for (const parentGroup of Selectors.parents(target, SELECTOR_NAV_LIST_GROUP)) {
302
+ for (const sibling of Selectors.prev(parentGroup, SELECTOR_LINK_ITEMS)) {
303
+ sibling.classList.add(CLASS_NAME_ACTIVE);
208
304
  }
209
305
  }
210
306
  }
211
307
 
308
+ /**
309
+ * Убирает активный класс со всех элементов
310
+ * @param {HTMLElement} parent — контейнер для очистки
311
+ * @private
312
+ */
212
313
  _clearActiveClass(parent) {
213
- parent.classList.remove(CLASS_NAME_ACTIVE)
314
+ parent.classList.remove(CLASS_NAME_ACTIVE);
214
315
 
215
- const activeNodes = Selectors.findAll(`${SELECTOR_TARGET_LINKS}.${CLASS_NAME_ACTIVE}`, parent);
216
- for (const node of activeNodes) {
217
- node.classList.remove(CLASS_NAME_ACTIVE)
316
+ const activeLinks = Selectors.findAll(`${SELECTOR_TARGET_LINKS}.${CLASS_NAME_ACTIVE}`, parent);
317
+ for (const link of activeLinks) {
318
+ link.classList.remove(CLASS_NAME_ACTIVE);
218
319
  }
219
320
  }
220
321
  }
221
322
 
323
+ /**
324
+ * Инициализация через data-атрибуты при загрузке DOM
325
+ */
222
326
  EventHandler.on(window, EVENT_LOAD_DATA_API, () => {
223
327
  for (const spy of Selectors.findAll(SELECTOR_DATA_SPY)) {
224
- VGSpy.getOrCreateInstance(spy)
328
+ VGSpy.getOrCreateInstance(spy);
225
329
  }
226
- })
330
+ });
227
331
 
228
332
  export default VGSpy;