vgapp 0.8.1 → 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.
@@ -0,0 +1,180 @@
1
+ # VGSelect — Кастомный `<select>` с расширенными возможностями
2
+
3
+ `VGSelect` — это продвинутый JavaScript-модуль для замены стандартного HTML-элемента `<select>` на полностью кастомизируемый компонент с поддержкой поиска, мультивыбора, динамической загрузки данных, i18n, пагинации и автоматического обновления через `MutationObserver`.
4
+
5
+ ---
6
+
7
+ ## ✅ Основные возможности
8
+
9
+ | Функция | Поддержка |
10
+ |--------|---------|
11
+ | 🔹 Кастомный дизайн | ✅ |
12
+ | 🔹 Поиск (локальный и удалённый) | ✅ |
13
+ | 🔹 Поддержка мультивыбора (`multiple`) | ✅ |
14
+ | 🔹 Динамическая загрузка данных (AJAX) | ✅ |
15
+ | 🔹 Пагинация и "Загрузить ещё" | ✅ |
16
+ | 🔹 i18n (многоязычность) | ✅ |
17
+ | 🔹 Автообновление при изменении `<select>` | ✅ (`MutationObserver`) |
18
+ | 🔹 Обработка `optgroup` | ✅ |
19
+ | 🔹 Поддержка `disabled`, `required`, `placeholder` | ✅ |
20
+ | 🔹 Полная доступность (ARIA) | ✅ |
21
+ | 🔹 Коллбэки и события | ✅ |
22
+
23
+ ---
24
+
25
+ ## 📦 Установка и инициализация
26
+
27
+ ### HTML
28
+ ```html
29
+ <select id="country" class="vg-select w-100" name="country" data-max="10" required>
30
+ <option value="2" data-price="1">Россия</option>
31
+ <option value="3" data-price="2">Узбекистан</option>
32
+ <option value="4" data-price="3">Казахстан</option>
33
+ <option value="5" data-price="4" selected>Белоруссия</option>
34
+ <option value="6" data-price="5" disabled>Китай</option>
35
+ </select>
36
+ ```
37
+ ### JavaScript
38
+ ```js
39
+ js import VGSelect from './app/modules/vgselect/js/vgselect';
40
+ // Инициализация
41
+ VGSelect.init(document.getElementById('mySelect'), {
42
+ lang: 'ru',
43
+ placeholder: 'Выберите значение',
44
+ search: {
45
+ enabled: true,
46
+ remote: true,
47
+ route: '/api/search',
48
+ delay: 300,
49
+ minTerm: 1,
50
+ pagination: true,
51
+ loadMoreText: 'Загрузить ещё'
52
+ },
53
+ onInit: (element) => console.log('VGSelect инициализирован'),
54
+ onChange: (element, data) => console.log('Значение изменено:', data)
55
+ });
56
+ ```
57
+
58
+ ---
59
+
60
+ ## 🔧 Параметры инициализации
61
+
62
+ | Параметр | Тип | Описание |
63
+ |--------|-----|---------|
64
+ | `lang` | `string` | Язык интерфейса (поддерживается i18n). По умолчанию: `ru` |
65
+ | `placeholder` | `string` | Текст плейсхолдера |
66
+ | `search.enabled` | `boolean` | Включить поле поиска |
67
+ | `search.remote` | `boolean` | Поиск через AJAX |
68
+ | `search.route` | `string` | URL для удалённого поиска |
69
+ | `search.delay` | `number` | Задержка перед запросом (мс) |
70
+ | `search.minTerm` | `number` | Минимальная длина запроса |
71
+ | `search.pagination` | `boolean` | Включить пагинацию |
72
+ | `search.pageParam` | `string` | Название параметра страницы в URL (`page`) |
73
+ | `search.termParam` | `string` | Название параметра поиска (`q`) |
74
+ | `search.perPage` | `number` | Количество элементов на страницу |
75
+ | `search.loadMoreText` | `string` | Текст кнопки "Загрузить ещё" |
76
+ | `onInit` | `function` | Коллбэк при инициализации |
77
+ | `onShow` | `function` | Коллбэк при открытии |
78
+ | `onHide` | `function` | Коллбэк при закрытии |
79
+ | `onChange` | `function` | Коллбэк при изменении значения |
80
+ | `onSelect` | `function` | Коллбэк при выборе элемента |
81
+ | `onClear` | `function` | Коллбэк при очистке выбора |
82
+ | `onLoadNext` | `function` | Коллбэк при загрузке следующей страницы |
83
+ | `onSearch` | `function` | Коллбэк при вводе в поиске |
84
+
85
+ ---
86
+
87
+ ## 🌐 i18n (международные сообщения)
88
+
89
+ Модуль поддерживает локализацию через:
90
+ - `lang_titles(lang, component)` — заголовки
91
+ - `lang_messages(lang, component)` — сообщения (например, "Загрузка...")
92
+ - `lang_buttons(lang, component)` — кнопки (например, "Загрузить ещё")
93
+
94
+ Поддерживаемые языки: `ru`, `en` и др. (настраивается в ядре).
95
+
96
+ ---
97
+
98
+ ## 🔁 Динамическая загрузка данных
99
+
100
+ Если включён `search.remote`, компонент отправляет запрос на указанный `route` с параметрами:
101
+
102
+ ```html
103
+ ?q=searchTerm&page=1&per_page=20
104
+ ```
105
+
106
+ Ожидается JSON-ответ:
107
+ ```json
108
+ {
109
+ "results":
110
+ [
111
+ { "id": "1", "text": "Опция 1" },
112
+ { "id": "2", "text": "Опция 2" }
113
+ ],
114
+ "pagination": {
115
+ "current_page": 1,
116
+ "total_pages": 5
117
+ }
118
+ }
119
+ ```
120
+ ---
121
+
122
+ ## 🔍 Поиск
123
+
124
+ - **Локальный**: фильтрация уже существующих опций.
125
+ - **Удалённый**: AJAX-запрос с пагинацией.
126
+ - Поддержка кнопки **"Загрузить ещё"** при включённой пагинации.
127
+
128
+ ---
129
+
130
+ ## 🔄 Автообновление (MutationObserver)
131
+
132
+ Компонент автоматически обновляется при:
133
+ - Изменении `option` или `optgroup` в исходном `<select>`
134
+ - Изменении атрибутов: `disabled`, `required`, `hidden`, `style`
135
+ - Добавлении/удалении опций через DOM
136
+
137
+ ---
138
+
139
+ ## 🎯 API методы
140
+
141
+ | Метод | Описание |
142
+ |------|---------|
143
+ | `VGSelect.init(select, params, rebuild)` | Инициализация |
144
+ | `VGSelect.destroy(select)` | Удаление компонента |
145
+ | `VGSelect.updateUI(select)` | Обновление отображаемого значения |
146
+ | `VGSelect.changeSelector(select, value, data)` | Программная установка значения |
147
+ | `VGSelect.addOptions(select, data, { preserve })` | Добавление опций (удобно при AJAX) |
148
+ | `instance.show()` | Открыть выпадающий список |
149
+ | `instance.hide()` | Закрыть выпадающий список |
150
+ | `instance.toggle()` | Переключить состояние |
151
+
152
+ ---
153
+
154
+ ## 📣 События
155
+
156
+ | Событие | Описание |
157
+ |--------|---------|
158
+ | `vg.select.init` | Инициализация завершена |
159
+ | `vg.select.show` | Начало открытия |
160
+ | `vg.select.shown` | Открытие завершено |
161
+ | `vg.select.hide` | Начало закрытия |
162
+ | `vg.select.hidden` | Закрытие завершено |
163
+ | `vg.select.change` | Значение изменено |
164
+ | `vg.select.select` | Элемент выбран |
165
+ | `vg.select.clear` | Выбор очищен |
166
+ | `vg.select.rebuild` | Список перестроен (после поиска) |
167
+ | `vg.select.loadNext` | Загружена следующая страница |
168
+ | `vg.select.error` | Ошибка (AJAX, данные и т.д.) |
169
+
170
+ ---
171
+
172
+ ## 📝 Лицензия
173
+
174
+ MIT. Свободно использовать и модифицировать.
175
+
176
+ ---
177
+
178
+ 📌 *Разработано в рамках фронтенд-системы VG Modules.*
179
+ > 🚀 Автор: VEGAS STUDIO (vegas-dev.com)
180
+ > 📍 Поддерживается в проектах VEGAS
@@ -78,3 +78,23 @@ $select-search-map: (
78
78
  background-color: #f2f2f2,
79
79
  color: #000000
80
80
  ) !default;
81
+
82
+ $select-tags-map: (
83
+ gap: 4px,
84
+ width: 100%,
85
+ min-height: 32px,
86
+ padding: 2px 0
87
+ ) !default;
88
+
89
+ $select-tag-map: (
90
+ gap: 4px,
91
+ background: #0d6efd,
92
+ color: white,
93
+ padding: 2px 6px,
94
+ border-radius: 4px,
95
+ font-size: 14px,
96
+ remove-width: 12px,
97
+ remove-height: 12px,
98
+ remove-stroke: white,
99
+ remove-stroke-width: 2
100
+ ) !default;
@@ -18,8 +18,8 @@ select {
18
18
  top: 0;
19
19
  opacity: 0;
20
20
  z-index: -1000;
21
- width: 100%;
22
- height: 100%;
21
+ width: 0;
22
+ height: 0;
23
23
  display: inline-block;
24
24
  visibility: hidden;
25
25
  }
@@ -34,6 +34,8 @@ select {
34
34
  @include mix-vars('select-optgroup-hover', $select-optgroup-hover-map);
35
35
  @include mix-vars('select-list-hover', $select-list-hover-map);
36
36
  @include mix-vars('select-search', $select-search-map);
37
+ @include mix-vars('select-tags', $select-tags-map);
38
+ @include mix-vars('select-tag', $select-tag-map);
37
39
  --vg-select-list-max-height: #{$select-list-max-height};
38
40
  --vg-select-list-scrollbar-width: #{$select-list-scrollbar-width};
39
41
  --vg-select-list-scrollbar-bg: #{$select-list-scrollbar-bg};
@@ -46,6 +48,44 @@ select {
46
48
  }
47
49
  }
48
50
 
51
+ &-tags {
52
+ display: flex;
53
+ flex-wrap: wrap;
54
+ gap: var(--vg-select-tags-gap);
55
+ align-items: center;
56
+ width: var(--vg-select-tags-width);
57
+ min-height: var(--vg-select-tags-min-height);
58
+ padding: var(--vg-select-tags-padding);
59
+ }
60
+
61
+ &-tag {
62
+ display: flex;
63
+ align-items: center;
64
+ gap: var(--vg-select-tag-gap);
65
+ background: var(--vg-select-tag-background);
66
+ color: var(--vg-select-tag-color);
67
+ padding: var(--vg-select-tag-padding);
68
+ border-radius: var(--vg-select-tag-border-radius);
69
+ font-size: var(--vg-select-tag-font-size);
70
+
71
+ &-remove {
72
+ cursor: pointer;
73
+ opacity: 0.7;
74
+ width: var(--vg-select-tag-remove-width);
75
+ height: var(--vg-select-tag-remove-height);
76
+ stroke: var(--vg-select-tag-remove-stroke);
77
+ stroke-width: var(--vg-select-tag-remove-stroke-width);
78
+
79
+ &:hover {
80
+ opacity: 1;
81
+ }
82
+ }
83
+ }
84
+
85
+ &-multiple-input {
86
+ font: inherit;
87
+ }
88
+
49
89
  &-current {
50
90
  @each $key, $value in $select-current-map {
51
91
  #{$key}: var(--vg-select-current-#{$key})
@@ -1,35 +1,85 @@
1
1
  import BaseModule from "../../base-module";
2
- import {isDisabled, isVisible, mergeDeepObject} from "../../../utils/js/functions";
2
+ import { isDisabled, isVisible, mergeDeepObject } from "../../../utils/js/functions";
3
3
  import EventHandler from "../../../utils/js/dom/event";
4
- import {dismissTrigger} from "../../module-fn";
4
+ import { dismissTrigger } from "../../module-fn";
5
5
  import Selectors from "../../../utils/js/dom/selectors";
6
6
  import Backdrop from "../../../utils/js/components/backdrop";
7
7
  import ScrollBarHelper from "../../../utils/js/components/scrollbar";
8
8
 
9
-
10
9
  /**
11
- * Constants
10
+ * @constant {string} NAME - Имя модуля.
12
11
  */
13
12
  const NAME = 'sidebar';
13
+
14
+ /**
15
+ * @constant {string} NAME_KEY - Пространство имён для событий.
16
+ */
14
17
  const NAME_KEY = 'vg.sidebar';
15
- const SELECTOR_DATA_TOGGLE= '[data-vg-toggle="sidebar"]';
16
18
 
19
+ /**
20
+ * @constant {string} SELECTOR_DATA_TOGGLE - Селектор для элементов активации сайдбара.
21
+ */
22
+ const SELECTOR_DATA_TOGGLE = '[data-vg-toggle="sidebar"]';
23
+
24
+ /**
25
+ * @constant {string} CLASS_NAME_SHOW - Класс, отвечающий за отображение сайдбара.
26
+ */
17
27
  const CLASS_NAME_SHOW = 'show';
18
- const CLASS_NAME_OPEN = 'vg-sidebar-open';
19
28
 
20
- const EVENT_KEY_HIDE = `${NAME_KEY}.hide`;
21
- const EVENT_KEY_HIDDEN = `${NAME_KEY}.hidden`;
22
- const EVENT_KEY_SHOW = `${NAME_KEY}.show`;
23
- const EVENT_KEY_SHOWN = `${NAME_KEY}.shown`;
24
- const EVENT_KEY_LOADED = `${NAME_KEY}.loaded`;
29
+ /**
30
+ * @constant {string} CLASS_NAME_OPEN - Класс, добавляемый к body при открытом сайдбаре.
31
+ */
32
+ const CLASS_NAME_OPEN = 'vg-sidebar-open';
25
33
 
26
- const EVENT_KEY_KEYDOWN_DISMISS = `keydown.dismiss.${NAME_KEY}`;
27
- const EVENT_KEY_HIDE_PREVENTED = `hidePrevented.${NAME_KEY}`;
28
- const EVENT_KEY_CLICK_DATA_API = `click.${NAME_KEY}.data.api`;
29
- const EVENT_KEY_POPSTATE_DATA_API = `popstate.${NAME_KEY}.data.api`;
30
- const EVENT_KEY_DOM_LOADED_DATA_API = `DOMContentLoaded.${NAME_KEY}.data.api`;
34
+ /**
35
+ * @constant {Object} EVENT_KEYS - Объект с ключами событий для модуля.
36
+ */
37
+ const EVENT_KEYS = {
38
+ HIDE: `${NAME_KEY}.hide`,
39
+ HIDDEN: `${NAME_KEY}.hidden`,
40
+ SHOW: `${NAME_KEY}.show`,
41
+ SHOWN: `${NAME_KEY}.shown`,
42
+ LOADED: `${NAME_KEY}.loaded`,
43
+ KEYDOWN_DISMISS: `keydown.dismiss.${NAME_KEY}`,
44
+ HIDE_PREVENTED: `hidePrevented.${NAME_KEY}`,
45
+ CLICK_DATA_API: `click.${NAME_KEY}.data.api`,
46
+ POPSTATE_DATA_API: `popstate.${NAME_KEY}.data.api`,
47
+ DOM_LOADED_DATA_API: `DOMContentLoaded.${NAME_KEY}.data.api`,
48
+ };
31
49
 
50
+ /**
51
+ * Класс VGSidebar реализует функциональность боковой панели (сайдбара) с поддержкой:
52
+ * - открытия/закрытия по клику или хэшу
53
+ * - поддержки backdrop
54
+ * - блокировки скролла при открытии
55
+ * - анимаций
56
+ * - AJAX-загрузки контента
57
+ *
58
+ * @extends BaseModule
59
+ */
32
60
  class VGSidebar extends BaseModule {
61
+ /**
62
+ * Создаёт экземпляр VGSidebar.
63
+ *
64
+ * @param {HTMLElement} element - Основной элемент сайдбара.
65
+ * @param {Object} params - Параметры конфигурации.
66
+ * @param {boolean} [params.backdrop=true] - Показывать подложку.
67
+ * @param {boolean} [params.overflow=true] - Блокировать скролл при открытии.
68
+ * @param {boolean} [params.keyboard=true] - Закрывать по клавише Escape.
69
+ * @param {boolean} [params.hash=false] - Поддержка открытия по хэшу URL.
70
+ * @param {Object} [params.animation] - Настройки анимации.
71
+ * @param {boolean} [params.animation.enable=false] - Включить анимацию.
72
+ * @param {string} [params.animation.in='animate__rollIn'] - Класс входной анимации.
73
+ * @param {string} [params.animation.out='animate__rollOut'] - Класс выходной анимации.
74
+ * @param {number} [params.animation.delay=800] - Задержка перед закрытием (мс).
75
+ * @param {Object} [params.ajax] - Параметры AJAX-загрузки.
76
+ * @param {string} [params.ajax.route=''] - URL для загрузки.
77
+ * @param {string} [params.ajax.target=''] - Селектор цели для вставки.
78
+ * @param {string} [params.ajax.method='get'] - HTTP-метод.
79
+ * @param {boolean} [params.ajax.loader=false] - Показывать лоадер.
80
+ * @param {boolean} [params.ajax.once=false] - Загружать только один раз.
81
+ * @param {boolean} [params.ajax.output=true] - Вставлять ответ в DOM.
82
+ */
33
83
  constructor(element, params = {}) {
34
84
  super(element, params);
35
85
 
@@ -54,79 +104,105 @@ class VGSidebar extends BaseModule {
54
104
  }
55
105
  }, params));
56
106
 
107
+ this._scrollBar = new ScrollBarHelper();
108
+ this._params.animation.delay = this._params.animation.enable ? this._params.animation.delay : 0;
109
+
57
110
  this._addEventListeners();
58
111
  this._dismissElement();
59
-
60
- this._scrollBar = new ScrollBarHelper();
61
- this._params.animation.delay = !this._params.animation.enable ? 0 : this._params.animation.delay;
62
- this._animation(this._element, VGSidebar.NAME_KEY, this._params.animation);
112
+ this._animation(this._element, NAME_KEY, this._params.animation);
63
113
  }
64
114
 
115
+ /**
116
+ * Статическое свойство: имя модуля.
117
+ * @returns {string}
118
+ */
65
119
  static get NAME() {
66
120
  return NAME;
67
121
  }
68
122
 
123
+ /**
124
+ * Статическое свойство: ключ для событий и данных.
125
+ * @returns {string}
126
+ */
69
127
  static get NAME_KEY() {
70
- return NAME_KEY
128
+ return NAME_KEY;
71
129
  }
72
130
 
131
+ /**
132
+ * Переключает состояние сайдбара (открыть/закрыть).
133
+ *
134
+ * @param {HTMLElement} [relatedTarget] - Элемент, инициировавший открытие.
135
+ * @returns {void}
136
+ */
73
137
  toggle(relatedTarget) {
74
- return !this._isShown() ? this.show(relatedTarget) : this.hide();
138
+ return this._isShown() ? this.hide() : this.show(relatedTarget);
75
139
  }
76
140
 
141
+ /**
142
+ * Открывает сайдбар.
143
+ *
144
+ * @param {HTMLElement} [relatedTarget] - Элемент, инициировавший открытие.
145
+ * @returns {void}
146
+ */
77
147
  show(relatedTarget) {
78
- const _this = this;
79
- if (isDisabled(_this._element)) return;
148
+ if (isDisabled(this._element)) return;
80
149
 
81
- if (relatedTarget) _this._params = _this._getParams(relatedTarget, _this._params);
150
+ if (relatedTarget) {
151
+ this._params = this._getParams(relatedTarget, this._params);
152
+ }
82
153
 
83
- _this._route(function (status, data) {
84
- EventHandler.trigger(_this._element, EVENT_KEY_LOADED, {stats: status, data: data});
154
+ // Событие загрузки (может использоваться для AJAX)
155
+ this._route((status, data) => {
156
+ EventHandler.trigger(this._element, EVENT_KEYS.LOADED, { stats: status, data });
85
157
  });
86
158
 
87
- const showEvent = EventHandler.trigger(_this._element, EVENT_KEY_SHOW, { relatedTarget })
159
+ const showEvent = EventHandler.trigger(this._element, EVENT_KEYS.SHOW, { relatedTarget });
88
160
  if (showEvent.defaultPrevented) return;
89
161
 
90
- if (_this._params.backdrop) {
162
+ if (this._params.backdrop) {
91
163
  Backdrop.show();
92
164
  }
93
165
 
94
- if (_this._params.overflow) {
166
+ if (this._params.overflow) {
95
167
  this._scrollBar.hide();
96
168
  }
97
169
 
98
170
  if (this._params.hash) {
99
- window.history.pushState(null, "vg-sidebar-open", "#" + this._element.id);
100
-
101
- EventHandler.on(window, EVENT_KEY_POPSTATE_DATA_API, () => {
102
- this.hide();
103
- });
171
+ window.history.pushState(null, '', `#${this._element.id}`);
172
+ EventHandler.on(window, EVENT_KEYS.POPSTATE_DATA_API, () => this.hide());
104
173
  }
105
174
 
106
- _this._element.classList.add(CLASS_NAME_SHOW);
175
+ this._element.classList.add(CLASS_NAME_SHOW);
107
176
  document.body.classList.add(CLASS_NAME_OPEN);
108
177
 
109
- const completeCallBack = () => {
110
- EventHandler.on(Selectors.find('.vg-backdrop'), 'mousedown.vg.backdrop', function () {
111
- _this.hide();
112
- });
178
+ const completeCallback = () => {
179
+ const backdrop = Selectors.find('.vg-backdrop');
180
+ if (backdrop) {
181
+ EventHandler.on(backdrop, 'mousedown.vg.backdrop', () => this.hide());
182
+ }
183
+ EventHandler.trigger(this._element, EVENT_KEYS.SHOWN, { relatedTarget });
184
+ };
113
185
 
114
- EventHandler.trigger(_this._element, EVENT_KEY_SHOWN, { relatedTarget });
115
- }
116
- _this._queueCallback(completeCallBack, _this._element, true, 50)
186
+ this._queueCallback(completeCallback, this._element, true, 50);
117
187
  }
118
188
 
189
+ /**
190
+ * Закрывает сайдбар.
191
+ *
192
+ * @param {boolean} [isLeaveBackDrop=false] - Не убирать подложку.
193
+ * @returns {void}
194
+ */
119
195
  hide(isLeaveBackDrop = false) {
120
196
  if (isDisabled(this._element)) return;
121
197
 
122
- const hideEvent = EventHandler.trigger(this._element, EVENT_KEY_HIDE);
198
+ const hideEvent = EventHandler.trigger(this._element, EVENT_KEYS.HIDE);
123
199
  if (hideEvent.defaultPrevented) return;
124
200
 
125
201
  document.body.classList.remove(CLASS_NAME_OPEN);
202
+ this._element.classList.remove(CLASS_NAME_SHOW);
126
203
 
127
204
  setTimeout(() => {
128
- this._element.setAttribute('aria-expanded', false);
129
- this._element.classList.remove(CLASS_NAME_SHOW);
205
+ this._element.setAttribute('aria-expanded', 'false');
130
206
 
131
207
  const completeCallback = () => {
132
208
  if (!isLeaveBackDrop) {
@@ -136,88 +212,122 @@ class VGSidebar extends BaseModule {
136
212
  this._scrollBar.reset();
137
213
  }
138
214
  });
139
- }
140
-
141
- if (this._params.overflow) {
215
+ } else if (this._params.overflow) {
142
216
  this._scrollBar.reset();
143
217
  }
144
218
 
145
219
  if (this._params.hash) {
146
- history.pushState("", document.title, window.location.pathname + window.location.search);
220
+ history.replaceState('', document.title, window.location.pathname + window.location.search);
147
221
  }
148
222
 
149
- EventHandler.trigger(this._element, EVENT_KEY_HIDDEN);
223
+ EventHandler.trigger(this._element, EVENT_KEYS.HIDDEN);
150
224
  }
151
- }
225
+ };
226
+
152
227
  this._queueCallback(completeCallback, this._element, true);
153
228
  }, this._params.animation.delay);
154
229
  }
155
230
 
231
+ /**
232
+ * Очищает ресурсы модуля.
233
+ * @override
234
+ */
156
235
  dispose() {
157
236
  super.dispose();
237
+ EventHandler.off(this._element, EVENT_KEYS.HIDE);
238
+ EventHandler.off(window, EVENT_KEYS.POPSTATE_DATA_API);
239
+ this._scrollBar.reset();
158
240
  }
159
241
 
242
+ /**
243
+ * Проверяет, открыт ли сайдбар.
244
+ * @returns {boolean}
245
+ * @private
246
+ */
160
247
  _isShown() {
161
248
  return this._element.classList.contains(CLASS_NAME_SHOW);
162
249
  }
163
250
 
251
+ /**
252
+ * Добавляет глобальные слушатели событий (например, Escape).
253
+ * @private
254
+ */
164
255
  _addEventListeners() {
165
- EventHandler.on(document, EVENT_KEY_KEYDOWN_DISMISS, event => {
256
+ EventHandler.on(document, EVENT_KEYS.KEYDOWN_DISMISS, (event) => {
166
257
  if (event.key !== 'Escape') return;
167
258
 
168
259
  if (this._params.keyboard) {
169
260
  this.hide();
170
- return;
261
+ } else {
262
+ EventHandler.trigger(this._element, EVENT_KEYS.HIDE_PREVENTED);
171
263
  }
172
-
173
- EventHandler.trigger(this._element, EVENT_KEY_HIDE_PREVENTED)
174
264
  });
175
265
  }
266
+
267
+ /**
268
+ * Инициализирует поведение закрытия по клику вне (через `dismissTrigger`).
269
+ * @private
270
+ */
271
+ _dismissElement() {
272
+ dismissTrigger(this);
273
+ }
274
+
275
+ /**
276
+ * Заглушка для возможной AJAX-логики. Может быть переопределена.
277
+ * @param {Function} callback - Колбэк после загрузки.
278
+ * @private
279
+ */
280
+ _route(callback) {
281
+ // Здесь может быть реализация AJAX-загрузки
282
+ callback(true, null);
283
+ }
176
284
  }
177
285
 
286
+ // Автоматическая инициализация по data-атрибутам
178
287
  dismissTrigger(VGSidebar);
179
288
 
180
289
  /**
181
- * Data API implementation
290
+ * Реализация Data API: открытие сайдбара по data-атрибуту.
182
291
  */
183
- EventHandler.on(document, EVENT_KEY_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (event) {
292
+ EventHandler.on(document, EVENT_KEYS.CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (event) {
184
293
  const target = Selectors.getElementFromSelector(this);
294
+ if (!target) return;
185
295
 
186
296
  if (['A', 'AREA'].includes(this.tagName)) {
187
- event.preventDefault()
297
+ event.preventDefault();
188
298
  }
189
299
 
190
- if (isDisabled(this)) {
191
- return
192
- }
300
+ if (isDisabled(this)) return;
301
+
302
+ this.setAttribute('aria-expanded', 'true');
193
303
 
194
- this.setAttribute('aria-expanded', true);
195
- EventHandler.one(target, EVENT_KEY_HIDDEN, () => {
196
- this.setAttribute('aria-expanded', false);
197
- })
304
+ // Сбрасываем атрибут после закрытия
305
+ EventHandler.one(target, EVENT_KEYS.HIDDEN, () => {
306
+ this.setAttribute('aria-expanded', 'false');
307
+ });
198
308
 
199
- const alreadyOpen = Selectors.find('.vg-sidebar.show')
309
+ // Закрываем уже открытый сайдбар
310
+ const alreadyOpen = Selectors.find('.vg-sidebar.show');
200
311
  if (alreadyOpen && alreadyOpen !== target) {
201
- VGSidebar.getInstance(alreadyOpen).hide()
312
+ VGSidebar.getInstance(alreadyOpen).hide();
202
313
  }
203
314
 
204
- const data = VGSidebar.getOrCreateInstance(target)
205
- data.toggle(this);
315
+ const instance = VGSidebar.getOrCreateInstance(target);
316
+ instance.toggle(this);
206
317
  });
207
318
 
208
- EventHandler.on(document, EVENT_KEY_DOM_LOADED_DATA_API, function () {
209
- let targetHash = window.location.hash.slice(1);
210
- if (targetHash) {
211
- let target = Selectors.find('#' + targetHash);
212
- if (target && target.classList.contains('vg-sidebar')) {
213
- if (isDisabled(target)) {
214
- return;
215
- }
216
-
217
- const data = VGSidebar.getOrCreateInstance(target)
218
- data.toggle();
219
- }
319
+ /**
320
+ * Открытие сайдбара по хэшу при загрузке страницы.
321
+ */
322
+ EventHandler.on(document, EVENT_KEYS.DOM_LOADED_DATA_API, function () {
323
+ const hash = window.location.hash.slice(1);
324
+ if (!hash) return;
325
+
326
+ const target = Selectors.find(`#${hash}`);
327
+ if (target && target.classList.contains('vg-sidebar') && !isDisabled(target)) {
328
+ const instance = VGSidebar.getOrCreateInstance(target);
329
+ instance.toggle();
220
330
  }
221
- })
331
+ });
222
332
 
223
- export default VGSidebar;
333
+ export default VGSidebar;