@keenmate/pure-admin-core 2.9.0-rc03 → 2.9.0-rc05
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/README.md +55 -13
- package/dist/css/main.css +554 -7
- package/package.json +3 -1
- package/snippets/buttons.html +51 -0
- package/snippets/cards.html +132 -47
- package/snippets/comparison.html +26 -22
- package/snippets/manifest.json +180 -115
- package/snippets/range-group.html +125 -0
- package/snippets/splitter.html +44 -38
- package/snippets/statistics.html +31 -0
- package/src/js/btn-split-auto-absorb.js +327 -0
- package/src/js/command-palette.js +472 -0
- package/src/js/file-selector.js +1275 -0
- package/src/js/internal/logging.js +121 -0
- package/src/js/logic-tree-renderer.js +303 -0
- package/src/js/modal-dialogs.js +460 -0
- package/src/js/overflow.js +371 -0
- package/src/js/pa-stat-fit.js +184 -0
- package/src/js/range-group.js +663 -0
- package/src/js/search-autocomplete-v2.js +907 -0
- package/src/js/search-autocomplete.js +434 -0
- package/src/js/settings-panel.js +245 -0
- package/src/js/split-button.js +141 -0
- package/src/js/splitter.js +1323 -0
- package/src/js/toast-service.js +302 -0
- package/src/js/tooltips-popovers.js +275 -0
- package/src/js/virtual-scroll.js +143 -0
- package/src/js/virtual-textbox.js +803 -0
- package/src/scss/_core.scss +7 -0
- package/src/scss/core-components/_buttons.scss +44 -0
- package/src/scss/core-components/_cards.scss +95 -6
- package/src/scss/core-components/_overflow.scss +50 -0
- package/src/scss/core-components/_range-group.scss +474 -0
- package/src/scss/core-components/_statistics.scss +163 -0
- package/src/scss/variables/_components.scss +41 -2
|
@@ -0,0 +1,1275 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure Admin - File Selector Component
|
|
3
|
+
* Handles file input styling, drag & drop, previews, and upload progress
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
(function() {
|
|
7
|
+
'use strict';
|
|
8
|
+
|
|
9
|
+
// ============================================================================
|
|
10
|
+
// Helper Functions
|
|
11
|
+
// ============================================================================
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Format bytes to human-readable size
|
|
15
|
+
*/
|
|
16
|
+
function formatFileSize(bytes) {
|
|
17
|
+
if (bytes === 0) return '0 Bytes';
|
|
18
|
+
const k = 1024;
|
|
19
|
+
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
|
20
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
21
|
+
return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Get file type from extension
|
|
26
|
+
*/
|
|
27
|
+
function getFileType(filename) {
|
|
28
|
+
const ext = filename.split('.').pop().toLowerCase();
|
|
29
|
+
const types = {
|
|
30
|
+
// Images
|
|
31
|
+
jpg: 'image', jpeg: 'image', png: 'image', gif: 'image', webp: 'image', svg: 'image',
|
|
32
|
+
// Documents
|
|
33
|
+
pdf: 'pdf', doc: 'doc', docx: 'docx', txt: 'txt',
|
|
34
|
+
// Spreadsheets
|
|
35
|
+
xls: 'xls', xlsx: 'xlsx', csv: 'xls',
|
|
36
|
+
// Archives
|
|
37
|
+
zip: 'zip', rar: 'rar', '7z': 'zip', tar: 'zip', gz: 'zip',
|
|
38
|
+
// Media
|
|
39
|
+
mp4: 'video', avi: 'video', mov: 'video', wmv: 'video',
|
|
40
|
+
mp3: 'audio', wav: 'audio', flac: 'audio', ogg: 'audio'
|
|
41
|
+
};
|
|
42
|
+
return types[ext] || 'default';
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Get file icon class based on type
|
|
47
|
+
*/
|
|
48
|
+
function getFileIconClass(filename) {
|
|
49
|
+
const type = getFileType(filename);
|
|
50
|
+
return `pa-file-icon pa-file-icon--${type}`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Check if file is an image
|
|
55
|
+
*/
|
|
56
|
+
function isImageFile(file) {
|
|
57
|
+
return file.type.startsWith('image/');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Create file preview URL using FileReader
|
|
62
|
+
*/
|
|
63
|
+
function createImagePreview(file, callback) {
|
|
64
|
+
if (!isImageFile(file)) {
|
|
65
|
+
callback(null);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const reader = new FileReader();
|
|
70
|
+
reader.onload = (e) => callback(e.target.result);
|
|
71
|
+
reader.onerror = () => callback(null);
|
|
72
|
+
reader.readAsDataURL(file);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ============================================================================
|
|
76
|
+
// Section 1 & 2: Basic File Input (Single & Multiple)
|
|
77
|
+
// ============================================================================
|
|
78
|
+
|
|
79
|
+
function initBasicFileInput(inputId) {
|
|
80
|
+
const input = document.getElementById(inputId);
|
|
81
|
+
if (!input) return;
|
|
82
|
+
|
|
83
|
+
const button = input.nextElementSibling;
|
|
84
|
+
const filenameDisplay = button.nextElementSibling;
|
|
85
|
+
|
|
86
|
+
button.addEventListener('click', () => input.click());
|
|
87
|
+
|
|
88
|
+
input.addEventListener('change', (e) => {
|
|
89
|
+
const files = e.target.files;
|
|
90
|
+
if (files.length === 0) {
|
|
91
|
+
filenameDisplay.textContent = 'No file chosen';
|
|
92
|
+
} else if (files.length === 1) {
|
|
93
|
+
filenameDisplay.textContent = files[0].name;
|
|
94
|
+
} else {
|
|
95
|
+
filenameDisplay.textContent = `${files.length} files selected`;
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ============================================================================
|
|
101
|
+
// Section 3 & 4: Drag & Drop Zones
|
|
102
|
+
// ============================================================================
|
|
103
|
+
|
|
104
|
+
function initDropzone(dropzoneId, listId = null) {
|
|
105
|
+
const dropzone = document.getElementById(dropzoneId);
|
|
106
|
+
if (!dropzone) return;
|
|
107
|
+
|
|
108
|
+
const input = dropzone.querySelector('.pa-file-dropzone__input');
|
|
109
|
+
const listContainer = listId ? document.getElementById(listId) : null;
|
|
110
|
+
const filesInside = dropzone.dataset.filesInside === 'true';
|
|
111
|
+
let selectedFiles = [];
|
|
112
|
+
|
|
113
|
+
// Click to browse
|
|
114
|
+
dropzone.addEventListener('click', (e) => {
|
|
115
|
+
if (e.target === input) return; // Prevent double-trigger when clicking invisible input
|
|
116
|
+
if (e.target.classList.contains('pa-file-item__remove')) return;
|
|
117
|
+
if (e.target.classList.contains('pa-file-dropzone__file-card-remove')) return;
|
|
118
|
+
input.click();
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
// Drag events
|
|
122
|
+
dropzone.addEventListener('dragover', (e) => {
|
|
123
|
+
e.preventDefault();
|
|
124
|
+
dropzone.classList.add('pa-file-dropzone--active');
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
dropzone.addEventListener('dragleave', (e) => {
|
|
128
|
+
e.preventDefault();
|
|
129
|
+
if (e.target === dropzone) {
|
|
130
|
+
dropzone.classList.remove('pa-file-dropzone--active');
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
dropzone.addEventListener('drop', (e) => {
|
|
135
|
+
e.preventDefault();
|
|
136
|
+
dropzone.classList.remove('pa-file-dropzone--active');
|
|
137
|
+
|
|
138
|
+
const files = Array.from(e.dataTransfer.files);
|
|
139
|
+
handleFiles(files);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// Input change
|
|
143
|
+
input.addEventListener('change', (e) => {
|
|
144
|
+
const files = Array.from(e.target.files);
|
|
145
|
+
handleFiles(files);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
function handleFiles(files) {
|
|
149
|
+
if (!input.multiple) {
|
|
150
|
+
selectedFiles = files.slice(0, 1);
|
|
151
|
+
} else {
|
|
152
|
+
selectedFiles = [...selectedFiles, ...files];
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Render based on mode
|
|
156
|
+
if (filesInside) {
|
|
157
|
+
renderFilesInside(selectedFiles);
|
|
158
|
+
} else if (!input.multiple && !listContainer && selectedFiles.length > 0) {
|
|
159
|
+
showSelectedFileInDropzone(selectedFiles[0]);
|
|
160
|
+
} else if (listContainer) {
|
|
161
|
+
renderFileList(selectedFiles, listContainer);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function showSelectedFileInDropzone(file) {
|
|
166
|
+
const content = dropzone.querySelector('.pa-file-dropzone__content');
|
|
167
|
+
content.innerHTML = `
|
|
168
|
+
<div class="pa-file-dropzone__icon">✓</div>
|
|
169
|
+
<div class="pa-file-dropzone__text">
|
|
170
|
+
<strong>${escapeHtml(file.name)}</strong>
|
|
171
|
+
</div>
|
|
172
|
+
<div class="pa-file-dropzone__hint">
|
|
173
|
+
${formatFileSize(file.size)} • Click to change
|
|
174
|
+
</div>
|
|
175
|
+
`;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function renderFilesInside(files) {
|
|
179
|
+
// Remove existing content
|
|
180
|
+
const existingContent = dropzone.querySelector('.pa-file-dropzone__content');
|
|
181
|
+
const existingPrompt = dropzone.querySelector('.pa-file-dropzone__drop-prompt');
|
|
182
|
+
const existingGrid = dropzone.querySelector('.pa-file-dropzone__files-grid');
|
|
183
|
+
|
|
184
|
+
if (existingContent) existingContent.remove();
|
|
185
|
+
|
|
186
|
+
// Create or update drop prompt
|
|
187
|
+
if (!existingPrompt) {
|
|
188
|
+
const prompt = document.createElement('div');
|
|
189
|
+
prompt.className = 'pa-file-dropzone__drop-prompt';
|
|
190
|
+
prompt.innerHTML = '<span class="pa-file-dropzone__drop-prompt-icon">📎</span> Drop files here or click to browse';
|
|
191
|
+
dropzone.insertBefore(prompt, dropzone.firstChild);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Create or get files grid
|
|
195
|
+
let filesGrid = existingGrid;
|
|
196
|
+
if (!filesGrid) {
|
|
197
|
+
filesGrid = document.createElement('div');
|
|
198
|
+
filesGrid.className = 'pa-file-dropzone__files-grid';
|
|
199
|
+
dropzone.appendChild(filesGrid);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Render files
|
|
203
|
+
if (files.length === 0) {
|
|
204
|
+
filesGrid.innerHTML = '';
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
filesGrid.innerHTML = files.map((file, index) => `
|
|
209
|
+
<div class="pa-file-dropzone__file-card">
|
|
210
|
+
<div class="pa-file-dropzone__file-card-icon">${getFileIconEmoji(file.name)}</div>
|
|
211
|
+
<div class="pa-file-dropzone__file-card-name" title="${escapeHtml(file.name)}">${escapeHtml(file.name)}</div>
|
|
212
|
+
<div class="pa-file-dropzone__file-card-size">${formatFileSize(file.size)}</div>
|
|
213
|
+
<button type="button" class="pa-file-dropzone__file-card-remove" data-index="${index}"></button>
|
|
214
|
+
</div>
|
|
215
|
+
`).join('');
|
|
216
|
+
|
|
217
|
+
// Attach remove handlers
|
|
218
|
+
filesGrid.querySelectorAll('.pa-file-dropzone__file-card-remove').forEach(btn => {
|
|
219
|
+
btn.addEventListener('click', (e) => {
|
|
220
|
+
e.stopPropagation();
|
|
221
|
+
const index = parseInt(btn.dataset.index);
|
|
222
|
+
selectedFiles.splice(index, 1);
|
|
223
|
+
renderFilesInside(selectedFiles);
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function getFileIconEmoji(filename) {
|
|
229
|
+
const type = getFileType(filename);
|
|
230
|
+
const icons = {
|
|
231
|
+
pdf: '📄',
|
|
232
|
+
doc: '📝', docx: '📝',
|
|
233
|
+
xls: '📊', xlsx: '📊',
|
|
234
|
+
zip: '🗜️', rar: '🗜️',
|
|
235
|
+
image: '🖼️',
|
|
236
|
+
video: '🎬',
|
|
237
|
+
audio: '🎵',
|
|
238
|
+
txt: '📃',
|
|
239
|
+
default: '📁'
|
|
240
|
+
};
|
|
241
|
+
return icons[type] || icons.default;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function renderFileList(files, container) {
|
|
245
|
+
if (files.length === 0) {
|
|
246
|
+
container.innerHTML = '';
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
container.innerHTML = files.map((file, index) => `
|
|
251
|
+
<div class="pa-file-item">
|
|
252
|
+
<div class="${getFileIconClass(file.name)}"></div>
|
|
253
|
+
<div class="pa-file-info">
|
|
254
|
+
<div class="pa-file-item__name">${escapeHtml(file.name)}</div>
|
|
255
|
+
<div class="pa-file-item__meta">${formatFileSize(file.size)}</div>
|
|
256
|
+
</div>
|
|
257
|
+
<button type="button" class="pa-file-item__remove" data-index="${index}"></button>
|
|
258
|
+
</div>
|
|
259
|
+
`).join('');
|
|
260
|
+
|
|
261
|
+
// Attach remove handlers
|
|
262
|
+
container.querySelectorAll('.pa-file-item__remove').forEach(btn => {
|
|
263
|
+
btn.addEventListener('click', (e) => {
|
|
264
|
+
e.stopPropagation();
|
|
265
|
+
const index = parseInt(btn.dataset.index);
|
|
266
|
+
selectedFiles.splice(index, 1);
|
|
267
|
+
renderFileList(selectedFiles, container);
|
|
268
|
+
});
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// ============================================================================
|
|
274
|
+
// Section 5: Image Preview with Thumbnails
|
|
275
|
+
// ============================================================================
|
|
276
|
+
|
|
277
|
+
function initImagePreview(dropzoneId, previewContainerId) {
|
|
278
|
+
const dropzone = document.getElementById(dropzoneId);
|
|
279
|
+
if (!dropzone) return;
|
|
280
|
+
|
|
281
|
+
const input = dropzone.querySelector('.pa-file-dropzone__input');
|
|
282
|
+
const previewContainer = document.getElementById(previewContainerId);
|
|
283
|
+
const filesInside = dropzone.dataset.filesInside === 'true';
|
|
284
|
+
let selectedFiles = [];
|
|
285
|
+
|
|
286
|
+
// Click to browse
|
|
287
|
+
dropzone.addEventListener('click', (e) => {
|
|
288
|
+
if (e.target === input) return; // Prevent double-trigger when clicking invisible input
|
|
289
|
+
if (e.target.classList.contains('pa-file-preview__remove')) return;
|
|
290
|
+
if (e.target.classList.contains('pa-file-dropzone__image-card-remove')) return;
|
|
291
|
+
input.click();
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
// Drag events
|
|
295
|
+
dropzone.addEventListener('dragover', (e) => {
|
|
296
|
+
e.preventDefault();
|
|
297
|
+
dropzone.classList.add('pa-file-dropzone--active');
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
dropzone.addEventListener('dragleave', (e) => {
|
|
301
|
+
e.preventDefault();
|
|
302
|
+
if (e.target === dropzone) {
|
|
303
|
+
dropzone.classList.remove('pa-file-dropzone--active');
|
|
304
|
+
}
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
dropzone.addEventListener('drop', (e) => {
|
|
308
|
+
e.preventDefault();
|
|
309
|
+
dropzone.classList.remove('pa-file-dropzone--active');
|
|
310
|
+
|
|
311
|
+
const files = Array.from(e.dataTransfer.files).filter(isImageFile);
|
|
312
|
+
handleFiles(files);
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
// Input change
|
|
316
|
+
input.addEventListener('change', (e) => {
|
|
317
|
+
const files = Array.from(e.target.files).filter(isImageFile);
|
|
318
|
+
handleFiles(files);
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
function handleFiles(files) {
|
|
322
|
+
selectedFiles = [...selectedFiles, ...files];
|
|
323
|
+
|
|
324
|
+
if (filesInside) {
|
|
325
|
+
renderImagesInside(selectedFiles);
|
|
326
|
+
} else {
|
|
327
|
+
renderPreviews(selectedFiles, previewContainer);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function renderImagesInside(files) {
|
|
332
|
+
// Remove existing content
|
|
333
|
+
const existingContent = dropzone.querySelector('.pa-file-dropzone__content');
|
|
334
|
+
const existingPrompt = dropzone.querySelector('.pa-file-dropzone__drop-prompt');
|
|
335
|
+
const existingGrid = dropzone.querySelector('.pa-file-dropzone__files-grid');
|
|
336
|
+
|
|
337
|
+
if (existingContent) existingContent.remove();
|
|
338
|
+
|
|
339
|
+
// Create or update drop prompt
|
|
340
|
+
if (!existingPrompt) {
|
|
341
|
+
const prompt = document.createElement('div');
|
|
342
|
+
prompt.className = 'pa-file-dropzone__drop-prompt';
|
|
343
|
+
prompt.innerHTML = '<span class="pa-file-dropzone__drop-prompt-icon">🖼️</span> Drop images here or click to browse';
|
|
344
|
+
dropzone.insertBefore(prompt, dropzone.firstChild);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Create or get files grid
|
|
348
|
+
let filesGrid = existingGrid;
|
|
349
|
+
if (!filesGrid) {
|
|
350
|
+
filesGrid = document.createElement('div');
|
|
351
|
+
filesGrid.className = 'pa-file-dropzone__files-grid';
|
|
352
|
+
dropzone.appendChild(filesGrid);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Render images
|
|
356
|
+
if (files.length === 0) {
|
|
357
|
+
filesGrid.innerHTML = '';
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
filesGrid.innerHTML = '';
|
|
362
|
+
files.forEach((file, index) => {
|
|
363
|
+
createImagePreview(file, (dataUrl) => {
|
|
364
|
+
if (!dataUrl) return;
|
|
365
|
+
|
|
366
|
+
const imageCard = document.createElement('div');
|
|
367
|
+
imageCard.className = 'pa-file-dropzone__image-card';
|
|
368
|
+
imageCard.innerHTML = `
|
|
369
|
+
<img src="${dataUrl}" alt="${escapeHtml(file.name)}">
|
|
370
|
+
<button type="button" class="pa-file-dropzone__image-card-remove" data-index="${index}"></button>
|
|
371
|
+
`;
|
|
372
|
+
|
|
373
|
+
filesGrid.appendChild(imageCard);
|
|
374
|
+
|
|
375
|
+
// Attach remove handler
|
|
376
|
+
imageCard.querySelector('.pa-file-dropzone__image-card-remove').addEventListener('click', (e) => {
|
|
377
|
+
e.stopPropagation();
|
|
378
|
+
selectedFiles.splice(index, 1);
|
|
379
|
+
renderImagesInside(selectedFiles);
|
|
380
|
+
});
|
|
381
|
+
});
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function renderPreviews(files, container) {
|
|
386
|
+
if (files.length === 0) {
|
|
387
|
+
container.innerHTML = '';
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
container.innerHTML = '';
|
|
392
|
+
files.forEach((file, index) => {
|
|
393
|
+
createImagePreview(file, (dataUrl) => {
|
|
394
|
+
if (!dataUrl) return;
|
|
395
|
+
|
|
396
|
+
const previewDiv = document.createElement('div');
|
|
397
|
+
previewDiv.className = 'pa-file-preview';
|
|
398
|
+
previewDiv.innerHTML = `
|
|
399
|
+
<img src="${dataUrl}" alt="${escapeHtml(file.name)}" class="pa-file-preview__image">
|
|
400
|
+
<button type="button" class="pa-file-preview__remove" data-index="${index}"></button>
|
|
401
|
+
<div class="pa-file-preview__overlay">${escapeHtml(file.name)}</div>
|
|
402
|
+
`;
|
|
403
|
+
|
|
404
|
+
container.appendChild(previewDiv);
|
|
405
|
+
|
|
406
|
+
// Attach remove handler
|
|
407
|
+
previewDiv.querySelector('.pa-file-preview__remove').addEventListener('click', (e) => {
|
|
408
|
+
e.stopPropagation();
|
|
409
|
+
selectedFiles.splice(index, 1);
|
|
410
|
+
renderPreviews(selectedFiles, container);
|
|
411
|
+
});
|
|
412
|
+
});
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// ============================================================================
|
|
418
|
+
// Section 6: File List with Details
|
|
419
|
+
// ============================================================================
|
|
420
|
+
|
|
421
|
+
function initDetailedFileList(dropzoneId, listId) {
|
|
422
|
+
const dropzone = document.getElementById(dropzoneId);
|
|
423
|
+
if (!dropzone) return;
|
|
424
|
+
|
|
425
|
+
const input = dropzone.querySelector('.pa-file-dropzone__input');
|
|
426
|
+
const listContainer = document.getElementById(listId);
|
|
427
|
+
const filesInside = dropzone.dataset.filesInside === 'true';
|
|
428
|
+
let selectedFiles = [];
|
|
429
|
+
|
|
430
|
+
// Click to browse
|
|
431
|
+
dropzone.addEventListener('click', (e) => {
|
|
432
|
+
if (e.target === input) return; // Prevent double-trigger when clicking invisible input
|
|
433
|
+
if (e.target.classList.contains('pa-file-item__remove')) return;
|
|
434
|
+
if (e.target.classList.contains('pa-file-dropzone__file-card-remove')) return;
|
|
435
|
+
input.click();
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
// Drag events
|
|
439
|
+
dropzone.addEventListener('dragover', (e) => {
|
|
440
|
+
e.preventDefault();
|
|
441
|
+
dropzone.classList.add('pa-file-dropzone--active');
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
dropzone.addEventListener('dragleave', (e) => {
|
|
445
|
+
e.preventDefault();
|
|
446
|
+
if (e.target === dropzone) {
|
|
447
|
+
dropzone.classList.remove('pa-file-dropzone--active');
|
|
448
|
+
}
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
dropzone.addEventListener('drop', (e) => {
|
|
452
|
+
e.preventDefault();
|
|
453
|
+
dropzone.classList.remove('pa-file-dropzone--active');
|
|
454
|
+
|
|
455
|
+
const files = Array.from(e.dataTransfer.files);
|
|
456
|
+
handleFiles(files);
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
// Input change
|
|
460
|
+
input.addEventListener('change', (e) => {
|
|
461
|
+
const files = Array.from(e.target.files);
|
|
462
|
+
handleFiles(files);
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
function handleFiles(files) {
|
|
466
|
+
selectedFiles = [...selectedFiles, ...files];
|
|
467
|
+
|
|
468
|
+
if (filesInside) {
|
|
469
|
+
renderFilesInsideDetailed(selectedFiles);
|
|
470
|
+
} else {
|
|
471
|
+
renderDetailedList(selectedFiles, listContainer);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function renderFilesInsideDetailed(files) {
|
|
476
|
+
// Remove existing content
|
|
477
|
+
const existingContent = dropzone.querySelector('.pa-file-dropzone__content');
|
|
478
|
+
const existingPrompt = dropzone.querySelector('.pa-file-dropzone__drop-prompt');
|
|
479
|
+
const existingGrid = dropzone.querySelector('.pa-file-dropzone__files-grid');
|
|
480
|
+
|
|
481
|
+
if (existingContent) existingContent.remove();
|
|
482
|
+
|
|
483
|
+
// Create or update drop prompt
|
|
484
|
+
if (!existingPrompt) {
|
|
485
|
+
const prompt = document.createElement('div');
|
|
486
|
+
prompt.className = 'pa-file-dropzone__drop-prompt';
|
|
487
|
+
prompt.innerHTML = '<span class="pa-file-dropzone__drop-prompt-icon">📁</span> Drop files here or click to browse';
|
|
488
|
+
dropzone.insertBefore(prompt, dropzone.firstChild);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// Create or get files grid
|
|
492
|
+
let filesGrid = existingGrid;
|
|
493
|
+
if (!filesGrid) {
|
|
494
|
+
filesGrid = document.createElement('div');
|
|
495
|
+
filesGrid.className = 'pa-file-dropzone__files-grid';
|
|
496
|
+
dropzone.appendChild(filesGrid);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// Render files
|
|
500
|
+
if (files.length === 0) {
|
|
501
|
+
filesGrid.innerHTML = '';
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
filesGrid.innerHTML = files.map((file, index) => {
|
|
506
|
+
const iconEmoji = getFileIconEmojiDetailed(file.name);
|
|
507
|
+
return `
|
|
508
|
+
<div class="pa-file-dropzone__file-card">
|
|
509
|
+
<div class="pa-file-dropzone__file-card-icon">${iconEmoji}</div>
|
|
510
|
+
<div class="pa-file-dropzone__file-card-name" title="${escapeHtml(file.name)}">${escapeHtml(file.name)}</div>
|
|
511
|
+
<div class="pa-file-dropzone__file-card-size">${formatFileSize(file.size)}</div>
|
|
512
|
+
<button type="button" class="pa-file-dropzone__file-card-remove" data-index="${index}"></button>
|
|
513
|
+
</div>
|
|
514
|
+
`;
|
|
515
|
+
}).join('');
|
|
516
|
+
|
|
517
|
+
// Attach remove handlers
|
|
518
|
+
filesGrid.querySelectorAll('.pa-file-dropzone__file-card-remove').forEach(btn => {
|
|
519
|
+
btn.addEventListener('click', (e) => {
|
|
520
|
+
e.stopPropagation();
|
|
521
|
+
const index = parseInt(btn.dataset.index);
|
|
522
|
+
selectedFiles.splice(index, 1);
|
|
523
|
+
renderFilesInsideDetailed(selectedFiles);
|
|
524
|
+
});
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
function getFileIconEmojiDetailed(filename) {
|
|
529
|
+
const type = getFileType(filename);
|
|
530
|
+
const icons = {
|
|
531
|
+
pdf: '📄',
|
|
532
|
+
doc: '📝', docx: '📝',
|
|
533
|
+
xls: '📊', xlsx: '📊',
|
|
534
|
+
zip: '🗜️', rar: '🗜️',
|
|
535
|
+
image: '🖼️',
|
|
536
|
+
video: '🎬',
|
|
537
|
+
audio: '🎵',
|
|
538
|
+
txt: '📃',
|
|
539
|
+
default: '📁'
|
|
540
|
+
};
|
|
541
|
+
return icons[type] || icons.default;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
function renderDetailedList(files, container) {
|
|
545
|
+
if (files.length === 0) {
|
|
546
|
+
container.innerHTML = '';
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
container.innerHTML = files.map((file, index) => `
|
|
551
|
+
<div class="pa-file-item">
|
|
552
|
+
<div class="pa-file-item__icon">
|
|
553
|
+
<div class="${getFileIconClass(file.name)}"></div>
|
|
554
|
+
</div>
|
|
555
|
+
<div class="pa-file-info">
|
|
556
|
+
<div class="pa-file-item__name">${escapeHtml(file.name)}</div>
|
|
557
|
+
<div class="pa-file-item__meta">
|
|
558
|
+
${formatFileSize(file.size)} • ${file.type || 'Unknown type'}
|
|
559
|
+
</div>
|
|
560
|
+
</div>
|
|
561
|
+
<button type="button" class="pa-file-item__remove" data-index="${index}"></button>
|
|
562
|
+
</div>
|
|
563
|
+
`).join('');
|
|
564
|
+
|
|
565
|
+
// Attach remove handlers
|
|
566
|
+
container.querySelectorAll('.pa-file-item__remove').forEach(btn => {
|
|
567
|
+
btn.addEventListener('click', (e) => {
|
|
568
|
+
e.stopPropagation();
|
|
569
|
+
const index = parseInt(btn.dataset.index);
|
|
570
|
+
selectedFiles.splice(index, 1);
|
|
571
|
+
renderDetailedList(selectedFiles, container);
|
|
572
|
+
});
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// ============================================================================
|
|
578
|
+
// Section 7: File Validation
|
|
579
|
+
// ============================================================================
|
|
580
|
+
|
|
581
|
+
function initValidatedDropzone(dropzoneId, errorId, successId) {
|
|
582
|
+
const dropzone = document.getElementById(dropzoneId);
|
|
583
|
+
if (!dropzone) return;
|
|
584
|
+
|
|
585
|
+
const input = dropzone.querySelector('.pa-file-dropzone__input');
|
|
586
|
+
const errorAlert = document.getElementById(errorId);
|
|
587
|
+
const successAlert = document.getElementById(successId);
|
|
588
|
+
const maxSize = parseInt(input.dataset.maxSize) || 2097152; // Default 2MB
|
|
589
|
+
const acceptTypes = input.accept.split(',').map(t => t.trim());
|
|
590
|
+
|
|
591
|
+
// Click to browse
|
|
592
|
+
dropzone.addEventListener('click', (e) => {
|
|
593
|
+
if (e.target === input) return; // Prevent double-trigger when clicking invisible input
|
|
594
|
+
input.click();
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
// Drag events
|
|
598
|
+
dropzone.addEventListener('dragover', (e) => {
|
|
599
|
+
e.preventDefault();
|
|
600
|
+
dropzone.classList.add('pa-file-dropzone--active');
|
|
601
|
+
});
|
|
602
|
+
|
|
603
|
+
dropzone.addEventListener('dragleave', (e) => {
|
|
604
|
+
e.preventDefault();
|
|
605
|
+
if (e.target === dropzone) {
|
|
606
|
+
dropzone.classList.remove('pa-file-dropzone--active');
|
|
607
|
+
}
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
dropzone.addEventListener('drop', (e) => {
|
|
611
|
+
e.preventDefault();
|
|
612
|
+
dropzone.classList.remove('pa-file-dropzone--active');
|
|
613
|
+
|
|
614
|
+
if (e.dataTransfer.files.length > 0) {
|
|
615
|
+
validateFile(e.dataTransfer.files[0]);
|
|
616
|
+
}
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
// Input change
|
|
620
|
+
input.addEventListener('change', (e) => {
|
|
621
|
+
if (e.target.files.length > 0) {
|
|
622
|
+
validateFile(e.target.files[0]);
|
|
623
|
+
}
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
function validateFile(file) {
|
|
627
|
+
// Hide previous messages
|
|
628
|
+
errorAlert.style.display = 'none';
|
|
629
|
+
successAlert.style.display = 'none';
|
|
630
|
+
|
|
631
|
+
// Check file size
|
|
632
|
+
if (file.size > maxSize) {
|
|
633
|
+
showError(`File is too large. Maximum size is ${formatFileSize(maxSize)}.`);
|
|
634
|
+
return;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
// Check file type
|
|
638
|
+
const fileType = file.type;
|
|
639
|
+
const isValidType = acceptTypes.some(type => {
|
|
640
|
+
if (type === fileType) return true;
|
|
641
|
+
if (type.endsWith('/*') && fileType.startsWith(type.replace('/*', '/'))) return true;
|
|
642
|
+
return false;
|
|
643
|
+
});
|
|
644
|
+
|
|
645
|
+
if (!isValidType) {
|
|
646
|
+
showError(`Invalid file type. Accepted types: ${acceptTypes.join(', ')}`);
|
|
647
|
+
return;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
// Validation passed
|
|
651
|
+
showSuccess(`✓ File validated: ${file.name} (${formatFileSize(file.size)})`);
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
function showError(message) {
|
|
655
|
+
errorAlert.textContent = message;
|
|
656
|
+
errorAlert.style.display = 'block';
|
|
657
|
+
input.value = ''; // Clear input
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
function showSuccess(message) {
|
|
661
|
+
successAlert.textContent = message;
|
|
662
|
+
successAlert.style.display = 'block';
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
// ============================================================================
|
|
667
|
+
// Section 8: Upload Progress
|
|
668
|
+
// ============================================================================
|
|
669
|
+
|
|
670
|
+
function initUploadProgress(dropzoneId, listId, buttonId) {
|
|
671
|
+
const dropzone = document.getElementById(dropzoneId);
|
|
672
|
+
if (!dropzone) return;
|
|
673
|
+
|
|
674
|
+
const input = dropzone.querySelector('.pa-file-dropzone__input');
|
|
675
|
+
const listContainer = document.getElementById(listId);
|
|
676
|
+
const uploadBtn = document.getElementById(buttonId);
|
|
677
|
+
const filesInside = dropzone.dataset.filesInside === 'true';
|
|
678
|
+
let selectedFiles = [];
|
|
679
|
+
|
|
680
|
+
// Click to browse
|
|
681
|
+
dropzone.addEventListener('click', (e) => {
|
|
682
|
+
if (e.target === input) return; // Prevent double-trigger when clicking invisible input
|
|
683
|
+
if (e.target.classList.contains('pa-file-item__remove')) return;
|
|
684
|
+
if (e.target.classList.contains('pa-file-dropzone__file-card-remove')) return;
|
|
685
|
+
input.click();
|
|
686
|
+
});
|
|
687
|
+
|
|
688
|
+
// Drag events
|
|
689
|
+
dropzone.addEventListener('dragover', (e) => {
|
|
690
|
+
e.preventDefault();
|
|
691
|
+
dropzone.classList.add('pa-file-dropzone--active');
|
|
692
|
+
});
|
|
693
|
+
|
|
694
|
+
dropzone.addEventListener('dragleave', (e) => {
|
|
695
|
+
e.preventDefault();
|
|
696
|
+
if (e.target === dropzone) {
|
|
697
|
+
dropzone.classList.remove('pa-file-dropzone--active');
|
|
698
|
+
}
|
|
699
|
+
});
|
|
700
|
+
|
|
701
|
+
dropzone.addEventListener('drop', (e) => {
|
|
702
|
+
e.preventDefault();
|
|
703
|
+
dropzone.classList.remove('pa-file-dropzone--active');
|
|
704
|
+
|
|
705
|
+
const files = Array.from(e.dataTransfer.files);
|
|
706
|
+
handleFiles(files);
|
|
707
|
+
});
|
|
708
|
+
|
|
709
|
+
// Input change
|
|
710
|
+
input.addEventListener('change', (e) => {
|
|
711
|
+
const files = Array.from(e.target.files);
|
|
712
|
+
handleFiles(files);
|
|
713
|
+
});
|
|
714
|
+
|
|
715
|
+
function handleFiles(files) {
|
|
716
|
+
selectedFiles = [...selectedFiles, ...files];
|
|
717
|
+
renderFileListWithProgress(selectedFiles, listContainer, 0);
|
|
718
|
+
uploadBtn.style.display = selectedFiles.length > 0 ? 'inline-flex' : 'none';
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
function renderFileListWithProgress(files, container, progress = 0) {
|
|
722
|
+
if (files.length === 0) {
|
|
723
|
+
container.innerHTML = '';
|
|
724
|
+
return;
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
container.innerHTML = files.map((file, index) => {
|
|
728
|
+
const statusClass = progress >= 100 ? 'pa-file-progress__status--complete' : '';
|
|
729
|
+
const statusText = progress >= 100 ? 'Complete' : progress > 0 ? `${progress}%` : 'Pending';
|
|
730
|
+
|
|
731
|
+
return `
|
|
732
|
+
<div class="pa-file-item">
|
|
733
|
+
<div class="pa-file-item__icon">
|
|
734
|
+
<div class="${getFileIconClass(file.name)}"></div>
|
|
735
|
+
</div>
|
|
736
|
+
<div class="pa-file-info">
|
|
737
|
+
<div class="pa-file-item__name">${escapeHtml(file.name)}</div>
|
|
738
|
+
<div class="pa-file-item__meta">${formatFileSize(file.size)}</div>
|
|
739
|
+
${progress > 0 ? `
|
|
740
|
+
<div class="pa-file-progress">
|
|
741
|
+
<div class="pa-file-progress__bar">
|
|
742
|
+
<div class="pa-file-progress__fill" style="width: ${progress}%"></div>
|
|
743
|
+
</div>
|
|
744
|
+
<div class="pa-file-progress__text">
|
|
745
|
+
<span class="pa-file-progress__status ${statusClass}">${statusText}</span>
|
|
746
|
+
</div>
|
|
747
|
+
</div>
|
|
748
|
+
` : ''}
|
|
749
|
+
</div>
|
|
750
|
+
${progress === 0 ? `<button type="button" class="pa-file-item__remove" data-index="${index}"></button>` : ''}
|
|
751
|
+
</div>
|
|
752
|
+
`;
|
|
753
|
+
}).join('');
|
|
754
|
+
|
|
755
|
+
// Attach remove handlers (only if not uploading)
|
|
756
|
+
if (progress === 0) {
|
|
757
|
+
container.querySelectorAll('.pa-file-item__remove').forEach(btn => {
|
|
758
|
+
btn.addEventListener('click', (e) => {
|
|
759
|
+
e.stopPropagation();
|
|
760
|
+
const index = parseInt(btn.dataset.index);
|
|
761
|
+
selectedFiles.splice(index, 1);
|
|
762
|
+
renderFileListWithProgress(selectedFiles, container, 0);
|
|
763
|
+
uploadBtn.style.display = selectedFiles.length > 0 ? 'inline-flex' : 'none';
|
|
764
|
+
});
|
|
765
|
+
});
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
// Upload button click
|
|
770
|
+
uploadBtn.addEventListener('click', () => {
|
|
771
|
+
uploadBtn.disabled = true;
|
|
772
|
+
simulateUpload();
|
|
773
|
+
});
|
|
774
|
+
|
|
775
|
+
function simulateUpload() {
|
|
776
|
+
let progress = 0;
|
|
777
|
+
const interval = setInterval(() => {
|
|
778
|
+
progress += Math.random() * 15;
|
|
779
|
+
if (progress >= 100) {
|
|
780
|
+
progress = 100;
|
|
781
|
+
clearInterval(interval);
|
|
782
|
+
uploadBtn.disabled = false;
|
|
783
|
+
}
|
|
784
|
+
renderFileListWithProgress(selectedFiles, listContainer, Math.floor(progress));
|
|
785
|
+
}, 200);
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
// ============================================================================
|
|
790
|
+
// Section 9: Ultra-Compact Modal Mode
|
|
791
|
+
// ============================================================================
|
|
792
|
+
|
|
793
|
+
function initCompactModal(dropzoneId, buttonId = null) {
|
|
794
|
+
const dropzone = document.getElementById(dropzoneId);
|
|
795
|
+
if (!dropzone) return;
|
|
796
|
+
|
|
797
|
+
const input = dropzone.querySelector('.pa-file-dropzone__input');
|
|
798
|
+
const uploadBtn = buttonId ? document.getElementById(buttonId) : null;
|
|
799
|
+
let selectedFiles = [];
|
|
800
|
+
let fileStates = []; // Track progress and status for each file
|
|
801
|
+
let currentUploadIndex = -1;
|
|
802
|
+
let modal = null;
|
|
803
|
+
|
|
804
|
+
// Click to browse
|
|
805
|
+
dropzone.addEventListener('click', (e) => {
|
|
806
|
+
if (e.target === input) return;
|
|
807
|
+
if (e.target.classList.contains('pa-file-dropzone__summary-count')) return;
|
|
808
|
+
input.click();
|
|
809
|
+
});
|
|
810
|
+
|
|
811
|
+
// Drag events
|
|
812
|
+
dropzone.addEventListener('dragover', (e) => {
|
|
813
|
+
e.preventDefault();
|
|
814
|
+
dropzone.classList.add('pa-file-dropzone--active');
|
|
815
|
+
});
|
|
816
|
+
|
|
817
|
+
dropzone.addEventListener('dragleave', (e) => {
|
|
818
|
+
e.preventDefault();
|
|
819
|
+
if (e.target === dropzone) {
|
|
820
|
+
dropzone.classList.remove('pa-file-dropzone--active');
|
|
821
|
+
}
|
|
822
|
+
});
|
|
823
|
+
|
|
824
|
+
dropzone.addEventListener('drop', (e) => {
|
|
825
|
+
e.preventDefault();
|
|
826
|
+
dropzone.classList.remove('pa-file-dropzone--active');
|
|
827
|
+
|
|
828
|
+
const files = Array.from(e.dataTransfer.files);
|
|
829
|
+
handleFiles(files);
|
|
830
|
+
});
|
|
831
|
+
|
|
832
|
+
// Input change
|
|
833
|
+
input.addEventListener('change', (e) => {
|
|
834
|
+
const files = Array.from(e.target.files);
|
|
835
|
+
handleFiles(files);
|
|
836
|
+
});
|
|
837
|
+
|
|
838
|
+
// Drag overlay feature
|
|
839
|
+
const overlayTargetId = dropzone.dataset.overlayTarget;
|
|
840
|
+
let dragOverlay = null;
|
|
841
|
+
|
|
842
|
+
if (overlayTargetId) {
|
|
843
|
+
// Listen for drag enter on document
|
|
844
|
+
document.addEventListener('dragenter', (e) => {
|
|
845
|
+
// Check if dragging files
|
|
846
|
+
if (e.dataTransfer && e.dataTransfer.types && e.dataTransfer.types.includes('Files')) {
|
|
847
|
+
const targetElement = document.getElementById(overlayTargetId);
|
|
848
|
+
if (targetElement && !dragOverlay) {
|
|
849
|
+
createOverlay(targetElement);
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
});
|
|
853
|
+
|
|
854
|
+
function createOverlay(targetElement) {
|
|
855
|
+
const rect = targetElement.getBoundingClientRect();
|
|
856
|
+
|
|
857
|
+
dragOverlay = document.createElement('div');
|
|
858
|
+
dragOverlay.className = 'pa-file-dropzone-overlay';
|
|
859
|
+
dragOverlay.style.cssText = `
|
|
860
|
+
position: fixed;
|
|
861
|
+
top: ${rect.top}px;
|
|
862
|
+
left: ${rect.left}px;
|
|
863
|
+
width: ${rect.width}px;
|
|
864
|
+
height: ${rect.height}px;
|
|
865
|
+
z-index: 9999;
|
|
866
|
+
pointer-events: all;
|
|
867
|
+
`;
|
|
868
|
+
|
|
869
|
+
dragOverlay.innerHTML = `
|
|
870
|
+
<div class="pa-file-dropzone-overlay__content">
|
|
871
|
+
<div class="pa-file-dropzone-overlay__icon">📎</div>
|
|
872
|
+
<div class="pa-file-dropzone-overlay__text">Drop files here</div>
|
|
873
|
+
</div>
|
|
874
|
+
`;
|
|
875
|
+
|
|
876
|
+
document.body.appendChild(dragOverlay);
|
|
877
|
+
|
|
878
|
+
// Handle drop on overlay
|
|
879
|
+
dragOverlay.addEventListener('dragover', (e) => {
|
|
880
|
+
e.preventDefault();
|
|
881
|
+
});
|
|
882
|
+
|
|
883
|
+
dragOverlay.addEventListener('drop', (e) => {
|
|
884
|
+
e.preventDefault();
|
|
885
|
+
const files = Array.from(e.dataTransfer.files);
|
|
886
|
+
handleFiles(files);
|
|
887
|
+
removeOverlay();
|
|
888
|
+
});
|
|
889
|
+
|
|
890
|
+
dragOverlay.addEventListener('dragleave', (e) => {
|
|
891
|
+
if (e.target === dragOverlay) {
|
|
892
|
+
removeOverlay();
|
|
893
|
+
}
|
|
894
|
+
});
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
function removeOverlay() {
|
|
898
|
+
if (dragOverlay) {
|
|
899
|
+
dragOverlay.remove();
|
|
900
|
+
dragOverlay = null;
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
// Remove on window blur or ESC
|
|
905
|
+
window.addEventListener('blur', removeOverlay);
|
|
906
|
+
document.addEventListener('keydown', (e) => {
|
|
907
|
+
if (e.key === 'Escape') removeOverlay();
|
|
908
|
+
});
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
function handleFiles(files) {
|
|
912
|
+
selectedFiles = [...selectedFiles, ...files];
|
|
913
|
+
fileStates = selectedFiles.map((file, index) => ({
|
|
914
|
+
file,
|
|
915
|
+
progress: 0,
|
|
916
|
+
status: 'pending' // pending, uploading, complete, error
|
|
917
|
+
}));
|
|
918
|
+
renderSummary();
|
|
919
|
+
if (uploadBtn) {
|
|
920
|
+
uploadBtn.style.display = selectedFiles.length > 0 ? 'inline-flex' : 'none';
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
function renderSummary() {
|
|
925
|
+
// Remove existing content and old summaries
|
|
926
|
+
const existingContent = dropzone.querySelector('.pa-file-dropzone__content');
|
|
927
|
+
const existingSummary = dropzone.querySelector('.pa-file-dropzone__summary');
|
|
928
|
+
if (existingContent) existingContent.remove();
|
|
929
|
+
if (existingSummary) existingSummary.remove();
|
|
930
|
+
|
|
931
|
+
// Calculate total size
|
|
932
|
+
const totalSize = selectedFiles.reduce((sum, file) => sum + file.size, 0);
|
|
933
|
+
|
|
934
|
+
// Create summary
|
|
935
|
+
const summary = document.createElement('div');
|
|
936
|
+
summary.className = 'pa-file-dropzone__summary';
|
|
937
|
+
|
|
938
|
+
const currentFile = currentUploadIndex >= 0 && currentUploadIndex < selectedFiles.length
|
|
939
|
+
? selectedFiles[currentUploadIndex].name
|
|
940
|
+
: 'Click to select files';
|
|
941
|
+
|
|
942
|
+
const currentProgress = currentUploadIndex >= 0
|
|
943
|
+
? fileStates[currentUploadIndex].progress
|
|
944
|
+
: 0;
|
|
945
|
+
|
|
946
|
+
summary.innerHTML = `
|
|
947
|
+
<div class="pa-file-dropzone__summary-line">
|
|
948
|
+
<span class="pa-file-dropzone__summary-icon">📎</span>
|
|
949
|
+
<span class="pa-file-dropzone__summary-count">${selectedFiles.length} file${selectedFiles.length !== 1 ? 's' : ''}</span>
|
|
950
|
+
<span class="pa-file-dropzone__summary-size">(${formatFileSize(totalSize)})</span>
|
|
951
|
+
<span>Click to view details</span>
|
|
952
|
+
</div>
|
|
953
|
+
${currentUploadIndex >= 0 ? `
|
|
954
|
+
<div class="pa-file-dropzone__summary-current">
|
|
955
|
+
Uploading ${currentUploadIndex + 1} of ${selectedFiles.length} files: ${escapeHtml(currentFile)}
|
|
956
|
+
</div>
|
|
957
|
+
<div class="pa-file-dropzone__summary-progress">
|
|
958
|
+
<div class="pa-file-progress__bar">
|
|
959
|
+
<div class="pa-file-progress__fill" style="width: ${currentProgress}%"></div>
|
|
960
|
+
</div>
|
|
961
|
+
</div>
|
|
962
|
+
` : ''}
|
|
963
|
+
`;
|
|
964
|
+
|
|
965
|
+
dropzone.appendChild(summary);
|
|
966
|
+
|
|
967
|
+
// Attach click handler to entire summary line
|
|
968
|
+
const summaryLine = summary.querySelector('.pa-file-dropzone__summary-line');
|
|
969
|
+
if (summaryLine && selectedFiles.length > 0) {
|
|
970
|
+
summaryLine.addEventListener('click', (e) => {
|
|
971
|
+
e.stopPropagation();
|
|
972
|
+
if (!modal) { // Only open if not already open
|
|
973
|
+
openModal();
|
|
974
|
+
}
|
|
975
|
+
});
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
function openModal() {
|
|
980
|
+
// Prevent duplicate modals
|
|
981
|
+
if (modal) return;
|
|
982
|
+
|
|
983
|
+
// Get reference element (the dropzone itself for alignment)
|
|
984
|
+
const referenceEl = dropzone;
|
|
985
|
+
|
|
986
|
+
// Create popover
|
|
987
|
+
modal = document.createElement('div');
|
|
988
|
+
modal.className = 'pa-file-popover';
|
|
989
|
+
modal.innerHTML = `
|
|
990
|
+
<div class="pa-file-popover__arrow" data-popper-arrow></div>
|
|
991
|
+
<div class="pa-file-popover__header">
|
|
992
|
+
<h3 class="pa-file-popover__title">Selected Files (${selectedFiles.length})</h3>
|
|
993
|
+
<button type="button" class="pa-file-popover__close">×</button>
|
|
994
|
+
</div>
|
|
995
|
+
<div class="pa-file-popover__body">
|
|
996
|
+
<table class="pa-file-popover__table">
|
|
997
|
+
<thead>
|
|
998
|
+
<tr>
|
|
999
|
+
<th style="width: 1%;"></th>
|
|
1000
|
+
<th>Name</th>
|
|
1001
|
+
<th>Size</th>
|
|
1002
|
+
<th>Progress</th>
|
|
1003
|
+
<th>Status</th>
|
|
1004
|
+
</tr>
|
|
1005
|
+
</thead>
|
|
1006
|
+
<tbody>
|
|
1007
|
+
${fileStates.map((state, index) => {
|
|
1008
|
+
const isUploading = index === currentUploadIndex;
|
|
1009
|
+
const canRemove = state.status === 'pending' && currentUploadIndex < 0;
|
|
1010
|
+
return `
|
|
1011
|
+
<tr class="${isUploading ? 'uploading' : ''}" data-index="${index}">
|
|
1012
|
+
<td class="pa-file-popover__remove-cell">
|
|
1013
|
+
${canRemove ? `
|
|
1014
|
+
<button type="button" class="pa-file-popover__remove-btn" data-index="${index}">×</button>
|
|
1015
|
+
` : ''}
|
|
1016
|
+
</td>
|
|
1017
|
+
<td>
|
|
1018
|
+
<div class="pa-file-popover__file-name" title="${escapeHtml(state.file.name)}">
|
|
1019
|
+
${escapeHtml(state.file.name)}
|
|
1020
|
+
</div>
|
|
1021
|
+
</td>
|
|
1022
|
+
<td class="pa-file-popover__file-size">${formatFileSize(state.file.size)}</td>
|
|
1023
|
+
<td class="pa-file-popover__progress-cell">
|
|
1024
|
+
<div class="pa-file-popover__progress-bar">
|
|
1025
|
+
<div class="pa-file-popover__progress-fill" style="width: ${state.progress}%"></div>
|
|
1026
|
+
</div>
|
|
1027
|
+
<div class="pa-file-popover__progress-text">${state.progress}%</div>
|
|
1028
|
+
</td>
|
|
1029
|
+
<td>
|
|
1030
|
+
<span class="pa-file-popover__status pa-file-popover__status--${state.status}">
|
|
1031
|
+
${state.status}
|
|
1032
|
+
</span>
|
|
1033
|
+
</td>
|
|
1034
|
+
</tr>
|
|
1035
|
+
`;
|
|
1036
|
+
}).join('')}
|
|
1037
|
+
</tbody>
|
|
1038
|
+
</table>
|
|
1039
|
+
</div>
|
|
1040
|
+
`;
|
|
1041
|
+
|
|
1042
|
+
document.body.appendChild(modal);
|
|
1043
|
+
|
|
1044
|
+
// Position with Floating UI
|
|
1045
|
+
const { computePosition, flip, shift, offset, arrow } = window.FloatingUIDOM;
|
|
1046
|
+
const arrowEl = modal.querySelector('.pa-file-popover__arrow');
|
|
1047
|
+
|
|
1048
|
+
async function updatePosition() {
|
|
1049
|
+
const { x, y, placement, middlewareData } = await computePosition(referenceEl, modal, {
|
|
1050
|
+
placement: 'bottom-start',
|
|
1051
|
+
middleware: [
|
|
1052
|
+
offset(8),
|
|
1053
|
+
flip(),
|
|
1054
|
+
shift({ padding: 8 }),
|
|
1055
|
+
arrow({ element: arrowEl })
|
|
1056
|
+
]
|
|
1057
|
+
});
|
|
1058
|
+
|
|
1059
|
+
Object.assign(modal.style, {
|
|
1060
|
+
left: `${x}px`,
|
|
1061
|
+
top: `${y}px`
|
|
1062
|
+
});
|
|
1063
|
+
|
|
1064
|
+
// Position arrow
|
|
1065
|
+
if (middlewareData.arrow) {
|
|
1066
|
+
const { x: arrowX, y: arrowY } = middlewareData.arrow;
|
|
1067
|
+
const staticSide = {
|
|
1068
|
+
top: 'bottom',
|
|
1069
|
+
right: 'left',
|
|
1070
|
+
bottom: 'top',
|
|
1071
|
+
left: 'right'
|
|
1072
|
+
}[placement.split('-')[0]];
|
|
1073
|
+
|
|
1074
|
+
Object.assign(arrowEl.style, {
|
|
1075
|
+
left: arrowX != null ? `${arrowX}px` : '',
|
|
1076
|
+
top: arrowY != null ? `${arrowY}px` : '',
|
|
1077
|
+
right: '',
|
|
1078
|
+
bottom: '',
|
|
1079
|
+
[staticSide]: '-4px'
|
|
1080
|
+
});
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
updatePosition();
|
|
1085
|
+
|
|
1086
|
+
// Close handlers
|
|
1087
|
+
const closeBtn = modal.querySelector('.pa-file-popover__close');
|
|
1088
|
+
closeBtn.addEventListener('click', closeModal);
|
|
1089
|
+
|
|
1090
|
+
// Click outside to close
|
|
1091
|
+
setTimeout(() => {
|
|
1092
|
+
document.addEventListener('click', handleOutsideClick);
|
|
1093
|
+
}, 0);
|
|
1094
|
+
|
|
1095
|
+
document.addEventListener('keydown', handleEscKey);
|
|
1096
|
+
|
|
1097
|
+
// Remove button handlers
|
|
1098
|
+
const removeButtons = modal.querySelectorAll('.pa-file-popover__remove-btn');
|
|
1099
|
+
removeButtons.forEach(btn => {
|
|
1100
|
+
btn.addEventListener('click', (e) => {
|
|
1101
|
+
e.stopPropagation();
|
|
1102
|
+
const index = parseInt(btn.dataset.index);
|
|
1103
|
+
removeFile(index);
|
|
1104
|
+
});
|
|
1105
|
+
});
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
function handleOutsideClick(e) {
|
|
1109
|
+
if (modal && !modal.contains(e.target) && !dropzone.contains(e.target)) {
|
|
1110
|
+
closeModal();
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
function removeFile(index) {
|
|
1115
|
+
// Remove file from arrays
|
|
1116
|
+
selectedFiles.splice(index, 1);
|
|
1117
|
+
fileStates.splice(index, 1);
|
|
1118
|
+
|
|
1119
|
+
// Update display
|
|
1120
|
+
renderSummary();
|
|
1121
|
+
|
|
1122
|
+
// Update or close modal
|
|
1123
|
+
if (selectedFiles.length === 0) {
|
|
1124
|
+
closeModal();
|
|
1125
|
+
if (uploadBtn) {
|
|
1126
|
+
uploadBtn.style.display = 'none';
|
|
1127
|
+
}
|
|
1128
|
+
} else if (modal) {
|
|
1129
|
+
// Re-render modal
|
|
1130
|
+
closeModal();
|
|
1131
|
+
openModal();
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
function closeModal() {
|
|
1136
|
+
if (modal) {
|
|
1137
|
+
document.removeEventListener('keydown', handleEscKey);
|
|
1138
|
+
document.removeEventListener('click', handleOutsideClick);
|
|
1139
|
+
modal.remove();
|
|
1140
|
+
modal = null;
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
function handleEscKey(e) {
|
|
1145
|
+
if (e.key === 'Escape') {
|
|
1146
|
+
closeModal();
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
function updateModalProgress() {
|
|
1151
|
+
if (!modal) return;
|
|
1152
|
+
|
|
1153
|
+
const tbody = modal.querySelector('tbody');
|
|
1154
|
+
fileStates.forEach((state, index) => {
|
|
1155
|
+
const row = tbody.querySelector(`tr[data-index="${index}"]`);
|
|
1156
|
+
if (!row) return;
|
|
1157
|
+
|
|
1158
|
+
// Update row highlight
|
|
1159
|
+
row.className = index === currentUploadIndex ? 'uploading' : '';
|
|
1160
|
+
|
|
1161
|
+
// Update progress bar and text
|
|
1162
|
+
const progressFill = row.querySelector('.pa-file-popover__progress-fill');
|
|
1163
|
+
const progressText = row.querySelector('.pa-file-popover__progress-text');
|
|
1164
|
+
const statusSpan = row.querySelector('.pa-file-popover__status');
|
|
1165
|
+
|
|
1166
|
+
if (progressFill) progressFill.style.width = `${state.progress}%`;
|
|
1167
|
+
if (progressText) progressText.textContent = `${state.progress.toFixed(2)}%`;
|
|
1168
|
+
if (statusSpan) {
|
|
1169
|
+
statusSpan.textContent = state.status;
|
|
1170
|
+
statusSpan.className = `pa-file-popover__status pa-file-popover__status--${state.status}`;
|
|
1171
|
+
}
|
|
1172
|
+
});
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
// Upload button handler
|
|
1176
|
+
if (uploadBtn) {
|
|
1177
|
+
uploadBtn.addEventListener('click', () => {
|
|
1178
|
+
uploadBtn.disabled = true;
|
|
1179
|
+
simulateUpload();
|
|
1180
|
+
});
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
function simulateUpload() {
|
|
1184
|
+
if (selectedFiles.length === 0) return;
|
|
1185
|
+
|
|
1186
|
+
currentUploadIndex = 0;
|
|
1187
|
+
uploadNextFile();
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
function uploadNextFile() {
|
|
1191
|
+
if (currentUploadIndex >= selectedFiles.length) {
|
|
1192
|
+
// All done
|
|
1193
|
+
currentUploadIndex = -1;
|
|
1194
|
+
if (uploadBtn) uploadBtn.disabled = false;
|
|
1195
|
+
renderSummary();
|
|
1196
|
+
return;
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
fileStates[currentUploadIndex].status = 'uploading';
|
|
1200
|
+
fileStates[currentUploadIndex].progress = 0;
|
|
1201
|
+
renderSummary();
|
|
1202
|
+
updateModalProgress();
|
|
1203
|
+
|
|
1204
|
+
const interval = setInterval(() => {
|
|
1205
|
+
fileStates[currentUploadIndex].progress += Math.random() * 15;
|
|
1206
|
+
// Round to 2 decimal places
|
|
1207
|
+
fileStates[currentUploadIndex].progress = Math.round(fileStates[currentUploadIndex].progress * 100) / 100;
|
|
1208
|
+
|
|
1209
|
+
if (fileStates[currentUploadIndex].progress >= 100) {
|
|
1210
|
+
fileStates[currentUploadIndex].progress = 100;
|
|
1211
|
+
fileStates[currentUploadIndex].status = 'complete';
|
|
1212
|
+
clearInterval(interval);
|
|
1213
|
+
|
|
1214
|
+
renderSummary();
|
|
1215
|
+
updateModalProgress();
|
|
1216
|
+
|
|
1217
|
+
// Move to next file
|
|
1218
|
+
currentUploadIndex++;
|
|
1219
|
+
setTimeout(uploadNextFile, 300);
|
|
1220
|
+
} else {
|
|
1221
|
+
renderSummary();
|
|
1222
|
+
updateModalProgress();
|
|
1223
|
+
}
|
|
1224
|
+
}, 200);
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
// ============================================================================
|
|
1229
|
+
// Utility: Escape HTML
|
|
1230
|
+
// ============================================================================
|
|
1231
|
+
|
|
1232
|
+
function escapeHtml(text) {
|
|
1233
|
+
const div = document.createElement('div');
|
|
1234
|
+
div.textContent = text;
|
|
1235
|
+
return div.innerHTML;
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
// ============================================================================
|
|
1239
|
+
// Initialize All Sections
|
|
1240
|
+
// ============================================================================
|
|
1241
|
+
|
|
1242
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
1243
|
+
// Section 1: Basic file input - single
|
|
1244
|
+
initBasicFileInput('file-single');
|
|
1245
|
+
|
|
1246
|
+
// Section 2: Basic file input - multiple
|
|
1247
|
+
initBasicFileInput('file-multiple');
|
|
1248
|
+
|
|
1249
|
+
// Section 3: Drag & drop - single
|
|
1250
|
+
initDropzone('dropzone-single');
|
|
1251
|
+
|
|
1252
|
+
// Section 4: Drag & drop - multiple with list
|
|
1253
|
+
initDropzone('dropzone-multiple', 'dropzone-multiple-list');
|
|
1254
|
+
|
|
1255
|
+
// Section 5: Image preview with thumbnails
|
|
1256
|
+
initImagePreview('dropzone-preview', 'preview-container');
|
|
1257
|
+
|
|
1258
|
+
// Section 6: File list with details
|
|
1259
|
+
initDetailedFileList('dropzone-detailed', 'detailed-file-list');
|
|
1260
|
+
|
|
1261
|
+
// Section 7: File validation
|
|
1262
|
+
initValidatedDropzone('dropzone-validated', 'validation-error', 'validation-success');
|
|
1263
|
+
|
|
1264
|
+
// Section 8: Upload progress
|
|
1265
|
+
initUploadProgress('dropzone-progress', 'progress-file-list', 'upload-btn');
|
|
1266
|
+
|
|
1267
|
+
// Compact mode examples
|
|
1268
|
+
initDropzone('dropzone-compact');
|
|
1269
|
+
initImagePreview('dropzone-compact-images');
|
|
1270
|
+
|
|
1271
|
+
// Ultra-compact modal mode
|
|
1272
|
+
initCompactModal('dropzone-ultracompact', 'upload-ultracompact-btn');
|
|
1273
|
+
});
|
|
1274
|
+
|
|
1275
|
+
})();
|