vgapp 1.5.7 → 1.5.8
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/app/langs/en/buttons.json +17 -17
- package/app/langs/en/messages.json +36 -36
- package/app/langs/ru/buttons.json +17 -17
- package/app/langs/ru/messages.json +1 -1
- package/app/modules/vgalert/js/vgalert.js +7 -7
- package/app/modules/vgalert/scss/_variables.scss +16 -16
- package/app/modules/vgcollapse/scss/_variables.scss +7 -7
- package/app/modules/vgcollapse/scss/vgcollapse.scss +5 -5
- package/app/modules/vgdropdown/scss/_variables.scss +14 -14
- package/app/modules/vgdropdown/scss/vgdropdown.scss +5 -5
- package/app/modules/vgfilepreview/js/i18n.js +56 -56
- package/app/modules/vgfilepreview/js/renderers/image-modal.js +145 -145
- package/app/modules/vgfilepreview/js/renderers/image.js +92 -92
- package/app/modules/vgfilepreview/js/renderers/index.js +19 -19
- package/app/modules/vgfilepreview/js/renderers/office-modal.js +168 -168
- package/app/modules/vgfilepreview/js/renderers/office.js +79 -79
- package/app/modules/vgfilepreview/js/renderers/pdf-modal.js +260 -260
- package/app/modules/vgfilepreview/js/renderers/pdf.js +76 -76
- package/app/modules/vgfilepreview/js/renderers/playlist.js +71 -71
- package/app/modules/vgfilepreview/js/renderers/text-modal.js +343 -343
- package/app/modules/vgfilepreview/js/renderers/text.js +83 -83
- package/app/modules/vgfilepreview/js/renderers/zip-modal.js +522 -522
- package/app/modules/vgfilepreview/js/renderers/zip.js +89 -89
- package/app/modules/vgfilepreview/readme.md +68 -68
- package/app/modules/vgfiles/js/base.js +198 -198
- package/app/modules/vgfiles/js/droppable.js +17 -17
- package/app/modules/vgfiles/js/render.js +153 -153
- package/app/modules/vgfiles/js/sortable.js +52 -52
- package/app/modules/vgfiles/js/vgfiles.js +43 -43
- package/app/modules/vgfiles/readme.md +123 -123
- package/app/modules/vgfiles/scss/_mixins.scss +13 -13
- package/app/modules/vgfiles/scss/_variables.scss +10 -10
- package/app/modules/vgfiles/scss/vgfiles.scss +28 -28
- package/app/modules/vgformsender/js/hideshowpass.js +22 -22
- package/app/modules/vgformsender/readme.md +1 -1
- package/app/modules/vgformsender/scss/_variables.scss +5 -5
- package/app/modules/vgformsender/scss/vgformsender.scss +42 -42
- package/app/modules/vglawcookie/js/vglawcookie.js +19 -19
- package/app/modules/vglawcookie/scss/_variables.scss +13 -13
- package/app/modules/vglawcookie/scss/vglawcookie.scss +5 -5
- package/app/modules/vgmodal/js/vgmodal.interaction.js +216 -216
- package/app/modules/vgmodal/js/vgmodal.js +9 -9
- package/app/modules/vgmodal/js/vgmodal.minimized.js +227 -227
- package/app/modules/vgmodal/scss/_variables.scss +17 -17
- package/app/modules/vgmodal/scss/vgmodal.scss +77 -77
- package/app/modules/vgnav/js/vgnav.js +62 -62
- package/app/modules/vgnav/scss/_breakpoints.scss +36 -36
- package/app/modules/vgnav/scss/_hamburger.scss +5 -5
- package/app/modules/vgnav/scss/_placement.scss +5 -5
- package/app/modules/vgnav/scss/_toggle.scss +5 -5
- package/app/modules/vgnav/scss/_variables.scss +26 -26
- package/app/modules/vgnav/scss/vgnav.scss +41 -41
- package/app/modules/vgnestable/README.md +39 -39
- package/app/modules/vgrangeslider/js/skins.js +8 -8
- package/app/modules/vgrangeslider/js/vgrangeslider.js +29 -29
- package/app/modules/vgrangeslider/scss/_variables.scss +55 -55
- package/app/modules/vgrangeslider/scss/vgrangeslider.scss +239 -239
- package/app/modules/vgrollup/js/vgrollup.js +15 -15
- package/app/modules/vgrollup/scss/_variables.scss +15 -15
- package/app/modules/vgrollup/scss/vgrollup.scss +6 -6
- package/app/modules/vgselect/js/handlers.js +6 -6
- package/app/modules/vgselect/js/vgselect.js +167 -123
- package/app/modules/vgselect/readme.md +11 -11
- package/app/modules/vgselect/scss/_variables.scss +51 -51
- package/app/modules/vgselect/scss/vgselect.scss +1 -1
- package/app/modules/vgsidebar/js/vgsidebar.js +21 -21
- package/app/modules/vgsidebar/scss/_variables.scss +10 -10
- package/app/modules/vgsidebar/scss/vgsidebar.scss +5 -5
- package/app/modules/vgspy/js/vgspy.js +43 -43
- package/app/modules/vgtable/js/_filters.js +10 -6
- package/app/modules/vgtable/js/_options.js +16 -16
- package/app/modules/vgtable/js/_pagination.js +118 -118
- package/app/modules/vgtable/js/_responsive.js +79 -79
- package/app/modules/vgtable/js/_sticky-header.js +349 -280
- package/app/modules/vgtable/js/vgtable.js +98 -98
- package/app/modules/vgtable/scss/_sorting.scss +6 -6
- package/app/modules/vgtable/scss/_sticky-header.scss +31 -31
- package/app/modules/vgtable/scss/_variables.scss +6 -4
- package/app/modules/vgtable/scss/vgtable.scss +8 -4
- package/app/modules/vgtabs/js/vgtabs.js +91 -91
- package/app/modules/vgtabs/scss/_variables.scss +23 -23
- package/app/modules/vgtabs/scss/vgtabs.scss +5 -5
- package/app/modules/vgtoast/js/vgtoast.js +4 -4
- package/app/modules/vgtoast/scss/_variables.scss +13 -13
- package/app/modules/vgtoast/scss/vgtoast.scss +5 -5
- package/app/modules/vgtooltip/js/vgtooltip.js +127 -127
- package/app/modules/vgtooltip/scss/_variables.scss +10 -10
- package/app/modules/vgtooltip/scss/vgtooltip.scss +5 -5
- package/app/utils/js/components/audio-metadata.js +240 -240
- package/app/utils/js/components/backdrop.js +116 -116
- package/app/utils/js/components/file-icon.js +7 -7
- package/app/utils/js/components/file-preview.js +304 -304
- package/app/utils/js/components/responsive.js +165 -165
- package/app/utils/js/components/sanitize.js +150 -150
- package/app/utils/js/components/video-metadata.js +140 -140
- package/app/utils/scss/_backdrop.scss +5 -5
- package/app/utils/scss/_theme-default.scss +12 -12
- package/app/utils/scss/_theme.scss +7 -7
- package/app/utils/scss/default.scss +9 -9
- package/app/utils/scss/functions.scss +21 -21
- package/app/utils/scss/index.scss +4 -4
- package/app/utils/scss/mixin.scss +16 -16
- package/app/utils/scss/variables.scss +147 -147
- package/build/vgapp.css +1 -1
- package/build/vgapp.css.map +1 -1
- package/build/vgapp.js +1 -1
- package/build/vgapp.js.map +1 -1
- package/index.js +7 -7
- package/package.json +1 -1
|
@@ -1,165 +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
|
-
}
|
|
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
|
+
}
|
|
@@ -1,150 +1,150 @@
|
|
|
1
|
-
const SVG_NAMESPACE = "http://www.w3.org/2000/svg";
|
|
2
|
-
const DEFAULT_ALLOWED_SVG_TAGS = new Set([
|
|
3
|
-
"svg",
|
|
4
|
-
"g",
|
|
5
|
-
"path",
|
|
6
|
-
"line",
|
|
7
|
-
"polyline",
|
|
8
|
-
"polygon",
|
|
9
|
-
"circle",
|
|
10
|
-
"ellipse",
|
|
11
|
-
"rect",
|
|
12
|
-
"defs",
|
|
13
|
-
"symbol",
|
|
14
|
-
"title",
|
|
15
|
-
"use"
|
|
16
|
-
]);
|
|
17
|
-
const DEFAULT_ALLOWED_SVG_ATTRS = new Set([
|
|
18
|
-
"xmlns",
|
|
19
|
-
"viewbox",
|
|
20
|
-
"width",
|
|
21
|
-
"height",
|
|
22
|
-
"fill",
|
|
23
|
-
"stroke",
|
|
24
|
-
"stroke-width",
|
|
25
|
-
"stroke-linecap",
|
|
26
|
-
"stroke-linejoin",
|
|
27
|
-
"stroke-miterlimit",
|
|
28
|
-
"stroke-dasharray",
|
|
29
|
-
"stroke-dashoffset",
|
|
30
|
-
"opacity",
|
|
31
|
-
"transform",
|
|
32
|
-
"class",
|
|
33
|
-
"x",
|
|
34
|
-
"y",
|
|
35
|
-
"x1",
|
|
36
|
-
"y1",
|
|
37
|
-
"x2",
|
|
38
|
-
"y2",
|
|
39
|
-
"cx",
|
|
40
|
-
"cy",
|
|
41
|
-
"r",
|
|
42
|
-
"rx",
|
|
43
|
-
"ry",
|
|
44
|
-
"points",
|
|
45
|
-
"d",
|
|
46
|
-
"role",
|
|
47
|
-
"focusable",
|
|
48
|
-
"aria-hidden",
|
|
49
|
-
"href",
|
|
50
|
-
"xlink:href"
|
|
51
|
-
]);
|
|
52
|
-
|
|
53
|
-
function escapeHtml(value) {
|
|
54
|
-
return String(value)
|
|
55
|
-
.replace(/&/g, "&")
|
|
56
|
-
.replace(/</g, "<")
|
|
57
|
-
.replace(/>/g, ">")
|
|
58
|
-
.replace(/"/g, """)
|
|
59
|
-
.replace(/'/g, "'");
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function sanitizeSvgNode(node, allowedTags = DEFAULT_ALLOWED_SVG_TAGS, allowedAttrs = DEFAULT_ALLOWED_SVG_ATTRS) {
|
|
63
|
-
if (!node) {
|
|
64
|
-
return null;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
if (node.nodeType === Node.TEXT_NODE) {
|
|
68
|
-
return document.createTextNode(node.textContent || "");
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
if (node.nodeType !== Node.ELEMENT_NODE) {
|
|
72
|
-
return null;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
const tagName = node.tagName.toLowerCase();
|
|
76
|
-
if (!allowedTags.has(tagName)) {
|
|
77
|
-
return null;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
const safeNode = document.createElementNS(SVG_NAMESPACE, tagName);
|
|
81
|
-
|
|
82
|
-
Array.from(node.attributes || []).forEach((attr) => {
|
|
83
|
-
const name = attr.name.toLowerCase();
|
|
84
|
-
const attrValue = attr.value || "";
|
|
85
|
-
|
|
86
|
-
if (!allowedAttrs.has(name) || name.startsWith("on")) {
|
|
87
|
-
return;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
if ((name === "href" || name === "xlink:href") && attrValue && !attrValue.startsWith("#")) {
|
|
91
|
-
return;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
safeNode.setAttribute(name, attrValue);
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
Array.from(node.childNodes || []).forEach((child) => {
|
|
98
|
-
const safeChild = sanitizeSvgNode(child, allowedTags, allowedAttrs);
|
|
99
|
-
if (safeChild) {
|
|
100
|
-
safeNode.append(safeChild);
|
|
101
|
-
}
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
return safeNode;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
export function toSafeHtmlString(value, {
|
|
108
|
-
allowedSvgTags = DEFAULT_ALLOWED_SVG_TAGS,
|
|
109
|
-
allowedSvgAttrs = DEFAULT_ALLOWED_SVG_ATTRS
|
|
110
|
-
} = {}) {
|
|
111
|
-
if (value === null || value === undefined) {
|
|
112
|
-
return "";
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
const content = String(value).trim();
|
|
116
|
-
if (!content) {
|
|
117
|
-
return "";
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
if (!content.includes("<")) {
|
|
121
|
-
return escapeHtml(content);
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
const template = document.createElement("template");
|
|
125
|
-
template.innerHTML = content;
|
|
126
|
-
|
|
127
|
-
const sanitizedParts = Array.from(template.content.childNodes)
|
|
128
|
-
.map((node) => sanitizeSvgNode(node, allowedSvgTags, allowedSvgAttrs))
|
|
129
|
-
.filter(Boolean)
|
|
130
|
-
.map((node) => {
|
|
131
|
-
if (node.nodeType === Node.TEXT_NODE) {
|
|
132
|
-
return escapeHtml(node.textContent || "");
|
|
133
|
-
}
|
|
134
|
-
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
135
|
-
return node.outerHTML;
|
|
136
|
-
}
|
|
137
|
-
return "";
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
const sanitized = sanitizedParts.join("");
|
|
141
|
-
return sanitized || escapeHtml(content);
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
class Sanitize {
|
|
145
|
-
static toSafeHtmlString(value, options) {
|
|
146
|
-
return toSafeHtmlString(value, options);
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
export default Sanitize;
|
|
1
|
+
const SVG_NAMESPACE = "http://www.w3.org/2000/svg";
|
|
2
|
+
const DEFAULT_ALLOWED_SVG_TAGS = new Set([
|
|
3
|
+
"svg",
|
|
4
|
+
"g",
|
|
5
|
+
"path",
|
|
6
|
+
"line",
|
|
7
|
+
"polyline",
|
|
8
|
+
"polygon",
|
|
9
|
+
"circle",
|
|
10
|
+
"ellipse",
|
|
11
|
+
"rect",
|
|
12
|
+
"defs",
|
|
13
|
+
"symbol",
|
|
14
|
+
"title",
|
|
15
|
+
"use"
|
|
16
|
+
]);
|
|
17
|
+
const DEFAULT_ALLOWED_SVG_ATTRS = new Set([
|
|
18
|
+
"xmlns",
|
|
19
|
+
"viewbox",
|
|
20
|
+
"width",
|
|
21
|
+
"height",
|
|
22
|
+
"fill",
|
|
23
|
+
"stroke",
|
|
24
|
+
"stroke-width",
|
|
25
|
+
"stroke-linecap",
|
|
26
|
+
"stroke-linejoin",
|
|
27
|
+
"stroke-miterlimit",
|
|
28
|
+
"stroke-dasharray",
|
|
29
|
+
"stroke-dashoffset",
|
|
30
|
+
"opacity",
|
|
31
|
+
"transform",
|
|
32
|
+
"class",
|
|
33
|
+
"x",
|
|
34
|
+
"y",
|
|
35
|
+
"x1",
|
|
36
|
+
"y1",
|
|
37
|
+
"x2",
|
|
38
|
+
"y2",
|
|
39
|
+
"cx",
|
|
40
|
+
"cy",
|
|
41
|
+
"r",
|
|
42
|
+
"rx",
|
|
43
|
+
"ry",
|
|
44
|
+
"points",
|
|
45
|
+
"d",
|
|
46
|
+
"role",
|
|
47
|
+
"focusable",
|
|
48
|
+
"aria-hidden",
|
|
49
|
+
"href",
|
|
50
|
+
"xlink:href"
|
|
51
|
+
]);
|
|
52
|
+
|
|
53
|
+
function escapeHtml(value) {
|
|
54
|
+
return String(value)
|
|
55
|
+
.replace(/&/g, "&")
|
|
56
|
+
.replace(/</g, "<")
|
|
57
|
+
.replace(/>/g, ">")
|
|
58
|
+
.replace(/"/g, """)
|
|
59
|
+
.replace(/'/g, "'");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function sanitizeSvgNode(node, allowedTags = DEFAULT_ALLOWED_SVG_TAGS, allowedAttrs = DEFAULT_ALLOWED_SVG_ATTRS) {
|
|
63
|
+
if (!node) {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
68
|
+
return document.createTextNode(node.textContent || "");
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (node.nodeType !== Node.ELEMENT_NODE) {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const tagName = node.tagName.toLowerCase();
|
|
76
|
+
if (!allowedTags.has(tagName)) {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const safeNode = document.createElementNS(SVG_NAMESPACE, tagName);
|
|
81
|
+
|
|
82
|
+
Array.from(node.attributes || []).forEach((attr) => {
|
|
83
|
+
const name = attr.name.toLowerCase();
|
|
84
|
+
const attrValue = attr.value || "";
|
|
85
|
+
|
|
86
|
+
if (!allowedAttrs.has(name) || name.startsWith("on")) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if ((name === "href" || name === "xlink:href") && attrValue && !attrValue.startsWith("#")) {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
safeNode.setAttribute(name, attrValue);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
Array.from(node.childNodes || []).forEach((child) => {
|
|
98
|
+
const safeChild = sanitizeSvgNode(child, allowedTags, allowedAttrs);
|
|
99
|
+
if (safeChild) {
|
|
100
|
+
safeNode.append(safeChild);
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
return safeNode;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function toSafeHtmlString(value, {
|
|
108
|
+
allowedSvgTags = DEFAULT_ALLOWED_SVG_TAGS,
|
|
109
|
+
allowedSvgAttrs = DEFAULT_ALLOWED_SVG_ATTRS
|
|
110
|
+
} = {}) {
|
|
111
|
+
if (value === null || value === undefined) {
|
|
112
|
+
return "";
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const content = String(value).trim();
|
|
116
|
+
if (!content) {
|
|
117
|
+
return "";
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (!content.includes("<")) {
|
|
121
|
+
return escapeHtml(content);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const template = document.createElement("template");
|
|
125
|
+
template.innerHTML = content;
|
|
126
|
+
|
|
127
|
+
const sanitizedParts = Array.from(template.content.childNodes)
|
|
128
|
+
.map((node) => sanitizeSvgNode(node, allowedSvgTags, allowedSvgAttrs))
|
|
129
|
+
.filter(Boolean)
|
|
130
|
+
.map((node) => {
|
|
131
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
132
|
+
return escapeHtml(node.textContent || "");
|
|
133
|
+
}
|
|
134
|
+
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
135
|
+
return node.outerHTML;
|
|
136
|
+
}
|
|
137
|
+
return "";
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
const sanitized = sanitizedParts.join("");
|
|
141
|
+
return sanitized || escapeHtml(content);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
class Sanitize {
|
|
145
|
+
static toSafeHtmlString(value, options) {
|
|
146
|
+
return toSafeHtmlString(value, options);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export default Sanitize;
|