vgapp 1.1.3 → 1.1.5

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 +30 -1
  2. package/README.md +48 -48
  3. package/agents.md +7 -0
  4. package/app/langs/en/buttons.json +17 -17
  5. package/app/langs/en/messages.json +36 -36
  6. package/app/langs/ru/buttons.json +17 -17
  7. package/app/langs/ru/messages.json +36 -36
  8. package/app/modules/vgfilepreview/js/i18n.js +56 -56
  9. package/app/modules/vgfilepreview/js/renderers/image-modal.js +145 -145
  10. package/app/modules/vgfilepreview/js/renderers/image.js +92 -92
  11. package/app/modules/vgfilepreview/js/renderers/index.js +19 -19
  12. package/app/modules/vgfilepreview/js/renderers/office-modal.js +168 -168
  13. package/app/modules/vgfilepreview/js/renderers/office.js +79 -79
  14. package/app/modules/vgfilepreview/js/renderers/pdf-modal.js +260 -260
  15. package/app/modules/vgfilepreview/js/renderers/pdf.js +76 -76
  16. package/app/modules/vgfilepreview/js/renderers/playlist.js +71 -71
  17. package/app/modules/vgfilepreview/js/renderers/text-modal.js +343 -343
  18. package/app/modules/vgfilepreview/js/renderers/text.js +83 -83
  19. package/app/modules/vgfilepreview/js/renderers/video-modal.js +272 -272
  20. package/app/modules/vgfilepreview/js/renderers/video.js +80 -80
  21. package/app/modules/vgfilepreview/js/renderers/zip-modal.js +522 -522
  22. package/app/modules/vgfilepreview/js/renderers/zip.js +89 -89
  23. package/app/modules/vgfilepreview/js/vgfilepreview.js +965 -530
  24. package/app/modules/vgfilepreview/readme.md +68 -68
  25. package/app/modules/vgfilepreview/scss/_variables.scss +113 -113
  26. package/app/modules/vgfilepreview/scss/vgfilepreview.scss +464 -460
  27. package/app/modules/vgfiles/js/base.js +463 -463
  28. package/app/modules/vgfiles/js/droppable.js +260 -260
  29. package/app/modules/vgfiles/js/render.js +153 -153
  30. package/app/modules/vgfiles/js/vgfiles.js +41 -41
  31. package/app/modules/vgfiles/readme.md +123 -123
  32. package/app/modules/vgfiles/scss/_variables.scss +18 -18
  33. package/app/modules/vgfiles/scss/vgfiles.scss +148 -148
  34. package/app/modules/vgformsender/js/vgformsender.js +13 -13
  35. package/app/modules/vgmodal/js/vgmodal.drag.js +332 -0
  36. package/app/modules/vgmodal/js/vgmodal.js +116 -14
  37. package/app/modules/vgmodal/js/vgmodal.resize.js +435 -0
  38. package/app/modules/vgnav/js/vgnav.js +135 -135
  39. package/app/modules/vgnav/readme.md +67 -67
  40. package/app/modules/vgnestable/README.md +307 -307
  41. package/app/modules/vgnestable/scss/_variables.scss +60 -60
  42. package/app/modules/vgnestable/scss/vgnestable.scss +163 -163
  43. package/app/modules/vgselect/js/vgselect.js +39 -39
  44. package/app/modules/vgselect/scss/vgselect.scss +22 -22
  45. package/app/modules/vgspy/readme.md +28 -28
  46. package/app/utils/js/components/audio-metadata.js +240 -240
  47. package/app/utils/js/components/file-icon.js +109 -109
  48. package/app/utils/js/components/file-preview.js +304 -304
  49. package/app/utils/js/components/sanitize.js +150 -150
  50. package/app/utils/js/components/video-metadata.js +140 -0
  51. package/build/vgapp.css +1 -1
  52. package/build/vgapp.css.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
+