@dmitryvim/form-builder 0.2.26 → 0.2.27

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.
@@ -2119,7 +2119,7 @@ function updateSwitcherField(element, fieldPath, value, context) {
2119
2119
  }
2120
2120
  }
2121
2121
 
2122
- // src/components/file.ts
2122
+ // src/components/file/constraints.ts
2123
2123
  function getAllowedExtensions(accept) {
2124
2124
  if (!accept) return [];
2125
2125
  if (typeof accept === "object" && Array.isArray(accept.extensions)) {
@@ -2131,18 +2131,669 @@ function getAllowedExtensions(accept) {
2131
2131
  return [];
2132
2132
  }
2133
2133
  function isFileExtensionAllowed(fileName, allowedExtensions) {
2134
- var _a;
2134
+ var _a, _b;
2135
2135
  if (allowedExtensions.length === 0) return true;
2136
- const ext = ((_a = fileName.split(".").pop()) == null ? void 0 : _a.toLowerCase()) || "";
2136
+ const ext = (_b = (_a = fileName.split(".").pop()) == null ? void 0 : _a.toLowerCase()) != null ? _b : "";
2137
2137
  return allowedExtensions.includes(ext);
2138
2138
  }
2139
2139
  function isFileSizeAllowed(file, maxSizeMB) {
2140
2140
  if (maxSizeMB === Infinity) return true;
2141
2141
  return file.size <= maxSizeMB * 1024 * 1024;
2142
2142
  }
2143
+ function addPrefillFilesToIndex(initialFiles, resourceIndex) {
2144
+ var _a;
2145
+ for (const resourceId of initialFiles) {
2146
+ if (resourceIndex.has(resourceId)) continue;
2147
+ const filename = resourceId.split("/").pop() || "file";
2148
+ const extension = (_a = filename.split(".").pop()) == null ? void 0 : _a.toLowerCase();
2149
+ let fileType = "application/octet-stream";
2150
+ if (extension) {
2151
+ if (["jpg", "jpeg", "png", "gif", "webp"].includes(extension)) {
2152
+ fileType = `image/${extension === "jpg" ? "jpeg" : extension}`;
2153
+ } else if (["mp4", "webm", "mov", "avi"].includes(extension)) {
2154
+ fileType = `video/${extension === "mov" ? "quicktime" : extension}`;
2155
+ }
2156
+ }
2157
+ resourceIndex.set(resourceId, {
2158
+ name: filename,
2159
+ type: fileType,
2160
+ size: 0,
2161
+ uploadedAt: /* @__PURE__ */ new Date(),
2162
+ file: void 0
2163
+ });
2164
+ }
2165
+ }
2166
+
2167
+ // src/components/file/styles.ts
2168
+ var STYLE_ID = "fb-file-styles";
2169
+ function ensureFileStyles() {
2170
+ if (typeof document === "undefined") return;
2171
+ if (document.getElementById(STYLE_ID)) return;
2172
+ const style = document.createElement("style");
2173
+ style.id = STYLE_ID;
2174
+ style.setAttribute("data-fb-file-styles", "true");
2175
+ style.textContent = `
2176
+ @keyframes fb-spin { to { transform: rotate(360deg); } }
2177
+
2178
+ /* Spinner used during single-file and multi-file upload */
2179
+ .fb-spinner {
2180
+ width: 36px;
2181
+ height: 36px;
2182
+ border: 3px solid rgba(0,0,0,0.12);
2183
+ border-top-color: var(--fb-text-secondary-color, #6b7280);
2184
+ border-radius: 50%;
2185
+ animation: fb-spin 0.7s linear infinite;
2186
+ flex-shrink: 0;
2187
+ }
2188
+
2189
+ /* Base tile: fixed 160\xD7160 square, theme-aware background */
2190
+ .fb-tile {
2191
+ width: var(--fb-tile-size, 160px);
2192
+ height: var(--fb-tile-size, 160px);
2193
+ flex-shrink: 0;
2194
+ position: relative;
2195
+ overflow: hidden;
2196
+ border-radius: var(--fb-border-radius, 0.5rem);
2197
+ background: var(--fb-file-upload-bg-color, #f3f4f6);
2198
+ }
2199
+
2200
+ /* Uploaded resource tile \u2014 adds a visible border */
2201
+ .fb-tile-resource {
2202
+ border: 1px solid var(--fb-file-upload-border-color, #d1d5db);
2203
+ }
2204
+
2205
+ /* Uploading placeholder tile \u2014 dashed border, uploading indicator */
2206
+ .fb-tile-uploading {
2207
+ border: 2px dashed var(--fb-file-upload-border-color, #d1d5db);
2208
+ }
2209
+
2210
+ /* "+" add-more tile */
2211
+ .fb-tile-add {
2212
+ border: 2px dashed var(--fb-file-upload-border-color, #d1d5db);
2213
+ display: flex;
2214
+ align-items: center;
2215
+ justify-content: center;
2216
+ cursor: pointer;
2217
+ font-size: 32px;
2218
+ color: var(--fb-file-upload-text-color, #9ca3af);
2219
+ transition:
2220
+ border-color var(--fb-transition-duration, 200ms),
2221
+ color var(--fb-transition-duration, 200ms);
2222
+ }
2223
+ .fb-tile-add:hover {
2224
+ border-color: var(--fb-file-upload-hover-border-color, #3b82f6);
2225
+ color: var(--fb-text-color, #1f2937);
2226
+ }
2227
+
2228
+ /* Count chip shown when at maxCount */
2229
+ .fb-tile-counter {
2230
+ font-size: 11px;
2231
+ color: var(--fb-text-secondary-color, #6b7280);
2232
+ background: var(--fb-file-upload-bg-color, #f3f4f6);
2233
+ border: 1px solid var(--fb-file-upload-border-color, #d1d5db);
2234
+ border-radius: 4px;
2235
+ padding: 2px 6px;
2236
+ align-self: flex-end;
2237
+ margin-bottom: 4px;
2238
+ }
2239
+
2240
+ /* Empty-state dropzone */
2241
+ .fb-file-dropzone {
2242
+ width: 100%;
2243
+ height: 128px;
2244
+ border: 2px dashed var(--fb-file-upload-border-color, #d1d5db);
2245
+ border-radius: var(--fb-border-radius, 0.5rem);
2246
+ display: flex;
2247
+ flex-direction: column;
2248
+ align-items: center;
2249
+ justify-content: center;
2250
+ gap: 4px;
2251
+ cursor: pointer;
2252
+ transition:
2253
+ border-color var(--fb-transition-duration, 200ms),
2254
+ background var(--fb-transition-duration, 200ms);
2255
+ }
2256
+ .fb-file-dropzone:hover {
2257
+ border-color: var(--fb-file-upload-hover-border-color, #3b82f6);
2258
+ background: var(--fb-background-hover-color, #f9fafb);
2259
+ }
2260
+
2261
+ /* Inline text inside tiles */
2262
+ .fb-tile-label {
2263
+ font-size: 9px;
2264
+ color: var(--fb-text-secondary-color, #6b7280);
2265
+ text-align: center;
2266
+ overflow: hidden;
2267
+ word-break: break-all;
2268
+ max-height: 28px;
2269
+ }
2270
+ .fb-tile-uploading-text {
2271
+ font-size: 8px;
2272
+ color: var(--fb-file-upload-text-color, #9ca3af);
2273
+ }
2274
+ .fb-tile-hint {
2275
+ font-size: 11px;
2276
+ color: var(--fb-file-upload-text-color, #9ca3af);
2277
+ margin-top: 4px;
2278
+ }
2279
+ .fb-tile-empty-text {
2280
+ font-size: 12px;
2281
+ color: var(--fb-text-secondary-color, #6b7280);
2282
+ padding: 4px 0;
2283
+ }
2284
+ .fb-dropzone-primary-text {
2285
+ font-size: 13px;
2286
+ color: var(--fb-text-secondary-color, #6b7280);
2287
+ }
2288
+ .fb-dropzone-hint-text {
2289
+ font-size: 11px;
2290
+ color: var(--fb-file-upload-text-color, #9ca3af);
2291
+ }
2292
+
2293
+ /* Hover overlay + X-button on resource tiles */
2294
+ .fb-tile-overlay {
2295
+ position: absolute;
2296
+ inset: 0;
2297
+ background: transparent;
2298
+ transition: background var(--fb-transition-duration, 200ms);
2299
+ display: flex;
2300
+ align-items: flex-start;
2301
+ justify-content: flex-end;
2302
+ }
2303
+ .fb-tile-resource:hover .fb-tile-overlay {
2304
+ background: var(--fb-tile-hover-overlay-color, rgba(0,0,0,0.4));
2305
+ }
2306
+ .fb-tile-x-btn {
2307
+ margin: 3px;
2308
+ width: 18px;
2309
+ height: 18px;
2310
+ background: var(--fb-error-color, #ef4444);
2311
+ color: var(--fb-file-bg-color, #fff);
2312
+ border: none;
2313
+ border-radius: 50%;
2314
+ font-size: 11px;
2315
+ line-height: 1;
2316
+ cursor: pointer;
2317
+ display: flex;
2318
+ align-items: center;
2319
+ justify-content: center;
2320
+ opacity: 0;
2321
+ transition: opacity var(--fb-transition-duration, 200ms);
2322
+ }
2323
+ .fb-tile-resource:hover .fb-tile-x-btn {
2324
+ opacity: 1;
2325
+ }
2326
+
2327
+ /* Video play button overlay (readonly tiles with video thumbnails) */
2328
+ .fb-video-overlay {
2329
+ position: absolute;
2330
+ inset: 0;
2331
+ display: flex;
2332
+ align-items: center;
2333
+ justify-content: center;
2334
+ background: var(--fb-tile-hover-overlay-color, rgba(0,0,0,0.25));
2335
+ }
2336
+ .fb-play-btn {
2337
+ background: var(--fb-file-bg-color, rgba(255,255,255,0.9));
2338
+ border-radius: 50%;
2339
+ display: flex;
2340
+ align-items: center;
2341
+ justify-content: center;
2342
+ }
2343
+
2344
+ /* Edit-mode local video preview wrapper */
2345
+ .fb-video-preview-wrap {
2346
+ position: relative;
2347
+ width: 100%;
2348
+ height: 100%;
2349
+ }
2350
+
2351
+ /* Hover overlay for edit-mode local video (Remove / Change buttons) */
2352
+ .fb-video-btn-overlay {
2353
+ position: absolute;
2354
+ top: 8px;
2355
+ right: 8px;
2356
+ z-index: 10;
2357
+ display: flex;
2358
+ gap: 4px;
2359
+ opacity: 0;
2360
+ transition: opacity var(--fb-transition-duration, 200ms);
2361
+ pointer-events: none;
2362
+ }
2363
+ .fb-video-preview-wrap:hover .fb-video-btn-overlay {
2364
+ opacity: 1;
2365
+ pointer-events: auto;
2366
+ }
2367
+ .fb-video-btn {
2368
+ border: none;
2369
+ border-radius: var(--fb-border-radius, 4px);
2370
+ font-size: 11px;
2371
+ padding: 4px 8px;
2372
+ cursor: pointer;
2373
+ color: #fff;
2374
+ line-height: 1.2;
2375
+ }
2376
+ .fb-video-btn-delete {
2377
+ background: rgba(220, 38, 38, 0.85);
2378
+ }
2379
+ .fb-video-btn-delete:hover {
2380
+ background: rgba(185, 28, 28, 0.95);
2381
+ }
2382
+ .fb-video-btn-change {
2383
+ background: rgba(31, 41, 55, 0.85);
2384
+ }
2385
+ .fb-video-btn-change:hover {
2386
+ background: rgba(17, 24, 39, 0.95);
2387
+ }
2388
+
2389
+ /* Tile action icon buttons (download / open / remove) \u2014 shown on tile hover */
2390
+ .fb-tile-actions {
2391
+ position: absolute;
2392
+ top: 3px;
2393
+ right: 3px;
2394
+ display: flex;
2395
+ flex-direction: row;
2396
+ gap: 3px;
2397
+ opacity: 0;
2398
+ transition: opacity var(--fb-transition-duration, 200ms);
2399
+ z-index: 10;
2400
+ }
2401
+ .fb-tile-resource:hover .fb-tile-actions {
2402
+ opacity: 1;
2403
+ }
2404
+ .fb-tile-action-btn {
2405
+ width: 28px;
2406
+ height: 28px;
2407
+ display: flex;
2408
+ align-items: center;
2409
+ justify-content: center;
2410
+ border: none;
2411
+ border-radius: 50%;
2412
+ cursor: pointer;
2413
+ background: rgba(31, 41, 55, 0.75);
2414
+ color: #fff;
2415
+ padding: 0;
2416
+ flex-shrink: 0;
2417
+ transition:
2418
+ background var(--fb-transition-duration, 200ms),
2419
+ opacity var(--fb-transition-duration, 200ms);
2420
+ }
2421
+ .fb-tile-action-btn:hover {
2422
+ background: rgba(17, 24, 39, 0.95);
2423
+ }
2424
+ .fb-tile-action-remove {
2425
+ background: rgba(220, 38, 38, 0.8);
2426
+ }
2427
+ .fb-tile-action-remove:hover {
2428
+ background: rgba(185, 28, 28, 0.95);
2429
+ }
2430
+
2431
+ /* Actions row inside zoom popup \u2014 always visible while popup is shown */
2432
+ .fb-tile-zoom-preview .fb-tile-actions {
2433
+ position: absolute;
2434
+ top: 6px;
2435
+ right: 6px;
2436
+ opacity: 1;
2437
+ z-index: 10000;
2438
+ }
2439
+
2440
+ /* Hover zoom preview popup for image tiles \u2014 appended to document.body (fixed) */
2441
+ .fb-tile-zoom-preview {
2442
+ position: fixed;
2443
+ z-index: 9999;
2444
+ background: var(--fb-background-color, #fff);
2445
+ border: 1px solid var(--fb-file-upload-border-color, #d1d5db);
2446
+ border-radius: var(--fb-border-radius, 0.5rem);
2447
+ box-shadow: 0 4px 16px rgba(0,0,0,0.18);
2448
+ padding: 4px;
2449
+ width: 350px;
2450
+ height: 350px;
2451
+ pointer-events: none;
2452
+ opacity: 0;
2453
+ transition: opacity 150ms ease;
2454
+ }
2455
+ .fb-tile-zoom-preview.fb-tile-zoom-preview--visible {
2456
+ opacity: 1;
2457
+ }
2458
+ .fb-tile-zoom-preview-img {
2459
+ width: 100%;
2460
+ height: 100%;
2461
+ object-fit: contain;
2462
+ display: block;
2463
+ background: var(--fb-file-upload-bg-color, #f3f4f6);
2464
+ border-radius: calc(var(--fb-border-radius, 0.5rem) - 2px);
2465
+ }
2466
+ `;
2467
+ document.head.appendChild(style);
2468
+ }
2469
+
2470
+ // src/components/file/dom.ts
2471
+ var TILE_SIZE = "160px";
2472
+ function createFileTile() {
2473
+ ensureFileStyles();
2474
+ const tile = document.createElement("div");
2475
+ tile.className = "fb-tile";
2476
+ return tile;
2477
+ }
2478
+ function showFileError(container, message) {
2479
+ var _a, _b;
2480
+ const existing = (_a = container.closest(".space-y-2")) == null ? void 0 : _a.querySelector(".file-error-message");
2481
+ if (existing) existing.remove();
2482
+ const errorEl = document.createElement("div");
2483
+ errorEl.className = "file-error-message error-message";
2484
+ errorEl.style.cssText = `
2485
+ color: var(--fb-error-color);
2486
+ font-size: var(--fb-font-size-small);
2487
+ margin-top: 0.25rem;
2488
+ `;
2489
+ errorEl.textContent = message;
2490
+ (_b = container.closest(".space-y-2")) == null ? void 0 : _b.appendChild(errorEl);
2491
+ }
2492
+ function clearFileError(container) {
2493
+ var _a;
2494
+ const existing = (_a = container.closest(".space-y-2")) == null ? void 0 : _a.querySelector(".file-error-message");
2495
+ if (existing) existing.remove();
2496
+ }
2497
+ function addDeleteButton(container, state, onDelete) {
2498
+ const existingOverlay = container.querySelector(".delete-overlay");
2499
+ if (existingOverlay) existingOverlay.remove();
2500
+ const overlay = document.createElement("div");
2501
+ overlay.className = "delete-overlay absolute inset-0 bg-black bg-opacity-50 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center";
2502
+ const deleteBtn = document.createElement("button");
2503
+ deleteBtn.className = "bg-red-600 text-white px-3 py-1 rounded text-sm hover:bg-red-700 transition-colors";
2504
+ deleteBtn.textContent = t("removeElement", state);
2505
+ deleteBtn.onclick = (e) => {
2506
+ e.stopPropagation();
2507
+ onDelete();
2508
+ };
2509
+ overlay.appendChild(deleteBtn);
2510
+ container.appendChild(overlay);
2511
+ }
2512
+ function findFilePicker(container) {
2513
+ var _a;
2514
+ let el = container.parentElement;
2515
+ while (el && !el.dataset.filesWrapper) {
2516
+ el = el.parentElement;
2517
+ }
2518
+ return (_a = el == null ? void 0 : el.querySelector('input[type="file"]')) != null ? _a : null;
2519
+ }
2520
+ function createUploadingTile(fileName, state) {
2521
+ ensureFileStyles();
2522
+ const tile = createFileTile();
2523
+ tile.classList.add("fb-tile-uploading");
2524
+ tile.className += " fb-uploading-tile";
2525
+ const label = fileName.length > 10 ? fileName.substring(0, 8) + "\u2026" : fileName;
2526
+ tile.innerHTML = `
2527
+ <div style="display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;gap:6px;padding:4px;">
2528
+ <div class="fb-spinner"></div>
2529
+ <div class="fb-tile-label">${escapeHtml(label)}</div>
2530
+ <div class="fb-tile-uploading-text">${escapeHtml(t("uploadingFile", state))}</div>
2531
+ </div>`;
2532
+ return tile;
2533
+ }
2534
+ function ensureTilesWrap(list) {
2535
+ const existing = list.querySelector(".fb-tiles-wrap");
2536
+ if (existing) return existing;
2537
+ const dropzone = list.querySelector(".fb-file-dropzone");
2538
+ if (dropzone) dropzone.remove();
2539
+ const tilesWrap = document.createElement("div");
2540
+ tilesWrap.className = "fb-tiles-wrap";
2541
+ tilesWrap.style.cssText = "display:flex;flex-wrap:wrap;gap:6px;align-items:flex-start;";
2542
+ const addTile = document.createElement("div");
2543
+ addTile.className = "fb-tile fb-tile-add";
2544
+ addTile.innerHTML = "+";
2545
+ tilesWrap.appendChild(addTile);
2546
+ list.appendChild(tilesWrap);
2547
+ return tilesWrap;
2548
+ }
2549
+ function setEmptyFileContainer(fileContainer, state, hint) {
2550
+ const hintHtml = hint ? `<div class="text-xs text-gray-500 mt-1">${escapeHtml(hint)}</div>` : "";
2551
+ fileContainer.innerHTML = `
2552
+ <div class="flex flex-col items-center justify-center h-full text-gray-400">
2553
+ <svg class="w-6 h-6 mb-2" fill="currentColor" viewBox="0 0 24 24">
2554
+ <path d="M21 19V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2zM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5l3.5-4.5z"/>
2555
+ </svg>
2556
+ <div class="text-sm text-center">${escapeHtml(t("clickDragText", state))}</div>
2557
+ ${hintHtml}
2558
+ </div>
2559
+ `;
2560
+ }
2561
+ function setupDragAndDrop(element, dropHandler) {
2562
+ element.addEventListener("dragover", (e) => {
2563
+ e.preventDefault();
2564
+ element.classList.add("border-blue-500", "bg-blue-50");
2565
+ });
2566
+ element.addEventListener("dragleave", (e) => {
2567
+ e.preventDefault();
2568
+ element.classList.remove("border-blue-500", "bg-blue-50");
2569
+ });
2570
+ element.addEventListener("drop", (e) => {
2571
+ var _a;
2572
+ e.preventDefault();
2573
+ element.classList.remove("border-blue-500", "bg-blue-50");
2574
+ if ((_a = e.dataTransfer) == null ? void 0 : _a.files) {
2575
+ dropHandler(e.dataTransfer.files);
2576
+ }
2577
+ });
2578
+ }
2579
+
2580
+ // src/components/file/preview.ts
2581
+ var ICON_DOWNLOAD = `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>`;
2582
+ var ICON_OPEN = `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/></svg>`;
2583
+ var ICON_REMOVE = `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/><path d="M10 11v6"/><path d="M14 11v6"/><path d="M9 6V4a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2v2"/></svg>`;
2584
+ function canDownload(state, meta) {
2585
+ return Boolean(
2586
+ state.config.downloadFile || state.config.getDownloadUrl || state.config.getThumbnail || (meta == null ? void 0 : meta.file)
2587
+ );
2588
+ }
2589
+ function canOpenInTab(state, meta) {
2590
+ return Boolean(
2591
+ state.config.getDownloadUrl || state.config.getThumbnail || (meta == null ? void 0 : meta.file)
2592
+ );
2593
+ }
2594
+ function createTileActions(options) {
2595
+ const { canRemove, removeHandler, state, resourceId, fileName, meta } = options;
2596
+ const group = document.createElement("div");
2597
+ group.className = "fb-tile-actions";
2598
+ const makeBtn = (icon, label, cls) => {
2599
+ const btn = document.createElement("button");
2600
+ btn.type = "button";
2601
+ btn.className = `fb-tile-action-btn ${cls}`;
2602
+ btn.innerHTML = icon;
2603
+ btn.title = label;
2604
+ btn.setAttribute("aria-label", label);
2605
+ btn.addEventListener("click", (e) => {
2606
+ e.stopPropagation();
2607
+ });
2608
+ return btn;
2609
+ };
2610
+ if (canDownload(state, meta)) {
2611
+ const dlBtn = makeBtn(ICON_DOWNLOAD, t("downloadFile", state), "fb-tile-action-download");
2612
+ dlBtn.addEventListener("click", () => {
2613
+ triggerTileDownload(resourceId, fileName, state, meta);
2614
+ });
2615
+ group.appendChild(dlBtn);
2616
+ }
2617
+ if (canOpenInTab(state, meta)) {
2618
+ const openBtn = makeBtn(ICON_OPEN, t("openInNewTab", state), "fb-tile-action-open");
2619
+ openBtn.addEventListener("click", () => {
2620
+ triggerTileOpen(resourceId, state, meta).catch((err) => {
2621
+ console.error("Open failed:", err);
2622
+ });
2623
+ });
2624
+ group.appendChild(openBtn);
2625
+ }
2626
+ if (canRemove && removeHandler) {
2627
+ const rmBtn = makeBtn(ICON_REMOVE, t("removeElement", state), "fb-tile-action-remove");
2628
+ rmBtn.addEventListener("click", () => {
2629
+ removeHandler();
2630
+ });
2631
+ group.appendChild(rmBtn);
2632
+ }
2633
+ return group;
2634
+ }
2635
+ var localFileUrlCache = /* @__PURE__ */ new WeakMap();
2636
+ function getLocalFileUrl(file) {
2637
+ let url = localFileUrlCache.get(file);
2638
+ if (!url) {
2639
+ url = URL.createObjectURL(file);
2640
+ localFileUrlCache.set(file, url);
2641
+ }
2642
+ return url;
2643
+ }
2644
+ function releaseLocalFileUrl(file) {
2645
+ if (!file) return;
2646
+ const url = localFileUrlCache.get(file);
2647
+ if (url) {
2648
+ URL.revokeObjectURL(url);
2649
+ localFileUrlCache.delete(file);
2650
+ }
2651
+ }
2652
+ function triggerTileDownload(resourceId, fileName, state, meta) {
2653
+ if (state.config.downloadFile) {
2654
+ state.config.downloadFile(resourceId, fileName);
2655
+ return;
2656
+ }
2657
+ if ((meta == null ? void 0 : meta.file) instanceof File) {
2658
+ downloadBlob(meta.file, fileName || meta.file.name);
2659
+ return;
2660
+ }
2661
+ forceDownload(resourceId, fileName, state).catch((err) => {
2662
+ console.error("Download failed:", err);
2663
+ });
2664
+ }
2665
+ async function triggerTileOpen(resourceId, state, meta) {
2666
+ let url = null;
2667
+ if (state.config.getDownloadUrl) {
2668
+ url = state.config.getDownloadUrl(resourceId);
2669
+ } else if (state.config.getThumbnail) {
2670
+ url = await state.config.getThumbnail(resourceId);
2671
+ } else if ((meta == null ? void 0 : meta.file) instanceof File) {
2672
+ url = getLocalFileUrl(meta.file);
2673
+ }
2674
+ if (url) {
2675
+ window.open(url, "_blank");
2676
+ }
2677
+ }
2678
+ var sharedZoomPopup = null;
2679
+ var zoomTimer = null;
2680
+ var zoomHideTimer = null;
2681
+ var zoomOwner = null;
2682
+ function getOrCreateZoomPopup() {
2683
+ if (!sharedZoomPopup) {
2684
+ sharedZoomPopup = document.createElement("div");
2685
+ sharedZoomPopup.className = "fb-tile-zoom-preview";
2686
+ const img = document.createElement("img");
2687
+ img.className = "fb-tile-zoom-preview-img";
2688
+ sharedZoomPopup.appendChild(img);
2689
+ sharedZoomPopup.addEventListener("mouseenter", cancelHideZoomPopup);
2690
+ sharedZoomPopup.addEventListener("mouseleave", scheduleHideZoomPopup);
2691
+ }
2692
+ return sharedZoomPopup;
2693
+ }
2694
+ function positionZoomPopup(popup, tile) {
2695
+ const tileRect = tile.getBoundingClientRect();
2696
+ const popupSize = 350;
2697
+ const margin = 6;
2698
+ const padding = 8;
2699
+ let top;
2700
+ if (tileRect.top - popupSize - margin >= padding) {
2701
+ top = tileRect.top - popupSize - margin;
2702
+ } else if (tileRect.bottom + margin + popupSize + padding <= window.innerHeight) {
2703
+ top = tileRect.bottom + margin;
2704
+ } else {
2705
+ top = Math.max(padding, Math.min(window.innerHeight - popupSize - padding, tileRect.top));
2706
+ }
2707
+ const tileCenterX = tileRect.left + tileRect.width / 2;
2708
+ let left = tileCenterX - popupSize / 2;
2709
+ left = Math.max(padding, Math.min(window.innerWidth - popupSize - padding, left));
2710
+ popup.style.top = `${top}px`;
2711
+ popup.style.left = `${left}px`;
2712
+ }
2713
+ function scheduleHideZoomPopup() {
2714
+ if (zoomHideTimer !== null) {
2715
+ clearTimeout(zoomHideTimer);
2716
+ }
2717
+ zoomHideTimer = setTimeout(() => {
2718
+ zoomHideTimer = null;
2719
+ removeZoomPopupNow();
2720
+ }, 100);
2721
+ }
2722
+ function cancelHideZoomPopup() {
2723
+ if (zoomHideTimer !== null) {
2724
+ clearTimeout(zoomHideTimer);
2725
+ zoomHideTimer = null;
2726
+ }
2727
+ }
2728
+ function removeZoomPopupNow() {
2729
+ if (zoomTimer !== null) {
2730
+ clearTimeout(zoomTimer);
2731
+ zoomTimer = null;
2732
+ }
2733
+ if (sharedZoomPopup && sharedZoomPopup.parentNode) {
2734
+ sharedZoomPopup.classList.remove("fb-tile-zoom-preview--visible");
2735
+ sharedZoomPopup.parentNode.removeChild(sharedZoomPopup);
2736
+ }
2737
+ zoomOwner = null;
2738
+ }
2739
+ function attachZoomHover(tile, src, alt, actionsEl) {
2740
+ tile.dataset.zoomSrc = src;
2741
+ tile.dataset.zoomAlt = alt;
2742
+ tile.addEventListener("mouseenter", () => {
2743
+ cancelHideZoomPopup();
2744
+ if (zoomOwner !== tile) {
2745
+ removeZoomPopupNow();
2746
+ }
2747
+ zoomOwner = tile;
2748
+ zoomTimer = setTimeout(() => {
2749
+ zoomTimer = null;
2750
+ const popup = getOrCreateZoomPopup();
2751
+ const existingActions = popup.querySelector(".fb-tile-actions");
2752
+ if (existingActions) existingActions.remove();
2753
+ const img = popup.querySelector(".fb-tile-zoom-preview-img");
2754
+ img.src = src;
2755
+ img.alt = alt;
2756
+ if (actionsEl) {
2757
+ popup.appendChild(actionsEl.cloneNode(true));
2758
+ attachClonedActionListeners(
2759
+ popup.querySelector(".fb-tile-actions"),
2760
+ actionsEl
2761
+ );
2762
+ }
2763
+ popup.style.pointerEvents = "auto";
2764
+ positionZoomPopup(popup, tile);
2765
+ document.body.appendChild(popup);
2766
+ popup.getBoundingClientRect();
2767
+ popup.classList.add("fb-tile-zoom-preview--visible");
2768
+ }, 200);
2769
+ });
2770
+ tile.addEventListener("mouseleave", () => {
2771
+ if (zoomTimer !== null) {
2772
+ clearTimeout(zoomTimer);
2773
+ zoomTimer = null;
2774
+ zoomOwner = null;
2775
+ } else {
2776
+ scheduleHideZoomPopup();
2777
+ }
2778
+ });
2779
+ }
2780
+ function attachClonedActionListeners(cloned, original) {
2781
+ const originalBtns = Array.from(original.querySelectorAll(".fb-tile-action-btn"));
2782
+ const clonedBtns = Array.from(cloned.querySelectorAll(".fb-tile-action-btn"));
2783
+ clonedBtns.forEach((clonedBtn, i) => {
2784
+ const origBtn = originalBtns[i];
2785
+ if (origBtn) {
2786
+ clonedBtn.addEventListener("click", (e) => {
2787
+ e.stopPropagation();
2788
+ origBtn.click();
2789
+ });
2790
+ }
2791
+ });
2792
+ }
2143
2793
  function renderLocalImagePreview(container, file, fileName, state) {
2144
2794
  const img = document.createElement("img");
2145
2795
  img.className = "w-full h-full object-contain";
2796
+ img.style.background = "var(--fb-file-upload-bg-color,#f3f4f6)";
2146
2797
  img.alt = fileName || t("previewAlt", state);
2147
2798
  const reader = new FileReader();
2148
2799
  reader.onload = (e) => {
@@ -2152,23 +2803,27 @@ function renderLocalImagePreview(container, file, fileName, state) {
2152
2803
  reader.readAsDataURL(file);
2153
2804
  container.appendChild(img);
2154
2805
  }
2155
- function renderLocalVideoPreview(container, file, videoType, resourceId, state, deps) {
2156
- const videoUrl = URL.createObjectURL(file);
2806
+ function setupDragDropless(container, _deps) {
2157
2807
  container.onclick = null;
2158
2808
  const newContainer = container.cloneNode(false);
2159
2809
  if (container.parentNode) {
2160
2810
  container.parentNode.replaceChild(newContainer, container);
2161
2811
  }
2812
+ return newContainer;
2813
+ }
2814
+ function renderLocalVideoPreview(container, file, videoType, resourceId, state, deps) {
2815
+ const videoUrl = URL.createObjectURL(file);
2816
+ const newContainer = setupDragDropless(container);
2162
2817
  newContainer.innerHTML = `
2163
- <div class="relative group h-full">
2818
+ <div class="fb-video-preview-wrap">
2164
2819
  <video class="w-full h-full object-contain" controls preload="auto" muted src="${videoUrl}">
2165
2820
  ${escapeHtml(t("videoNotSupported", state))}
2166
2821
  </video>
2167
- <div class="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity z-10 flex gap-1">
2168
- <button class="bg-red-600 bg-opacity-75 hover:bg-opacity-90 text-white p-1 rounded text-xs delete-file-btn">
2822
+ <div class="fb-video-btn-overlay">
2823
+ <button class="fb-video-btn fb-video-btn-delete delete-file-btn">
2169
2824
  ${escapeHtml(t("removeElement", state))}
2170
2825
  </button>
2171
- <button class="bg-gray-800 bg-opacity-75 hover:bg-opacity-90 text-white p-1 rounded text-xs change-file-btn">
2826
+ <button class="fb-video-btn fb-video-btn-change change-file-btn">
2172
2827
  ${escapeHtml(t("changeButton", state))}
2173
2828
  </button>
2174
2829
  </div>
@@ -2178,20 +2833,15 @@ function renderLocalVideoPreview(container, file, videoType, resourceId, state,
2178
2833
  return newContainer;
2179
2834
  }
2180
2835
  function attachVideoButtonHandlers(container, resourceId, state, deps) {
2181
- const changeBtn = container.querySelector(
2182
- ".change-file-btn"
2183
- );
2836
+ const changeBtn = container.querySelector(".change-file-btn");
2184
2837
  if (changeBtn) {
2185
2838
  changeBtn.onclick = (e) => {
2839
+ var _a;
2186
2840
  e.stopPropagation();
2187
- if (deps == null ? void 0 : deps.picker) {
2188
- deps.picker.click();
2189
- }
2841
+ (_a = deps == null ? void 0 : deps.picker) == null ? void 0 : _a.click();
2190
2842
  };
2191
2843
  }
2192
- const deleteBtn = container.querySelector(
2193
- ".delete-file-btn"
2194
- );
2844
+ const deleteBtn = container.querySelector(".delete-file-btn");
2195
2845
  if (deleteBtn) {
2196
2846
  deleteBtn.onclick = (e) => {
2197
2847
  e.stopPropagation();
@@ -2211,9 +2861,6 @@ function handleVideoDelete(container, resourceId, state, deps) {
2211
2861
  if (deps == null ? void 0 : deps.fileUploadHandler) {
2212
2862
  container.onclick = deps.fileUploadHandler;
2213
2863
  }
2214
- if (deps == null ? void 0 : deps.dragHandler) {
2215
- setupDragAndDrop(container, deps.dragHandler);
2216
- }
2217
2864
  container.innerHTML = `
2218
2865
  <div class="flex flex-col items-center justify-center h-full text-gray-400">
2219
2866
  <svg class="w-6 h-6 mb-2" fill="currentColor" viewBox="0 0 24 24">
@@ -2222,16 +2869,9 @@ function handleVideoDelete(container, resourceId, state, deps) {
2222
2869
  <div class="text-sm text-center">${escapeHtml(t("clickDragText", state))}</div>
2223
2870
  </div>
2224
2871
  `;
2225
- }
2226
- function renderUploadedVideoPreview(container, thumbnailUrl, _videoType, state) {
2227
- const video = document.createElement("video");
2228
- video.className = "w-full h-full object-contain";
2229
- video.controls = true;
2230
- video.preload = "metadata";
2231
- video.muted = true;
2232
- video.src = thumbnailUrl;
2233
- video.appendChild(document.createTextNode(t("videoNotSupported", state)));
2234
- container.appendChild(video);
2872
+ if (deps == null ? void 0 : deps.setupDrop) {
2873
+ deps.setupDrop(container);
2874
+ }
2235
2875
  }
2236
2876
  function renderDeleteButton(container, resourceId, state) {
2237
2877
  addDeleteButton(container, state, () => {
@@ -2254,13 +2894,12 @@ function renderDeleteButton(container, resourceId, state) {
2254
2894
  });
2255
2895
  }
2256
2896
  async function renderLocalFilePreview(container, meta, fileName, resourceId, isReadonly, state, deps) {
2257
- if (!meta.file || !(meta.file instanceof File)) {
2258
- return;
2259
- }
2260
- if (meta.type && meta.type.startsWith("image/")) {
2897
+ var _a, _b, _c;
2898
+ if (!meta.file || !(meta.file instanceof File)) return;
2899
+ if ((_a = meta.type) == null ? void 0 : _a.startsWith("image/")) {
2261
2900
  renderLocalImagePreview(container, meta.file, fileName, state);
2262
- } else if (meta.type && meta.type.startsWith("video/")) {
2263
- const newContainer = renderLocalVideoPreview(
2901
+ } else if ((_b = meta.type) == null ? void 0 : _b.startsWith("video/")) {
2902
+ container = renderLocalVideoPreview(
2264
2903
  container,
2265
2904
  meta.file,
2266
2905
  meta.type,
@@ -2268,15 +2907,25 @@ async function renderLocalFilePreview(container, meta, fileName, resourceId, isR
2268
2907
  state,
2269
2908
  deps
2270
2909
  );
2271
- container = newContainer;
2272
2910
  } else {
2273
- container.innerHTML = `<div class="flex flex-col items-center justify-center h-full text-gray-400"><div class="text-2xl mb-2">\u{1F4C1}</div><div class="text-sm">${escapeHtml(fileName)}</div></div>`;
2911
+ container.innerHTML = `<div class="flex flex-col items-center justify-center h-full text-gray-400"><div style="font-size:36px;" class="mb-2">\u{1F4C1}</div><div class="text-sm">${escapeHtml(fileName)}</div></div>`;
2274
2912
  }
2275
- if (!isReadonly && !(meta.type && meta.type.startsWith("video/"))) {
2913
+ if (!isReadonly && !((_c = meta.type) == null ? void 0 : _c.startsWith("video/"))) {
2276
2914
  renderDeleteButton(container, resourceId, state);
2277
2915
  }
2278
2916
  }
2917
+ function renderUploadedVideoPreview(container, thumbnailUrl, state) {
2918
+ const video = document.createElement("video");
2919
+ video.className = "w-full h-full object-contain";
2920
+ video.controls = true;
2921
+ video.preload = "metadata";
2922
+ video.muted = true;
2923
+ video.src = thumbnailUrl;
2924
+ video.appendChild(document.createTextNode(t("videoNotSupported", state)));
2925
+ container.appendChild(video);
2926
+ }
2279
2927
  async function renderUploadedFilePreview(container, resourceId, fileName, meta, state) {
2928
+ var _a;
2280
2929
  if (!state.config.getThumbnail) {
2281
2930
  setEmptyFileContainer(container, state);
2282
2931
  return;
@@ -2285,11 +2934,12 @@ async function renderUploadedFilePreview(container, resourceId, fileName, meta,
2285
2934
  const thumbnailUrl = await state.config.getThumbnail(resourceId);
2286
2935
  if (thumbnailUrl) {
2287
2936
  clear(container);
2288
- if (meta && meta.type && meta.type.startsWith("video/")) {
2289
- renderUploadedVideoPreview(container, thumbnailUrl, meta.type, state);
2937
+ if ((_a = meta == null ? void 0 : meta.type) == null ? void 0 : _a.startsWith("video/")) {
2938
+ renderUploadedVideoPreview(container, thumbnailUrl, state);
2290
2939
  } else {
2291
2940
  const img = document.createElement("img");
2292
2941
  img.className = "w-full h-full object-contain";
2942
+ img.style.background = "var(--fb-file-upload-bg-color,#f3f4f6)";
2293
2943
  img.alt = fileName || t("previewAlt", state);
2294
2944
  img.src = thumbnailUrl;
2295
2945
  container.appendChild(img);
@@ -2318,9 +2968,6 @@ async function renderFilePreview(container, resourceId, state, options = {}) {
2318
2968
  );
2319
2969
  }
2320
2970
  clear(container);
2321
- if (isReadonly) {
2322
- container.classList.add("cursor-pointer");
2323
- }
2324
2971
  const meta = state.resourceIndex.get(resourceId);
2325
2972
  if (meta && meta.file && meta.file instanceof File) {
2326
2973
  await renderLocalFilePreview(
@@ -2333,369 +2980,296 @@ async function renderFilePreview(container, resourceId, state, options = {}) {
2333
2980
  deps
2334
2981
  );
2335
2982
  } else {
2336
- await renderUploadedFilePreview(
2337
- container,
2338
- resourceId,
2339
- fileName,
2340
- meta,
2341
- state
2342
- );
2983
+ await renderUploadedFilePreview(container, resourceId, fileName, meta, state);
2343
2984
  const isVideo = (_a = meta == null ? void 0 : meta.type) == null ? void 0 : _a.startsWith("video/");
2344
2985
  if (!isReadonly && !isVideo) {
2345
2986
  renderDeleteButton(container, resourceId, state);
2346
2987
  }
2347
2988
  }
2348
2989
  }
2349
- async function renderFilePreviewReadonly(resourceId, state, fileName) {
2990
+ function resolveFileName(resourceId, meta, fileName) {
2991
+ var _a;
2992
+ if (fileName) return fileName;
2993
+ if ((_a = meta == null ? void 0 : meta.name) == null ? void 0 : _a.includes(".")) return meta.name;
2994
+ const basename = resourceId.includes("/") ? resourceId.split("/").pop() : resourceId;
2995
+ return (basename == null ? void 0 : basename.includes(".")) ? basename : "";
2996
+ }
2997
+ async function renderFilePreviewReadonly(resourceId, state, fileName, options = {}) {
2350
2998
  var _a, _b;
2351
2999
  const meta = state.resourceIndex.get(resourceId);
2352
- const actualFileName = (meta == null ? void 0 : meta.name) || resourceId.split("/").pop() || "file";
2353
- const isPSD = actualFileName.toLowerCase().match(/\.psd$/);
2354
- const fileResult = document.createElement("div");
2355
- fileResult.className = isPSD ? "space-y-2" : "space-y-3";
2356
- const previewContainer = document.createElement("div");
2357
- if (isPSD) {
2358
- previewContainer.className = "bg-gray-100 rounded-lg overflow-hidden cursor-pointer hover:opacity-90 transition-opacity flex items-center p-3 max-w-sm";
2359
- } else {
2360
- previewContainer.className = "bg-gray-100 rounded-lg overflow-hidden cursor-pointer hover:opacity-90 transition-opacity";
2361
- }
2362
- const isImage = !isPSD && (((_a = meta == null ? void 0 : meta.type) == null ? void 0 : _a.startsWith("image/")) || actualFileName.toLowerCase().match(/\.(jpg|jpeg|png|gif|webp)$/));
2363
- const isVideo = ((_b = meta == null ? void 0 : meta.type) == null ? void 0 : _b.startsWith("video/")) || actualFileName.toLowerCase().match(/\.(mp4|webm|avi|mov)$/);
2364
- if (isImage) {
3000
+ const actualFileName = resolveFileName(resourceId, meta, fileName);
3001
+ const { canRemove = false, removeHandler = null } = options;
3002
+ const isImage = ((_a = meta == null ? void 0 : meta.type) == null ? void 0 : _a.startsWith("image/")) || Boolean(actualFileName.toLowerCase().match(/\.(jpg|jpeg|png|gif|webp)$/));
3003
+ const isVideo = ((_b = meta == null ? void 0 : meta.type) == null ? void 0 : _b.startsWith("video/")) || Boolean(actualFileName.toLowerCase().match(/\.(mp4|webm|avi|mov)$/));
3004
+ const tile = createFileTile();
3005
+ tile.classList.add("fb-tile-resource");
3006
+ tile.style.cursor = "pointer";
3007
+ if (actualFileName) {
3008
+ tile.title = actualFileName;
3009
+ }
3010
+ const localFileUrl = (meta == null ? void 0 : meta.file) instanceof File ? getLocalFileUrl(meta.file) : null;
3011
+ const resolveOpenUrl = async () => {
3012
+ if (state.config.getDownloadUrl) return state.config.getDownloadUrl(resourceId);
3013
+ if (state.config.getThumbnail) return state.config.getThumbnail(resourceId);
3014
+ return localFileUrl;
3015
+ };
3016
+ tile.onclick = async () => {
3017
+ const url = await resolveOpenUrl();
3018
+ if (url) {
3019
+ window.open(url, "_blank");
3020
+ } else if (state.config.downloadFile) {
3021
+ state.config.downloadFile(resourceId, actualFileName);
3022
+ } else {
3023
+ forceDownload(resourceId, actualFileName, state).catch((err) => {
3024
+ console.error("Download failed:", err);
3025
+ });
3026
+ }
3027
+ };
3028
+ const actionsEl = createTileActions({
3029
+ canRemove,
3030
+ removeHandler,
3031
+ state,
3032
+ resourceId,
3033
+ fileName: actualFileName,
3034
+ meta
3035
+ });
3036
+ const resolveImageDisplayUrl = async () => {
2365
3037
  if (state.config.getThumbnail) {
2366
3038
  try {
2367
- const thumbnailUrl = await state.config.getThumbnail(resourceId);
2368
- if (thumbnailUrl) {
2369
- previewContainer.innerHTML = `<img src="${thumbnailUrl}" alt="${escapeHtml(actualFileName)}" class="w-full h-auto">`;
2370
- } else {
2371
- previewContainer.innerHTML = `<div class="aspect-video flex items-center justify-center text-gray-400"><div class="text-center"><div class="text-4xl mb-2">\u{1F5BC}\uFE0F</div><div class="text-sm">${escapeHtml(actualFileName)}</div></div></div>`;
2372
- }
2373
- } catch (error) {
2374
- console.warn("getThumbnail failed for", resourceId, error);
2375
- previewContainer.innerHTML = `<div class="aspect-video flex items-center justify-center text-gray-400"><div class="text-center"><div class="text-4xl mb-2">\u{1F5BC}\uFE0F</div><div class="text-sm">${escapeHtml(actualFileName)}</div></div></div>`;
3039
+ const url = await state.config.getThumbnail(resourceId);
3040
+ if (url) return url;
3041
+ } catch {
2376
3042
  }
3043
+ }
3044
+ return localFileUrl;
3045
+ };
3046
+ const renderImageFallback = () => {
3047
+ tile.innerHTML = `<div style="display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;font-size:36px;">\u{1F5BC}\uFE0F</div>`;
3048
+ tile.appendChild(actionsEl);
3049
+ };
3050
+ if (isImage) {
3051
+ const displayUrl = await resolveImageDisplayUrl();
3052
+ if (displayUrl) {
3053
+ const img = document.createElement("img");
3054
+ img.style.cssText = "width:100%;height:100%;object-fit:contain;background:var(--fb-file-upload-bg-color,#f3f4f6);";
3055
+ img.alt = actualFileName;
3056
+ img.src = displayUrl;
3057
+ tile.appendChild(img);
3058
+ tile.appendChild(actionsEl);
3059
+ attachZoomHover(tile, displayUrl, actualFileName, actionsEl);
2377
3060
  } else {
2378
- previewContainer.innerHTML = `<div class="aspect-video flex items-center justify-center text-gray-400"><div class="text-center"><div class="text-4xl mb-2">\u{1F5BC}\uFE0F</div><div class="text-sm">${escapeHtml(actualFileName)}</div></div></div>`;
3061
+ renderImageFallback();
2379
3062
  }
2380
3063
  } else if (isVideo) {
2381
3064
  if (state.config.getThumbnail) {
2382
3065
  try {
2383
3066
  const videoUrl = await state.config.getThumbnail(resourceId);
2384
3067
  if (videoUrl) {
2385
- previewContainer.innerHTML = `
2386
- <div class="relative group">
2387
- <video class="w-full h-auto" controls preload="auto" muted src="${videoUrl}">
2388
- ${escapeHtml(t("videoNotSupported", state))}
2389
- </video>
2390
- <div class="absolute inset-0 bg-black bg-opacity-20 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center pointer-events-none">
2391
- <div class="bg-white bg-opacity-90 rounded-full p-3">
2392
- <svg class="w-8 h-8 text-gray-800" fill="currentColor" viewBox="0 0 24 24">
2393
- <path d="M8 5v14l11-7z"/>
2394
- </svg>
2395
- </div>
3068
+ tile.innerHTML = `
3069
+ <img style="width:100%;height:100%;object-fit:contain;background:var(--fb-file-upload-bg-color,#f3f4f6);" alt="${escapeHtml(actualFileName)}" src="${videoUrl}">
3070
+ <div class="fb-video-overlay">
3071
+ <div class="fb-play-btn" style="width:22px;height:22px;">
3072
+ <svg width="10" height="12" viewBox="0 0 10 12" fill="currentColor"><path d="M0 0l10 6-10 6z"/></svg>
2396
3073
  </div>
2397
- </div>
2398
- `;
3074
+ </div>`;
3075
+ tile.appendChild(actionsEl);
2399
3076
  } else {
2400
- previewContainer.innerHTML = `<div class="aspect-video flex items-center justify-center text-gray-400"><div class="text-center"><div class="text-4xl mb-2">\u{1F3A5}</div><div class="text-sm">${escapeHtml(actualFileName)}</div></div></div>`;
3077
+ tile.innerHTML = `<div style="display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;font-size:36px;">\u{1F3A5}</div>`;
3078
+ tile.appendChild(actionsEl);
2401
3079
  }
2402
- } catch (error) {
2403
- console.warn("getThumbnail failed for video", resourceId, error);
2404
- previewContainer.innerHTML = `<div class="aspect-video flex items-center justify-center text-gray-400"><div class="text-center"><div class="text-4xl mb-2">\u{1F3A5}</div><div class="text-sm">${escapeHtml(actualFileName)}</div></div></div>`;
3080
+ } catch {
3081
+ tile.innerHTML = `<div style="display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;font-size:36px;">\u{1F3A5}</div>`;
3082
+ tile.appendChild(actionsEl);
2405
3083
  }
2406
3084
  } else {
2407
- previewContainer.innerHTML = `<div class="aspect-video flex items-center justify-center text-gray-400"><div class="text-center"><div class="text-4xl mb-2">\u{1F3A5}</div><div class="text-sm">${escapeHtml(actualFileName)}</div></div></div>`;
3085
+ tile.innerHTML = `<div style="display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;font-size:36px;">\u{1F3A5}</div>`;
3086
+ tile.appendChild(actionsEl);
2408
3087
  }
2409
3088
  } else {
2410
- const fileIcon = isPSD ? "\u{1F3A8}" : "\u{1F4C1}";
2411
- const fileDescription = isPSD ? "PSD File" : "Document";
2412
- if (isPSD) {
2413
- previewContainer.innerHTML = `
2414
- <div class="flex items-center space-x-3">
2415
- <div class="text-3xl text-gray-400">${fileIcon}</div>
2416
- <div class="flex-1 min-w-0">
2417
- <div class="text-sm font-medium text-gray-900 truncate">${escapeHtml(actualFileName)}</div>
2418
- <div class="text-xs text-gray-500">${fileDescription}</div>
2419
- </div>
2420
- </div>
2421
- `;
2422
- } else {
2423
- previewContainer.innerHTML = `<div class="aspect-video flex items-center justify-center text-gray-400"><div class="text-center"><div class="text-4xl mb-2">${fileIcon}</div><div class="text-sm">${escapeHtml(actualFileName)}</div><div class="text-xs text-gray-500 mt-1">${fileDescription}</div></div></div>`;
3089
+ if (state.config.getThumbnail) {
3090
+ try {
3091
+ const thumbUrl = await state.config.getThumbnail(resourceId);
3092
+ if (thumbUrl) {
3093
+ const img = document.createElement("img");
3094
+ img.style.cssText = "width:100%;height:100%;object-fit:contain;background:var(--fb-file-upload-bg-color,#f3f4f6);";
3095
+ img.alt = actualFileName || resourceId;
3096
+ img.src = thumbUrl;
3097
+ tile.appendChild(img);
3098
+ tile.appendChild(actionsEl);
3099
+ return tile;
3100
+ }
3101
+ } catch {
3102
+ }
2424
3103
  }
3104
+ const captionHtml = actualFileName ? `<div class="fb-tile-label">${escapeHtml(actualFileName.length > 10 ? actualFileName.substring(0, 8) + "\u2026" : actualFileName)}</div>
3105
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor"><path d="M19 9h-4V3H9v6H5l7 7 7-7zm-7 9H5v2h14v-2h-7z"/></svg>` : "";
3106
+ tile.innerHTML = `
3107
+ <div style="display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;padding:6px;gap:4px;">
3108
+ <div style="font-size:36px;">\u{1F4C1}</div>
3109
+ ${captionHtml}
3110
+ </div>`;
3111
+ tile.appendChild(actionsEl);
2425
3112
  }
2426
- const fileNameElement = document.createElement("p");
2427
- fileNameElement.className = isPSD ? "hidden" : "text-sm font-medium text-gray-900 text-center";
2428
- fileNameElement.textContent = actualFileName;
2429
- const downloadButton = document.createElement("button");
2430
- downloadButton.className = "w-full px-3 py-2 text-sm bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors";
2431
- downloadButton.textContent = t("downloadButton", state);
2432
- downloadButton.onclick = (e) => {
2433
- e.preventDefault();
2434
- e.stopPropagation();
2435
- if (state.config.downloadFile) {
2436
- state.config.downloadFile(resourceId, actualFileName);
2437
- } else {
2438
- forceDownload(resourceId, actualFileName, state);
2439
- }
2440
- };
2441
- fileResult.appendChild(previewContainer);
2442
- fileResult.appendChild(fileNameElement);
2443
- fileResult.appendChild(downloadButton);
2444
- return fileResult;
3113
+ return tile;
2445
3114
  }
2446
- function renderResourcePills(container, rids, state, onRemove, hint, countInfo) {
2447
- clear(container);
2448
- const buildHintLine = () => {
2449
- const parts = [t("clickDragTextMultiple", state)];
2450
- if (hint) parts.push(hint);
2451
- if (countInfo) parts.push(countInfo);
2452
- return parts.join(" \u2022 ");
2453
- };
2454
- const isInitialRender = !container.classList.contains("grid");
2455
- if ((!rids || rids.length === 0) && isInitialRender) {
2456
- const gridContainer2 = document.createElement("div");
2457
- gridContainer2.className = "grid grid-cols-4 gap-3 mb-3";
2458
- for (let i = 0; i < 4; i++) {
2459
- const slot = document.createElement("div");
2460
- slot.className = "aspect-square bg-gray-100 border-2 border-dashed border-gray-300 rounded flex items-center justify-center cursor-pointer hover:border-gray-400 transition-colors";
2461
- const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
2462
- svg.setAttribute("class", "w-12 h-12 text-gray-400");
2463
- svg.setAttribute("fill", "currentColor");
2464
- svg.setAttribute("viewBox", "0 0 24 24");
2465
- const path = document.createElementNS(
2466
- "http://www.w3.org/2000/svg",
2467
- "path"
2468
- );
2469
- path.setAttribute(
2470
- "d",
2471
- "M21 19V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2zM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5l3.5-4.5z"
2472
- );
2473
- svg.appendChild(path);
2474
- slot.appendChild(svg);
2475
- slot.onclick = () => {
2476
- let filesWrapper = container.parentElement;
2477
- while (filesWrapper && !filesWrapper.classList.contains("space-y-2")) {
2478
- filesWrapper = filesWrapper.parentElement;
2479
- }
2480
- if (!filesWrapper && container.classList.contains("space-y-2")) {
2481
- filesWrapper = container;
2482
- }
2483
- const fileInput = filesWrapper == null ? void 0 : filesWrapper.querySelector(
2484
- 'input[type="file"]'
2485
- );
2486
- if (fileInput) fileInput.click();
2487
- };
2488
- gridContainer2.appendChild(slot);
2489
- }
2490
- const hintText2 = document.createElement("div");
2491
- hintText2.className = "text-center text-xs text-gray-500 mt-2";
2492
- hintText2.textContent = buildHintLine();
2493
- container.appendChild(gridContainer2);
2494
- container.appendChild(hintText2);
2495
- return;
2496
- }
2497
- const gridContainer = document.createElement("div");
2498
- gridContainer.className = "files-list grid grid-cols-4 gap-3";
2499
- const currentImagesCount = rids ? rids.length : 0;
2500
- const rowsNeeded = Math.floor(currentImagesCount / 4) + 1;
2501
- const slotsNeeded = rowsNeeded * 4;
2502
- for (let i = 0; i < slotsNeeded; i++) {
2503
- const slot = document.createElement("div");
2504
- if (rids && i < rids.length) {
2505
- const rid = rids[i];
2506
- const meta = state.resourceIndex.get(rid);
2507
- slot.className = "resource-pill aspect-square bg-gray-100 rounded-lg overflow-hidden relative group border border-gray-300";
2508
- slot.dataset.resourceId = rid;
2509
- renderThumbnailForResource(slot, rid, meta, state).catch((err) => {
2510
- console.error("Failed to render thumbnail:", err);
2511
- slot.innerHTML = `<div class="flex flex-col items-center justify-center h-full text-gray-400">
2512
- <div class="text-2xl mb-1">\u{1F4C1}</div>
2513
- <div class="text-xs">${escapeHtml(t("previewError", state))}</div>
2514
- </div>`;
2515
- });
2516
- if (onRemove) {
2517
- const overlay = document.createElement("div");
2518
- overlay.className = "absolute inset-0 bg-black bg-opacity-50 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center";
2519
- const removeBtn = document.createElement("button");
2520
- removeBtn.className = "bg-red-600 text-white px-2 py-1 rounded text-xs";
2521
- removeBtn.textContent = t("removeElement", state);
2522
- removeBtn.onclick = (e) => {
2523
- e.stopPropagation();
2524
- onRemove(rid);
2525
- };
2526
- overlay.appendChild(removeBtn);
2527
- slot.appendChild(overlay);
2528
- }
2529
- } else {
2530
- slot.className = "aspect-square bg-gray-100 border-2 border-dashed border-gray-300 rounded-lg flex items-center justify-center cursor-pointer hover:border-gray-400 transition-colors";
2531
- slot.innerHTML = '<svg class="w-12 h-12 text-gray-400" fill="currentColor" viewBox="0 0 24 24"><path d="M21 19V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2zM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5l3.5-4.5z"/></svg>';
2532
- slot.onclick = () => {
2533
- let filesWrapper = container.parentElement;
2534
- while (filesWrapper && !filesWrapper.classList.contains("space-y-2")) {
2535
- filesWrapper = filesWrapper.parentElement;
2536
- }
2537
- if (!filesWrapper && container.classList.contains("space-y-2")) {
2538
- filesWrapper = container;
2539
- }
2540
- const fileInput = filesWrapper == null ? void 0 : filesWrapper.querySelector(
2541
- 'input[type="file"]'
2542
- );
2543
- if (fileInput) fileInput.click();
2544
- };
2545
- }
2546
- gridContainer.appendChild(slot);
2547
- }
2548
- container.appendChild(gridContainer);
2549
- const hintText = document.createElement("div");
2550
- hintText.className = "text-center text-xs text-gray-500 mt-2";
2551
- hintText.textContent = buildHintLine();
2552
- container.appendChild(hintText);
2553
- }
2554
- function renderThumbnailError(slot, state, iconSize = "w-12 h-12") {
2555
- slot.innerHTML = `<div class="flex flex-col items-center justify-center h-full text-gray-400">
2556
- <svg class="${escapeHtml(iconSize)} text-red-400" fill="currentColor" viewBox="0 0 24 24">
2557
- <path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z"/>
2558
- </svg>
2559
- <div class="text-xs mt-1 text-red-600">${escapeHtml(t("previewError", state))}</div>
2560
- </div>`;
3115
+ async function renderSingleFileEditTile(fileContainer, resourceId, state, deps) {
3116
+ var _a, _b, _c;
3117
+ const meta = state.resourceIndex.get(resourceId);
3118
+ const fileName = (_b = (_a = meta == null ? void 0 : meta.name) != null ? _a : resourceId.split("/").pop()) != null ? _b : "";
3119
+ const removeHandler = (_c = deps.onRemove) != null ? _c : null;
3120
+ const tile = await renderFilePreviewReadonly(resourceId, state, fileName, {
3121
+ canRemove: true,
3122
+ removeHandler
3123
+ });
3124
+ fileContainer.className = "file-preview-container";
3125
+ fileContainer.removeAttribute("style");
3126
+ clear(fileContainer);
3127
+ fileContainer.appendChild(tile);
2561
3128
  }
2562
- async function renderThumbnailForResource(slot, rid, meta, state) {
2563
- var _a, _b;
2564
- if (meta && ((_a = meta.type) == null ? void 0 : _a.startsWith("image/"))) {
3129
+ async function fillTileContent(tile, rid, meta, state, actionsEl) {
3130
+ var _a, _b, _c;
3131
+ if ((_a = meta == null ? void 0 : meta.type) == null ? void 0 : _a.startsWith("image/")) {
2565
3132
  if (meta.file && meta.file instanceof File) {
2566
3133
  const img = document.createElement("img");
2567
- img.className = "w-full h-full object-contain";
3134
+ img.style.cssText = "width:100%;height:100%;object-fit:contain;background:var(--fb-file-upload-bg-color,#f3f4f6);";
2568
3135
  img.alt = meta.name;
2569
3136
  const reader = new FileReader();
2570
3137
  reader.onload = (e) => {
2571
3138
  var _a2;
2572
3139
  img.src = ((_a2 = e.target) == null ? void 0 : _a2.result) || "";
3140
+ attachZoomHover(tile, img.src, meta.name, actionsEl != null ? actionsEl : null);
2573
3141
  };
2574
3142
  reader.readAsDataURL(meta.file);
2575
- slot.appendChild(img);
3143
+ tile.appendChild(img);
2576
3144
  } else if (state.config.getThumbnail) {
2577
3145
  try {
2578
3146
  const url = await state.config.getThumbnail(rid);
2579
3147
  if (url) {
2580
3148
  const img = document.createElement("img");
2581
- img.className = "w-full h-full object-contain";
3149
+ img.style.cssText = "width:100%;height:100%;object-fit:contain;background:var(--fb-file-upload-bg-color,#f3f4f6);";
2582
3150
  img.alt = meta.name;
2583
3151
  img.src = url;
2584
- slot.appendChild(img);
3152
+ tile.appendChild(img);
3153
+ attachZoomHover(tile, url, meta.name, actionsEl != null ? actionsEl : null);
2585
3154
  } else {
2586
- slot.innerHTML = `<div class="flex flex-col items-center justify-center h-full text-gray-400">
2587
- <svg class="w-12 h-12" fill="currentColor" viewBox="0 0 24 24">
2588
- <path d="M21 19V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2zM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5l3.5-4.5z"/>
2589
- </svg>
2590
- </div>`;
3155
+ tile.innerHTML = `<div style="display:flex;align-items:center;justify-content:center;height:100%;font-size:36px;">\u{1F5BC}\uFE0F</div>`;
2591
3156
  }
2592
3157
  } catch (error) {
2593
3158
  const err = error instanceof Error ? error : new Error(String(error));
2594
- if (state.config.onThumbnailError) {
2595
- state.config.onThumbnailError(err, rid);
2596
- }
2597
- renderThumbnailError(slot, state);
3159
+ if (state.config.onThumbnailError) state.config.onThumbnailError(err, rid);
3160
+ tile.innerHTML = `<div style="display:flex;align-items:center;justify-content:center;height:100%;font-size:16px;color:var(--fb-error-color,#ef4444);">\u2715</div>`;
2598
3161
  }
2599
3162
  } else {
2600
- slot.innerHTML = `<div class="flex flex-col items-center justify-center h-full text-gray-400">
2601
- <svg class="w-12 h-12" fill="currentColor" viewBox="0 0 24 24">
2602
- <path d="M21 19V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2zM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5l3.5-4.5z"/>
2603
- </svg>
2604
- </div>`;
3163
+ tile.innerHTML = `<div style="display:flex;align-items:center;justify-content:center;height:100%;font-size:36px;">\u{1F5BC}\uFE0F</div>`;
2605
3164
  }
2606
- } else if (meta && ((_b = meta.type) == null ? void 0 : _b.startsWith("video/"))) {
3165
+ if (actionsEl) tile.appendChild(actionsEl);
3166
+ } else if ((_b = meta == null ? void 0 : meta.type) == null ? void 0 : _b.startsWith("video/")) {
2607
3167
  if (meta.file && meta.file instanceof File) {
2608
3168
  const videoUrl = URL.createObjectURL(meta.file);
2609
- slot.innerHTML = `
2610
- <div class="relative group h-full w-full">
2611
- <video class="w-full h-full object-contain" preload="metadata" muted src="${videoUrl}">
2612
- </video>
2613
- <div class="absolute inset-0 bg-black bg-opacity-30 flex items-center justify-center">
2614
- <div class="bg-white bg-opacity-90 rounded-full p-1">
2615
- <svg class="w-4 h-4 text-gray-800" fill="currentColor" viewBox="0 0 24 24">
2616
- <path d="M8 5v14l11-7z"/>
2617
- </svg>
2618
- </div>
3169
+ tile.innerHTML = `
3170
+ <video style="width:100%;height:100%;" preload="metadata" muted src="${videoUrl}"></video>
3171
+ <div class="fb-video-overlay">
3172
+ <div class="fb-play-btn" style="width:20px;height:20px;">
3173
+ <svg width="8" height="10" viewBox="0 0 8 10" fill="currentColor"><path d="M0 0l8 5-8 5z"/></svg>
2619
3174
  </div>
2620
- </div>
2621
- `;
3175
+ </div>`;
2622
3176
  } else if (state.config.getThumbnail) {
2623
3177
  try {
2624
3178
  const videoUrl = await state.config.getThumbnail(rid);
2625
3179
  if (videoUrl) {
2626
- slot.innerHTML = `
2627
- <div class="relative group h-full w-full">
2628
- <video class="w-full h-full object-contain" preload="metadata" muted src="${videoUrl}">
2629
- </video>
2630
- <div class="absolute inset-0 bg-black bg-opacity-30 flex items-center justify-center">
2631
- <div class="bg-white bg-opacity-90 rounded-full p-1">
2632
- <svg class="w-4 h-4 text-gray-800" fill="currentColor" viewBox="0 0 24 24">
2633
- <path d="M8 5v14l11-7z"/>
2634
- </svg>
2635
- </div>
3180
+ tile.innerHTML = `
3181
+ <img style="width:100%;height:100%;object-fit:contain;background:var(--fb-file-upload-bg-color,#f3f4f6);" alt="${escapeHtml(meta.name)}" src="${videoUrl}">
3182
+ <div class="fb-video-overlay">
3183
+ <div class="fb-play-btn" style="width:20px;height:20px;">
3184
+ <svg width="8" height="10" viewBox="0 0 8 10" fill="currentColor"><path d="M0 0l8 5-8 5z"/></svg>
2636
3185
  </div>
2637
- </div>
2638
- `;
3186
+ </div>`;
2639
3187
  } else {
2640
- slot.innerHTML = `<div class="flex flex-col items-center justify-center h-full text-gray-400">
2641
- <svg class="w-8 h-8" fill="currentColor" viewBox="0 0 24 24">
2642
- <path d="M8 5v14l11-7z"/>
2643
- </svg>
2644
- <div class="text-xs mt-1">${escapeHtml((meta == null ? void 0 : meta.name) || "Video")}</div>
2645
- </div>`;
3188
+ tile.innerHTML = `<div style="display:flex;align-items:center;justify-content:center;height:100%;font-size:36px;">\u{1F3A5}</div>`;
2646
3189
  }
2647
3190
  } catch (error) {
2648
3191
  const err = error instanceof Error ? error : new Error(String(error));
2649
- if (state.config.onThumbnailError) {
2650
- state.config.onThumbnailError(err, rid);
2651
- }
2652
- renderThumbnailError(slot, state, "w-8 h-8");
3192
+ if (state.config.onThumbnailError) state.config.onThumbnailError(err, rid);
3193
+ tile.innerHTML = `<div style="display:flex;align-items:center;justify-content:center;height:100%;font-size:16px;color:var(--fb-error-color,#ef4444);">\u2715</div>`;
2653
3194
  }
2654
3195
  } else {
2655
- slot.innerHTML = `<div class="flex flex-col items-center justify-center h-full text-gray-400">
2656
- <svg class="w-8 h-8" fill="currentColor" viewBox="0 0 24 24">
2657
- <path d="M8 5v14l11-7z"/>
2658
- </svg>
2659
- <div class="text-xs mt-1">${escapeHtml((meta == null ? void 0 : meta.name) || "Video")}</div>
2660
- </div>`;
3196
+ tile.innerHTML = `<div style="display:flex;align-items:center;justify-content:center;height:100%;font-size:36px;">\u{1F3A5}</div>`;
2661
3197
  }
3198
+ if (actionsEl) tile.appendChild(actionsEl);
2662
3199
  } else {
2663
- slot.innerHTML = `<div class="flex flex-col items-center justify-center h-full text-gray-400">
2664
- <div class="text-2xl mb-1">\u{1F4C1}</div>
2665
- <div class="text-xs">${escapeHtml((meta == null ? void 0 : meta.name) || "File")}</div>
2666
- </div>`;
3200
+ const name = (_c = meta == null ? void 0 : meta.name) != null ? _c : "";
3201
+ const hasExtension = name.includes(".");
3202
+ const captionHtml = hasExtension ? `<div class="fb-tile-label">${escapeHtml(name.length > 10 ? name.substring(0, 8) + "\u2026" : name)}</div>` : "";
3203
+ tile.innerHTML = `
3204
+ <div style="display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;padding:6px;gap:4px;">
3205
+ <div style="font-size:36px;">\u{1F4C1}</div>
3206
+ ${captionHtml}
3207
+ </div>`;
3208
+ if (actionsEl) tile.appendChild(actionsEl);
2667
3209
  }
2668
3210
  }
2669
- function setEmptyFileContainer(fileContainer, state, hint) {
2670
- const hintHtml = hint ? `<div class="text-xs text-gray-500 mt-1">${escapeHtml(hint)}</div>` : "";
2671
- fileContainer.innerHTML = `
2672
- <div class="flex flex-col items-center justify-center h-full text-gray-400">
2673
- <svg class="w-6 h-6 mb-2" fill="currentColor" viewBox="0 0 24 24">
2674
- <path d="M21 19V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2zM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5l3.5-4.5z"/>
2675
- </svg>
2676
- <div class="text-sm text-center">${escapeHtml(t("clickDragText", state))}</div>
2677
- ${hintHtml}
2678
- </div>
2679
- `;
3211
+ async function forceDownload(resourceId, fileName, state) {
3212
+ try {
3213
+ let fileUrl = null;
3214
+ if (state.config.getDownloadUrl) {
3215
+ fileUrl = state.config.getDownloadUrl(resourceId);
3216
+ } else if (state.config.getThumbnail) {
3217
+ fileUrl = await state.config.getThumbnail(resourceId);
3218
+ }
3219
+ if (fileUrl) {
3220
+ const finalUrl = fileUrl.startsWith("http") ? fileUrl : new URL(fileUrl, window.location.href).href;
3221
+ const response = await fetch(finalUrl);
3222
+ if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
3223
+ const blob = await response.blob();
3224
+ downloadBlob(blob, fileName);
3225
+ } else {
3226
+ throw new Error("No download URL available for resource");
3227
+ }
3228
+ } catch (error) {
3229
+ const err = error instanceof Error ? error : new Error(String(error));
3230
+ if (state.config.onDownloadError) {
3231
+ state.config.onDownloadError(err, resourceId, fileName);
3232
+ }
3233
+ console.error(`File download failed for ${fileName}:`, err);
3234
+ throw err;
3235
+ }
2680
3236
  }
2681
- function showFileError(container, message) {
2682
- var _a, _b;
2683
- const existing = (_a = container.closest(".space-y-2")) == null ? void 0 : _a.querySelector(".file-error-message");
2684
- if (existing) existing.remove();
2685
- const errorEl = document.createElement("div");
2686
- errorEl.className = "file-error-message error-message";
2687
- errorEl.style.cssText = `
2688
- color: var(--fb-error-color);
2689
- font-size: var(--fb-font-size-small);
2690
- margin-top: 0.25rem;
2691
- `;
2692
- errorEl.textContent = message;
2693
- (_b = container.closest(".space-y-2")) == null ? void 0 : _b.appendChild(errorEl);
3237
+ function downloadBlob(blob, fileName) {
3238
+ try {
3239
+ const blobUrl = URL.createObjectURL(blob);
3240
+ const link = document.createElement("a");
3241
+ link.href = blobUrl;
3242
+ link.download = fileName;
3243
+ link.style.display = "none";
3244
+ document.body.appendChild(link);
3245
+ link.click();
3246
+ document.body.removeChild(link);
3247
+ setTimeout(() => {
3248
+ URL.revokeObjectURL(blobUrl);
3249
+ }, 100);
3250
+ } catch (error) {
3251
+ throw new Error(`Blob download failed: ${error.message}`);
3252
+ }
2694
3253
  }
2695
- function clearFileError(container) {
2696
- var _a;
2697
- const existing = (_a = container.closest(".space-y-2")) == null ? void 0 : _a.querySelector(".file-error-message");
2698
- if (existing) existing.remove();
3254
+
3255
+ // src/components/file/upload.ts
3256
+ async function uploadSingleFile(file, state) {
3257
+ if (!state.config.uploadFile) {
3258
+ throw new Error(
3259
+ "No upload handler configured. Set uploadHandler via FormBuilder.setUploadHandler()"
3260
+ );
3261
+ }
3262
+ try {
3263
+ const rid = await state.config.uploadFile(file);
3264
+ if (typeof rid !== "string") {
3265
+ throw new Error("Upload handler must return a string resource ID");
3266
+ }
3267
+ return rid;
3268
+ } catch (error) {
3269
+ const err = error instanceof Error ? error : new Error(String(error));
3270
+ if (state.config.onUploadError) state.config.onUploadError(err, file);
3271
+ throw new Error(`File upload failed: ${err.message}`);
3272
+ }
2699
3273
  }
2700
3274
  async function handleFileSelect(file, container, fieldName, state, deps = null, instance, allowedExtensions = [], maxSizeMB = Infinity) {
2701
3275
  var _a, _b;
@@ -2715,24 +3289,18 @@ async function handleFileSelect(file, container, fieldName, state, deps = null,
2715
3289
  return;
2716
3290
  }
2717
3291
  clearFileError(container);
3292
+ ensureFileStyles();
3293
+ container.innerHTML = `
3294
+ <div style="display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;gap:6px;padding:6px;">
3295
+ <div class="fb-spinner"></div>
3296
+ <div style="font-size:11px;color:var(--fb-text-secondary-color,#6b7280);text-align:center;">${escapeHtml(t("uploadingFile", state))}</div>
3297
+ </div>`;
2718
3298
  let rid;
2719
- if (state.config.uploadFile) {
2720
- try {
2721
- rid = await state.config.uploadFile(file);
2722
- if (typeof rid !== "string") {
2723
- throw new Error("Upload handler must return a string resource ID");
2724
- }
2725
- } catch (error) {
2726
- const err = error instanceof Error ? error : new Error(String(error));
2727
- if (state.config.onUploadError) {
2728
- state.config.onUploadError(err, file);
2729
- }
2730
- throw new Error(`File upload failed: ${err.message}`);
2731
- }
2732
- } else {
2733
- throw new Error(
2734
- "No upload handler configured. Set uploadHandler via FormBuilder.setUploadHandler()"
2735
- );
3299
+ try {
3300
+ rid = await uploadSingleFile(file, state);
3301
+ } catch (error) {
3302
+ setEmptyFileContainer(container, state);
3303
+ throw error;
2736
3304
  }
2737
3305
  state.resourceIndex.set(rid, {
2738
3306
  name: file.name,
@@ -2740,7 +3308,6 @@ async function handleFileSelect(file, container, fieldName, state, deps = null,
2740
3308
  size: file.size,
2741
3309
  uploadedAt: /* @__PURE__ */ new Date(),
2742
3310
  file
2743
- // Store the file object for local preview
2744
3311
  });
2745
3312
  let hiddenInput = (_a = container.parentElement) == null ? void 0 : _a.querySelector(
2746
3313
  'input[type="hidden"]'
@@ -2752,693 +3319,712 @@ async function handleFileSelect(file, container, fieldName, state, deps = null,
2752
3319
  (_b = container.parentElement) == null ? void 0 : _b.appendChild(hiddenInput);
2753
3320
  }
2754
3321
  hiddenInput.value = rid;
2755
- renderFilePreview(container, rid, state, {
2756
- fileName: file.name,
2757
- isReadonly: false,
2758
- deps
2759
- }).catch(console.error);
3322
+ const isVideo = file.type.startsWith("video/");
3323
+ if (!isVideo && deps) {
3324
+ renderSingleFileEditTile(container, rid, state, deps).catch(console.error);
3325
+ } else {
3326
+ renderFilePreview(container, rid, state, {
3327
+ fileName: file.name,
3328
+ isReadonly: false,
3329
+ deps
3330
+ }).catch(console.error);
3331
+ }
2760
3332
  if (instance && !state.config.readonly) {
2761
3333
  instance.triggerOnChange(fieldName, rid);
2762
3334
  }
2763
3335
  }
2764
- function setupDragAndDrop(element, dropHandler) {
2765
- element.addEventListener("dragover", (e) => {
2766
- e.preventDefault();
2767
- element.classList.add("border-blue-500", "bg-blue-50");
2768
- });
2769
- element.addEventListener("dragleave", (e) => {
2770
- e.preventDefault();
2771
- element.classList.remove("border-blue-500", "bg-blue-50");
2772
- });
2773
- element.addEventListener("drop", (e) => {
3336
+ function filterAndSlice(allFiles, currentCount, constraints, state) {
3337
+ const rejectedByExt = allFiles.filter(
3338
+ (f) => !isFileExtensionAllowed(f.name, constraints.allowedExtensions)
3339
+ );
3340
+ const afterExt = allFiles.filter(
3341
+ (f) => isFileExtensionAllowed(f.name, constraints.allowedExtensions)
3342
+ );
3343
+ const rejectedBySize = afterExt.filter(
3344
+ (f) => !isFileSizeAllowed(f, constraints.maxSize)
3345
+ );
3346
+ const valid = afterExt.filter((f) => isFileSizeAllowed(f, constraints.maxSize));
3347
+ const remaining = constraints.maxCount === Infinity ? valid.length : Math.max(0, constraints.maxCount - currentCount);
3348
+ const accepted = valid.slice(0, remaining);
3349
+ const skippedByCount = valid.length - accepted.length;
3350
+ const errorParts = [];
3351
+ if (rejectedByExt.length > 0) {
3352
+ const formats = constraints.allowedExtensions.join(", ");
3353
+ const names = rejectedByExt.map((f) => f.name).join(", ");
3354
+ errorParts.push(t("invalidFileExtension", state, { name: names, formats }));
3355
+ }
3356
+ if (rejectedBySize.length > 0) {
3357
+ const names = rejectedBySize.map((f) => f.name).join(", ");
3358
+ errorParts.push(
3359
+ t("fileTooLarge", state, { name: names, maxSize: constraints.maxSize })
3360
+ );
3361
+ }
3362
+ if (skippedByCount > 0) {
3363
+ errorParts.push(
3364
+ t("filesLimitExceeded", state, {
3365
+ skipped: skippedByCount,
3366
+ max: constraints.maxCount
3367
+ })
3368
+ );
3369
+ }
3370
+ return { accepted, errorMessage: errorParts.join(" \u2022 ") };
3371
+ }
3372
+ async function uploadBatch(accepted, resourceIds, listEl, state) {
3373
+ await Promise.all(
3374
+ accepted.map(async (file) => {
3375
+ const placeholder = createUploadingTile(file.name, state);
3376
+ if (listEl) {
3377
+ const tilesWrap = ensureTilesWrap(listEl);
3378
+ const addTile = tilesWrap.querySelector(".fb-tile-add");
3379
+ if (addTile) {
3380
+ tilesWrap.insertBefore(placeholder, addTile);
3381
+ } else {
3382
+ tilesWrap.appendChild(placeholder);
3383
+ }
3384
+ }
3385
+ try {
3386
+ const rid = await uploadSingleFile(file, state);
3387
+ state.resourceIndex.set(rid, {
3388
+ name: file.name,
3389
+ type: file.type,
3390
+ size: file.size,
3391
+ uploadedAt: /* @__PURE__ */ new Date(),
3392
+ file: void 0
3393
+ });
3394
+ resourceIds.push(rid);
3395
+ } finally {
3396
+ placeholder.remove();
3397
+ }
3398
+ })
3399
+ );
3400
+ }
3401
+ function setupFilesDropHandler(filesContainer, resourceIds, state, updateCallback, constraints, pathKey, instance) {
3402
+ setupDragAndDrop(filesContainer, async (files) => {
2774
3403
  var _a;
2775
- e.preventDefault();
2776
- element.classList.remove("border-blue-500", "bg-blue-50");
2777
- if ((_a = e.dataTransfer) == null ? void 0 : _a.files) {
2778
- dropHandler(e.dataTransfer.files);
3404
+ const { accepted, errorMessage } = filterAndSlice(
3405
+ Array.from(files),
3406
+ resourceIds.length,
3407
+ constraints,
3408
+ state
3409
+ );
3410
+ if (errorMessage) {
3411
+ showFileError(filesContainer, errorMessage);
3412
+ } else {
3413
+ clearFileError(filesContainer);
3414
+ }
3415
+ const list = (_a = filesContainer.querySelector(".files-list")) != null ? _a : filesContainer;
3416
+ await uploadBatch(accepted, resourceIds, list, state);
3417
+ updateCallback();
3418
+ if (instance && pathKey && !state.config.readonly) {
3419
+ instance.triggerOnChange(pathKey, resourceIds);
2779
3420
  }
2780
3421
  });
2781
3422
  }
2782
- function addDeleteButton(container, state, onDelete) {
2783
- const existingOverlay = container.querySelector(".delete-overlay");
2784
- if (existingOverlay) {
2785
- existingOverlay.remove();
2786
- }
2787
- const overlay = document.createElement("div");
2788
- overlay.className = "delete-overlay absolute inset-0 bg-black bg-opacity-50 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center";
2789
- const deleteBtn = document.createElement("button");
2790
- deleteBtn.className = "bg-red-600 text-white px-3 py-1 rounded text-sm hover:bg-red-700 transition-colors";
2791
- deleteBtn.textContent = t("removeElement", state);
2792
- deleteBtn.onclick = (e) => {
2793
- e.stopPropagation();
2794
- onDelete();
3423
+ function setupFilesPickerHandler(filesPicker, resourceIds, state, updateCallback, constraints, pathKey, instance) {
3424
+ filesPicker.onchange = async () => {
3425
+ if (!filesPicker.files) return;
3426
+ const wrapperEl = filesPicker.closest(".space-y-2") || filesPicker.parentElement;
3427
+ const { accepted, errorMessage } = filterAndSlice(
3428
+ Array.from(filesPicker.files),
3429
+ resourceIds.length,
3430
+ constraints,
3431
+ state
3432
+ );
3433
+ if (errorMessage && wrapperEl) {
3434
+ showFileError(wrapperEl, errorMessage);
3435
+ } else if (wrapperEl) {
3436
+ clearFileError(wrapperEl);
3437
+ }
3438
+ const listEl = wrapperEl == null ? void 0 : wrapperEl.querySelector(".files-list");
3439
+ await uploadBatch(accepted, resourceIds, listEl != null ? listEl : null, state);
3440
+ updateCallback();
3441
+ filesPicker.value = "";
3442
+ if (instance && pathKey && !state.config.readonly) {
3443
+ instance.triggerOnChange(pathKey, resourceIds);
3444
+ }
2795
3445
  };
2796
- overlay.appendChild(deleteBtn);
2797
- container.appendChild(overlay);
2798
3446
  }
2799
- async function uploadSingleFile(file, state) {
2800
- if (state.config.uploadFile) {
2801
- try {
2802
- const rid = await state.config.uploadFile(file);
2803
- if (typeof rid !== "string") {
2804
- throw new Error("Upload handler must return a string resource ID");
2805
- }
2806
- return rid;
2807
- } catch (error) {
2808
- const err = error instanceof Error ? error : new Error(String(error));
2809
- if (state.config.onUploadError) {
2810
- state.config.onUploadError(err, file);
3447
+
3448
+ // src/components/file/render-edit.ts
3449
+ function handleInitialFileData(initial, fileContainer, pathKey, fileWrapper, state, deps) {
3450
+ var _a, _b;
3451
+ if (!state.resourceIndex.has(initial)) {
3452
+ const filename = initial.split("/").pop() || "file";
3453
+ const extension = (_a = filename.split(".").pop()) == null ? void 0 : _a.toLowerCase();
3454
+ let fileType = "application/octet-stream";
3455
+ if (extension) {
3456
+ if (["jpg", "jpeg", "png", "gif", "webp"].includes(extension)) {
3457
+ fileType = `image/${extension === "jpg" ? "jpeg" : extension}`;
3458
+ } else if (["mp4", "webm", "mov", "avi"].includes(extension)) {
3459
+ fileType = `video/${extension === "mov" ? "quicktime" : extension}`;
2811
3460
  }
2812
- throw new Error(`File upload failed: ${err.message}`);
2813
3461
  }
3462
+ state.resourceIndex.set(initial, {
3463
+ name: filename,
3464
+ type: fileType,
3465
+ size: 0,
3466
+ uploadedAt: /* @__PURE__ */ new Date(),
3467
+ file: void 0
3468
+ });
3469
+ }
3470
+ const meta = state.resourceIndex.get(initial);
3471
+ const isVideo = (_b = meta == null ? void 0 : meta.type) == null ? void 0 : _b.startsWith("video/");
3472
+ if (isVideo) {
3473
+ renderFilePreview(fileContainer, initial, state, {
3474
+ fileName: initial,
3475
+ isReadonly: false,
3476
+ deps
3477
+ }).catch(console.error);
2814
3478
  } else {
2815
- throw new Error(
2816
- "No upload handler configured. Set uploadHandler via FormBuilder.setUploadHandler()"
2817
- );
3479
+ renderSingleFileEditTile(fileContainer, initial, state, deps).catch(console.error);
2818
3480
  }
3481
+ const hiddenInput = document.createElement("input");
3482
+ hiddenInput.type = "hidden";
3483
+ hiddenInput.name = pathKey;
3484
+ hiddenInput.value = initial;
3485
+ fileWrapper.appendChild(hiddenInput);
2819
3486
  }
2820
- async function forceDownload(resourceId, fileName, state) {
2821
- try {
2822
- let fileUrl = null;
2823
- if (state.config.getDownloadUrl) {
2824
- fileUrl = state.config.getDownloadUrl(resourceId);
2825
- } else if (state.config.getThumbnail) {
2826
- fileUrl = await state.config.getThumbnail(resourceId);
2827
- }
2828
- if (fileUrl) {
2829
- const finalUrl = fileUrl.startsWith("http") ? fileUrl : new URL(fileUrl, window.location.href).href;
2830
- const response = await fetch(finalUrl);
2831
- if (!response.ok) {
2832
- throw new Error(`HTTP error! status: ${response.status}`);
2833
- }
2834
- const blob = await response.blob();
2835
- downloadBlob(blob, fileName);
3487
+ function renderResourcePills(container, rids, state, onRemove, hint, countInfo, maxCount, isReadonly = false) {
3488
+ var _a;
3489
+ ensureFileStyles();
3490
+ const wrapper = container.closest("[data-files-wrapper]");
3491
+ if (wrapper) {
3492
+ wrapper.dataset.resourceIds = JSON.stringify(rids != null ? rids : []);
3493
+ }
3494
+ while (container.firstChild) container.removeChild(container.firstChild);
3495
+ const ridList = rids != null ? rids : [];
3496
+ const atMax = maxCount !== void 0 && ridList.length >= maxCount;
3497
+ const buildSubHint = () => {
3498
+ const parts = [];
3499
+ if (hint) parts.push(hint);
3500
+ if (countInfo) parts.push(countInfo);
3501
+ return parts.join(" \u2022 ");
3502
+ };
3503
+ const openPicker = () => {
3504
+ const picker = findFilePicker(container);
3505
+ if (picker) picker.click();
3506
+ };
3507
+ if (ridList.length === 0) {
3508
+ if (isReadonly) {
3509
+ const emptyEl = document.createElement("div");
3510
+ emptyEl.className = "fb-tile-empty-text";
3511
+ emptyEl.textContent = t("noFilesSelected", state);
3512
+ container.appendChild(emptyEl);
2836
3513
  } else {
2837
- throw new Error("No download URL available for resource");
3514
+ const dropzone = document.createElement("div");
3515
+ dropzone.className = "fb-file-dropzone";
3516
+ const subHint2 = buildSubHint();
3517
+ dropzone.innerHTML = `
3518
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor" style="flex-shrink:0;color:var(--fb-file-upload-text-color,#9ca3af);">
3519
+ <path d="M19.35 10.04A7.49 7.49 0 0012 4C9.11 4 6.6 5.64 5.35 8.04A5.994 5.994 0 000 14c0 3.31 2.69 6 6 6h13c2.76 0 5-2.24 5-5 0-2.64-2.05-4.78-4.65-4.96zM14 13v4h-4v-4H7l5-5 5 5h-3z"/>
3520
+ </svg>
3521
+ <div class="fb-dropzone-primary-text">${escapeHtml(t("clickDragTextMultiple", state))}</div>
3522
+ ${subHint2 ? `<div class="fb-dropzone-hint-text">${escapeHtml(subHint2)}</div>` : ""}
3523
+ `;
3524
+ dropzone.onclick = openPicker;
3525
+ container.appendChild(dropzone);
2838
3526
  }
2839
- } catch (error) {
2840
- const err = error instanceof Error ? error : new Error(String(error));
2841
- if (state.config.onDownloadError) {
2842
- state.config.onDownloadError(err, resourceId, fileName);
2843
- }
2844
- console.error(`File download failed for ${fileName}:`, err);
2845
- throw err;
2846
- }
2847
- }
2848
- function downloadBlob(blob, fileName) {
2849
- try {
2850
- const blobUrl = URL.createObjectURL(blob);
2851
- const link = document.createElement("a");
2852
- link.href = blobUrl;
2853
- link.download = fileName;
2854
- link.style.display = "none";
2855
- document.body.appendChild(link);
2856
- link.click();
2857
- document.body.removeChild(link);
2858
- setTimeout(() => {
2859
- URL.revokeObjectURL(blobUrl);
2860
- }, 100);
2861
- } catch (error) {
2862
- throw new Error(`Blob download failed: ${error.message}`);
3527
+ return;
2863
3528
  }
2864
- }
2865
- function addPrefillFilesToIndex(initialFiles, state) {
2866
- if (initialFiles.length > 0) {
2867
- initialFiles.forEach((resourceId) => {
2868
- var _a;
2869
- if (!state.resourceIndex.has(resourceId)) {
2870
- const filename = resourceId.split("/").pop() || "file";
2871
- const extension = (_a = filename.split(".").pop()) == null ? void 0 : _a.toLowerCase();
2872
- let fileType = "application/octet-stream";
2873
- if (extension) {
2874
- if (["jpg", "jpeg", "png", "gif", "webp"].includes(extension)) {
2875
- fileType = `image/${extension === "jpg" ? "jpeg" : extension}`;
2876
- } else if (["mp4", "webm", "mov", "avi"].includes(extension)) {
2877
- fileType = `video/${extension === "mov" ? "quicktime" : extension}`;
2878
- }
2879
- }
2880
- state.resourceIndex.set(resourceId, {
2881
- name: filename,
2882
- type: fileType,
2883
- size: 0,
2884
- uploadedAt: /* @__PURE__ */ new Date(),
2885
- file: void 0
2886
- });
2887
- }
3529
+ const tilesWrap = document.createElement("div");
3530
+ tilesWrap.className = "fb-tiles-wrap";
3531
+ tilesWrap.style.cssText = "display:flex;flex-wrap:wrap;gap:6px;align-items:flex-start;";
3532
+ for (const rid of ridList) {
3533
+ const meta = state.resourceIndex.get(rid);
3534
+ const tile = createFileTile();
3535
+ tile.classList.add("fb-tile-resource", "resource-pill");
3536
+ tile.dataset.resourceId = rid;
3537
+ const actionsEl = createTileActions({
3538
+ canRemove: !isReadonly && onRemove !== null,
3539
+ removeHandler: onRemove ? () => onRemove(rid) : null,
3540
+ state,
3541
+ resourceId: rid,
3542
+ fileName: (_a = meta == null ? void 0 : meta.name) != null ? _a : ""
2888
3543
  });
2889
- }
2890
- }
2891
- function handleInitialFileData(initial, fileContainer, pathKey, fileWrapper, state, deps) {
2892
- var _a;
2893
- if (!state.resourceIndex.has(initial)) {
2894
- const filename = initial.split("/").pop() || "file";
2895
- const extension = (_a = filename.split(".").pop()) == null ? void 0 : _a.toLowerCase();
2896
- let fileType = "application/octet-stream";
2897
- if (extension) {
2898
- if (["jpg", "jpeg", "png", "gif", "webp"].includes(extension)) {
2899
- fileType = `image/${extension === "jpg" ? "jpeg" : extension}`;
2900
- } else if (["mp4", "webm", "mov", "avi"].includes(extension)) {
2901
- fileType = `video/${extension === "mov" ? "quicktime" : extension}`;
2902
- }
2903
- }
2904
- state.resourceIndex.set(initial, {
2905
- name: filename,
2906
- type: fileType,
2907
- size: 0,
2908
- uploadedAt: /* @__PURE__ */ new Date(),
2909
- file: void 0
3544
+ fillTileContent(tile, rid, meta, state, actionsEl).catch((err) => {
3545
+ console.error("Failed to render tile:", err);
2910
3546
  });
3547
+ tilesWrap.appendChild(tile);
3548
+ }
3549
+ if (!isReadonly && !atMax) {
3550
+ const addTile = document.createElement("div");
3551
+ addTile.className = "fb-tile fb-tile-add";
3552
+ addTile.innerHTML = "+";
3553
+ addTile.onclick = openPicker;
3554
+ tilesWrap.appendChild(addTile);
3555
+ } else if (!isReadonly && atMax) {
3556
+ const chip = document.createElement("div");
3557
+ chip.className = "fb-tile-counter";
3558
+ chip.textContent = t("filesCounter", state, {
3559
+ count: ridList.length,
3560
+ max: maxCount
3561
+ });
3562
+ tilesWrap.appendChild(chip);
3563
+ }
3564
+ container.appendChild(tilesWrap);
3565
+ const subHint = buildSubHint();
3566
+ if (subHint) {
3567
+ const hintEl = document.createElement("div");
3568
+ hintEl.className = "fb-tile-hint";
3569
+ hintEl.textContent = subHint;
3570
+ container.appendChild(hintEl);
2911
3571
  }
2912
- renderFilePreview(fileContainer, initial, state, {
2913
- fileName: initial,
2914
- isReadonly: false,
2915
- deps
2916
- }).catch(console.error);
2917
- const hiddenInput = document.createElement("input");
2918
- hiddenInput.type = "hidden";
2919
- hiddenInput.name = pathKey;
2920
- hiddenInput.value = initial;
2921
- fileWrapper.appendChild(hiddenInput);
2922
- }
2923
- function setupFilesDropHandler(filesContainer, initialFiles, state, updateCallback, constraints, pathKey, instance) {
2924
- setupDragAndDrop(filesContainer, async (files) => {
2925
- const allFiles = Array.from(files);
2926
- const rejectedByExtension = allFiles.filter(
2927
- (f) => !isFileExtensionAllowed(f.name, constraints.allowedExtensions)
2928
- );
2929
- const afterExtension = allFiles.filter(
2930
- (f) => isFileExtensionAllowed(f.name, constraints.allowedExtensions)
2931
- );
2932
- const rejectedBySize = afterExtension.filter(
2933
- (f) => !isFileSizeAllowed(f, constraints.maxSize)
2934
- );
2935
- const validFiles = afterExtension.filter(
2936
- (f) => isFileSizeAllowed(f, constraints.maxSize)
2937
- );
2938
- const remaining = constraints.maxCount === Infinity ? validFiles.length : Math.max(0, constraints.maxCount - initialFiles.length);
2939
- const arr = validFiles.slice(0, remaining);
2940
- const skippedByCount = validFiles.length - arr.length;
2941
- const errorParts = [];
2942
- if (rejectedByExtension.length > 0) {
2943
- const formats = constraints.allowedExtensions.join(", ");
2944
- const names = rejectedByExtension.map((f) => f.name).join(", ");
2945
- errorParts.push(
2946
- t("invalidFileExtension", state, { name: names, formats })
2947
- );
2948
- }
2949
- if (rejectedBySize.length > 0) {
2950
- const names = rejectedBySize.map((f) => f.name).join(", ");
2951
- errorParts.push(
2952
- t("fileTooLarge", state, { name: names, maxSize: constraints.maxSize })
2953
- );
2954
- }
2955
- if (skippedByCount > 0) {
2956
- errorParts.push(
2957
- t("filesLimitExceeded", state, {
2958
- skipped: skippedByCount,
2959
- max: constraints.maxCount
2960
- })
2961
- );
2962
- }
2963
- if (errorParts.length > 0) {
2964
- showFileError(filesContainer, errorParts.join(" \u2022 "));
2965
- } else {
2966
- clearFileError(filesContainer);
2967
- }
2968
- for (const file of arr) {
2969
- const rid = await uploadSingleFile(file, state);
2970
- state.resourceIndex.set(rid, {
2971
- name: file.name,
2972
- type: file.type,
2973
- size: file.size,
2974
- uploadedAt: /* @__PURE__ */ new Date(),
2975
- file: void 0
2976
- });
2977
- initialFiles.push(rid);
2978
- }
2979
- updateCallback();
2980
- if (instance && pathKey && !state.config.readonly) {
2981
- instance.triggerOnChange(pathKey, initialFiles);
2982
- }
2983
- });
2984
- }
2985
- function setupFilesPickerHandler(filesPicker, initialFiles, state, updateCallback, constraints, pathKey, instance) {
2986
- filesPicker.onchange = async () => {
2987
- if (filesPicker.files) {
2988
- const allFiles = Array.from(filesPicker.files);
2989
- const rejectedByExtension = allFiles.filter(
2990
- (f) => !isFileExtensionAllowed(f.name, constraints.allowedExtensions)
2991
- );
2992
- const afterExtension = allFiles.filter(
2993
- (f) => isFileExtensionAllowed(f.name, constraints.allowedExtensions)
2994
- );
2995
- const rejectedBySize = afterExtension.filter(
2996
- (f) => !isFileSizeAllowed(f, constraints.maxSize)
2997
- );
2998
- const validFiles = afterExtension.filter(
2999
- (f) => isFileSizeAllowed(f, constraints.maxSize)
3000
- );
3001
- const remaining = constraints.maxCount === Infinity ? validFiles.length : Math.max(0, constraints.maxCount - initialFiles.length);
3002
- const arr = validFiles.slice(0, remaining);
3003
- const skippedByCount = validFiles.length - arr.length;
3004
- const errorParts = [];
3005
- if (rejectedByExtension.length > 0) {
3006
- const formats = constraints.allowedExtensions.join(", ");
3007
- const names = rejectedByExtension.map((f) => f.name).join(", ");
3008
- errorParts.push(
3009
- t("invalidFileExtension", state, { name: names, formats })
3010
- );
3011
- }
3012
- if (rejectedBySize.length > 0) {
3013
- const names = rejectedBySize.map((f) => f.name).join(", ");
3014
- errorParts.push(
3015
- t("fileTooLarge", state, {
3016
- name: names,
3017
- maxSize: constraints.maxSize
3018
- })
3019
- );
3020
- }
3021
- if (skippedByCount > 0) {
3022
- errorParts.push(
3023
- t("filesLimitExceeded", state, {
3024
- skipped: skippedByCount,
3025
- max: constraints.maxCount
3026
- })
3027
- );
3028
- }
3029
- const wrapper = filesPicker.closest(".space-y-2") || filesPicker.parentElement;
3030
- if (errorParts.length > 0 && wrapper) {
3031
- showFileError(wrapper, errorParts.join(" \u2022 "));
3032
- } else if (wrapper) {
3033
- clearFileError(wrapper);
3034
- }
3035
- for (const file of arr) {
3036
- const rid = await uploadSingleFile(file, state);
3037
- state.resourceIndex.set(rid, {
3038
- name: file.name,
3039
- type: file.type,
3040
- size: file.size,
3041
- uploadedAt: /* @__PURE__ */ new Date(),
3042
- file: void 0
3043
- });
3044
- initialFiles.push(rid);
3045
- }
3046
- }
3047
- updateCallback();
3048
- filesPicker.value = "";
3049
- if (instance && pathKey && !state.config.readonly) {
3050
- instance.triggerOnChange(pathKey, initialFiles);
3051
- }
3052
- };
3053
3572
  }
3054
- function renderFileElement(element, ctx, wrapper, pathKey) {
3055
- var _a, _b;
3573
+ function renderFileElementEdit(element, ctx, wrapper, pathKey) {
3574
+ var _a, _b, _c;
3056
3575
  const state = ctx.state;
3057
- if (isElementReadonly(element, state, ctx)) {
3058
- const rawInitial = ctx.prefill[element.key];
3059
- const initial = typeof rawInitial === "string" ? rawInitial : "";
3060
- if (initial) {
3061
- const hiddenInput = document.createElement("input");
3062
- hiddenInput.type = "hidden";
3063
- hiddenInput.name = pathKey;
3064
- hiddenInput.value = initial;
3065
- wrapper.appendChild(hiddenInput);
3066
- renderFilePreviewReadonly(initial, state).then((filePreview) => {
3067
- wrapper.appendChild(filePreview);
3068
- }).catch((err) => {
3069
- console.error("Failed to render file preview:", err);
3070
- const emptyState = document.createElement("div");
3071
- emptyState.className = "aspect-video bg-gray-100 rounded-lg flex items-center justify-center text-gray-500";
3072
- emptyState.innerHTML = `<div class="text-center">${escapeHtml(t("previewUnavailable", state))}</div>`;
3073
- wrapper.appendChild(emptyState);
3074
- });
3075
- } else {
3076
- const emptyState = document.createElement("div");
3077
- emptyState.className = "aspect-video bg-gray-100 rounded-lg flex items-center justify-center text-gray-500";
3078
- emptyState.innerHTML = `<div class="text-center">${escapeHtml(t("noFileSelected", state))}</div>`;
3079
- wrapper.appendChild(emptyState);
3080
- }
3081
- } else {
3082
- const fileWrapper = document.createElement("div");
3083
- fileWrapper.className = "space-y-2";
3084
- const picker = document.createElement("input");
3085
- picker.type = "file";
3086
- picker.name = pathKey;
3087
- picker.style.display = "none";
3088
- if (element.accept) {
3089
- picker.accept = typeof element.accept === "string" ? element.accept : ((_a = element.accept.extensions) == null ? void 0 : _a.map((ext) => `.${ext}`).join(",")) || "";
3090
- }
3091
- const fileContainer = document.createElement("div");
3092
- fileContainer.className = "file-preview-container w-full aspect-square max-w-xs bg-gray-100 rounded-lg overflow-hidden relative group cursor-pointer";
3093
- const initial = ctx.prefill[element.key];
3094
- const allowedExts = getAllowedExtensions(element.accept);
3095
- const maxSizeMB = (_b = element.maxSize) != null ? _b : Infinity;
3096
- const fileUploadHandler = () => picker.click();
3097
- const dragHandler = (files) => {
3576
+ const fileWrapper = document.createElement("div");
3577
+ fileWrapper.className = "space-y-2";
3578
+ const picker = document.createElement("input");
3579
+ picker.type = "file";
3580
+ picker.name = pathKey;
3581
+ picker.style.display = "none";
3582
+ if (element.accept) {
3583
+ picker.accept = typeof element.accept === "string" ? element.accept : ((_a = element.accept.extensions) == null ? void 0 : _a.map((ext) => `.${ext}`).join(",")) || "";
3584
+ }
3585
+ const fileContainer = document.createElement("div");
3586
+ fileContainer.className = "file-preview-container";
3587
+ const initial = ctx.prefill[element.key];
3588
+ const allowedExts = getAllowedExtensions(element.accept);
3589
+ const maxSizeMB = (_b = element.maxSize) != null ? _b : Infinity;
3590
+ const handlers = {
3591
+ fileUploadHandler() {
3592
+ picker.click();
3593
+ },
3594
+ dragHandler(files) {
3098
3595
  if (files.length > 0) {
3099
- const deps = { picker, fileUploadHandler, dragHandler };
3100
3596
  handleFileSelect(
3101
3597
  files[0],
3102
3598
  fileContainer,
3103
3599
  pathKey,
3104
3600
  state,
3105
- deps,
3601
+ buildDeps(),
3106
3602
  ctx.instance,
3107
3603
  allowedExts,
3108
3604
  maxSizeMB
3109
3605
  );
3110
3606
  }
3111
- };
3112
- if (initial) {
3113
- handleInitialFileData(
3114
- initial,
3607
+ },
3608
+ setupDrop(container) {
3609
+ setupDragAndDrop(container, handlers.dragHandler);
3610
+ },
3611
+ restoreDropzone() {
3612
+ const hint = makeFieldHint(element, state);
3613
+ fileContainer.className = "file-preview-container w-full max-w-md bg-gray-100 rounded-lg overflow-hidden relative group cursor-pointer";
3614
+ fileContainer.style.height = "128px";
3615
+ setEmptyFileContainer(fileContainer, state, hint);
3616
+ fileContainer.onclick = handlers.fileUploadHandler;
3617
+ setupDragAndDrop(fileContainer, handlers.dragHandler);
3618
+ },
3619
+ onRemove() {
3620
+ var _a2;
3621
+ const hiddenInput = fileWrapper.querySelector('input[type="hidden"]');
3622
+ const currentRid = hiddenInput == null ? void 0 : hiddenInput.value;
3623
+ if (currentRid) {
3624
+ releaseLocalFileUrl((_a2 = state.resourceIndex.get(currentRid)) == null ? void 0 : _a2.file);
3625
+ }
3626
+ if (hiddenInput) hiddenInput.value = "";
3627
+ handlers.restoreDropzone();
3628
+ }
3629
+ };
3630
+ const buildDeps = () => ({
3631
+ picker,
3632
+ fileUploadHandler: handlers.fileUploadHandler,
3633
+ dragHandler: handlers.dragHandler,
3634
+ setupDrop: handlers.setupDrop,
3635
+ onRemove: handlers.onRemove
3636
+ });
3637
+ if (initial) {
3638
+ handleInitialFileData(
3639
+ initial,
3640
+ fileContainer,
3641
+ pathKey,
3642
+ fileWrapper,
3643
+ state,
3644
+ buildDeps()
3645
+ );
3646
+ const prefillMeta = state.resourceIndex.get(initial);
3647
+ if ((_c = prefillMeta == null ? void 0 : prefillMeta.type) == null ? void 0 : _c.startsWith("video/")) {
3648
+ fileContainer.onclick = handlers.fileUploadHandler;
3649
+ setupDragAndDrop(fileContainer, handlers.dragHandler);
3650
+ }
3651
+ } else {
3652
+ handlers.restoreDropzone();
3653
+ }
3654
+ picker.onchange = () => {
3655
+ if (picker.files && picker.files.length > 0) {
3656
+ handleFileSelect(
3657
+ picker.files[0],
3115
3658
  fileContainer,
3116
3659
  pathKey,
3117
- fileWrapper,
3118
3660
  state,
3119
- {
3120
- picker,
3121
- fileUploadHandler,
3122
- dragHandler
3123
- }
3661
+ buildDeps(),
3662
+ ctx.instance,
3663
+ allowedExts,
3664
+ maxSizeMB
3124
3665
  );
3125
- } else {
3126
- const hint = makeFieldHint(element, state);
3127
- setEmptyFileContainer(fileContainer, state, hint);
3128
3666
  }
3129
- fileContainer.onclick = fileUploadHandler;
3130
- setupDragAndDrop(fileContainer, dragHandler);
3131
- picker.onchange = () => {
3132
- if (picker.files && picker.files.length > 0) {
3133
- const deps = { picker, fileUploadHandler, dragHandler };
3134
- handleFileSelect(
3135
- picker.files[0],
3136
- fileContainer,
3137
- pathKey,
3138
- state,
3139
- deps,
3140
- ctx.instance,
3141
- allowedExts,
3142
- maxSizeMB
3143
- );
3144
- }
3145
- };
3146
- fileWrapper.appendChild(fileContainer);
3147
- fileWrapper.appendChild(picker);
3148
- wrapper.appendChild(fileWrapper);
3149
- }
3667
+ };
3668
+ fileWrapper.appendChild(fileContainer);
3669
+ fileWrapper.appendChild(picker);
3670
+ wrapper.appendChild(fileWrapper);
3150
3671
  }
3151
- function renderFilesElement(element, ctx, wrapper, pathKey) {
3672
+ function renderFilesElementEdit(element, ctx, wrapper, pathKey) {
3152
3673
  var _a, _b;
3153
3674
  const state = ctx.state;
3154
- if (isElementReadonly(element, state, ctx)) {
3155
- const rawPrefill = ctx.prefill[element.key];
3156
- const initialFiles = Array.isArray(rawPrefill) ? rawPrefill : [];
3157
- const filesWrapper = document.createElement("div");
3158
- filesWrapper.className = "space-y-2";
3159
- filesWrapper.dataset.filesWrapper = pathKey;
3160
- const filesList = document.createElement("div");
3161
- filesList.className = "files-list";
3162
- initialFiles.forEach((resourceId) => {
3163
- const pill = document.createElement("div");
3164
- pill.className = "resource-pill";
3165
- pill.dataset.resourceId = resourceId;
3166
- filesList.appendChild(pill);
3167
- });
3168
- filesWrapper.appendChild(filesList);
3169
- wrapper.appendChild(filesWrapper);
3170
- const resultsWrapper = document.createElement("div");
3171
- resultsWrapper.className = "space-y-4";
3172
- if (initialFiles.length > 0) {
3173
- initialFiles.forEach((resourceId) => {
3174
- renderFilePreviewReadonly(resourceId, state).then((filePreview) => {
3175
- resultsWrapper.appendChild(filePreview);
3176
- }).catch((err) => {
3177
- console.error("Failed to render file preview:", err);
3178
- });
3179
- });
3180
- } else {
3181
- resultsWrapper.innerHTML = `<div class="aspect-video bg-gray-100 rounded-lg flex items-center justify-center text-gray-500"><div class="text-center">${escapeHtml(t("noFilesSelected", state))}</div></div>`;
3182
- }
3183
- wrapper.appendChild(resultsWrapper);
3184
- } else {
3185
- let updateFilesList2 = function() {
3186
- renderResourcePills(
3187
- list,
3188
- initialFiles,
3189
- state,
3190
- (ridToRemove) => {
3191
- const index = initialFiles.indexOf(ridToRemove);
3192
- if (index > -1) {
3193
- initialFiles.splice(index, 1);
3194
- }
3195
- updateFilesList2();
3196
- },
3197
- filesFieldHint
3198
- );
3199
- };
3200
- const filesWrapper = document.createElement("div");
3201
- filesWrapper.className = "space-y-2";
3202
- filesWrapper.dataset.filesWrapper = pathKey;
3203
- const filesPicker = document.createElement("input");
3204
- filesPicker.type = "file";
3205
- filesPicker.name = pathKey;
3206
- filesPicker.multiple = true;
3207
- filesPicker.style.display = "none";
3208
- if (element.accept) {
3209
- filesPicker.accept = typeof element.accept === "string" ? element.accept : ((_a = element.accept.extensions) == null ? void 0 : _a.map((ext) => `.${ext}`).join(",")) || "";
3210
- }
3211
- const filesContainer = document.createElement("div");
3212
- filesContainer.className = "border-2 border-dashed border-gray-300 rounded-lg p-3 hover:border-gray-400 transition-colors";
3213
- const list = document.createElement("div");
3214
- list.className = "files-list";
3215
- const initialFiles = ctx.prefill[element.key] || [];
3216
- addPrefillFilesToIndex(initialFiles, state);
3217
- const filesFieldHint = makeFieldHint(element, state);
3218
- const filesConstraints = {
3219
- maxCount: Infinity,
3220
- allowedExtensions: getAllowedExtensions(element.accept),
3221
- maxSize: (_b = element.maxSize) != null ? _b : Infinity
3222
- };
3223
- updateFilesList2();
3224
- setupFilesDropHandler(
3225
- filesContainer,
3226
- initialFiles,
3227
- state,
3228
- updateFilesList2,
3229
- filesConstraints,
3230
- pathKey,
3231
- ctx.instance
3232
- );
3233
- setupFilesPickerHandler(
3234
- filesPicker,
3675
+ const filesWrapper = document.createElement("div");
3676
+ filesWrapper.className = "space-y-2";
3677
+ filesWrapper.dataset.filesWrapper = pathKey;
3678
+ const filesPicker = document.createElement("input");
3679
+ filesPicker.type = "file";
3680
+ filesPicker.name = pathKey;
3681
+ filesPicker.multiple = true;
3682
+ filesPicker.style.display = "none";
3683
+ if (element.accept) {
3684
+ filesPicker.accept = typeof element.accept === "string" ? element.accept : ((_a = element.accept.extensions) == null ? void 0 : _a.map((ext) => `.${ext}`).join(",")) || "";
3685
+ }
3686
+ const filesContainer = document.createElement("div");
3687
+ filesContainer.className = "files-list-wrapper";
3688
+ filesContainer.style.cssText = "border:2px dashed var(--fb-file-upload-border-color,#d1d5db);border-radius:var(--fb-border-radius,0.5rem);padding:8px;transition:border-color var(--fb-transition-duration,200ms),background var(--fb-transition-duration,200ms);";
3689
+ const list = document.createElement("div");
3690
+ list.className = "files-list";
3691
+ const initialFiles = ctx.prefill[element.key] || [];
3692
+ addPrefillFilesToIndex(initialFiles, state.resourceIndex);
3693
+ filesWrapper.dataset.resourceIds = JSON.stringify(initialFiles);
3694
+ const filesFieldHint = makeFieldHint(element, state);
3695
+ const filesConstraints = {
3696
+ maxCount: Infinity,
3697
+ allowedExtensions: getAllowedExtensions(element.accept),
3698
+ maxSize: (_b = element.maxSize) != null ? _b : Infinity
3699
+ };
3700
+ filesContainer.appendChild(list);
3701
+ filesWrapper.appendChild(filesPicker);
3702
+ filesWrapper.appendChild(filesContainer);
3703
+ wrapper.appendChild(filesWrapper);
3704
+ function updateFilesList() {
3705
+ const currentlyReadonly = isElementReadonly(element, state);
3706
+ renderResourcePills(
3707
+ list,
3235
3708
  initialFiles,
3236
3709
  state,
3237
- updateFilesList2,
3238
- filesConstraints,
3239
- pathKey,
3240
- ctx.instance
3710
+ currentlyReadonly ? null : (ridToRemove) => {
3711
+ var _a2;
3712
+ releaseLocalFileUrl((_a2 = state.resourceIndex.get(ridToRemove)) == null ? void 0 : _a2.file);
3713
+ const index = initialFiles.indexOf(ridToRemove);
3714
+ if (index > -1) initialFiles.splice(index, 1);
3715
+ updateFilesList();
3716
+ },
3717
+ filesFieldHint,
3718
+ void 0,
3719
+ void 0,
3720
+ currentlyReadonly
3241
3721
  );
3242
- filesContainer.appendChild(list);
3243
- filesWrapper.appendChild(filesContainer);
3244
- filesWrapper.appendChild(filesPicker);
3245
- wrapper.appendChild(filesWrapper);
3246
3722
  }
3723
+ updateFilesList();
3724
+ setupFilesDropHandler(
3725
+ filesContainer,
3726
+ initialFiles,
3727
+ state,
3728
+ updateFilesList,
3729
+ filesConstraints,
3730
+ pathKey,
3731
+ ctx.instance
3732
+ );
3733
+ setupFilesPickerHandler(
3734
+ filesPicker,
3735
+ initialFiles,
3736
+ state,
3737
+ updateFilesList,
3738
+ filesConstraints,
3739
+ pathKey,
3740
+ ctx.instance
3741
+ );
3247
3742
  }
3248
- function renderMultipleFileElement(element, ctx, wrapper, pathKey) {
3743
+ function renderMultipleFileElementEdit(element, ctx, wrapper, pathKey) {
3249
3744
  var _a, _b, _c, _d;
3250
3745
  const state = ctx.state;
3251
3746
  const minFiles = (_a = element.minCount) != null ? _a : 0;
3252
3747
  const maxFiles = (_b = element.maxCount) != null ? _b : Infinity;
3253
- if (isElementReadonly(element, state, ctx)) {
3254
- const rawPrefill = ctx.prefill[element.key];
3255
- const initialFiles = Array.isArray(rawPrefill) ? rawPrefill : [];
3256
- const filesWrapper = document.createElement("div");
3257
- filesWrapper.className = "space-y-2";
3258
- filesWrapper.dataset.filesWrapper = pathKey;
3259
- const filesList = document.createElement("div");
3260
- filesList.className = "files-list";
3261
- initialFiles.forEach((resourceId) => {
3262
- const pill = document.createElement("div");
3263
- pill.className = "resource-pill";
3264
- pill.dataset.resourceId = resourceId;
3265
- filesList.appendChild(pill);
3266
- });
3267
- filesWrapper.appendChild(filesList);
3268
- wrapper.appendChild(filesWrapper);
3269
- const resultsWrapper = document.createElement("div");
3270
- resultsWrapper.className = "space-y-4";
3271
- if (initialFiles.length > 0) {
3272
- initialFiles.forEach((resourceId) => {
3273
- renderFilePreviewReadonly(resourceId, state).then((filePreview) => {
3274
- resultsWrapper.appendChild(filePreview);
3275
- }).catch((err) => {
3276
- console.error("Failed to render file preview:", err);
3277
- });
3278
- });
3279
- } else {
3280
- resultsWrapper.innerHTML = `<div class="aspect-video bg-gray-100 rounded-lg flex items-center justify-center text-gray-500"><div class="text-center">${escapeHtml(t("noFilesSelected", state))}</div></div>`;
3281
- }
3282
- wrapper.appendChild(resultsWrapper);
3283
- } else {
3284
- const filesWrapper = document.createElement("div");
3285
- filesWrapper.className = "space-y-2";
3286
- filesWrapper.dataset.filesWrapper = pathKey;
3287
- const filesPicker = document.createElement("input");
3288
- filesPicker.type = "file";
3289
- filesPicker.name = pathKey;
3290
- filesPicker.multiple = true;
3291
- filesPicker.style.display = "none";
3292
- if (element.accept) {
3293
- filesPicker.accept = typeof element.accept === "string" ? element.accept : ((_c = element.accept.extensions) == null ? void 0 : _c.map((ext) => `.${ext}`).join(",")) || "";
3294
- }
3295
- const filesContainer = document.createElement("div");
3296
- filesContainer.className = "files-list space-y-2";
3297
- filesWrapper.appendChild(filesPicker);
3298
- filesWrapper.appendChild(filesContainer);
3299
- const initialFiles = Array.isArray(ctx.prefill[element.key]) ? [...ctx.prefill[element.key]] : [];
3300
- addPrefillFilesToIndex(initialFiles, state);
3301
- const multipleFilesHint = makeFieldHint(element, state);
3302
- const multipleConstraints = {
3303
- maxCount: maxFiles,
3304
- allowedExtensions: getAllowedExtensions(element.accept),
3305
- maxSize: (_d = element.maxSize) != null ? _d : Infinity
3306
- };
3307
- const buildCountInfo = () => {
3308
- const countText = initialFiles.length === 1 ? t("fileCountSingle", state, { count: initialFiles.length }) : t("fileCountPlural", state, { count: initialFiles.length });
3309
- const minMaxText = minFiles > 0 || maxFiles < Infinity ? ` ${t("fileCountRange", state, { min: minFiles, max: maxFiles })}` : "";
3310
- return countText + minMaxText;
3311
- };
3312
- const updateFilesDisplay = () => {
3313
- renderResourcePills(
3314
- filesContainer,
3315
- initialFiles,
3316
- state,
3317
- (index) => {
3318
- initialFiles.splice(initialFiles.indexOf(index), 1);
3319
- updateFilesDisplay();
3320
- },
3321
- multipleFilesHint,
3322
- buildCountInfo()
3323
- );
3324
- };
3325
- setupFilesDropHandler(
3326
- filesContainer,
3748
+ const filesWrapper = document.createElement("div");
3749
+ filesWrapper.className = "space-y-2";
3750
+ filesWrapper.dataset.filesWrapper = pathKey;
3751
+ const filesPicker = document.createElement("input");
3752
+ filesPicker.type = "file";
3753
+ filesPicker.name = pathKey;
3754
+ filesPicker.multiple = true;
3755
+ filesPicker.style.display = "none";
3756
+ if (element.accept) {
3757
+ filesPicker.accept = typeof element.accept === "string" ? element.accept : ((_c = element.accept.extensions) == null ? void 0 : _c.map((ext) => `.${ext}`).join(",")) || "";
3758
+ }
3759
+ const filesContainer = document.createElement("div");
3760
+ filesContainer.className = "files-list-wrapper";
3761
+ filesContainer.style.cssText = "border:2px dashed var(--fb-file-upload-border-color,#d1d5db);border-radius:var(--fb-border-radius,0.5rem);padding:8px;transition:border-color var(--fb-transition-duration,200ms),background var(--fb-transition-duration,200ms);";
3762
+ const list = document.createElement("div");
3763
+ list.className = "files-list";
3764
+ filesWrapper.appendChild(filesPicker);
3765
+ filesWrapper.appendChild(filesContainer);
3766
+ filesContainer.appendChild(list);
3767
+ const initialFiles = Array.isArray(ctx.prefill[element.key]) ? [...ctx.prefill[element.key]] : [];
3768
+ addPrefillFilesToIndex(initialFiles, state.resourceIndex);
3769
+ filesWrapper.dataset.resourceIds = JSON.stringify(initialFiles);
3770
+ const multipleFilesHint = makeFieldHint(element, state);
3771
+ const multipleConstraints = {
3772
+ maxCount: maxFiles,
3773
+ allowedExtensions: getAllowedExtensions(element.accept),
3774
+ maxSize: (_d = element.maxSize) != null ? _d : Infinity
3775
+ };
3776
+ const buildCountInfo = () => {
3777
+ const countText = initialFiles.length === 1 ? t("fileCountSingle", state, { count: initialFiles.length }) : t("fileCountPlural", state, { count: initialFiles.length });
3778
+ const minMaxText = minFiles > 0 || maxFiles < Infinity ? ` ${t("fileCountRange", state, { min: minFiles, max: maxFiles })}` : "";
3779
+ return countText + minMaxText;
3780
+ };
3781
+ const updateFilesDisplay = () => {
3782
+ const currentlyReadonly = isElementReadonly(element, state);
3783
+ renderResourcePills(
3784
+ list,
3327
3785
  initialFiles,
3328
3786
  state,
3329
- updateFilesDisplay,
3330
- multipleConstraints,
3331
- pathKey,
3332
- ctx.instance
3787
+ currentlyReadonly ? null : (index) => {
3788
+ var _a2;
3789
+ releaseLocalFileUrl((_a2 = state.resourceIndex.get(index)) == null ? void 0 : _a2.file);
3790
+ initialFiles.splice(initialFiles.indexOf(index), 1);
3791
+ updateFilesDisplay();
3792
+ },
3793
+ multipleFilesHint,
3794
+ buildCountInfo(),
3795
+ maxFiles < Infinity ? maxFiles : void 0,
3796
+ currentlyReadonly
3333
3797
  );
3334
- setupFilesPickerHandler(
3335
- filesPicker,
3336
- initialFiles,
3337
- state,
3338
- updateFilesDisplay,
3339
- multipleConstraints,
3340
- pathKey,
3341
- ctx.instance
3798
+ };
3799
+ setupFilesDropHandler(
3800
+ filesContainer,
3801
+ initialFiles,
3802
+ state,
3803
+ updateFilesDisplay,
3804
+ multipleConstraints,
3805
+ pathKey,
3806
+ ctx.instance
3807
+ );
3808
+ setupFilesPickerHandler(
3809
+ filesPicker,
3810
+ initialFiles,
3811
+ state,
3812
+ updateFilesDisplay,
3813
+ multipleConstraints,
3814
+ pathKey,
3815
+ ctx.instance
3816
+ );
3817
+ updateFilesDisplay();
3818
+ wrapper.appendChild(filesWrapper);
3819
+ }
3820
+
3821
+ // src/components/file/validate.ts
3822
+ function readMultiFileResourceIds(scopeRoot, fullKey) {
3823
+ const wrapper = scopeRoot.querySelector(
3824
+ `[data-files-wrapper="${fullKey}"]`
3825
+ );
3826
+ if (!wrapper) return [];
3827
+ const encoded = wrapper.dataset.resourceIds;
3828
+ if (encoded === void 0) {
3829
+ throw new Error(
3830
+ `readMultiFileResourceIds: [data-files-wrapper="${fullKey}"] is missing data-resource-ids attribute. This is a render bug.`
3342
3831
  );
3343
- updateFilesDisplay();
3344
- wrapper.appendChild(filesWrapper);
3345
3832
  }
3833
+ const parsed = JSON.parse(encoded);
3834
+ if (!Array.isArray(parsed)) {
3835
+ throw new Error(
3836
+ `readMultiFileResourceIds: data-resource-ids on [data-files-wrapper="${fullKey}"] is not a JSON array. Got: ${encoded}`
3837
+ );
3838
+ }
3839
+ return parsed;
3346
3840
  }
3347
- function validateFileElement(element, key, context) {
3841
+ function validateFileCount(key, resourceIds, element, state, errors) {
3842
+ var _a, _b;
3843
+ const minFiles = "minCount" in element ? (_a = element.minCount) != null ? _a : 0 : 0;
3844
+ const maxFiles = "maxCount" in element ? (_b = element.maxCount) != null ? _b : Infinity : Infinity;
3845
+ if (element.required && resourceIds.length === 0) {
3846
+ errors.push(`${key}: ${t("required", state)}`);
3847
+ }
3848
+ if (resourceIds.length < minFiles) {
3849
+ errors.push(`${key}: ${t("minFiles", state, { min: minFiles })}`);
3850
+ }
3851
+ if (resourceIds.length > maxFiles) {
3852
+ errors.push(`${key}: ${t("maxFiles", state, { max: maxFiles })}`);
3853
+ }
3854
+ }
3855
+ function validateFileExtensions(key, resourceIds, element, state, errors) {
3348
3856
  var _a;
3349
- const errors = [];
3350
- const { scopeRoot, skipValidation, path } = context;
3351
- const isMultipleField = element.type === "files" || "multiple" in element && Boolean(element.multiple);
3352
- const validateFileCount = (key2, resourceIds, element2) => {
3353
- var _a2, _b;
3354
- if (skipValidation) return;
3355
- const { state } = context;
3356
- const minFiles = "minCount" in element2 ? (_a2 = element2.minCount) != null ? _a2 : 0 : 0;
3357
- const maxFiles = "maxCount" in element2 ? (_b = element2.maxCount) != null ? _b : Infinity : Infinity;
3358
- if (element2.required && resourceIds.length === 0) {
3359
- errors.push(`${key2}: ${t("required", state)}`);
3360
- }
3361
- if (resourceIds.length < minFiles) {
3362
- errors.push(`${key2}: ${t("minFiles", state, { min: minFiles })}`);
3363
- }
3364
- if (resourceIds.length > maxFiles) {
3365
- errors.push(`${key2}: ${t("maxFiles", state, { max: maxFiles })}`);
3366
- }
3367
- };
3368
- const validateFileExtensions = (key2, resourceIds, element2) => {
3369
- var _a2;
3370
- if (skipValidation) return;
3371
- const { state } = context;
3372
- const acceptField = "accept" in element2 ? element2.accept : void 0;
3373
- const allowedExtensions = getAllowedExtensions(acceptField);
3374
- if (allowedExtensions.length === 0) return;
3375
- const formats = allowedExtensions.join(", ");
3376
- for (const rid of resourceIds) {
3377
- const meta = state.resourceIndex.get(rid);
3378
- const fileName = (_a2 = meta == null ? void 0 : meta.name) != null ? _a2 : rid;
3379
- if (!isFileExtensionAllowed(fileName, allowedExtensions)) {
3380
- errors.push(
3381
- `${key2}: ${t("invalidFileExtension", state, { name: fileName, formats })}`
3382
- );
3383
- }
3857
+ const acceptField = "accept" in element ? element.accept : void 0;
3858
+ const allowedExtensions = getAllowedExtensions(acceptField);
3859
+ if (allowedExtensions.length === 0) return;
3860
+ const formats = allowedExtensions.join(", ");
3861
+ for (const rid of resourceIds) {
3862
+ const meta = state.resourceIndex.get(rid);
3863
+ const fileName = (_a = meta == null ? void 0 : meta.name) != null ? _a : rid;
3864
+ if (!isFileExtensionAllowed(fileName, allowedExtensions)) {
3865
+ errors.push(
3866
+ `${key}: ${t("invalidFileExtension", state, { name: fileName, formats })}`
3867
+ );
3384
3868
  }
3385
- };
3386
- const validateFileSizes = (key2, resourceIds, element2) => {
3387
- var _a2;
3388
- if (skipValidation) return;
3389
- const { state } = context;
3390
- const maxSizeMB = "maxSize" in element2 ? (_a2 = element2.maxSize) != null ? _a2 : Infinity : Infinity;
3391
- if (maxSizeMB === Infinity) return;
3392
- for (const rid of resourceIds) {
3393
- const meta = state.resourceIndex.get(rid);
3394
- if (!meta) continue;
3395
- if (meta.size > maxSizeMB * 1024 * 1024) {
3396
- errors.push(
3397
- `${key2}: ${t("fileTooLarge", state, { name: meta.name, maxSize: maxSizeMB })}`
3398
- );
3399
- }
3869
+ }
3870
+ }
3871
+ function validateFileSizes(key, resourceIds, element, state, errors) {
3872
+ var _a;
3873
+ const maxSizeMB = "maxSize" in element ? (_a = element.maxSize) != null ? _a : Infinity : Infinity;
3874
+ if (maxSizeMB === Infinity) return;
3875
+ for (const rid of resourceIds) {
3876
+ const meta = state.resourceIndex.get(rid);
3877
+ if (!meta) continue;
3878
+ if (meta.size > maxSizeMB * 1024 * 1024) {
3879
+ errors.push(
3880
+ `${key}: ${t("fileTooLarge", state, { name: meta.name, maxSize: maxSizeMB })}`
3881
+ );
3400
3882
  }
3401
- };
3883
+ }
3884
+ }
3885
+ function validateMultiFile(element, key, context) {
3886
+ const { scopeRoot, skipValidation, path, state } = context;
3887
+ const errors = [];
3888
+ const fullKey = pathJoin(path, key);
3889
+ const resourceIds = readMultiFileResourceIds(scopeRoot, fullKey);
3890
+ if (!skipValidation) {
3891
+ validateFileCount(key, resourceIds, element, state, errors);
3892
+ validateFileExtensions(key, resourceIds, element, state, errors);
3893
+ validateFileSizes(key, resourceIds, element, state, errors);
3894
+ }
3895
+ return { value: resourceIds, errors };
3896
+ }
3897
+ function validateSingleFile(element, key, context) {
3898
+ var _a;
3899
+ const { scopeRoot, skipValidation, state } = context;
3900
+ const errors = [];
3901
+ const input = scopeRoot.querySelector(
3902
+ `input[name$="${key}"][type="hidden"]`
3903
+ );
3904
+ const rid = (_a = input == null ? void 0 : input.value) != null ? _a : "";
3905
+ if (!skipValidation && element.required && rid === "") {
3906
+ errors.push(`${key}: ${t("required", state)}`);
3907
+ return { value: null, errors };
3908
+ }
3909
+ if (!skipValidation && rid !== "") {
3910
+ validateFileExtensions(key, [rid], element, state, errors);
3911
+ validateFileSizes(key, [rid], element, state, errors);
3912
+ }
3913
+ return { value: rid || null, errors };
3914
+ }
3915
+ function validateFileElement(element, key, context) {
3916
+ const isMultipleField = element.type === "files" || "multiple" in element && Boolean(element.multiple);
3402
3917
  if (isMultipleField) {
3403
- const fullKey = pathJoin(path, key);
3404
- const filesWrapper = scopeRoot.querySelector(
3405
- `[data-files-wrapper="${fullKey}"]`
3406
- );
3407
- const container = (filesWrapper == null ? void 0 : filesWrapper.querySelector(".files-list")) || null;
3408
- const resourceIds = [];
3409
- if (container) {
3410
- const pills = container.querySelectorAll(".resource-pill");
3411
- pills.forEach((pill) => {
3412
- const resourceId = pill.dataset.resourceId;
3413
- if (resourceId) {
3414
- resourceIds.push(resourceId);
3415
- }
3416
- });
3417
- }
3418
- validateFileCount(key, resourceIds, element);
3419
- validateFileExtensions(key, resourceIds, element);
3420
- validateFileSizes(key, resourceIds, element);
3421
- return { value: resourceIds, errors };
3918
+ return validateMultiFile(element, key, context);
3919
+ }
3920
+ return validateSingleFile(element, key, context);
3921
+ }
3922
+
3923
+ // src/components/file/render-readonly.ts
3924
+ function renderFileElementReadonly(element, ctx, wrapper, pathKey) {
3925
+ const state = ctx.state;
3926
+ const rawInitial = ctx.prefill[element.key];
3927
+ const initial = typeof rawInitial === "string" ? rawInitial : "";
3928
+ if (initial) {
3929
+ addPrefillFilesToIndex([initial], state.resourceIndex);
3930
+ const hiddenInput = document.createElement("input");
3931
+ hiddenInput.type = "hidden";
3932
+ hiddenInput.name = pathKey;
3933
+ hiddenInput.value = initial;
3934
+ wrapper.appendChild(hiddenInput);
3935
+ renderFilePreviewReadonly(initial, state).then((filePreview) => {
3936
+ wrapper.appendChild(filePreview);
3937
+ }).catch((err) => {
3938
+ console.error("Failed to render file preview:", err);
3939
+ wrapper.appendChild(buildEmptyReadonlyTile(state));
3940
+ });
3422
3941
  } else {
3423
- const input = scopeRoot.querySelector(
3424
- `input[name$="${key}"][type="hidden"]`
3425
- );
3426
- const rid = (_a = input == null ? void 0 : input.value) != null ? _a : "";
3427
- if (!skipValidation && element.required && rid === "") {
3428
- errors.push(`${key}: ${t("required", context.state)}`);
3429
- return { value: null, errors };
3430
- }
3431
- if (!skipValidation && rid !== "") {
3432
- validateFileExtensions(key, [rid], element);
3433
- validateFileSizes(key, [rid], element);
3434
- }
3435
- return { value: rid || null, errors };
3942
+ wrapper.appendChild(buildEmptyReadonlyTile(state));
3943
+ }
3944
+ }
3945
+ function buildEmptyReadonlyTile(state) {
3946
+ const emptyState = document.createElement("div");
3947
+ emptyState.style.cssText = `
3948
+ width:${TILE_SIZE};
3949
+ height:${TILE_SIZE};
3950
+ display:flex;
3951
+ align-items:center;
3952
+ justify-content:center;
3953
+ background:var(--fb-file-upload-bg-color,#f3f4f6);
3954
+ border-radius:var(--fb-border-radius,0.5rem);
3955
+ border:1px solid var(--fb-file-upload-border-color,#d1d5db);
3956
+ `;
3957
+ emptyState.innerHTML = `<div style="font-size:11px;text-align:center;color:var(--fb-text-secondary-color,#6b7280);">${escapeHtml(t("noFileSelected", state))}</div>`;
3958
+ return emptyState;
3959
+ }
3960
+ function renderMultiFileReadonly(rids, state, wrapper, pathKey, marginTop) {
3961
+ addPrefillFilesToIndex(rids, state.resourceIndex);
3962
+ const filesWrapper = document.createElement("div");
3963
+ filesWrapper.dataset.filesWrapper = pathKey;
3964
+ filesWrapper.dataset.resourceIds = JSON.stringify(rids);
3965
+ wrapper.appendChild(filesWrapper);
3966
+ if (rids.length === 0) {
3967
+ const emptyEl = document.createElement("div");
3968
+ emptyEl.className = "fb-tile-empty-text";
3969
+ emptyEl.textContent = t("noFilesSelected", state);
3970
+ filesWrapper.appendChild(emptyEl);
3971
+ return;
3972
+ }
3973
+ const tilesWrap = document.createElement("div");
3974
+ tilesWrap.style.cssText = `display:flex;flex-wrap:wrap;gap:6px;${marginTop ? `margin-top:${marginTop};` : ""}`;
3975
+ filesWrapper.appendChild(tilesWrap);
3976
+ const placeholders = rids.map(() => {
3977
+ const placeholder = document.createElement("div");
3978
+ placeholder.style.cssText = `width:${TILE_SIZE};height:${TILE_SIZE};`;
3979
+ tilesWrap.appendChild(placeholder);
3980
+ return placeholder;
3981
+ });
3982
+ for (let i = 0; i < rids.length; i++) {
3983
+ const resourceId = rids[i];
3984
+ const placeholder = placeholders[i];
3985
+ renderFilePreviewReadonly(resourceId, state).then((tileEl) => {
3986
+ placeholder.replaceWith(tileEl);
3987
+ }).catch((err) => {
3988
+ console.error("Failed to render readonly tile:", err);
3989
+ });
3990
+ }
3991
+ }
3992
+ function renderFilesElementReadonly(element, ctx, wrapper, pathKey) {
3993
+ const rawPrefill = ctx.prefill[element.key];
3994
+ const initialFiles = Array.isArray(rawPrefill) ? rawPrefill : [];
3995
+ renderMultiFileReadonly(initialFiles, ctx.state, wrapper, pathKey);
3996
+ }
3997
+ function renderMultipleFileElementReadonly(element, ctx, wrapper, pathKey) {
3998
+ const rawPrefill = ctx.prefill[element.key];
3999
+ const initialFiles = Array.isArray(rawPrefill) ? rawPrefill : [];
4000
+ renderMultiFileReadonly(initialFiles, ctx.state, wrapper, pathKey, "4px");
4001
+ }
4002
+
4003
+ // src/components/file.ts
4004
+ function renderFileElement(element, ctx, wrapper, pathKey) {
4005
+ if (isElementReadonly(element, ctx.state, ctx)) {
4006
+ renderFileElementReadonly(element, ctx, wrapper, pathKey);
4007
+ } else {
4008
+ renderFileElementEdit(element, ctx, wrapper, pathKey);
4009
+ }
4010
+ }
4011
+ function renderFilesElement(element, ctx, wrapper, pathKey) {
4012
+ if (isElementReadonly(element, ctx.state, ctx)) {
4013
+ renderFilesElementReadonly(element, ctx, wrapper, pathKey);
4014
+ } else {
4015
+ renderFilesElementEdit(element, ctx, wrapper, pathKey);
4016
+ }
4017
+ }
4018
+ function renderMultipleFileElement(element, ctx, wrapper, pathKey) {
4019
+ if (isElementReadonly(element, ctx.state, ctx)) {
4020
+ renderMultipleFileElementReadonly(element, ctx, wrapper, pathKey);
4021
+ } else {
4022
+ renderMultipleFileElementEdit(element, ctx, wrapper, pathKey);
3436
4023
  }
3437
4024
  }
3438
4025
  function updateFileField(element, fieldPath, value, context) {
3439
- var _a;
3440
4026
  const { scopeRoot, state } = context;
3441
- if ("multiple" in element && element.multiple) {
4027
+ if (element.type === "files" || "multiple" in element && element.multiple) {
3442
4028
  if (!Array.isArray(value)) {
3443
4029
  console.warn(
3444
4030
  `updateFileField: Expected array for multiple file field "${fieldPath}", got ${typeof value}`
@@ -3446,32 +4032,22 @@ function updateFileField(element, fieldPath, value, context) {
3446
4032
  return;
3447
4033
  }
3448
4034
  value.forEach((resourceId) => {
3449
- var _a2;
3450
4035
  if (resourceId && typeof resourceId === "string") {
3451
4036
  if (!state.resourceIndex.has(resourceId)) {
3452
- const filename = resourceId.split("/").pop() || "file";
3453
- const extension = (_a2 = filename.split(".").pop()) == null ? void 0 : _a2.toLowerCase();
3454
- let fileType = "application/octet-stream";
3455
- if (extension) {
3456
- if (["jpg", "jpeg", "png", "gif", "webp"].includes(extension)) {
3457
- fileType = `image/${extension === "jpg" ? "jpeg" : extension}`;
3458
- } else if (["mp4", "webm", "mov", "avi"].includes(extension)) {
3459
- fileType = `video/${extension === "mov" ? "quicktime" : extension}`;
3460
- }
3461
- }
3462
- state.resourceIndex.set(resourceId, {
3463
- name: filename,
3464
- type: fileType,
3465
- size: 0,
3466
- uploadedAt: /* @__PURE__ */ new Date(),
3467
- file: void 0
3468
- });
4037
+ addResourceToIndex(resourceId, state);
3469
4038
  }
3470
4039
  }
3471
4040
  });
3472
- console.info(
3473
- `updateFileField: Multiple file field "${fieldPath}" updated. Preview update requires re-render.`
4041
+ const filesWrapper = scopeRoot.querySelector(
4042
+ `[data-files-wrapper="${fieldPath}"]`
3474
4043
  );
4044
+ if (filesWrapper) {
4045
+ filesWrapper.dataset.resourceIds = JSON.stringify(value);
4046
+ } else {
4047
+ console.warn(
4048
+ `updateFileField: [data-files-wrapper="${fieldPath}"] not found in DOM; data-resource-ids not updated`
4049
+ );
4050
+ }
3475
4051
  } else {
3476
4052
  const hiddenInput = scopeRoot.querySelector(
3477
4053
  `input[name="${fieldPath}"][type="hidden"]`
@@ -3485,23 +4061,7 @@ function updateFileField(element, fieldPath, value, context) {
3485
4061
  hiddenInput.value = value != null ? String(value) : "";
3486
4062
  if (value && typeof value === "string") {
3487
4063
  if (!state.resourceIndex.has(value)) {
3488
- const filename = value.split("/").pop() || "file";
3489
- const extension = (_a = filename.split(".").pop()) == null ? void 0 : _a.toLowerCase();
3490
- let fileType = "application/octet-stream";
3491
- if (extension) {
3492
- if (["jpg", "jpeg", "png", "gif", "webp"].includes(extension)) {
3493
- fileType = `image/${extension === "jpg" ? "jpeg" : extension}`;
3494
- } else if (["mp4", "webm", "mov", "avi"].includes(extension)) {
3495
- fileType = `video/${extension === "mov" ? "quicktime" : extension}`;
3496
- }
3497
- }
3498
- state.resourceIndex.set(value, {
3499
- name: filename,
3500
- type: fileType,
3501
- size: 0,
3502
- uploadedAt: /* @__PURE__ */ new Date(),
3503
- file: void 0
3504
- });
4064
+ addResourceToIndex(value, state);
3505
4065
  }
3506
4066
  console.info(
3507
4067
  `updateFileField: File field "${fieldPath}" updated. Preview update requires re-render.`
@@ -3509,6 +4069,26 @@ function updateFileField(element, fieldPath, value, context) {
3509
4069
  }
3510
4070
  }
3511
4071
  }
4072
+ function addResourceToIndex(resourceId, state) {
4073
+ var _a;
4074
+ const filename = resourceId.split("/").pop() || "file";
4075
+ const extension = (_a = filename.split(".").pop()) == null ? void 0 : _a.toLowerCase();
4076
+ let fileType = "application/octet-stream";
4077
+ if (extension) {
4078
+ if (["jpg", "jpeg", "png", "gif", "webp"].includes(extension)) {
4079
+ fileType = `image/${extension === "jpg" ? "jpeg" : extension}`;
4080
+ } else if (["mp4", "webm", "mov", "avi"].includes(extension)) {
4081
+ fileType = `video/${extension === "mov" ? "quicktime" : extension}`;
4082
+ }
4083
+ }
4084
+ state.resourceIndex.set(resourceId, {
4085
+ name: filename,
4086
+ type: fileType,
4087
+ size: 0,
4088
+ uploadedAt: /* @__PURE__ */ new Date(),
4089
+ file: void 0
4090
+ });
4091
+ }
3512
4092
 
3513
4093
  // src/components/colour.ts
3514
4094
  function normalizeColourValue(value) {
@@ -8256,6 +8836,8 @@ var defaultConfig = {
8256
8836
  noFileSelected: "No file selected",
8257
8837
  noFilesSelected: "No files selected",
8258
8838
  downloadButton: "Download",
8839
+ downloadFile: "Download",
8840
+ openInNewTab: "Open in new tab",
8259
8841
  changeButton: "Change",
8260
8842
  placeholderText: "Enter text",
8261
8843
  previewAlt: "Preview",
@@ -8277,6 +8859,8 @@ var defaultConfig = {
8277
8859
  fileCountSingle: "{count} file",
8278
8860
  fileCountPlural: "{count} files",
8279
8861
  fileCountRange: "({min}-{max})",
8862
+ uploadingFile: "Uploading\u2026",
8863
+ filesCounter: "{count}/{max}",
8280
8864
  // Validation errors
8281
8865
  required: "Required",
8282
8866
  minItems: "Minimum {min} items required",
@@ -8318,6 +8902,8 @@ var defaultConfig = {
8318
8902
  noFileSelected: "\u0424\u0430\u0439\u043B \u043D\u0435 \u0432\u044B\u0431\u0440\u0430\u043D",
8319
8903
  noFilesSelected: "\u041D\u0435\u0442 \u0444\u0430\u0439\u043B\u043E\u0432",
8320
8904
  downloadButton: "\u0421\u043A\u0430\u0447\u0430\u0442\u044C",
8905
+ downloadFile: "\u0421\u043A\u0430\u0447\u0430\u0442\u044C",
8906
+ openInNewTab: "\u041E\u0442\u043A\u0440\u044B\u0442\u044C \u0432 \u043D\u043E\u0432\u043E\u0439 \u0432\u043A\u043B\u0430\u0434\u043A\u0435",
8321
8907
  changeButton: "\u0418\u0437\u043C\u0435\u043D\u0438\u0442\u044C",
8322
8908
  placeholderText: "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0442\u0435\u043A\u0441\u0442",
8323
8909
  previewAlt: "\u041F\u0440\u0435\u0434\u043F\u0440\u043E\u0441\u043C\u043E\u0442\u0440",
@@ -8339,6 +8925,8 @@ var defaultConfig = {
8339
8925
  fileCountSingle: "{count} \u0444\u0430\u0439\u043B",
8340
8926
  fileCountPlural: "{count} \u0444\u0430\u0439\u043B\u043E\u0432",
8341
8927
  fileCountRange: "({min}-{max})",
8928
+ uploadingFile: "\u0417\u0430\u0433\u0440\u0443\u0437\u043A\u0430\u2026",
8929
+ filesCounter: "{count}/{max}",
8342
8930
  // Validation errors
8343
8931
  required: "\u041E\u0431\u044F\u0437\u0430\u0442\u0435\u043B\u044C\u043D\u043E\u0435 \u043F\u043E\u043B\u0435",
8344
8932
  minItems: "\u041C\u0438\u043D\u0438\u043C\u0443\u043C {min} \u044D\u043B\u0435\u043C\u0435\u043D\u0442\u043E\u0432",