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
@@ -1,289 +1,805 @@
1
- import BaseModule from "../../base-module";
2
- import {isDisabled, isVisible, mergeDeepObject} from "../../../utils/js/functions";
1
+ import VGFilesBase from "./base";
2
+ import FileUploader from "./loader";
3
+ import VGFilesDroppable from "./droppable";
4
+ import {getSVG} from "../../module-fn";
3
5
  import EventHandler from "../../../utils/js/dom/event";
4
6
  import Selectors from "../../../utils/js/dom/selectors";
7
+ import {isElement, normalizeData} from "../../../utils/js/functions";
8
+ import {Classes, Manipulator} from "../../../utils/js/dom/manipulator";
9
+ import {lang_buttons, lang_messages} from "../../../utils/js/components/lang";
10
+ import VGAlert from "../../vgalert";
11
+ import VGToast from "../../vgtoast";
12
+ import VGFilesTemplateRender from "./render";
5
13
 
6
- /**
7
- * Constants
8
- */
9
14
  const NAME = 'files';
10
15
  const NAME_KEY = 'vg.files';
11
- const SELECTOR_DATA_TOGGLE= '[data-vg-toggle="files"]';
16
+
17
+ const SELECTOR_DATA_TOGGLE = '[data-vg-toggle="files"]';
18
+ const SELECTOR_DATA_RELOAD = '[data-vg-reload="file"]';
19
+ const SELECTOR_DATA_DISMISS = '[data-vg-dismiss="file"]';
20
+ const SELECTOR_DATA_DISMISS_ALL = '[data-vg-dismiss="vg-files"]';
12
21
 
13
22
  const CLASS_NAME_CONTAINER = 'vg-files';
14
- const CLASS_NAME_INFO = CLASS_NAME_CONTAINER + '-info';
15
- const CLASS_NAME_IMAGES = CLASS_NAME_CONTAINER + '-info--images';
16
- const CLASS_NAME_LIST = CLASS_NAME_CONTAINER + '-info--list';
17
- const CLASS_NAME_FAKE = CLASS_NAME_CONTAINER + '-info-fake';
18
-
19
- const EVENT_KEY_CHANGE = `${NAME_KEY}.change`;
20
- const EVENT_KEY_LOADED = `${NAME_KEY}.loaded`;
21
-
22
- const EVENT_KEY_DOM_LOADED_DATA_API = `DOMContentLoaded.${NAME_KEY}.data.api`;
23
-
24
- class VGFiles extends BaseModule {
25
- constructor(element, params = {}) {
26
- super(element, params);
27
-
28
- this._params = this._getParams(this._element, mergeDeepObject({
29
- limits: {
30
- count: 0,
31
- sizes: 0
32
- },
33
- image: false,
34
- info: true,
35
- types: ['image/png', "image/jpeg", "image/bmp", "image/ico", "image/gif", "image/jfif", "image/tiff", "image/webp"],
36
- ajax: {
37
- route: '',
38
- target: '',
39
- method: 'get',
40
- loader: false,
41
- once: false,
42
- output: true,
43
- }
44
- }, params));
45
- this._files = [];
46
- this.id = this._element.querySelector('[data-vg-toggle]').getAttribute('id') || undefined;
47
- this.name = this._element.querySelector('[data-vg-toggle]').getAttribute('name') || undefined;
48
- this.accept = this._element.querySelector('[data-vg-toggle]').getAttribute('accept') || undefined;
49
-
50
- this._addEventListener();
51
- }
52
-
53
- static get NAME() {
54
- return NAME;
55
- }
56
-
57
- static get NAME_KEY() {
58
- return NAME_KEY
59
- }
60
-
61
- change(input) {
62
- let values = input.files,
63
- appended_files = [];
64
-
65
- this.clear();
66
-
67
- if (values.length) {
68
- if (this._params.limits.count !== 1) {
69
- input.removeAttribute('id');
70
- input.removeAttribute('data-vg-toggle');
71
- input.classList.add(CLASS_NAME_FAKE);
72
- input.onchange = null;
73
-
74
- let accept = this.accept ? 'accept="' + this.accept + '"' : '';
75
-
76
- this._element.insertAdjacentHTML('beforeEnd', '<input type="file" name="'+ this.name +'" id="'+ this.id +'" data-vg-toggle="files" ' + accept + ' multiple>');
77
- this._addEventListener();
78
- }
79
-
80
- appended_files = this.append(values);
81
-
82
- if (appended_files.length) {
83
- let $fileInfo = Selectors.find('.' + CLASS_NAME_INFO, this._element);
84
- $fileInfo.classList.add('show');
85
-
86
- let $count = $fileInfo.querySelector('.' + CLASS_NAME_INFO + '--wrapper-count');
87
- if ($count) $count.innerHTML = appended_files.length + '<span>[' + this._getSizes(appended_files, true) + ']</span>';
88
-
89
- this.setImages(appended_files);
90
- this.setInfoList(appended_files);
91
- }
92
-
93
- EventHandler.trigger(this._element, EVENT_KEY_CHANGE);
94
-
95
- this._route((status, data) => {
96
- EventHandler.trigger(this._element, EVENT_KEY_LOADED, {stats: status, data: data});
97
- });
98
- }
99
- }
100
-
101
- append(values) {
102
- this._files.push(values);
103
- return pushFiles(this._files, this._params.limits.count);
104
-
105
- function pushFiles (files, limit) {
106
- let arr = [];
107
- for (let i = 0; i <= files.length - 1; i++) {
108
- let count = 1;
109
- for (const file of files[i]) {
110
- if (limit === 0) {
111
- arr.push(file);
112
- } else {
113
- if (count <= limit) {
114
- arr.unshift(file);
115
- }
116
- }
117
-
118
- count++;
119
- }
120
- }
121
-
122
- if (limit > 0 && arr.length > limit) {
123
- arr.splice(limit, arr.length - limit);
124
- }
125
-
126
- return arr;
127
- }
128
- }
129
-
130
- clear(all = false) {
131
- let $filesInfo = Selectors.find('.' + CLASS_NAME_INFO, this._element);
132
-
133
- if ($filesInfo) {
134
- if (this._params.image) {
135
- let $filesInfoImages = Selectors.find('.' + CLASS_NAME_IMAGES, $filesInfo);
136
-
137
- if ($filesInfoImages) {
138
- let $images = Selectors.findAll('span', $filesInfoImages);
139
- if ($images.length) {
140
- for (const $image of $images) {
141
- $image.parentNode.removeChild($image);
142
- }
143
- }
144
- }
145
- }
146
-
147
- if (this._params.info) {
148
- let $filesInfoList = Selectors.find('.' + CLASS_NAME_LIST, $filesInfo);
149
- if ($filesInfoList) {
150
- let $li = Selectors.findAll('li', $filesInfoList);
151
- if ($li.length) {
152
- for (const $item of $li) {
153
- $item.parentNode.removeChild($item);
154
- }
155
- }
156
- }
157
- }
158
- }
159
-
160
- if (all) {
161
- this._element.querySelector('[type="file"]').value = '';
162
-
163
- let fakeInputs = Selectors.findAll('.' + CLASS_NAME_FAKE, this._element);
164
- if (fakeInputs.length) {
165
- for (const fakeInput of fakeInputs) {
166
- fakeInput.remove();
167
- }
168
- }
169
-
170
- if ($filesInfo) {
171
- $filesInfo.classList.remove('show');
172
- }
173
-
174
- this._files = [];
175
- }
176
- }
177
-
178
- dispose() {
179
- super.dispose();
180
- }
181
-
182
- setImages(files) {
183
- if (this._params.image) {
184
- const $fileInfo = Selectors.find('.' + CLASS_NAME_INFO, this._element);
185
- if ($fileInfo) {
186
- let $selector = Selectors.find('.' + CLASS_NAME_IMAGES, this._element);
187
- if (!$selector) {
188
- $selector = document.createElement('div');
189
- $selector.classList.add(CLASS_NAME_IMAGES);
190
- $fileInfo.prepend($selector);
191
- }
192
-
193
- for (const file of files) {
194
- if (this._checkType(file.type)) {
195
- let src = URL.createObjectURL(file);
196
- $selector.insertAdjacentHTML('beforeEnd', '<span><img src="'+ src +'" alt="#"></span>');
197
- }
198
- }
199
- }
200
- }
201
- }
202
-
203
- setInfoList(files) {
204
- if (this._params.info) {
205
- const $fileInfo = Selectors.find('.' + CLASS_NAME_INFO, this._element);
206
- if ($fileInfo) {
207
- let $list = Selectors.find('.' + CLASS_NAME_LIST, this._element);
208
- if (!$list) {
209
- $list = document.createElement('ul');
210
- $list.classList.add(CLASS_NAME_LIST);
211
- $fileInfo.append($list);
212
- }
213
-
214
- let i = 1;
215
- for (const file of files) {
216
- let size = this._getSizes(file.size);
217
- $list.insertAdjacentHTML('beforeEnd', '<li><span>'+ (i) + '.</span><span>' + file.name + '</span><span>['+ size +']</span></li>');
218
- i++;
219
- }
220
- }
221
- }
222
- }
223
-
224
- _getSizes(size, array = false) {
225
- let size_kb = size / 1024,
226
- size_mb = size_kb / 1024,
227
- size_gb = size_mb / 1024,
228
- size_tb = size_gb / 1024;
229
-
230
- let output = 0;
231
-
232
- if (size_kb <= 1024) {
233
- output = size_kb.toFixed(3) + ' Kb';
234
- } else if (size_kb >= 1024 && size_mb <= 1024) {
235
- output = size_mb.toFixed(3) + ' Mb';
236
- } else if (size_mb >= 1024 && size_gb <= 1024) {
237
- output = size_gb.toFixed(3) + ' Gb';
238
- } else {
239
- output = size_tb.toFixed(3) + ' Tb';
240
- }
241
-
242
- if (array) {
243
- let arrSizes = [];
244
- size.map(function (el) {
245
- arrSizes.push(el.size);
246
- })
247
-
248
- output = arrSizes.reduce( function (a, b) {
249
- return a + b
250
- });
251
-
252
- output = this._getSizes(output);
253
- }
254
-
255
- return output;
256
- }
257
-
258
- _checkType(type) {
259
- return this._params.types.includes(type);
260
- }
261
-
262
- _addEventListener() {
263
- const _this = this;
264
-
265
- [... Selectors.findAll(SELECTOR_DATA_TOGGLE, _this._element)].forEach(el => {
266
- el.addEventListener('change', function () {
267
- _this.change(this)
268
- })
269
- });
270
-
271
- let $dismiss = Selectors.find('[data-dismiss="vg-files"]', _this._element);
272
- $dismiss.onclick = function () {
273
- _this.clear(true);
274
-
275
- return false;
276
- }
277
- }
23
+ const CLASS_NAME_STAT = `${CLASS_NAME_CONTAINER}-stat`;
24
+ const CLASS_NAME_COMPLETED = 'completed';
25
+ const CLASS_NAME_PENDING = 'pending';
26
+ const CLASS_NAME_LOADING = 'loading';
27
+ const CLASS_NAME_FAILING = 'failing';
28
+ const CLASS_NAME_LOADED = 'loaded';
29
+
30
+ class VGFiles extends VGFilesBase {
31
+ constructor(element, params = {}) {
32
+ const defaults = {
33
+ init: true,
34
+ allowed: false,
35
+ lang: document.documentElement.lang || 'ru',
36
+ limits: { count: 0, sizes: 10, total: 0 },
37
+ image: false,
38
+ detach: true,
39
+ info: true,
40
+ types: [],
41
+ ajax: false,
42
+ prepend: true,
43
+ uploads: {
44
+ mode: 'sequential',
45
+ route: '',
46
+ maxParallel: 3,
47
+ maxConcurrent: 1,
48
+ retryAttempts: 1,
49
+ retryDelay: 1000,
50
+ },
51
+ removes: {
52
+ all: { route: '', alert: true, toast: true },
53
+ single: { route: '', alert: true, toast: true }
54
+ },
55
+ sortable: {
56
+ enabled: false,
57
+ route: '',
58
+ handle: '.file',
59
+ lists: [`vg-files-info--list`, `vg-files-drop--list`]
60
+ },
61
+ callbacks: {
62
+ onInit: null,
63
+ onChange: null,
64
+ onUploadStart: null,
65
+ onUploadProgress: null,
66
+ onUploadComplete: null,
67
+ onUploadError: null,
68
+ onUploadAllComplete: null,
69
+ onRemoveFile: null,
70
+ onClear: null,
71
+ onReload: null
72
+ }
73
+ };
74
+
75
+ super(element, params, defaults);
76
+
77
+ this._files = [];
78
+ this._uploadedKeys = new Set();
79
+ this._failingUploadedKeys = new Set();
80
+ this._pendingUploadedKeys = new Set();
81
+ this._unUploadedFiles = [];
82
+ this._uploader = null;
83
+ this._sortable = null;
84
+ this._render = null;
85
+
86
+ this.isRenderNonInit = false;
87
+ this._initExtended();
88
+ }
89
+
90
+ static get NAME() { return NAME; }
91
+ static get NAME_KEY() { return NAME_KEY; }
92
+
93
+ _initExtended() {
94
+ if (!this._isInitialized) return;
95
+
96
+ this._render = new VGFilesTemplateRender(this, this._element, this._params);
97
+
98
+ if (this._params.ajax) {
99
+ this._params.allowed = false;
100
+
101
+ if (this.isRenderNonInit) return;
102
+ this.isRenderNonInit = this._render.init();
103
+ if (this._render.parsedFiles.length) {
104
+ this._addExternalFiles(this._render.parsedFiles);
105
+ }
106
+ }
107
+ if (this._params.allowed && !this._params.ajax) this._params.detach = false;
108
+ if (this._nodes.drop) {
109
+ this._params.image = true;
110
+ this._params.detach = true;
111
+
112
+ VGFilesDroppable.getOrCreateInstance(this._nodes.drop, this._params).init();
113
+ }
114
+
115
+ this._addEventListenerExtended();
116
+ this._renderStat();
117
+ this._triggerCallback('onInit', { element: this._element });
118
+ }
119
+
120
+ _addExternalFiles(files) {
121
+ if (!Array.isArray(files) || !files.length) return;
122
+
123
+ files.forEach(fileData => {
124
+ const file = new File([""], fileData.name, {
125
+ type: fileData.type || "application/octet-stream",
126
+ lastModified: fileData.lastModified || Date.now()
127
+ });
128
+
129
+ // Добавляем ID, если он есть
130
+ Object.defineProperty(file, 'id', {
131
+ value: fileData.id,
132
+ writable: true,
133
+ enumerable: true
134
+ });
135
+
136
+ // Добавляем Size, если он есть
137
+ Object.defineProperty(file, 'size', {
138
+ value: fileData.size,
139
+ writable: true,
140
+ enumerable: true
141
+ });
142
+
143
+ // Добавляем Src, если он есть
144
+ Object.defineProperty(file, 'src', {
145
+ value: fileData.src,
146
+ writable: true,
147
+ enumerable: true
148
+ });
149
+
150
+ const fileKey = this._getFileKey(file);
151
+
152
+ // Помечаем как уже загруженные
153
+ this._uploadedKeys.add(fileKey);
154
+ this._pendingUploadedKeys.delete(fileKey);
155
+ this._failingUploadedKeys.delete(fileKey);
156
+
157
+ // Добавляем в общий список
158
+ this._files.push(file);
159
+ });
160
+
161
+ // Перестраиваем интерфейс
162
+ this._renderUI(this._files);
163
+
164
+ // Обновляем статистику
165
+ this._renderStat();
166
+ this._updateStat();
167
+ this._setStatItem('completed', this._uploadedKeys.size);
168
+
169
+ // Если нужно — запустить sortable
170
+ if (this._params.sortable?.enabled && this._params.sortable.route && !this._sortable) {
171
+ import('./sortable.js').then(module => {
172
+ this._sortable = new module.default(this, this._params.sortable);
173
+ }).catch(err => {
174
+ console.error('Ошибка загрузки VGFilesSortable:', err);
175
+ });
176
+ }
177
+
178
+ // Триггерим изменение
179
+ this._triggerCallback('onChange', { files: this._files });
180
+ EventHandler.trigger(this._element, `${NAME_KEY}.change`, { files: this._files });
181
+ }
182
+
183
+ _addEventListenerExtended() {
184
+ Selectors.findAll(SELECTOR_DATA_TOGGLE, this._element).forEach(el => {
185
+ el.removeEventListener('change', this.change.bind(this));
186
+ el.addEventListener('change', e => this._handleChange(e));
187
+ });
188
+ }
189
+
190
+ _handleChange(e) {
191
+ if (this._params.ajax) this.uploadAll(this._files);
192
+ this._triggerCallback('onChange', { files: this._files, input: e?.target || e?.src || '' });
193
+ EventHandler.trigger(this._element, `${NAME_KEY}.change`, { files: this._files });
194
+ }
195
+
196
+ async uploadAll(files) {
197
+ if (!this._params.ajax || !this._params.uploads.route) return;
198
+
199
+ if (!this._uploadedKeys.size) {
200
+ this._failingUploadedKeys.clear();
201
+ }
202
+
203
+ const notUploadedFiles = files.filter(f => !this._uploadedKeys.has(this._getFileKey(f)));
204
+ if (!notUploadedFiles.length) return;
205
+
206
+ if (!this._uploader || this._uploader.isIdle()) {
207
+ this._uploader = new FileUploader({
208
+ mode: this._params.uploads.mode,
209
+ maxConcurrent: this._params.uploads.maxConcurrent,
210
+ maxParallel: this._params.uploads.maxParallel,
211
+ retryAttempts: this._params.uploads.retryAttempts,
212
+ retryDelay: this._params.uploads.retryDelay
213
+ });
214
+
215
+ this._setupUploadEventHandlers();
216
+ }
217
+
218
+ notUploadedFiles.forEach(file => {
219
+ const fileKey = this._getFileKey(file);
220
+ this._pendingUploadedKeys.add(fileKey);
221
+ this._unUploadedFiles = this._unUploadedFiles.filter(f => this._getFileKey(f) !== fileKey);
222
+ });
223
+
224
+ this._setStatItem('pending', this._pendingUploadedKeys.size);
225
+
226
+ const uploadParams = {
227
+ additionalData: {
228
+ timestamp: new Date().toISOString(),
229
+ source: 'web_uploader',
230
+ prepend: this._params.prepend
231
+ }
232
+ };
233
+
234
+ this._triggerCallback('onUploadStart', {
235
+ files: notUploadedFiles,
236
+ total: notUploadedFiles.length
237
+ });
238
+
239
+ try {
240
+ await this._uploader.uploadFiles(notUploadedFiles, this._params.uploads.route, uploadParams);
241
+ } catch (error) {
242
+ console.error('Bulk upload failed:', error);
243
+ }
244
+ }
245
+
246
+ async upload(file) {
247
+ if (!this._params.ajax || !this._params.uploads.route) return;
248
+
249
+ if (!this._uploader || this._uploader.isIdle()) {
250
+ this._uploader = new FileUploader({
251
+ mode: this._params.uploads.mode,
252
+ maxConcurrent: this._params.uploads.maxConcurrent,
253
+ maxParallel: this._params.uploads.maxParallel,
254
+ retryAttempts: this._params.uploads.retryAttempts,
255
+ retryDelay: this._params.uploads.retryDelay
256
+ });
257
+ }
258
+
259
+ const fileKey = this._getFileKey(file);
260
+
261
+ // Убедимся, что файл в списке ожидания
262
+ if (!this._pendingUploadedKeys.has(fileKey)) {
263
+ this._pendingUploadedKeys.add(fileKey);
264
+ this._setStatItem('pending', this._pendingUploadedKeys.size);
265
+ }
266
+
267
+ // Убираем из unUploadedFiles, если был там
268
+ this._unUploadedFiles = this._unUploadedFiles.filter(f => this._getFileKey(f) !== fileKey);
269
+
270
+ // Обновляем UI: устанавливаем состояние "loading"
271
+ const $item = this._getItemElement(file);
272
+ if ($item) {
273
+ Classes.remove($item, CLASS_NAME_FAILING);
274
+ Classes.add($item, CLASS_NAME_LOADING);
275
+ Classes.add($item, CLASS_NAME_PENDING);
276
+ }
277
+
278
+ // Настройка событий загрузки
279
+ this._setupUploadEventHandlers(file);
280
+
281
+ const uploadParams = {
282
+ additionalData: {
283
+ timestamp: new Date().toISOString(),
284
+ source: 'web_uploader'
285
+ }
286
+ };
287
+
288
+ try {
289
+ await this._uploader.uploadFiles([file], this._params.uploads.route, uploadParams);
290
+ } catch (error) {
291
+ console.error('Upload failed:', error);
292
+ // Ошибка будет обработана через onError callback
293
+ }
294
+ }
295
+
296
+ _setupUploadEventHandlers() {
297
+ if (typeof this._uploader.offAll === 'function') {
298
+ this._uploader.offAll();
299
+ } else {
300
+ this._uploader.off('progress');
301
+ this._uploader.off('complete');
302
+ this._uploader.off('error');
303
+ this._uploader.off('allComplete');
304
+ }
305
+
306
+ this._updateStat(true)
307
+
308
+ this._uploader.onProgress((uploadData) => {
309
+ const file = uploadData.file;
310
+ const $item = this._getItemElement(file);
311
+ if (!$item) return;
312
+
313
+ Classes.add($item, CLASS_NAME_LOADING);
314
+ Classes.add($item, CLASS_NAME_PENDING);
315
+
316
+ const button = this._getButtonElement(file);
317
+ if (isElement(button)) {
318
+ const li = button.closest('li');
319
+ if (li) {
320
+ const fileRemove = Selectors.find('.file-remove', li);
321
+ if (fileRemove) {
322
+ fileRemove.innerHTML = '';
323
+ fileRemove.appendChild(this._setButtonElement(file, true));
324
+ }
325
+ }
326
+ }
327
+
328
+ this._triggerCallback('onUploadProgress', {
329
+ file: uploadData.file,
330
+ progress: uploadData.progress,
331
+ bytesSent: uploadData.bytesSent,
332
+ totalBytes: uploadData.totalBytes
333
+ });
334
+ });
335
+
336
+ this._uploader.onComplete((uploadData) => {
337
+ const file = uploadData.file;
338
+ const fileKey = this._getFileKey(file);
339
+ const $item = this._getItemElement(file);
340
+
341
+ if (!$item) return;
342
+
343
+ Classes.replace($item, CLASS_NAME_LOADING, CLASS_NAME_LOADED);
344
+ Classes.remove($item, CLASS_NAME_PENDING);
345
+
346
+ const button = this._getButtonElement(file);
347
+ const id = normalizeData(uploadData.result?.response?.id) || uploadData.id || file.lastModified;
348
+ file.id = id;
349
+
350
+ if (isElement(button) && id) {
351
+ const li = button.closest('li');
352
+ if (li) {
353
+ Manipulator.set(li, 'data-id', id);
354
+ this._setButtonElement(file);
355
+
356
+ const fileRemove = Selectors.find('.file-remove', li);
357
+ if (fileRemove) {
358
+ fileRemove.innerHTML = '';
359
+ fileRemove.appendChild(this._setButtonElement(file, true, 'completed'));
360
+ Classes.add(li, CLASS_NAME_COMPLETED);
361
+
362
+ setTimeout(() => {
363
+ fileRemove.innerHTML = '';
364
+ fileRemove.appendChild(this._setButtonElement(file));
365
+ Classes.remove(li, CLASS_NAME_COMPLETED);
366
+ }, 1000);
367
+ }
368
+ }
369
+ }
370
+
371
+ this._uploadedKeys.add(fileKey);
372
+ this._failingUploadedKeys.delete(fileKey);
373
+ this._pendingUploadedKeys.delete(fileKey);
374
+
375
+ this._setStatItem('completed', this._uploadedKeys.size);
376
+ this._setStatItem('pending', this._pendingUploadedKeys.size);
377
+
378
+ this._triggerCallback('onUploadComplete', {
379
+ file: uploadData.file,
380
+ response: uploadData.result?.response,
381
+ status: uploadData.result?.status,
382
+ id: file.id
383
+ });
384
+ });
385
+
386
+ this._uploader.onError((uploadData) => {
387
+ const file = uploadData.file;
388
+ const fileKey = this._getFileKey(file);
389
+ const $item = this._getItemElement(file);
390
+
391
+ if (!$item) return;
392
+
393
+ this._uploadedKeys.delete(fileKey);
394
+ this._failingUploadedKeys.add(fileKey);
395
+ this._pendingUploadedKeys.delete(fileKey);
396
+ this._unUploadedFiles.push(file);
397
+
398
+ Classes.remove($item, CLASS_NAME_PENDING);
399
+ Classes.replace($item, CLASS_NAME_LOADING, CLASS_NAME_FAILING);
400
+
401
+ this._setStatItem('failing', this._failingUploadedKeys.size);
402
+ this._setStatItem('pending', this._pendingUploadedKeys.size);
403
+
404
+ const button = this._getButtonElement(file);
405
+ if (isElement(button)) {
406
+ const li = button.closest('li');
407
+ if (li) {
408
+ this._setButtonElement(file);
409
+
410
+ const fileRemove = Selectors.find('.file-remove', li);
411
+ if (fileRemove) {
412
+ fileRemove.innerHTML = '';
413
+ fileRemove.appendChild(this._setButtonElement(file, true, 'failing'));
414
+ Classes.add(li, CLASS_NAME_FAILING);
415
+ }
416
+ }
417
+ }
418
+
419
+ this._triggerCallback('onUploadError', {
420
+ file: uploadData.file
421
+ });
422
+ });
423
+
424
+ this._uploader.onAllComplete(() => {
425
+ EventHandler.trigger(this._element, `${NAME_KEY}.upload.allComplete`);
426
+ this._updateStat(false);
427
+
428
+ if (!this._failingUploadedKeys.size) {
429
+ if (this._params.sortable?.enabled && this._params.sortable.route) {
430
+ import('./sortable.js').then(module => {
431
+ if (this._sortable && typeof this._sortable.destroy === 'function') {
432
+ this._sortable.destroy();
433
+ }
434
+
435
+ this._sortable = new module.default(this, this._params.sortable);
436
+ }).catch(err => {
437
+ console.error('Ошибка загрузки VGFilesSortable:', err);
438
+ });
439
+ }
440
+ }
441
+
442
+ this._triggerCallback('onUploadAllComplete', {
443
+ uploaded: this._uploadedKeys.size,
444
+ failed: this._failingUploadedKeys.size,
445
+ total: this._files.length
446
+ });
447
+ });
448
+ }
449
+
450
+ reload(button) {
451
+ if (!this._params.ajax || !this._params.uploads.route) return;
452
+
453
+ const dataButton = Manipulator.get(button, 'data');
454
+ const fileData = {
455
+ name: dataButton.name,
456
+ size: dataButton.size,
457
+ type: dataButton.type,
458
+ lastModified: dataButton['last-modified']
459
+ };
460
+
461
+ const fileKey = this._getFileKey(fileData);
462
+ if (!this._failingUploadedKeys.has(fileKey)) return;
463
+
464
+ const fileToRetry = this._unUploadedFiles.find(f => this._getFileKey(f) === fileKey);
465
+ if (!fileToRetry) return;
466
+
467
+ this._failingUploadedKeys.delete(fileKey);
468
+ this._pendingUploadedKeys.add(fileKey);
469
+
470
+ this._setStatItem('failing', this._failingUploadedKeys.size);
471
+ this._setStatItem('pending', this._pendingUploadedKeys.size);
472
+
473
+ this._triggerCallback('onReload', {
474
+ button: button,
475
+ file: fileToRetry
476
+ });
477
+
478
+ this.upload(fileToRetry);
479
+ }
480
+
481
+ removeFile(button) {
482
+ const name = normalizeData(Manipulator.get(button, 'data-name'));
483
+ const size = normalizeData(Manipulator.get(button, 'data-size'));
484
+ const id = normalizeData(Manipulator.get(button, 'data-id'));
485
+
486
+ const fileToRemove = this._files.find(f => f.name === name && f.size === size);
487
+ if (fileToRemove) {
488
+ const key = this._getFileKey(fileToRemove);
489
+ this._uploadedKeys.delete(key);
490
+ this._pendingUploadedKeys.delete(key);
491
+ this._failingUploadedKeys.delete(key);
492
+ }
493
+
494
+ this._getItemElement().forEach(el => {
495
+ const btn = Selectors.find('button', el);
496
+ if (!btn) return;
497
+ const btnId = normalizeData(Manipulator.get(btn, 'data-id'));
498
+ const btnName = normalizeData(Manipulator.get(btn, 'data-name'));
499
+ const btnSize = normalizeData(Manipulator.get(btn, 'data-size'));
500
+
501
+ this._files.forEach(file => {
502
+ if (file.name === btnName && file.size === btnSize) {
503
+ file.id = btnId;
504
+ }
505
+ });
506
+
507
+ if (fileToRemove?.name === btnName && fileToRemove?.size === btnSize) {
508
+ fileToRemove.id = btnId;
509
+ }
510
+ });
511
+
512
+ if (this._params.ajax && this._params.removes.single.route) {
513
+ if (!id) return;
514
+
515
+ const route = this._params.removes.single.route + '/' + encodeURIComponent(id);
516
+ const paramsAjax = {
517
+ route: route,
518
+ method: 'delete'
519
+ };
520
+
521
+ const _completeRemoveFile = (data) => {
522
+ this._files = this._files.filter(f => !(f.name === name && f.size === size));
523
+ this._updateStatsAfterRemove();
524
+
525
+ if (this._files.length) {
526
+ this._renderUI(this._files);
527
+ } else {
528
+ this.clear(true, true);
529
+ }
530
+
531
+ if (this._params.removes.single.toast) {
532
+ VGToast.run(data?.response?.message);
533
+ }
534
+ };
535
+
536
+ if (this._params.removes.single.alert) {
537
+ VGAlert.confirm(button, {
538
+ lang: this._params.lang,
539
+ ajax: paramsAjax,
540
+ buttons: {
541
+ agree: {
542
+ text: lang_buttons(this._params.lang, NAME)['agree'],
543
+ class: ["btn-danger"],
544
+ },
545
+ cancel: {
546
+ text: lang_buttons(this._params.lang, NAME)['cancel'],
547
+ class: ["btn-outline-danger"],
548
+ },
549
+ },
550
+ message: {
551
+ title: lang_messages(this._params.lang, NAME)['title'],
552
+ description: lang_messages(this._params.lang, NAME)['description']
553
+ }
554
+ });
555
+
556
+ EventHandler.one(button, 'vg.alert.accept', (event) => {
557
+ _completeRemoveFile(event.vgalert.data);
558
+ });
559
+ } else {
560
+ this._params.ajax = paramsAjax;
561
+ this._route((status, data) => {
562
+ _completeRemoveFile(data);
563
+ });
564
+ }
565
+ } else {
566
+ this._files = this._files.filter(f => !(f.name === name && f.size === size));
567
+ this._updateStatsAfterRemove();
568
+ this._files.length ? this.build() : this.clear(true);
569
+ }
570
+
571
+ this._resetFileInput();
572
+
573
+ this._triggerCallback('onRemoveFile', {
574
+ button: button,
575
+ name: name,
576
+ size: size,
577
+ id: id,
578
+ remaining: this._files.length
579
+ });
580
+ }
581
+
582
+ _updateStatsAfterRemove() {
583
+ this._setStatItem('completed', this._uploadedKeys.size);
584
+ this._setStatItem('pending', this._pendingUploadedKeys.size);
585
+ this._setStatItem('failing', this._failingUploadedKeys.size);
586
+ this._updateStat();
587
+ }
588
+
589
+ _getItemElement(file = null) {
590
+ let className = `${this._getClass('info-list')}`;
591
+ if (this._nodes.drop) className = `${this._getClass('drop-list')}`;
592
+
593
+ if (!file) {
594
+ return Selectors.findAll(
595
+ `.${className} li.loaded`,
596
+ this._element
597
+ )
598
+ } else {
599
+ return Selectors.find(
600
+ `.${className} li[data-name="${file.name}"][data-size="${file.size}"]`,
601
+ this._element
602
+ );
603
+ }
604
+ }
605
+
606
+ _getButtonElement(file) {
607
+ return Selectors.find(`button[data-name="${file.name}"][data-size="${file.size}"]`, this._element);
608
+ }
609
+
610
+ _setButtonElement(file, isAjax = false, status = '') {
611
+ let icon = getSVG('trash'), action = 'data-vg-dismiss';
612
+ if (!this._params.info) icon = getSVG('cross');
613
+ if (isAjax) {
614
+ icon = getSVG('spinner');
615
+ if (status === 'completed') icon = getSVG('check');
616
+ else action = 'data-vg-reload';
617
+ }
618
+ if (this._failingUploadedKeys.has(this._getFileKey(file))) {
619
+ icon = getSVG('spinner');
620
+ action = 'data-vg-reload';
621
+ }
622
+
623
+ return this._tpl.button([
624
+ this._tpl.i({}, icon, { isHTML: true })
625
+ ], 'button', {
626
+ type: 'button',
627
+ [action]: 'file',
628
+ 'data-name': file.name,
629
+ 'data-size': file.size,
630
+ 'data-type': file.type,
631
+ 'data-last-modified': file.lastModified,
632
+ 'data-id': file.id || ''
633
+ });
634
+ }
635
+
636
+ _renderStat() {
637
+ if (!this._nodes.stat) return;
638
+ if (!this._params.ajax) return;
639
+
640
+ const $progress = Selectors.find(`.${CLASS_NAME_STAT}-progress`, this._nodes.stat);
641
+ if (!$progress) return;
642
+
643
+ let $statList = Selectors.find(`.${CLASS_NAME_STAT}-progress-list`, $progress);
644
+ if (!$statList) {
645
+ $statList = this._tpl.ul([
646
+ this._tpl.li({
647
+ class: 'stat-item pending',
648
+ 'data-count': '0',
649
+ 'title': lang_messages(this._params.lang, NAME)['pending']
650
+ }, [
651
+ this._tpl.span({ class: 'stat-label' }, getSVG('cloud-dot'), { isHTML: true }),
652
+ this._tpl.span({ class: 'stat-value' }, 0),
653
+ ]),
654
+ this._tpl.li({
655
+ class: 'stat-item completed',
656
+ 'data-count': '0',
657
+ 'title': lang_messages(this._params.lang, NAME)['completed']
658
+ }, [
659
+ this._tpl.span({ class: 'stat-label' }, getSVG('cloud-dot'), { isHTML: true }),
660
+ this._tpl.span({ class: 'stat-value' }, 0),
661
+ ]),
662
+ this._tpl.li({
663
+ class: 'stat-item failing',
664
+ 'data-count': '0',
665
+ 'title': lang_messages(this._params.lang, NAME)['failing']
666
+ }, [
667
+ this._tpl.span({ class: 'stat-label' }, getSVG('cloud-dot'), { isHTML: true }),
668
+ this._tpl.span({ class: 'stat-value' }, 0),
669
+ ])
670
+
671
+ ], { class: CLASS_NAME_STAT + '-progress-list' }, true);
672
+ $progress.appendChild($statList);
673
+ }
674
+ }
675
+
676
+ _setStatItem(status, count) {
677
+ if (!this._nodes.stat) return;
678
+
679
+ const $item = Selectors.find(`.${CLASS_NAME_STAT}-progress-list li.${status}`, this._nodes.stat);
680
+ if (!$item) return;
681
+
682
+ Manipulator.set($item, 'data-count', count);
683
+ const $value = Selectors.find('.stat-value', $item);
684
+ if ($value) $value.innerHTML = count;
685
+ }
686
+
687
+ _renderUIStatusDropInfoAjax(files) {
688
+ if (!this._params.ajax) return;
689
+
690
+ files.forEach(file => {
691
+ const $item = this._getItemElement(file);
692
+ if (!$item) return;
693
+
694
+ const key = this._getFileKey(file);
695
+ if (this._uploadedKeys.has(key)) {
696
+ Classes.replace($item, CLASS_NAME_PENDING, CLASS_NAME_COMPLETED);
697
+ Classes.add($item, CLASS_NAME_LOADED);
698
+ } else if (this._failingUploadedKeys.has(key)) {
699
+ Classes.replace($item, CLASS_NAME_PENDING, CLASS_NAME_FAILING);
700
+ } else {
701
+ Classes.add($item, CLASS_NAME_PENDING);
702
+ }
703
+ });
704
+ }
705
+
706
+ _triggerCallback(name, data) {
707
+ const cb = this._params.callbacks?.[name];
708
+ if (typeof cb === 'function') {
709
+ try { cb.call(this, data, this); } catch (e) { console.error(`${name} callback error:`, e); }
710
+ }
711
+ }
712
+
713
+ _getUploadedIds() {
714
+ return Array.from(this._uploadedKeys).map(key => {
715
+ const file = this._files.find(f => this._getFileKey(f) === key);
716
+ return file?.id || null;
717
+ }).filter(id => id !== null);
718
+ }
719
+
720
+ dispose() {
721
+ if (this._sortable && typeof this._sortable.destroy === 'function') {
722
+ this._sortable.destroy();
723
+ }
724
+ this._sortable = null;
725
+
726
+ this.clear();
727
+ if (this._uploader) this._uploader.destroy();
728
+ super.dispose();
729
+ }
278
730
  }
279
731
 
280
- /**
281
- * Data API implementation
282
- */
283
- EventHandler.on(document, EVENT_KEY_DOM_LOADED_DATA_API, function () {
284
- [... Selectors.findAll('.vg-files')].forEach(el => {
285
- VGFiles.getOrCreateInstance(el);
286
- })
287
- })
288
-
289
- export default VGFiles;
732
+ EventHandler.on(document, `DOMContentLoaded.${NAME_KEY}.data.api`, () => {
733
+ Selectors.findAll(`.${CLASS_NAME_CONTAINER}`).forEach(el => VGFiles.getOrCreateInstance(el));
734
+ });
735
+
736
+ EventHandler.on(document, `click.${NAME_KEY}.data.api`, SELECTOR_DATA_DISMISS, function (e) {
737
+ const target = e.target.closest(`.${CLASS_NAME_CONTAINER}`);
738
+ if (target) VGFiles.getOrCreateInstance(target).removeFile(e.target.closest(SELECTOR_DATA_DISMISS));
739
+ });
740
+
741
+ EventHandler.on(document, `click.${NAME_KEY}.data.api`, SELECTOR_DATA_RELOAD, function (e) {
742
+ const target = e.target.closest(`.${CLASS_NAME_CONTAINER}`);
743
+ if (target) VGFiles.getOrCreateInstance(target).reload(e.target.closest(SELECTOR_DATA_RELOAD));
744
+ });
745
+
746
+ EventHandler.on(document, `click.${NAME_KEY}.data.api`, SELECTOR_DATA_DISMISS_ALL, function (e) {
747
+ const target = e.target.closest(`.${CLASS_NAME_CONTAINER}`);
748
+ if (!target) return;
749
+
750
+ const instance = VGFiles.getOrCreateInstance(target);
751
+
752
+ if (instance._params.ajax && instance._params.removes.all.route) {
753
+ e.preventDefault();
754
+
755
+ const route = instance._params.removes.all.route;
756
+ const paramsAjax = {
757
+ route: route,
758
+ method: 'post',
759
+ data: { ids: instance._getUploadedIds() }
760
+ };
761
+
762
+ const _completeClearAll = () => {
763
+ instance.clear(true, true);
764
+ };
765
+
766
+ if (instance._params.removes.all.alert) {
767
+ VGAlert.confirm(e.target, {
768
+ lang: instance._params.lang,
769
+ ajax: paramsAjax,
770
+ buttons: {
771
+ agree: {
772
+ text: lang_buttons(instance._params.lang, NAME)['agree'],
773
+ class: ["btn-danger"],
774
+ },
775
+ cancel: {
776
+ text: lang_buttons(instance._params.lang, NAME)['cancel'],
777
+ class: ["btn-outline-danger"],
778
+ },
779
+ },
780
+ message: {
781
+ title: lang_messages(instance._params.lang, NAME)['title'],
782
+ description: lang_messages(instance._params.lang, NAME)['descriptions']
783
+ }
784
+ });
785
+
786
+ EventHandler.one(e.target, 'vg.alert.accept', (event) => {
787
+ if (instance._params.removes.all.toast) {
788
+ VGToast.run(event.vgalert.data?.response?.message);
789
+ }
790
+ _completeClearAll();
791
+ });
792
+ } else {
793
+ instance._route(paramsAjax, (status, data) => {
794
+ if (instance._params.removes.all.toast && data?.response?.message) {
795
+ VGToast.run(data.response.message);
796
+ }
797
+ _completeClearAll();
798
+ });
799
+ }
800
+ } else {
801
+ instance.clear(true, true);
802
+ }
803
+ });
804
+
805
+ export default VGFiles;