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.
@@ -5,26 +5,76 @@ import {execute, isDisabled, makeRandomString, mergeDeepObject} from "../../../u
5
5
  import Selectors from "../../../utils/js/dom/selectors";
6
6
 
7
7
  /**
8
- * Constants
8
+ * @constant {string} NAME - Имя модуля.
9
9
  */
10
10
  const NAME = 'toast';
11
+
12
+ /**
13
+ * @constant {string} NAME_KEY - Пространство имён для событий.
14
+ */
11
15
  const NAME_KEY = 'vg.toast';
12
- const SELECTOR_DATA_TOGGLE= '[data-vg-toggle="toast"]';
13
16
 
14
- const CLASS_NAME_OPEN = 'vg-toast-open';
15
- const CLASS_NAME_SHOW = 'show';
16
- const CLASS_NAME_SHOWN = 'shown';
17
+ /**
18
+ * @constant {string} SELECTOR_DATA_TOGGLE - Селектор для активации через data-атрибут.
19
+ */
20
+ const SELECTOR_DATA_TOGGLE = '[data-vg-toggle="toast"]';
21
+
22
+ /**
23
+ * @constant {string} CLASS_NAME_OPEN - Класс, добавляемый к body при открытии любого тоста.
24
+ */
25
+ const CLASS_NAME_OPEN = 'vg-toast-open';
17
26
 
27
+ /**
28
+ * @constant {string} CLASS_NAME_SHOW - Класс, показывающий, что тост видим.
29
+ */
30
+ const CLASS_NAME_SHOW = 'show';
31
+
32
+ /**
33
+ * @constant {string} CLASS_NAME_SHOWN - Класс, добавляемый после завершения анимации появления.
34
+ */
35
+ const CLASS_NAME_SHOWN = 'shown';
36
+
37
+ // События
18
38
  const EVENT_KEY_HIDE = `${NAME_KEY}.hide`;
19
39
  const EVENT_KEY_HIDDEN = `${NAME_KEY}.hidden`;
20
40
  const EVENT_KEY_SHOW = `${NAME_KEY}.show`;
21
41
  const EVENT_KEY_SHOWN = `${NAME_KEY}.shown`;
22
42
  const EVENT_KEY_LOADED = `${NAME_KEY}.loaded`;
23
-
24
43
  const EVENT_KEY_KEYDOWN_DISMISS = `keydown.dismiss.${NAME_KEY}`;
25
44
  const EVENT_KEY_HIDE_PREVENTED = `hidePrevented.${NAME_KEY}`;
26
45
  const EVENT_KEY_CLICK_DATA_API = `click.${NAME_KEY}.data.api`;
27
46
 
47
+ /**
48
+ * @typedef {Object} ToastParams
49
+ * @property {boolean} static - Сохранять ли тост в DOM после скрытия.
50
+ * @property {string} placement - Расположение: 'top left', 'bottom center' и т.д.
51
+ * @property {boolean} autohide - Автоматически скрывать.
52
+ * @property {number} delay - Задержка перед авто-скрытием (мс).
53
+ * @property {boolean} enableClickToast - Закрывать по клику на тост.
54
+ * @property {boolean} enableButtonClose - Добавить кнопку закрытия.
55
+ * @property {boolean} keyboard - Закрывать по Esc.
56
+ * @property {string} theme - Тема: 'dark', 'light' и т.д.
57
+ * @property {Object} stack - Настройки стека уведомлений.
58
+ * @property {boolean} stack.enable - Разрешить стек.
59
+ * @property {number} stack.max - Макс. количество тостов одновременно.
60
+ * @property {Object} animation - Анимация.
61
+ * @property {boolean} animation.enable - Включить анимацию.
62
+ * @property {string} animation.in - Анимация входа (Animate.css).
63
+ * @property {string} animation.out - Анимация выхода.
64
+ * @property {number} animation.delay - Длительность анимации.
65
+ * @property {Object} ajax - Настройки AJAX.
66
+ * @property {string} ajax.route - URL для загрузки.
67
+ * @property {string} ajax.target - Селектор контейнера.
68
+ * @property {string} ajax.method - HTTP-метод.
69
+ * @property {boolean} ajax.loader - Показывать лоадер.
70
+ * @property {boolean} ajax.once - Загружать один раз.
71
+ * @property {boolean} ajax.output - Выводить результат.
72
+ */
73
+
74
+ /**
75
+ * Параметры по умолчанию
76
+ * @type {ToastParams}
77
+ */
28
78
  const defaultParams = {
29
79
  static: true,
30
80
  placement: 'bottom center',
@@ -54,59 +104,95 @@ const defaultParams = {
54
104
  }
55
105
  };
56
106
 
107
+ /**
108
+ * Класс VGToast — модуль уведомлений (тосты)
109
+ * Поддерживает стек, анимации, авто-скрытие, AJAX-контент, горячие клавиши.
110
+ */
57
111
  class VGToast extends BaseModule {
112
+ /**
113
+ * Создаёт экземпляр VGToast
114
+ * @param {Element} element - HTML-элемент тоста.
115
+ * @param {Partial<ToastParams>} params - Пользовательские параметры.
116
+ */
58
117
  constructor(element, params = {}) {
59
118
  super(element, params);
60
119
 
120
+ /** @private */
61
121
  this._params = this._getParams(element, mergeDeepObject(defaultParams, params));
62
122
  this._animation(this._element, VGToast.NAME_KEY, this._params.animation);
63
123
  this._dismissElement();
64
124
  this._addEventListeners();
65
125
 
126
+ /** @private */
66
127
  this._timeout = null;
67
128
  }
68
129
 
130
+ /**
131
+ * Имя модуля
132
+ * @returns {string}
133
+ */
69
134
  static get NAME() {
70
135
  return NAME;
71
136
  }
72
137
 
138
+ /**
139
+ * Пространство имён событий
140
+ * @returns {string}
141
+ */
73
142
  static get NAME_KEY() {
74
- return NAME_KEY
143
+ return NAME_KEY;
75
144
  }
76
145
 
146
+ /**
147
+ * Глобальный метод для быстрого создания тоста
148
+ * @param {string|Array<string>} text - Текст или [заголовок, тело].
149
+ * @param {Partial<ToastParams>} [params] - Параметры.
150
+ * @param {Function} [callback] - Вызывается после создания.
151
+ * @returns {VGToast}
152
+ */
77
153
  static run(text, params = {}, callback) {
78
154
  return VGToast.build(text, params, callback);
79
155
  }
80
156
 
157
+ /**
158
+ * Создаёт и показывает новый тост
159
+ * @param {string|Array<string>} text - Текст уведомления.
160
+ * @param {Partial<ToastParams>} [params] - Параметры.
161
+ * @param {Function} [callback] - Вызывается после появления.
162
+ * @returns {VGToast}
163
+ */
81
164
  static build(text, params, callback) {
82
- params = mergeDeepObject(defaultParams, {static: false, autohide: true}, params);
165
+ params = mergeDeepObject(defaultParams, { static: false, autohide: true }, params);
83
166
 
84
- let target = document.createElement('div');
167
+ const id = 'vg-toast-' + makeRandomString();
168
+ const target = document.createElement('div');
85
169
  target.classList.add('vg-toast');
86
- target.id = 'vg-toast-' + makeRandomString();
170
+ target.id = id;
87
171
 
88
- if ('theme' in params) {
89
- target.classList.add('vg-toast-' + params.theme);
172
+ // Тема
173
+ if (params.theme) {
174
+ target.classList.add(`vg-toast-${params.theme}`);
90
175
  }
91
176
 
92
- if ('placement' in params) {
93
- params.placement.split(' ').forEach(val => target.classList.add(val));
177
+ // Позиция
178
+ if (params.placement) {
179
+ params.placement.split(' ').forEach(cls => target.classList.add(cls));
94
180
  }
95
181
 
96
- let wrapper = document.createElement('div');
182
+ const wrapper = document.createElement('div');
97
183
  wrapper.classList.add('vg-toast-wrapper');
98
184
 
99
- if ('type' in params) {
100
- let icon = document.createElement('div');
185
+ // Иконка (если задан тип)
186
+ if (params.type) {
187
+ const icon = document.createElement('div');
101
188
  icon.classList.add('vg-toast-icon');
102
-
103
189
  wrapper.append(icon);
104
190
  }
105
191
 
106
- let content = document.createElement('div');
192
+ const content = document.createElement('div');
107
193
  content.classList.add('vg-toast-content');
108
194
 
109
- let body = document.createElement('div');
195
+ const body = document.createElement('div');
110
196
  body.classList.add('vg-toast-body');
111
197
 
112
198
  if (typeof text === 'string') {
@@ -114,23 +200,20 @@ class VGToast extends BaseModule {
114
200
  content.append(body);
115
201
  } else if (Array.isArray(text)) {
116
202
  if (text.length > 1) {
117
- let header = document.createElement('div');
203
+ const header = document.createElement('div');
118
204
  header.classList.add('vg-toast-header');
119
205
  header.innerHTML = text[0];
120
206
  content.append(header);
121
-
122
- body.innerHTML = text[1];
123
- content.append(body);
124
- } else {
125
- body.innerHTML = text[0];
126
- content.append(body);
127
207
  }
208
+ body.innerHTML = text[1];
209
+ content.append(body);
128
210
  }
129
211
 
130
212
  wrapper.append(content);
131
213
 
132
- if ('enableButtonClose' in params && params.enableButtonClose) {
133
- let button = document.createElement('div');
214
+ // Кнопка закрытия
215
+ if (params.enableButtonClose) {
216
+ const button = document.createElement('div');
134
217
  button.classList.add('vg-toast-button');
135
218
  button.innerHTML = '<button class="vg-btn-close" data-vg-dismiss="toast"></button>';
136
219
  wrapper.append(button);
@@ -139,34 +222,45 @@ class VGToast extends BaseModule {
139
222
  target.append(wrapper);
140
223
  document.body.append(target);
141
224
 
142
- let instance = VGToast.getOrCreateInstance(target, params);
143
- if ('animation' in params) {
225
+ const instance = VGToast.getOrCreateInstance(target, params);
226
+ if (params.animation) {
144
227
  instance._animation(target, VGToast.NAME_KEY, params.animation);
145
228
  }
146
229
 
147
230
  execute(callback, [instance]);
148
231
  instance.show();
232
+
233
+ return instance;
149
234
  }
150
235
 
236
+ /**
237
+ * Переключает состояние (показать/скрыть)
238
+ * @param {Element} [relatedTarget] - Элемент, вызвавший тост.
239
+ * @returns {VGToast}
240
+ */
151
241
  toggle(relatedTarget) {
152
- return !this._isShown() ? this.show(relatedTarget) : this.hide();
242
+ return this._isShown() ? this.hide() : this.show(relatedTarget);
153
243
  }
154
244
 
245
+ /**
246
+ * Показывает тост
247
+ * @param {Element} [relatedTarget] - Элемент, инициировавший показ.
248
+ * @returns {void}
249
+ */
155
250
  show(relatedTarget) {
156
251
  if (isDisabled(this._element)) return;
157
252
 
158
253
  this._clearTimeout();
159
254
 
160
255
  this._params = this._getParams(relatedTarget || {}, this._params);
161
- this._route(function (status, data) {
162
- EventHandler.trigger(this._element, EVENT_KEY_LOADED, {stats: status, data: data});
256
+ this._route((status, data) => {
257
+ EventHandler.trigger(this._element, EVENT_KEY_LOADED, { stats: status, data });
163
258
  });
164
259
 
165
- const showEvent = EventHandler.trigger(this._element, EVENT_KEY_SHOW, { relatedTarget })
260
+ const showEvent = EventHandler.trigger(this._element, EVENT_KEY_SHOW, { relatedTarget });
166
261
  if (showEvent.defaultPrevented) return;
167
262
 
168
- this._element?.classList.remove(CLASS_NAME_SHOW);
169
-
263
+ this._element.classList.remove(CLASS_NAME_SHOWN);
170
264
  this._element.classList.add(CLASS_NAME_SHOW);
171
265
  document.body.classList.add(CLASS_NAME_OPEN);
172
266
 
@@ -176,20 +270,25 @@ class VGToast extends BaseModule {
176
270
  this._element.classList.add(CLASS_NAME_SHOWN);
177
271
  this._scheduleHide();
178
272
  EventHandler.trigger(this._element, EVENT_KEY_SHOWN, { relatedTarget });
179
- }
273
+ };
274
+
180
275
  this._queueCallback(completeCallBack, this._element, true, this._params.animation.delay);
181
276
  }
182
277
 
278
+ /**
279
+ * Скрывает тост
280
+ * @returns {void}
281
+ */
183
282
  hide() {
184
283
  if (isDisabled(this._element)) return;
185
284
 
186
285
  const hideEvent = EventHandler.trigger(this._element, EVENT_KEY_HIDE);
187
286
  if (hideEvent.defaultPrevented) return;
188
287
 
189
- this._element?.classList.remove(CLASS_NAME_SHOWN);
288
+ this._element.classList.remove(CLASS_NAME_SHOWN);
190
289
 
191
290
  setTimeout(() => {
192
- this._element?.classList.remove(CLASS_NAME_SHOW);
291
+ this._element.classList.remove(CLASS_NAME_SHOW);
193
292
 
194
293
  const completeCallback = () => {
195
294
  document.body.classList.remove(CLASS_NAME_OPEN);
@@ -202,181 +301,186 @@ class VGToast extends BaseModule {
202
301
  if (!this._params.static) {
203
302
  this.dispose();
204
303
  }
205
- }
304
+ };
305
+
206
306
  this._queueCallback(completeCallback, this._element, false, this._params.animation.delay);
207
307
  }, this._params.animation.delay);
208
308
  }
209
309
 
310
+ /**
311
+ * Удаляет тост из DOM и снимает обработчики
312
+ * @override
313
+ */
210
314
  dispose() {
211
315
  this._clearTimeout();
212
- if (this._isShown()) {
213
- this._element.classList.remove(CLASS_NAME_SHOW);
214
- }
215
-
216
316
  if (!this._params.static) {
217
317
  this._element.remove();
218
318
  }
219
-
220
319
  super.dispose();
221
320
  }
222
321
 
322
+ /**
323
+ * Устанавливает таймер на скрытие
324
+ * @private
325
+ */
223
326
  _scheduleHide() {
224
- if (!this._params.autohide) {
225
- return;
226
- }
327
+ if (!this._params.autohide) return;
227
328
 
228
- this._timeout = setTimeout(() => {
229
- this.hide();
230
- }, this._params.delay);
329
+ this._timeout = setTimeout(() => this.hide(), this._params.delay);
231
330
  }
232
331
 
332
+ /**
333
+ * Проверяет, показан ли тост
334
+ * @private
335
+ * @returns {boolean}
336
+ */
233
337
  _isShown() {
234
338
  return this._element.classList.contains(CLASS_NAME_SHOW);
235
339
  }
236
340
 
237
- _setPlacement() {
238
- let elms = this._enableStack();
341
+ /**
342
+ * Возвращает список активных тостов с вертикальными смещениями
343
+ * Учитывает стек и максимальное количество
344
+ * @private
345
+ * @returns {Array<{el: Element, top: number}>}
346
+ */
347
+ _enableStack() {
348
+ const placement = this._params.placement;
349
+ const isVerticalCenter = placement.includes('center');
350
+ const isTop = placement.includes('top');
351
+ const isBottom = !isTop; // по умолчанию снизу
352
+
353
+ // Фильтруем тосты с таким же направлением (top или bottom)
354
+ const stackClass = isTop ? 'top' : 'bottom';
355
+ const elmsShown = Selectors.findAll(`.vg-toast.show.${stackClass}`)
356
+ .filter(el => {
357
+ const instance = VGToast.getInstance(el);
358
+ return instance?._params.stack.enable;
359
+ });
239
360
 
240
- if (this._params.stack.enable) {
241
- if (elms.length > this._params.stack.max) {
242
- let elm = elms.shift();
243
- VGToast.getInstance(elm.el).hide();
244
- }
361
+ if (!this._params.stack.enable) {
362
+ // Скрываем другие тосты, если стек выключен
363
+ elmsShown
364
+ .filter(el => el !== this._element)
365
+ .forEach(el => VGToast.getInstance(el).hide());
366
+ return [{ el: this._element, top: 0 }];
245
367
  }
246
368
 
247
- elms.forEach(elm => {
248
- let isPlacementClassTop = elm.el.classList.contains('top'),
249
- isPlacementClassBottom = elm.el.classList.contains('bottom'),
250
- isPlacementClassLeft = elm.el.classList.contains('left'),
251
- isPlacementClassRight = elm.el.classList.contains('right'),
252
- isPlacementClassCenter = elm.el.classList.contains('center');
253
-
254
- if (!isPlacementClassTop &&
255
- !isPlacementClassBottom &&
256
- !isPlacementClassCenter &&
257
- !isPlacementClassRight &&
258
- !isPlacementClassLeft
259
- ) {
260
- isPlacementClassBottom = true;
261
- isPlacementClassCenter = true;
262
- }
263
-
264
- if (isPlacementClassCenter) {
265
- if (isPlacementClassLeft) {
266
- elm.el.style.left = 0;
267
- elm.el.style.bottom = 'calc(50% - ('+ elm.top +'px))';
268
- } else if (isPlacementClassRight) {
269
- elm.el.style.right = 0;
270
- elm.el.style.bottom = 'calc(50% - ('+ elm.top +'px))';
271
- } else if (isPlacementClassBottom) {
272
- elm.el.style.left = 'calc(50% - ('+ elm.el.clientWidth +'px) / 2)';
273
- elm.el.style.bottom = elm.top + 'px';
274
- } else if (isPlacementClassTop) {
275
- elm.el.style.left = 'calc(50% - ('+ elm.el.clientWidth +'px) / 2)';
276
- elm.el.style.top = elm.top + 'px';
277
- } else {
278
- elm.el.style.left = 'calc(50% - ('+ elm.el.clientHeight +'px) / 2)';
279
- elm.el.style.bottom = 'calc(50% - '+ elm.top +'px)';
280
- }
281
- } else {
282
- if (isPlacementClassLeft) elm.el.style.left = 0;
283
- if (isPlacementClassBottom) elm.el.style.bottom = elm.top + 'px';
284
- if (isPlacementClassTop) elm.el.style.top = elm.top + 'px';
285
- if (isPlacementClassRight) elm.el.style.right = 0;
286
- }
287
- });
288
- }
369
+ // Ограничиваем по max
370
+ if (elmsShown.length >= this._params.stack.max) {
371
+ const excess = elmsShown.slice(0, elmsShown.length - this._params.stack.max + 1);
372
+ excess.forEach(el => VGToast.getInstance(el).hide());
373
+ }
289
374
 
290
- _enableStack() {
291
- let elmsShown = [... Selectors.findAll('.vg-toast.show')], top = 0;
375
+ // Вычисляем смещение (по высоте)
376
+ const prevEls = elmsShown.filter(el => el !== this._element);
377
+ const offset = prevEls.reduce((sum, el) => sum + el.clientHeight, 0);
292
378
 
293
- if (!this._params.stack.enable) {
294
- elmsShown.forEach(el => {
295
- if (el !== this._element) {
296
- VGToast.getInstance(el).hide()
297
- }
379
+ return elmsShown.includes(this._element)
380
+ ? elmsShown.map((el, index) => {
381
+ const heightSum = elmsShown.slice(0, index).reduce((sum, e) => sum + e.clientHeight, 0);
382
+ return { el, top: heightSum };
298
383
  })
384
+ : [{ el: this._element, top: offset }];
385
+ }
299
386
 
300
- return [{
301
- el: this._element,
302
- top: 0,
303
- }];
304
- }
305
-
306
- elmsShown = elmsShown.map(el => {
307
- return {
308
- el: el,
309
- top: el.clientHeight
387
+ /**
388
+ * Устанавливает позицию тостов с учётом стека
389
+ * @private
390
+ */
391
+ _setPlacement() {
392
+ const elms = this._enableStack();
393
+ const isCenter = this._params.placement.includes('center');
394
+ const isLeft = this._params.placement.includes('left');
395
+ const isRight = this._params.placement.includes('right');
396
+ const isTop = this._params.placement.includes('top');
397
+
398
+ const stackClass = isTop ? 'top' : 'bottom';
399
+
400
+ elms.forEach(({ el, top }) => {
401
+ const style = el.style;
402
+ style.left = '';
403
+ style.right = '';
404
+ style.top = '';
405
+ style.bottom = '';
406
+ style.transform = '';
407
+
408
+ if (isCenter) {
409
+ style.left = '50%';
410
+ style.transform = 'translateX(-50%)';
411
+ } else if (isLeft) {
412
+ style.left = '0';
413
+ } else if (isRight) {
414
+ style.right = '0';
415
+ } else {
416
+ // по умолчанию: центрирование
417
+ style.left = '50%';
418
+ style.transform = 'translateX(-50%)';
310
419
  }
311
- });
312
420
 
313
- return elmsShown.map(function (value, index) {
314
- if (index === 0) {
315
- return {
316
- el: value.el,
317
- top: 0
318
- }
421
+ if (isTop) {
422
+ style.top = top + 'px';
319
423
  } else {
320
- top += value.top
321
-
322
- return {
323
- el: value.el,
324
- top: top
325
- }
424
+ style.bottom = top + 'px';
326
425
  }
327
426
  });
328
427
  }
329
428
 
429
+ /**
430
+ * Очищает таймер
431
+ * @private
432
+ */
330
433
  _clearTimeout() {
331
- clearTimeout(this._timeout);
332
- this._timeout = null;
434
+ if (this._timeout) {
435
+ clearTimeout(this._timeout);
436
+ this._timeout = null;
437
+ }
333
438
  }
334
439
 
440
+ /**
441
+ * Назначает обработчики событий
442
+ * @private
443
+ */
335
444
  _addEventListeners() {
336
- EventHandler.on(document, EVENT_KEY_KEYDOWN_DISMISS, event => {
337
- if (event.key !== 'Escape') return;
338
-
339
- if (this._params.keyboard) {
340
- this.hide();
341
- return;
342
- }
343
-
344
- EventHandler.trigger(this._element, EVENT_KEY_HIDE_PREVENTED)
345
- });
445
+ // Закрытие по Esc
446
+ if (this._params.keyboard) {
447
+ EventHandler.on(document, EVENT_KEY_KEYDOWN_DISMISS, event => {
448
+ if (event.key === 'Escape' && this._isShown()) {
449
+ this.hide();
450
+ }
451
+ });
452
+ }
346
453
 
454
+ // Закрытие по клику на тост
347
455
  if (this._params.enableClickToast) {
348
456
  this._element.classList.add('vg-toast-pointer');
349
-
350
- EventHandler.on(document, EVENT_KEY_CLICK_DATA_API, '#' + this._element.id, () => {
457
+ EventHandler.on(document, EVENT_KEY_CLICK_DATA_API, `#${this._element.id}`, () => {
351
458
  this.hide();
352
- })
459
+ });
353
460
  }
354
461
  }
355
462
  }
356
463
 
464
+ // Автоматическое закрытие по data-vg-dismiss
357
465
  dismissTrigger(VGToast);
358
466
 
359
467
  /**
360
- * Data API implementation
468
+ * Реализация Data API
361
469
  */
362
470
  EventHandler.on(document, EVENT_KEY_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (event) {
363
471
  const target = Selectors.getElementFromSelector(this);
364
-
365
472
  if (['A', 'AREA'].includes(this.tagName)) {
366
- event.preventDefault()
367
- }
368
-
369
- if (isDisabled(this)) {
370
- return
473
+ event.preventDefault();
371
474
  }
475
+ if (isDisabled(this)) return;
372
476
 
373
- this.setAttribute('aria-expanded', true);
477
+ this.setAttribute('aria-expanded', 'true');
374
478
  EventHandler.one(target, EVENT_KEY_HIDDEN, () => {
375
- this.setAttribute('aria-expanded', false);
479
+ this.setAttribute('aria-expanded', 'false');
376
480
  });
377
481
 
378
482
  const data = VGToast.getOrCreateInstance(target);
379
483
  data.toggle(this);
380
484
  });
381
485
 
382
- export default VGToast;
486
+ export default VGToast;