vgapp 1.5.5 → 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/vgtable.js +38 -11
- 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
|
* Описание: основной модуль базовых таблиц 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;
|
|
@@ -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; });
|
|
@@ -1436,7 +1462,8 @@ class VGTable extends BaseModule {
|
|
|
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
|
+
}
|