vgapp 1.5.4 → 1.5.6
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/modules/vgnav/js/vgnav.js +62 -58
- package/app/modules/vgrangeslider/js/skins.js +8 -3
- package/app/modules/vgrangeslider/js/vgrangeslider.js +29 -17
- package/app/modules/vgrollup/js/vgrollup.js +15 -7
- package/app/modules/vgtable/js/_options.js +16 -5
- package/app/modules/vgtable/js/_pagination.js +118 -24
- package/app/modules/vgtable/js/_responsive.js +79 -0
- package/app/modules/vgtable/js/_sticky-header.js +44 -4
- package/app/modules/vgtable/js/vgtable.js +45 -18
- package/app/utils/js/components/responsive.js +165 -0
- package/build/vgapp.js +1 -1
- package/build/vgapp.js.map +1 -1
- package/index.js +7 -1
- package/package.json +1 -1
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Описание: адаптер общего Responsive для представления VGTable.
|
|
3
|
+
* Возможности: отбор безопасных параметров пагинации и события таблицы; границы, наследование и подписки делегируются Responsive.
|
|
4
|
+
*/
|
|
5
|
+
import EventHandler from "../../../utils/js/dom/event";
|
|
6
|
+
import {Responsive} from "../../../utils/js/components/responsive";
|
|
7
|
+
|
|
8
|
+
const object = (value) => value && typeof value === 'object' && !Array.isArray(value);
|
|
9
|
+
|
|
10
|
+
// Только представление: page/per, callbacks, storage и параметры запросов не принимаются.
|
|
11
|
+
const paginationProfile = (value) => {
|
|
12
|
+
const result = {};
|
|
13
|
+
if (!object(value)) return result;
|
|
14
|
+
if (value.maxButtons === null || (Number.isInteger(value.maxButtons) && value.maxButtons >= 3)) result.maxButtons = value.maxButtons;
|
|
15
|
+
if (['left', 'center', 'right', 'between'].includes(value.align)) result.align = value.align;
|
|
16
|
+
if (['top', 'bottom', 'both'].includes(value.position)) result.position = value.position;
|
|
17
|
+
if (object(value.size)) {
|
|
18
|
+
result.size = {};
|
|
19
|
+
if (typeof value.size.enabled === 'boolean') result.size.enabled = value.size.enabled;
|
|
20
|
+
if (value.size.label === false || typeof value.size.label === 'string') result.size.label = value.size.label;
|
|
21
|
+
}
|
|
22
|
+
if (object(value.quick) && [true, false, 'auto'].includes(value.quick.enabled)) result.quick = {enabled: value.quick.enabled};
|
|
23
|
+
return result;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
class _responsive {
|
|
27
|
+
constructor(table, options, onChange) {
|
|
28
|
+
this._table = table;
|
|
29
|
+
this._onChange = onChange;
|
|
30
|
+
this._responsive = new Responsive({breakpoints: options.breakpoints, window: table.ownerDocument.defaultView});
|
|
31
|
+
this._profiles = Object.fromEntries(Object.keys(this._responsive.breakpoints).map((name) => [name, paginationProfile(options[name]?.pagination)]));
|
|
32
|
+
this._breakpoint = null;
|
|
33
|
+
this._pagination = {};
|
|
34
|
+
this._width = 0;
|
|
35
|
+
this._valid = this._responsive.isValid();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
init() {
|
|
39
|
+
if (!this._valid) {
|
|
40
|
+
console.warn('VGTable: responsive.breakpoints must start at xs: 0 and increase through xxl.');
|
|
41
|
+
return this;
|
|
42
|
+
}
|
|
43
|
+
this.refresh();
|
|
44
|
+
this._responsive.subscribe(() => this.refresh());
|
|
45
|
+
return this;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
refresh(force = false) {
|
|
49
|
+
if (!this._valid || !this._responsive) return this.getState();
|
|
50
|
+
this._width = this._responsive.viewport().width;
|
|
51
|
+
const breakpoint = this._responsive.getBreakpointKey(this._width);
|
|
52
|
+
if (!force && breakpoint === this._breakpoint) return this.getState();
|
|
53
|
+
const previous = this._breakpoint;
|
|
54
|
+
this._breakpoint = breakpoint;
|
|
55
|
+
this._pagination = this._responsive.resolve(this._profiles, {}, this._width);
|
|
56
|
+
this._onChange();
|
|
57
|
+
if (previous !== null && previous !== breakpoint) {
|
|
58
|
+
EventHandler.trigger(this._table, 'responsivechange.vg.table', {...this.getState(), previous});
|
|
59
|
+
}
|
|
60
|
+
return this.getState();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
getState() {
|
|
64
|
+
return {
|
|
65
|
+
breakpoint: this._breakpoint,
|
|
66
|
+
width: this._responsive?.viewport().width ?? this._width,
|
|
67
|
+
reason: this._valid ? null : 'invalid-breakpoints',
|
|
68
|
+
pagination: {...this._pagination, size: {...this._pagination.size}, quick: {...this._pagination.quick}},
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
dispose() {
|
|
73
|
+
this._width = this._responsive?.viewport().width ?? this._width;
|
|
74
|
+
this._responsive?.dispose();
|
|
75
|
+
this._responsive = null;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export default _responsive;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Описание: разметка Fixed Header с отдельными слоями заголовка и тела VGTable.
|
|
3
|
-
* Возможности: перенос настоящего thead без клона,
|
|
3
|
+
* Возможности: перенос настоящего thead без клона, intrinsic-минимумы и синхронизация ширин колонок и горизонтальной прокрутки.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
const MODE_CLASSES = ['vg-table-wrapper--sticky-container', 'vg-table-wrapper--sticky-page'];
|
|
@@ -68,9 +68,19 @@ class _stickyHeader {
|
|
|
68
68
|
return this._headerTable || this._element;
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
-
getBody() {
|
|
72
|
-
return this._body;
|
|
73
|
-
}
|
|
71
|
+
getBody() {
|
|
72
|
+
return this._body;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
refreshIntrinsicMinimums() {
|
|
76
|
+
const widths = this._measureIntrinsicColumnMinimums();
|
|
77
|
+
this._getHeaders().forEach((header, index) => {
|
|
78
|
+
const key = this._columnKey(header, index);
|
|
79
|
+
if (this._hasDeclaredWidth(header) || this._hardColumnWidths.has(key) || !(widths[index] > 0)) return;
|
|
80
|
+
this._columnMinimumWidths.set(key, Math.max(this._columnMinimumWidths.get(key) || 0, widths[index]));
|
|
81
|
+
});
|
|
82
|
+
return this.refresh();
|
|
83
|
+
}
|
|
74
84
|
|
|
75
85
|
syncScroll() {
|
|
76
86
|
this._syncScroll();
|
|
@@ -215,6 +225,36 @@ class _stickyHeader {
|
|
|
215
225
|
});
|
|
216
226
|
}
|
|
217
227
|
|
|
228
|
+
_measureIntrinsicColumnMinimums() {
|
|
229
|
+
if (!this._wrapper || !this._head) return [];
|
|
230
|
+
|
|
231
|
+
const probe = this._element.cloneNode(true);
|
|
232
|
+
probe.removeAttribute('data-vg-table');
|
|
233
|
+
probe.removeAttribute('data-vg-table-sticky-header');
|
|
234
|
+
probe.setAttribute('aria-hidden', 'true');
|
|
235
|
+
probe.querySelector(':scope > colgroup[data-vg-table-sticky-colgroup]')?.remove();
|
|
236
|
+
probe.insertBefore(this._head.cloneNode(true), probe.tBodies[0] || null);
|
|
237
|
+
probe.querySelectorAll('thead th').forEach((header) => header.style.setProperty('white-space', 'nowrap', 'important'));
|
|
238
|
+
[
|
|
239
|
+
['position', 'absolute'],
|
|
240
|
+
['inset', '0 auto auto 0'],
|
|
241
|
+
['visibility', 'hidden'],
|
|
242
|
+
['pointer-events', 'none'],
|
|
243
|
+
['width', 'min-content'],
|
|
244
|
+
['min-width', '0'],
|
|
245
|
+
['max-width', 'none'],
|
|
246
|
+
['table-layout', 'auto'],
|
|
247
|
+
].forEach(([property, value]) => probe.style.setProperty(property, value, 'important'));
|
|
248
|
+
|
|
249
|
+
this._wrapper.append(probe);
|
|
250
|
+
try {
|
|
251
|
+
const rows = Array.from(probe.tHead?.rows || []);
|
|
252
|
+
return this._measureRow(rows.at(-1));
|
|
253
|
+
} finally {
|
|
254
|
+
probe.remove();
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
218
258
|
_rememberColumnGeometry(widths) {
|
|
219
259
|
const headers = this._getHeaders();
|
|
220
260
|
const sourceCols = Array.from(this._element.querySelector(':scope > colgroup:not([data-vg-table-sticky-colgroup])')?.children || []);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Описание: основной модуль базовых таблиц VGTable.
|
|
3
|
-
* Возможности: i18n, wrapper/container, состояния и URL state, local/remote, sticky,
|
|
3
|
+
* Возможности: i18n, wrapper/container, состояния и URL state, local/remote, sticky, колонки и строки, сортировка, дерево, responsive-пагинация, выбор и panning.
|
|
4
4
|
*/
|
|
5
5
|
import BaseModule from "../../base-module";
|
|
6
6
|
import {mergeDeepObject} from "../../../utils/js/functions";
|
|
@@ -9,7 +9,8 @@ import Selectors from "../../../utils/js/dom/selectors";
|
|
|
9
9
|
import _sorting from "./_sorting.js";
|
|
10
10
|
import _panning from "./_panning.js";
|
|
11
11
|
import _selection from "./_selection.js";
|
|
12
|
-
import _pagination from "./_pagination.js";
|
|
12
|
+
import _pagination from "./_pagination.js";
|
|
13
|
+
import _responsive from "./_responsive.js";
|
|
13
14
|
import _expandable from "./_expandable.js";
|
|
14
15
|
import _stickyHeader from "./_sticky-header.js";
|
|
15
16
|
import _fixedColumns from "./_fixed-columns.js";
|
|
@@ -63,7 +64,8 @@ class VGTable extends BaseModule {
|
|
|
63
64
|
this._panning = null;
|
|
64
65
|
this._selection = null;
|
|
65
66
|
this._expandable = null;
|
|
66
|
-
this._pagination = null;
|
|
67
|
+
this._pagination = null;
|
|
68
|
+
this._responsive = null;
|
|
67
69
|
this._filters = null;
|
|
68
70
|
this._search = null;
|
|
69
71
|
this._skeleton = null;
|
|
@@ -150,8 +152,8 @@ class VGTable extends BaseModule {
|
|
|
150
152
|
'sortchange.vg.table',
|
|
151
153
|
this._isRemote ? this._boundRemoteSortChange : this._boundLocalSortChange
|
|
152
154
|
);
|
|
153
|
-
this._stickyHeader?.refresh?.();
|
|
154
155
|
}
|
|
156
|
+
this._stickyHeader?.refreshIntrinsicMinimums?.();
|
|
155
157
|
|
|
156
158
|
// Фиксируем настоящие ячейки через native sticky, включая отдельный слой Fixed Header
|
|
157
159
|
if (!this._complex && !this._fixedColumns && this._params.fixedColumns.enabled === true) {
|
|
@@ -187,9 +189,17 @@ class VGTable extends BaseModule {
|
|
|
187
189
|
this._expandable.init();
|
|
188
190
|
}
|
|
189
191
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
192
|
+
if (!this._responsive && this._params.responsive?.enabled === true) {
|
|
193
|
+
this._responsive = new _responsive(this._element, this._params.responsive, () => {
|
|
194
|
+
this._pagination?.updatePresentation(this._paginationOptions());
|
|
195
|
+
});
|
|
196
|
+
this._responsive.init();
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Включаем локальную или серверную пагинацию
|
|
200
|
+
if (!this._pagination && this._params.pagination.enabled === true) {
|
|
201
|
+
this._pagination = new _pagination(this._element, Object.assign({}, this._paginationOptions(), {
|
|
202
|
+
responsive: this._responsive?.getState().reason === null,
|
|
193
203
|
remote: this._isRemote,
|
|
194
204
|
onChange: (state) => this._handlePaginationChange(state),
|
|
195
205
|
}));
|
|
@@ -300,15 +310,27 @@ class VGTable extends BaseModule {
|
|
|
300
310
|
this._normalizeColumnsDataOptions();
|
|
301
311
|
this._normalizeRowReorderDataOptions();
|
|
302
312
|
if (this._remote) this._remote._options.labels = this._dictionary.remote || {};
|
|
303
|
-
this._pagination?.
|
|
313
|
+
this._pagination?.updatePresentation(this._paginationOptions());
|
|
304
314
|
this._expandable?.refresh?.();
|
|
305
315
|
this._columns?.refresh?.();
|
|
306
316
|
this._rowReorder?.refresh?.();
|
|
307
317
|
const state = this._states?.getState?.();
|
|
308
318
|
if (state) this._states.render(state.type);
|
|
309
319
|
EventHandler.trigger(this._element, 'localechange.vg.table', {locale: normalized});
|
|
310
|
-
return normalized;
|
|
311
|
-
}
|
|
320
|
+
return normalized;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/** Текущий брейкпоинт и накопленные переопределения; null при выключенном responsive. */
|
|
324
|
+
getResponsiveState() { return this._responsive?.getState() || null; }
|
|
325
|
+
|
|
326
|
+
/** Принудительно обновляет только адаптивное представление, без загрузки данных. */
|
|
327
|
+
refreshResponsive() { return this._responsive?.refresh(true) || null; }
|
|
328
|
+
|
|
329
|
+
_paginationOptions() {
|
|
330
|
+
const base = this._params.pagination;
|
|
331
|
+
const profile = this._responsive?.getState().pagination || {};
|
|
332
|
+
return {...base, ...profile, size: {...base.size, ...profile.size}, quick: {...base.quick, ...profile.quick}};
|
|
333
|
+
}
|
|
312
334
|
|
|
313
335
|
_mergeLocale(target, source) {
|
|
314
336
|
if (!target || !source || typeof source !== 'object') return target;
|
|
@@ -524,7 +546,11 @@ class VGTable extends BaseModule {
|
|
|
524
546
|
assign('ellipsis', (value) => { pagination.ellipsis = boolean(value); });
|
|
525
547
|
assign('ellipsis-hover', (value) => { pagination.ellipsisHover = boolean(value); });
|
|
526
548
|
assign('ellipsis-after', (value) => { pagination.threshold = number(value, pagination.threshold); });
|
|
527
|
-
assign('max-visible-pages', (value) => { pagination.visible = number(value, pagination.visible); });
|
|
549
|
+
assign('max-visible-pages', (value) => { pagination.visible = number(value, pagination.visible); });
|
|
550
|
+
assign('max-buttons', (value) => {
|
|
551
|
+
const parsed = Number(value);
|
|
552
|
+
pagination.maxButtons = Number.isInteger(parsed) && parsed >= 3 ? parsed : null;
|
|
553
|
+
});
|
|
528
554
|
assign('show-per-page', (value) => { pagination.size.enabled = boolean(value); });
|
|
529
555
|
assign('show-per-page-label', (value) => { pagination.size.label = boolean(value) ? 'Строк на странице' : false; });
|
|
530
556
|
assign('per-page-label', (value) => { pagination.size.label = value; });
|
|
@@ -1137,7 +1163,7 @@ class VGTable extends BaseModule {
|
|
|
1137
1163
|
|
|
1138
1164
|
_refreshColumnsLayout(structure = false) {
|
|
1139
1165
|
this._sorting?.refresh?.(structure);
|
|
1140
|
-
this._stickyHeader?.
|
|
1166
|
+
this._stickyHeader?.refreshIntrinsicMinimums?.();
|
|
1141
1167
|
this._fixedColumns?.refresh?.();
|
|
1142
1168
|
}
|
|
1143
1169
|
|
|
@@ -1426,17 +1452,18 @@ class VGTable extends BaseModule {
|
|
|
1426
1452
|
_afterRemoteRender(detail = {}) {
|
|
1427
1453
|
if (detail.renderedCount > 0) this._states?.clear();
|
|
1428
1454
|
this._columns?.refresh?.();
|
|
1429
|
-
this._sorting?.refresh?.();
|
|
1430
|
-
this._expandable?.refresh?.();
|
|
1431
|
-
this._rowReorder?.refresh?.();
|
|
1432
|
-
this._stickyHeader?.
|
|
1433
|
-
this._fixedColumns?.refresh?.();
|
|
1455
|
+
this._sorting?.refresh?.();
|
|
1456
|
+
this._expandable?.refresh?.();
|
|
1457
|
+
this._rowReorder?.refresh?.();
|
|
1458
|
+
this._stickyHeader?.refreshIntrinsicMinimums?.();
|
|
1459
|
+
this._fixedColumns?.refresh?.();
|
|
1434
1460
|
}
|
|
1435
1461
|
|
|
1436
1462
|
/**
|
|
1437
1463
|
* Очистка ресурсов
|
|
1438
1464
|
*/
|
|
1439
|
-
dispose() {
|
|
1465
|
+
dispose() {
|
|
1466
|
+
this._responsive?.dispose();
|
|
1440
1467
|
if (this._remote) this._remote.dispose();
|
|
1441
1468
|
if (this._skeleton) this._skeleton.dispose();
|
|
1442
1469
|
if (this._urlState) this._urlState.dispose();
|
|
@@ -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
|
+
}
|