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.
@@ -28,7 +28,8 @@
28
28
  &.pending,
29
29
  &.loading,
30
30
  &.failing {
31
- .file-actions {
31
+ .file-actions,
32
+ .file-custom > :not(.file-remove):not(.file-info) {
32
33
  display: none;
33
34
  }
34
35
  }
@@ -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 STYLE_PROPERTIES = ['--vg-table-sticky-top', '--vg-table-sticky-max-height'];
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
- const widths = this._resolveLayoutWidths();
82
- this._applyColgroup(this._headerTable, widths);
83
- this._applyColgroup(this._element, widths);
84
- const scrollbar = Math.max(0, this._body.offsetWidth - this._body.clientWidth);
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
- const weights = flexible.map((index) => {
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._columnWeights.get(key) || measured[index] || 1;
258
- });
259
- const weightTotal = weights.reduce((total, weight) => total + weight, 0) || flexible.length;
260
- let distributed = 0;
261
- flexible.forEach((index, position) => {
262
- const width = position === flexible.length - 1
263
- ? Math.max(0, available - distributed)
264
- : available * weights[position] / weightTotal;
265
- widths[index] = width;
266
- distributed += width;
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 widths;
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
- if (typeof getComputedStyle !== 'function') return 0;
299
- const value = Number.parseFloat(getComputedStyle(this._element).minWidth || '0');
300
- return Number.isFinite(value) ? value : 0;
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._isRemote = Boolean(String(this._params.request.route || '').trim());
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._element.classList.remove('vg-table-complex');
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;