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
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Описание: адаптивная навигация VGNav с выпадающими пунктами и гамбургером.
|
|
3
|
+
* Возможности: click/hover, вложенные меню, позиционирование, события и интеграция с VGSidebar.
|
|
4
|
+
*/
|
|
5
|
+
import BaseModule from "../../base-module";
|
|
2
6
|
import Selectors from "../../../utils/js/dom/selectors";
|
|
3
7
|
import {
|
|
4
8
|
execute,
|
|
@@ -29,7 +33,7 @@ const CLASS_NAME_ACTIVE = 'active';
|
|
|
29
33
|
/**
|
|
30
34
|
* Constants toggle
|
|
31
35
|
*/
|
|
32
|
-
const SELECTOR_DATA_TOGGLE = '.' + CLASS_NAME + ' a';
|
|
36
|
+
const SELECTOR_DATA_TOGGLE = '.' + CLASS_NAME + ' .dropdown > a';
|
|
33
37
|
|
|
34
38
|
/**
|
|
35
39
|
* Constants Events
|
|
@@ -215,34 +219,43 @@ class VGNav extends BaseModule {
|
|
|
215
219
|
}
|
|
216
220
|
}
|
|
217
221
|
|
|
218
|
-
show(relatedTarget) {
|
|
219
|
-
let target = relatedTarget.relatedTarget;
|
|
220
|
-
|
|
221
|
-
if (!target || isDisabled(target)) return;
|
|
222
|
-
|
|
223
|
-
if (!
|
|
224
|
-
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
if (
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
222
|
+
show(relatedTarget) {
|
|
223
|
+
let target = relatedTarget.relatedTarget;
|
|
224
|
+
|
|
225
|
+
if (!target || isDisabled(target)) return;
|
|
226
|
+
const drop = Selectors.find(':scope > .dropdown-content', target);
|
|
227
|
+
if (!drop) return;
|
|
228
|
+
|
|
229
|
+
const showEvent = EventHandler.trigger(target, EVENT_KEY_SHOW, { relatedTarget });
|
|
230
|
+
if (showEvent.defaultPrevented) return;
|
|
231
|
+
|
|
232
|
+
if (!target.closest('.dropdown-content')) {
|
|
233
|
+
target.classList.add('first');
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const link = target.firstElementChild;
|
|
232
237
|
|
|
233
238
|
if (link) link.setAttribute('aria-expanded', 'true');
|
|
234
|
-
drop.classList.add(CLASS_NAME_SHOW);
|
|
235
|
-
target.classList.add(CLASS_NAME_ACTIVE);
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
239
|
+
drop.classList.add(CLASS_NAME_SHOW);
|
|
240
|
+
target.classList.add(CLASS_NAME_ACTIVE);
|
|
241
|
+
// Координаты Placement имеют приоритет над старой CSS-схемой top/bottom/right.
|
|
242
|
+
drop.style.bottom = 'auto';
|
|
243
|
+
drop.style.right = 'auto';
|
|
244
|
+
const openToSide = !!target.closest('.dropdown-content') || this._params.placement === 'vertical';
|
|
245
|
+
|
|
246
|
+
const $placement = new Placement({
|
|
247
|
+
reference: target,
|
|
248
|
+
drop: drop,
|
|
249
|
+
placement: openToSide ? 'right-start' : 'bottom-start',
|
|
250
|
+
fallbackPlacements: openToSide
|
|
251
|
+
? ['left-start', 'right-end', 'left-end', 'bottom-start', 'top-start']
|
|
252
|
+
: ['top-start', 'bottom-end', 'top-end'],
|
|
253
|
+
offset: openToSide ? [6, 0] : [0, 6],
|
|
254
|
+
boundary: 'clippingParents',
|
|
255
|
+
autoFlip: true,
|
|
256
|
+
overflowProtection: true,
|
|
257
|
+
clamp: true,
|
|
258
|
+
isMerge: false
|
|
246
259
|
});
|
|
247
260
|
|
|
248
261
|
$placement._setPlacement();
|
|
@@ -264,13 +277,8 @@ class VGNav extends BaseModule {
|
|
|
264
277
|
this._queueCallback(completeCallBack, drop, true, 10);
|
|
265
278
|
}
|
|
266
279
|
|
|
267
|
-
hide(relatedTarget) {
|
|
268
|
-
const _this = this;
|
|
269
|
-
if ('ontouchstart' in document.documentElement) {
|
|
270
|
-
for (const element of [].concat(...document.body.children)) {
|
|
271
|
-
EventHandler.off(element, 'mouseover', noop);
|
|
272
|
-
}
|
|
273
|
-
}
|
|
280
|
+
hide(relatedTarget) {
|
|
281
|
+
const _this = this;
|
|
274
282
|
|
|
275
283
|
let element = relatedTarget.relatedTarget;
|
|
276
284
|
|
|
@@ -288,7 +296,7 @@ class VGNav extends BaseModule {
|
|
|
288
296
|
element.classList.remove('first');
|
|
289
297
|
}
|
|
290
298
|
|
|
291
|
-
[...Selectors.findAll('.' + CLASS_NAME_SHOW, element)].forEach(function (el
|
|
299
|
+
[...Selectors.findAll('.dropdown-content.' + CLASS_NAME_SHOW, element)].forEach(function (el) {
|
|
292
300
|
el.classList.remove(CLASS_NAME_FADE);
|
|
293
301
|
|
|
294
302
|
let parent = el.closest('.dropdown');
|
|
@@ -299,21 +307,14 @@ class VGNav extends BaseModule {
|
|
|
299
307
|
let link = el.previousElementSibling;
|
|
300
308
|
if (link) link.setAttribute('aria-expanded', 'false');
|
|
301
309
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
const dropData = _this._openDrops.get(el);
|
|
312
|
-
if (dropData) {
|
|
313
|
-
window.removeEventListener('scroll', dropData.scrollHandler, { capture: true });
|
|
314
|
-
window.removeEventListener('resize', dropData.resizeHandler);
|
|
315
|
-
_this._openDrops.delete(el);
|
|
316
|
-
}
|
|
310
|
+
const completeCallback = () => {
|
|
311
|
+
if (parent.classList.contains(CLASS_NAME_ACTIVE)) return;
|
|
312
|
+
el.classList.remove(CLASS_NAME_SHOW);
|
|
313
|
+
EventHandler.trigger(el, EVENT_KEY_HIDDEN, relatedTarget);
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
_this._queueCallback(completeCallback, el, true, 500);
|
|
317
|
+
_this._cleanupDrop(el);
|
|
317
318
|
});
|
|
318
319
|
}
|
|
319
320
|
}
|
|
@@ -341,13 +342,16 @@ class VGNav extends BaseModule {
|
|
|
341
342
|
}
|
|
342
343
|
}
|
|
343
344
|
|
|
344
|
-
_cleanupDrop(drop) {
|
|
345
|
-
const dropData = this._openDrops.get(drop);
|
|
346
|
-
if (dropData) {
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
this._openDrops.
|
|
350
|
-
|
|
345
|
+
_cleanupDrop(drop) {
|
|
346
|
+
const dropData = this._openDrops.get(drop);
|
|
347
|
+
if (dropData) {
|
|
348
|
+
this._openDrops.delete(drop);
|
|
349
|
+
// Обработчики общие для экземпляра: другие открытые уровни ещё нуждаются в них.
|
|
350
|
+
if (!this._openDrops.size) {
|
|
351
|
+
window.removeEventListener('scroll', dropData.scrollHandler, { capture: true });
|
|
352
|
+
window.removeEventListener('resize', dropData.resizeHandler);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
351
355
|
}
|
|
352
356
|
|
|
353
357
|
_isElementInViewport(el) {
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Описание: визуальные режимы VGRangeSlider — default, ruler и status.
|
|
3
|
+
* Возможности: шкала с адаптивными координатами, активные отметки и пороговые цвета с текстовыми статусами.
|
|
4
|
+
*/
|
|
5
|
+
const SKIN_CLASS_DEFAULT = 'is-skin-default';
|
|
2
6
|
const SKIN_CLASS_RULER = 'is-skin-ruler';
|
|
3
7
|
const SKIN_CLASS_STATUS = 'is-skin-status';
|
|
4
8
|
const SKIN_CLASS_RULER_DIM = 'is-ruler-dim-inactive';
|
|
@@ -206,8 +210,9 @@ const syncRangeSliderSkin = (skin, state, helpers) => {
|
|
|
206
210
|
|
|
207
211
|
const isRange = state.from !== state.to || helpers.isRange;
|
|
208
212
|
|
|
209
|
-
skin.ticks.forEach((tick, index) => {
|
|
210
|
-
const value = skin.values[index];
|
|
213
|
+
skin.ticks.forEach((tick, index) => {
|
|
214
|
+
const value = skin.values[index];
|
|
215
|
+
tick.style.left = `${helpers.toPositionPx(value)}px`;
|
|
211
216
|
const isActive = isRange
|
|
212
217
|
? value >= state.from && value <= state.to
|
|
213
218
|
: value <= state.from;
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Описание: одиночные и диапазонные слайдеры VGRangeSlider на нативных range-input.
|
|
3
|
+
* Возможности: Data API, шкала и статусы, форматирование, синхронизация формы, события и управление значениями.
|
|
4
|
+
*/
|
|
5
|
+
import BaseModule from "../../base-module";
|
|
2
6
|
import EventHandler from "../../../utils/js/dom/event";
|
|
3
7
|
import Selectors from "../../../utils/js/dom/selectors";
|
|
4
8
|
import { mergeDeepObject, normalizeData } from "../../../utils/js/functions";
|
|
@@ -259,9 +263,10 @@ class VGRangeSlider extends BaseModule {
|
|
|
259
263
|
this._dom.labelFrom = Selectors.find('.vg-range-slider__label--from', root);
|
|
260
264
|
this._dom.labelTo = Selectors.find('.vg-range-slider__label--to', root);
|
|
261
265
|
this._dom.labelSeparator = Selectors.find('.vg-range-slider__separator', root);
|
|
262
|
-
this._dom.hiddenMin
|
|
263
|
-
this._dom.hiddenMax
|
|
264
|
-
this._dom.skin = applyRangeSliderSkin(root, this._params, this._state, {
|
|
266
|
+
this._dom.hiddenMin ||= root.querySelector('input[data-vgrangeslider-hidden="min"]');
|
|
267
|
+
this._dom.hiddenMax ||= root.querySelector('input[data-vgrangeslider-hidden="max"]');
|
|
268
|
+
this._dom.skin = applyRangeSliderSkin(root, this._params, this._state, {
|
|
269
|
+
isRange: this._isRange,
|
|
265
270
|
formatValue: (value) => this._formatValue(value),
|
|
266
271
|
toPositionPx: (value) => this._toPositionPx(value),
|
|
267
272
|
});
|
|
@@ -338,11 +343,11 @@ class VGRangeSlider extends BaseModule {
|
|
|
338
343
|
root.appendChild(this._createLabels(this._isRange));
|
|
339
344
|
}
|
|
340
345
|
|
|
341
|
-
if (this._isRange) {
|
|
342
|
-
this._ensureHiddenInput(root, 'min', this._params.input?.min, this._params.name?.min);
|
|
343
|
-
this._ensureHiddenInput(root, 'max', this._params.input?.max, this._params.name?.max);
|
|
344
|
-
} else if (this._params.name?.min) {
|
|
345
|
-
this._ensureHiddenInput(root, 'min', this._params.input?.min, this._params.name.min);
|
|
346
|
+
if (this._isRange) {
|
|
347
|
+
this._dom.hiddenMin = this._ensureHiddenInput(root, 'min', this._params.input?.min, this._params.name?.min);
|
|
348
|
+
this._dom.hiddenMax = this._ensureHiddenInput(root, 'max', this._params.input?.max, this._params.name?.max);
|
|
349
|
+
} else if (this._params.name?.min) {
|
|
350
|
+
this._dom.hiddenMin = this._ensureHiddenInput(root, 'min', this._params.input?.min, this._params.name.min);
|
|
346
351
|
}
|
|
347
352
|
|
|
348
353
|
return root;
|
|
@@ -399,7 +404,8 @@ class VGRangeSlider extends BaseModule {
|
|
|
399
404
|
if (name) input.name = name;
|
|
400
405
|
root.appendChild(input);
|
|
401
406
|
}
|
|
402
|
-
input.setAttribute('data-vgrangeslider-hidden', role);
|
|
407
|
+
input.setAttribute('data-vgrangeslider-hidden', role);
|
|
408
|
+
return input;
|
|
403
409
|
}
|
|
404
410
|
|
|
405
411
|
_bindEvents() {
|
|
@@ -442,7 +448,8 @@ class VGRangeSlider extends BaseModule {
|
|
|
442
448
|
window.addEventListener('resize', this._handleResize);
|
|
443
449
|
}
|
|
444
450
|
|
|
445
|
-
_syncUI() {
|
|
451
|
+
_syncUI() {
|
|
452
|
+
if (!this._isRange) this._state.to = this._state.from;
|
|
446
453
|
const fromPercent = this._toPercent(this._state.from);
|
|
447
454
|
const toPercent = this._toPercent(this._isRange ? this._state.to : this._state.from);
|
|
448
455
|
const fromPosition = this._toPositionPx(this._state.from);
|
|
@@ -467,10 +474,11 @@ class VGRangeSlider extends BaseModule {
|
|
|
467
474
|
if (this._dom.labelSeparator) this._dom.labelSeparator.hidden = !this._isRange;
|
|
468
475
|
|
|
469
476
|
this._syncTargets();
|
|
470
|
-
syncRangeSliderSkin(this._dom.skin, this._state, {
|
|
471
|
-
isRange: this._isRange,
|
|
472
|
-
params: this._params,
|
|
473
|
-
dom: this._dom,
|
|
477
|
+
syncRangeSliderSkin(this._dom.skin, this._state, {
|
|
478
|
+
isRange: this._isRange,
|
|
479
|
+
params: this._params,
|
|
480
|
+
dom: this._dom,
|
|
481
|
+
toPositionPx: (value) => this._toPositionPx(value),
|
|
474
482
|
});
|
|
475
483
|
}
|
|
476
484
|
|
|
@@ -565,8 +573,12 @@ class VGRangeSlider extends BaseModule {
|
|
|
565
573
|
const numeric = Number(normalizeData(value));
|
|
566
574
|
const safeValue = Number.isFinite(numeric) ? numeric : fallback;
|
|
567
575
|
const clamped = Math.min(Math.max(safeValue, min), max);
|
|
568
|
-
const
|
|
569
|
-
const
|
|
576
|
+
const configuredStep = Number(this._state?.step ?? this._params.step);
|
|
577
|
+
const step = Number.isFinite(configuredStep) && configuredStep > 0 ? configuredStep : 1;
|
|
578
|
+
// Округление не должно выводить значение за max или с нативной сетки шага.
|
|
579
|
+
const maxSteps = Math.floor((max - min) / step + 1e-9);
|
|
580
|
+
const steps = Math.min(Math.round((clamped - min) / step), maxSteps);
|
|
581
|
+
const stepped = steps * step + min;
|
|
570
582
|
return Number(stepped.toFixed(5));
|
|
571
583
|
}
|
|
572
584
|
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Описание: сворачивание текста и списков VGRollup.
|
|
3
|
+
* Возможности: ограничение высоты, строк и количества элементов, локализация кнопок, Data API, callbacks и события.
|
|
4
|
+
*/
|
|
5
|
+
import BaseModule from "../../base-module";
|
|
2
6
|
import { execute, isDisabled, mergeDeepObject } from "../../../utils/js/functions";
|
|
3
7
|
import EventHandler from "../../../utils/js/dom/event";
|
|
4
8
|
import Selectors from "../../../utils/js/dom/selectors";
|
|
@@ -98,7 +102,7 @@ class VGRollup extends BaseModule {
|
|
|
98
102
|
}
|
|
99
103
|
};
|
|
100
104
|
|
|
101
|
-
|
|
105
|
+
const lang = Manipulator.get(element, 'data-lang') || params.lang || defaultParams.lang;
|
|
102
106
|
|
|
103
107
|
// Локализация текстов кнопок
|
|
104
108
|
defaultParams.button.more = lang_buttons(lang, NAME)['show'];
|
|
@@ -288,7 +292,8 @@ class VGRollup extends BaseModule {
|
|
|
288
292
|
|
|
289
293
|
if (isEllipsis && line) {
|
|
290
294
|
Classes.add(element, this.classes.ellipsis);
|
|
291
|
-
element.style.lineClamp = Number(line);
|
|
295
|
+
element.style.lineClamp = Number(line);
|
|
296
|
+
element.style.setProperty('-webkit-line-clamp', String(Number(line)));
|
|
292
297
|
} else if (isEllipsis) {
|
|
293
298
|
console.error("Переменная [data-line] или параметр[line] не должны быть пустыми");
|
|
294
299
|
}
|
|
@@ -377,13 +382,14 @@ class VGRollup extends BaseModule {
|
|
|
377
382
|
|
|
378
383
|
if (this._params.ellipsis.line) {
|
|
379
384
|
Classes.add(el, this.classes.ellipsis);
|
|
380
|
-
el.style.lineClamp = this._params.ellipsis.line;
|
|
385
|
+
el.style.lineClamp = this._params.ellipsis.line;
|
|
386
|
+
el.style.setProperty('-webkit-line-clamp', String(this._params.ellipsis.line));
|
|
381
387
|
}
|
|
382
388
|
|
|
383
389
|
if (this._params.fade) Classes.add(el, this.classes.fade);
|
|
384
390
|
if (this._params.transition) Classes.add(el, this.classes.transition);
|
|
385
391
|
|
|
386
|
-
execute(this._params.callbacks.
|
|
392
|
+
execute(this._params.callbacks.collapse, [el, this])
|
|
387
393
|
} else if (content === 'elements') {
|
|
388
394
|
const items = Selectors.findAll('.' + this._params.elements, el);
|
|
389
395
|
items.forEach((item, index) => {
|
|
@@ -399,7 +405,9 @@ class VGRollup extends BaseModule {
|
|
|
399
405
|
} else {
|
|
400
406
|
const { hidden, ellipsis, fade } = this.classes;
|
|
401
407
|
Classes.remove(el, [hidden, ellipsis, fade]);
|
|
402
|
-
|
|
408
|
+
el.style.removeProperty('height');
|
|
409
|
+
el.style.removeProperty('line-clamp');
|
|
410
|
+
el.style.removeProperty('-webkit-line-clamp');
|
|
403
411
|
|
|
404
412
|
if (this._params.content === 'elements') {
|
|
405
413
|
const items = Selectors.findAll('.' + this._params.elements, el);
|
|
@@ -455,4 +463,4 @@ EventHandler.on(document, EVENT_KEY_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, functi
|
|
|
455
463
|
VGRollup.toggle(target, this);
|
|
456
464
|
});
|
|
457
465
|
|
|
458
|
-
export default VGRollup;
|
|
466
|
+
export default VGRollup;
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Описание: константы и параметры по умолчанию базовой таблицы VGTable.
|
|
3
|
-
* Возможности: единая настройка Data API, i18n, wrapper, remote
|
|
4
|
-
*/
|
|
3
|
+
* Возможности: единая настройка Data API, i18n, wrapper, remote, фильтров, sticky, колонок, строк, выбора и адаптивной пагинации.
|
|
4
|
+
*/
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Служебные константы модуля.
|
|
8
8
|
*/
|
|
9
9
|
// Имя компонента в публичном API. Варианты: фиксированное значение 'table'.
|
|
10
|
-
const NAME = 'table';
|
|
10
|
+
const NAME = 'table';
|
|
11
11
|
// Ключ экземпляра в Data API. Варианты: фиксированное значение 'vg.table'.
|
|
12
12
|
const NAME_KEY = `vg.${NAME}`;
|
|
13
13
|
// Селектор автоматической инициализации. Варианты: элементы с атрибутом data-vg-table.
|
|
@@ -21,7 +21,16 @@ const TABLE_CONTAINER_SELECTOR = '.vg-table-container';
|
|
|
21
21
|
// Маркер container, созданного компонентом. Варианты: пустой data-атрибут.
|
|
22
22
|
const GENERATED_TABLE_CONTAINER_ATTRIBUTE = 'data-vg-table-generated-container';
|
|
23
23
|
|
|
24
|
-
const DEFAULT_OPTIONS = {
|
|
24
|
+
const DEFAULT_OPTIONS = {
|
|
25
|
+
/** Профили представления: базовые параметры + все достигнутые границы xs–xxl. */
|
|
26
|
+
responsive: {
|
|
27
|
+
// Включается явно; Data API: data-responsive-enabled.
|
|
28
|
+
enabled: false,
|
|
29
|
+
// Локальные переопределения границ общего Responsive; пустой объект сохраняет глобальные значения.
|
|
30
|
+
breakpoints: {},
|
|
31
|
+
// Профили принимают pagination: maxButtons, align, position, size.enabled/label, quick.enabled.
|
|
32
|
+
xs: {}, sm: {}, md: {}, lg: {}, xl: {}, xxl: {},
|
|
33
|
+
},
|
|
25
34
|
// Активная локаль встроенного интерфейса. Data API: data-locale. Варианты: 'ru' | 'en' | региональный BCP 47 код.
|
|
26
35
|
locale: 'ru',
|
|
27
36
|
// Пользовательские словари, объединяемые со встроенными ru/en. Data API отсутствует; варианты: объект {locale: {group: {key: value}}}.
|
|
@@ -359,7 +368,9 @@ const DEFAULT_OPTIONS = {
|
|
|
359
368
|
// Задаёт число страниц, после которого разрешены многоточие и quick='auto'. Варианты: целое число >= 1.
|
|
360
369
|
threshold: 5,
|
|
361
370
|
// Задаёт максимальное число соседних страниц между первой и последней. Варианты: целое число >= 1.
|
|
362
|
-
visible: 5,
|
|
371
|
+
visible: 5,
|
|
372
|
+
// Лимит номеров и многоточий без prev/next; null сохраняет visible/threshold. Data API: data-pagination-max-buttons.
|
|
373
|
+
maxButtons: null,
|
|
363
374
|
|
|
364
375
|
/** Поле и VGDropdown для выбора количества строк. */
|
|
365
376
|
size: {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Описание: локальная и remote-пагинация строк базовой таблицы VGTable.
|
|
3
|
-
* Возможности: выбор страницы и размера, серверная meta,
|
|
3
|
+
* Возможности: выбор страницы и размера, серверная meta, лимит кнопок, responsive-представление без запросов, многоточия, быстрый переход и сохранение.
|
|
4
4
|
*/
|
|
5
5
|
import EventHandler from "../../../utils/js/dom/event";
|
|
6
6
|
import VGDropdown from "../../vgdropdown";
|
|
@@ -16,7 +16,10 @@ class _pagination {
|
|
|
16
16
|
this._options = options;
|
|
17
17
|
this._host = table.closest('.vg-table-wrapper') || table.parentElement;
|
|
18
18
|
this._containers = [];
|
|
19
|
-
this._dropdowns = [];
|
|
19
|
+
this._dropdowns = [];
|
|
20
|
+
this._externalVisibility = new Map();
|
|
21
|
+
this._manageExternalVisibility = options.responsive === true;
|
|
22
|
+
this._updatingPresentation = false;
|
|
20
23
|
this._page = this._positiveInt(options.page, 1);
|
|
21
24
|
this._perPage = this._clampPerPage(options.per);
|
|
22
25
|
this._remote = options.remote === true;
|
|
@@ -29,20 +32,24 @@ class _pagination {
|
|
|
29
32
|
this._boundFocusOut = this._handleFocusOut.bind(this);
|
|
30
33
|
}
|
|
31
34
|
|
|
32
|
-
init() {
|
|
33
|
-
this._restoreState();
|
|
34
|
-
this._containers = this._resolveContainers();
|
|
35
|
-
this.
|
|
35
|
+
init() {
|
|
36
|
+
this._restoreState();
|
|
37
|
+
this._containers = this._resolveContainers();
|
|
38
|
+
this._bindContainers();
|
|
39
|
+
this.refresh();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
_bindContainers() {
|
|
43
|
+
this._containers.forEach((container) => {
|
|
36
44
|
container.addEventListener('click', this._boundClick);
|
|
37
45
|
container.addEventListener('change', this._boundChange);
|
|
38
46
|
container.addEventListener('keydown', this._boundKeydown);
|
|
39
47
|
container.addEventListener('focusin', this._boundFocusIn);
|
|
40
48
|
container.addEventListener('focusout', this._boundFocusOut);
|
|
41
49
|
});
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
dispose() {
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
_releaseContainers() {
|
|
46
53
|
this._disposeDropdowns();
|
|
47
54
|
this._containers.forEach((container) => {
|
|
48
55
|
container.removeEventListener('click', this._boundClick);
|
|
@@ -50,15 +57,69 @@ class _pagination {
|
|
|
50
57
|
container.removeEventListener('keydown', this._boundKeydown);
|
|
51
58
|
container.removeEventListener('focusin', this._boundFocusIn);
|
|
52
59
|
container.removeEventListener('focusout', this._boundFocusOut);
|
|
53
|
-
if (container.hasAttribute(GENERATED_ATTRIBUTE)) container.remove();
|
|
54
|
-
});
|
|
60
|
+
if (container.hasAttribute(GENERATED_ATTRIBUTE)) container.remove();
|
|
61
|
+
});
|
|
62
|
+
this._containers = [];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
dispose() {
|
|
66
|
+
this._releaseContainers();
|
|
67
|
+
this._externalVisibility.forEach((hidden, container) => { container.hidden = hidden; });
|
|
68
|
+
this._externalVisibility.clear();
|
|
55
69
|
this._rows().forEach((row) => {
|
|
56
70
|
row.hidden = row.getAttribute('data-vg-table-expand-hidden') === 'true'
|
|
57
71
|
|| row.hasAttribute('data-vg-table-filter-hidden');
|
|
58
72
|
row.removeAttribute('data-vg-table-page-row');
|
|
59
73
|
});
|
|
60
|
-
this._containers = [];
|
|
61
|
-
}
|
|
74
|
+
this._containers = [];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Обновляет только панели: не трогает строки, page/per, storage, scroll или onChange. */
|
|
78
|
+
updatePresentation(options) {
|
|
79
|
+
const focus = this._captureFocus();
|
|
80
|
+
const position = this._position();
|
|
81
|
+
this._updatingPresentation = true;
|
|
82
|
+
try {
|
|
83
|
+
this._options = {...this._options, ...options};
|
|
84
|
+
if (position !== this._position()) {
|
|
85
|
+
this._manageExternalVisibility = true;
|
|
86
|
+
this._releaseContainers();
|
|
87
|
+
this._containers = this._resolveContainers();
|
|
88
|
+
this._bindContainers();
|
|
89
|
+
}
|
|
90
|
+
this._renderControls();
|
|
91
|
+
} finally {
|
|
92
|
+
this._updatingPresentation = false;
|
|
93
|
+
}
|
|
94
|
+
this._restoreFocus(focus);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
_captureFocus() {
|
|
98
|
+
const active = this._table.ownerDocument.activeElement;
|
|
99
|
+
const container = this._containers.find((item) => item.contains(active));
|
|
100
|
+
if (!container) return null;
|
|
101
|
+
const attribute = ['data-pagination-page', 'data-pagination-per-page', 'data-pagination-quick-input', 'data-pagination-quick-button']
|
|
102
|
+
.find((name) => active.hasAttribute(name));
|
|
103
|
+
return {
|
|
104
|
+
position: this._positionFor(container), attribute, value: attribute ? active.getAttribute(attribute) : null,
|
|
105
|
+
inputValue: active.tagName === 'INPUT' ? active.value : null,
|
|
106
|
+
start: active.selectionStart, end: active.selectionEnd,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
_restoreFocus(focus) {
|
|
111
|
+
if (!focus) return;
|
|
112
|
+
const container = this._containers.find((item) => this._positionFor(item) === focus.position) || this._containers[0];
|
|
113
|
+
if (!container) return;
|
|
114
|
+
const matching = focus.attribute ? Array.from(container.querySelectorAll(`[${focus.attribute}]`))
|
|
115
|
+
.find((item) => !item.disabled && item.getAttribute(focus.attribute) === focus.value) : null;
|
|
116
|
+
const target = matching || container.querySelector('[aria-current="page"]');
|
|
117
|
+
target?.focus({preventScroll: true});
|
|
118
|
+
if (matching && focus.inputValue !== null) {
|
|
119
|
+
matching.value = focus.inputValue;
|
|
120
|
+
if (focus.start !== null && focus.start !== undefined) matching.setSelectionRange(focus.start, focus.end);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
62
123
|
|
|
63
124
|
setPage(page, emit = false, source = 'api') {
|
|
64
125
|
const nextPage = this._clampPage(page);
|
|
@@ -131,7 +192,11 @@ class _pagination {
|
|
|
131
192
|
});
|
|
132
193
|
}
|
|
133
194
|
|
|
134
|
-
|
|
195
|
+
this._renderControls();
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
_renderControls() {
|
|
199
|
+
const markup = this._buildMarkup();
|
|
135
200
|
this._disposeDropdowns();
|
|
136
201
|
this._containers.forEach((container) => {
|
|
137
202
|
container.className = `vg-table-pagination vg-table-pagination--${this._positionFor(container)}`;
|
|
@@ -221,8 +286,10 @@ class _pagination {
|
|
|
221
286
|
return `<svg class="vg-table-page__ellipsis-chevron vg-table-page__ellipsis-chevron--${direction}" viewBox="0 0 20 20" focusable="false" aria-hidden="true">${paths}</svg>`;
|
|
222
287
|
}
|
|
223
288
|
|
|
224
|
-
_buildPages() {
|
|
225
|
-
const total = this._totalPages();
|
|
289
|
+
_buildPages() {
|
|
290
|
+
const total = this._totalPages();
|
|
291
|
+
const maxButtons = this._options.maxButtons;
|
|
292
|
+
if (Number.isInteger(maxButtons) && maxButtons >= 3) return this._buildLimitedPages(total, maxButtons);
|
|
226
293
|
const visible = Math.max(1, this._positiveInt(this._options.visible, 5));
|
|
227
294
|
const threshold = Math.max(1, this._positiveInt(this._options.threshold, 5));
|
|
228
295
|
if (this._options.ellipsis === false || total <= threshold || total <= visible + 2) {
|
|
@@ -237,8 +304,30 @@ class _pagination {
|
|
|
237
304
|
for (let page = start; page <= end; page += 1) pages.push(page);
|
|
238
305
|
if (end < total - 1) pages.push({direction: 'next'});
|
|
239
306
|
pages.push(total);
|
|
240
|
-
return pages;
|
|
241
|
-
}
|
|
307
|
+
return pages;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
_buildLimitedPages(total, maximum) {
|
|
311
|
+
if (total <= maximum) return this._range(1, total);
|
|
312
|
+
if (maximum < 5 || this._options.ellipsis === false) {
|
|
313
|
+
const start = Math.max(1, Math.min(this._page - Math.floor(maximum / 2), total - maximum + 1));
|
|
314
|
+
return this._range(start, start + maximum - 1);
|
|
315
|
+
}
|
|
316
|
+
// Резервируем края и подбираем наибольшее окно, учитывая оба многоточия в лимите.
|
|
317
|
+
for (let count = maximum - 2; count >= 1; count -= 1) {
|
|
318
|
+
const start = Math.max(2, Math.min(this._page - Math.floor(count / 2), total - count));
|
|
319
|
+
const end = start + count - 1;
|
|
320
|
+
const pages = [1];
|
|
321
|
+
if (start === 3) pages.push(2);
|
|
322
|
+
else if (start > 3) pages.push({direction: 'prev'});
|
|
323
|
+
pages.push(...this._range(start, end));
|
|
324
|
+
if (end === total - 2) pages.push(total - 1);
|
|
325
|
+
else if (end < total - 2) pages.push({direction: 'next'});
|
|
326
|
+
pages.push(total);
|
|
327
|
+
if (pages.length <= maximum) return pages;
|
|
328
|
+
}
|
|
329
|
+
return [this._page];
|
|
330
|
+
}
|
|
242
331
|
|
|
243
332
|
_handleClick(event) {
|
|
244
333
|
const sizeOption = event.target.closest('[data-pagination-per-page-option]');
|
|
@@ -297,7 +386,8 @@ class _pagination {
|
|
|
297
386
|
if (input) input.value = String(this._perPage);
|
|
298
387
|
}
|
|
299
388
|
|
|
300
|
-
_handleFocusOut(event) {
|
|
389
|
+
_handleFocusOut(event) {
|
|
390
|
+
if (this._updatingPresentation) return;
|
|
301
391
|
const input = event.target.closest('[data-pagination-per-page]');
|
|
302
392
|
if (!input) return;
|
|
303
393
|
const related = event.relatedTarget instanceof Element ? event.relatedTarget : null;
|
|
@@ -323,10 +413,14 @@ class _pagination {
|
|
|
323
413
|
_resolveContainers() {
|
|
324
414
|
if (!this._host) return [];
|
|
325
415
|
const position = this._position();
|
|
326
|
-
const existing = Array.from(this._host.querySelectorAll('[data-vg-table-pagination]'));
|
|
327
|
-
if (existing.length) {
|
|
328
|
-
|
|
329
|
-
|
|
416
|
+
const existing = Array.from(this._host.querySelectorAll('[data-vg-table-pagination]'));
|
|
417
|
+
if (existing.length) {
|
|
418
|
+
const selected = position === 'both' ? existing : [existing.find((item) => item.getAttribute('data-position') === position) || existing[0]];
|
|
419
|
+
if (this._manageExternalVisibility) existing.forEach((container) => {
|
|
420
|
+
if (!this._externalVisibility.has(container)) this._externalVisibility.set(container, container.hidden);
|
|
421
|
+
container.hidden = !selected.includes(container);
|
|
422
|
+
});
|
|
423
|
+
return selected;
|
|
330
424
|
}
|
|
331
425
|
|
|
332
426
|
const positions = position === 'both' ? ['top', 'bottom'] : [position];
|