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.
- package/README.md +24 -2
- package/app/modules/base-module.js +14 -2
- package/app/modules/module-fn.js +5 -4
- package/app/modules/vgalert/js/vgalert.js +21 -1
- package/app/modules/vgalert/scss/vgalert.scss +48 -0
- package/app/modules/vgdropdown/js/vgdropdown.js +58 -27
- package/app/modules/vgfilepreview/js/renderers/video-modal.js +5 -1
- package/app/modules/vgfilepreview/js/renderers/video.js +6 -1
- package/app/modules/vgfilepreview/js/vgfilepreview.js +5 -1
- package/app/modules/vgfiles/js/base.js +14 -5
- package/app/modules/vgfiles/js/droppable.js +37 -31
- package/app/modules/vgfiles/js/vgfiles.js +5 -2
- package/app/modules/vgfiles/scss/_variables.scss +3 -1
- package/app/modules/vgfiles/scss/vgfiles.scss +55 -10
- package/app/modules/vgformsender/js/vgformsender.js +5 -2
- package/app/modules/vgloadmore/js/vgloadmore.js +35 -7
- package/app/modules/vgnestable/js/vgnestable.js +140 -0
- package/app/modules/vgnestable/scss/_variables.scss +16 -9
- package/app/modules/vgnestable/scss/vgnestable.scss +35 -2
- package/app/modules/vgtable/index.js +3 -0
- package/app/modules/vgtable/js/_columns.js +567 -0
- package/app/modules/vgtable/js/_expandable.js +242 -0
- package/app/modules/vgtable/js/_filters.js +264 -0
- package/app/modules/vgtable/js/_fixed-columns.js +249 -0
- package/app/modules/vgtable/js/_i18n.js +75 -0
- package/app/modules/vgtable/js/_options.js +428 -0
- package/app/modules/vgtable/js/_pagination.js +491 -0
- package/app/modules/vgtable/js/_panning.js +124 -0
- package/app/modules/vgtable/js/_params-groups.js +21 -0
- package/app/modules/vgtable/js/_remote.js +395 -0
- package/app/modules/vgtable/js/_row-reorder.js +268 -0
- package/app/modules/vgtable/js/_search.js +134 -0
- package/app/modules/vgtable/js/_selection.js +113 -0
- package/app/modules/vgtable/js/_skeleton.js +123 -0
- package/app/modules/vgtable/js/_sorting.js +340 -0
- package/app/modules/vgtable/js/_states.js +143 -0
- package/app/modules/vgtable/js/_sticky-header.js +193 -0
- package/app/modules/vgtable/js/_url-state.js +178 -0
- package/app/modules/vgtable/js/vgtable.js +1444 -0
- package/app/modules/vgtable/readme.md +922 -0
- package/app/modules/vgtable/scss/_columns.scss +52 -0
- package/app/modules/vgtable/scss/_expandable.scss +54 -0
- package/app/modules/vgtable/scss/_fixed-columns.scss +117 -0
- package/app/modules/vgtable/scss/_pagination.scss +191 -0
- package/app/modules/vgtable/scss/_row-reorder.scss +45 -0
- package/app/modules/vgtable/scss/_skeleton.scss +65 -0
- package/app/modules/vgtable/scss/_sorting.scss +89 -0
- package/app/modules/vgtable/scss/_states.scss +70 -0
- package/app/modules/vgtable/scss/_sticky-header.scss +121 -0
- package/app/modules/vgtable/scss/_variables.scss +296 -0
- package/app/modules/vgtable/scss/vgtable.scss +206 -0
- package/app/utils/js/components/placement.js +64 -10
- package/app/vgapp.js +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/index.js +1 -1
- package/index.scss +3 -3
- package/package.json +5 -5
- package/app/modules/vgdynamictable/index.js +0 -4
- package/app/modules/vgdynamictable/js/editable.js +0 -438
- package/app/modules/vgdynamictable/js/expandable.js +0 -248
- package/app/modules/vgdynamictable/js/filters.js +0 -450
- package/app/modules/vgdynamictable/js/fixed.js +0 -566
- package/app/modules/vgdynamictable/js/options.js +0 -650
- package/app/modules/vgdynamictable/js/pagination.js +0 -623
- package/app/modules/vgdynamictable/js/search.js +0 -82
- package/app/modules/vgdynamictable/js/skeleton.js +0 -144
- package/app/modules/vgdynamictable/js/sortable.js +0 -442
- package/app/modules/vgdynamictable/js/summary-footer.js +0 -284
- package/app/modules/vgdynamictable/js/table-remote.js +0 -821
- package/app/modules/vgdynamictable/js/table-state.js +0 -243
- package/app/modules/vgdynamictable/js/table-url-state.js +0 -444
- package/app/modules/vgdynamictable/js/utils/common.js +0 -48
- package/app/modules/vgdynamictable/js/vgdynamictable.js +0 -3870
- package/app/modules/vgdynamictable/js/viewport.js +0 -322
- package/app/modules/vgdynamictable/readme.md +0 -251
- package/app/modules/vgdynamictable/scss/_actions.scss +0 -196
- package/app/modules/vgdynamictable/scss/_pagination.scss +0 -186
- package/app/modules/vgdynamictable/scss/_skeleton.scss +0 -63
- package/app/modules/vgdynamictable/scss/_sortable.scss +0 -66
- package/app/modules/vgdynamictable/scss/_table.scss +0 -137
- package/app/modules/vgdynamictable/scss/_variables.scss +0 -138
- 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;
|