vgapp 0.7.9 → 0.8.0

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 (47) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/app/langs/en/buttons.json +10 -0
  3. package/app/langs/en/messages.json +32 -0
  4. package/app/langs/en/titles.json +6 -0
  5. package/app/langs/ru/buttons.json +10 -0
  6. package/app/langs/ru/messages.json +32 -0
  7. package/app/langs/ru/titles.json +6 -0
  8. package/app/modules/base-module.js +12 -1
  9. package/app/modules/module-fn.js +20 -9
  10. package/app/modules/vgalert/js/vgalert.js +12 -6
  11. package/app/modules/vgalert/readme.md +1 -1
  12. package/app/modules/vgcollapse/readme.md +1 -1
  13. package/app/modules/vgdropdown/js/vgdropdown.js +140 -38
  14. package/app/modules/vgdropdown/readme.md +225 -0
  15. package/app/modules/vgfiles/js/base.js +499 -0
  16. package/app/modules/vgfiles/js/droppable.js +159 -0
  17. package/app/modules/vgfiles/js/loader.js +389 -0
  18. package/app/modules/vgfiles/js/render.js +83 -0
  19. package/app/modules/vgfiles/js/sortable.js +155 -0
  20. package/app/modules/vgfiles/js/vgfiles.js +796 -280
  21. package/app/modules/vgfiles/readme.md +193 -0
  22. package/app/modules/vgfiles/scss/_animations.scss +18 -0
  23. package/app/modules/vgfiles/scss/_mixins.scss +73 -0
  24. package/app/modules/vgfiles/scss/_variables.scss +103 -26
  25. package/app/modules/vgfiles/scss/vgfiles.scss +573 -60
  26. package/app/modules/vgformsender/js/vgformsender.js +5 -1
  27. package/app/modules/vgformsender/readme.md +1 -1
  28. package/app/modules/vglawcookie/js/vglawcookie.js +96 -62
  29. package/app/modules/vglawcookie/readme.md +102 -0
  30. package/app/modules/vgsidebar/js/vgsidebar.js +6 -4
  31. package/app/utils/js/components/ajax.js +172 -122
  32. package/app/utils/js/components/animation.js +124 -39
  33. package/app/utils/js/components/backdrop.js +54 -31
  34. package/app/utils/js/components/lang.js +69 -88
  35. package/app/utils/js/components/params.js +34 -31
  36. package/app/utils/js/components/scrollbar.js +118 -67
  37. package/app/utils/js/components/templater.js +14 -4
  38. package/app/utils/js/dom/cookie.js +107 -64
  39. package/app/utils/js/dom/data.js +68 -20
  40. package/app/utils/js/dom/event.js +272 -239
  41. package/app/utils/js/dom/manipulator.js +135 -62
  42. package/app/utils/js/dom/selectors.js +134 -59
  43. package/app/utils/js/functions.js +183 -349
  44. package/build/vgapp.css +1 -1
  45. package/build/vgapp.css.map +1 -1
  46. package/package.json +1 -1
  47. package/app/utils/js/components/overflow.js +0 -28
@@ -0,0 +1,389 @@
1
+ import Ajax from "../../../utils/js/components/ajax";
2
+ import { normalizeData } from "../../../utils/js/functions";
3
+
4
+ class FileUploader {
5
+ constructor(options = {}) {
6
+ // Настройки
7
+ this.mode = options.mode || 'sequential';
8
+ this.maxParallel = options.maxParallel || 3;
9
+ this.maxConcurrent = options.maxConcurrent || 1; // для sequential
10
+ this.retryAttempts = options.retryAttempts || 3;
11
+ this.retryDelay = options.retryDelay || 1000;
12
+
13
+ // Состояние загрузок
14
+ this.uploads = new Map(); // id → uploadData
15
+ this.completed = [];
16
+ this.queue = []; // очередь для sequential
17
+ this.waitingPromises = []; // для parallel: промисы, ожидающие слот
18
+
19
+ // Состояние
20
+ this.activeCount = 0;
21
+ this.isPaused = false;
22
+
23
+ // Коллбэки
24
+ this.callbacks = {
25
+ progress: [],
26
+ complete: [],
27
+ error: [],
28
+ allComplete: []
29
+ };
30
+
31
+ this._checkAllCompleteBound = this.checkAllComplete.bind(this);
32
+ }
33
+
34
+ // === ОСНОВНЫЕ МЕТОДЫ ===
35
+
36
+ /**
37
+ * Загрузка одного файла
38
+ */
39
+ async uploadFile(file, url, options = {}) {
40
+ const id = this.generateId();
41
+ const uploadData = {
42
+ id,
43
+ file,
44
+ url,
45
+ options,
46
+ status: 'pending',
47
+ progress: 0,
48
+ attempts: 0,
49
+ result: null,
50
+ error: null,
51
+ startTime: null,
52
+ endTime: null,
53
+ controller: new AbortController(), // для отмены
54
+ signal: null // будет установлен при старте
55
+ };
56
+
57
+ this.uploads.set(id, uploadData);
58
+
59
+ if (this.mode === 'sequential') {
60
+ return this._addToSequentialQueue(uploadData);
61
+ } else {
62
+ return this._startParallelUpload(uploadData);
63
+ }
64
+ }
65
+
66
+ /**
67
+ * Массовая загрузка файлов
68
+ */
69
+ async uploadFiles(files, url, options = {}) {
70
+ const promises = files.map(file => this.uploadFile(file, url, options));
71
+ const results = await Promise.allSettled(promises);
72
+
73
+ return results.map(result => {
74
+ if (result.status === 'fulfilled') {
75
+ return { file: result.value.file?.name, success: true, result: result.value };
76
+ } else {
77
+ return { file: result.reason?.file?.name || 'unknown', success: false, error: result.reason };
78
+ }
79
+ });
80
+ }
81
+
82
+ // === РЕЖИМЫ ЗАГРУЗКИ ===
83
+
84
+ _addToSequentialQueue(uploadData) {
85
+ return new Promise((resolve, reject) => {
86
+ this.queue.push({ uploadData, resolve, reject });
87
+ this._processSequentialQueue();
88
+ });
89
+ }
90
+
91
+ async _processSequentialQueue() {
92
+ if (
93
+ this.isPaused ||
94
+ this.activeCount >= this.maxConcurrent ||
95
+ this.queue.length === 0
96
+ ) {
97
+ return;
98
+ }
99
+
100
+ const { uploadData, resolve, reject } = this.queue[0]; // не удаляем, пока не начнём
101
+ this.activeCount++;
102
+
103
+ try {
104
+ const result = await this._executeUpload(uploadData);
105
+ resolve(result);
106
+ } catch (error) {
107
+ reject(error);
108
+ } finally {
109
+ this.queue.shift(); // удаляем только после завершения
110
+ this.activeCount--;
111
+ await this._processSequentialQueue();
112
+ }
113
+ }
114
+
115
+ async _startParallelUpload(uploadData) {
116
+ // Ждём свободный слот
117
+ if (this.activeCount >= this.maxParallel) {
118
+ await new Promise(resolve => this.waitingPromises.push(resolve));
119
+ }
120
+
121
+ this.activeCount++;
122
+ uploadData.startTime = Date.now();
123
+
124
+ try {
125
+ return await this._executeUpload(uploadData);
126
+ } finally {
127
+ this.activeCount--;
128
+ this._notifyWaiting(); // освободили слот
129
+ }
130
+ }
131
+
132
+ _notifyWaiting() {
133
+ if (this.waitingPromises.length > 0 && this.activeCount < this.maxParallel) {
134
+ const resolve = this.waitingPromises.shift();
135
+ resolve();
136
+ }
137
+ }
138
+
139
+ // === ВЫПОЛНЕНИЕ ЗАГРУЗКИ ===
140
+
141
+ async _executeUpload(uploadData, attempt = 1) {
142
+ uploadData.status = 'uploading';
143
+ uploadData.attempts = attempt;
144
+ uploadData.signal = uploadData.controller.signal;
145
+
146
+ try {
147
+ const result = await this._performUpload(uploadData);
148
+ this._completeUpload(uploadData, result);
149
+ return result;
150
+ } catch (error) {
151
+ if (uploadData.status === 'cancelled') return;
152
+
153
+ uploadData.error = error;
154
+
155
+ if (attempt < this.retryAttempts) {
156
+ uploadData.status = 'retrying';
157
+ this.notifyProgress(uploadData);
158
+ await this._delay(this.retryDelay * Math.pow(2, attempt)); // экспоненциальная задержка
159
+ return this._executeUpload(uploadData, attempt + 1);
160
+ } else {
161
+ uploadData.status = 'failed';
162
+ uploadData.endTime = Date.now();
163
+ this.notifyProgress(uploadData);
164
+ this.notifyError(uploadData, error);
165
+ this._checkAllCompleteBound();
166
+ throw error;
167
+ }
168
+ }
169
+ }
170
+
171
+ _performUpload(uploadData) {
172
+ return new Promise((resolve, reject) => {
173
+ const ajax = new Ajax();
174
+ const formData = new FormData();
175
+
176
+ formData.append('file', uploadData.file);
177
+ formData.append('_token', ajax._getCsrfToken() || '');
178
+
179
+ if (uploadData.options.additionalData) {
180
+ Object.entries(uploadData.options.additionalData).forEach(([key, value]) => {
181
+ formData.append(key, value);
182
+ });
183
+ }
184
+
185
+ // Передаём AbortController.signal
186
+ const config = {
187
+ onProgress: (percent, event) => {
188
+ uploadData.progress = percent;
189
+ this.notifyProgress(uploadData);
190
+ },
191
+ onSuccess: (data) => {
192
+ resolve(normalizeData(data));
193
+ },
194
+ onError: (err) => {
195
+ reject(normalizeData(err));
196
+ },
197
+ signal: uploadData.signal
198
+ };
199
+
200
+ // Предполагаем, что ajax.post поддерживает signal
201
+ const xhr = ajax.post(uploadData.url, formData, config);
202
+ uploadData.xhr = xhr; // для отмены
203
+ });
204
+ }
205
+
206
+ _completeUpload(uploadData, result) {
207
+ uploadData.status = 'completed';
208
+ uploadData.progress = 100;
209
+ uploadData.result = result;
210
+ uploadData.endTime = Date.now();
211
+
212
+ this.completed.push({ ...uploadData });
213
+ this.notifyComplete(uploadData);
214
+ this._checkAllCompleteBound();
215
+ }
216
+
217
+ // === УПРАВЛЕНИЕ ===
218
+
219
+ cancelUpload(id) {
220
+ const uploadData = this.uploads.get(id);
221
+ if (!uploadData || ['completed', 'failed', 'cancelled'].includes(uploadData.status)) {
222
+ return false;
223
+ }
224
+
225
+ // Отмена
226
+ uploadData.controller.abort();
227
+ if (uploadData.xhr && typeof uploadData.xhr.abort === 'function') {
228
+ uploadData.xhr.abort();
229
+ }
230
+
231
+ uploadData.status = 'cancelled';
232
+ uploadData.endTime = Date.now();
233
+ this.notifyProgress(uploadData);
234
+
235
+ // Удаление из очереди
236
+ const queueIndex = this.queue.findIndex(task => task.uploadData.id === id);
237
+ if (queueIndex > -1) {
238
+ this.queue.splice(queueIndex, 1);
239
+ }
240
+
241
+ return true;
242
+ }
243
+
244
+ setMode(mode, maxParallel = 3) {
245
+ this.mode = mode;
246
+ this.maxParallel = maxParallel;
247
+
248
+ if (mode === 'parallel') {
249
+ this.queue = [];
250
+ this._notifyWaiting(); // разблокировать ожидающие промисы
251
+ }
252
+ }
253
+
254
+ pause() {
255
+ this.isPaused = true;
256
+ }
257
+
258
+ resume() {
259
+ this.isPaused = false;
260
+ if (this.mode === 'sequential') {
261
+ this._processSequentialQueue();
262
+ } else {
263
+ this._notifyWaiting(); // может быть, кто-то ждёт
264
+ }
265
+ }
266
+
267
+ clear() {
268
+ this.uploads.forEach((_, id) => {
269
+ this.cancelUpload(id);
270
+ });
271
+
272
+ this.uploads.clear();
273
+ this.completed = [];
274
+ this.queue = [];
275
+ this.waitingPromises = [];
276
+ this.activeCount = 0;
277
+ }
278
+
279
+ // === СТАТИСТИКА И ВСПОМОГАТЕЛЬНЫЕ ===
280
+
281
+ getStats() {
282
+ const all = Array.from(this.uploads.values());
283
+ const totalSize = all.reduce((sum, u) => sum + (u.file?.size || 0), 0);
284
+
285
+ return {
286
+ total: all.length,
287
+ active: this.activeCount,
288
+ pending: all.filter(u => u.status === 'pending').length,
289
+ uploading: all.filter(u => u.status === 'uploading').length,
290
+ completed: all.filter(u => u.status === 'completed').length,
291
+ failed: all.filter(u => u.status === 'failed').length,
292
+ cancelled: all.filter(u => u.status === 'cancelled').length,
293
+ mode: this.mode,
294
+ isPaused: this.isPaused,
295
+ queueLength: this.queue.length,
296
+ totalSizeFormatted: (totalSize / (1024 * 1024)).toFixed(2) + ' MB'
297
+ };
298
+ }
299
+
300
+ printDebugStats() {
301
+ const stats = this.getStats();
302
+ console.group('--- Uploader Statistics ---');
303
+ console.table({
304
+ 'Mode': stats.mode,
305
+ 'Status': stats.isPaused ? 'Paused' : 'Running',
306
+ 'Total Files': stats.total,
307
+ 'Uploading': stats.uploading,
308
+ 'Completed': stats.completed,
309
+ 'Failed': stats.failed,
310
+ 'Queue': stats.queueLength
311
+ });
312
+ console.log(`Total Size: ${stats.totalSizeFormatted}`);
313
+ console.groupEnd();
314
+ }
315
+
316
+ generateId() {
317
+ return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
318
+ }
319
+
320
+ _delay(ms) {
321
+ return new Promise(resolve => setTimeout(resolve, ms));
322
+ }
323
+
324
+ checkAllComplete() {
325
+ const allDone = Array.from(this.uploads.values()).every(u =>
326
+ ['completed', 'failed', 'cancelled'].includes(u.status)
327
+ );
328
+
329
+ if (allDone && this.uploads.size > 0) {
330
+ this.notifyAllComplete();
331
+ }
332
+ }
333
+
334
+ // === КОЛЛБЭКИ ===
335
+
336
+ onProgress(callback) { this.callbacks.progress.push(callback); }
337
+ onComplete(callback) { this.callbacks.complete.push(callback); }
338
+ onError(callback) { this.callbacks.error.push(callback); }
339
+ onAllComplete(callback) { this.callbacks.allComplete.push(callback); }
340
+
341
+ notifyProgress(uploadData) {
342
+ this.callbacks.progress.forEach(cb => this._safeCall(cb, { ...uploadData, stats: this.getStats() }));
343
+ }
344
+
345
+ notifyComplete(uploadData) {
346
+ this.callbacks.complete.forEach(cb => this._safeCall(cb, uploadData));
347
+ }
348
+
349
+ notifyError(uploadData, error) {
350
+ this.callbacks.error.forEach(cb => this._safeCall(cb, uploadData, error));
351
+ }
352
+
353
+ notifyAllComplete() {
354
+ this.callbacks.allComplete.forEach(cb => this._safeCall(cb, { completed: this.completed, stats: this.getStats() }));
355
+ }
356
+
357
+ isIdle() {
358
+ const hasActiveUploads = this.activeCount > 0;
359
+ const hasPendingInQueue = this.queue.length > 0;
360
+ const hasWaitingPromises = this.waitingPromises.length > 0;
361
+ const hasRunningUploads = Array.from(this.uploads.values()).some(u =>
362
+ ['uploading', 'retrying', 'pending'].includes(u.status)
363
+ );
364
+
365
+ return !hasActiveUploads && !hasPendingInQueue && !hasWaitingPromises && !hasRunningUploads;
366
+ }
367
+
368
+ /**
369
+ * Отписывает все обработчики событий
370
+ * Предотвращает утечки памяти при повторной инициализации
371
+ */
372
+ offAll() {
373
+ this.callbacks.progress = [];
374
+ this.callbacks.complete = [];
375
+ this.callbacks.error = [];
376
+ this.callbacks.allComplete = [];
377
+ return this;
378
+ }
379
+
380
+ _safeCall(callback, ...args) {
381
+ try {
382
+ callback(...args);
383
+ } catch (error) {
384
+ console.error('Callback error:', error);
385
+ }
386
+ }
387
+ }
388
+
389
+ export default FileUploader;
@@ -0,0 +1,83 @@
1
+ import { isElement, normalizeData } from "../../../utils/js/functions";
2
+ import Params from "../../../utils/js/components/params";
3
+ import Selectors from "../../../utils/js/dom/selectors";
4
+ import { Classes, Manipulator } from "../../../utils/js/dom/manipulator";
5
+
6
+ class VGFilesTemplateRender {
7
+ constructor(vgFilesInstance, element, params = {}) {
8
+ this.module = vgFilesInstance;
9
+ this.element = isElement(element);
10
+
11
+ if (!this.element) {
12
+ console.error('Invalid element provided:', element);
13
+ return;
14
+ }
15
+
16
+ this._params = new Params(params, element).get();
17
+ this._nodes = {
18
+ info: this.module._nodes.info,
19
+ drop: this.module._nodes.drop
20
+ };
21
+
22
+ this.bufferTemplate = '';
23
+ this.parsedFiles = [];
24
+ }
25
+
26
+ init() {
27
+ const $targetNode = this._nodes.info || this._nodes.drop;
28
+ if (!$targetNode) return false;
29
+
30
+ const area = this._nodes.info ? 'info' : 'drop';
31
+ return this._nativeRenderFiles(area, $targetNode);
32
+ }
33
+
34
+ _nativeRenderFiles(area, $node) {
35
+ const $list = Selectors.find(`.vg-files-${area}--list`, $node);
36
+ if (!$list) return false;
37
+
38
+ const $items = Array.from($list.children).filter(li => li.tagName === 'LI');
39
+ if ($items.length === 0) return false;
40
+
41
+ // Сохраняем шаблон только один раз
42
+ this._setTemplateInBuffer($items);
43
+
44
+ if (!this.bufferTemplate) return false;
45
+
46
+ // Парсим данные файлов
47
+ this.parsedFiles = $items
48
+ .map(li => {
49
+ const rawData = Manipulator.get(li, 'data-file');
50
+ if (!rawData) return null;
51
+
52
+ const dataFile = normalizeData(rawData);
53
+ const requiredKeys = ['id', 'name', 'size', 'type', 'src'];
54
+ const isValid = requiredKeys.every(key => dataFile.hasOwnProperty(key));
55
+
56
+ return isValid ? dataFile : null;
57
+ })
58
+ .filter(Boolean); // Убираем null
59
+
60
+ return true;
61
+ }
62
+
63
+ _setTemplateInBuffer($items) {
64
+ if (this.bufferTemplate || $items.length === 0) return;
65
+
66
+ const firstItem = $items[0];
67
+
68
+ // Если нет data-file — шаблон извлекается и элемент удаляется
69
+ if (!Manipulator.has(firstItem, 'data-file')) {
70
+ this.bufferTemplate = firstItem.outerHTML;
71
+ firstItem.remove();
72
+ } else {
73
+ this.bufferTemplate = firstItem.outerHTML;
74
+ }
75
+ }
76
+
77
+ dispose() {
78
+ this.bufferTemplate = '';
79
+ this.parsedFiles = [];
80
+ }
81
+ }
82
+
83
+ export default VGFilesTemplateRender;
@@ -0,0 +1,155 @@
1
+ import Selectors from "../../../utils/js/dom/selectors";
2
+ import {isElement, normalizeData} from "../../../utils/js/functions";
3
+ import Ajax from "../../../utils/js/components/ajax";
4
+ import VGToast from "../../vgtoast";
5
+
6
+ class VGFilesSortable {
7
+ constructor(vgFilesInstance, options = {}) {
8
+ this._vg = vgFilesInstance;
9
+ this._params = {
10
+ handle: '.file',
11
+ itemSelector: 'li.file',
12
+ route: null,
13
+ method: 'POST',
14
+ toast: true,
15
+ ...options
16
+ };
17
+
18
+ if (!options.itemSelector && options.handle) {
19
+ this._params.itemSelector = `li${options.handle}`;
20
+ }
21
+
22
+ if (!this._params.route) {
23
+ console.warn('VGFilesSortable: Не указан маршрут `route` для сохранения порядка.');
24
+ }
25
+ if (this._params.lists?.length) {
26
+ this._list = this._params.lists.map(l => Selectors.find('.' + l, this._vg._element)).find(s => s);
27
+ } else {
28
+ this._list = this._vg._nodes.list;
29
+ }
30
+
31
+ if (!isElement(this._list)) {
32
+ console.error('VGFilesSortable: Не найден контейнер списка файлов');
33
+ return;
34
+ }
35
+
36
+ this._draggedItem = null;
37
+
38
+ this._boundOnDragStart = this._onDragStart.bind(this);
39
+ this._boundOnDragEnd = this._onDragEnd.bind(this);
40
+ this._boundOnDragOver = this._onDragOver.bind(this);
41
+ this._boundOnDrop = this._onDrop.bind(this);
42
+
43
+ this._init();
44
+ }
45
+
46
+ _init() {
47
+ this._enableDraggableItems();
48
+
49
+ this._setupEvents();
50
+ this._vg._triggerCallback('onSortableInit');
51
+ }
52
+
53
+ _enableDraggableItems() {
54
+ const items = this._list.querySelectorAll(this._params.itemSelector);
55
+
56
+ items.forEach(li => {
57
+ li.setAttribute('draggable', 'true');
58
+
59
+ const img = li.querySelector('img');
60
+ if (img) img.setAttribute('draggable', 'false');
61
+ });
62
+ }
63
+
64
+ _setupEvents() {
65
+ this._list.addEventListener('dragstart', this._boundOnDragStart);
66
+ this._list.addEventListener('dragend', this._boundOnDragEnd);
67
+ this._list.addEventListener('dragover', this._boundOnDragOver);
68
+ this._list.addEventListener('drop', this._boundOnDrop);
69
+ }
70
+
71
+ _onDragStart(e) {
72
+ const handleEl = e.target.closest(this._params.handle);
73
+ if (!handleEl) return;
74
+
75
+ const item = e.target.closest(this._params.itemSelector) || e.target.closest('li');
76
+ if (!item) return;
77
+
78
+ this._draggedItem = item;
79
+
80
+ e.dataTransfer.effectAllowed = 'move';
81
+ e.dataTransfer.setData('text/plain', 'vgsortable');
82
+
83
+ item.classList.add('dragging');
84
+ requestAnimationFrame(() => item.classList.add('dragging-transparent'));
85
+ }
86
+
87
+ _onDragEnd(e) {
88
+ if (this._draggedItem) {
89
+ this._draggedItem.classList.remove('dragging', 'dragging-transparent');
90
+ this._draggedItem = null;
91
+ this._saveOrder();
92
+ }
93
+ }
94
+
95
+ _onDragOver(e) {
96
+ if (!this._draggedItem) return;
97
+
98
+ e.preventDefault();
99
+ e.dataTransfer.dropEffect = 'move';
100
+
101
+ const currentTarget =
102
+ e.target.closest(this._params.itemSelector) ||
103
+ e.target.closest('li');
104
+
105
+ if (!currentTarget || currentTarget === this._draggedItem) return;
106
+
107
+ const rect = currentTarget.getBoundingClientRect();
108
+ const midpoint = rect.height / 2;
109
+ const offsetFromTop = e.clientY - rect.top;
110
+
111
+ if (offsetFromTop < midpoint) {
112
+ this._list.insertBefore(this._draggedItem, currentTarget);
113
+ } else {
114
+ this._list.insertBefore(this._draggedItem, currentTarget.nextSibling);
115
+ }
116
+ }
117
+
118
+ _onDrop(e) {
119
+ if (!this._draggedItem) return;
120
+
121
+ e.preventDefault();
122
+ e.stopPropagation();
123
+ return false;
124
+ }
125
+
126
+ _saveOrder() {
127
+ const ids = [... new Set(this._getUploadedIds())]
128
+
129
+ if (!ids.length || !this._params.route) return;
130
+
131
+ const xhr = new Ajax();
132
+ xhr.post(this._params.route, { ids }, {
133
+ onSuccess: (data) => VGToast.run(data.response.message),
134
+ });
135
+ }
136
+
137
+ _getUploadedIds() {
138
+ return Array.from(this._list.querySelectorAll('[data-id]'))
139
+ .map(el => {
140
+ const id = normalizeData(el.getAttribute('data-id'));
141
+ return id ? id : null;
142
+ }).filter(id => id !== null);
143
+ }
144
+
145
+ destroy() {
146
+ this._list.removeEventListener('dragstart', this._boundOnDragStart);
147
+ this._list.removeEventListener('dragend', this._boundOnDragEnd);
148
+ this._list.removeEventListener('dragover', this._boundOnDragOver);
149
+ this._list.removeEventListener('drop', this._boundOnDrop);
150
+
151
+ this._draggedItem = null;
152
+ }
153
+ }
154
+
155
+ export default VGFilesSortable