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,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;
|