vgapp 1.5.2 → 1.5.4
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/vgfiles/scss/_mixins.scss +2 -1
- package/app/modules/vgtable/js/_sticky-header.js +106 -23
- package/app/modules/vgtable/js/vgtable.js +51 -15
- package/app/modules/vgtable/scss/_sticky-header.scss +3 -3
- 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/package.json +1 -1
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
|
|
6
6
|
const MODE_CLASSES = ['vg-table-wrapper--sticky-container', 'vg-table-wrapper--sticky-page'];
|
|
7
7
|
const CONTAINER_MODE_CLASSES = ['vg-table-container--sticky-container', 'vg-table-container--sticky-page'];
|
|
8
|
-
const
|
|
8
|
+
const CONTENT_MIN_WIDTH_PROPERTY = '--vg-table-sticky-content-min-width';
|
|
9
|
+
const STYLE_PROPERTIES = ['--vg-table-sticky-top', '--vg-table-sticky-max-height', CONTENT_MIN_WIDTH_PROPERTY];
|
|
9
10
|
const GENERATED_LAYER_ATTRIBUTE = 'data-vg-table-sticky-generated';
|
|
10
11
|
|
|
11
12
|
class _stickyHeader {
|
|
@@ -23,6 +24,7 @@ class _stickyHeader {
|
|
|
23
24
|
this._previousStyles = new Map();
|
|
24
25
|
this._columnWeights = new Map();
|
|
25
26
|
this._hardColumnWidths = new Map();
|
|
27
|
+
this._columnMinimumWidths = new Map();
|
|
26
28
|
this._hasSourceColgroup = false;
|
|
27
29
|
this._resizeObserver = null;
|
|
28
30
|
this._resizeFrame = null;
|
|
@@ -75,13 +77,20 @@ class _stickyHeader {
|
|
|
75
77
|
return this;
|
|
76
78
|
}
|
|
77
79
|
|
|
78
|
-
refresh() {
|
|
79
|
-
if (!this._header || !this._headerTable || !this._body) return this;
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
this.
|
|
83
|
-
|
|
84
|
-
|
|
80
|
+
refresh() {
|
|
81
|
+
if (!this._header || !this._headerTable || !this._body) return this;
|
|
82
|
+
|
|
83
|
+
let widths = this._resolveLayoutWidths();
|
|
84
|
+
const attempts = this._getHeaders().length + 2;
|
|
85
|
+
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
|
86
|
+
this._applyColgroup(this._headerTable, widths);
|
|
87
|
+
this._applyColgroup(this._element, widths);
|
|
88
|
+
const headerChanged = this._captureOverflowingHeaderMinimums();
|
|
89
|
+
const contentChanged = this._syncContentMinimumWidth(widths);
|
|
90
|
+
if (!headerChanged && !contentChanged) break;
|
|
91
|
+
widths = this._resolveLayoutWidths();
|
|
92
|
+
}
|
|
93
|
+
const scrollbar = Math.max(0, this._body.offsetWidth - this._body.clientWidth);
|
|
85
94
|
this._header.style.paddingInlineEnd = `${scrollbar}px`;
|
|
86
95
|
this._header.style.setProperty('--vg-table-sticky-scrollbar-width', `${scrollbar}px`);
|
|
87
96
|
this._header.classList.toggle('vg-table-header--scrollbar', scrollbar > 0);
|
|
@@ -215,6 +224,8 @@ class _stickyHeader {
|
|
|
215
224
|
this._columnWeights.set(key, widths[index] > 0 ? widths[index] : 1);
|
|
216
225
|
if (this._hasDeclaredWidth(header) || this._hasDeclaredWidth(sourceCols[index])) {
|
|
217
226
|
this._hardColumnWidths.set(key, widths[index] > 0 ? widths[index] : 0);
|
|
227
|
+
} else if (this._isEmptyHeader(header) && widths[index] > 0) {
|
|
228
|
+
this._columnMinimumWidths.set(key, widths[index]);
|
|
218
229
|
}
|
|
219
230
|
});
|
|
220
231
|
}
|
|
@@ -252,20 +263,87 @@ class _stickyHeader {
|
|
|
252
263
|
hard.forEach((width, index) => { widths[index] = width; });
|
|
253
264
|
|
|
254
265
|
if (!flexible.length) return widths;
|
|
255
|
-
|
|
266
|
+
this._distributeFlexibleWidths(widths, flexible, available, headers, measured);
|
|
267
|
+
return widths;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
_distributeFlexibleWidths(widths, indexes, available, headers, measured) {
|
|
271
|
+
let pending = indexes.slice();
|
|
272
|
+
let remaining = available;
|
|
273
|
+
const minimumTotal = pending.reduce((total, index) => {
|
|
256
274
|
const key = this._columnKey(headers[index], index);
|
|
257
|
-
return this.
|
|
258
|
-
});
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
275
|
+
return total + (this._columnMinimumWidths.get(key) || 0);
|
|
276
|
+
}, 0);
|
|
277
|
+
|
|
278
|
+
if (minimumTotal > remaining) {
|
|
279
|
+
pending.forEach((index) => {
|
|
280
|
+
const key = this._columnKey(headers[index], index);
|
|
281
|
+
widths[index] = this._columnMinimumWidths.get(key) || 0;
|
|
282
|
+
});
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
while (pending.length) {
|
|
287
|
+
const weights = pending.map((index) => {
|
|
288
|
+
const key = this._columnKey(headers[index], index);
|
|
289
|
+
return this._columnWeights.get(key) || measured[index] || 1;
|
|
290
|
+
});
|
|
291
|
+
const weightTotal = weights.reduce((total, weight) => total + weight, 0) || pending.length;
|
|
292
|
+
const constrained = pending.filter((index, position) => {
|
|
293
|
+
const key = this._columnKey(headers[index], index);
|
|
294
|
+
const minimum = this._columnMinimumWidths.get(key) || 0;
|
|
295
|
+
return minimum > remaining * weights[position] / weightTotal;
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
if (!constrained.length) {
|
|
299
|
+
let distributed = 0;
|
|
300
|
+
pending.forEach((index, position) => {
|
|
301
|
+
const width = position === pending.length - 1
|
|
302
|
+
? Math.max(0, remaining - distributed)
|
|
303
|
+
: remaining * weights[position] / weightTotal;
|
|
304
|
+
widths[index] = width;
|
|
305
|
+
distributed += width;
|
|
306
|
+
});
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
constrained.forEach((index) => {
|
|
311
|
+
const key = this._columnKey(headers[index], index);
|
|
312
|
+
const minimum = this._columnMinimumWidths.get(key) || 0;
|
|
313
|
+
widths[index] = minimum;
|
|
314
|
+
remaining -= minimum;
|
|
315
|
+
});
|
|
316
|
+
pending = pending.filter((index) => !constrained.includes(index));
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
_captureOverflowingHeaderMinimums() {
|
|
321
|
+
let changed = false;
|
|
322
|
+
this._getHeaders().forEach((header, index) => {
|
|
323
|
+
const key = this._columnKey(header, index);
|
|
324
|
+
if (this._hasDeclaredWidth(header) || this._hardColumnWidths.has(key)) return;
|
|
325
|
+
|
|
326
|
+
const rendered = header.getBoundingClientRect?.().width || header.clientWidth || 0;
|
|
327
|
+
const required = header.scrollWidth || 0;
|
|
328
|
+
const current = this._columnMinimumWidths.get(key) || 0;
|
|
329
|
+
if (required <= rendered + 1 || required <= current + 1) return;
|
|
330
|
+
|
|
331
|
+
this._columnMinimumWidths.set(key, required);
|
|
332
|
+
changed = true;
|
|
267
333
|
});
|
|
268
|
-
return
|
|
334
|
+
return changed;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
_syncContentMinimumWidth(widths) {
|
|
338
|
+
const total = widths.reduce((sum, width) => sum + width, 0);
|
|
339
|
+
const viewport = this._body?.clientWidth || this._container?.clientWidth || total;
|
|
340
|
+
const previous = this._previousStyles.get(CONTENT_MIN_WIDTH_PROPERTY);
|
|
341
|
+
const required = Math.max(total, this._element?.scrollWidth || 0);
|
|
342
|
+
const next = required > viewport + 1 ? `${Math.ceil(required)}px` : previous;
|
|
343
|
+
const current = this._container.style.getPropertyValue(CONTENT_MIN_WIDTH_PROPERTY);
|
|
344
|
+
if (next) this._container.style.setProperty(CONTENT_MIN_WIDTH_PROPERTY, next);
|
|
345
|
+
else this._container.style.removeProperty(CONTENT_MIN_WIDTH_PROPERTY);
|
|
346
|
+
return current !== (next || '');
|
|
269
347
|
}
|
|
270
348
|
|
|
271
349
|
_getHeaders() {
|
|
@@ -277,6 +355,10 @@ class _stickyHeader {
|
|
|
277
355
|
return String(header?.getAttribute('data-field') || index).trim();
|
|
278
356
|
}
|
|
279
357
|
|
|
358
|
+
_isEmptyHeader(header) {
|
|
359
|
+
return Boolean(header) && header.childElementCount === 0 && String(header.textContent || '').trim() === '';
|
|
360
|
+
}
|
|
361
|
+
|
|
280
362
|
_hasDeclaredWidth(element) {
|
|
281
363
|
if (!element) return false;
|
|
282
364
|
return element.hasAttribute('width')
|
|
@@ -295,9 +377,10 @@ class _stickyHeader {
|
|
|
295
377
|
}
|
|
296
378
|
|
|
297
379
|
_readTableMinimumWidth() {
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
380
|
+
const generated = Number.parseFloat(this._container?.style.getPropertyValue(CONTENT_MIN_WIDTH_PROPERTY) || '0');
|
|
381
|
+
if (typeof getComputedStyle !== 'function') return Number.isFinite(generated) ? generated : 0;
|
|
382
|
+
const declared = Number.parseFloat(getComputedStyle(this._element).minWidth || '0');
|
|
383
|
+
return Math.max(Number.isFinite(generated) ? generated : 0, Number.isFinite(declared) ? declared : 0);
|
|
301
384
|
}
|
|
302
385
|
|
|
303
386
|
_syncScroll() {
|
|
@@ -34,7 +34,8 @@ import {
|
|
|
34
34
|
WRAPPER_SELECTOR,
|
|
35
35
|
} from "./_options.js";
|
|
36
36
|
|
|
37
|
-
const FILTER_HIDDEN_ATTRIBUTE = 'data-vg-table-filter-hidden';
|
|
37
|
+
const FILTER_HIDDEN_ATTRIBUTE = 'data-vg-table-filter-hidden';
|
|
38
|
+
const NOT_SPLITTER_CLASS = 'not-splitter';
|
|
38
39
|
|
|
39
40
|
class VGTable extends BaseModule {
|
|
40
41
|
constructor(element, params = {}) {
|
|
@@ -77,8 +78,11 @@ class VGTable extends BaseModule {
|
|
|
77
78
|
this._fixedColumns = null;
|
|
78
79
|
this._columns = null;
|
|
79
80
|
this._rowReorder = null;
|
|
80
|
-
this._remote = null;
|
|
81
|
-
this.
|
|
81
|
+
this._remote = null;
|
|
82
|
+
this._autoNotSplitterHeaders = new Set();
|
|
83
|
+
this._emptyHeaderSortOptions = new Map();
|
|
84
|
+
this._emptyHeaderSplittersSynced = false;
|
|
85
|
+
this._isRemote = Boolean(String(this._params.request.route || '').trim());
|
|
82
86
|
this._complex = false;
|
|
83
87
|
this._wrapper = null;
|
|
84
88
|
this._ownsWrapper = false;
|
|
@@ -108,8 +112,9 @@ class VGTable extends BaseModule {
|
|
|
108
112
|
*/
|
|
109
113
|
init() {
|
|
110
114
|
this._ensureWrapper();
|
|
111
|
-
this._ensureTableContainer();
|
|
112
|
-
this._syncComplexState();
|
|
115
|
+
this._ensureTableContainer();
|
|
116
|
+
this._syncComplexState();
|
|
117
|
+
this._syncEmptyHeaderSplitters();
|
|
113
118
|
|
|
114
119
|
// Включаем нативный sticky-заголовок без клонирования DOM
|
|
115
120
|
if (!this._stickyHeader && this._params.stickyHeader.enabled === true) {
|
|
@@ -141,11 +146,12 @@ class VGTable extends BaseModule {
|
|
|
141
146
|
if (!this._complex && !this._sorting && this._params.sort.enabled === true && this._params.rowReorder.enabled !== true) {
|
|
142
147
|
this._sorting = new _sorting(this._element, Object.assign({}, this._params.sort, {remote: this._isRemote}), this._stickyHeader?.getHeaderTable());
|
|
143
148
|
this._sorting.init();
|
|
144
|
-
this._element.addEventListener(
|
|
145
|
-
'sortchange.vg.table',
|
|
146
|
-
this._isRemote ? this._boundRemoteSortChange : this._boundLocalSortChange
|
|
147
|
-
);
|
|
148
|
-
|
|
149
|
+
this._element.addEventListener(
|
|
150
|
+
'sortchange.vg.table',
|
|
151
|
+
this._isRemote ? this._boundRemoteSortChange : this._boundLocalSortChange
|
|
152
|
+
);
|
|
153
|
+
this._stickyHeader?.refresh?.();
|
|
154
|
+
}
|
|
149
155
|
|
|
150
156
|
// Фиксируем настоящие ячейки через native sticky, включая отдельный слой Fixed Header
|
|
151
157
|
if (!this._complex && !this._fixedColumns && this._params.fixedColumns.enabled === true) {
|
|
@@ -445,15 +451,38 @@ class VGTable extends BaseModule {
|
|
|
445
451
|
* Определяет таблицу с многострочным thead, colspan или rowspan.
|
|
446
452
|
* @private
|
|
447
453
|
*/
|
|
448
|
-
_syncComplexState() {
|
|
454
|
+
_syncComplexState() {
|
|
449
455
|
const rows = this._isRemote
|
|
450
456
|
? Array.from(this._element.tHead?.rows || [])
|
|
451
457
|
: Array.from(this._element.rows || []);
|
|
452
458
|
const hasSpans = rows.some((row) => Array.from(row.cells || []).some((cell) => cell.colSpan > 1 || cell.rowSpan > 1));
|
|
453
459
|
this._complex = Boolean((this._element.tHead?.rows.length || 0) > 1 || hasSpans);
|
|
454
460
|
this._element.classList.toggle('vg-table-complex', this._complex);
|
|
455
|
-
this._element.toggleAttribute('data-vg-table-complex', this._complex);
|
|
456
|
-
}
|
|
461
|
+
this._element.toggleAttribute('data-vg-table-complex', this._complex);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* Скрывает разделитель и отключает сортировку у пустых заголовочных ячеек.
|
|
466
|
+
* @private
|
|
467
|
+
*/
|
|
468
|
+
_syncEmptyHeaderSplitters() {
|
|
469
|
+
if (this._emptyHeaderSplittersSynced) return;
|
|
470
|
+
|
|
471
|
+
Array.from(this._element.tHead?.querySelectorAll('th') || []).forEach((header) => {
|
|
472
|
+
const isEmpty = header.childElementCount === 0 && String(header.textContent || '').trim() === '';
|
|
473
|
+
if (!isEmpty) return;
|
|
474
|
+
|
|
475
|
+
if (!header.classList.contains(NOT_SPLITTER_CLASS)) {
|
|
476
|
+
header.classList.add(NOT_SPLITTER_CLASS);
|
|
477
|
+
this._autoNotSplitterHeaders.add(header);
|
|
478
|
+
}
|
|
479
|
+
if (String(header.getAttribute('data-sort-enabled')).toLowerCase() !== 'false') {
|
|
480
|
+
this._emptyHeaderSortOptions.set(header, header.getAttribute('data-sort-enabled'));
|
|
481
|
+
header.setAttribute('data-sort-enabled', 'false');
|
|
482
|
+
}
|
|
483
|
+
});
|
|
484
|
+
this._emptyHeaderSplittersSynced = true;
|
|
485
|
+
}
|
|
457
486
|
|
|
458
487
|
/**
|
|
459
488
|
* Подготовка переменных для пагинации
|
|
@@ -1426,8 +1455,15 @@ class VGTable extends BaseModule {
|
|
|
1426
1455
|
this._element.removeEventListener('sortchange.vg.table', this._boundPaginationRefresh);
|
|
1427
1456
|
this._element.removeEventListener('sortchange.vg.table', this._boundLocalSortChange);
|
|
1428
1457
|
this._element.removeEventListener('sortchange.vg.table', this._boundRemoteSortChange);
|
|
1429
|
-
if (this._pagination) this._pagination.dispose();
|
|
1430
|
-
this.
|
|
1458
|
+
if (this._pagination) this._pagination.dispose();
|
|
1459
|
+
this._autoNotSplitterHeaders.forEach((header) => header.classList.remove(NOT_SPLITTER_CLASS));
|
|
1460
|
+
this._autoNotSplitterHeaders.clear();
|
|
1461
|
+
this._emptyHeaderSortOptions.forEach((value, header) => {
|
|
1462
|
+
if (value === null) header.removeAttribute('data-sort-enabled');
|
|
1463
|
+
else header.setAttribute('data-sort-enabled', value);
|
|
1464
|
+
});
|
|
1465
|
+
this._emptyHeaderSortOptions.clear();
|
|
1466
|
+
this._element.classList.remove('vg-table-complex');
|
|
1431
1467
|
this._element.removeAttribute('data-vg-table-complex');
|
|
1432
1468
|
this._removeGeneratedTableContainer();
|
|
1433
1469
|
this._removeGeneratedWrapper();
|
|
@@ -57,9 +57,9 @@
|
|
|
57
57
|
|
|
58
58
|
.vg-table-header__table,
|
|
59
59
|
.vg-table-body > .vg-table {
|
|
60
|
-
width: 100%;
|
|
61
|
-
min-width: 100
|
|
62
|
-
table-layout: fixed;
|
|
60
|
+
width: 100%;
|
|
61
|
+
min-width: max(100%, var(--vg-table-sticky-content-min-width, 0px));
|
|
62
|
+
table-layout: fixed;
|
|
63
63
|
overflow: visible;
|
|
64
64
|
border: 0;
|
|
65
65
|
border-radius: 0;
|