vgapp 1.4.7 → 1.4.9

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.
Files changed (85) hide show
  1. package/README.md +24 -2
  2. package/app/modules/base-module.js +14 -2
  3. package/app/modules/module-fn.js +5 -4
  4. package/app/modules/vgalert/js/vgalert.js +21 -1
  5. package/app/modules/vgalert/scss/vgalert.scss +48 -0
  6. package/app/modules/vgdropdown/js/vgdropdown.js +58 -27
  7. package/app/modules/vgfilepreview/js/renderers/video-modal.js +5 -1
  8. package/app/modules/vgfilepreview/js/renderers/video.js +6 -1
  9. package/app/modules/vgfilepreview/js/vgfilepreview.js +5 -1
  10. package/app/modules/vgfiles/js/base.js +14 -5
  11. package/app/modules/vgfiles/js/droppable.js +37 -31
  12. package/app/modules/vgfiles/js/vgfiles.js +5 -2
  13. package/app/modules/vgfiles/scss/_variables.scss +3 -1
  14. package/app/modules/vgfiles/scss/vgfiles.scss +55 -10
  15. package/app/modules/vgformsender/js/vgformsender.js +5 -2
  16. package/app/modules/vgloadmore/js/vgloadmore.js +35 -7
  17. package/app/modules/vgnestable/js/vgnestable.js +140 -0
  18. package/app/modules/vgnestable/scss/_variables.scss +16 -9
  19. package/app/modules/vgnestable/scss/vgnestable.scss +35 -2
  20. package/app/modules/vgtable/index.js +3 -0
  21. package/app/modules/vgtable/js/_columns.js +567 -0
  22. package/app/modules/vgtable/js/_expandable.js +242 -0
  23. package/app/modules/vgtable/js/_filters.js +264 -0
  24. package/app/modules/vgtable/js/_fixed-columns.js +249 -0
  25. package/app/modules/vgtable/js/_i18n.js +75 -0
  26. package/app/modules/vgtable/js/_options.js +428 -0
  27. package/app/modules/vgtable/js/_pagination.js +491 -0
  28. package/app/modules/vgtable/js/_panning.js +124 -0
  29. package/app/modules/vgtable/js/_params-groups.js +21 -0
  30. package/app/modules/vgtable/js/_remote.js +395 -0
  31. package/app/modules/vgtable/js/_row-reorder.js +268 -0
  32. package/app/modules/vgtable/js/_search.js +134 -0
  33. package/app/modules/vgtable/js/_selection.js +113 -0
  34. package/app/modules/vgtable/js/_skeleton.js +123 -0
  35. package/app/modules/vgtable/js/_sorting.js +340 -0
  36. package/app/modules/vgtable/js/_states.js +143 -0
  37. package/app/modules/vgtable/js/_sticky-header.js +193 -0
  38. package/app/modules/vgtable/js/_url-state.js +178 -0
  39. package/app/modules/vgtable/js/vgtable.js +1444 -0
  40. package/app/modules/vgtable/readme.md +922 -0
  41. package/app/modules/vgtable/scss/_columns.scss +52 -0
  42. package/app/modules/vgtable/scss/_expandable.scss +54 -0
  43. package/app/modules/vgtable/scss/_fixed-columns.scss +117 -0
  44. package/app/modules/vgtable/scss/_pagination.scss +191 -0
  45. package/app/modules/vgtable/scss/_row-reorder.scss +45 -0
  46. package/app/modules/vgtable/scss/_skeleton.scss +65 -0
  47. package/app/modules/vgtable/scss/_sorting.scss +89 -0
  48. package/app/modules/vgtable/scss/_states.scss +70 -0
  49. package/app/modules/vgtable/scss/_sticky-header.scss +121 -0
  50. package/app/modules/vgtable/scss/_variables.scss +296 -0
  51. package/app/modules/vgtable/scss/vgtable.scss +206 -0
  52. package/app/utils/js/components/placement.js +64 -10
  53. package/app/vgapp.js +3 -3
  54. package/build/vgapp.css +1 -1
  55. package/build/vgapp.css.map +1 -1
  56. package/build/vgapp.js +1 -1
  57. package/build/vgapp.js.map +1 -1
  58. package/index.js +1 -1
  59. package/index.scss +3 -3
  60. package/package.json +5 -5
  61. package/app/modules/vgdynamictable/index.js +0 -4
  62. package/app/modules/vgdynamictable/js/editable.js +0 -438
  63. package/app/modules/vgdynamictable/js/expandable.js +0 -248
  64. package/app/modules/vgdynamictable/js/filters.js +0 -450
  65. package/app/modules/vgdynamictable/js/fixed.js +0 -566
  66. package/app/modules/vgdynamictable/js/options.js +0 -650
  67. package/app/modules/vgdynamictable/js/pagination.js +0 -623
  68. package/app/modules/vgdynamictable/js/search.js +0 -82
  69. package/app/modules/vgdynamictable/js/skeleton.js +0 -144
  70. package/app/modules/vgdynamictable/js/sortable.js +0 -442
  71. package/app/modules/vgdynamictable/js/summary-footer.js +0 -284
  72. package/app/modules/vgdynamictable/js/table-remote.js +0 -821
  73. package/app/modules/vgdynamictable/js/table-state.js +0 -243
  74. package/app/modules/vgdynamictable/js/table-url-state.js +0 -444
  75. package/app/modules/vgdynamictable/js/utils/common.js +0 -48
  76. package/app/modules/vgdynamictable/js/vgdynamictable.js +0 -3870
  77. package/app/modules/vgdynamictable/js/viewport.js +0 -322
  78. package/app/modules/vgdynamictable/readme.md +0 -251
  79. package/app/modules/vgdynamictable/scss/_actions.scss +0 -196
  80. package/app/modules/vgdynamictable/scss/_pagination.scss +0 -186
  81. package/app/modules/vgdynamictable/scss/_skeleton.scss +0 -63
  82. package/app/modules/vgdynamictable/scss/_sortable.scss +0 -66
  83. package/app/modules/vgdynamictable/scss/_table.scss +0 -137
  84. package/app/modules/vgdynamictable/scss/_variables.scss +0 -138
  85. package/app/modules/vgdynamictable/scss/vgdynamictable.scss +0 -264
@@ -0,0 +1,340 @@
1
+ /**
2
+ * Описание: локальная и серверная сортировка строк базовой таблицы VGTable.
3
+ * Возможности: стабильная одиночная и мультисортировка, Shift-режим, remote-события, Fixed Header, клавиатура, приоритеты и подсветка колонок.
4
+ */
5
+ import EventHandler from "../../../utils/js/dom/event";
6
+
7
+
8
+ const EVENT_SORT_CHANGE = 'sortchange.vg.table';
9
+ const SORTABLE_SELECTOR = 'th';
10
+ const INTERACTIVE_SELECTOR = 'a, button, input, select, textarea, [contenteditable="true"], [data-vg-table-column-resize-handle]';
11
+
12
+ class _sorting {
13
+ constructor(table, options = {}, headerTable = table) {
14
+ this._table = table;
15
+ this._headerTable = headerTable;
16
+ this._options = Object.assign({hover: false, multiple: false, multipleWithShift: true}, options);
17
+ this._headers = [];
18
+ this._originalRows = new Map();
19
+ this._sorts = [];
20
+ this._boundClick = this._handleClick.bind(this);
21
+ this._boundKeydown = this._handleKeydown.bind(this);
22
+ }
23
+
24
+ init() {
25
+ this._headers = this._getHeaders();
26
+ this._rememberOriginalRows();
27
+ this._table.classList.toggle('vg-table-sort-hover', this._options.hover === true);
28
+ this._headerTable.classList.toggle('vg-table-sort-hover', this._options.hover === true);
29
+ this._table.classList.toggle('vg-table-sort-multiple', this._options.multiple === true);
30
+ this._headerTable.classList.toggle('vg-table-sort-multiple', this._options.multiple === true);
31
+
32
+ this._headers.forEach((header) => {
33
+ if (!this._isSortable(header)) return;
34
+
35
+ header.classList.add('vg-table-sortable');
36
+ header.setAttribute('aria-sort', 'none');
37
+ this._decorateHeader(header);
38
+ if (!header.hasAttribute('tabindex')) header.tabIndex = 0;
39
+ });
40
+
41
+ this._headerTable.addEventListener('click', this._boundClick);
42
+ this._headerTable.addEventListener('keydown', this._boundKeydown);
43
+ }
44
+
45
+ dispose() {
46
+ this._headerTable.removeEventListener('click', this._boundClick);
47
+ this._headerTable.removeEventListener('keydown', this._boundKeydown);
48
+ this._table.classList.remove('vg-table-sort-hover');
49
+ this._headerTable.classList.remove('vg-table-sort-hover');
50
+ this._table.classList.remove('vg-table-sort-multiple');
51
+ this._headerTable.classList.remove('vg-table-sort-multiple');
52
+
53
+ this._headers.forEach((header) => {
54
+ header.classList.remove('vg-table-sortable');
55
+ header.removeAttribute('aria-sort');
56
+ header.removeAttribute('data-sort-direction');
57
+ header.removeAttribute('data-sort-priority');
58
+ header.querySelector('[data-vg-table-sort-controls]')?.remove();
59
+ });
60
+
61
+ this._sorts = [];
62
+ this._syncColumnHighlight();
63
+ this._headers = [];
64
+ this._originalRows.clear();
65
+ }
66
+
67
+ setSort(column, direction = 'asc', emit = false) {
68
+ const index = this._resolveColumnIndex(column);
69
+ return this.setSorts([{columnIndex: index, direction}], emit);
70
+ }
71
+
72
+ setSorts(sorts = [], emit = false) {
73
+ const normalized = this._normalizeSorts(sorts);
74
+ if (sorts.length && !normalized.length) return false;
75
+ this._sorts = this._options.multiple === true ? normalized : normalized.slice(0, 1);
76
+ this._applySort();
77
+ this._syncHeaders();
78
+ if (emit) this._emitChange();
79
+ return true;
80
+ }
81
+
82
+ getSort() {
83
+ const sort = this._sorts[0];
84
+ if (!sort) return null;
85
+ const header = this._headers[sort.index];
86
+ return {
87
+ columnIndex: sort.index,
88
+ field: header ? String(header.getAttribute('data-field') || '').trim() : '',
89
+ direction: sort.direction,
90
+ };
91
+ }
92
+
93
+ getSorts() {
94
+ return this._sorts.map((sort) => {
95
+ const header = this._headers[sort.index];
96
+ return {
97
+ columnIndex: sort.index,
98
+ field: header ? String(header.getAttribute('data-field') || '').trim() : '',
99
+ direction: sort.direction,
100
+ };
101
+ });
102
+ }
103
+
104
+ getSortState() {
105
+ const sort = this.getSort();
106
+ return sort ? Object.assign({}, sort, {sorts: this.getSorts()}) : null;
107
+ }
108
+
109
+ refresh(structure = false) {
110
+ if (structure) {
111
+ const sorts = this.getSorts();
112
+ this._headers = this._getHeaders();
113
+ this._headers.forEach((header) => {
114
+ if (!this._isSortable(header)) return;
115
+ header.classList.add('vg-table-sortable');
116
+ if (!header.hasAttribute('aria-sort')) header.setAttribute('aria-sort', 'none');
117
+ this._decorateHeader(header);
118
+ if (!header.hasAttribute('tabindex')) header.tabIndex = 0;
119
+ });
120
+ this.setSorts(sorts, false);
121
+ } else {
122
+ this._syncColumnHighlight();
123
+ }
124
+ return this.getSort();
125
+ }
126
+
127
+ clearSort(emit = false) {
128
+ this._sorts = [];
129
+ if (this._options.remote !== true) this._restoreOriginalRows();
130
+ this._syncHeaders();
131
+ if (emit) this._emitChange();
132
+ }
133
+
134
+ _getHeaders() {
135
+ const head = this._headerTable.tHead || this._headerTable.querySelector('thead');
136
+ if (!head || !head.rows.length) return [];
137
+ return Array.from(head.rows[head.rows.length - 1].querySelectorAll(SORTABLE_SELECTOR));
138
+ }
139
+
140
+ _isSortable(header) {
141
+ return String(header.getAttribute('data-sort-enabled') || '').toLowerCase() !== 'false';
142
+ }
143
+
144
+ _rememberOriginalRows() {
145
+ Array.from(this._table.tBodies || []).forEach((body) => {
146
+ this._originalRows.set(body, Array.from(body.rows));
147
+ });
148
+ }
149
+
150
+ _restoreOriginalRows() {
151
+ this._originalRows.forEach((rows, body) => {
152
+ rows.forEach((row) => body.append(row));
153
+ });
154
+ }
155
+
156
+ _handleClick(event) {
157
+ if (event.target.closest(INTERACTIVE_SELECTOR)) return;
158
+
159
+ const header = event.target.closest(SORTABLE_SELECTOR);
160
+ if (!header || !this._headers.includes(header) || !this._isSortable(header)) return;
161
+ this._cycleSort(this._headers.indexOf(header), this._isMultipleMode(event));
162
+ }
163
+
164
+ _handleKeydown(event) {
165
+ if (event.key !== 'Enter' && event.key !== ' ') return;
166
+ if (event.target.closest(INTERACTIVE_SELECTOR)) return;
167
+
168
+ const header = event.target.closest(SORTABLE_SELECTOR);
169
+ if (!header || !this._headers.includes(header) || !this._isSortable(header)) return;
170
+
171
+ event.preventDefault();
172
+ this._cycleSort(this._headers.indexOf(header), this._isMultipleMode(event));
173
+ }
174
+
175
+ _isMultipleMode(event) {
176
+ if (this._options.multiple !== true) return false;
177
+ return this._options.multipleWithShift === false || event?.shiftKey === true;
178
+ }
179
+
180
+ _cycleSort(index, multipleMode = false) {
181
+ const current = this._sorts.find((sort) => sort.index === index);
182
+ if (!current) {
183
+ this._setColumnSort(index, 'asc', multipleMode);
184
+ return;
185
+ }
186
+ if (current.direction === 'asc') {
187
+ this._setColumnSort(index, 'desc', multipleMode);
188
+ return;
189
+ }
190
+ const next = multipleMode
191
+ ? this._sorts.filter((sort) => sort.index !== index)
192
+ : [];
193
+ this.setSorts(next, true);
194
+ }
195
+
196
+ _setColumnSort(index, direction, multipleMode) {
197
+ const next = multipleMode ? this._sorts.slice() : [];
198
+ const currentIndex = next.findIndex((sort) => sort.index === index);
199
+ const item = {columnIndex: index, direction};
200
+ if (currentIndex >= 0) next.splice(currentIndex, 1, item);
201
+ else next.push(item);
202
+ this.setSorts(next, true);
203
+ }
204
+
205
+ _applySort() {
206
+ if (this._options.remote === true || !this._sorts.length) return;
207
+
208
+ this._originalRows.forEach((originalRows, body) => {
209
+ const originalIndex = new Map(originalRows.map((row, rowIndex) => [row, rowIndex]));
210
+ const rows = Array.from(body.rows);
211
+ rows.sort((leftRow, rightRow) => {
212
+ for (const sort of this._sorts) {
213
+ const header = this._headers[sort.index];
214
+ const type = String(header?.getAttribute('data-sort-type') || 'auto').toLowerCase();
215
+ const result = this._compareCells(leftRow.cells[sort.index], rightRow.cells[sort.index], type);
216
+ if (result !== 0) return sort.direction === 'desc' ? -result : result;
217
+ }
218
+ return (originalIndex.get(leftRow) ?? 0) - (originalIndex.get(rightRow) ?? 0);
219
+ });
220
+ rows.forEach((row) => body.append(row));
221
+ });
222
+ }
223
+
224
+ _compareCells(leftCell, rightCell, type) {
225
+ const left = this._getCellValue(leftCell);
226
+ const right = this._getCellValue(rightCell);
227
+
228
+ if (type === 'number' || (type === 'auto' && this._isNumeric(left) && this._isNumeric(right))) {
229
+ return this._toNumber(left) - this._toNumber(right);
230
+ }
231
+
232
+ if (type === 'date') {
233
+ const leftTime = Date.parse(left);
234
+ const rightTime = Date.parse(right);
235
+ if (!Number.isNaN(leftTime) && !Number.isNaN(rightTime)) return leftTime - rightTime;
236
+ }
237
+
238
+ return left.localeCompare(right, undefined, {numeric: true, sensitivity: 'base'});
239
+ }
240
+
241
+ _getCellValue(cell) {
242
+ if (!cell) return '';
243
+ return String(cell.getAttribute('data-sort-value') ?? cell.textContent ?? '').trim();
244
+ }
245
+
246
+ _isNumeric(value) {
247
+ return value !== '' && Number.isFinite(this._toNumber(value));
248
+ }
249
+
250
+ _toNumber(value) {
251
+ return Number(String(value).replace(/\s/g, '').replace(',', '.'));
252
+ }
253
+
254
+ _decorateHeader(header) {
255
+ if (header.querySelector('[data-vg-table-sort-controls]')) return;
256
+
257
+ const controls = document.createElement('span');
258
+ controls.className = 'vg-table-sort-controls';
259
+ controls.setAttribute('data-vg-table-sort-controls', '');
260
+ controls.setAttribute('aria-hidden', 'true');
261
+ controls.innerHTML = `
262
+ <span class="vg-table-sort-priority" data-vg-table-sort-priority hidden></span>
263
+ <span class="vg-table-sort-chevron vg-table-sort-chevron-asc"></span>
264
+ <span class="vg-table-sort-chevron vg-table-sort-chevron-desc"></span>
265
+ `;
266
+ header.append(controls);
267
+ }
268
+
269
+ _syncHeaders() {
270
+ this._headers.forEach((header, index) => {
271
+ const priority = this._sorts.findIndex((sort) => sort.index === index);
272
+ const direction = priority >= 0 ? this._sorts[priority].direction : null;
273
+ const priorityNode = header.querySelector('[data-vg-table-sort-priority]');
274
+ header.setAttribute('aria-sort', direction === 'asc' ? 'ascending' : direction === 'desc' ? 'descending' : 'none');
275
+ if (direction) {
276
+ header.setAttribute('data-sort-direction', direction);
277
+ header.setAttribute('data-sort-priority', String(priority + 1));
278
+ if (priorityNode) {
279
+ priorityNode.textContent = String(priority + 1);
280
+ priorityNode.hidden = this._sorts.length < 2;
281
+ }
282
+ } else {
283
+ header.removeAttribute('data-sort-direction');
284
+ header.removeAttribute('data-sort-priority');
285
+ if (priorityNode) priorityNode.hidden = true;
286
+ }
287
+ });
288
+ this._syncColumnHighlight();
289
+ }
290
+
291
+ _syncColumnHighlight() {
292
+ const rows = [...Array.from(this._headerTable.rows || []), ...Array.from(this._table.rows || [])]
293
+ .filter((row) => !row.hasAttribute('data-vg-table-state-row'));
294
+ rows.forEach((row) => {
295
+ Array.from(row.cells || []).forEach((cell) => {
296
+ cell.removeAttribute('data-sorted-column');
297
+ cell.removeAttribute('data-sorted-priority');
298
+ });
299
+ });
300
+
301
+ this._sorts.forEach((sort, priority) => rows.forEach((row) => {
302
+ const cell = row.cells[sort.index];
303
+ if (cell) {
304
+ cell.setAttribute('data-sorted-column', '1');
305
+ cell.setAttribute('data-sorted-priority', String(priority + 1));
306
+ }
307
+ }));
308
+ }
309
+
310
+ _normalizeSorts(sorts) {
311
+ const result = [];
312
+ const seen = new Set();
313
+ (Array.isArray(sorts) ? sorts : []).forEach((sort) => {
314
+ const column = sort?.field ?? sort?.columnIndex ?? sort?.index;
315
+ const index = this._resolveColumnIndex(column);
316
+ if (index < 0 || seen.has(index) || !this._isSortable(this._headers[index])) return;
317
+ seen.add(index);
318
+ result.push({
319
+ index,
320
+ direction: String(sort?.direction ?? sort?.dir ?? 'asc').toLowerCase() === 'desc' ? 'desc' : 'asc',
321
+ });
322
+ });
323
+ return result;
324
+ }
325
+
326
+ _emitChange() {
327
+ EventHandler.trigger(this._table, EVENT_SORT_CHANGE, {sort: this.getSort(), sorts: this.getSorts()});
328
+ }
329
+
330
+ _resolveColumnIndex(column) {
331
+ if (typeof column === 'string') {
332
+ return this._headers.findIndex((header) => String(header.getAttribute('data-field') || '').trim() === column.trim());
333
+ }
334
+
335
+ const index = Number.parseInt(column, 10);
336
+ return Number.isInteger(index) ? index : -1;
337
+ }
338
+ }
339
+
340
+ export default _sorting;
@@ -0,0 +1,143 @@
1
+ /**
2
+ * Описание: единый визуальный слой состояний VGTable.
3
+ * Возможности: empty, filtered-empty и error, действия retry/reset, live-region и публичное состояние.
4
+ */
5
+ import EventHandler from "../../../utils/js/dom/event";
6
+
7
+
8
+ const EVENT_CHANGE = 'statechange.vg.table';
9
+ const STATE_ROW_ATTRIBUTE = 'data-vg-table-state-row';
10
+
11
+ class _states {
12
+ constructor(table, wrapper, options = {}, hooks = {}) {
13
+ this._table = table;
14
+ this._wrapper = wrapper;
15
+ this._options = options;
16
+ this._hooks = hooks;
17
+ this._state = null;
18
+ this._boundClick = this._handleClick.bind(this);
19
+ }
20
+
21
+ init() {
22
+ this._table.addEventListener('click', this._boundClick);
23
+ return this;
24
+ }
25
+
26
+ dispose() {
27
+ this._table.removeEventListener('click', this._boundClick);
28
+ this.clear(false);
29
+ }
30
+
31
+ render(type, message = '') {
32
+ if (this._options.enabled === false) return null;
33
+ const normalized = ['empty', 'filtered-empty', 'error'].includes(String(type)) ? String(type) : 'empty';
34
+ this._removeRow();
35
+ const row = this._table.ownerDocument.createElement('tr');
36
+ row.setAttribute(STATE_ROW_ATTRIBUTE, '');
37
+ const cell = this._table.ownerDocument.createElement('td');
38
+ cell.colSpan = this._columns();
39
+ cell.setAttribute('data-vg-table-state', normalized);
40
+ cell.setAttribute('role', normalized === 'error' ? 'alert' : 'status');
41
+ const content = this._table.ownerDocument.createElement('div');
42
+ content.className = 'vg-table-state';
43
+ const icon = this._table.ownerDocument.createElement('span');
44
+ icon.className = `vg-table-state__icon vg-table-state__icon--${normalized}`;
45
+ icon.setAttribute('aria-hidden', 'true');
46
+ const text = this._table.ownerDocument.createElement('span');
47
+ text.className = 'vg-table-state__text';
48
+ text.textContent = message || this._label(normalized);
49
+ content.append(icon, text);
50
+ const actions = this._actions(normalized);
51
+ if (actions) content.append(actions);
52
+ cell.append(content);
53
+ row.append(cell);
54
+ if (this._hooks.remote === true) this._body().replaceChildren(row);
55
+ else this._body().append(row);
56
+ this._state = {type: normalized, message: text.textContent};
57
+ this._wrapper?.setAttribute('data-vg-table-state', normalized);
58
+ EventHandler.trigger(this._table, EVENT_CHANGE, this.getState());
59
+ return this.getState();
60
+ }
61
+
62
+ clear(emit = true) {
63
+ const previous = this._state;
64
+ this._removeRow();
65
+ this._state = null;
66
+ this._wrapper?.removeAttribute('data-vg-table-state');
67
+ if (emit && previous) EventHandler.trigger(this._table, EVENT_CHANGE, {type: null, previous});
68
+ return previous;
69
+ }
70
+
71
+ syncLocal(activeQuery = false) {
72
+ const rows = this._dataRows();
73
+ if (!rows.length) return this.render('empty');
74
+ const matched = rows.filter((row) => !row.hasAttribute('data-vg-table-filter-hidden'));
75
+ if (activeQuery && !matched.length) return this.render('filtered-empty');
76
+ this.clear();
77
+ return null;
78
+ }
79
+
80
+ getState() {
81
+ return this._state ? Object.assign({}, this._state) : null;
82
+ }
83
+
84
+ _actions(type) {
85
+ if (type !== 'error' && type !== 'filtered-empty') return null;
86
+ const actions = this._table.ownerDocument.createElement('span');
87
+ actions.className = 'vg-table-state__actions';
88
+ const button = this._table.ownerDocument.createElement('button');
89
+ button.type = 'button';
90
+ button.className = 'vg-table-state__action';
91
+ if (type === 'error') {
92
+ button.setAttribute('data-vg-table-retry', '');
93
+ button.textContent = this._label('retry');
94
+ } else {
95
+ button.setAttribute('data-vg-table-state-reset', '');
96
+ button.textContent = this._label('reset');
97
+ }
98
+ actions.append(button);
99
+ return actions;
100
+ }
101
+
102
+ _handleClick(event) {
103
+ const reset = event.target.closest('[data-vg-table-state-reset]');
104
+ if (!reset) return;
105
+ event.preventDefault();
106
+ this._hooks.reset?.();
107
+ }
108
+
109
+ _label(key) {
110
+ const labels = this._options.labels || {};
111
+ const fallbacks = {
112
+ empty: 'Нет данных',
113
+ 'filtered-empty': 'По вашему запросу ничего не найдено',
114
+ error: 'Не удалось загрузить данные',
115
+ retry: 'Повторить',
116
+ reset: 'Сбросить фильтры',
117
+ };
118
+ return String(labels[key] || fallbacks[key] || '');
119
+ }
120
+
121
+ _columns() {
122
+ const headerTable = this._hooks.getHeaderTable?.() || this._table;
123
+ const rows = Array.from(headerTable.tHead?.rows || []);
124
+ return Math.max(1, rows.length ? rows.at(-1).cells.length : 1);
125
+ }
126
+
127
+ _dataRows() {
128
+ return Array.from(this._table.tBodies || [])
129
+ .flatMap((body) => Array.from(body.rows || []))
130
+ .filter((row) => !row.hasAttribute(STATE_ROW_ATTRIBUTE));
131
+ }
132
+
133
+ _removeRow() {
134
+ this._body().querySelector(`[${STATE_ROW_ATTRIBUTE}]`)?.remove();
135
+ }
136
+
137
+ _body() {
138
+ return this._table.tBodies[0] || this._table.createTBody();
139
+ }
140
+ }
141
+
142
+ export {STATE_ROW_ATTRIBUTE};
143
+ export default _states;
@@ -0,0 +1,193 @@
1
+ /**
2
+ * Описание: разметка Fixed Header с отдельными слоями заголовка и тела VGTable.
3
+ * Возможности: перенос настоящего thead без клона, синхронизация колонок и горизонтальной прокрутки.
4
+ */
5
+
6
+ const MODE_CLASSES = ['vg-table-wrapper--sticky-container', 'vg-table-wrapper--sticky-page'];
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'];
9
+ const GENERATED_LAYER_ATTRIBUTE = 'data-vg-table-sticky-generated';
10
+
11
+ class _stickyHeader {
12
+ constructor(element, wrapper, container, params = {}) {
13
+ this._element = element;
14
+ this._wrapper = wrapper;
15
+ this._container = container;
16
+ this._params = params;
17
+ this._head = element.tHead;
18
+ this._header = null;
19
+ this._headerTable = null;
20
+ this._body = null;
21
+ this._previousClasses = new Map();
22
+ this._previousContainerClasses = new Map();
23
+ this._previousStyles = new Map();
24
+ this._boundScroll = this._syncScroll.bind(this);
25
+ this._boundResize = this.refresh.bind(this);
26
+ this._boundPageScroll = this._syncStickyState.bind(this);
27
+ }
28
+
29
+ init() {
30
+ if (!this._wrapper || !this._container || !this._head) return this;
31
+
32
+ const widths = this._measureColumns();
33
+ MODE_CLASSES.forEach((className) => this._previousClasses.set(className, this._wrapper.classList.contains(className)));
34
+ CONTAINER_MODE_CLASSES.forEach((className) => this._previousContainerClasses.set(className, this._container.classList.contains(className)));
35
+ STYLE_PROPERTIES.forEach((property) => this._previousStyles.set(property, this._container.style.getPropertyValue(property)));
36
+
37
+ this._wrapper.classList.add(`vg-table-wrapper--sticky-${this._params.mode}`);
38
+ this._container.classList.add(`vg-table-container--sticky-${this._params.mode}`);
39
+ if (this._params.top !== null && String(this._params.top).trim() !== '') {
40
+ this._container.style.setProperty('--vg-table-sticky-top', this._toCssLength(this._params.top, '0px'));
41
+ }
42
+ this._container.style.setProperty('--vg-table-sticky-max-height', this._toCssLength(this._params.maxHeight, '24rem'));
43
+ this._element.setAttribute('data-vg-table-sticky-header', this._params.mode);
44
+
45
+ this._buildLayers(widths);
46
+ this._body.addEventListener('scroll', this._boundScroll, {passive: true});
47
+ if (typeof window !== 'undefined') {
48
+ window.addEventListener('resize', this._boundResize);
49
+ if (this._params.mode === 'page') window.addEventListener('scroll', this._boundPageScroll, {passive: true});
50
+ }
51
+ this.refresh();
52
+ return this;
53
+ }
54
+
55
+ getHeaderTable() {
56
+ return this._headerTable || this._element;
57
+ }
58
+
59
+ getBody() {
60
+ return this._body;
61
+ }
62
+
63
+ syncScroll() {
64
+ this._syncScroll();
65
+ return this;
66
+ }
67
+
68
+ refresh() {
69
+ if (!this._header || !this._headerTable || !this._body) return this;
70
+
71
+ const widths = this._measureBodyColumns();
72
+ this._applyColgroup(this._headerTable, widths);
73
+ this._applyColgroup(this._element, widths);
74
+ const scrollbar = Math.max(0, this._body.offsetWidth - this._body.clientWidth);
75
+ this._header.style.paddingInlineEnd = `${scrollbar}px`;
76
+ this._header.style.setProperty('--vg-table-sticky-scrollbar-width', `${scrollbar}px`);
77
+ this._header.classList.toggle('vg-table-header--scrollbar', scrollbar > 0);
78
+ this._container.classList.toggle('vg-table-container--scrollbar', scrollbar > 0);
79
+ this._syncScroll();
80
+ this._syncStickyState();
81
+ return this;
82
+ }
83
+
84
+ dispose() {
85
+ if (this._body) this._body.removeEventListener('scroll', this._boundScroll);
86
+ if (typeof window !== 'undefined') {
87
+ window.removeEventListener('resize', this._boundResize);
88
+ window.removeEventListener('scroll', this._boundPageScroll);
89
+ }
90
+ this._header?.classList.remove('is-stuck');
91
+ if (this._head && this._headerTable?.contains(this._head)) this._element.prepend(this._head);
92
+ this._header?.remove();
93
+ this._container?.classList.remove('vg-table-container--scrollbar');
94
+ if (this._body?.contains(this._element)) this._body.before(this._element);
95
+ this._body?.remove();
96
+ this._element.querySelector(':scope > colgroup[data-vg-table-sticky-colgroup]')?.remove();
97
+
98
+ MODE_CLASSES.forEach((className) => {
99
+ this._wrapper.classList.toggle(className, this._previousClasses.get(className) === true);
100
+ });
101
+ CONTAINER_MODE_CLASSES.forEach((className) => {
102
+ this._container.classList.toggle(className, this._previousContainerClasses.get(className) === true);
103
+ });
104
+ STYLE_PROPERTIES.forEach((property) => {
105
+ const previous = this._previousStyles.get(property);
106
+ if (previous) this._container.style.setProperty(property, previous);
107
+ else this._container.style.removeProperty(property);
108
+ });
109
+ this._element.removeAttribute('data-vg-table-sticky-header');
110
+ }
111
+
112
+ _buildLayers(widths) {
113
+ this._header = this._element.ownerDocument.createElement('div');
114
+ this._header.className = 'vg-table-header';
115
+ this._header.setAttribute(GENERATED_LAYER_ATTRIBUTE, '');
116
+ this._headerTable = this._element.ownerDocument.createElement('table');
117
+ this._headerTable.className = `${this._element.className} vg-table-header__table`;
118
+ this._headerTable.style.cssText = this._element.style.cssText;
119
+ this._headerTable.removeAttribute('data-vg-table');
120
+ const sourceColgroup = this._element.querySelector(':scope > colgroup:not([data-vg-table-sticky-colgroup])');
121
+ if (sourceColgroup) this._headerTable.append(sourceColgroup.cloneNode(true));
122
+ else this._applyColgroup(this._headerTable, widths);
123
+ this._headerTable.append(this._head);
124
+ this._header.append(this._headerTable);
125
+
126
+ this._body = this._element.ownerDocument.createElement('div');
127
+ this._body.className = 'vg-table-body';
128
+ this._body.setAttribute(GENERATED_LAYER_ATTRIBUTE, '');
129
+ this._applyColgroup(this._element, widths);
130
+ this._element.before(this._header, this._body);
131
+ this._body.append(this._element);
132
+ }
133
+
134
+ _measureColumns() {
135
+ const row = this._element.tBodies[0]?.rows[0] || this._head.rows[this._head.rows.length - 1];
136
+ return this._measureRow(row);
137
+ }
138
+
139
+ _measureBodyColumns() {
140
+ const row = this._element.tBodies[0]?.rows[0];
141
+ const widths = this._measureRow(row);
142
+ return widths.some((width) => width > 0) ? widths : this._measureRow(this._head.rows[this._head.rows.length - 1]);
143
+ }
144
+
145
+ _measureRow(row) {
146
+ if (!row) return [];
147
+ return Array.from(row.cells).flatMap((cell) => {
148
+ const span = Math.max(1, cell.colSpan || 1);
149
+ const width = cell.getBoundingClientRect().width / span;
150
+ return Array.from({length: span}, () => width);
151
+ });
152
+ }
153
+
154
+ _applyColgroup(table, widths) {
155
+ if (!widths.length) return;
156
+ if (table.querySelector(':scope > colgroup:not([data-vg-table-sticky-colgroup])')) return;
157
+ let colgroup = table.querySelector(':scope > colgroup[data-vg-table-sticky-colgroup]');
158
+ if (!colgroup) {
159
+ colgroup = table.ownerDocument.createElement('colgroup');
160
+ colgroup.setAttribute('data-vg-table-sticky-colgroup', '');
161
+ table.prepend(colgroup);
162
+ }
163
+ colgroup.replaceChildren(...widths.map((width) => {
164
+ const col = table.ownerDocument.createElement('col');
165
+ if (width > 0) col.style.width = `${width}px`;
166
+ return col;
167
+ }));
168
+ }
169
+
170
+ _syncScroll() {
171
+ if (this._header && this._body) this._header.scrollLeft = this._body.scrollLeft;
172
+ }
173
+
174
+ _syncStickyState() {
175
+ if (!this._header || this._params.mode !== 'page') return;
176
+ const headerRect = this._header.getBoundingClientRect();
177
+ const containerRect = this._container.getBoundingClientRect();
178
+ const top = Number.parseFloat(window.getComputedStyle(this._header).top) || 0;
179
+ const atStickyTop = Math.abs(headerRect.top - top) <= 1;
180
+ const insideContainer = containerRect.top < top && containerRect.bottom > top + headerRect.height;
181
+ this._header.classList.toggle('is-stuck', atStickyTop && insideContainer);
182
+ }
183
+
184
+ _toCssLength(value, fallback) {
185
+ if (typeof value === 'number' && Number.isFinite(value)) return `${Math.max(0, value)}px`;
186
+ const normalized = String(value ?? '').trim();
187
+ if (!normalized) return fallback;
188
+ if (/^\d+(?:\.\d+)?$/.test(normalized)) return `${normalized}px`;
189
+ return normalized;
190
+ }
191
+ }
192
+
193
+ export default _stickyHeader;