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