@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.
Files changed (35) hide show
  1. package/README.md +55 -13
  2. package/dist/css/main.css +554 -7
  3. package/package.json +3 -1
  4. package/snippets/buttons.html +51 -0
  5. package/snippets/cards.html +132 -47
  6. package/snippets/comparison.html +26 -22
  7. package/snippets/manifest.json +180 -115
  8. package/snippets/range-group.html +125 -0
  9. package/snippets/splitter.html +44 -38
  10. package/snippets/statistics.html +31 -0
  11. package/src/js/btn-split-auto-absorb.js +327 -0
  12. package/src/js/command-palette.js +472 -0
  13. package/src/js/file-selector.js +1275 -0
  14. package/src/js/internal/logging.js +121 -0
  15. package/src/js/logic-tree-renderer.js +303 -0
  16. package/src/js/modal-dialogs.js +460 -0
  17. package/src/js/overflow.js +371 -0
  18. package/src/js/pa-stat-fit.js +184 -0
  19. package/src/js/range-group.js +663 -0
  20. package/src/js/search-autocomplete-v2.js +907 -0
  21. package/src/js/search-autocomplete.js +434 -0
  22. package/src/js/settings-panel.js +245 -0
  23. package/src/js/split-button.js +141 -0
  24. package/src/js/splitter.js +1323 -0
  25. package/src/js/toast-service.js +302 -0
  26. package/src/js/tooltips-popovers.js +275 -0
  27. package/src/js/virtual-scroll.js +143 -0
  28. package/src/js/virtual-textbox.js +803 -0
  29. package/src/scss/_core.scss +7 -0
  30. package/src/scss/core-components/_buttons.scss +44 -0
  31. package/src/scss/core-components/_cards.scss +95 -6
  32. package/src/scss/core-components/_overflow.scss +50 -0
  33. package/src/scss/core-components/_range-group.scss +474 -0
  34. package/src/scss/core-components/_statistics.scss +163 -0
  35. 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
+ })();