vgapp 1.1.6 → 1.1.7

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 (54) hide show
  1. package/CHANGELOG.md +10 -1
  2. package/README.md +48 -48
  3. package/app/langs/en/buttons.json +17 -17
  4. package/app/langs/en/messages.json +36 -36
  5. package/app/langs/ru/buttons.json +17 -17
  6. package/app/langs/ru/messages.json +36 -36
  7. package/app/modules/vgfilepreview/js/i18n.js +56 -56
  8. package/app/modules/vgfilepreview/js/renderers/image-modal.js +145 -145
  9. package/app/modules/vgfilepreview/js/renderers/image.js +92 -92
  10. package/app/modules/vgfilepreview/js/renderers/index.js +19 -19
  11. package/app/modules/vgfilepreview/js/renderers/office-modal.js +168 -168
  12. package/app/modules/vgfilepreview/js/renderers/office.js +79 -79
  13. package/app/modules/vgfilepreview/js/renderers/pdf-modal.js +260 -260
  14. package/app/modules/vgfilepreview/js/renderers/pdf.js +76 -76
  15. package/app/modules/vgfilepreview/js/renderers/playlist.js +71 -71
  16. package/app/modules/vgfilepreview/js/renderers/text-modal.js +343 -343
  17. package/app/modules/vgfilepreview/js/renderers/text.js +83 -83
  18. package/app/modules/vgfilepreview/js/renderers/video-modal.js +272 -272
  19. package/app/modules/vgfilepreview/js/renderers/video.js +80 -80
  20. package/app/modules/vgfilepreview/js/renderers/zip-modal.js +522 -522
  21. package/app/modules/vgfilepreview/js/renderers/zip.js +89 -89
  22. package/app/modules/vgfilepreview/js/vgfilepreview.js +7 -7
  23. package/app/modules/vgfilepreview/readme.md +68 -68
  24. package/app/modules/vgfilepreview/scss/_variables.scss +113 -113
  25. package/app/modules/vgfilepreview/scss/vgfilepreview.scss +464 -464
  26. package/app/modules/vgfiles/js/base.js +26 -26
  27. package/app/modules/vgfiles/js/droppable.js +260 -260
  28. package/app/modules/vgfiles/js/render.js +153 -153
  29. package/app/modules/vgfiles/js/vgfiles.js +41 -41
  30. package/app/modules/vgfiles/readme.md +123 -123
  31. package/app/modules/vgfiles/scss/_variables.scss +18 -18
  32. package/app/modules/vgfiles/scss/vgfiles.scss +148 -148
  33. package/app/modules/vgformsender/js/vgformsender.js +1 -1
  34. package/app/modules/vgmodal/js/vgmodal.drag.js +332 -332
  35. package/app/modules/vgmodal/js/vgmodal.js +33 -33
  36. package/app/modules/vgmodal/js/vgmodal.resize.js +435 -435
  37. package/app/modules/vgnav/js/vgnav.js +135 -135
  38. package/app/modules/vgnav/readme.md +67 -67
  39. package/app/modules/vgnestable/README.md +307 -307
  40. package/app/modules/vgnestable/scss/_variables.scss +60 -60
  41. package/app/modules/vgnestable/scss/vgnestable.scss +163 -163
  42. package/app/modules/vgselect/js/vgselect.js +39 -39
  43. package/app/modules/vgselect/scss/vgselect.scss +22 -22
  44. package/app/modules/vgspy/readme.md +28 -28
  45. package/app/utils/js/components/audio-metadata.js +240 -240
  46. package/app/utils/js/components/file-icon.js +109 -109
  47. package/app/utils/js/components/file-preview.js +304 -304
  48. package/app/utils/js/components/sanitize.js +150 -150
  49. package/app/utils/js/components/video-metadata.js +140 -140
  50. package/build/vgapp.css +1 -1
  51. package/build/vgapp.css.map +1 -1
  52. package/build/vgapp.js.map +1 -1
  53. package/index.scss +9 -9
  54. package/package.json +1 -1
@@ -1,154 +1,154 @@
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 { Manipulator } from "../../../utils/js/dom/manipulator";
5
-
6
- class VGFilesTemplateRender {
7
- constructor(vgFilesInstance, element, params = {}) {
8
- this.module = vgFilesInstance;
9
- this.element = isElement(element) ? element : null;
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
- this._setTemplateInBuffer($items);
42
- if (!this.bufferTemplate) return false;
43
-
44
- this.parsedFiles = $items
45
- .map(li => {
46
- const rawData = Manipulator.get(li, 'data-file');
47
- if (!rawData) return null;
48
-
49
- return this._parseDataFile(rawData);
50
- })
51
- .filter(Boolean);
52
-
53
- return true;
54
- }
55
-
56
- _parseDataFile(rawData) {
57
- const dataFile = normalizeData(rawData);
58
- if (!dataFile || typeof dataFile !== 'object' || Array.isArray(dataFile)) return null;
59
-
60
- const binaryMeta = this._extractBinaryMeta(dataFile);
61
- const name = this._toStringOrNull(dataFile.name);
62
- const id = this._toStringOrNull(dataFile.id);
63
- const src = this._toStringOrNull(dataFile.src);
64
- const type = this._toStringOrNull(dataFile.type) || binaryMeta.type || 'application/octet-stream';
65
- const size = this._toNumberOrNull(dataFile.size) ?? binaryMeta.size ?? 0;
66
- const lastModified = this._toNumberOrNull(dataFile.lastModified ?? dataFile['last-modified']) ?? binaryMeta.lastModified ?? Date.now();
67
-
68
- const result = {
69
- id,
70
- name,
71
- size,
72
- type,
73
- src,
74
- lastModified
75
- };
76
-
77
- if (dataFile.image !== undefined && dataFile.image !== null && dataFile.image !== '') {
78
- result.image = dataFile.image;
79
- }
80
-
81
- const requiredKeys = ['id', 'name', 'size', 'type', 'src'];
82
- const isValid = requiredKeys.every((key) => {
83
- const value = result[key];
84
- if (key === 'size') return Number.isFinite(value);
85
- return value !== undefined && value !== null && value !== '';
86
- });
87
-
88
- if (!isValid) return null;
89
-
90
- const customData = {};
91
- const reserved = new Set(['id', 'name', 'size', 'type', 'src', 'image', 'lastModified', 'last-modified']);
92
-
93
- Object.entries(dataFile).forEach(([key, value]) => {
94
- if (reserved.has(key)) return;
95
- if (value === undefined || value === null || value === '') return;
96
- customData[key] = value;
97
- });
98
-
99
- if (Object.keys(customData).length) {
100
- result.customData = customData;
101
- }
102
-
103
- return result;
104
- }
105
-
106
- _extractBinaryMeta(dataFile) {
107
- const binary = dataFile.file || dataFile.blob || dataFile.originFile || null;
108
- if (!(binary instanceof Blob)) return {};
109
-
110
- const size = this._toNumberOrNull(binary.size);
111
- const type = this._toStringOrNull(binary.type);
112
- const lastModified = (binary instanceof File)
113
- ? this._toNumberOrNull(binary.lastModified)
114
- : null;
115
-
116
- return {
117
- size: size ?? null,
118
- type: type || null,
119
- lastModified: lastModified ?? null
120
- };
121
- }
122
-
123
- _toStringOrNull(value) {
124
- if (value === undefined || value === null) return null;
125
- const normalized = String(value).trim();
126
- return normalized ? normalized : null;
127
- }
128
-
129
- _toNumberOrNull(value) {
130
- if (value === undefined || value === null || value === '') return null;
131
- const normalized = Number(value);
132
- return Number.isFinite(normalized) ? normalized : null;
133
- }
134
-
135
- _setTemplateInBuffer($items) {
136
- if (this.bufferTemplate || $items.length === 0) return;
137
-
138
- const firstItem = $items[0];
139
-
140
- if (!Manipulator.has(firstItem, 'data-file')) {
141
- this.bufferTemplate = firstItem.outerHTML;
142
- firstItem.remove();
143
- } else {
144
- this.bufferTemplate = firstItem.outerHTML;
145
- }
146
- }
147
-
148
- dispose() {
149
- this.bufferTemplate = '';
150
- this.parsedFiles = [];
151
- }
152
- }
153
-
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 { Manipulator } from "../../../utils/js/dom/manipulator";
5
+
6
+ class VGFilesTemplateRender {
7
+ constructor(vgFilesInstance, element, params = {}) {
8
+ this.module = vgFilesInstance;
9
+ this.element = isElement(element) ? element : null;
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
+ this._setTemplateInBuffer($items);
42
+ if (!this.bufferTemplate) return false;
43
+
44
+ this.parsedFiles = $items
45
+ .map(li => {
46
+ const rawData = Manipulator.get(li, 'data-file');
47
+ if (!rawData) return null;
48
+
49
+ return this._parseDataFile(rawData);
50
+ })
51
+ .filter(Boolean);
52
+
53
+ return true;
54
+ }
55
+
56
+ _parseDataFile(rawData) {
57
+ const dataFile = normalizeData(rawData);
58
+ if (!dataFile || typeof dataFile !== 'object' || Array.isArray(dataFile)) return null;
59
+
60
+ const binaryMeta = this._extractBinaryMeta(dataFile);
61
+ const name = this._toStringOrNull(dataFile.name);
62
+ const id = this._toStringOrNull(dataFile.id);
63
+ const src = this._toStringOrNull(dataFile.src);
64
+ const type = this._toStringOrNull(dataFile.type) || binaryMeta.type || 'application/octet-stream';
65
+ const size = this._toNumberOrNull(dataFile.size) ?? binaryMeta.size ?? 0;
66
+ const lastModified = this._toNumberOrNull(dataFile.lastModified ?? dataFile['last-modified']) ?? binaryMeta.lastModified ?? Date.now();
67
+
68
+ const result = {
69
+ id,
70
+ name,
71
+ size,
72
+ type,
73
+ src,
74
+ lastModified
75
+ };
76
+
77
+ if (dataFile.image !== undefined && dataFile.image !== null && dataFile.image !== '') {
78
+ result.image = dataFile.image;
79
+ }
80
+
81
+ const requiredKeys = ['id', 'name', 'size', 'type', 'src'];
82
+ const isValid = requiredKeys.every((key) => {
83
+ const value = result[key];
84
+ if (key === 'size') return Number.isFinite(value);
85
+ return value !== undefined && value !== null && value !== '';
86
+ });
87
+
88
+ if (!isValid) return null;
89
+
90
+ const customData = {};
91
+ const reserved = new Set(['id', 'name', 'size', 'type', 'src', 'image', 'lastModified', 'last-modified']);
92
+
93
+ Object.entries(dataFile).forEach(([key, value]) => {
94
+ if (reserved.has(key)) return;
95
+ if (value === undefined || value === null || value === '') return;
96
+ customData[key] = value;
97
+ });
98
+
99
+ if (Object.keys(customData).length) {
100
+ result.customData = customData;
101
+ }
102
+
103
+ return result;
104
+ }
105
+
106
+ _extractBinaryMeta(dataFile) {
107
+ const binary = dataFile.file || dataFile.blob || dataFile.originFile || null;
108
+ if (!(binary instanceof Blob)) return {};
109
+
110
+ const size = this._toNumberOrNull(binary.size);
111
+ const type = this._toStringOrNull(binary.type);
112
+ const lastModified = (binary instanceof File)
113
+ ? this._toNumberOrNull(binary.lastModified)
114
+ : null;
115
+
116
+ return {
117
+ size: size ?? null,
118
+ type: type || null,
119
+ lastModified: lastModified ?? null
120
+ };
121
+ }
122
+
123
+ _toStringOrNull(value) {
124
+ if (value === undefined || value === null) return null;
125
+ const normalized = String(value).trim();
126
+ return normalized ? normalized : null;
127
+ }
128
+
129
+ _toNumberOrNull(value) {
130
+ if (value === undefined || value === null || value === '') return null;
131
+ const normalized = Number(value);
132
+ return Number.isFinite(normalized) ? normalized : null;
133
+ }
134
+
135
+ _setTemplateInBuffer($items) {
136
+ if (this.bufferTemplate || $items.length === 0) return;
137
+
138
+ const firstItem = $items[0];
139
+
140
+ if (!Manipulator.has(firstItem, 'data-file')) {
141
+ this.bufferTemplate = firstItem.outerHTML;
142
+ firstItem.remove();
143
+ } else {
144
+ this.bufferTemplate = firstItem.outerHTML;
145
+ }
146
+ }
147
+
148
+ dispose() {
149
+ this.bufferTemplate = '';
150
+ this.parsedFiles = [];
151
+ }
152
+ }
153
+
154
154
  export default VGFilesTemplateRender;
@@ -482,14 +482,14 @@ class VGFiles extends VGFilesBase {
482
482
  if (li) {
483
483
  this._setButtonElement(file);
484
484
 
485
- const fileRemove = Selectors.find('.file-remove', li);
486
- if (fileRemove) {
487
- fileRemove.innerHTML = '';
488
- fileRemove.appendChild(this._setFailingButtons(file));
489
- Classes.add(li, CLASS_NAME_FAILING);
490
- }
491
- }
492
- }
485
+ const fileRemove = Selectors.find('.file-remove', li);
486
+ if (fileRemove) {
487
+ fileRemove.innerHTML = '';
488
+ fileRemove.appendChild(this._setFailingButtons(file));
489
+ Classes.add(li, CLASS_NAME_FAILING);
490
+ }
491
+ }
492
+ }
493
493
 
494
494
  const payload = { file: uploadData.file };
495
495
 
@@ -677,15 +677,15 @@ class VGFiles extends VGFilesBase {
677
677
  }
678
678
  });
679
679
 
680
- if (this._params.ajax && this._params.removes.single.route) {
681
- if (!id) {
682
- this._files = this._files.filter(f => !(f.name === name && f.size === size));
683
- this._updateStatsAfterRemove();
684
- this._files.length ? this.build() : this.clear(true);
685
- emitRemove();
686
- this._resetFileInput();
687
- return;
688
- }
680
+ if (this._params.ajax && this._params.removes.single.route) {
681
+ if (!id) {
682
+ this._files = this._files.filter(f => !(f.name === name && f.size === size));
683
+ this._updateStatsAfterRemove();
684
+ this._files.length ? this.build() : this.clear(true);
685
+ emitRemove();
686
+ this._resetFileInput();
687
+ return;
688
+ }
689
689
 
690
690
  const routeBase = this._params.removes.single.route;
691
691
  const routeSeparator = routeBase.includes('?') ? '&' : '?';
@@ -775,36 +775,36 @@ class VGFiles extends VGFilesBase {
775
775
  return Selectors.find(`button[data-name="${file.name}"][data-size="${file.size}"]`, this._element);
776
776
  }
777
777
 
778
- _setButtonElement(file, isAjax = false, status = '') {
779
- let icon = getSVG('trash'), action = 'data-vg-dismiss';
780
- if (!this._params.info) icon = getSVG('cross');
781
- if (isAjax) {
782
- icon = getSVG('spinner');
783
- if (status === 'completed') icon = getSVG('check');
784
- else action = 'data-vg-reload';
785
- }
786
-
787
- return this._tpl.button([
788
- this._tpl.i({}, icon, { isHTML: true })
789
- ], 'button', this._buildFileDataAttributes(file, {
778
+ _setButtonElement(file, isAjax = false, status = '') {
779
+ let icon = getSVG('trash'), action = 'data-vg-dismiss';
780
+ if (!this._params.info) icon = getSVG('cross');
781
+ if (isAjax) {
782
+ icon = getSVG('spinner');
783
+ if (status === 'completed') icon = getSVG('check');
784
+ else action = 'data-vg-reload';
785
+ }
786
+
787
+ return this._tpl.button([
788
+ this._tpl.i({}, icon, { isHTML: true })
789
+ ], 'button', this._buildFileDataAttributes(file, {
790
790
  type: 'button',
791
791
  [action]: 'file',
792
792
  'data-name': file.name,
793
793
  'data-size': file.size ?? 0,
794
794
  'data-type': file.type || '',
795
795
  'data-last-modified': file.lastModified || '',
796
- 'data-id': file.id || ''
797
- }));
798
- }
799
-
800
- _setFailingButtons(file) {
801
- const wrapper = this._tpl.div({ class: 'file-failing-actions' }, [
802
- this._setButtonElement(file, true, 'failing'),
803
- this._setButtonElement(file)
804
- ]);
805
-
806
- return wrapper;
807
- }
796
+ 'data-id': file.id || ''
797
+ }));
798
+ }
799
+
800
+ _setFailingButtons(file) {
801
+ const wrapper = this._tpl.div({ class: 'file-failing-actions' }, [
802
+ this._setButtonElement(file, true, 'failing'),
803
+ this._setButtonElement(file)
804
+ ]);
805
+
806
+ return wrapper;
807
+ }
808
808
 
809
809
  _renderStat() {
810
810
  if (!this._nodes.stat) return;
@@ -1,123 +1,123 @@
1
- # VGFiles
2
-
3
- `VGFiles` — модуль загрузки и управления файлами с поддержкой локального режима и AJAX.
4
-
5
- ## Новые фичи
6
-
7
- - Поддержка `replace` для single-mode (`limits.count = 1`): новый файл заменяет предыдущий без накопления.
8
- - Переименование входящих файлов через `rename: true | (file, index) => string`.
9
- - `smartdrop`: глобальная подсветка dropzone, если на экране только одна видимая зона дропа.
10
- - Расширенный lifecycle для внешних файлов из `data-file`: парсинг `customData`, `src/image`, бинарных метаданных.
11
- - Интеграция с `VGFilePreview` в info-списке (`ui.nameOnly: true`) для inline-аудио, скачивания и предпросмотра по клику.
12
- - Авто-обогащение аудиофайлов метаданными (title/cover) через `extractAudioMetadata`.
13
- - Гибкие confirm-хуки для удаления (`removes.single.confirm`, `removes.all.confirm`) с контрактом `accepted/data`.
14
- - Повторная загрузка упавших файлов через `data-vg-reload="file"` и кнопки retry.
15
- - Проброс `customData` файла в `data-*` атрибуты элементов списка.
16
-
17
- ## Базовые возможности
18
-
19
- - Выбор файлов через `<input type="file">` и drag&drop.
20
- - Лимиты: количество, размер файла, общий размер, MIME-типы.
21
- - Режимы загрузки: `sequential` и `parallel`.
22
- - Автоматическая/ручная инициализация.
23
- - Сортировка загруженных файлов с сохранением порядка на сервере.
24
- - Удаление одного файла и очистка всего списка.
25
-
26
- ## Инициализация
27
-
28
- ```js
29
- const files = new VGFiles(document.querySelector('.vg-files'), {
30
- ajax: true,
31
- smartdrop: true,
32
- rename: true,
33
- replace: true,
34
- uploads: {
35
- route: '/api/upload',
36
- mode: 'sequential'
37
- },
38
- limits: {
39
- count: 10,
40
- sizes: 5,
41
- total: 50
42
- }
43
- });
44
- ```
45
-
46
- ## Ключевые параметры
47
-
48
- - `allowed` — разрешить drag-drop режим и особенности drop-зоны.
49
- - `ajax` — включить серверную загрузку.
50
- - `replace` (`default: true`) — замена файла в single-mode.
51
- - `rename` (`default: false`) — переименование файлов до добавления в список.
52
- - `smartdrop` (`default: false`) — глобальная логика подсказки drop-зоны.
53
- - `prepend` (`default: true`) — добавление новых файлов в начало.
54
- - `uploads.mode` — `'sequential' | 'parallel'`.
55
- - `uploads.maxParallel`, `uploads.maxConcurrent`, `uploads.retryAttempts`, `uploads.retryDelay`.
56
- - `removes.single.confirm`, `removes.all.confirm` — кастомные confirm-обработчики.
57
- - `sortable.enabled`, `sortable.route`, `sortable.handle`, `sortable.lists`.
58
-
59
- ## Внешние (уже загруженные) файлы
60
-
61
- Модуль может стартовать с предзаполненным списком через `data-file` в `<li>`.
62
-
63
- Поддерживаемые поля объекта:
64
-
65
- - `id`, `name`, `size`, `type`, `src` (обязательный набор для валидного внешнего файла)
66
- - `image` (опционально)
67
- - `lastModified` / `last-modified` (опционально)
68
- - любые дополнительные поля -> попадают в `customData`
69
-
70
- `customData` дальше пробрасывается в `data-*` атрибуты элементов файла и может использоваться в шаблонах/интеграциях.
71
-
72
- ## AJAX-события и callbacks
73
-
74
- DOM-события:
75
-
76
- - `vg.files.change`
77
- - `vg.files.upload.start`
78
- - `vg.files.upload.progress`
79
- - `vg.files.upload.complete`
80
- - `vg.files.upload.error`
81
- - `vg.files.upload.allComplete`
82
- - `vg.files.remove`
83
- - `vg.files.reload`
84
-
85
- Callbacks (`params.callbacks`):
86
-
87
- - `onInit`
88
- - `onChange` (`{ files, input, inputFiles }`)
89
- - `onUploadStart`
90
- - `onUploadProgress`
91
- - `onUploadComplete`
92
- - `onUploadError`
93
- - `onUploadAllComplete`
94
- - `onRemoveFile`
95
- - `onClear`
96
- - `onReload`
97
-
98
- ## Удаление с кастомным confirm
99
-
100
- Если передан `removes.single.confirm` или `removes.all.confirm`, модуль вызывает вашу функцию вместо стандартного `VGAlert`.
101
-
102
- Контракт результата:
103
-
104
- - `true` или `{ accepted: true }` — подтвердить.
105
- - `false` или `{ accepted: false }` — отменить.
106
- - `{ accepted: true, data }` — подтвердить и использовать готовый ответ удаления (без дополнительного AJAX-запроса).
107
-
108
- ## Статусы файлов
109
-
110
- - `pending` — ожидает загрузки.
111
- - `loading` — идет загрузка.
112
- - `completed` — успешно загружен.
113
- - `failing` — ошибка загрузки (доступен reload).
114
-
115
- ## Методы API
116
-
117
- - `upload(file)`
118
- - `uploadAll(files)`
119
- - `reload(button)`
120
- - `removeFile(button)`
121
- - `clear(resetInput, uiOnly)`
122
- - `dispose()`
123
-
1
+ # VGFiles
2
+
3
+ `VGFiles` — модуль загрузки и управления файлами с поддержкой локального режима и AJAX.
4
+
5
+ ## Новые фичи
6
+
7
+ - Поддержка `replace` для single-mode (`limits.count = 1`): новый файл заменяет предыдущий без накопления.
8
+ - Переименование входящих файлов через `rename: true | (file, index) => string`.
9
+ - `smartdrop`: глобальная подсветка dropzone, если на экране только одна видимая зона дропа.
10
+ - Расширенный lifecycle для внешних файлов из `data-file`: парсинг `customData`, `src/image`, бинарных метаданных.
11
+ - Интеграция с `VGFilePreview` в info-списке (`ui.nameOnly: true`) для inline-аудио, скачивания и предпросмотра по клику.
12
+ - Авто-обогащение аудиофайлов метаданными (title/cover) через `extractAudioMetadata`.
13
+ - Гибкие confirm-хуки для удаления (`removes.single.confirm`, `removes.all.confirm`) с контрактом `accepted/data`.
14
+ - Повторная загрузка упавших файлов через `data-vg-reload="file"` и кнопки retry.
15
+ - Проброс `customData` файла в `data-*` атрибуты элементов списка.
16
+
17
+ ## Базовые возможности
18
+
19
+ - Выбор файлов через `<input type="file">` и drag&drop.
20
+ - Лимиты: количество, размер файла, общий размер, MIME-типы.
21
+ - Режимы загрузки: `sequential` и `parallel`.
22
+ - Автоматическая/ручная инициализация.
23
+ - Сортировка загруженных файлов с сохранением порядка на сервере.
24
+ - Удаление одного файла и очистка всего списка.
25
+
26
+ ## Инициализация
27
+
28
+ ```js
29
+ const files = new VGFiles(document.querySelector('.vg-files'), {
30
+ ajax: true,
31
+ smartdrop: true,
32
+ rename: true,
33
+ replace: true,
34
+ uploads: {
35
+ route: '/api/upload',
36
+ mode: 'sequential'
37
+ },
38
+ limits: {
39
+ count: 10,
40
+ sizes: 5,
41
+ total: 50
42
+ }
43
+ });
44
+ ```
45
+
46
+ ## Ключевые параметры
47
+
48
+ - `allowed` — разрешить drag-drop режим и особенности drop-зоны.
49
+ - `ajax` — включить серверную загрузку.
50
+ - `replace` (`default: true`) — замена файла в single-mode.
51
+ - `rename` (`default: false`) — переименование файлов до добавления в список.
52
+ - `smartdrop` (`default: false`) — глобальная логика подсказки drop-зоны.
53
+ - `prepend` (`default: true`) — добавление новых файлов в начало.
54
+ - `uploads.mode` — `'sequential' | 'parallel'`.
55
+ - `uploads.maxParallel`, `uploads.maxConcurrent`, `uploads.retryAttempts`, `uploads.retryDelay`.
56
+ - `removes.single.confirm`, `removes.all.confirm` — кастомные confirm-обработчики.
57
+ - `sortable.enabled`, `sortable.route`, `sortable.handle`, `sortable.lists`.
58
+
59
+ ## Внешние (уже загруженные) файлы
60
+
61
+ Модуль может стартовать с предзаполненным списком через `data-file` в `<li>`.
62
+
63
+ Поддерживаемые поля объекта:
64
+
65
+ - `id`, `name`, `size`, `type`, `src` (обязательный набор для валидного внешнего файла)
66
+ - `image` (опционально)
67
+ - `lastModified` / `last-modified` (опционально)
68
+ - любые дополнительные поля -> попадают в `customData`
69
+
70
+ `customData` дальше пробрасывается в `data-*` атрибуты элементов файла и может использоваться в шаблонах/интеграциях.
71
+
72
+ ## AJAX-события и callbacks
73
+
74
+ DOM-события:
75
+
76
+ - `vg.files.change`
77
+ - `vg.files.upload.start`
78
+ - `vg.files.upload.progress`
79
+ - `vg.files.upload.complete`
80
+ - `vg.files.upload.error`
81
+ - `vg.files.upload.allComplete`
82
+ - `vg.files.remove`
83
+ - `vg.files.reload`
84
+
85
+ Callbacks (`params.callbacks`):
86
+
87
+ - `onInit`
88
+ - `onChange` (`{ files, input, inputFiles }`)
89
+ - `onUploadStart`
90
+ - `onUploadProgress`
91
+ - `onUploadComplete`
92
+ - `onUploadError`
93
+ - `onUploadAllComplete`
94
+ - `onRemoveFile`
95
+ - `onClear`
96
+ - `onReload`
97
+
98
+ ## Удаление с кастомным confirm
99
+
100
+ Если передан `removes.single.confirm` или `removes.all.confirm`, модуль вызывает вашу функцию вместо стандартного `VGAlert`.
101
+
102
+ Контракт результата:
103
+
104
+ - `true` или `{ accepted: true }` — подтвердить.
105
+ - `false` или `{ accepted: false }` — отменить.
106
+ - `{ accepted: true, data }` — подтвердить и использовать готовый ответ удаления (без дополнительного AJAX-запроса).
107
+
108
+ ## Статусы файлов
109
+
110
+ - `pending` — ожидает загрузки.
111
+ - `loading` — идет загрузка.
112
+ - `completed` — успешно загружен.
113
+ - `failing` — ошибка загрузки (доступен reload).
114
+
115
+ ## Методы API
116
+
117
+ - `upload(file)`
118
+ - `uploadAll(files)`
119
+ - `reload(button)`
120
+ - `removeFile(button)`
121
+ - `clear(resetInput, uiOnly)`
122
+ - `dispose()`
123
+