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,395 @@
1
+ /**
2
+ * Описание: remote-источник строк для VGTable через общий Ajax requestRoute.
3
+ * Возможности: data/view/auto ответы, серверная пагинация и сортировка, parammap, отмена гонок, cache, состояния и экспорт.
4
+ */
5
+ import EventHandler from "../../../utils/js/dom/event";
6
+
7
+
8
+ const EVENT_BEFORE_LOAD = 'beforeload.vg.table';
9
+ const EVENT_REQUEST_SUCCESS = 'requestsuccess.vg.table';
10
+ const EVENT_REQUEST_ERROR = 'requesterror.vg.table';
11
+ const EVENT_DATA_LOADED = 'dataloaded.vg.table';
12
+ const EVENT_AFTER_RENDER = 'afterrender.vg.table';
13
+
14
+ class _remote {
15
+ constructor(table, options = {}, hooks = {}) {
16
+ this._table = table;
17
+ this._options = options;
18
+ this._hooks = hooks;
19
+ this._cache = new Map();
20
+ this._controller = null;
21
+ this._token = 0;
22
+ this._params = {};
23
+ this._lastMeta = {};
24
+ this._lastRequest = null;
25
+ this._loading = false;
26
+ this._boundClick = this._handleClick.bind(this);
27
+ }
28
+
29
+ init() {
30
+ this._table.addEventListener('click', this._boundClick);
31
+ return this.load();
32
+ }
33
+
34
+ dispose() {
35
+ this._token += 1;
36
+ this._controller?.abort?.();
37
+ this._table.removeEventListener('click', this._boundClick);
38
+ this._table.removeAttribute('aria-busy');
39
+ this._table.removeAttribute('data-vg-table-remote');
40
+ this._cache.clear();
41
+ this._controller = null;
42
+ }
43
+
44
+ load(options = {}) {
45
+ const pagination = this._hooks.getPagination?.() || {};
46
+ const page = this._positiveInt(options.page, pagination.page || 1);
47
+ const perPage = this._positiveInt(options.perPage, pagination.perPage || 10);
48
+ if (options.params && typeof options.params === 'object' && !Array.isArray(options.params)) {
49
+ this.setParams(options.params, options.replaceParams === true);
50
+ }
51
+
52
+ return this._request(page, perPage, {
53
+ force: options.force === true,
54
+ source: String(options.source || 'api'),
55
+ });
56
+ }
57
+
58
+ reload(options = {}) {
59
+ return this.load(Object.assign({}, options, {force: options.force !== false}));
60
+ }
61
+
62
+ setParams(params = {}, replace = false) {
63
+ const source = params && typeof params === 'object' && !Array.isArray(params) ? params : {};
64
+ this._params = replace ? Object.assign({}, source) : Object.assign({}, this._params, source);
65
+ return Object.assign({}, this._params);
66
+ }
67
+
68
+ clearCache() {
69
+ this._cache.clear();
70
+ }
71
+
72
+ getState() {
73
+ return {
74
+ loading: this._loading,
75
+ params: Object.assign({}, this._params),
76
+ meta: Object.assign({}, this._lastMeta),
77
+ request: this._lastRequest ? Object.assign({}, this._lastRequest) : null,
78
+ };
79
+ }
80
+
81
+ export(format = 'csv', options = {}) {
82
+ const route = String(options.route || this._options.export?.route || this._options.route || '').trim();
83
+ if (!route) return '';
84
+
85
+ const pagination = this._hooks.getPagination?.() || {};
86
+ const params = this._mapParams(this._buildParams(
87
+ this._positiveInt(options.page, pagination.page || 1),
88
+ this._positiveInt(options.perPage, pagination.perPage || 10)
89
+ ));
90
+ params.format = String(format || 'csv').toLowerCase() === 'xlsx' ? 'xlsx' : 'csv';
91
+ const url = this._hooks.buildUrl(params, route, this._requestConfig(route));
92
+ if (options.open !== false && typeof window !== 'undefined') {
93
+ window.open?.(url.toString(), '_blank', 'noopener');
94
+ }
95
+ return url.toString();
96
+ }
97
+
98
+ async _request(page, perPage, options = {}) {
99
+ const route = String(this._options.route || '').trim();
100
+ if (!route) return null;
101
+
102
+ const requestParams = this._mapParams(this._buildParams(page, perPage));
103
+ const method = String(this._options.method || 'GET').toUpperCase();
104
+ const cacheKey = this._cacheKey(route, method, requestParams);
105
+ const cached = options.force ? null : this._readCache(cacheKey);
106
+ const startedAt = Date.now();
107
+ const token = ++this._token;
108
+ const requestId = `vg-table-${Date.now()}-${token}`;
109
+ const detail = {
110
+ requestId,
111
+ page,
112
+ perPage,
113
+ params: Object.assign({}, requestParams),
114
+ source: options.source,
115
+ fromCache: Boolean(cached),
116
+ };
117
+ this._lastRequest = detail;
118
+ this._controller?.abort?.();
119
+ this._controller = cached || typeof AbortController !== 'function' ? null : new AbortController();
120
+ this._loading = true;
121
+ this._table.setAttribute('data-vg-table-remote', '');
122
+ this._table.setAttribute('aria-busy', 'true');
123
+ this._emit(EVENT_BEFORE_LOAD, detail);
124
+ if (!cached) {
125
+ const rendered = this._hooks.renderLoading?.();
126
+ if (!rendered) this._renderState(this._label('loading', 'Загрузка…'), 'loading');
127
+ }
128
+
129
+ try {
130
+ const response = cached || await this._hooks.request(this._buildRequest(requestParams, this._controller?.signal, options.source));
131
+ if (token !== this._token) return null;
132
+ if (!cached) await this._waitMinLoadingDelay(startedAt);
133
+ if (token !== this._token) return null;
134
+ if (!cached) this._writeCache(cacheKey, response);
135
+ const result = this._applyResponse(response, page, perPage);
136
+ this._emit(EVENT_REQUEST_SUCCESS, Object.assign({}, detail, {response, meta: result.meta}));
137
+ this._emit(EVENT_DATA_LOADED, Object.assign({}, detail, result, {response}));
138
+ this._emit(EVENT_AFTER_RENDER, Object.assign({}, detail, {
139
+ renderedCount: result.renderedCount,
140
+ total: result.meta.total,
141
+ }));
142
+ return result;
143
+ } catch (error) {
144
+ if (error?.name === 'AbortError' || token !== this._token) return null;
145
+ if (!cached) await this._waitMinLoadingDelay(startedAt);
146
+ if (token !== this._token) return null;
147
+ if (!this._hooks.renderError?.(error)) this._renderState(this._label('error', 'Не удалось загрузить данные'), 'error');
148
+ this._emit(EVENT_REQUEST_ERROR, Object.assign({}, detail, {error}));
149
+ return null;
150
+ } finally {
151
+ if (token === this._token) {
152
+ this._loading = false;
153
+ this._table.setAttribute('aria-busy', 'false');
154
+ this._controller = null;
155
+ }
156
+ }
157
+ }
158
+
159
+ _waitMinLoadingDelay(startedAt) {
160
+ const delay = this._nonNegativeInt(this._hooks.getLoadingMinDelay?.(), 0);
161
+ const remaining = delay - Math.max(0, Date.now() - startedAt);
162
+ return remaining > 0
163
+ ? new Promise((resolve) => window.setTimeout(resolve, remaining))
164
+ : Promise.resolve();
165
+ }
166
+
167
+ _buildRequest(params, signal, source = 'api') {
168
+ const method = String(this._options.method || 'GET').toUpperCase();
169
+ const isGetLike = method === 'GET' || method === 'HEAD';
170
+ const config = this._requestConfig();
171
+ if (source === 'retry') config.headers['X-VGTable-Retry'] = '1';
172
+ return Object.assign(config, {
173
+ method,
174
+ params: isGetLike ? params : {},
175
+ body: isGetLike ? null : params,
176
+ signal,
177
+ responseType: 'json',
178
+ });
179
+ }
180
+
181
+ _requestConfig(route = this._options.route) {
182
+ return {
183
+ route,
184
+ method: this._options.method,
185
+ credentials: this._options.credentials,
186
+ headers: Object.assign({}, this._options.headers || {}),
187
+ baseParams: Object.assign({}, this._options.params || {}),
188
+ };
189
+ }
190
+
191
+ _buildParams(page, perPage) {
192
+ const params = Object.assign({}, this._params, {page, per_page: perPage});
193
+ const sort = this._hooks.getSort?.();
194
+ const sorts = Array.isArray(sort?.sorts) && sort.sorts.length ? sort.sorts : (sort?.field ? [sort] : []);
195
+ if (sorts.length) {
196
+ params.sort = sorts.map((item) => item.field).filter(Boolean).join(',');
197
+ params.dir = sorts.map((item) => item.direction === 'desc' ? 'desc' : 'asc').join(',');
198
+ }
199
+
200
+ const mode = this._responseMode();
201
+ params.responsemode = mode;
202
+ if (mode !== 'data') {
203
+ const viewParam = String(this._options.viewparam || '').trim();
204
+ const viewValue = String(this._options.viewvalue || '').trim();
205
+ if (viewParam && viewValue) params[viewParam] = viewValue;
206
+ const fieldsParam = String(this._options.fieldsparam || '').trim();
207
+ const fields = this._fields();
208
+ if (fieldsParam && fields.length) params[fieldsParam] = fields.join(',');
209
+ }
210
+ return params;
211
+ }
212
+
213
+ _mapParams(params) {
214
+ const map = this._options.parammap && typeof this._options.parammap === 'object'
215
+ ? this._options.parammap
216
+ : {};
217
+ return Object.keys(params).reduce((result, key) => {
218
+ const mapped = String(Object.prototype.hasOwnProperty.call(map, key) ? map[key] : key).trim() || key;
219
+ result[mapped] = params[key];
220
+ return result;
221
+ }, {});
222
+ }
223
+
224
+ _applyResponse(response, requestedPage, requestedPerPage) {
225
+ const rows = this._extractRows(response);
226
+ const view = this._extractView(response);
227
+ const meta = this._normalizeMeta(this._readPath(response, this._options.metapath), rows, requestedPage, requestedPerPage);
228
+ const mode = this._responseMode();
229
+ const useView = mode === 'view' || (mode === 'auto' && typeof view === 'string' && view.trim() !== '');
230
+ let renderedCount = 0;
231
+
232
+ if (useView && typeof view === 'string' && view.trim() !== '') {
233
+ this._body().innerHTML = view;
234
+ renderedCount = this._body().rows.length;
235
+ } else if (Array.isArray(rows) && rows.length) {
236
+ renderedCount = this._renderRows(rows);
237
+ } else {
238
+ if (!this._hooks.renderEmpty?.({rows, view, meta})) this._renderState(this._label('empty', 'Ничего не найдено'), 'empty');
239
+ }
240
+
241
+ this._lastMeta = meta;
242
+ this._hooks.applyMeta?.(meta);
243
+ this._hooks.afterRender?.({rows, view, meta, renderedCount});
244
+ return {rows, view, meta, renderedCount};
245
+ }
246
+
247
+ _renderRows(rows) {
248
+ const body = this._body();
249
+ const fields = this._fields();
250
+ const fragment = this._table.ownerDocument.createDocumentFragment();
251
+ rows.forEach((row) => {
252
+ const tr = this._table.ownerDocument.createElement('tr');
253
+ fields.forEach((field) => {
254
+ const td = this._table.ownerDocument.createElement('td');
255
+ const value = this._readPath(row, field);
256
+ td.textContent = value === undefined || value === null ? '' : String(value);
257
+ tr.append(td);
258
+ });
259
+ fragment.append(tr);
260
+ });
261
+ body.replaceChildren(fragment);
262
+ return rows.length;
263
+ }
264
+
265
+ _renderState(message, state) {
266
+ const body = this._body();
267
+ const row = this._table.ownerDocument.createElement('tr');
268
+ const cell = this._table.ownerDocument.createElement('td');
269
+ cell.colSpan = Math.max(1, this._fields().length || this._headerCells().length);
270
+ cell.setAttribute('data-vg-table-state', state);
271
+ const text = this._table.ownerDocument.createElement('span');
272
+ text.textContent = message;
273
+ cell.append(text);
274
+ if (state === 'error') {
275
+ const button = this._table.ownerDocument.createElement('button');
276
+ button.type = 'button';
277
+ button.className = 'vg-table-state__retry';
278
+ button.setAttribute('data-vg-table-retry', '');
279
+ button.textContent = this._label('retry', 'Повторить');
280
+ cell.append(button);
281
+ }
282
+ row.append(cell);
283
+ body.replaceChildren(row);
284
+ }
285
+
286
+ _label(key, fallback) {
287
+ return String(this._options.labels?.[key] || fallback || '');
288
+ }
289
+
290
+ _normalizeMeta(source, rows, page, perPage) {
291
+ const meta = source && typeof source === 'object' && !Array.isArray(source) ? source : {};
292
+ const normalizedPage = this._positiveInt(meta.page ?? meta.current_page, page);
293
+ const normalizedPerPage = this._positiveInt(meta.per_page ?? meta.perPage, perPage);
294
+ const total = this._nonNegativeInt(meta.total, Array.isArray(rows) ? rows.length : 0);
295
+ const pages = this._positiveInt(meta.pages ?? meta.last_page, Math.max(1, Math.ceil(total / normalizedPerPage)));
296
+ return Object.assign({}, meta, {
297
+ page: Math.min(normalizedPage, pages),
298
+ per_page: normalizedPerPage,
299
+ total,
300
+ pages,
301
+ });
302
+ }
303
+
304
+ _extractRows(response) {
305
+ const rows = this._readPath(response, this._options.datapath);
306
+ return Array.isArray(rows) ? rows : [];
307
+ }
308
+
309
+ _extractView(response) {
310
+ const view = this._readPath(response, this._options.viewpath);
311
+ return typeof view === 'string' ? view : '';
312
+ }
313
+
314
+ _readPath(source, path) {
315
+ const parts = String(path || '').split('.').map((item) => item.trim()).filter(Boolean);
316
+ let current = source;
317
+ for (const part of parts) {
318
+ if (!current || typeof current !== 'object' || !Object.prototype.hasOwnProperty.call(current, part)) return undefined;
319
+ current = current[part];
320
+ }
321
+ return current;
322
+ }
323
+
324
+ _fields() {
325
+ return this._headerCells()
326
+ .map((header) => String(header.getAttribute('data-field') || '').trim())
327
+ .filter(Boolean);
328
+ }
329
+
330
+ _headerCells() {
331
+ const headerTable = this._hooks.getHeaderTable?.() || this._table;
332
+ const head = headerTable.tHead || headerTable.querySelector('thead');
333
+ return head?.rows.length ? Array.from(head.rows[head.rows.length - 1].cells) : [];
334
+ }
335
+
336
+ _body() {
337
+ return this._table.tBodies[0] || this._table.createTBody();
338
+ }
339
+
340
+ _responseMode() {
341
+ const mode = String(this._options.responsemode || 'data').toLowerCase();
342
+ return ['data', 'view', 'auto'].includes(mode) ? mode : 'data';
343
+ }
344
+
345
+ _cacheKey(route, method, params) {
346
+ const query = Object.keys(params).sort().map((key) => {
347
+ const value = Array.isArray(params[key]) ? params[key].join(',') : params[key];
348
+ return `${encodeURIComponent(key)}=${encodeURIComponent(value ?? '')}`;
349
+ }).join('&');
350
+ return `${method}:${route}?${query}`;
351
+ }
352
+
353
+ _readCache(key) {
354
+ if (this._options.cache?.enable === false) return null;
355
+ const entry = this._cache.get(key);
356
+ if (!entry) return null;
357
+ const ttl = this._positiveInt(this._options.cache?.ttl, 30000);
358
+ if (Date.now() - entry.createdAt > ttl) {
359
+ this._cache.delete(key);
360
+ return null;
361
+ }
362
+ entry.usedAt = Date.now();
363
+ return entry.response;
364
+ }
365
+
366
+ _writeCache(key, response) {
367
+ if (this._options.cache?.enable === false) return;
368
+ this._cache.set(key, {response, createdAt: Date.now(), usedAt: Date.now()});
369
+ const max = this._positiveInt(this._options.cache?.max, 30);
370
+ if (this._cache.size <= max) return;
371
+ const oldest = Array.from(this._cache.entries()).sort((left, right) => left[1].usedAt - right[1].usedAt)[0];
372
+ if (oldest) this._cache.delete(oldest[0]);
373
+ }
374
+
375
+ _handleClick(event) {
376
+ if (!event.target.closest('[data-vg-table-retry]')) return;
377
+ this.reload({source: 'retry'});
378
+ }
379
+
380
+ _emit(name, detail) {
381
+ EventHandler.trigger(this._table, name, detail);
382
+ }
383
+
384
+ _positiveInt(value, fallback) {
385
+ const parsed = Number.parseInt(value, 10);
386
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
387
+ }
388
+
389
+ _nonNegativeInt(value, fallback) {
390
+ const parsed = Number.parseInt(value, 10);
391
+ return Number.isFinite(parsed) && parsed >= 0 ? parsed : fallback;
392
+ }
393
+ }
394
+
395
+ export default _remote;
@@ -0,0 +1,268 @@
1
+ /**
2
+ * Описание: самостоятельное изменение порядка строк VGTable.
3
+ * Возможности: режим строки или handle, native drag-and-drop, клавиатура, localStorage, публичные методы и события.
4
+ */
5
+ import EventHandler from "../../../utils/js/dom/event";
6
+
7
+ const EVENT_REORDER = 'rowreorder.vg.table';
8
+ const DRAGGING_ATTRIBUTE = 'data-vg-table-row-dragging';
9
+ const OVER_ATTRIBUTE = 'data-vg-table-row-drag-over';
10
+ const DECORATED_ATTRIBUTE = 'data-vg-table-row-reorder';
11
+ const STATE_ROW_SELECTOR = '[data-vg-table-state-row], [data-vg-table-skeleton-row]';
12
+ const INTERACTIVE_SELECTOR = 'a, button, input, select, textarea, [contenteditable="true"]';
13
+
14
+ class _rowReorder {
15
+ constructor(table, wrapper, options = {}, hooks = {}) {
16
+ this._table = table;
17
+ this._wrapper = wrapper;
18
+ this._options = options;
19
+ this._hooks = hooks;
20
+ this._original = new Map();
21
+ this._rowState = new WeakMap();
22
+ this._handleState = new WeakMap();
23
+ this._drag = {row: null, fromIndex: -1, target: null, after: false};
24
+ this._boundDragStart = this._handleDragStart.bind(this);
25
+ this._boundDragOver = this._handleDragOver.bind(this);
26
+ this._boundDrop = this._handleDrop.bind(this);
27
+ this._boundDragEnd = this._clearDrag.bind(this);
28
+ this._boundKeydown = this._handleKeydown.bind(this);
29
+ }
30
+
31
+ init() {
32
+ Array.from(this._table.tBodies || []).forEach(body => this._original.set(body, this._rows(body)));
33
+ this._restoreStoredOrder();
34
+ this.refresh();
35
+ this._table.addEventListener('dragstart', this._boundDragStart);
36
+ this._table.addEventListener('dragover', this._boundDragOver);
37
+ this._table.addEventListener('drop', this._boundDrop);
38
+ this._table.addEventListener('dragend', this._boundDragEnd);
39
+ this._table.addEventListener('keydown', this._boundKeydown);
40
+ return this;
41
+ }
42
+
43
+ refresh() {
44
+ this._rows().forEach((row, index) => this._decorate(row, index));
45
+ return this.getState();
46
+ }
47
+
48
+ getState() {
49
+ return {order: this._rows().map((row, index) => this._key(row, index)), rows: this._rows()};
50
+ }
51
+
52
+ move(row, target, position = 'before', emit = true) {
53
+ const moving = this._resolveRow(row);
54
+ const destination = this._resolveRow(target);
55
+ if (!moving || !destination || moving === destination || moving.parentElement !== destination.parentElement) return false;
56
+ if (this._disabled(moving) || this._disabled(destination)) return false;
57
+ const rows = this._rows(moving.parentElement);
58
+ const fromIndex = rows.indexOf(moving);
59
+ const after = String(position).toLowerCase() === 'after';
60
+ if ((!after && moving.nextElementSibling === destination) || (after && destination.nextElementSibling === moving)) return false;
61
+ moving.parentElement.insertBefore(moving, after ? destination.nextSibling : destination);
62
+ this._afterMove(moving, fromIndex, emit);
63
+ return true;
64
+ }
65
+
66
+ reset(emit = true) {
67
+ this._original.forEach((rows, body) => rows.forEach(row => body.append(row)));
68
+ this._clearStorage();
69
+ this.refresh();
70
+ this._hooks.refreshLayout?.();
71
+ if (emit) EventHandler.trigger(this._table, EVENT_REORDER, Object.assign({reset: true}, this.getState()));
72
+ return this.getState();
73
+ }
74
+
75
+ dispose() {
76
+ this._table.removeEventListener('dragstart', this._boundDragStart);
77
+ this._table.removeEventListener('dragover', this._boundDragOver);
78
+ this._table.removeEventListener('drop', this._boundDrop);
79
+ this._table.removeEventListener('dragend', this._boundDragEnd);
80
+ this._table.removeEventListener('keydown', this._boundKeydown);
81
+ this._clearDrag();
82
+ this._rows().forEach(row => this._undecorate(row));
83
+ this._original.forEach((rows, body) => rows.forEach(row => body.append(row)));
84
+ this._original.clear();
85
+ }
86
+
87
+ _decorate(row, index) {
88
+ if (!this._rowState.has(row)) this._rowState.set(row, {
89
+ draggable: row.getAttribute('draggable'), tabindex: row.getAttribute('tabindex'), label: row.getAttribute('aria-label'),
90
+ });
91
+ const disabled = this._disabled(row);
92
+ const mode = this._mode();
93
+ row.setAttribute(DECORATED_ATTRIBUTE, disabled ? 'disabled' : mode);
94
+ row.draggable = !disabled && mode === 'row';
95
+ if (!disabled && mode === 'row') {
96
+ if (!row.hasAttribute('tabindex')) row.tabIndex = 0;
97
+ row.setAttribute('aria-label', this._handleLabel(row, index));
98
+ }
99
+ this._handles(row).forEach(handle => {
100
+ if (!this._handleState.has(handle)) this._handleState.set(handle, {
101
+ draggable: handle.getAttribute('draggable'), tabindex: handle.getAttribute('tabindex'),
102
+ role: handle.getAttribute('role'), label: handle.getAttribute('aria-label'), disabled: handle.getAttribute('aria-disabled'),
103
+ });
104
+ handle.draggable = !disabled && mode === 'handle';
105
+ if (!disabled && !handle.hasAttribute('tabindex')) handle.tabIndex = 0;
106
+ if (!handle.hasAttribute('role')) handle.setAttribute('role', 'button');
107
+ handle.setAttribute('aria-label', this._handleLabel(row, index));
108
+ handle.toggleAttribute('aria-disabled', disabled);
109
+ });
110
+ }
111
+
112
+ _undecorate(row) {
113
+ row.removeAttribute(DECORATED_ATTRIBUTE);
114
+ row.removeAttribute(DRAGGING_ATTRIBUTE);
115
+ row.removeAttribute(OVER_ATTRIBUTE);
116
+ const rowState = this._rowState.get(row) || {};
117
+ this._restoreAttribute(row, 'draggable', rowState.draggable);
118
+ this._restoreAttribute(row, 'tabindex', rowState.tabindex);
119
+ this._restoreAttribute(row, 'aria-label', rowState.label);
120
+ this._handles(row).forEach(handle => {
121
+ const state = this._handleState.get(handle) || {};
122
+ this._restoreAttribute(handle, 'draggable', state.draggable);
123
+ this._restoreAttribute(handle, 'tabindex', state.tabindex);
124
+ this._restoreAttribute(handle, 'role', state.role);
125
+ this._restoreAttribute(handle, 'aria-label', state.label);
126
+ this._restoreAttribute(handle, 'aria-disabled', state.disabled);
127
+ });
128
+ }
129
+
130
+ _restoreAttribute(element, name, value) {
131
+ if (value === null || value === undefined) element.removeAttribute(name);
132
+ else element.setAttribute(name, value);
133
+ }
134
+
135
+ _handleDragStart(event) {
136
+ const row = event.target.closest?.(`tr[${DECORATED_ATTRIBUTE}]`);
137
+ if (!row || this._disabled(row)) return;
138
+ if (this._mode() === 'handle' && !event.target.closest?.(this._handleSelector())) {
139
+ event.preventDefault();
140
+ return;
141
+ }
142
+ if (this._mode() === 'row' && event.target.closest?.(INTERACTIVE_SELECTOR)) {
143
+ event.preventDefault();
144
+ return;
145
+ }
146
+ this._drag = {row, fromIndex: this._rows(row.parentElement).indexOf(row), target: null, after: false};
147
+ row.setAttribute(DRAGGING_ATTRIBUTE, '');
148
+ this._wrapper?.classList.add('vg-table-wrapper--row-dragging');
149
+ if (event.dataTransfer) {
150
+ event.dataTransfer.effectAllowed = 'move';
151
+ try { event.dataTransfer.setData('text/plain', this._key(row, this._drag.fromIndex)); } catch (error) { /* noop */ }
152
+ }
153
+ }
154
+
155
+ _handleDragOver(event) {
156
+ if (!this._drag.row) return;
157
+ const target = event.target.closest?.(`tr[${DECORATED_ATTRIBUTE}]`);
158
+ if (!target || target === this._drag.row || target.parentElement !== this._drag.row.parentElement || this._disabled(target)) return;
159
+ event.preventDefault();
160
+ const rect = target.getBoundingClientRect();
161
+ const after = event.clientY > rect.top + rect.height / 2;
162
+ this._rows().forEach(row => row.removeAttribute(OVER_ATTRIBUTE));
163
+ target.setAttribute(OVER_ATTRIBUTE, after ? 'after' : 'before');
164
+ this._drag.target = target;
165
+ this._drag.after = after;
166
+ }
167
+
168
+ _handleDrop(event) {
169
+ if (!this._drag.row || !this._drag.target) return this._clearDrag();
170
+ event.preventDefault();
171
+ const {row, target, after, fromIndex} = this._drag;
172
+ row.parentElement.insertBefore(row, after ? target.nextSibling : target);
173
+ this._clearDrag();
174
+ this._afterMove(row, fromIndex, true);
175
+ }
176
+
177
+ _handleKeydown(event) {
178
+ if (!['ArrowUp', 'ArrowDown', 'Home', 'End'].includes(event.key)) return;
179
+ const handle = event.target.closest?.(this._handleSelector());
180
+ const row = handle?.closest?.(`tr[${DECORATED_ATTRIBUTE}]`)
181
+ || (this._mode() === 'row' ? event.target.closest?.(`tr[${DECORATED_ATTRIBUTE}]`) : null);
182
+ if (!row || this._disabled(row)) return;
183
+ if (!handle && event.target !== row && event.target.closest?.(INTERACTIVE_SELECTOR)) return;
184
+ const rows = this._rows(row.parentElement).filter(item => !this._disabled(item));
185
+ const index = rows.indexOf(row);
186
+ const target = event.key === 'Home' ? rows[0] : event.key === 'End' ? rows.at(-1) : rows[index + (event.key === 'ArrowUp' ? -1 : 1)];
187
+ if (!target || target === row) return;
188
+ event.preventDefault();
189
+ this.move(row, target, event.key === 'ArrowDown' || event.key === 'End' ? 'after' : 'before', true);
190
+ (handle || row).focus?.();
191
+ }
192
+
193
+ _afterMove(row, fromIndex, emit) {
194
+ this.refresh();
195
+ this._storeOrder();
196
+ this._hooks.refreshLayout?.();
197
+ if (!emit) return;
198
+ const rows = this._rows(row.parentElement);
199
+ const toIndex = rows.indexOf(row);
200
+ EventHandler.trigger(this._table, EVENT_REORDER, {
201
+ row, key: this._key(row, toIndex), fromIndex, toIndex,
202
+ beforeRow: row.previousElementSibling || null, afterRow: row.nextElementSibling || null,
203
+ order: rows.map((item, index) => this._key(item, index)),
204
+ });
205
+ }
206
+
207
+ _clearDrag() {
208
+ this._rows().forEach(row => {
209
+ row.removeAttribute(DRAGGING_ATTRIBUTE);
210
+ row.removeAttribute(OVER_ATTRIBUTE);
211
+ });
212
+ this._wrapper?.classList.remove('vg-table-wrapper--row-dragging');
213
+ this._drag = {row: null, fromIndex: -1, target: null, after: false};
214
+ }
215
+
216
+ _rows(body = null) {
217
+ const bodies = body ? [body] : Array.from(this._table.tBodies || []);
218
+ return bodies.flatMap(item => Array.from(item.rows || [])).filter(row => !row.matches(STATE_ROW_SELECTOR));
219
+ }
220
+
221
+ _resolveRow(value) {
222
+ if (value?.tagName === 'TR' && this._table.contains(value)) return value;
223
+ const rows = this._rows();
224
+ if (typeof value === 'number') return rows[Number.parseInt(value, 10)] || null;
225
+ const key = String(value || '').trim();
226
+ return rows.find((row, index) => this._key(row, index) === key) || null;
227
+ }
228
+
229
+ _key(row, index) {
230
+ return String(row.getAttribute(this._options.keyAttr || 'data-row-key') ?? row.getAttribute('data-id') ?? index);
231
+ }
232
+
233
+ _disabled(row) { return row.hasAttribute('data-row-reorder-disabled'); }
234
+ _mode() { return String(this._options.mode || 'handle').toLowerCase() === 'row' ? 'row' : 'handle'; }
235
+ _handleSelector() { return String(this._options.handleSelector || '[data-row-reorder-handle]').trim() || '[data-row-reorder-handle]'; }
236
+ _handles(row) { return Array.from(row.querySelectorAll(this._handleSelector())); }
237
+ _handleLabel(row, index) {
238
+ const template = String(this._options.labels?.handle || 'Изменить позицию строки {row}');
239
+ return template.replace('{row}', this._key(row, index));
240
+ }
241
+
242
+ _storageKey() {
243
+ if (this._options.persist !== true || typeof window === 'undefined') return '';
244
+ return String(this._options.storageKey || '').trim() || (this._table.id ? `vg-table:${this._table.id}:row-order` : '');
245
+ }
246
+ _storeOrder() {
247
+ const key = this._storageKey();
248
+ if (key) try { window.localStorage.setItem(key, JSON.stringify(this.getState().order)); } catch (error) { /* noop */ }
249
+ }
250
+ _restoreStoredOrder() {
251
+ const key = this._storageKey();
252
+ if (!key) return;
253
+ let order = [];
254
+ try { order = JSON.parse(window.localStorage.getItem(key)) || []; } catch (error) { return; }
255
+ if (!Array.isArray(order)) return;
256
+ order.forEach((rowKey, targetIndex) => {
257
+ const row = this._resolveRow(String(rowKey));
258
+ const target = this._rows(row?.parentElement)[targetIndex];
259
+ if (row && target && row !== target && !this._disabled(row) && !this._disabled(target)) target.before(row);
260
+ });
261
+ }
262
+ _clearStorage() {
263
+ const key = this._storageKey();
264
+ if (key) try { window.localStorage.removeItem(key); } catch (error) { /* noop */ }
265
+ }
266
+ }
267
+
268
+ export default _rowReorder;