vgapp 1.5.5 → 1.5.7

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.
@@ -1,4 +1,8 @@
1
- import BaseModule from "../../base-module";
1
+ /**
2
+ * Описание: уведомления VGToast через Data API и JavaScript.
3
+ * Возможности: стек, AJAX, анимации, автоскрытие, перетаскивание и изменение размера.
4
+ */
5
+ import BaseModule from "../../base-module";
2
6
  import EventHandler from "../../../utils/js/dom/event";
3
7
  import {dismissTrigger, getSVG} from "../../module-fn";
4
8
  import Sanitize from "../../../utils/js/components/sanitize";
@@ -331,6 +335,7 @@ class VGToast extends BaseModule {
331
335
  */
332
336
  show(relatedTarget) {
333
337
  if (isDisabled(this._element)) return;
338
+ if (this._isShown() && !this._isHiding) return;
334
339
 
335
340
  const element = this._element;
336
341
  this._clearTimeout();
@@ -466,7 +471,7 @@ class VGToast extends BaseModule {
466
471
  const elmsShown = Selectors.findAll(`.vg-toast.show.${stackClass}`)
467
472
  .filter(el => {
468
473
  const instance = VGToast.getInstance(el);
469
- return instance?._params.stack.enable;
474
+ return instance && !instance._isHiding;
470
475
  });
471
476
 
472
477
  if (!this._params.stack.enable) {
@@ -478,8 +483,9 @@ class VGToast extends BaseModule {
478
483
  }
479
484
 
480
485
  // Ограничиваем по max
481
- if (elmsShown.length >= this._params.stack.max) {
482
- const excess = elmsShown.slice(0, elmsShown.length - this._params.stack.max + 1);
486
+ const max = Math.max(1, Number(this._params.stack.max) || 5);
487
+ if (elmsShown.length > max) {
488
+ const excess = elmsShown.slice(0, elmsShown.length - max);
483
489
  excess.forEach(el => VGToast.getInstance(el).hide());
484
490
  }
485
491
 
@@ -507,7 +513,7 @@ class VGToast extends BaseModule {
507
513
  const visibleStack = Selectors.findAll(`.vg-toast.show.${stackClass}`)
508
514
  .filter(el => {
509
515
  const instance = VGToast.getInstance(el);
510
- return instance?._params.stack.enable;
516
+ return instance && !instance._isHiding;
511
517
  });
512
518
  const elms = visibleStack.length ? visibleStack : stackItems.map(item => item.el);
513
519
  let offset = 0;
@@ -1,4 +1,8 @@
1
- import BaseModule from "../../base-module";
1
+ /**
2
+ * Описание: всплывающие подсказки и информационные popover VGApp.
3
+ * Возможности: Data API, позиционирование, события и очистка при удалении триггера.
4
+ */
5
+ import BaseModule from "../../base-module";
2
6
  import {isDisabled, makeRandomString, mergeDeepObject} from "../../../utils/js/functions";
3
7
  import EventHandler from "../../../utils/js/dom/event";
4
8
  import Selectors from "../../../utils/js/dom/selectors";
@@ -80,8 +84,13 @@ class VGTooltip extends BaseModule {
80
84
  constructor(element, params = {}) {
81
85
  super(element, params);
82
86
 
83
- this._params = this._getParams(element, mergeDeepObject(defaultParams, params));
84
- this._tooltip = null;
87
+ this._params = this._getParams(element, mergeDeepObject(defaultParams, params));
88
+ // Геометрические массивы заменяются целиком, а не дополняют defaults.
89
+ const dataParams = this._getParams(element, {});
90
+ ['offset', 'fallbackPlacements'].forEach(key => {
91
+ this._params[key] = [...(dataParams[key] ?? params[key] ?? defaultParams[key])];
92
+ });
93
+ this._tooltip = null;
85
94
  this._isHiding = false;
86
95
  this._showTimeout = null;
87
96
  this._hideTimeout = null;
@@ -249,10 +258,11 @@ class VGTooltip extends BaseModule {
249
258
  tooltip.classList.add('vg-tooltip-popover');
250
259
  }
251
260
 
252
- const inner = document.createElement('div');
253
- inner.classList.add('vg-tooltip-inner');
254
-
255
- if (this._params.content) {
261
+ const inner = document.createElement('div');
262
+ inner.classList.add('vg-tooltip-inner');
263
+ const content = this._params.content || this._element.dataset.vgContent || '';
264
+
265
+ if (content) {
256
266
  const titleBlock = document.createElement('div');
257
267
  titleBlock.classList.add('vg-tooltip-inner--title');
258
268
 
@@ -261,10 +271,10 @@ class VGTooltip extends BaseModule {
261
271
 
262
272
  if (this._params.html) {
263
273
  titleBlock.innerHTML = title;
264
- contentBlock.innerHTML = this._params.content;
274
+ contentBlock.innerHTML = content;
265
275
  } else {
266
276
  titleBlock.textContent = title;
267
- contentBlock.textContent = this._params.content;
277
+ contentBlock.textContent = content;
268
278
  }
269
279
 
270
280
  inner.append(titleBlock);
@@ -1,4 +1,8 @@
1
- const FILE_ICON_BY_EXT = {
1
+ /**
2
+ * Описание: определение категории файла и соответствующей SVG-иконки.
3
+ * Возможности: распознаёт MIME-тип и расширение, поддерживает явные имена иконок и общий fallback.
4
+ */
5
+ const FILE_ICON_BY_EXT = {
2
6
  pdf: 'file-pdf',
3
7
  doc: 'file-word',
4
8
  docx: 'file-word',
@@ -34,7 +38,8 @@ const FILE_ICON_BY_EXT = {
34
38
  tiff: 'file-image',
35
39
  heic: 'file-image',
36
40
  heif: 'file-image',
37
- avif: 'file-image',
41
+ avif: 'file-image',
42
+ ico: 'file-image',
38
43
  mp3: 'file-audio',
39
44
  wav: 'file-audio',
40
45
  ogg: 'file-audio',
@@ -0,0 +1,165 @@
1
+ /**
2
+ * Описание: общий сервис брейкпоинтов и адаптивных настроек VGApp.
3
+ * Возможности: xs–xxl, глобальные и локальные границы, наследование профилей, подписки, viewport и эвристики устройства/touch.
4
+ */
5
+ import {normalizeData} from "../functions";
6
+
7
+ export const DEFAULT_BREAKPOINTS = Object.freeze({xs: 0, sm: 576, md: 768, lg: 992, xl: 1200, xxl: 1400});
8
+
9
+ const plainObject = (value) => {
10
+ if (value === null || typeof value !== 'object') return false;
11
+ const prototype = Object.getPrototypeOf(value);
12
+ return prototype === null || Object.getPrototypeOf(prototype) === null;
13
+ };
14
+ const entries = (value) => Object.entries(value).filter(([key]) => !['__proto__', 'constructor', 'prototype'].includes(key));
15
+ const clone = (value) => Array.isArray(value) ? value.map(clone)
16
+ : plainObject(value) ? Object.fromEntries(entries(value).map(([key, item]) => [key, clone(item)])) : value;
17
+
18
+ // В профилях массив заменяется целиком, а не конкатенируется как в mergeDeepObject.
19
+ const merge = (base, profile) => {
20
+ const result = plainObject(base) ? clone(base) : {};
21
+ if (plainObject(profile)) entries(profile).forEach(([key, value]) => {
22
+ result[key] = plainObject(value) ? merge(result[key], value) : clone(value);
23
+ });
24
+ return result;
25
+ };
26
+
27
+ export class Responsive {
28
+ constructor(options = {}) {
29
+ this._window = options.window === undefined ? (typeof window === 'undefined' ? null : window) : options.window;
30
+ const globalPoints = this._window?.Breakpoints ?? this._window?.breakpoints;
31
+ const globalMap = normalizeData(globalPoints ?? {});
32
+ const localMap = normalizeData(options.breakpoints ?? {});
33
+ const sourcesValid = plainObject(globalMap) && plainObject(localMap);
34
+ const combined = {...DEFAULT_BREAKPOINTS, ...(plainObject(globalMap) ? globalMap : {}), ...(plainObject(localMap) ? localMap : {})};
35
+ this._breakpoints = Object.freeze(Object.fromEntries(entries(combined).map(([key, value]) => [key,
36
+ typeof value === 'string' && value.trim() !== '' ? Number(value) : value,
37
+ ])));
38
+ const standardNames = Object.keys(DEFAULT_BREAKPOINTS);
39
+ const widths = Object.values(this._breakpoints);
40
+ this._valid = sourcesValid && widths.every((width) => Number.isFinite(width) && width >= 0)
41
+ && new Set(widths).size === widths.length
42
+ && standardNames.every((name, index) => index === 0 ? this._breakpoints[name] === 0
43
+ : this._breakpoints[name] > this._breakpoints[standardNames[index - 1]]);
44
+ this._keys = this._valid ? Object.keys(this._breakpoints).sort((a, b) => this._breakpoints[a] - this._breakpoints[b]) : [];
45
+ this._subscribers = new Set();
46
+ this._previous = null;
47
+ this._boundResize = () => {
48
+ const state = this.getState();
49
+ if (state.breakpoint === this._previous) return;
50
+ const previous = this._previous;
51
+ this._previous = state.breakpoint;
52
+ Array.from(this._subscribers).forEach((subscriber) => {
53
+ if (!this._subscribers.has(subscriber)) return;
54
+ try { subscriber({...this.getState(), previous}); }
55
+ catch (error) {
56
+ if (typeof this._window?.reportError === 'function') this._window.reportError(error);
57
+ else console.error(error);
58
+ }
59
+ });
60
+ };
61
+ }
62
+
63
+ /** Копия границ; внешнее изменение не влияет на экземпляр. */
64
+ get breakpoints() { return {...this._breakpoints}; }
65
+ isValid() { return this._valid; }
66
+
67
+ /** Без аргумента — карта; имя — min-width; число (включая 0) — имя диапазона. */
68
+ breakpoint(point) {
69
+ if (point === undefined) return this.breakpoints;
70
+ if (typeof point === 'number') return this.getBreakpointKey(point);
71
+ return this.checkBreakpoint(point) ? this._breakpoints[point] : null;
72
+ }
73
+
74
+ checkBreakpoint(point) {
75
+ return this._valid && typeof point === 'string' && Object.prototype.hasOwnProperty.call(this._breakpoints, point);
76
+ }
77
+
78
+ breakpointDown(point) { return this.checkBreakpoint(point) && this.viewport().width < this._breakpoints[point]; }
79
+ breakpointUp(point) { return this.checkBreakpoint(point) && this.viewport().width >= this._breakpoints[point]; }
80
+
81
+ /** Полуоткрытый диапазон [start, end), без пересечения соседних диапазонов. */
82
+ breakpointBetween(start, end) {
83
+ const width = this.viewport().width;
84
+ return this.checkBreakpoint(start) && this.checkBreakpoint(end)
85
+ && this._breakpoints[start] < this._breakpoints[end]
86
+ && width >= this._breakpoints[start] && width < this._breakpoints[end];
87
+ }
88
+
89
+ getActiveBreakpoints(width = this.viewport().width) {
90
+ if (!this._valid || !Number.isFinite(width) || width < 0) return [];
91
+ return this._keys.filter((key) => width >= this._breakpoints[key]);
92
+ }
93
+
94
+ getBreakpointKey(width = this.viewport().width) { return this.getActiveBreakpoints(width).at(-1) ?? null; }
95
+
96
+ /** База + все достигнутые профили; вложенные объекты объединяются, массивы заменяются. */
97
+ resolve(profiles = {}, base = {}, width = this.viewport().width) {
98
+ return this.getActiveBreakpoints(width).reduce((result, key) => merge(result, profiles?.[key]), merge({}, base));
99
+ }
100
+
101
+ getState() {
102
+ const viewport = this.viewport();
103
+ return {...viewport, breakpoint: this.getBreakpointKey(viewport.width), active: this.getActiveBreakpoints(viewport.width),
104
+ reason: this._valid ? null : 'invalid-breakpoints'};
105
+ }
106
+
107
+ /** Уведомляет только о смене диапазона; возвращает функцию отписки. */
108
+ subscribe(callback, {immediate = false} = {}) {
109
+ if (typeof callback !== 'function') throw new TypeError('Responsive.subscribe expects a function.');
110
+ if (!this._valid) return () => {};
111
+ const subscriber = (state) => callback(state);
112
+ if (this._subscribers.size === 0) {
113
+ this._previous = this.getBreakpointKey();
114
+ this._window?.addEventListener('resize', this._boundResize);
115
+ }
116
+ this._subscribers.add(subscriber);
117
+ const unsubscribe = () => {
118
+ this._subscribers.delete(subscriber);
119
+ if (this._subscribers.size === 0) this._window?.removeEventListener('resize', this._boundResize);
120
+ };
121
+ if (immediate) {
122
+ try { callback({...this.getState(), previous: null}); }
123
+ catch (error) { unsubscribe(); throw error; }
124
+ }
125
+ return unsubscribe;
126
+ }
127
+
128
+ dispose() {
129
+ this._window?.removeEventListener('resize', this._boundResize);
130
+ this._subscribers.clear();
131
+ }
132
+
133
+ viewport() {
134
+ return {width: this._window?.innerWidth ?? 0, height: this._window?.innerHeight ?? 0};
135
+ }
136
+
137
+ /** Возможность touch-ввода, не классификация телефона или планшета. */
138
+ detectTouchDevice() {
139
+ return !!this._window && (Number(this._window.navigator?.maxTouchPoints) > 0 || 'ontouchstart' in this._window);
140
+ }
141
+
142
+ /** Эвристики устройства не используются при выборе responsive-профилей. */
143
+ isMobileDevice() {
144
+ if (!this._window) return false;
145
+ const userAgent = this._window.navigator?.userAgent || '';
146
+ return /Android|iPhone|iPad|iPod/i.test(userAgent)
147
+ || (this.detectTouchDevice() && this.viewport().width < 768 && this._window.devicePixelRatio >= 2);
148
+ }
149
+
150
+ isTabletDevice() {
151
+ if (!this._window) return false;
152
+ const navigator = this._window.navigator || {};
153
+ const userAgent = (navigator.userAgent || '').toLowerCase();
154
+ const {width, height} = this.viewport();
155
+ const short = Math.min(width, height);
156
+ const long = Math.max(width, height);
157
+ return /ipad/.test(userAgent) || (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1)
158
+ || (/android/.test(userAgent) && !/mobile/.test(userAgent) && long > 800)
159
+ || (this.detectTouchDevice() && short >= 600 && short <= 1200 && long >= 800 && long <= 1600);
160
+ }
161
+
162
+ detectDevice() {
163
+ return this.isTabletDevice() ? 'tablet' : this.isMobileDevice() ? 'mobile' : 'desktop';
164
+ }
165
+ }