vgapp 0.7.9 → 0.8.1
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/CHANGELOG.md +9 -0
- package/app/langs/en/buttons.json +16 -0
- package/app/langs/en/messages.json +32 -0
- package/app/langs/en/titles.json +6 -0
- package/app/langs/ru/buttons.json +16 -0
- package/app/langs/ru/messages.json +32 -0
- package/app/langs/ru/titles.json +6 -0
- package/app/modules/base-module.js +12 -1
- package/app/modules/module-fn.js +20 -9
- package/app/modules/vgalert/js/vgalert.js +12 -6
- package/app/modules/vgalert/readme.md +1 -1
- package/app/modules/vgcollapse/readme.md +1 -1
- package/app/modules/vgdropdown/js/vgdropdown.js +140 -38
- package/app/modules/vgdropdown/readme.md +225 -0
- package/app/modules/vgfiles/js/base.js +499 -0
- package/app/modules/vgfiles/js/droppable.js +159 -0
- package/app/modules/vgfiles/js/loader.js +389 -0
- package/app/modules/vgfiles/js/render.js +83 -0
- package/app/modules/vgfiles/js/sortable.js +155 -0
- package/app/modules/vgfiles/js/vgfiles.js +796 -280
- package/app/modules/vgfiles/readme.md +193 -0
- package/app/modules/vgfiles/scss/_animations.scss +18 -0
- package/app/modules/vgfiles/scss/_mixins.scss +73 -0
- package/app/modules/vgfiles/scss/_variables.scss +103 -26
- package/app/modules/vgfiles/scss/vgfiles.scss +573 -60
- package/app/modules/vgformsender/js/vgformsender.js +5 -1
- package/app/modules/vgformsender/readme.md +1 -1
- package/app/modules/vglawcookie/js/vglawcookie.js +96 -62
- package/app/modules/vglawcookie/readme.md +102 -0
- package/app/modules/vgloadmore/js/vgloadmore.js +212 -112
- package/app/modules/vgloadmore/readme.md +145 -0
- package/app/modules/vgsidebar/js/vgsidebar.js +6 -4
- package/app/utils/js/components/ajax.js +172 -122
- package/app/utils/js/components/animation.js +124 -39
- package/app/utils/js/components/backdrop.js +54 -31
- package/app/utils/js/components/lang.js +69 -88
- package/app/utils/js/components/params.js +34 -31
- package/app/utils/js/components/scrollbar.js +118 -67
- package/app/utils/js/components/templater.js +14 -4
- package/app/utils/js/dom/cookie.js +107 -64
- package/app/utils/js/dom/data.js +68 -20
- package/app/utils/js/dom/event.js +272 -239
- package/app/utils/js/dom/manipulator.js +135 -62
- package/app/utils/js/dom/selectors.js +134 -59
- package/app/utils/js/functions.js +183 -349
- package/build/vgapp.css +1 -1
- package/build/vgapp.css.map +1 -1
- package/package.json +1 -1
- 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
|