@doyosi/laraisy 1.0.1 → 1.0.3
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/LICENSE +1 -1
- package/package.json +1 -1
- package/src/CodeInput.js +48 -48
- package/src/DSAlert.js +352 -352
- package/src/DSAvatar.js +207 -207
- package/src/DSDelete.js +274 -274
- package/src/DSForm.js +568 -568
- package/src/DSGridOrTable.js +453 -453
- package/src/DSLocaleSwitcher.js +239 -239
- package/src/DSLogout.js +293 -293
- package/src/DSNotifications.js +365 -365
- package/src/DSRestore.js +181 -181
- package/src/DSSelect.js +1071 -1071
- package/src/DSSelectBox.js +563 -563
- package/src/DSSimpleSlider.js +517 -517
- package/src/DSSvgFetch.js +69 -69
- package/src/DSTable/DSTableExport.js +68 -68
- package/src/DSTable/DSTableFilter.js +224 -224
- package/src/DSTable/DSTablePagination.js +136 -136
- package/src/DSTable/DSTableSearch.js +40 -40
- package/src/DSTable/DSTableSelection.js +192 -192
- package/src/DSTable/DSTableSort.js +58 -58
- package/src/DSTable.js +353 -353
- package/src/DSTabs.js +488 -488
- package/src/DSUpload.js +887 -887
- package/dist/CodeInput.d.ts +0 -10
- package/dist/DSAlert.d.ts +0 -112
- package/dist/DSAvatar.d.ts +0 -45
- package/dist/DSDelete.d.ts +0 -61
- package/dist/DSForm.d.ts +0 -151
- package/dist/DSGridOrTable/DSGOTRenderer.d.ts +0 -60
- package/dist/DSGridOrTable/DSGOTViewToggle.d.ts +0 -26
- package/dist/DSGridOrTable.d.ts +0 -296
- package/dist/DSLocaleSwitcher.d.ts +0 -71
- package/dist/DSLogout.d.ts +0 -76
- package/dist/DSNotifications.d.ts +0 -54
- package/dist/DSRestore.d.ts +0 -56
- package/dist/DSSelect.d.ts +0 -221
- package/dist/DSSelectBox.d.ts +0 -123
- package/dist/DSSimpleSlider.d.ts +0 -136
- package/dist/DSSvgFetch.d.ts +0 -17
- package/dist/DSTable/DSTableExport.d.ts +0 -11
- package/dist/DSTable/DSTableFilter.d.ts +0 -40
- package/dist/DSTable/DSTablePagination.d.ts +0 -12
- package/dist/DSTable/DSTableSearch.d.ts +0 -8
- package/dist/DSTable/DSTableSelection.d.ts +0 -46
- package/dist/DSTable/DSTableSort.d.ts +0 -8
- package/dist/DSTable.d.ts +0 -116
- package/dist/DSTabs.d.ts +0 -156
- package/dist/DSUpload.d.ts +0 -220
- package/dist/index.d.ts +0 -17
package/src/DSUpload.js
CHANGED
|
@@ -1,887 +1,887 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* DSUpload
|
|
3
|
-
*
|
|
4
|
-
* A comprehensive file upload component with:
|
|
5
|
-
* - Image/file preview
|
|
6
|
-
* - Progress bar support
|
|
7
|
-
* - File type validation
|
|
8
|
-
* - File size validation
|
|
9
|
-
* - Old value / default image support
|
|
10
|
-
* - Reset functionality
|
|
11
|
-
* - Drag and drop support
|
|
12
|
-
* - Full event system
|
|
13
|
-
*/
|
|
14
|
-
export class DSUpload {
|
|
15
|
-
static instances = new Map();
|
|
16
|
-
static instanceCounter = 0;
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Default Icons
|
|
20
|
-
*/
|
|
21
|
-
static icons = {
|
|
22
|
-
upload: `<svg class="w-8 h-8" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"/></svg>`,
|
|
23
|
-
file: `<svg class="w-12 h-12" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/></svg>`,
|
|
24
|
-
image: `<svg class="w-12 h-12" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"/></svg>`,
|
|
25
|
-
close: `<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/></svg>`,
|
|
26
|
-
reset: `<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/></svg>`,
|
|
27
|
-
check: `<svg class="w-5 h-5 text-success" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/></svg>`
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Default configuration
|
|
32
|
-
*/
|
|
33
|
-
static defaults = {
|
|
34
|
-
// Preview options
|
|
35
|
-
preview: true, // Enable preview
|
|
36
|
-
previewMaxHeight: '200px', // Max height for preview image
|
|
37
|
-
defaultImage: null, // Default/placeholder image URL
|
|
38
|
-
oldValue: null, // Old/current file URL (for edit forms)
|
|
39
|
-
|
|
40
|
-
// File validation
|
|
41
|
-
accept: '*', // Accepted file types (e.g., 'image/*', '.pdf,.doc')
|
|
42
|
-
maxSize: 5 * 1024 * 1024, // Max file size in bytes (default 5MB)
|
|
43
|
-
minSize: 0, // Min file size in bytes
|
|
44
|
-
|
|
45
|
-
// UI options
|
|
46
|
-
showProgressBar: true, // Show upload progress bar
|
|
47
|
-
showFileInfo: true, // Show file name and size
|
|
48
|
-
showResetButton: true, // Show reset button when file selected
|
|
49
|
-
showRemoveButton: true, // Show remove/clear button
|
|
50
|
-
dropzone: true, // Enable drag and drop
|
|
51
|
-
dropzoneText: 'Drop file here or click to upload',
|
|
52
|
-
browseText: 'Browse',
|
|
53
|
-
|
|
54
|
-
// Styling
|
|
55
|
-
wrapperClass: '',
|
|
56
|
-
previewClass: '',
|
|
57
|
-
dropzoneClass: '',
|
|
58
|
-
|
|
59
|
-
// Behavior
|
|
60
|
-
autoUpload: false, // Auto upload on select (requires uploadUrl)
|
|
61
|
-
uploadUrl: null, // URL for AJAX upload
|
|
62
|
-
removeUrl: null, // URL for AJAX removal (DELETE request)
|
|
63
|
-
uploadMethod: 'POST', // Upload HTTP method
|
|
64
|
-
uploadFieldName: null, // Override form field name for upload
|
|
65
|
-
|
|
66
|
-
// Size display
|
|
67
|
-
sizeUnit: 'auto', // 'auto', 'KB', 'MB', 'GB'
|
|
68
|
-
|
|
69
|
-
// Translations
|
|
70
|
-
translations: {
|
|
71
|
-
dropzone: 'Drop file here or click to upload',
|
|
72
|
-
browse: 'Browse',
|
|
73
|
-
remove: 'Remove',
|
|
74
|
-
reset: 'Reset',
|
|
75
|
-
fileTooBig: 'File is too large. Maximum size is {max}',
|
|
76
|
-
fileTooSmall: 'File is too small. Minimum size is {min}',
|
|
77
|
-
invalidType: 'Invalid file type. Accepted types: {types}',
|
|
78
|
-
uploadError: 'Upload failed',
|
|
79
|
-
uploading: 'Uploading...'
|
|
80
|
-
}
|
|
81
|
-
};
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* @param {string|HTMLElement} selector - Input element or selector
|
|
85
|
-
* @param {Object} config - Configuration options
|
|
86
|
-
*/
|
|
87
|
-
constructor(selector, config = {}) {
|
|
88
|
-
this.instanceId = `ds-upload-${++DSUpload.instanceCounter}`;
|
|
89
|
-
|
|
90
|
-
// Find the input element
|
|
91
|
-
const el = typeof selector === 'string' ? document.querySelector(selector) : selector;
|
|
92
|
-
|
|
93
|
-
if (!el) {
|
|
94
|
-
throw new Error('DSUpload: Element not found.');
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
// Store original input
|
|
98
|
-
this.originalInput = el.tagName === 'INPUT' ? el : el.querySelector('input[type="file"]');
|
|
99
|
-
if (!this.originalInput) {
|
|
100
|
-
throw new Error('DSUpload: No file input found.');
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// Find or create wrapper
|
|
104
|
-
this.wrapper = el.closest('.form-group') || el.parentElement;
|
|
105
|
-
|
|
106
|
-
// Merge config with data attributes
|
|
107
|
-
this.cfg = this._buildConfig(config);
|
|
108
|
-
|
|
109
|
-
// State
|
|
110
|
-
this._file = null;
|
|
111
|
-
this._previewUrl = null;
|
|
112
|
-
this._isUploading = false;
|
|
113
|
-
this._progress = 0;
|
|
114
|
-
this._originalValue = this.cfg.oldValue || this.cfg.defaultImage;
|
|
115
|
-
|
|
116
|
-
// Event listeners
|
|
117
|
-
this._listeners = {};
|
|
118
|
-
this._boundHandlers = {};
|
|
119
|
-
|
|
120
|
-
// Initialize
|
|
121
|
-
this._init();
|
|
122
|
-
|
|
123
|
-
// Register instance
|
|
124
|
-
DSUpload.instances.set(this.instanceId, this);
|
|
125
|
-
this.wrapper.dataset.dsUploadId = this.instanceId;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
/**
|
|
129
|
-
* Static factory
|
|
130
|
-
*/
|
|
131
|
-
static create(selector, config = {}) {
|
|
132
|
-
return new DSUpload(selector, config);
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
/**
|
|
136
|
-
* Get instance by element
|
|
137
|
-
*/
|
|
138
|
-
static getInstance(element) {
|
|
139
|
-
const el = typeof element === 'string' ? document.querySelector(element) : element;
|
|
140
|
-
if (!el) return null;
|
|
141
|
-
const wrapper = el.closest('[data-ds-upload-id]');
|
|
142
|
-
if (!wrapper) return null;
|
|
143
|
-
return DSUpload.instances.get(wrapper.dataset.dsUploadId);
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
/**
|
|
147
|
-
* Auto-initialize all elements with [data-ds-upload]
|
|
148
|
-
*/
|
|
149
|
-
static initAll(selector = '[data-ds-upload]') {
|
|
150
|
-
document.querySelectorAll(selector).forEach(el => {
|
|
151
|
-
if (!el.closest('[data-ds-upload-id]')) {
|
|
152
|
-
new DSUpload(el);
|
|
153
|
-
}
|
|
154
|
-
});
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
// ==================== INITIALIZATION ====================
|
|
158
|
-
|
|
159
|
-
_buildConfig(userConfig) {
|
|
160
|
-
const dataConfig = this._parseDataAttributes();
|
|
161
|
-
return { ...DSUpload.defaults, ...dataConfig, ...userConfig };
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
_parseDataAttributes() {
|
|
165
|
-
const data = this.originalInput.dataset;
|
|
166
|
-
const config = {};
|
|
167
|
-
|
|
168
|
-
if (data.preview !== undefined) config.preview = data.preview !== 'false';
|
|
169
|
-
if (data.defaultImage) config.defaultImage = data.defaultImage;
|
|
170
|
-
if (data.fallbackImage) config.fallbackImage = data.fallbackImage;
|
|
171
|
-
if (data.oldValue) config.oldValue = data.oldValue;
|
|
172
|
-
if (data.accept) config.accept = data.accept;
|
|
173
|
-
if (data.maxSize) config.maxSize = parseInt(data.maxSize, 10);
|
|
174
|
-
if (data.minSize) config.minSize = parseInt(data.minSize, 10);
|
|
175
|
-
if (data.dropzone !== undefined) config.dropzone = data.dropzone !== 'false';
|
|
176
|
-
if (data.showProgressBar !== undefined) config.showProgressBar = data.showProgressBar !== 'false';
|
|
177
|
-
if (data.uploadUrl) config.uploadUrl = data.uploadUrl;
|
|
178
|
-
if (data.removeUrl) config.removeUrl = data.removeUrl;
|
|
179
|
-
|
|
180
|
-
return config;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
_init() {
|
|
184
|
-
this._buildDOM();
|
|
185
|
-
this._cacheElements();
|
|
186
|
-
this._bindEvents();
|
|
187
|
-
this._setInitialPreview();
|
|
188
|
-
|
|
189
|
-
if (this.cfg.accept && this.cfg.accept !== '*') {
|
|
190
|
-
this.originalInput.setAttribute('accept', this.cfg.accept);
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
_buildDOM() {
|
|
195
|
-
// Hide original input
|
|
196
|
-
this.originalInput.style.display = 'none';
|
|
197
|
-
this.originalInput.classList.add('ds-upload-input');
|
|
198
|
-
|
|
199
|
-
// Create upload UI
|
|
200
|
-
const container = document.createElement('div');
|
|
201
|
-
container.className = `ds-upload-container relative ${this.cfg.wrapperClass}`;
|
|
202
|
-
container.innerHTML = `
|
|
203
|
-
<!-- Dropzone / Upload Area -->
|
|
204
|
-
<div class="ds-upload-dropzone border-2 border-dashed border-base-300 rounded-sm p-3 pb-1 text-center cursor-pointer transition-all hover:border-primary hover:bg-base-200/50 ${this.cfg.dropzoneClass}"
|
|
205
|
-
data-ds-upload-dropzone>
|
|
206
|
-
<div class="ds-upload-placeholder flex flex-col items-center gap-2" data-ds-upload-placeholder>
|
|
207
|
-
<span class="text-base-content/40">${DSUpload.icons.upload}</span>
|
|
208
|
-
<p class="text-sm text-base-content/60">${this.cfg.translations.dropzone}</p>
|
|
209
|
-
<button type="button" class="btn btn-primary btn-sm mt-2" data-ds-upload-browse>
|
|
210
|
-
${this.cfg.translations.browse}
|
|
211
|
-
</button>
|
|
212
|
-
</div>
|
|
213
|
-
|
|
214
|
-
<!-- Preview Area -->
|
|
215
|
-
<div class="ds-upload-preview hidden" data-ds-upload-preview>
|
|
216
|
-
<div class="relative inline-block">
|
|
217
|
-
<img src="" alt="Preview"
|
|
218
|
-
class="max-h-[${this.cfg.previewMaxHeight}] max-w-full rounded-lg object-contain mx-auto ${this.cfg.previewClass}"
|
|
219
|
-
data-ds-upload-preview-img>
|
|
220
|
-
<div class="ds-upload-file-preview hidden flex-col items-center gap-2" data-ds-upload-file-preview>
|
|
221
|
-
<span class="text-base-content/40">${DSUpload.icons.file}</span>
|
|
222
|
-
<span class="text-sm font-medium" data-ds-upload-filename></span>
|
|
223
|
-
</div>
|
|
224
|
-
</div>
|
|
225
|
-
|
|
226
|
-
<!-- File Info -->
|
|
227
|
-
<div class="ds-upload-info mt-3 text-center" data-ds-upload-info>
|
|
228
|
-
<p class="text-sm font-medium text-base-content" data-ds-upload-name></p>
|
|
229
|
-
<p class="text-xs text-base-content/60" data-ds-upload-size></p>
|
|
230
|
-
</div>
|
|
231
|
-
|
|
232
|
-
<!-- Action Buttons -->
|
|
233
|
-
<div class="ds-upload-actions flex justify-center gap-2 mt-3">
|
|
234
|
-
<button type="button" class="btn btn-ghost btn-sm gap-1" data-ds-upload-reset title="${this.cfg.translations.reset}">
|
|
235
|
-
${DSUpload.icons.reset}
|
|
236
|
-
<span>${this.cfg.translations.reset}</span>
|
|
237
|
-
</button>
|
|
238
|
-
<button type="button" class="btn btn-ghost btn-sm text-error gap-1" data-ds-upload-remove title="${this.cfg.translations.remove}">
|
|
239
|
-
${DSUpload.icons.close}
|
|
240
|
-
<span>${this.cfg.translations.remove}</span>
|
|
241
|
-
</button>
|
|
242
|
-
</div>
|
|
243
|
-
</div>
|
|
244
|
-
|
|
245
|
-
<!-- Progress Bar -->
|
|
246
|
-
<div class="ds-upload-progress hidden mt-4" data-ds-upload-progress>
|
|
247
|
-
<div class="flex justify-between text-xs text-base-content/60 mb-1">
|
|
248
|
-
<span>${this.cfg.translations.uploading}</span>
|
|
249
|
-
<span data-ds-upload-progress-text>0%</span>
|
|
250
|
-
</div>
|
|
251
|
-
<progress class="progress progress-primary w-full" value="0" max="100" data-ds-upload-progress-bar></progress>
|
|
252
|
-
</div>
|
|
253
|
-
</div>
|
|
254
|
-
|
|
255
|
-
<!-- Error Message -->
|
|
256
|
-
<div class="ds-upload-error hidden mt-2" data-ds-upload-error>
|
|
257
|
-
<p class="text-sm text-error flex items-center gap-1">
|
|
258
|
-
<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
259
|
-
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
|
260
|
-
</svg>
|
|
261
|
-
<span data-ds-upload-error-text></span>
|
|
262
|
-
</p>
|
|
263
|
-
</div>
|
|
264
|
-
`;
|
|
265
|
-
|
|
266
|
-
// Insert after original input
|
|
267
|
-
this.originalInput.insertAdjacentElement('afterend', container);
|
|
268
|
-
this.container = container;
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
_cacheElements() {
|
|
272
|
-
this.elements = {
|
|
273
|
-
dropzone: this.container.querySelector('[data-ds-upload-dropzone]'),
|
|
274
|
-
placeholder: this.container.querySelector('[data-ds-upload-placeholder]'),
|
|
275
|
-
preview: this.container.querySelector('[data-ds-upload-preview]'),
|
|
276
|
-
previewImg: this.container.querySelector('[data-ds-upload-preview-img]'),
|
|
277
|
-
filePreview: this.container.querySelector('[data-ds-upload-file-preview]'),
|
|
278
|
-
filename: this.container.querySelector('[data-ds-upload-filename]'),
|
|
279
|
-
info: this.container.querySelector('[data-ds-upload-info]'),
|
|
280
|
-
name: this.container.querySelector('[data-ds-upload-name]'),
|
|
281
|
-
size: this.container.querySelector('[data-ds-upload-size]'),
|
|
282
|
-
reset: this.container.querySelector('[data-ds-upload-reset]'),
|
|
283
|
-
remove: this.container.querySelector('[data-ds-upload-remove]'),
|
|
284
|
-
browse: this.container.querySelector('[data-ds-upload-browse]'),
|
|
285
|
-
progress: this.container.querySelector('[data-ds-upload-progress]'),
|
|
286
|
-
progressBar: this.container.querySelector('[data-ds-upload-progress-bar]'),
|
|
287
|
-
progressText: this.container.querySelector('[data-ds-upload-progress-text]'),
|
|
288
|
-
error: this.container.querySelector('[data-ds-upload-error]'),
|
|
289
|
-
errorText: this.container.querySelector('[data-ds-upload-error-text]')
|
|
290
|
-
};
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
_bindEvents() {
|
|
294
|
-
this._boundHandlers = {
|
|
295
|
-
onInputChange: this._onInputChange.bind(this),
|
|
296
|
-
onDropzoneClick: this._onDropzoneClick.bind(this),
|
|
297
|
-
onDragOver: this._onDragOver.bind(this),
|
|
298
|
-
onDragLeave: this._onDragLeave.bind(this),
|
|
299
|
-
onDrop: this._onDrop.bind(this),
|
|
300
|
-
onReset: this._onReset.bind(this),
|
|
301
|
-
onRemove: this._onRemove.bind(this),
|
|
302
|
-
onBrowse: this._onBrowse.bind(this)
|
|
303
|
-
};
|
|
304
|
-
|
|
305
|
-
// Input change
|
|
306
|
-
this.originalInput.addEventListener('change', this._boundHandlers.onInputChange);
|
|
307
|
-
|
|
308
|
-
// Dropzone click
|
|
309
|
-
this.elements.dropzone.addEventListener('click', this._boundHandlers.onDropzoneClick);
|
|
310
|
-
|
|
311
|
-
// Drag and drop
|
|
312
|
-
if (this.cfg.dropzone) {
|
|
313
|
-
this.elements.dropzone.addEventListener('dragover', this._boundHandlers.onDragOver);
|
|
314
|
-
this.elements.dropzone.addEventListener('dragleave', this._boundHandlers.onDragLeave);
|
|
315
|
-
this.elements.dropzone.addEventListener('drop', this._boundHandlers.onDrop);
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
// Buttons
|
|
319
|
-
this.elements.browse.addEventListener('click', this._boundHandlers.onBrowse);
|
|
320
|
-
this.elements.reset.addEventListener('click', this._boundHandlers.onReset);
|
|
321
|
-
this.elements.remove.addEventListener('click', this._boundHandlers.onRemove);
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
_setInitialPreview() {
|
|
325
|
-
const initialUrl = this.cfg.oldValue || this.cfg.defaultImage;
|
|
326
|
-
// Treat removeUrl as proof of a real value existing on the server
|
|
327
|
-
const hasRealValue = !!this.cfg.oldValue || !!this.cfg.removeUrl;
|
|
328
|
-
|
|
329
|
-
if (initialUrl && this.cfg.preview) {
|
|
330
|
-
this._showPreview(initialUrl, null, true, hasRealValue);
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
// Hide reset button initially (no new file selected yet)
|
|
334
|
-
this.elements.reset.classList.add('hidden');
|
|
335
|
-
|
|
336
|
-
// Hide remove button if only showing default placeholder logic handled in _showPreview
|
|
337
|
-
// based on hasRealValue passed above.
|
|
338
|
-
|
|
339
|
-
this._emit('ready', { hasInitialValue: !!initialUrl, hasRealValue });
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
// ==================== EVENT HANDLERS ====================
|
|
343
|
-
|
|
344
|
-
_onInputChange(e) {
|
|
345
|
-
const file = e.target.files[0];
|
|
346
|
-
if (file) {
|
|
347
|
-
this._processFile(file);
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
_onDropzoneClick(e) {
|
|
352
|
-
// Don't trigger if clicking buttons
|
|
353
|
-
if (e.target.closest('button')) return;
|
|
354
|
-
this.originalInput.click();
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
_onBrowse(e) {
|
|
358
|
-
e.stopPropagation();
|
|
359
|
-
this.originalInput.click();
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
_onDragOver(e) {
|
|
363
|
-
e.preventDefault();
|
|
364
|
-
e.stopPropagation();
|
|
365
|
-
this.elements.dropzone.classList.add('border-primary', 'bg-primary/10');
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
_onDragLeave(e) {
|
|
369
|
-
e.preventDefault();
|
|
370
|
-
e.stopPropagation();
|
|
371
|
-
this.elements.dropzone.classList.remove('border-primary', 'bg-primary/10');
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
_onDrop(e) {
|
|
375
|
-
e.preventDefault();
|
|
376
|
-
e.stopPropagation();
|
|
377
|
-
this.elements.dropzone.classList.remove('border-primary', 'bg-primary/10');
|
|
378
|
-
|
|
379
|
-
const file = e.dataTransfer.files[0];
|
|
380
|
-
if (file) {
|
|
381
|
-
this._processFile(file);
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
_onReset(e) {
|
|
386
|
-
e.stopPropagation();
|
|
387
|
-
this.reset();
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
async _onRemove(e) {
|
|
391
|
-
e.stopPropagation();
|
|
392
|
-
|
|
393
|
-
// If we have a removeUrl and a real value (implied by removeUrl presence), try to delete via AJAX
|
|
394
|
-
// validation: ensure we don't have a new file selected (this._file should be null)
|
|
395
|
-
if (this.cfg.removeUrl && !this._file) {
|
|
396
|
-
if (confirm('Are you sure you want to remove this file?')) {
|
|
397
|
-
const success = await this.remove();
|
|
398
|
-
if (success) {
|
|
399
|
-
// File is deleted from server.
|
|
400
|
-
// If we have a fallback image, set it as the new default
|
|
401
|
-
// Otherwise clear default so we show placeholder
|
|
402
|
-
this.cfg.defaultImage = this.cfg.fallbackImage || null;
|
|
403
|
-
|
|
404
|
-
this.cfg.oldValue = null;
|
|
405
|
-
// Also clear removeUrl to prevent trying to delete again
|
|
406
|
-
this.cfg.removeUrl = null;
|
|
407
|
-
|
|
408
|
-
this.clear();
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
|
-
} else {
|
|
412
|
-
this.clear();
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
// ==================== FILE PROCESSING ====================
|
|
417
|
-
|
|
418
|
-
_processFile(file) {
|
|
419
|
-
// Clear previous errors
|
|
420
|
-
this._hideError();
|
|
421
|
-
|
|
422
|
-
// Validate file type
|
|
423
|
-
if (!this._validateType(file)) {
|
|
424
|
-
this._showError(
|
|
425
|
-
this.cfg.translations.invalidType.replace('{types}', this.cfg.accept)
|
|
426
|
-
);
|
|
427
|
-
this._emit('error', { error: 'invalid_type', file });
|
|
428
|
-
return false;
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
// Validate file size
|
|
432
|
-
if (!this._validateSize(file)) {
|
|
433
|
-
if (file.size > this.cfg.maxSize) {
|
|
434
|
-
this._showError(
|
|
435
|
-
this.cfg.translations.fileTooBig.replace('{max}', this._formatSize(this.cfg.maxSize))
|
|
436
|
-
);
|
|
437
|
-
} else {
|
|
438
|
-
this._showError(
|
|
439
|
-
this.cfg.translations.fileTooSmall.replace('{min}', this._formatSize(this.cfg.minSize))
|
|
440
|
-
);
|
|
441
|
-
}
|
|
442
|
-
this._emit('error', { error: 'invalid_size', file });
|
|
443
|
-
return false;
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
// Store file
|
|
447
|
-
this._file = file;
|
|
448
|
-
|
|
449
|
-
// Update input
|
|
450
|
-
const dt = new DataTransfer();
|
|
451
|
-
dt.items.add(file);
|
|
452
|
-
this.originalInput.files = dt.files;
|
|
453
|
-
|
|
454
|
-
// Show preview
|
|
455
|
-
if (this.cfg.preview) {
|
|
456
|
-
if (this._isImage(file)) {
|
|
457
|
-
const reader = new FileReader();
|
|
458
|
-
reader.onload = (e) => {
|
|
459
|
-
this._showPreview(e.target.result, file);
|
|
460
|
-
};
|
|
461
|
-
reader.readAsDataURL(file);
|
|
462
|
-
} else {
|
|
463
|
-
this._showPreview(null, file);
|
|
464
|
-
}
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
// Auto upload
|
|
468
|
-
if (this.cfg.autoUpload && this.cfg.uploadUrl) {
|
|
469
|
-
this.upload();
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
this._emit('select', { file });
|
|
473
|
-
return true;
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
_validateType(file) {
|
|
477
|
-
if (!this.cfg.accept || this.cfg.accept === '*') return true;
|
|
478
|
-
|
|
479
|
-
const accept = this.cfg.accept.toLowerCase().split(',').map(t => t.trim());
|
|
480
|
-
const fileType = file.type.toLowerCase();
|
|
481
|
-
const fileExt = '.' + file.name.split('.').pop().toLowerCase();
|
|
482
|
-
|
|
483
|
-
return accept.some(type => {
|
|
484
|
-
if (type.endsWith('/*')) {
|
|
485
|
-
// Wildcard like image/*
|
|
486
|
-
return fileType.startsWith(type.replace('/*', '/'));
|
|
487
|
-
}
|
|
488
|
-
if (type.startsWith('.')) {
|
|
489
|
-
// Extension like .pdf
|
|
490
|
-
return fileExt === type;
|
|
491
|
-
}
|
|
492
|
-
// Exact MIME type
|
|
493
|
-
return fileType === type;
|
|
494
|
-
});
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
_validateSize(file) {
|
|
498
|
-
if (this.cfg.maxSize && file.size > this.cfg.maxSize) return false;
|
|
499
|
-
if (this.cfg.minSize && file.size < this.cfg.minSize) return false;
|
|
500
|
-
return true;
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
_isImage(file) {
|
|
504
|
-
return file.type.startsWith('image/');
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
// ==================== UI UPDATES ====================
|
|
508
|
-
|
|
509
|
-
_showPreview(url, file, isInitial = false, hasRealValue = false) {
|
|
510
|
-
this.elements.placeholder.classList.add('hidden');
|
|
511
|
-
this.elements.preview.classList.remove('hidden');
|
|
512
|
-
|
|
513
|
-
if (url && (isInitial || (file && this._isImage(file)))) {
|
|
514
|
-
// Image preview
|
|
515
|
-
this.elements.previewImg.src = url;
|
|
516
|
-
this.elements.previewImg.classList.remove('hidden');
|
|
517
|
-
this.elements.filePreview.classList.add('hidden');
|
|
518
|
-
this._previewUrl = url;
|
|
519
|
-
} else {
|
|
520
|
-
// File icon preview
|
|
521
|
-
this.elements.previewImg.classList.add('hidden');
|
|
522
|
-
this.elements.filePreview.classList.remove('hidden');
|
|
523
|
-
this.elements.filePreview.classList.add('flex');
|
|
524
|
-
if (file) {
|
|
525
|
-
this.elements.filename.textContent = file.name;
|
|
526
|
-
}
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
// File info
|
|
530
|
-
if (file && this.cfg.showFileInfo) {
|
|
531
|
-
this.elements.info.classList.remove('hidden');
|
|
532
|
-
this.elements.name.textContent = file.name;
|
|
533
|
-
this.elements.size.textContent = this._formatSize(file.size);
|
|
534
|
-
} else if (isInitial) {
|
|
535
|
-
this.elements.info.classList.add('hidden');
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
// Show/hide buttons based on context
|
|
539
|
-
if (file) {
|
|
540
|
-
// New file selected - show only reset (to go back to original)
|
|
541
|
-
this.elements.reset.classList.remove('hidden');
|
|
542
|
-
this.elements.remove.classList.add('hidden');
|
|
543
|
-
} else if (isInitial && hasRealValue) {
|
|
544
|
-
// Initial load with real value (oldValue from DB) - show remove only
|
|
545
|
-
this.elements.reset.classList.add('hidden');
|
|
546
|
-
if (this.cfg.showRemoveButton) {
|
|
547
|
-
this.elements.remove.classList.remove('hidden');
|
|
548
|
-
}
|
|
549
|
-
} else if (isInitial) {
|
|
550
|
-
// Initial load with default placeholder only - hide both
|
|
551
|
-
this.elements.reset.classList.add('hidden');
|
|
552
|
-
this.elements.remove.classList.add('hidden');
|
|
553
|
-
}
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
_showPlaceholder() {
|
|
557
|
-
this.elements.placeholder.classList.remove('hidden');
|
|
558
|
-
this.elements.preview.classList.add('hidden');
|
|
559
|
-
this.elements.info.classList.add('hidden');
|
|
560
|
-
this.elements.reset.classList.add('hidden');
|
|
561
|
-
this.elements.remove.classList.add('hidden');
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
_showProgress(percent) {
|
|
565
|
-
this.elements.progress.classList.remove('hidden');
|
|
566
|
-
this.elements.progressBar.value = percent;
|
|
567
|
-
this.elements.progressText.textContent = `${Math.round(percent)}%`;
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
_hideProgress() {
|
|
571
|
-
this.elements.progress.classList.add('hidden');
|
|
572
|
-
this.elements.progressBar.value = 0;
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
_showError(message) {
|
|
576
|
-
this.elements.error.classList.remove('hidden');
|
|
577
|
-
this.elements.errorText.textContent = message;
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
_hideError() {
|
|
581
|
-
this.elements.error.classList.add('hidden');
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
// ==================== PUBLIC API ====================
|
|
585
|
-
|
|
586
|
-
/**
|
|
587
|
-
* Get the current file
|
|
588
|
-
*/
|
|
589
|
-
getFile() {
|
|
590
|
-
return this._file;
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
/**
|
|
594
|
-
* Get file info
|
|
595
|
-
*/
|
|
596
|
-
getFileInfo() {
|
|
597
|
-
if (!this._file) return null;
|
|
598
|
-
return {
|
|
599
|
-
name: this._file.name,
|
|
600
|
-
size: this._file.size,
|
|
601
|
-
type: this._file.type,
|
|
602
|
-
formattedSize: this._formatSize(this._file.size)
|
|
603
|
-
};
|
|
604
|
-
}
|
|
605
|
-
|
|
606
|
-
/**
|
|
607
|
-
* Check if a file is selected
|
|
608
|
-
*/
|
|
609
|
-
hasFile() {
|
|
610
|
-
return this._file !== null;
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
/**
|
|
614
|
-
* Get preview URL
|
|
615
|
-
*/
|
|
616
|
-
getPreviewUrl() {
|
|
617
|
-
return this._previewUrl;
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
/**
|
|
621
|
-
* Clear the file input
|
|
622
|
-
*/
|
|
623
|
-
clear() {
|
|
624
|
-
this._file = null;
|
|
625
|
-
this._previewUrl = null;
|
|
626
|
-
this.originalInput.value = '';
|
|
627
|
-
|
|
628
|
-
// Check if we should show default or blank
|
|
629
|
-
if (this.cfg.defaultImage) {
|
|
630
|
-
this._showPreview(this.cfg.defaultImage, null, true);
|
|
631
|
-
this.elements.reset.classList.add('hidden');
|
|
632
|
-
} else {
|
|
633
|
-
this._showPlaceholder();
|
|
634
|
-
}
|
|
635
|
-
|
|
636
|
-
this._hideError();
|
|
637
|
-
this._emit('clear');
|
|
638
|
-
this._emit('change', { file: null });
|
|
639
|
-
}
|
|
640
|
-
|
|
641
|
-
/**
|
|
642
|
-
* Reset to original/old value
|
|
643
|
-
*/
|
|
644
|
-
reset() {
|
|
645
|
-
this._file = null;
|
|
646
|
-
this._previewUrl = null;
|
|
647
|
-
this.originalInput.value = '';
|
|
648
|
-
|
|
649
|
-
const originalUrl = this.cfg.oldValue || this.cfg.defaultImage;
|
|
650
|
-
|
|
651
|
-
if (originalUrl) {
|
|
652
|
-
this._showPreview(originalUrl, null, true);
|
|
653
|
-
this.elements.reset.classList.add('hidden');
|
|
654
|
-
} else {
|
|
655
|
-
this._showPlaceholder();
|
|
656
|
-
}
|
|
657
|
-
|
|
658
|
-
this._hideError();
|
|
659
|
-
this._emit('reset');
|
|
660
|
-
this._emit('change', { file: null });
|
|
661
|
-
}
|
|
662
|
-
|
|
663
|
-
/**
|
|
664
|
-
* Set a file programmatically
|
|
665
|
-
*/
|
|
666
|
-
setFile(file) {
|
|
667
|
-
if (file instanceof File) {
|
|
668
|
-
this._processFile(file);
|
|
669
|
-
}
|
|
670
|
-
}
|
|
671
|
-
|
|
672
|
-
/**
|
|
673
|
-
* Set preview URL directly (for existing files)
|
|
674
|
-
*/
|
|
675
|
-
setPreview(url) {
|
|
676
|
-
if (url) {
|
|
677
|
-
this._showPreview(url, null, true);
|
|
678
|
-
this._previewUrl = url;
|
|
679
|
-
}
|
|
680
|
-
}
|
|
681
|
-
|
|
682
|
-
/**
|
|
683
|
-
* Upload file via AJAX
|
|
684
|
-
*/
|
|
685
|
-
async upload() {
|
|
686
|
-
if (!this._file || !this.cfg.uploadUrl) return null;
|
|
687
|
-
|
|
688
|
-
this._isUploading = true;
|
|
689
|
-
this._emit('uploadStart', { file: this._file });
|
|
690
|
-
|
|
691
|
-
const formData = new FormData();
|
|
692
|
-
const fieldName = this.cfg.uploadFieldName || this.originalInput.name || 'file';
|
|
693
|
-
formData.append(fieldName, this._file);
|
|
694
|
-
|
|
695
|
-
try {
|
|
696
|
-
const response = await this._sendRequest(formData);
|
|
697
|
-
this._isUploading = false;
|
|
698
|
-
this._hideProgress();
|
|
699
|
-
|
|
700
|
-
if (response.ok) {
|
|
701
|
-
this._emit('uploadSuccess', { response: response.data });
|
|
702
|
-
return response.data;
|
|
703
|
-
} else {
|
|
704
|
-
throw new Error(response.data?.message || this.cfg.translations.uploadError);
|
|
705
|
-
}
|
|
706
|
-
} catch (error) {
|
|
707
|
-
this._isUploading = false;
|
|
708
|
-
this._hideProgress();
|
|
709
|
-
this._showError(error.message || this.cfg.translations.uploadError);
|
|
710
|
-
this._emit('uploadError', { error });
|
|
711
|
-
return null;
|
|
712
|
-
}
|
|
713
|
-
}
|
|
714
|
-
|
|
715
|
-
/**
|
|
716
|
-
* Remove file via AJAX
|
|
717
|
-
*/
|
|
718
|
-
async remove() {
|
|
719
|
-
if (!this.cfg.removeUrl) return false;
|
|
720
|
-
|
|
721
|
-
try {
|
|
722
|
-
const response = await this._sendRequest(null, 'DELETE', this.cfg.removeUrl);
|
|
723
|
-
|
|
724
|
-
if (response.ok) {
|
|
725
|
-
this._emit('removeSuccess', { response: response.data });
|
|
726
|
-
return true;
|
|
727
|
-
} else {
|
|
728
|
-
throw new Error(response.data?.message || 'Removal failed');
|
|
729
|
-
}
|
|
730
|
-
} catch (error) {
|
|
731
|
-
this._showError(error.message || 'Removal failed');
|
|
732
|
-
this._emit('removeError', { error });
|
|
733
|
-
return false;
|
|
734
|
-
}
|
|
735
|
-
}
|
|
736
|
-
|
|
737
|
-
async _sendRequest(formData = null, method = null, url = null) {
|
|
738
|
-
const headers = {
|
|
739
|
-
'Accept': 'application/json',
|
|
740
|
-
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]')?.content || ''
|
|
741
|
-
};
|
|
742
|
-
|
|
743
|
-
// Use XMLHttpRequest for progress tracking
|
|
744
|
-
return new Promise((resolve, reject) => {
|
|
745
|
-
const xhr = new XMLHttpRequest();
|
|
746
|
-
|
|
747
|
-
if (formData) {
|
|
748
|
-
xhr.upload.addEventListener('progress', (e) => {
|
|
749
|
-
if (e.lengthComputable && this.cfg.showProgressBar) {
|
|
750
|
-
const percent = (e.loaded / e.total) * 100;
|
|
751
|
-
this._showProgress(percent);
|
|
752
|
-
this._emit('uploadProgress', { loaded: e.loaded, total: e.total, percent });
|
|
753
|
-
}
|
|
754
|
-
});
|
|
755
|
-
}
|
|
756
|
-
|
|
757
|
-
xhr.addEventListener('load', () => {
|
|
758
|
-
let data = {};
|
|
759
|
-
try { data = JSON.parse(xhr.responseText); } catch { }
|
|
760
|
-
resolve({
|
|
761
|
-
ok: xhr.status >= 200 && xhr.status < 300,
|
|
762
|
-
status: xhr.status,
|
|
763
|
-
data
|
|
764
|
-
});
|
|
765
|
-
});
|
|
766
|
-
|
|
767
|
-
xhr.addEventListener('error', () => reject(new Error('Network error')));
|
|
768
|
-
|
|
769
|
-
xhr.open(method || this.cfg.uploadMethod, url || this.cfg.uploadUrl);
|
|
770
|
-
for (const [key, value] of Object.entries(headers)) {
|
|
771
|
-
xhr.setRequestHeader(key, value);
|
|
772
|
-
}
|
|
773
|
-
xhr.send(formData);
|
|
774
|
-
});
|
|
775
|
-
}
|
|
776
|
-
|
|
777
|
-
/**
|
|
778
|
-
* Enable the uploader
|
|
779
|
-
*/
|
|
780
|
-
enable() {
|
|
781
|
-
this.originalInput.disabled = false;
|
|
782
|
-
this.elements.dropzone.classList.remove('opacity-50', 'pointer-events-none');
|
|
783
|
-
this._emit('enable');
|
|
784
|
-
}
|
|
785
|
-
|
|
786
|
-
/**
|
|
787
|
-
* Disable the uploader
|
|
788
|
-
*/
|
|
789
|
-
disable() {
|
|
790
|
-
this.originalInput.disabled = true;
|
|
791
|
-
this.elements.dropzone.classList.add('opacity-50', 'pointer-events-none');
|
|
792
|
-
this._emit('disable');
|
|
793
|
-
}
|
|
794
|
-
|
|
795
|
-
/**
|
|
796
|
-
* Subscribe to events
|
|
797
|
-
*/
|
|
798
|
-
on(event, handler) {
|
|
799
|
-
if (!this._listeners[event]) {
|
|
800
|
-
this._listeners[event] = new Set();
|
|
801
|
-
}
|
|
802
|
-
this._listeners[event].add(handler);
|
|
803
|
-
return this;
|
|
804
|
-
}
|
|
805
|
-
|
|
806
|
-
/**
|
|
807
|
-
* Unsubscribe from events
|
|
808
|
-
*/
|
|
809
|
-
off(event, handler) {
|
|
810
|
-
if (this._listeners[event]) {
|
|
811
|
-
if (handler) {
|
|
812
|
-
this._listeners[event].delete(handler);
|
|
813
|
-
} else {
|
|
814
|
-
this._listeners[event].clear();
|
|
815
|
-
}
|
|
816
|
-
}
|
|
817
|
-
return this;
|
|
818
|
-
}
|
|
819
|
-
|
|
820
|
-
/**
|
|
821
|
-
* Destroy instance
|
|
822
|
-
*/
|
|
823
|
-
destroy() {
|
|
824
|
-
// Remove event listeners
|
|
825
|
-
this.originalInput.removeEventListener('change', this._boundHandlers.onInputChange);
|
|
826
|
-
this.elements.dropzone.removeEventListener('click', this._boundHandlers.onDropzoneClick);
|
|
827
|
-
this.elements.dropzone.removeEventListener('dragover', this._boundHandlers.onDragOver);
|
|
828
|
-
this.elements.dropzone.removeEventListener('dragleave', this._boundHandlers.onDragLeave);
|
|
829
|
-
this.elements.dropzone.removeEventListener('drop', this._boundHandlers.onDrop);
|
|
830
|
-
this.elements.browse.removeEventListener('click', this._boundHandlers.onBrowse);
|
|
831
|
-
this.elements.reset.removeEventListener('click', this._boundHandlers.onReset);
|
|
832
|
-
this.elements.remove.removeEventListener('click', this._boundHandlers.onRemove);
|
|
833
|
-
|
|
834
|
-
// Show original input
|
|
835
|
-
this.originalInput.style.display = '';
|
|
836
|
-
this.originalInput.classList.remove('ds-upload-input');
|
|
837
|
-
|
|
838
|
-
// Remove container
|
|
839
|
-
this.container.remove();
|
|
840
|
-
|
|
841
|
-
// Clear state
|
|
842
|
-
this._listeners = {};
|
|
843
|
-
DSUpload.instances.delete(this.instanceId);
|
|
844
|
-
delete this.wrapper.dataset.dsUploadId;
|
|
845
|
-
|
|
846
|
-
this._emit('destroy');
|
|
847
|
-
}
|
|
848
|
-
|
|
849
|
-
// ==================== UTILITIES ====================
|
|
850
|
-
|
|
851
|
-
_emit(event, detail = {}) {
|
|
852
|
-
(this._listeners[event] || new Set()).forEach(fn => {
|
|
853
|
-
try { fn(detail); } catch (err) { console.warn('DSUpload event error:', err); }
|
|
854
|
-
});
|
|
855
|
-
|
|
856
|
-
this.wrapper.dispatchEvent(new CustomEvent(`dsupload:${event}`, {
|
|
857
|
-
bubbles: true,
|
|
858
|
-
detail: { instance: this, ...detail }
|
|
859
|
-
}));
|
|
860
|
-
}
|
|
861
|
-
|
|
862
|
-
_formatSize(bytes) {
|
|
863
|
-
if (bytes === 0) return '0 Bytes';
|
|
864
|
-
|
|
865
|
-
const k = 1024;
|
|
866
|
-
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
|
867
|
-
|
|
868
|
-
if (this.cfg.sizeUnit !== 'auto') {
|
|
869
|
-
const i = sizes.indexOf(this.cfg.sizeUnit);
|
|
870
|
-
if (i > 0) {
|
|
871
|
-
return (bytes / Math.pow(k, i)).toFixed(2) + ' ' + sizes[i];
|
|
872
|
-
}
|
|
873
|
-
}
|
|
874
|
-
|
|
875
|
-
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
876
|
-
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
|
877
|
-
}
|
|
878
|
-
}
|
|
879
|
-
|
|
880
|
-
// Auto-init on DOM ready
|
|
881
|
-
if (typeof document !== 'undefined') {
|
|
882
|
-
if (document.readyState === 'loading') {
|
|
883
|
-
document.addEventListener('DOMContentLoaded', () => DSUpload.initAll());
|
|
884
|
-
} else {
|
|
885
|
-
DSUpload.initAll();
|
|
886
|
-
}
|
|
887
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* DSUpload
|
|
3
|
+
*
|
|
4
|
+
* A comprehensive file upload component with:
|
|
5
|
+
* - Image/file preview
|
|
6
|
+
* - Progress bar support
|
|
7
|
+
* - File type validation
|
|
8
|
+
* - File size validation
|
|
9
|
+
* - Old value / default image support
|
|
10
|
+
* - Reset functionality
|
|
11
|
+
* - Drag and drop support
|
|
12
|
+
* - Full event system
|
|
13
|
+
*/
|
|
14
|
+
export class DSUpload {
|
|
15
|
+
static instances = new Map();
|
|
16
|
+
static instanceCounter = 0;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Default Icons
|
|
20
|
+
*/
|
|
21
|
+
static icons = {
|
|
22
|
+
upload: `<svg class="w-8 h-8" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"/></svg>`,
|
|
23
|
+
file: `<svg class="w-12 h-12" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/></svg>`,
|
|
24
|
+
image: `<svg class="w-12 h-12" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"/></svg>`,
|
|
25
|
+
close: `<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/></svg>`,
|
|
26
|
+
reset: `<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/></svg>`,
|
|
27
|
+
check: `<svg class="w-5 h-5 text-success" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/></svg>`
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Default configuration
|
|
32
|
+
*/
|
|
33
|
+
static defaults = {
|
|
34
|
+
// Preview options
|
|
35
|
+
preview: true, // Enable preview
|
|
36
|
+
previewMaxHeight: '200px', // Max height for preview image
|
|
37
|
+
defaultImage: null, // Default/placeholder image URL
|
|
38
|
+
oldValue: null, // Old/current file URL (for edit forms)
|
|
39
|
+
|
|
40
|
+
// File validation
|
|
41
|
+
accept: '*', // Accepted file types (e.g., 'image/*', '.pdf,.doc')
|
|
42
|
+
maxSize: 5 * 1024 * 1024, // Max file size in bytes (default 5MB)
|
|
43
|
+
minSize: 0, // Min file size in bytes
|
|
44
|
+
|
|
45
|
+
// UI options
|
|
46
|
+
showProgressBar: true, // Show upload progress bar
|
|
47
|
+
showFileInfo: true, // Show file name and size
|
|
48
|
+
showResetButton: true, // Show reset button when file selected
|
|
49
|
+
showRemoveButton: true, // Show remove/clear button
|
|
50
|
+
dropzone: true, // Enable drag and drop
|
|
51
|
+
dropzoneText: 'Drop file here or click to upload',
|
|
52
|
+
browseText: 'Browse',
|
|
53
|
+
|
|
54
|
+
// Styling
|
|
55
|
+
wrapperClass: '',
|
|
56
|
+
previewClass: '',
|
|
57
|
+
dropzoneClass: '',
|
|
58
|
+
|
|
59
|
+
// Behavior
|
|
60
|
+
autoUpload: false, // Auto upload on select (requires uploadUrl)
|
|
61
|
+
uploadUrl: null, // URL for AJAX upload
|
|
62
|
+
removeUrl: null, // URL for AJAX removal (DELETE request)
|
|
63
|
+
uploadMethod: 'POST', // Upload HTTP method
|
|
64
|
+
uploadFieldName: null, // Override form field name for upload
|
|
65
|
+
|
|
66
|
+
// Size display
|
|
67
|
+
sizeUnit: 'auto', // 'auto', 'KB', 'MB', 'GB'
|
|
68
|
+
|
|
69
|
+
// Translations
|
|
70
|
+
translations: {
|
|
71
|
+
dropzone: 'Drop file here or click to upload',
|
|
72
|
+
browse: 'Browse',
|
|
73
|
+
remove: 'Remove',
|
|
74
|
+
reset: 'Reset',
|
|
75
|
+
fileTooBig: 'File is too large. Maximum size is {max}',
|
|
76
|
+
fileTooSmall: 'File is too small. Minimum size is {min}',
|
|
77
|
+
invalidType: 'Invalid file type. Accepted types: {types}',
|
|
78
|
+
uploadError: 'Upload failed',
|
|
79
|
+
uploading: 'Uploading...'
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* @param {string|HTMLElement} selector - Input element or selector
|
|
85
|
+
* @param {Object} config - Configuration options
|
|
86
|
+
*/
|
|
87
|
+
constructor(selector, config = {}) {
|
|
88
|
+
this.instanceId = `ds-upload-${++DSUpload.instanceCounter}`;
|
|
89
|
+
|
|
90
|
+
// Find the input element
|
|
91
|
+
const el = typeof selector === 'string' ? document.querySelector(selector) : selector;
|
|
92
|
+
|
|
93
|
+
if (!el) {
|
|
94
|
+
throw new Error('DSUpload: Element not found.');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Store original input
|
|
98
|
+
this.originalInput = el.tagName === 'INPUT' ? el : el.querySelector('input[type="file"]');
|
|
99
|
+
if (!this.originalInput) {
|
|
100
|
+
throw new Error('DSUpload: No file input found.');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Find or create wrapper
|
|
104
|
+
this.wrapper = el.closest('.form-group') || el.parentElement;
|
|
105
|
+
|
|
106
|
+
// Merge config with data attributes
|
|
107
|
+
this.cfg = this._buildConfig(config);
|
|
108
|
+
|
|
109
|
+
// State
|
|
110
|
+
this._file = null;
|
|
111
|
+
this._previewUrl = null;
|
|
112
|
+
this._isUploading = false;
|
|
113
|
+
this._progress = 0;
|
|
114
|
+
this._originalValue = this.cfg.oldValue || this.cfg.defaultImage;
|
|
115
|
+
|
|
116
|
+
// Event listeners
|
|
117
|
+
this._listeners = {};
|
|
118
|
+
this._boundHandlers = {};
|
|
119
|
+
|
|
120
|
+
// Initialize
|
|
121
|
+
this._init();
|
|
122
|
+
|
|
123
|
+
// Register instance
|
|
124
|
+
DSUpload.instances.set(this.instanceId, this);
|
|
125
|
+
this.wrapper.dataset.dsUploadId = this.instanceId;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Static factory
|
|
130
|
+
*/
|
|
131
|
+
static create(selector, config = {}) {
|
|
132
|
+
return new DSUpload(selector, config);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Get instance by element
|
|
137
|
+
*/
|
|
138
|
+
static getInstance(element) {
|
|
139
|
+
const el = typeof element === 'string' ? document.querySelector(element) : element;
|
|
140
|
+
if (!el) return null;
|
|
141
|
+
const wrapper = el.closest('[data-ds-upload-id]');
|
|
142
|
+
if (!wrapper) return null;
|
|
143
|
+
return DSUpload.instances.get(wrapper.dataset.dsUploadId);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Auto-initialize all elements with [data-ds-upload]
|
|
148
|
+
*/
|
|
149
|
+
static initAll(selector = '[data-ds-upload]') {
|
|
150
|
+
document.querySelectorAll(selector).forEach(el => {
|
|
151
|
+
if (!el.closest('[data-ds-upload-id]')) {
|
|
152
|
+
new DSUpload(el);
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// ==================== INITIALIZATION ====================
|
|
158
|
+
|
|
159
|
+
_buildConfig(userConfig) {
|
|
160
|
+
const dataConfig = this._parseDataAttributes();
|
|
161
|
+
return { ...DSUpload.defaults, ...dataConfig, ...userConfig };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
_parseDataAttributes() {
|
|
165
|
+
const data = this.originalInput.dataset;
|
|
166
|
+
const config = {};
|
|
167
|
+
|
|
168
|
+
if (data.preview !== undefined) config.preview = data.preview !== 'false';
|
|
169
|
+
if (data.defaultImage) config.defaultImage = data.defaultImage;
|
|
170
|
+
if (data.fallbackImage) config.fallbackImage = data.fallbackImage;
|
|
171
|
+
if (data.oldValue) config.oldValue = data.oldValue;
|
|
172
|
+
if (data.accept) config.accept = data.accept;
|
|
173
|
+
if (data.maxSize) config.maxSize = parseInt(data.maxSize, 10);
|
|
174
|
+
if (data.minSize) config.minSize = parseInt(data.minSize, 10);
|
|
175
|
+
if (data.dropzone !== undefined) config.dropzone = data.dropzone !== 'false';
|
|
176
|
+
if (data.showProgressBar !== undefined) config.showProgressBar = data.showProgressBar !== 'false';
|
|
177
|
+
if (data.uploadUrl) config.uploadUrl = data.uploadUrl;
|
|
178
|
+
if (data.removeUrl) config.removeUrl = data.removeUrl;
|
|
179
|
+
|
|
180
|
+
return config;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
_init() {
|
|
184
|
+
this._buildDOM();
|
|
185
|
+
this._cacheElements();
|
|
186
|
+
this._bindEvents();
|
|
187
|
+
this._setInitialPreview();
|
|
188
|
+
|
|
189
|
+
if (this.cfg.accept && this.cfg.accept !== '*') {
|
|
190
|
+
this.originalInput.setAttribute('accept', this.cfg.accept);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
_buildDOM() {
|
|
195
|
+
// Hide original input
|
|
196
|
+
this.originalInput.style.display = 'none';
|
|
197
|
+
this.originalInput.classList.add('ds-upload-input');
|
|
198
|
+
|
|
199
|
+
// Create upload UI
|
|
200
|
+
const container = document.createElement('div');
|
|
201
|
+
container.className = `ds-upload-container relative ${this.cfg.wrapperClass}`;
|
|
202
|
+
container.innerHTML = `
|
|
203
|
+
<!-- Dropzone / Upload Area -->
|
|
204
|
+
<div class="ds-upload-dropzone border-2 border-dashed border-base-300 rounded-sm p-3 pb-1 text-center cursor-pointer transition-all hover:border-primary hover:bg-base-200/50 ${this.cfg.dropzoneClass}"
|
|
205
|
+
data-ds-upload-dropzone>
|
|
206
|
+
<div class="ds-upload-placeholder flex flex-col items-center gap-2" data-ds-upload-placeholder>
|
|
207
|
+
<span class="text-base-content/40">${DSUpload.icons.upload}</span>
|
|
208
|
+
<p class="text-sm text-base-content/60">${this.cfg.translations.dropzone}</p>
|
|
209
|
+
<button type="button" class="btn btn-primary btn-sm mt-2" data-ds-upload-browse>
|
|
210
|
+
${this.cfg.translations.browse}
|
|
211
|
+
</button>
|
|
212
|
+
</div>
|
|
213
|
+
|
|
214
|
+
<!-- Preview Area -->
|
|
215
|
+
<div class="ds-upload-preview hidden" data-ds-upload-preview>
|
|
216
|
+
<div class="relative inline-block">
|
|
217
|
+
<img src="" alt="Preview"
|
|
218
|
+
class="max-h-[${this.cfg.previewMaxHeight}] max-w-full rounded-lg object-contain mx-auto ${this.cfg.previewClass}"
|
|
219
|
+
data-ds-upload-preview-img>
|
|
220
|
+
<div class="ds-upload-file-preview hidden flex-col items-center gap-2" data-ds-upload-file-preview>
|
|
221
|
+
<span class="text-base-content/40">${DSUpload.icons.file}</span>
|
|
222
|
+
<span class="text-sm font-medium" data-ds-upload-filename></span>
|
|
223
|
+
</div>
|
|
224
|
+
</div>
|
|
225
|
+
|
|
226
|
+
<!-- File Info -->
|
|
227
|
+
<div class="ds-upload-info mt-3 text-center" data-ds-upload-info>
|
|
228
|
+
<p class="text-sm font-medium text-base-content" data-ds-upload-name></p>
|
|
229
|
+
<p class="text-xs text-base-content/60" data-ds-upload-size></p>
|
|
230
|
+
</div>
|
|
231
|
+
|
|
232
|
+
<!-- Action Buttons -->
|
|
233
|
+
<div class="ds-upload-actions flex justify-center gap-2 mt-3">
|
|
234
|
+
<button type="button" class="btn btn-ghost btn-sm gap-1" data-ds-upload-reset title="${this.cfg.translations.reset}">
|
|
235
|
+
${DSUpload.icons.reset}
|
|
236
|
+
<span>${this.cfg.translations.reset}</span>
|
|
237
|
+
</button>
|
|
238
|
+
<button type="button" class="btn btn-ghost btn-sm text-error gap-1" data-ds-upload-remove title="${this.cfg.translations.remove}">
|
|
239
|
+
${DSUpload.icons.close}
|
|
240
|
+
<span>${this.cfg.translations.remove}</span>
|
|
241
|
+
</button>
|
|
242
|
+
</div>
|
|
243
|
+
</div>
|
|
244
|
+
|
|
245
|
+
<!-- Progress Bar -->
|
|
246
|
+
<div class="ds-upload-progress hidden mt-4" data-ds-upload-progress>
|
|
247
|
+
<div class="flex justify-between text-xs text-base-content/60 mb-1">
|
|
248
|
+
<span>${this.cfg.translations.uploading}</span>
|
|
249
|
+
<span data-ds-upload-progress-text>0%</span>
|
|
250
|
+
</div>
|
|
251
|
+
<progress class="progress progress-primary w-full" value="0" max="100" data-ds-upload-progress-bar></progress>
|
|
252
|
+
</div>
|
|
253
|
+
</div>
|
|
254
|
+
|
|
255
|
+
<!-- Error Message -->
|
|
256
|
+
<div class="ds-upload-error hidden mt-2" data-ds-upload-error>
|
|
257
|
+
<p class="text-sm text-error flex items-center gap-1">
|
|
258
|
+
<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
259
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
|
260
|
+
</svg>
|
|
261
|
+
<span data-ds-upload-error-text></span>
|
|
262
|
+
</p>
|
|
263
|
+
</div>
|
|
264
|
+
`;
|
|
265
|
+
|
|
266
|
+
// Insert after original input
|
|
267
|
+
this.originalInput.insertAdjacentElement('afterend', container);
|
|
268
|
+
this.container = container;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
_cacheElements() {
|
|
272
|
+
this.elements = {
|
|
273
|
+
dropzone: this.container.querySelector('[data-ds-upload-dropzone]'),
|
|
274
|
+
placeholder: this.container.querySelector('[data-ds-upload-placeholder]'),
|
|
275
|
+
preview: this.container.querySelector('[data-ds-upload-preview]'),
|
|
276
|
+
previewImg: this.container.querySelector('[data-ds-upload-preview-img]'),
|
|
277
|
+
filePreview: this.container.querySelector('[data-ds-upload-file-preview]'),
|
|
278
|
+
filename: this.container.querySelector('[data-ds-upload-filename]'),
|
|
279
|
+
info: this.container.querySelector('[data-ds-upload-info]'),
|
|
280
|
+
name: this.container.querySelector('[data-ds-upload-name]'),
|
|
281
|
+
size: this.container.querySelector('[data-ds-upload-size]'),
|
|
282
|
+
reset: this.container.querySelector('[data-ds-upload-reset]'),
|
|
283
|
+
remove: this.container.querySelector('[data-ds-upload-remove]'),
|
|
284
|
+
browse: this.container.querySelector('[data-ds-upload-browse]'),
|
|
285
|
+
progress: this.container.querySelector('[data-ds-upload-progress]'),
|
|
286
|
+
progressBar: this.container.querySelector('[data-ds-upload-progress-bar]'),
|
|
287
|
+
progressText: this.container.querySelector('[data-ds-upload-progress-text]'),
|
|
288
|
+
error: this.container.querySelector('[data-ds-upload-error]'),
|
|
289
|
+
errorText: this.container.querySelector('[data-ds-upload-error-text]')
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
_bindEvents() {
|
|
294
|
+
this._boundHandlers = {
|
|
295
|
+
onInputChange: this._onInputChange.bind(this),
|
|
296
|
+
onDropzoneClick: this._onDropzoneClick.bind(this),
|
|
297
|
+
onDragOver: this._onDragOver.bind(this),
|
|
298
|
+
onDragLeave: this._onDragLeave.bind(this),
|
|
299
|
+
onDrop: this._onDrop.bind(this),
|
|
300
|
+
onReset: this._onReset.bind(this),
|
|
301
|
+
onRemove: this._onRemove.bind(this),
|
|
302
|
+
onBrowse: this._onBrowse.bind(this)
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
// Input change
|
|
306
|
+
this.originalInput.addEventListener('change', this._boundHandlers.onInputChange);
|
|
307
|
+
|
|
308
|
+
// Dropzone click
|
|
309
|
+
this.elements.dropzone.addEventListener('click', this._boundHandlers.onDropzoneClick);
|
|
310
|
+
|
|
311
|
+
// Drag and drop
|
|
312
|
+
if (this.cfg.dropzone) {
|
|
313
|
+
this.elements.dropzone.addEventListener('dragover', this._boundHandlers.onDragOver);
|
|
314
|
+
this.elements.dropzone.addEventListener('dragleave', this._boundHandlers.onDragLeave);
|
|
315
|
+
this.elements.dropzone.addEventListener('drop', this._boundHandlers.onDrop);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Buttons
|
|
319
|
+
this.elements.browse.addEventListener('click', this._boundHandlers.onBrowse);
|
|
320
|
+
this.elements.reset.addEventListener('click', this._boundHandlers.onReset);
|
|
321
|
+
this.elements.remove.addEventListener('click', this._boundHandlers.onRemove);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
_setInitialPreview() {
|
|
325
|
+
const initialUrl = this.cfg.oldValue || this.cfg.defaultImage;
|
|
326
|
+
// Treat removeUrl as proof of a real value existing on the server
|
|
327
|
+
const hasRealValue = !!this.cfg.oldValue || !!this.cfg.removeUrl;
|
|
328
|
+
|
|
329
|
+
if (initialUrl && this.cfg.preview) {
|
|
330
|
+
this._showPreview(initialUrl, null, true, hasRealValue);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Hide reset button initially (no new file selected yet)
|
|
334
|
+
this.elements.reset.classList.add('hidden');
|
|
335
|
+
|
|
336
|
+
// Hide remove button if only showing default placeholder logic handled in _showPreview
|
|
337
|
+
// based on hasRealValue passed above.
|
|
338
|
+
|
|
339
|
+
this._emit('ready', { hasInitialValue: !!initialUrl, hasRealValue });
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// ==================== EVENT HANDLERS ====================
|
|
343
|
+
|
|
344
|
+
_onInputChange(e) {
|
|
345
|
+
const file = e.target.files[0];
|
|
346
|
+
if (file) {
|
|
347
|
+
this._processFile(file);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
_onDropzoneClick(e) {
|
|
352
|
+
// Don't trigger if clicking buttons
|
|
353
|
+
if (e.target.closest('button')) return;
|
|
354
|
+
this.originalInput.click();
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
_onBrowse(e) {
|
|
358
|
+
e.stopPropagation();
|
|
359
|
+
this.originalInput.click();
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
_onDragOver(e) {
|
|
363
|
+
e.preventDefault();
|
|
364
|
+
e.stopPropagation();
|
|
365
|
+
this.elements.dropzone.classList.add('border-primary', 'bg-primary/10');
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
_onDragLeave(e) {
|
|
369
|
+
e.preventDefault();
|
|
370
|
+
e.stopPropagation();
|
|
371
|
+
this.elements.dropzone.classList.remove('border-primary', 'bg-primary/10');
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
_onDrop(e) {
|
|
375
|
+
e.preventDefault();
|
|
376
|
+
e.stopPropagation();
|
|
377
|
+
this.elements.dropzone.classList.remove('border-primary', 'bg-primary/10');
|
|
378
|
+
|
|
379
|
+
const file = e.dataTransfer.files[0];
|
|
380
|
+
if (file) {
|
|
381
|
+
this._processFile(file);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
_onReset(e) {
|
|
386
|
+
e.stopPropagation();
|
|
387
|
+
this.reset();
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
async _onRemove(e) {
|
|
391
|
+
e.stopPropagation();
|
|
392
|
+
|
|
393
|
+
// If we have a removeUrl and a real value (implied by removeUrl presence), try to delete via AJAX
|
|
394
|
+
// validation: ensure we don't have a new file selected (this._file should be null)
|
|
395
|
+
if (this.cfg.removeUrl && !this._file) {
|
|
396
|
+
if (confirm('Are you sure you want to remove this file?')) {
|
|
397
|
+
const success = await this.remove();
|
|
398
|
+
if (success) {
|
|
399
|
+
// File is deleted from server.
|
|
400
|
+
// If we have a fallback image, set it as the new default
|
|
401
|
+
// Otherwise clear default so we show placeholder
|
|
402
|
+
this.cfg.defaultImage = this.cfg.fallbackImage || null;
|
|
403
|
+
|
|
404
|
+
this.cfg.oldValue = null;
|
|
405
|
+
// Also clear removeUrl to prevent trying to delete again
|
|
406
|
+
this.cfg.removeUrl = null;
|
|
407
|
+
|
|
408
|
+
this.clear();
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
} else {
|
|
412
|
+
this.clear();
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// ==================== FILE PROCESSING ====================
|
|
417
|
+
|
|
418
|
+
_processFile(file) {
|
|
419
|
+
// Clear previous errors
|
|
420
|
+
this._hideError();
|
|
421
|
+
|
|
422
|
+
// Validate file type
|
|
423
|
+
if (!this._validateType(file)) {
|
|
424
|
+
this._showError(
|
|
425
|
+
this.cfg.translations.invalidType.replace('{types}', this.cfg.accept)
|
|
426
|
+
);
|
|
427
|
+
this._emit('error', { error: 'invalid_type', file });
|
|
428
|
+
return false;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// Validate file size
|
|
432
|
+
if (!this._validateSize(file)) {
|
|
433
|
+
if (file.size > this.cfg.maxSize) {
|
|
434
|
+
this._showError(
|
|
435
|
+
this.cfg.translations.fileTooBig.replace('{max}', this._formatSize(this.cfg.maxSize))
|
|
436
|
+
);
|
|
437
|
+
} else {
|
|
438
|
+
this._showError(
|
|
439
|
+
this.cfg.translations.fileTooSmall.replace('{min}', this._formatSize(this.cfg.minSize))
|
|
440
|
+
);
|
|
441
|
+
}
|
|
442
|
+
this._emit('error', { error: 'invalid_size', file });
|
|
443
|
+
return false;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// Store file
|
|
447
|
+
this._file = file;
|
|
448
|
+
|
|
449
|
+
// Update input
|
|
450
|
+
const dt = new DataTransfer();
|
|
451
|
+
dt.items.add(file);
|
|
452
|
+
this.originalInput.files = dt.files;
|
|
453
|
+
|
|
454
|
+
// Show preview
|
|
455
|
+
if (this.cfg.preview) {
|
|
456
|
+
if (this._isImage(file)) {
|
|
457
|
+
const reader = new FileReader();
|
|
458
|
+
reader.onload = (e) => {
|
|
459
|
+
this._showPreview(e.target.result, file);
|
|
460
|
+
};
|
|
461
|
+
reader.readAsDataURL(file);
|
|
462
|
+
} else {
|
|
463
|
+
this._showPreview(null, file);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// Auto upload
|
|
468
|
+
if (this.cfg.autoUpload && this.cfg.uploadUrl) {
|
|
469
|
+
this.upload();
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
this._emit('select', { file });
|
|
473
|
+
return true;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
_validateType(file) {
|
|
477
|
+
if (!this.cfg.accept || this.cfg.accept === '*') return true;
|
|
478
|
+
|
|
479
|
+
const accept = this.cfg.accept.toLowerCase().split(',').map(t => t.trim());
|
|
480
|
+
const fileType = file.type.toLowerCase();
|
|
481
|
+
const fileExt = '.' + file.name.split('.').pop().toLowerCase();
|
|
482
|
+
|
|
483
|
+
return accept.some(type => {
|
|
484
|
+
if (type.endsWith('/*')) {
|
|
485
|
+
// Wildcard like image/*
|
|
486
|
+
return fileType.startsWith(type.replace('/*', '/'));
|
|
487
|
+
}
|
|
488
|
+
if (type.startsWith('.')) {
|
|
489
|
+
// Extension like .pdf
|
|
490
|
+
return fileExt === type;
|
|
491
|
+
}
|
|
492
|
+
// Exact MIME type
|
|
493
|
+
return fileType === type;
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
_validateSize(file) {
|
|
498
|
+
if (this.cfg.maxSize && file.size > this.cfg.maxSize) return false;
|
|
499
|
+
if (this.cfg.minSize && file.size < this.cfg.minSize) return false;
|
|
500
|
+
return true;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
_isImage(file) {
|
|
504
|
+
return file.type.startsWith('image/');
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// ==================== UI UPDATES ====================
|
|
508
|
+
|
|
509
|
+
_showPreview(url, file, isInitial = false, hasRealValue = false) {
|
|
510
|
+
this.elements.placeholder.classList.add('hidden');
|
|
511
|
+
this.elements.preview.classList.remove('hidden');
|
|
512
|
+
|
|
513
|
+
if (url && (isInitial || (file && this._isImage(file)))) {
|
|
514
|
+
// Image preview
|
|
515
|
+
this.elements.previewImg.src = url;
|
|
516
|
+
this.elements.previewImg.classList.remove('hidden');
|
|
517
|
+
this.elements.filePreview.classList.add('hidden');
|
|
518
|
+
this._previewUrl = url;
|
|
519
|
+
} else {
|
|
520
|
+
// File icon preview
|
|
521
|
+
this.elements.previewImg.classList.add('hidden');
|
|
522
|
+
this.elements.filePreview.classList.remove('hidden');
|
|
523
|
+
this.elements.filePreview.classList.add('flex');
|
|
524
|
+
if (file) {
|
|
525
|
+
this.elements.filename.textContent = file.name;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// File info
|
|
530
|
+
if (file && this.cfg.showFileInfo) {
|
|
531
|
+
this.elements.info.classList.remove('hidden');
|
|
532
|
+
this.elements.name.textContent = file.name;
|
|
533
|
+
this.elements.size.textContent = this._formatSize(file.size);
|
|
534
|
+
} else if (isInitial) {
|
|
535
|
+
this.elements.info.classList.add('hidden');
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// Show/hide buttons based on context
|
|
539
|
+
if (file) {
|
|
540
|
+
// New file selected - show only reset (to go back to original)
|
|
541
|
+
this.elements.reset.classList.remove('hidden');
|
|
542
|
+
this.elements.remove.classList.add('hidden');
|
|
543
|
+
} else if (isInitial && hasRealValue) {
|
|
544
|
+
// Initial load with real value (oldValue from DB) - show remove only
|
|
545
|
+
this.elements.reset.classList.add('hidden');
|
|
546
|
+
if (this.cfg.showRemoveButton) {
|
|
547
|
+
this.elements.remove.classList.remove('hidden');
|
|
548
|
+
}
|
|
549
|
+
} else if (isInitial) {
|
|
550
|
+
// Initial load with default placeholder only - hide both
|
|
551
|
+
this.elements.reset.classList.add('hidden');
|
|
552
|
+
this.elements.remove.classList.add('hidden');
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
_showPlaceholder() {
|
|
557
|
+
this.elements.placeholder.classList.remove('hidden');
|
|
558
|
+
this.elements.preview.classList.add('hidden');
|
|
559
|
+
this.elements.info.classList.add('hidden');
|
|
560
|
+
this.elements.reset.classList.add('hidden');
|
|
561
|
+
this.elements.remove.classList.add('hidden');
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
_showProgress(percent) {
|
|
565
|
+
this.elements.progress.classList.remove('hidden');
|
|
566
|
+
this.elements.progressBar.value = percent;
|
|
567
|
+
this.elements.progressText.textContent = `${Math.round(percent)}%`;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
_hideProgress() {
|
|
571
|
+
this.elements.progress.classList.add('hidden');
|
|
572
|
+
this.elements.progressBar.value = 0;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
_showError(message) {
|
|
576
|
+
this.elements.error.classList.remove('hidden');
|
|
577
|
+
this.elements.errorText.textContent = message;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
_hideError() {
|
|
581
|
+
this.elements.error.classList.add('hidden');
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// ==================== PUBLIC API ====================
|
|
585
|
+
|
|
586
|
+
/**
|
|
587
|
+
* Get the current file
|
|
588
|
+
*/
|
|
589
|
+
getFile() {
|
|
590
|
+
return this._file;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
/**
|
|
594
|
+
* Get file info
|
|
595
|
+
*/
|
|
596
|
+
getFileInfo() {
|
|
597
|
+
if (!this._file) return null;
|
|
598
|
+
return {
|
|
599
|
+
name: this._file.name,
|
|
600
|
+
size: this._file.size,
|
|
601
|
+
type: this._file.type,
|
|
602
|
+
formattedSize: this._formatSize(this._file.size)
|
|
603
|
+
};
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
/**
|
|
607
|
+
* Check if a file is selected
|
|
608
|
+
*/
|
|
609
|
+
hasFile() {
|
|
610
|
+
return this._file !== null;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
/**
|
|
614
|
+
* Get preview URL
|
|
615
|
+
*/
|
|
616
|
+
getPreviewUrl() {
|
|
617
|
+
return this._previewUrl;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
/**
|
|
621
|
+
* Clear the file input
|
|
622
|
+
*/
|
|
623
|
+
clear() {
|
|
624
|
+
this._file = null;
|
|
625
|
+
this._previewUrl = null;
|
|
626
|
+
this.originalInput.value = '';
|
|
627
|
+
|
|
628
|
+
// Check if we should show default or blank
|
|
629
|
+
if (this.cfg.defaultImage) {
|
|
630
|
+
this._showPreview(this.cfg.defaultImage, null, true);
|
|
631
|
+
this.elements.reset.classList.add('hidden');
|
|
632
|
+
} else {
|
|
633
|
+
this._showPlaceholder();
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
this._hideError();
|
|
637
|
+
this._emit('clear');
|
|
638
|
+
this._emit('change', { file: null });
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
/**
|
|
642
|
+
* Reset to original/old value
|
|
643
|
+
*/
|
|
644
|
+
reset() {
|
|
645
|
+
this._file = null;
|
|
646
|
+
this._previewUrl = null;
|
|
647
|
+
this.originalInput.value = '';
|
|
648
|
+
|
|
649
|
+
const originalUrl = this.cfg.oldValue || this.cfg.defaultImage;
|
|
650
|
+
|
|
651
|
+
if (originalUrl) {
|
|
652
|
+
this._showPreview(originalUrl, null, true);
|
|
653
|
+
this.elements.reset.classList.add('hidden');
|
|
654
|
+
} else {
|
|
655
|
+
this._showPlaceholder();
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
this._hideError();
|
|
659
|
+
this._emit('reset');
|
|
660
|
+
this._emit('change', { file: null });
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
/**
|
|
664
|
+
* Set a file programmatically
|
|
665
|
+
*/
|
|
666
|
+
setFile(file) {
|
|
667
|
+
if (file instanceof File) {
|
|
668
|
+
this._processFile(file);
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
/**
|
|
673
|
+
* Set preview URL directly (for existing files)
|
|
674
|
+
*/
|
|
675
|
+
setPreview(url) {
|
|
676
|
+
if (url) {
|
|
677
|
+
this._showPreview(url, null, true);
|
|
678
|
+
this._previewUrl = url;
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
/**
|
|
683
|
+
* Upload file via AJAX
|
|
684
|
+
*/
|
|
685
|
+
async upload() {
|
|
686
|
+
if (!this._file || !this.cfg.uploadUrl) return null;
|
|
687
|
+
|
|
688
|
+
this._isUploading = true;
|
|
689
|
+
this._emit('uploadStart', { file: this._file });
|
|
690
|
+
|
|
691
|
+
const formData = new FormData();
|
|
692
|
+
const fieldName = this.cfg.uploadFieldName || this.originalInput.name || 'file';
|
|
693
|
+
formData.append(fieldName, this._file);
|
|
694
|
+
|
|
695
|
+
try {
|
|
696
|
+
const response = await this._sendRequest(formData);
|
|
697
|
+
this._isUploading = false;
|
|
698
|
+
this._hideProgress();
|
|
699
|
+
|
|
700
|
+
if (response.ok) {
|
|
701
|
+
this._emit('uploadSuccess', { response: response.data });
|
|
702
|
+
return response.data;
|
|
703
|
+
} else {
|
|
704
|
+
throw new Error(response.data?.message || this.cfg.translations.uploadError);
|
|
705
|
+
}
|
|
706
|
+
} catch (error) {
|
|
707
|
+
this._isUploading = false;
|
|
708
|
+
this._hideProgress();
|
|
709
|
+
this._showError(error.message || this.cfg.translations.uploadError);
|
|
710
|
+
this._emit('uploadError', { error });
|
|
711
|
+
return null;
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
/**
|
|
716
|
+
* Remove file via AJAX
|
|
717
|
+
*/
|
|
718
|
+
async remove() {
|
|
719
|
+
if (!this.cfg.removeUrl) return false;
|
|
720
|
+
|
|
721
|
+
try {
|
|
722
|
+
const response = await this._sendRequest(null, 'DELETE', this.cfg.removeUrl);
|
|
723
|
+
|
|
724
|
+
if (response.ok) {
|
|
725
|
+
this._emit('removeSuccess', { response: response.data });
|
|
726
|
+
return true;
|
|
727
|
+
} else {
|
|
728
|
+
throw new Error(response.data?.message || 'Removal failed');
|
|
729
|
+
}
|
|
730
|
+
} catch (error) {
|
|
731
|
+
this._showError(error.message || 'Removal failed');
|
|
732
|
+
this._emit('removeError', { error });
|
|
733
|
+
return false;
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
async _sendRequest(formData = null, method = null, url = null) {
|
|
738
|
+
const headers = {
|
|
739
|
+
'Accept': 'application/json',
|
|
740
|
+
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]')?.content || ''
|
|
741
|
+
};
|
|
742
|
+
|
|
743
|
+
// Use XMLHttpRequest for progress tracking
|
|
744
|
+
return new Promise((resolve, reject) => {
|
|
745
|
+
const xhr = new XMLHttpRequest();
|
|
746
|
+
|
|
747
|
+
if (formData) {
|
|
748
|
+
xhr.upload.addEventListener('progress', (e) => {
|
|
749
|
+
if (e.lengthComputable && this.cfg.showProgressBar) {
|
|
750
|
+
const percent = (e.loaded / e.total) * 100;
|
|
751
|
+
this._showProgress(percent);
|
|
752
|
+
this._emit('uploadProgress', { loaded: e.loaded, total: e.total, percent });
|
|
753
|
+
}
|
|
754
|
+
});
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
xhr.addEventListener('load', () => {
|
|
758
|
+
let data = {};
|
|
759
|
+
try { data = JSON.parse(xhr.responseText); } catch { }
|
|
760
|
+
resolve({
|
|
761
|
+
ok: xhr.status >= 200 && xhr.status < 300,
|
|
762
|
+
status: xhr.status,
|
|
763
|
+
data
|
|
764
|
+
});
|
|
765
|
+
});
|
|
766
|
+
|
|
767
|
+
xhr.addEventListener('error', () => reject(new Error('Network error')));
|
|
768
|
+
|
|
769
|
+
xhr.open(method || this.cfg.uploadMethod, url || this.cfg.uploadUrl);
|
|
770
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
771
|
+
xhr.setRequestHeader(key, value);
|
|
772
|
+
}
|
|
773
|
+
xhr.send(formData);
|
|
774
|
+
});
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
/**
|
|
778
|
+
* Enable the uploader
|
|
779
|
+
*/
|
|
780
|
+
enable() {
|
|
781
|
+
this.originalInput.disabled = false;
|
|
782
|
+
this.elements.dropzone.classList.remove('opacity-50', 'pointer-events-none');
|
|
783
|
+
this._emit('enable');
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
/**
|
|
787
|
+
* Disable the uploader
|
|
788
|
+
*/
|
|
789
|
+
disable() {
|
|
790
|
+
this.originalInput.disabled = true;
|
|
791
|
+
this.elements.dropzone.classList.add('opacity-50', 'pointer-events-none');
|
|
792
|
+
this._emit('disable');
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
/**
|
|
796
|
+
* Subscribe to events
|
|
797
|
+
*/
|
|
798
|
+
on(event, handler) {
|
|
799
|
+
if (!this._listeners[event]) {
|
|
800
|
+
this._listeners[event] = new Set();
|
|
801
|
+
}
|
|
802
|
+
this._listeners[event].add(handler);
|
|
803
|
+
return this;
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
/**
|
|
807
|
+
* Unsubscribe from events
|
|
808
|
+
*/
|
|
809
|
+
off(event, handler) {
|
|
810
|
+
if (this._listeners[event]) {
|
|
811
|
+
if (handler) {
|
|
812
|
+
this._listeners[event].delete(handler);
|
|
813
|
+
} else {
|
|
814
|
+
this._listeners[event].clear();
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
return this;
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
/**
|
|
821
|
+
* Destroy instance
|
|
822
|
+
*/
|
|
823
|
+
destroy() {
|
|
824
|
+
// Remove event listeners
|
|
825
|
+
this.originalInput.removeEventListener('change', this._boundHandlers.onInputChange);
|
|
826
|
+
this.elements.dropzone.removeEventListener('click', this._boundHandlers.onDropzoneClick);
|
|
827
|
+
this.elements.dropzone.removeEventListener('dragover', this._boundHandlers.onDragOver);
|
|
828
|
+
this.elements.dropzone.removeEventListener('dragleave', this._boundHandlers.onDragLeave);
|
|
829
|
+
this.elements.dropzone.removeEventListener('drop', this._boundHandlers.onDrop);
|
|
830
|
+
this.elements.browse.removeEventListener('click', this._boundHandlers.onBrowse);
|
|
831
|
+
this.elements.reset.removeEventListener('click', this._boundHandlers.onReset);
|
|
832
|
+
this.elements.remove.removeEventListener('click', this._boundHandlers.onRemove);
|
|
833
|
+
|
|
834
|
+
// Show original input
|
|
835
|
+
this.originalInput.style.display = '';
|
|
836
|
+
this.originalInput.classList.remove('ds-upload-input');
|
|
837
|
+
|
|
838
|
+
// Remove container
|
|
839
|
+
this.container.remove();
|
|
840
|
+
|
|
841
|
+
// Clear state
|
|
842
|
+
this._listeners = {};
|
|
843
|
+
DSUpload.instances.delete(this.instanceId);
|
|
844
|
+
delete this.wrapper.dataset.dsUploadId;
|
|
845
|
+
|
|
846
|
+
this._emit('destroy');
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
// ==================== UTILITIES ====================
|
|
850
|
+
|
|
851
|
+
_emit(event, detail = {}) {
|
|
852
|
+
(this._listeners[event] || new Set()).forEach(fn => {
|
|
853
|
+
try { fn(detail); } catch (err) { console.warn('DSUpload event error:', err); }
|
|
854
|
+
});
|
|
855
|
+
|
|
856
|
+
this.wrapper.dispatchEvent(new CustomEvent(`dsupload:${event}`, {
|
|
857
|
+
bubbles: true,
|
|
858
|
+
detail: { instance: this, ...detail }
|
|
859
|
+
}));
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
_formatSize(bytes) {
|
|
863
|
+
if (bytes === 0) return '0 Bytes';
|
|
864
|
+
|
|
865
|
+
const k = 1024;
|
|
866
|
+
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
|
867
|
+
|
|
868
|
+
if (this.cfg.sizeUnit !== 'auto') {
|
|
869
|
+
const i = sizes.indexOf(this.cfg.sizeUnit);
|
|
870
|
+
if (i > 0) {
|
|
871
|
+
return (bytes / Math.pow(k, i)).toFixed(2) + ' ' + sizes[i];
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
876
|
+
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
// Auto-init on DOM ready
|
|
881
|
+
if (typeof document !== 'undefined') {
|
|
882
|
+
if (document.readyState === 'loading') {
|
|
883
|
+
document.addEventListener('DOMContentLoaded', () => DSUpload.initAll());
|
|
884
|
+
} else {
|
|
885
|
+
DSUpload.initAll();
|
|
886
|
+
}
|
|
887
|
+
}
|