@dmitryvim/form-builder 0.2.26 → 0.2.28
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 +60 -4
- package/dist/browser/formbuilder.min.js +534 -184
- package/dist/browser/formbuilder.v0.2.28.min.js +956 -0
- package/dist/cjs/index.cjs +2002 -1005
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/esm/index.js +1976 -992
- package/dist/esm/index.js.map +1 -1
- package/dist/form-builder.js +534 -184
- package/dist/types/components/file/constraints.d.ts +62 -0
- package/dist/types/components/file/dom.d.ts +44 -0
- package/dist/types/components/file/library.d.ts +49 -0
- package/dist/types/components/file/preview.d.ts +69 -0
- package/dist/types/components/file/render-edit.d.ts +18 -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 +25 -0
- package/dist/types/components/file/validate.d.ts +13 -0
- package/dist/types/components/file.d.ts +5 -27
- package/dist/types/index.d.ts +1 -1
- package/dist/types/types/config.d.ts +37 -0
- package/dist/types/types/index.d.ts +1 -1
- package/dist/types/types/schema.d.ts +13 -6
- 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,768 @@ 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
|
-
function
|
|
2107
|
+
function isSizeWithinLimit(bytes, maxSizeMB) {
|
|
2108
2108
|
if (maxSizeMB === Infinity) return true;
|
|
2109
|
-
return
|
|
2109
|
+
return bytes <= maxSizeMB * 1024 * 1024;
|
|
2110
|
+
}
|
|
2111
|
+
function isFileSizeAllowed(file, maxSizeMB) {
|
|
2112
|
+
return isSizeWithinLimit(file.size, maxSizeMB);
|
|
2113
|
+
}
|
|
2114
|
+
function getAllowedMimes(accept) {
|
|
2115
|
+
if (!accept) return [];
|
|
2116
|
+
if (typeof accept === "string") return [];
|
|
2117
|
+
if (!Array.isArray(accept.mime)) return [];
|
|
2118
|
+
return accept.mime.map((m) => m.toLowerCase());
|
|
2119
|
+
}
|
|
2120
|
+
function isMimeAllowed(mimeType, allowedMimes) {
|
|
2121
|
+
if (allowedMimes.length === 0) return true;
|
|
2122
|
+
const normalizedType = mimeType.toLowerCase();
|
|
2123
|
+
return allowedMimes.some((allowed) => {
|
|
2124
|
+
if (allowed.endsWith("/*")) {
|
|
2125
|
+
const prefix = allowed.slice(0, -1);
|
|
2126
|
+
return normalizedType.startsWith(prefix);
|
|
2127
|
+
}
|
|
2128
|
+
return normalizedType === allowed;
|
|
2129
|
+
});
|
|
2130
|
+
}
|
|
2131
|
+
function inferMimeFromResourceId(resourceId) {
|
|
2132
|
+
const filename = resourceId.split("/").pop() || "file";
|
|
2133
|
+
const extension = filename.split(".").pop()?.toLowerCase();
|
|
2134
|
+
if (extension) {
|
|
2135
|
+
if (["jpg", "jpeg", "png", "gif", "webp"].includes(extension)) {
|
|
2136
|
+
return `image/${extension === "jpg" ? "jpeg" : extension}`;
|
|
2137
|
+
}
|
|
2138
|
+
if (["mp4", "webm", "mov", "avi"].includes(extension)) {
|
|
2139
|
+
return `video/${extension === "mov" ? "quicktime" : extension}`;
|
|
2140
|
+
}
|
|
2141
|
+
}
|
|
2142
|
+
return "application/octet-stream";
|
|
2143
|
+
}
|
|
2144
|
+
function seedInferredResource(resourceId, resourceIndex) {
|
|
2145
|
+
if (resourceIndex.has(resourceId)) return;
|
|
2146
|
+
const filename = resourceId.split("/").pop() || "file";
|
|
2147
|
+
resourceIndex.set(resourceId, {
|
|
2148
|
+
name: filename,
|
|
2149
|
+
type: inferMimeFromResourceId(resourceId),
|
|
2150
|
+
size: 0,
|
|
2151
|
+
uploadedAt: /* @__PURE__ */ new Date(),
|
|
2152
|
+
file: void 0,
|
|
2153
|
+
inferredFromExtension: true
|
|
2154
|
+
});
|
|
2155
|
+
}
|
|
2156
|
+
function addPrefillFilesToIndex(initialFiles, resourceIndex) {
|
|
2157
|
+
for (const resourceId of initialFiles) {
|
|
2158
|
+
seedInferredResource(resourceId, resourceIndex);
|
|
2159
|
+
}
|
|
2160
|
+
}
|
|
2161
|
+
|
|
2162
|
+
// src/components/file/styles.ts
|
|
2163
|
+
var STYLE_ID = "fb-file-styles";
|
|
2164
|
+
function ensureFileStyles() {
|
|
2165
|
+
if (typeof document === "undefined") return;
|
|
2166
|
+
if (document.getElementById(STYLE_ID)) return;
|
|
2167
|
+
const style = document.createElement("style");
|
|
2168
|
+
style.id = STYLE_ID;
|
|
2169
|
+
style.setAttribute("data-fb-file-styles", "true");
|
|
2170
|
+
style.textContent = `
|
|
2171
|
+
@keyframes fb-spin { to { transform: rotate(360deg); } }
|
|
2172
|
+
|
|
2173
|
+
/* Spinner used during single-file and multi-file upload */
|
|
2174
|
+
.fb-spinner {
|
|
2175
|
+
width: 36px;
|
|
2176
|
+
height: 36px;
|
|
2177
|
+
border: 3px solid rgba(0,0,0,0.12);
|
|
2178
|
+
border-top-color: var(--fb-text-secondary-color, #6b7280);
|
|
2179
|
+
border-radius: 50%;
|
|
2180
|
+
animation: fb-spin 0.7s linear infinite;
|
|
2181
|
+
flex-shrink: 0;
|
|
2182
|
+
}
|
|
2183
|
+
|
|
2184
|
+
/* Base tile: fixed 160\xD7160 square, theme-aware background */
|
|
2185
|
+
.fb-tile {
|
|
2186
|
+
width: var(--fb-tile-size, 160px);
|
|
2187
|
+
height: var(--fb-tile-size, 160px);
|
|
2188
|
+
flex-shrink: 0;
|
|
2189
|
+
position: relative;
|
|
2190
|
+
overflow: hidden;
|
|
2191
|
+
border-radius: var(--fb-border-radius, 0.5rem);
|
|
2192
|
+
background: var(--fb-file-upload-bg-color, #f3f4f6);
|
|
2193
|
+
}
|
|
2194
|
+
|
|
2195
|
+
/* Uploaded resource tile \u2014 adds a visible border */
|
|
2196
|
+
.fb-tile-resource {
|
|
2197
|
+
border: 1px solid var(--fb-file-upload-border-color, #d1d5db);
|
|
2198
|
+
}
|
|
2199
|
+
|
|
2200
|
+
/* Uploading placeholder tile \u2014 dashed border, uploading indicator */
|
|
2201
|
+
.fb-tile-uploading {
|
|
2202
|
+
border: 2px dashed var(--fb-file-upload-border-color, #d1d5db);
|
|
2203
|
+
}
|
|
2204
|
+
|
|
2205
|
+
/* "+" add-more tile */
|
|
2206
|
+
.fb-tile-add {
|
|
2207
|
+
border: 2px dashed var(--fb-file-upload-border-color, #d1d5db);
|
|
2208
|
+
display: flex;
|
|
2209
|
+
align-items: center;
|
|
2210
|
+
justify-content: center;
|
|
2211
|
+
cursor: pointer;
|
|
2212
|
+
font-size: 32px;
|
|
2213
|
+
color: var(--fb-file-upload-text-color, #9ca3af);
|
|
2214
|
+
transition:
|
|
2215
|
+
border-color var(--fb-transition-duration, 200ms),
|
|
2216
|
+
color var(--fb-transition-duration, 200ms);
|
|
2217
|
+
}
|
|
2218
|
+
.fb-tile-add:hover {
|
|
2219
|
+
border-color: var(--fb-file-upload-hover-border-color, #3b82f6);
|
|
2220
|
+
color: var(--fb-text-color, #1f2937);
|
|
2221
|
+
}
|
|
2222
|
+
|
|
2223
|
+
/* Count chip shown when at maxCount */
|
|
2224
|
+
.fb-tile-counter {
|
|
2225
|
+
font-size: 11px;
|
|
2226
|
+
color: var(--fb-text-secondary-color, #6b7280);
|
|
2227
|
+
background: var(--fb-file-upload-bg-color, #f3f4f6);
|
|
2228
|
+
border: 1px solid var(--fb-file-upload-border-color, #d1d5db);
|
|
2229
|
+
border-radius: 4px;
|
|
2230
|
+
padding: 2px 6px;
|
|
2231
|
+
align-self: flex-end;
|
|
2232
|
+
margin-bottom: 4px;
|
|
2233
|
+
}
|
|
2234
|
+
|
|
2235
|
+
/* Empty-state dropzone */
|
|
2236
|
+
.fb-file-dropzone {
|
|
2237
|
+
width: 100%;
|
|
2238
|
+
height: 128px;
|
|
2239
|
+
border: 2px dashed var(--fb-file-upload-border-color, #d1d5db);
|
|
2240
|
+
border-radius: var(--fb-border-radius, 0.5rem);
|
|
2241
|
+
display: flex;
|
|
2242
|
+
flex-direction: column;
|
|
2243
|
+
align-items: center;
|
|
2244
|
+
justify-content: center;
|
|
2245
|
+
gap: 4px;
|
|
2246
|
+
cursor: pointer;
|
|
2247
|
+
transition:
|
|
2248
|
+
border-color var(--fb-transition-duration, 200ms),
|
|
2249
|
+
background var(--fb-transition-duration, 200ms);
|
|
2250
|
+
}
|
|
2251
|
+
.fb-file-dropzone:hover {
|
|
2252
|
+
border-color: var(--fb-file-upload-hover-border-color, #3b82f6);
|
|
2253
|
+
background: var(--fb-background-hover-color, #f9fafb);
|
|
2254
|
+
}
|
|
2255
|
+
|
|
2256
|
+
/* Inline text inside tiles */
|
|
2257
|
+
.fb-tile-label {
|
|
2258
|
+
font-size: 9px;
|
|
2259
|
+
color: var(--fb-text-secondary-color, #6b7280);
|
|
2260
|
+
text-align: center;
|
|
2261
|
+
overflow: hidden;
|
|
2262
|
+
word-break: break-all;
|
|
2263
|
+
max-height: 28px;
|
|
2264
|
+
}
|
|
2265
|
+
.fb-tile-uploading-text {
|
|
2266
|
+
font-size: 8px;
|
|
2267
|
+
color: var(--fb-file-upload-text-color, #9ca3af);
|
|
2268
|
+
}
|
|
2269
|
+
.fb-tile-hint {
|
|
2270
|
+
font-size: 11px;
|
|
2271
|
+
color: var(--fb-file-upload-text-color, #9ca3af);
|
|
2272
|
+
margin-top: 4px;
|
|
2273
|
+
}
|
|
2274
|
+
.fb-tile-empty-text {
|
|
2275
|
+
font-size: 12px;
|
|
2276
|
+
color: var(--fb-text-secondary-color, #6b7280);
|
|
2277
|
+
padding: 4px 0;
|
|
2278
|
+
}
|
|
2279
|
+
.fb-dropzone-primary-text {
|
|
2280
|
+
font-size: 13px;
|
|
2281
|
+
color: var(--fb-text-secondary-color, #6b7280);
|
|
2282
|
+
}
|
|
2283
|
+
.fb-dropzone-hint-text {
|
|
2284
|
+
font-size: 11px;
|
|
2285
|
+
color: var(--fb-file-upload-text-color, #9ca3af);
|
|
2286
|
+
}
|
|
2287
|
+
|
|
2288
|
+
/* Hover overlay + X-button on resource tiles */
|
|
2289
|
+
.fb-tile-overlay {
|
|
2290
|
+
position: absolute;
|
|
2291
|
+
inset: 0;
|
|
2292
|
+
background: transparent;
|
|
2293
|
+
transition: background var(--fb-transition-duration, 200ms);
|
|
2294
|
+
display: flex;
|
|
2295
|
+
align-items: flex-start;
|
|
2296
|
+
justify-content: flex-end;
|
|
2297
|
+
}
|
|
2298
|
+
.fb-tile-resource:hover .fb-tile-overlay {
|
|
2299
|
+
background: var(--fb-tile-hover-overlay-color, rgba(0,0,0,0.4));
|
|
2300
|
+
}
|
|
2301
|
+
.fb-tile-x-btn {
|
|
2302
|
+
margin: 3px;
|
|
2303
|
+
width: 18px;
|
|
2304
|
+
height: 18px;
|
|
2305
|
+
background: var(--fb-error-color, #ef4444);
|
|
2306
|
+
color: var(--fb-file-bg-color, #fff);
|
|
2307
|
+
border: none;
|
|
2308
|
+
border-radius: 50%;
|
|
2309
|
+
font-size: 11px;
|
|
2310
|
+
line-height: 1;
|
|
2311
|
+
cursor: pointer;
|
|
2312
|
+
display: flex;
|
|
2313
|
+
align-items: center;
|
|
2314
|
+
justify-content: center;
|
|
2315
|
+
opacity: 0;
|
|
2316
|
+
transition: opacity var(--fb-transition-duration, 200ms);
|
|
2317
|
+
}
|
|
2318
|
+
.fb-tile-resource:hover .fb-tile-x-btn {
|
|
2319
|
+
opacity: 1;
|
|
2320
|
+
}
|
|
2321
|
+
|
|
2322
|
+
/* Video play button overlay (readonly tiles with video thumbnails) */
|
|
2323
|
+
.fb-video-overlay {
|
|
2324
|
+
position: absolute;
|
|
2325
|
+
inset: 0;
|
|
2326
|
+
display: flex;
|
|
2327
|
+
align-items: center;
|
|
2328
|
+
justify-content: center;
|
|
2329
|
+
background: var(--fb-tile-hover-overlay-color, rgba(0,0,0,0.25));
|
|
2330
|
+
}
|
|
2331
|
+
.fb-play-btn {
|
|
2332
|
+
background: var(--fb-file-bg-color, rgba(255,255,255,0.9));
|
|
2333
|
+
border-radius: 50%;
|
|
2334
|
+
display: flex;
|
|
2335
|
+
align-items: center;
|
|
2336
|
+
justify-content: center;
|
|
2337
|
+
}
|
|
2338
|
+
|
|
2339
|
+
/* Edit-mode local video preview wrapper */
|
|
2340
|
+
.fb-video-preview-wrap {
|
|
2341
|
+
position: relative;
|
|
2342
|
+
width: 100%;
|
|
2343
|
+
height: 100%;
|
|
2344
|
+
}
|
|
2345
|
+
|
|
2346
|
+
/* Hover overlay for edit-mode local video (Remove / Change buttons) */
|
|
2347
|
+
.fb-video-btn-overlay {
|
|
2348
|
+
position: absolute;
|
|
2349
|
+
top: 8px;
|
|
2350
|
+
right: 8px;
|
|
2351
|
+
z-index: 10;
|
|
2352
|
+
display: flex;
|
|
2353
|
+
gap: 4px;
|
|
2354
|
+
opacity: 0;
|
|
2355
|
+
transition: opacity var(--fb-transition-duration, 200ms);
|
|
2356
|
+
pointer-events: none;
|
|
2357
|
+
}
|
|
2358
|
+
.fb-video-preview-wrap:hover .fb-video-btn-overlay {
|
|
2359
|
+
opacity: 1;
|
|
2360
|
+
pointer-events: auto;
|
|
2361
|
+
}
|
|
2362
|
+
.fb-video-btn {
|
|
2363
|
+
border: none;
|
|
2364
|
+
border-radius: var(--fb-border-radius, 4px);
|
|
2365
|
+
font-size: 11px;
|
|
2366
|
+
padding: 4px 8px;
|
|
2367
|
+
cursor: pointer;
|
|
2368
|
+
color: #fff;
|
|
2369
|
+
line-height: 1.2;
|
|
2370
|
+
}
|
|
2371
|
+
.fb-video-btn-delete {
|
|
2372
|
+
background: rgba(220, 38, 38, 0.85);
|
|
2373
|
+
}
|
|
2374
|
+
.fb-video-btn-delete:hover {
|
|
2375
|
+
background: rgba(185, 28, 28, 0.95);
|
|
2376
|
+
}
|
|
2377
|
+
.fb-video-btn-change {
|
|
2378
|
+
background: rgba(31, 41, 55, 0.85);
|
|
2379
|
+
}
|
|
2380
|
+
.fb-video-btn-change:hover {
|
|
2381
|
+
background: rgba(17, 24, 39, 0.95);
|
|
2382
|
+
}
|
|
2383
|
+
|
|
2384
|
+
/* Tile action icon buttons (download / open / remove) \u2014 shown on tile hover */
|
|
2385
|
+
.fb-tile-actions {
|
|
2386
|
+
position: absolute;
|
|
2387
|
+
top: 3px;
|
|
2388
|
+
right: 3px;
|
|
2389
|
+
display: flex;
|
|
2390
|
+
flex-direction: row;
|
|
2391
|
+
gap: 3px;
|
|
2392
|
+
opacity: 0;
|
|
2393
|
+
transition: opacity var(--fb-transition-duration, 200ms);
|
|
2394
|
+
z-index: 10;
|
|
2395
|
+
}
|
|
2396
|
+
.fb-tile-resource:hover .fb-tile-actions {
|
|
2397
|
+
opacity: 1;
|
|
2398
|
+
}
|
|
2399
|
+
.fb-tile-action-btn {
|
|
2400
|
+
width: 28px;
|
|
2401
|
+
height: 28px;
|
|
2402
|
+
display: flex;
|
|
2403
|
+
align-items: center;
|
|
2404
|
+
justify-content: center;
|
|
2405
|
+
border: none;
|
|
2406
|
+
border-radius: 50%;
|
|
2407
|
+
cursor: pointer;
|
|
2408
|
+
background: rgba(31, 41, 55, 0.75);
|
|
2409
|
+
color: #fff;
|
|
2410
|
+
padding: 0;
|
|
2411
|
+
flex-shrink: 0;
|
|
2412
|
+
transition:
|
|
2413
|
+
background var(--fb-transition-duration, 200ms),
|
|
2414
|
+
opacity var(--fb-transition-duration, 200ms);
|
|
2415
|
+
}
|
|
2416
|
+
.fb-tile-action-btn:hover {
|
|
2417
|
+
background: rgba(17, 24, 39, 0.95);
|
|
2418
|
+
}
|
|
2419
|
+
.fb-tile-action-remove {
|
|
2420
|
+
background: rgba(220, 38, 38, 0.8);
|
|
2421
|
+
}
|
|
2422
|
+
.fb-tile-action-remove:hover {
|
|
2423
|
+
background: rgba(185, 28, 28, 0.95);
|
|
2424
|
+
}
|
|
2425
|
+
|
|
2426
|
+
/* Actions row inside zoom popup \u2014 always visible while popup is shown */
|
|
2427
|
+
.fb-tile-zoom-preview .fb-tile-actions {
|
|
2428
|
+
position: absolute;
|
|
2429
|
+
top: 6px;
|
|
2430
|
+
right: 6px;
|
|
2431
|
+
opacity: 1;
|
|
2432
|
+
z-index: 10000;
|
|
2433
|
+
}
|
|
2434
|
+
|
|
2435
|
+
/* Two-card empty-state layout (upload card + library card) */
|
|
2436
|
+
.fb-file-card-row {
|
|
2437
|
+
display: flex;
|
|
2438
|
+
gap: 8px;
|
|
2439
|
+
align-items: stretch;
|
|
2440
|
+
}
|
|
2441
|
+
.fb-file-card-row .fb-file-dropzone,
|
|
2442
|
+
.fb-file-card-row .fb-file-library-card {
|
|
2443
|
+
flex: 1;
|
|
2444
|
+
min-width: 0;
|
|
2445
|
+
}
|
|
2446
|
+
|
|
2447
|
+
/* Library picker card \u2014 mirrors .fb-file-dropzone styling */
|
|
2448
|
+
.fb-file-library-card {
|
|
2449
|
+
height: 128px;
|
|
2450
|
+
border: 2px dashed var(--fb-file-upload-border-color, #d1d5db);
|
|
2451
|
+
border-radius: var(--fb-border-radius, 0.5rem);
|
|
2452
|
+
display: flex;
|
|
2453
|
+
flex-direction: column;
|
|
2454
|
+
align-items: center;
|
|
2455
|
+
justify-content: center;
|
|
2456
|
+
gap: 4px;
|
|
2457
|
+
cursor: pointer;
|
|
2458
|
+
background: none;
|
|
2459
|
+
padding: 0;
|
|
2460
|
+
transition:
|
|
2461
|
+
border-color var(--fb-transition-duration, 200ms),
|
|
2462
|
+
background var(--fb-transition-duration, 200ms);
|
|
2463
|
+
width: 100%;
|
|
2464
|
+
}
|
|
2465
|
+
.fb-file-library-card:hover,
|
|
2466
|
+
.fb-file-library-card:focus-visible {
|
|
2467
|
+
border-color: var(--fb-file-upload-hover-border-color, #3b82f6);
|
|
2468
|
+
background: var(--fb-background-hover-color, #f9fafb);
|
|
2469
|
+
outline: none;
|
|
2470
|
+
}
|
|
2471
|
+
.fb-file-library-card-icon {
|
|
2472
|
+
font-size: 24px;
|
|
2473
|
+
line-height: 1;
|
|
2474
|
+
flex-shrink: 0;
|
|
2475
|
+
}
|
|
2476
|
+
.fb-file-library-card-label {
|
|
2477
|
+
font-size: 13px;
|
|
2478
|
+
color: var(--fb-text-secondary-color, #6b7280);
|
|
2479
|
+
}
|
|
2480
|
+
.fb-file-library-card-hint {
|
|
2481
|
+
font-size: 11px;
|
|
2482
|
+
color: var(--fb-file-upload-text-color, #9ca3af);
|
|
2483
|
+
}
|
|
2484
|
+
|
|
2485
|
+
/* Library "\u{1F4DA}" add-tile \u2014 same size/style as the "+" add tile */
|
|
2486
|
+
.fb-tile-add-library {
|
|
2487
|
+
border: 2px dashed var(--fb-file-upload-border-color, #d1d5db);
|
|
2488
|
+
display: flex;
|
|
2489
|
+
align-items: center;
|
|
2490
|
+
justify-content: center;
|
|
2491
|
+
cursor: pointer;
|
|
2492
|
+
font-size: 24px;
|
|
2493
|
+
color: var(--fb-file-upload-text-color, #9ca3af);
|
|
2494
|
+
transition:
|
|
2495
|
+
border-color var(--fb-transition-duration, 200ms),
|
|
2496
|
+
color var(--fb-transition-duration, 200ms);
|
|
2497
|
+
background: none;
|
|
2498
|
+
padding: 0;
|
|
2499
|
+
width: var(--fb-tile-size, 160px);
|
|
2500
|
+
height: var(--fb-tile-size, 160px);
|
|
2501
|
+
flex-shrink: 0;
|
|
2502
|
+
position: relative;
|
|
2503
|
+
overflow: hidden;
|
|
2504
|
+
border-radius: var(--fb-border-radius, 0.5rem);
|
|
2505
|
+
}
|
|
2506
|
+
.fb-tile-add-library:hover,
|
|
2507
|
+
.fb-tile-add-library:focus-visible {
|
|
2508
|
+
border-color: var(--fb-file-upload-hover-border-color, #3b82f6);
|
|
2509
|
+
color: var(--fb-text-color, #1f2937);
|
|
2510
|
+
outline: none;
|
|
2511
|
+
}
|
|
2512
|
+
|
|
2513
|
+
/* Hover zoom preview popup for image tiles \u2014 appended to document.body (fixed) */
|
|
2514
|
+
.fb-tile-zoom-preview {
|
|
2515
|
+
position: fixed;
|
|
2516
|
+
z-index: 9999;
|
|
2517
|
+
background: var(--fb-background-color, #fff);
|
|
2518
|
+
border: 1px solid var(--fb-file-upload-border-color, #d1d5db);
|
|
2519
|
+
border-radius: var(--fb-border-radius, 0.5rem);
|
|
2520
|
+
box-shadow: 0 4px 16px rgba(0,0,0,0.18);
|
|
2521
|
+
padding: 4px;
|
|
2522
|
+
width: 350px;
|
|
2523
|
+
height: 350px;
|
|
2524
|
+
pointer-events: none;
|
|
2525
|
+
opacity: 0;
|
|
2526
|
+
transition: opacity 150ms ease;
|
|
2527
|
+
}
|
|
2528
|
+
.fb-tile-zoom-preview.fb-tile-zoom-preview--visible {
|
|
2529
|
+
opacity: 1;
|
|
2530
|
+
}
|
|
2531
|
+
.fb-tile-zoom-preview-img {
|
|
2532
|
+
width: 100%;
|
|
2533
|
+
height: 100%;
|
|
2534
|
+
object-fit: contain;
|
|
2535
|
+
display: block;
|
|
2536
|
+
background: var(--fb-file-upload-bg-color, #f3f4f6);
|
|
2537
|
+
border-radius: calc(var(--fb-border-radius, 0.5rem) - 2px);
|
|
2538
|
+
}
|
|
2539
|
+
`;
|
|
2540
|
+
document.head.appendChild(style);
|
|
2541
|
+
}
|
|
2542
|
+
|
|
2543
|
+
// src/components/file/dom.ts
|
|
2544
|
+
var TILE_SIZE = "160px";
|
|
2545
|
+
function createFileTile() {
|
|
2546
|
+
ensureFileStyles();
|
|
2547
|
+
const tile = document.createElement("div");
|
|
2548
|
+
tile.className = "fb-tile";
|
|
2549
|
+
return tile;
|
|
2550
|
+
}
|
|
2551
|
+
function showFileError(container, message) {
|
|
2552
|
+
const existing = container.closest(".space-y-2")?.querySelector(".file-error-message");
|
|
2553
|
+
if (existing) existing.remove();
|
|
2554
|
+
const errorEl = document.createElement("div");
|
|
2555
|
+
errorEl.className = "file-error-message error-message";
|
|
2556
|
+
errorEl.style.cssText = `
|
|
2557
|
+
color: var(--fb-error-color);
|
|
2558
|
+
font-size: var(--fb-font-size-small);
|
|
2559
|
+
margin-top: 0.25rem;
|
|
2560
|
+
`;
|
|
2561
|
+
errorEl.textContent = message;
|
|
2562
|
+
container.closest(".space-y-2")?.appendChild(errorEl);
|
|
2563
|
+
}
|
|
2564
|
+
function clearFileError(container) {
|
|
2565
|
+
const existing = container.closest(".space-y-2")?.querySelector(".file-error-message");
|
|
2566
|
+
if (existing) existing.remove();
|
|
2567
|
+
}
|
|
2568
|
+
function addDeleteButton(container, state, onDelete) {
|
|
2569
|
+
const existingOverlay = container.querySelector(".delete-overlay");
|
|
2570
|
+
if (existingOverlay) existingOverlay.remove();
|
|
2571
|
+
const overlay = document.createElement("div");
|
|
2572
|
+
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";
|
|
2573
|
+
const deleteBtn = document.createElement("button");
|
|
2574
|
+
deleteBtn.className = "bg-red-600 text-white px-3 py-1 rounded text-sm hover:bg-red-700 transition-colors";
|
|
2575
|
+
deleteBtn.textContent = t("removeElement", state);
|
|
2576
|
+
deleteBtn.onclick = (e) => {
|
|
2577
|
+
e.stopPropagation();
|
|
2578
|
+
onDelete();
|
|
2579
|
+
};
|
|
2580
|
+
overlay.appendChild(deleteBtn);
|
|
2581
|
+
container.appendChild(overlay);
|
|
2582
|
+
}
|
|
2583
|
+
function findFilePicker(container) {
|
|
2584
|
+
let el = container.parentElement;
|
|
2585
|
+
while (el && !el.dataset.filesWrapper) {
|
|
2586
|
+
el = el.parentElement;
|
|
2587
|
+
}
|
|
2588
|
+
return el?.querySelector('input[type="file"]') ?? null;
|
|
2589
|
+
}
|
|
2590
|
+
function createUploadingTile(fileName, state) {
|
|
2591
|
+
ensureFileStyles();
|
|
2592
|
+
const tile = createFileTile();
|
|
2593
|
+
tile.classList.add("fb-tile-uploading");
|
|
2594
|
+
tile.className += " fb-uploading-tile";
|
|
2595
|
+
const label = fileName.length > 10 ? fileName.substring(0, 8) + "\u2026" : fileName;
|
|
2596
|
+
tile.innerHTML = `
|
|
2597
|
+
<div style="display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;gap:6px;padding:4px;">
|
|
2598
|
+
<div class="fb-spinner"></div>
|
|
2599
|
+
<div class="fb-tile-label">${escapeHtml(label)}</div>
|
|
2600
|
+
<div class="fb-tile-uploading-text">${escapeHtml(t("uploadingFile", state))}</div>
|
|
2601
|
+
</div>`;
|
|
2602
|
+
return tile;
|
|
2603
|
+
}
|
|
2604
|
+
function ensureTilesWrap(list) {
|
|
2605
|
+
const existing = list.querySelector(".fb-tiles-wrap");
|
|
2606
|
+
if (existing) return existing;
|
|
2607
|
+
const dropzone = list.querySelector(".fb-file-dropzone");
|
|
2608
|
+
if (dropzone) dropzone.remove();
|
|
2609
|
+
const tilesWrap = document.createElement("div");
|
|
2610
|
+
tilesWrap.className = "fb-tiles-wrap";
|
|
2611
|
+
tilesWrap.style.cssText = "display:flex;flex-wrap:wrap;gap:6px;align-items:flex-start;";
|
|
2612
|
+
const addTile = document.createElement("div");
|
|
2613
|
+
addTile.className = "fb-tile fb-tile-add";
|
|
2614
|
+
addTile.innerHTML = "+";
|
|
2615
|
+
tilesWrap.appendChild(addTile);
|
|
2616
|
+
list.appendChild(tilesWrap);
|
|
2617
|
+
return tilesWrap;
|
|
2618
|
+
}
|
|
2619
|
+
function setEmptyFileContainer(fileContainer, state, hint) {
|
|
2620
|
+
const hintHtml = hint ? `<div class="text-xs text-gray-500 mt-1">${escapeHtml(hint)}</div>` : "";
|
|
2621
|
+
fileContainer.innerHTML = `
|
|
2622
|
+
<div class="flex flex-col items-center justify-center h-full text-gray-400">
|
|
2623
|
+
<svg class="w-6 h-6 mb-2" fill="currentColor" viewBox="0 0 24 24">
|
|
2624
|
+
<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"/>
|
|
2625
|
+
</svg>
|
|
2626
|
+
<div class="text-sm text-center">${escapeHtml(t("clickDragText", state))}</div>
|
|
2627
|
+
${hintHtml}
|
|
2628
|
+
</div>
|
|
2629
|
+
`;
|
|
2630
|
+
}
|
|
2631
|
+
function setupDragAndDrop(element, dropHandler) {
|
|
2632
|
+
element.addEventListener("dragover", (e) => {
|
|
2633
|
+
e.preventDefault();
|
|
2634
|
+
element.classList.add("border-blue-500", "bg-blue-50");
|
|
2635
|
+
});
|
|
2636
|
+
element.addEventListener("dragleave", (e) => {
|
|
2637
|
+
e.preventDefault();
|
|
2638
|
+
element.classList.remove("border-blue-500", "bg-blue-50");
|
|
2639
|
+
});
|
|
2640
|
+
element.addEventListener("drop", (e) => {
|
|
2641
|
+
e.preventDefault();
|
|
2642
|
+
element.classList.remove("border-blue-500", "bg-blue-50");
|
|
2643
|
+
if (e.dataTransfer?.files) {
|
|
2644
|
+
dropHandler(e.dataTransfer.files);
|
|
2645
|
+
}
|
|
2646
|
+
});
|
|
2647
|
+
}
|
|
2648
|
+
|
|
2649
|
+
// src/components/file/preview.ts
|
|
2650
|
+
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>`;
|
|
2651
|
+
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>`;
|
|
2652
|
+
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>`;
|
|
2653
|
+
function canDownload(state, meta) {
|
|
2654
|
+
return Boolean(
|
|
2655
|
+
state.config.downloadFile || state.config.getDownloadUrl || state.config.getThumbnail || meta?.file
|
|
2656
|
+
);
|
|
2657
|
+
}
|
|
2658
|
+
function canOpenInTab(state, meta) {
|
|
2659
|
+
return Boolean(
|
|
2660
|
+
state.config.getDownloadUrl || state.config.getThumbnail || meta?.file
|
|
2661
|
+
);
|
|
2662
|
+
}
|
|
2663
|
+
function createTileActions(options) {
|
|
2664
|
+
const { canRemove, removeHandler, state, resourceId, fileName, meta } = options;
|
|
2665
|
+
const group = document.createElement("div");
|
|
2666
|
+
group.className = "fb-tile-actions";
|
|
2667
|
+
const makeBtn = (icon, label, cls) => {
|
|
2668
|
+
const btn = document.createElement("button");
|
|
2669
|
+
btn.type = "button";
|
|
2670
|
+
btn.className = `fb-tile-action-btn ${cls}`;
|
|
2671
|
+
btn.innerHTML = icon;
|
|
2672
|
+
btn.title = label;
|
|
2673
|
+
btn.setAttribute("aria-label", label);
|
|
2674
|
+
btn.addEventListener("click", (e) => {
|
|
2675
|
+
e.stopPropagation();
|
|
2676
|
+
});
|
|
2677
|
+
return btn;
|
|
2678
|
+
};
|
|
2679
|
+
if (canDownload(state, meta)) {
|
|
2680
|
+
const dlBtn = makeBtn(ICON_DOWNLOAD, t("downloadFile", state), "fb-tile-action-download");
|
|
2681
|
+
dlBtn.addEventListener("click", () => {
|
|
2682
|
+
triggerTileDownload(resourceId, fileName, state, meta);
|
|
2683
|
+
});
|
|
2684
|
+
group.appendChild(dlBtn);
|
|
2685
|
+
}
|
|
2686
|
+
if (canOpenInTab(state, meta)) {
|
|
2687
|
+
const openBtn = makeBtn(ICON_OPEN, t("openInNewTab", state), "fb-tile-action-open");
|
|
2688
|
+
openBtn.addEventListener("click", () => {
|
|
2689
|
+
triggerTileOpen(resourceId, state, meta).catch((err) => {
|
|
2690
|
+
console.error("Open failed:", err);
|
|
2691
|
+
});
|
|
2692
|
+
});
|
|
2693
|
+
group.appendChild(openBtn);
|
|
2694
|
+
}
|
|
2695
|
+
if (canRemove && removeHandler) {
|
|
2696
|
+
const rmBtn = makeBtn(ICON_REMOVE, t("removeElement", state), "fb-tile-action-remove");
|
|
2697
|
+
rmBtn.addEventListener("click", () => {
|
|
2698
|
+
removeHandler();
|
|
2699
|
+
});
|
|
2700
|
+
group.appendChild(rmBtn);
|
|
2701
|
+
}
|
|
2702
|
+
return group;
|
|
2703
|
+
}
|
|
2704
|
+
var localFileUrlCache = /* @__PURE__ */ new WeakMap();
|
|
2705
|
+
function getLocalFileUrl(file) {
|
|
2706
|
+
let url = localFileUrlCache.get(file);
|
|
2707
|
+
if (!url) {
|
|
2708
|
+
url = URL.createObjectURL(file);
|
|
2709
|
+
localFileUrlCache.set(file, url);
|
|
2710
|
+
}
|
|
2711
|
+
return url;
|
|
2712
|
+
}
|
|
2713
|
+
function releaseLocalFileUrl(file) {
|
|
2714
|
+
if (!file) return;
|
|
2715
|
+
const url = localFileUrlCache.get(file);
|
|
2716
|
+
if (url) {
|
|
2717
|
+
URL.revokeObjectURL(url);
|
|
2718
|
+
localFileUrlCache.delete(file);
|
|
2719
|
+
}
|
|
2720
|
+
}
|
|
2721
|
+
function triggerTileDownload(resourceId, fileName, state, meta) {
|
|
2722
|
+
if (state.config.downloadFile) {
|
|
2723
|
+
state.config.downloadFile(resourceId, fileName);
|
|
2724
|
+
return;
|
|
2725
|
+
}
|
|
2726
|
+
if (meta?.file instanceof File) {
|
|
2727
|
+
downloadBlob(meta.file, fileName || meta.file.name);
|
|
2728
|
+
return;
|
|
2729
|
+
}
|
|
2730
|
+
forceDownload(resourceId, fileName, state).catch((err) => {
|
|
2731
|
+
console.error("Download failed:", err);
|
|
2732
|
+
});
|
|
2733
|
+
}
|
|
2734
|
+
async function triggerTileOpen(resourceId, state, meta) {
|
|
2735
|
+
let url = null;
|
|
2736
|
+
if (state.config.getDownloadUrl) {
|
|
2737
|
+
url = state.config.getDownloadUrl(resourceId);
|
|
2738
|
+
} else if (state.config.getThumbnail) {
|
|
2739
|
+
url = await state.config.getThumbnail(resourceId);
|
|
2740
|
+
} else if (meta?.file instanceof File) {
|
|
2741
|
+
url = getLocalFileUrl(meta.file);
|
|
2742
|
+
}
|
|
2743
|
+
if (url) {
|
|
2744
|
+
window.open(url, "_blank");
|
|
2745
|
+
}
|
|
2746
|
+
}
|
|
2747
|
+
var sharedZoomPopup = null;
|
|
2748
|
+
var zoomTimer = null;
|
|
2749
|
+
var zoomHideTimer = null;
|
|
2750
|
+
var zoomOwner = null;
|
|
2751
|
+
function getOrCreateZoomPopup() {
|
|
2752
|
+
if (!sharedZoomPopup) {
|
|
2753
|
+
sharedZoomPopup = document.createElement("div");
|
|
2754
|
+
sharedZoomPopup.className = "fb-tile-zoom-preview";
|
|
2755
|
+
const img = document.createElement("img");
|
|
2756
|
+
img.className = "fb-tile-zoom-preview-img";
|
|
2757
|
+
sharedZoomPopup.appendChild(img);
|
|
2758
|
+
sharedZoomPopup.addEventListener("mouseenter", cancelHideZoomPopup);
|
|
2759
|
+
sharedZoomPopup.addEventListener("mouseleave", scheduleHideZoomPopup);
|
|
2760
|
+
}
|
|
2761
|
+
return sharedZoomPopup;
|
|
2762
|
+
}
|
|
2763
|
+
function positionZoomPopup(popup, tile) {
|
|
2764
|
+
const tileRect = tile.getBoundingClientRect();
|
|
2765
|
+
const popupSize = 350;
|
|
2766
|
+
const margin = 6;
|
|
2767
|
+
const padding = 8;
|
|
2768
|
+
let top;
|
|
2769
|
+
if (tileRect.top - popupSize - margin >= padding) {
|
|
2770
|
+
top = tileRect.top - popupSize - margin;
|
|
2771
|
+
} else if (tileRect.bottom + margin + popupSize + padding <= window.innerHeight) {
|
|
2772
|
+
top = tileRect.bottom + margin;
|
|
2773
|
+
} else {
|
|
2774
|
+
top = Math.max(padding, Math.min(window.innerHeight - popupSize - padding, tileRect.top));
|
|
2775
|
+
}
|
|
2776
|
+
const tileCenterX = tileRect.left + tileRect.width / 2;
|
|
2777
|
+
let left = tileCenterX - popupSize / 2;
|
|
2778
|
+
left = Math.max(padding, Math.min(window.innerWidth - popupSize - padding, left));
|
|
2779
|
+
popup.style.top = `${top}px`;
|
|
2780
|
+
popup.style.left = `${left}px`;
|
|
2781
|
+
}
|
|
2782
|
+
function scheduleHideZoomPopup() {
|
|
2783
|
+
if (zoomHideTimer !== null) {
|
|
2784
|
+
clearTimeout(zoomHideTimer);
|
|
2785
|
+
}
|
|
2786
|
+
zoomHideTimer = setTimeout(() => {
|
|
2787
|
+
zoomHideTimer = null;
|
|
2788
|
+
removeZoomPopupNow();
|
|
2789
|
+
}, 100);
|
|
2790
|
+
}
|
|
2791
|
+
function cancelHideZoomPopup() {
|
|
2792
|
+
if (zoomHideTimer !== null) {
|
|
2793
|
+
clearTimeout(zoomHideTimer);
|
|
2794
|
+
zoomHideTimer = null;
|
|
2795
|
+
}
|
|
2796
|
+
}
|
|
2797
|
+
function removeZoomPopupNow() {
|
|
2798
|
+
if (zoomTimer !== null) {
|
|
2799
|
+
clearTimeout(zoomTimer);
|
|
2800
|
+
zoomTimer = null;
|
|
2801
|
+
}
|
|
2802
|
+
if (sharedZoomPopup && sharedZoomPopup.parentNode) {
|
|
2803
|
+
sharedZoomPopup.classList.remove("fb-tile-zoom-preview--visible");
|
|
2804
|
+
sharedZoomPopup.parentNode.removeChild(sharedZoomPopup);
|
|
2805
|
+
}
|
|
2806
|
+
zoomOwner = null;
|
|
2807
|
+
}
|
|
2808
|
+
function attachZoomHover(tile, src, alt, actionsEl) {
|
|
2809
|
+
tile.dataset.zoomSrc = src;
|
|
2810
|
+
tile.dataset.zoomAlt = alt;
|
|
2811
|
+
tile.addEventListener("mouseenter", () => {
|
|
2812
|
+
cancelHideZoomPopup();
|
|
2813
|
+
if (zoomOwner !== tile) {
|
|
2814
|
+
removeZoomPopupNow();
|
|
2815
|
+
}
|
|
2816
|
+
zoomOwner = tile;
|
|
2817
|
+
zoomTimer = setTimeout(() => {
|
|
2818
|
+
zoomTimer = null;
|
|
2819
|
+
const popup = getOrCreateZoomPopup();
|
|
2820
|
+
const existingActions = popup.querySelector(".fb-tile-actions");
|
|
2821
|
+
if (existingActions) existingActions.remove();
|
|
2822
|
+
const img = popup.querySelector(".fb-tile-zoom-preview-img");
|
|
2823
|
+
img.src = src;
|
|
2824
|
+
img.alt = alt;
|
|
2825
|
+
if (actionsEl) {
|
|
2826
|
+
popup.appendChild(actionsEl.cloneNode(true));
|
|
2827
|
+
attachClonedActionListeners(
|
|
2828
|
+
popup.querySelector(".fb-tile-actions"),
|
|
2829
|
+
actionsEl
|
|
2830
|
+
);
|
|
2831
|
+
}
|
|
2832
|
+
popup.style.pointerEvents = "auto";
|
|
2833
|
+
positionZoomPopup(popup, tile);
|
|
2834
|
+
document.body.appendChild(popup);
|
|
2835
|
+
popup.getBoundingClientRect();
|
|
2836
|
+
popup.classList.add("fb-tile-zoom-preview--visible");
|
|
2837
|
+
}, 200);
|
|
2838
|
+
});
|
|
2839
|
+
tile.addEventListener("mouseleave", () => {
|
|
2840
|
+
if (zoomTimer !== null) {
|
|
2841
|
+
clearTimeout(zoomTimer);
|
|
2842
|
+
zoomTimer = null;
|
|
2843
|
+
zoomOwner = null;
|
|
2844
|
+
} else {
|
|
2845
|
+
scheduleHideZoomPopup();
|
|
2846
|
+
}
|
|
2847
|
+
});
|
|
2848
|
+
}
|
|
2849
|
+
function attachClonedActionListeners(cloned, original) {
|
|
2850
|
+
const originalBtns = Array.from(original.querySelectorAll(".fb-tile-action-btn"));
|
|
2851
|
+
const clonedBtns = Array.from(cloned.querySelectorAll(".fb-tile-action-btn"));
|
|
2852
|
+
clonedBtns.forEach((clonedBtn, i) => {
|
|
2853
|
+
const origBtn = originalBtns[i];
|
|
2854
|
+
if (origBtn) {
|
|
2855
|
+
clonedBtn.addEventListener("click", (e) => {
|
|
2856
|
+
e.stopPropagation();
|
|
2857
|
+
origBtn.click();
|
|
2858
|
+
});
|
|
2859
|
+
}
|
|
2860
|
+
});
|
|
2110
2861
|
}
|
|
2111
2862
|
function renderLocalImagePreview(container, file, fileName, state) {
|
|
2112
2863
|
const img = document.createElement("img");
|
|
2113
2864
|
img.className = "w-full h-full object-contain";
|
|
2865
|
+
img.style.background = "var(--fb-file-upload-bg-color,#f3f4f6)";
|
|
2114
2866
|
img.alt = fileName || t("previewAlt", state);
|
|
2115
2867
|
const reader = new FileReader();
|
|
2116
2868
|
reader.onload = (e) => {
|
|
@@ -2119,23 +2871,27 @@ function renderLocalImagePreview(container, file, fileName, state) {
|
|
|
2119
2871
|
reader.readAsDataURL(file);
|
|
2120
2872
|
container.appendChild(img);
|
|
2121
2873
|
}
|
|
2122
|
-
function
|
|
2123
|
-
const videoUrl = URL.createObjectURL(file);
|
|
2874
|
+
function setupDragDropless(container, _deps) {
|
|
2124
2875
|
container.onclick = null;
|
|
2125
2876
|
const newContainer = container.cloneNode(false);
|
|
2126
2877
|
if (container.parentNode) {
|
|
2127
2878
|
container.parentNode.replaceChild(newContainer, container);
|
|
2128
2879
|
}
|
|
2880
|
+
return newContainer;
|
|
2881
|
+
}
|
|
2882
|
+
function renderLocalVideoPreview(container, file, videoType, resourceId, state, deps) {
|
|
2883
|
+
const videoUrl = URL.createObjectURL(file);
|
|
2884
|
+
const newContainer = setupDragDropless(container);
|
|
2129
2885
|
newContainer.innerHTML = `
|
|
2130
|
-
<div class="
|
|
2886
|
+
<div class="fb-video-preview-wrap">
|
|
2131
2887
|
<video class="w-full h-full object-contain" controls preload="auto" muted src="${videoUrl}">
|
|
2132
2888
|
${escapeHtml(t("videoNotSupported", state))}
|
|
2133
2889
|
</video>
|
|
2134
|
-
<div class="
|
|
2135
|
-
<button class="
|
|
2890
|
+
<div class="fb-video-btn-overlay">
|
|
2891
|
+
<button class="fb-video-btn fb-video-btn-delete delete-file-btn">
|
|
2136
2892
|
${escapeHtml(t("removeElement", state))}
|
|
2137
2893
|
</button>
|
|
2138
|
-
<button class="
|
|
2894
|
+
<button class="fb-video-btn fb-video-btn-change change-file-btn">
|
|
2139
2895
|
${escapeHtml(t("changeButton", state))}
|
|
2140
2896
|
</button>
|
|
2141
2897
|
</div>
|
|
@@ -2145,20 +2901,14 @@ function renderLocalVideoPreview(container, file, videoType, resourceId, state,
|
|
|
2145
2901
|
return newContainer;
|
|
2146
2902
|
}
|
|
2147
2903
|
function attachVideoButtonHandlers(container, resourceId, state, deps) {
|
|
2148
|
-
const changeBtn = container.querySelector(
|
|
2149
|
-
".change-file-btn"
|
|
2150
|
-
);
|
|
2904
|
+
const changeBtn = container.querySelector(".change-file-btn");
|
|
2151
2905
|
if (changeBtn) {
|
|
2152
2906
|
changeBtn.onclick = (e) => {
|
|
2153
2907
|
e.stopPropagation();
|
|
2154
|
-
|
|
2155
|
-
deps.picker.click();
|
|
2156
|
-
}
|
|
2908
|
+
deps?.picker?.click();
|
|
2157
2909
|
};
|
|
2158
2910
|
}
|
|
2159
|
-
const deleteBtn = container.querySelector(
|
|
2160
|
-
".delete-file-btn"
|
|
2161
|
-
);
|
|
2911
|
+
const deleteBtn = container.querySelector(".delete-file-btn");
|
|
2162
2912
|
if (deleteBtn) {
|
|
2163
2913
|
deleteBtn.onclick = (e) => {
|
|
2164
2914
|
e.stopPropagation();
|
|
@@ -2177,9 +2927,6 @@ function handleVideoDelete(container, resourceId, state, deps) {
|
|
|
2177
2927
|
if (deps?.fileUploadHandler) {
|
|
2178
2928
|
container.onclick = deps.fileUploadHandler;
|
|
2179
2929
|
}
|
|
2180
|
-
if (deps?.dragHandler) {
|
|
2181
|
-
setupDragAndDrop(container, deps.dragHandler);
|
|
2182
|
-
}
|
|
2183
2930
|
container.innerHTML = `
|
|
2184
2931
|
<div class="flex flex-col items-center justify-center h-full text-gray-400">
|
|
2185
2932
|
<svg class="w-6 h-6 mb-2" fill="currentColor" viewBox="0 0 24 24">
|
|
@@ -2188,16 +2935,9 @@ function handleVideoDelete(container, resourceId, state, deps) {
|
|
|
2188
2935
|
<div class="text-sm text-center">${escapeHtml(t("clickDragText", state))}</div>
|
|
2189
2936
|
</div>
|
|
2190
2937
|
`;
|
|
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);
|
|
2938
|
+
if (deps?.setupDrop) {
|
|
2939
|
+
deps.setupDrop(container);
|
|
2940
|
+
}
|
|
2201
2941
|
}
|
|
2202
2942
|
function renderDeleteButton(container, resourceId, state) {
|
|
2203
2943
|
addDeleteButton(container, state, () => {
|
|
@@ -2219,13 +2959,11 @@ function renderDeleteButton(container, resourceId, state) {
|
|
|
2219
2959
|
});
|
|
2220
2960
|
}
|
|
2221
2961
|
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/")) {
|
|
2962
|
+
if (!meta.file || !(meta.file instanceof File)) return;
|
|
2963
|
+
if (meta.type?.startsWith("image/")) {
|
|
2226
2964
|
renderLocalImagePreview(container, meta.file, fileName, state);
|
|
2227
|
-
} else if (meta.type
|
|
2228
|
-
|
|
2965
|
+
} else if (meta.type?.startsWith("video/")) {
|
|
2966
|
+
container = renderLocalVideoPreview(
|
|
2229
2967
|
container,
|
|
2230
2968
|
meta.file,
|
|
2231
2969
|
meta.type,
|
|
@@ -2233,14 +2971,23 @@ async function renderLocalFilePreview(container, meta, fileName, resourceId, isR
|
|
|
2233
2971
|
state,
|
|
2234
2972
|
deps
|
|
2235
2973
|
);
|
|
2236
|
-
container = newContainer;
|
|
2237
2974
|
} else {
|
|
2238
|
-
container.innerHTML = `<div class="flex flex-col items-center justify-center h-full text-gray-400"><div
|
|
2975
|
+
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
2976
|
}
|
|
2240
|
-
if (!isReadonly && !
|
|
2977
|
+
if (!isReadonly && !meta.type?.startsWith("video/")) {
|
|
2241
2978
|
renderDeleteButton(container, resourceId, state);
|
|
2242
2979
|
}
|
|
2243
2980
|
}
|
|
2981
|
+
function renderUploadedVideoPreview(container, thumbnailUrl, state) {
|
|
2982
|
+
const video = document.createElement("video");
|
|
2983
|
+
video.className = "w-full h-full object-contain";
|
|
2984
|
+
video.controls = true;
|
|
2985
|
+
video.preload = "metadata";
|
|
2986
|
+
video.muted = true;
|
|
2987
|
+
video.src = thumbnailUrl;
|
|
2988
|
+
video.appendChild(document.createTextNode(t("videoNotSupported", state)));
|
|
2989
|
+
container.appendChild(video);
|
|
2990
|
+
}
|
|
2244
2991
|
async function renderUploadedFilePreview(container, resourceId, fileName, meta, state) {
|
|
2245
2992
|
if (!state.config.getThumbnail) {
|
|
2246
2993
|
setEmptyFileContainer(container, state);
|
|
@@ -2250,11 +2997,12 @@ async function renderUploadedFilePreview(container, resourceId, fileName, meta,
|
|
|
2250
2997
|
const thumbnailUrl = await state.config.getThumbnail(resourceId);
|
|
2251
2998
|
if (thumbnailUrl) {
|
|
2252
2999
|
clear(container);
|
|
2253
|
-
if (meta
|
|
2254
|
-
renderUploadedVideoPreview(container, thumbnailUrl,
|
|
3000
|
+
if (meta?.type?.startsWith("video/")) {
|
|
3001
|
+
renderUploadedVideoPreview(container, thumbnailUrl, state);
|
|
2255
3002
|
} else {
|
|
2256
3003
|
const img = document.createElement("img");
|
|
2257
3004
|
img.className = "w-full h-full object-contain";
|
|
3005
|
+
img.style.background = "var(--fb-file-upload-bg-color,#f3f4f6)";
|
|
2258
3006
|
img.alt = fileName || t("previewAlt", state);
|
|
2259
3007
|
img.src = thumbnailUrl;
|
|
2260
3008
|
container.appendChild(img);
|
|
@@ -2282,9 +3030,6 @@ async function renderFilePreview(container, resourceId, state, options = {}) {
|
|
|
2282
3030
|
);
|
|
2283
3031
|
}
|
|
2284
3032
|
clear(container);
|
|
2285
|
-
if (isReadonly) {
|
|
2286
|
-
container.classList.add("cursor-pointer");
|
|
2287
|
-
}
|
|
2288
3033
|
const meta = state.resourceIndex.get(resourceId);
|
|
2289
3034
|
if (meta && meta.file && meta.file instanceof File) {
|
|
2290
3035
|
await renderLocalFilePreview(
|
|
@@ -2297,366 +3042,304 @@ async function renderFilePreview(container, resourceId, state, options = {}) {
|
|
|
2297
3042
|
deps
|
|
2298
3043
|
);
|
|
2299
3044
|
} else {
|
|
2300
|
-
await renderUploadedFilePreview(
|
|
2301
|
-
container,
|
|
2302
|
-
resourceId,
|
|
2303
|
-
fileName,
|
|
2304
|
-
meta,
|
|
2305
|
-
state
|
|
2306
|
-
);
|
|
3045
|
+
await renderUploadedFilePreview(container, resourceId, fileName, meta, state);
|
|
2307
3046
|
const isVideo = meta?.type?.startsWith("video/");
|
|
2308
3047
|
if (!isReadonly && !isVideo) {
|
|
2309
3048
|
renderDeleteButton(container, resourceId, state);
|
|
2310
3049
|
}
|
|
2311
3050
|
}
|
|
2312
3051
|
}
|
|
2313
|
-
|
|
3052
|
+
function resolveFileName(resourceId, meta, fileName) {
|
|
3053
|
+
if (fileName) return fileName;
|
|
3054
|
+
if (meta?.name?.includes(".")) return meta.name;
|
|
3055
|
+
const basename = resourceId.includes("/") ? resourceId.split("/").pop() : resourceId;
|
|
3056
|
+
return basename?.includes(".") ? basename : "";
|
|
3057
|
+
}
|
|
3058
|
+
async function renderFilePreviewReadonly(resourceId, state, fileName, options = {}) {
|
|
2314
3059
|
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
|
-
|
|
3060
|
+
const actualFileName = resolveFileName(resourceId, meta, fileName);
|
|
3061
|
+
const { canRemove = false, removeHandler = null } = options;
|
|
3062
|
+
const isImage = meta?.type?.startsWith("image/") || Boolean(actualFileName.toLowerCase().match(/\.(jpg|jpeg|png|gif|webp)$/));
|
|
3063
|
+
const isVideo = meta?.type?.startsWith("video/") || Boolean(actualFileName.toLowerCase().match(/\.(mp4|webm|avi|mov)$/));
|
|
3064
|
+
const tile = createFileTile();
|
|
3065
|
+
tile.classList.add("fb-tile-resource");
|
|
3066
|
+
tile.style.cursor = "pointer";
|
|
3067
|
+
if (actualFileName) {
|
|
3068
|
+
tile.title = actualFileName;
|
|
3069
|
+
}
|
|
3070
|
+
const localFileUrl = meta?.file instanceof File ? getLocalFileUrl(meta.file) : null;
|
|
3071
|
+
const resolveOpenUrl = async () => {
|
|
3072
|
+
if (state.config.getDownloadUrl) return state.config.getDownloadUrl(resourceId);
|
|
3073
|
+
if (state.config.getThumbnail) return state.config.getThumbnail(resourceId);
|
|
3074
|
+
return localFileUrl;
|
|
3075
|
+
};
|
|
3076
|
+
tile.onclick = async () => {
|
|
3077
|
+
const url = await resolveOpenUrl();
|
|
3078
|
+
if (url) {
|
|
3079
|
+
window.open(url, "_blank");
|
|
3080
|
+
} else if (state.config.downloadFile) {
|
|
3081
|
+
state.config.downloadFile(resourceId, actualFileName);
|
|
3082
|
+
} else {
|
|
3083
|
+
forceDownload(resourceId, actualFileName, state).catch((err) => {
|
|
3084
|
+
console.error("Download failed:", err);
|
|
3085
|
+
});
|
|
3086
|
+
}
|
|
3087
|
+
};
|
|
3088
|
+
const actionsEl = createTileActions({
|
|
3089
|
+
canRemove,
|
|
3090
|
+
removeHandler,
|
|
3091
|
+
state,
|
|
3092
|
+
resourceId,
|
|
3093
|
+
fileName: actualFileName,
|
|
3094
|
+
meta
|
|
3095
|
+
});
|
|
3096
|
+
const resolveImageDisplayUrl = async () => {
|
|
2328
3097
|
if (state.config.getThumbnail) {
|
|
2329
3098
|
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>`;
|
|
3099
|
+
const url = await state.config.getThumbnail(resourceId);
|
|
3100
|
+
if (url) return url;
|
|
3101
|
+
} catch {
|
|
2339
3102
|
}
|
|
3103
|
+
}
|
|
3104
|
+
return localFileUrl;
|
|
3105
|
+
};
|
|
3106
|
+
const renderImageFallback = () => {
|
|
3107
|
+
tile.innerHTML = `<div style="display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;font-size:36px;">\u{1F5BC}\uFE0F</div>`;
|
|
3108
|
+
tile.appendChild(actionsEl);
|
|
3109
|
+
};
|
|
3110
|
+
if (isImage) {
|
|
3111
|
+
const displayUrl = await resolveImageDisplayUrl();
|
|
3112
|
+
if (displayUrl) {
|
|
3113
|
+
const img = document.createElement("img");
|
|
3114
|
+
img.style.cssText = "width:100%;height:100%;object-fit:contain;background:var(--fb-file-upload-bg-color,#f3f4f6);";
|
|
3115
|
+
img.alt = actualFileName;
|
|
3116
|
+
img.src = displayUrl;
|
|
3117
|
+
tile.appendChild(img);
|
|
3118
|
+
tile.appendChild(actionsEl);
|
|
3119
|
+
attachZoomHover(tile, displayUrl, actualFileName, actionsEl);
|
|
2340
3120
|
} else {
|
|
2341
|
-
|
|
3121
|
+
renderImageFallback();
|
|
2342
3122
|
}
|
|
2343
3123
|
} else if (isVideo) {
|
|
2344
3124
|
if (state.config.getThumbnail) {
|
|
2345
3125
|
try {
|
|
2346
3126
|
const videoUrl = await state.config.getThumbnail(resourceId);
|
|
2347
3127
|
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>
|
|
3128
|
+
tile.innerHTML = `
|
|
3129
|
+
<img style="width:100%;height:100%;object-fit:contain;background:var(--fb-file-upload-bg-color,#f3f4f6);" alt="${escapeHtml(actualFileName)}" src="${videoUrl}">
|
|
3130
|
+
<div class="fb-video-overlay">
|
|
3131
|
+
<div class="fb-play-btn" style="width:22px;height:22px;">
|
|
3132
|
+
<svg width="10" height="12" viewBox="0 0 10 12" fill="currentColor"><path d="M0 0l10 6-10 6z"/></svg>
|
|
2359
3133
|
</div>
|
|
2360
|
-
</div
|
|
2361
|
-
|
|
3134
|
+
</div>`;
|
|
3135
|
+
tile.appendChild(actionsEl);
|
|
2362
3136
|
} else {
|
|
2363
|
-
|
|
3137
|
+
tile.innerHTML = `<div style="display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;font-size:36px;">\u{1F3A5}</div>`;
|
|
3138
|
+
tile.appendChild(actionsEl);
|
|
2364
3139
|
}
|
|
2365
|
-
} catch
|
|
2366
|
-
|
|
2367
|
-
|
|
3140
|
+
} catch {
|
|
3141
|
+
tile.innerHTML = `<div style="display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;font-size:36px;">\u{1F3A5}</div>`;
|
|
3142
|
+
tile.appendChild(actionsEl);
|
|
2368
3143
|
}
|
|
2369
3144
|
} else {
|
|
2370
|
-
|
|
3145
|
+
tile.innerHTML = `<div style="display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;font-size:36px;">\u{1F3A5}</div>`;
|
|
3146
|
+
tile.appendChild(actionsEl);
|
|
2371
3147
|
}
|
|
2372
3148
|
} else {
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
`;
|
|
2385
|
-
} else {
|
|
2386
|
-
previewContainer.innerHTML = `<div class="aspect-video flex items-center justify-center text-gray-400"><div class="text-center"><div class="text-4xl mb-2">${fileIcon}</div><div class="text-sm">${escapeHtml(actualFileName)}</div><div class="text-xs text-gray-500 mt-1">${fileDescription}</div></div></div>`;
|
|
2387
|
-
}
|
|
2388
|
-
}
|
|
2389
|
-
const fileNameElement = document.createElement("p");
|
|
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;
|
|
2408
|
-
}
|
|
2409
|
-
function renderResourcePills(container, rids, state, onRemove, hint, countInfo) {
|
|
2410
|
-
clear(container);
|
|
2411
|
-
const buildHintLine = () => {
|
|
2412
|
-
const parts = [t("clickDragTextMultiple", state)];
|
|
2413
|
-
if (hint) parts.push(hint);
|
|
2414
|
-
if (countInfo) parts.push(countInfo);
|
|
2415
|
-
return parts.join(" \u2022 ");
|
|
2416
|
-
};
|
|
2417
|
-
const isInitialRender = !container.classList.contains("grid");
|
|
2418
|
-
if ((!rids || rids.length === 0) && isInitialRender) {
|
|
2419
|
-
const gridContainer2 = document.createElement("div");
|
|
2420
|
-
gridContainer2.className = "grid grid-cols-4 gap-3 mb-3";
|
|
2421
|
-
for (let i = 0; i < 4; i++) {
|
|
2422
|
-
const slot = document.createElement("div");
|
|
2423
|
-
slot.className = "aspect-square bg-gray-100 border-2 border-dashed border-gray-300 rounded flex items-center justify-center cursor-pointer hover:border-gray-400 transition-colors";
|
|
2424
|
-
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
|
2425
|
-
svg.setAttribute("class", "w-12 h-12 text-gray-400");
|
|
2426
|
-
svg.setAttribute("fill", "currentColor");
|
|
2427
|
-
svg.setAttribute("viewBox", "0 0 24 24");
|
|
2428
|
-
const path = document.createElementNS(
|
|
2429
|
-
"http://www.w3.org/2000/svg",
|
|
2430
|
-
"path"
|
|
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;
|
|
3149
|
+
if (state.config.getThumbnail) {
|
|
3150
|
+
try {
|
|
3151
|
+
const thumbUrl = await state.config.getThumbnail(resourceId);
|
|
3152
|
+
if (thumbUrl) {
|
|
3153
|
+
const img = document.createElement("img");
|
|
3154
|
+
img.style.cssText = "width:100%;height:100%;object-fit:contain;background:var(--fb-file-upload-bg-color,#f3f4f6);";
|
|
3155
|
+
img.alt = actualFileName || resourceId;
|
|
3156
|
+
img.src = thumbUrl;
|
|
3157
|
+
tile.appendChild(img);
|
|
3158
|
+
tile.appendChild(actionsEl);
|
|
3159
|
+
return tile;
|
|
2445
3160
|
}
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
);
|
|
2449
|
-
if (fileInput) fileInput.click();
|
|
2450
|
-
};
|
|
2451
|
-
gridContainer2.appendChild(slot);
|
|
3161
|
+
} catch {
|
|
3162
|
+
}
|
|
2452
3163
|
}
|
|
2453
|
-
const
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
3164
|
+
const captionHtml = actualFileName ? `<div class="fb-tile-label">${escapeHtml(actualFileName.length > 10 ? actualFileName.substring(0, 8) + "\u2026" : actualFileName)}</div>
|
|
3165
|
+
<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>` : "";
|
|
3166
|
+
tile.innerHTML = `
|
|
3167
|
+
<div style="display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;padding:6px;gap:4px;">
|
|
3168
|
+
<div style="font-size:36px;">\u{1F4C1}</div>
|
|
3169
|
+
${captionHtml}
|
|
3170
|
+
</div>`;
|
|
3171
|
+
tile.appendChild(actionsEl);
|
|
2459
3172
|
}
|
|
2460
|
-
|
|
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);
|
|
3173
|
+
return tile;
|
|
2516
3174
|
}
|
|
2517
|
-
function
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
3175
|
+
async function renderSingleFileEditTile(fileContainer, resourceId, state, deps) {
|
|
3176
|
+
const meta = state.resourceIndex.get(resourceId);
|
|
3177
|
+
const fileName = meta?.name ?? resourceId.split("/").pop() ?? "";
|
|
3178
|
+
const removeHandler = deps.onRemove ?? null;
|
|
3179
|
+
const tile = await renderFilePreviewReadonly(resourceId, state, fileName, {
|
|
3180
|
+
canRemove: true,
|
|
3181
|
+
removeHandler
|
|
3182
|
+
});
|
|
3183
|
+
fileContainer.className = "file-preview-container";
|
|
3184
|
+
fileContainer.removeAttribute("style");
|
|
3185
|
+
clear(fileContainer);
|
|
3186
|
+
fileContainer.appendChild(tile);
|
|
2524
3187
|
}
|
|
2525
|
-
async function
|
|
2526
|
-
if (meta
|
|
3188
|
+
async function fillTileContent(tile, rid, meta, state, actionsEl) {
|
|
3189
|
+
if (meta?.type?.startsWith("image/")) {
|
|
2527
3190
|
if (meta.file && meta.file instanceof File) {
|
|
2528
3191
|
const img = document.createElement("img");
|
|
2529
|
-
img.
|
|
3192
|
+
img.style.cssText = "width:100%;height:100%;object-fit:contain;background:var(--fb-file-upload-bg-color,#f3f4f6);";
|
|
2530
3193
|
img.alt = meta.name;
|
|
2531
3194
|
const reader = new FileReader();
|
|
2532
3195
|
reader.onload = (e) => {
|
|
2533
3196
|
img.src = e.target?.result || "";
|
|
3197
|
+
attachZoomHover(tile, img.src, meta.name, actionsEl ?? null);
|
|
2534
3198
|
};
|
|
2535
3199
|
reader.readAsDataURL(meta.file);
|
|
2536
|
-
|
|
3200
|
+
tile.appendChild(img);
|
|
2537
3201
|
} else if (state.config.getThumbnail) {
|
|
2538
3202
|
try {
|
|
2539
3203
|
const url = await state.config.getThumbnail(rid);
|
|
2540
3204
|
if (url) {
|
|
2541
3205
|
const img = document.createElement("img");
|
|
2542
|
-
img.
|
|
3206
|
+
img.style.cssText = "width:100%;height:100%;object-fit:contain;background:var(--fb-file-upload-bg-color,#f3f4f6);";
|
|
2543
3207
|
img.alt = meta.name;
|
|
2544
3208
|
img.src = url;
|
|
2545
|
-
|
|
3209
|
+
tile.appendChild(img);
|
|
3210
|
+
attachZoomHover(tile, url, meta.name, actionsEl ?? null);
|
|
2546
3211
|
} 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>`;
|
|
3212
|
+
tile.innerHTML = `<div style="display:flex;align-items:center;justify-content:center;height:100%;font-size:36px;">\u{1F5BC}\uFE0F</div>`;
|
|
2552
3213
|
}
|
|
2553
3214
|
} catch (error) {
|
|
2554
3215
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
2555
|
-
if (state.config.onThumbnailError)
|
|
2556
|
-
|
|
2557
|
-
}
|
|
2558
|
-
renderThumbnailError(slot, state);
|
|
3216
|
+
if (state.config.onThumbnailError) state.config.onThumbnailError(err, rid);
|
|
3217
|
+
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
3218
|
}
|
|
2560
3219
|
} 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>`;
|
|
3220
|
+
tile.innerHTML = `<div style="display:flex;align-items:center;justify-content:center;height:100%;font-size:36px;">\u{1F5BC}\uFE0F</div>`;
|
|
2566
3221
|
}
|
|
2567
|
-
|
|
3222
|
+
if (actionsEl) tile.appendChild(actionsEl);
|
|
3223
|
+
} else if (meta?.type?.startsWith("video/")) {
|
|
2568
3224
|
if (meta.file && meta.file instanceof File) {
|
|
2569
3225
|
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>
|
|
3226
|
+
tile.innerHTML = `
|
|
3227
|
+
<video style="width:100%;height:100%;" preload="metadata" muted src="${videoUrl}"></video>
|
|
3228
|
+
<div class="fb-video-overlay">
|
|
3229
|
+
<div class="fb-play-btn" style="width:20px;height:20px;">
|
|
3230
|
+
<svg width="8" height="10" viewBox="0 0 8 10" fill="currentColor"><path d="M0 0l8 5-8 5z"/></svg>
|
|
2580
3231
|
</div>
|
|
2581
|
-
</div
|
|
2582
|
-
`;
|
|
3232
|
+
</div>`;
|
|
2583
3233
|
} else if (state.config.getThumbnail) {
|
|
2584
3234
|
try {
|
|
2585
3235
|
const videoUrl = await state.config.getThumbnail(rid);
|
|
2586
3236
|
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>
|
|
3237
|
+
tile.innerHTML = `
|
|
3238
|
+
<img style="width:100%;height:100%;object-fit:contain;background:var(--fb-file-upload-bg-color,#f3f4f6);" alt="${escapeHtml(meta.name)}" src="${videoUrl}">
|
|
3239
|
+
<div class="fb-video-overlay">
|
|
3240
|
+
<div class="fb-play-btn" style="width:20px;height:20px;">
|
|
3241
|
+
<svg width="8" height="10" viewBox="0 0 8 10" fill="currentColor"><path d="M0 0l8 5-8 5z"/></svg>
|
|
2597
3242
|
</div>
|
|
2598
|
-
</div
|
|
2599
|
-
`;
|
|
3243
|
+
</div>`;
|
|
2600
3244
|
} 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>`;
|
|
3245
|
+
tile.innerHTML = `<div style="display:flex;align-items:center;justify-content:center;height:100%;font-size:36px;">\u{1F3A5}</div>`;
|
|
2607
3246
|
}
|
|
2608
3247
|
} catch (error) {
|
|
2609
3248
|
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");
|
|
3249
|
+
if (state.config.onThumbnailError) state.config.onThumbnailError(err, rid);
|
|
3250
|
+
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
3251
|
}
|
|
2615
3252
|
} 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>`;
|
|
3253
|
+
tile.innerHTML = `<div style="display:flex;align-items:center;justify-content:center;height:100%;font-size:36px;">\u{1F3A5}</div>`;
|
|
2622
3254
|
}
|
|
3255
|
+
if (actionsEl) tile.appendChild(actionsEl);
|
|
2623
3256
|
} else {
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
3257
|
+
const name = meta?.name ?? "";
|
|
3258
|
+
const hasExtension = name.includes(".");
|
|
3259
|
+
const captionHtml = hasExtension ? `<div class="fb-tile-label">${escapeHtml(name.length > 10 ? name.substring(0, 8) + "\u2026" : name)}</div>` : "";
|
|
3260
|
+
tile.innerHTML = `
|
|
3261
|
+
<div style="display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;padding:6px;gap:4px;">
|
|
3262
|
+
<div style="font-size:36px;">\u{1F4C1}</div>
|
|
3263
|
+
${captionHtml}
|
|
3264
|
+
</div>`;
|
|
3265
|
+
if (actionsEl) tile.appendChild(actionsEl);
|
|
2628
3266
|
}
|
|
2629
3267
|
}
|
|
2630
|
-
function
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
}
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
3268
|
+
async function forceDownload(resourceId, fileName, state) {
|
|
3269
|
+
try {
|
|
3270
|
+
let fileUrl = null;
|
|
3271
|
+
if (state.config.getDownloadUrl) {
|
|
3272
|
+
fileUrl = state.config.getDownloadUrl(resourceId);
|
|
3273
|
+
} else if (state.config.getThumbnail) {
|
|
3274
|
+
fileUrl = await state.config.getThumbnail(resourceId);
|
|
3275
|
+
}
|
|
3276
|
+
if (fileUrl) {
|
|
3277
|
+
const finalUrl = fileUrl.startsWith("http") ? fileUrl : new URL(fileUrl, window.location.href).href;
|
|
3278
|
+
const response = await fetch(finalUrl);
|
|
3279
|
+
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
|
|
3280
|
+
const blob = await response.blob();
|
|
3281
|
+
downloadBlob(blob, fileName);
|
|
3282
|
+
} else {
|
|
3283
|
+
throw new Error("No download URL available for resource");
|
|
3284
|
+
}
|
|
3285
|
+
} catch (error) {
|
|
3286
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
3287
|
+
if (state.config.onDownloadError) {
|
|
3288
|
+
state.config.onDownloadError(err, resourceId, fileName);
|
|
3289
|
+
}
|
|
3290
|
+
console.error(`File download failed for ${fileName}:`, err);
|
|
3291
|
+
throw err;
|
|
3292
|
+
}
|
|
2654
3293
|
}
|
|
2655
|
-
function
|
|
2656
|
-
|
|
2657
|
-
|
|
3294
|
+
function downloadBlob(blob, fileName) {
|
|
3295
|
+
try {
|
|
3296
|
+
const blobUrl = URL.createObjectURL(blob);
|
|
3297
|
+
const link = document.createElement("a");
|
|
3298
|
+
link.href = blobUrl;
|
|
3299
|
+
link.download = fileName;
|
|
3300
|
+
link.style.display = "none";
|
|
3301
|
+
document.body.appendChild(link);
|
|
3302
|
+
link.click();
|
|
3303
|
+
document.body.removeChild(link);
|
|
3304
|
+
setTimeout(() => {
|
|
3305
|
+
URL.revokeObjectURL(blobUrl);
|
|
3306
|
+
}, 100);
|
|
3307
|
+
} catch (error) {
|
|
3308
|
+
throw new Error(`Blob download failed: ${error.message}`);
|
|
3309
|
+
}
|
|
2658
3310
|
}
|
|
2659
|
-
|
|
3311
|
+
|
|
3312
|
+
// src/components/file/upload.ts
|
|
3313
|
+
async function uploadSingleFile(file, state) {
|
|
3314
|
+
if (!state.config.uploadFile) {
|
|
3315
|
+
throw new Error(
|
|
3316
|
+
"No upload handler configured. Set uploadHandler via FormBuilder.setUploadHandler()"
|
|
3317
|
+
);
|
|
3318
|
+
}
|
|
3319
|
+
try {
|
|
3320
|
+
const rid = await state.config.uploadFile(file);
|
|
3321
|
+
if (typeof rid !== "string") {
|
|
3322
|
+
throw new Error("Upload handler must return a string resource ID");
|
|
3323
|
+
}
|
|
3324
|
+
return rid;
|
|
3325
|
+
} catch (error) {
|
|
3326
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
3327
|
+
if (state.config.onUploadError) state.config.onUploadError(err, file);
|
|
3328
|
+
throw new Error(`File upload failed: ${err.message}`);
|
|
3329
|
+
}
|
|
3330
|
+
}
|
|
3331
|
+
async function handleFileSelect(opts) {
|
|
3332
|
+
const {
|
|
3333
|
+
file,
|
|
3334
|
+
container,
|
|
3335
|
+
fieldName,
|
|
3336
|
+
state,
|
|
3337
|
+
deps = null,
|
|
3338
|
+
instance = null,
|
|
3339
|
+
allowedExtensions = [],
|
|
3340
|
+
allowedMimes = [],
|
|
3341
|
+
maxSizeMB = Infinity
|
|
3342
|
+
} = opts;
|
|
2660
3343
|
if (!isFileExtensionAllowed(file.name, allowedExtensions)) {
|
|
2661
3344
|
const formats = allowedExtensions.join(", ");
|
|
2662
3345
|
showFileError(
|
|
@@ -2665,6 +3348,14 @@ async function handleFileSelect(file, container, fieldName, state, deps = null,
|
|
|
2665
3348
|
);
|
|
2666
3349
|
return;
|
|
2667
3350
|
}
|
|
3351
|
+
if (!isMimeAllowed(file.type, allowedMimes)) {
|
|
3352
|
+
const mimes = allowedMimes.join(", ");
|
|
3353
|
+
showFileError(
|
|
3354
|
+
container,
|
|
3355
|
+
t("invalidFileMime", state, { name: file.name, type: file.type, mimes })
|
|
3356
|
+
);
|
|
3357
|
+
return;
|
|
3358
|
+
}
|
|
2668
3359
|
if (!isFileSizeAllowed(file, maxSizeMB)) {
|
|
2669
3360
|
showFileError(
|
|
2670
3361
|
container,
|
|
@@ -2673,24 +3364,18 @@ async function handleFileSelect(file, container, fieldName, state, deps = null,
|
|
|
2673
3364
|
return;
|
|
2674
3365
|
}
|
|
2675
3366
|
clearFileError(container);
|
|
3367
|
+
ensureFileStyles();
|
|
3368
|
+
container.innerHTML = `
|
|
3369
|
+
<div style="display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;gap:6px;padding:6px;">
|
|
3370
|
+
<div class="fb-spinner"></div>
|
|
3371
|
+
<div style="font-size:11px;color:var(--fb-text-secondary-color,#6b7280);text-align:center;">${escapeHtml(t("uploadingFile", state))}</div>
|
|
3372
|
+
</div>`;
|
|
2676
3373
|
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
|
-
);
|
|
3374
|
+
try {
|
|
3375
|
+
rid = await uploadSingleFile(file, state);
|
|
3376
|
+
} catch (error) {
|
|
3377
|
+
setEmptyFileContainer(container, state);
|
|
3378
|
+
throw error;
|
|
2694
3379
|
}
|
|
2695
3380
|
state.resourceIndex.set(rid, {
|
|
2696
3381
|
name: file.name,
|
|
@@ -2698,7 +3383,6 @@ async function handleFileSelect(file, container, fieldName, state, deps = null,
|
|
|
2698
3383
|
size: file.size,
|
|
2699
3384
|
uploadedAt: /* @__PURE__ */ new Date(),
|
|
2700
3385
|
file
|
|
2701
|
-
// Store the file object for local preview
|
|
2702
3386
|
});
|
|
2703
3387
|
let hiddenInput = container.parentElement?.querySelector(
|
|
2704
3388
|
'input[type="hidden"]'
|
|
@@ -2710,682 +3394,992 @@ async function handleFileSelect(file, container, fieldName, state, deps = null,
|
|
|
2710
3394
|
container.parentElement?.appendChild(hiddenInput);
|
|
2711
3395
|
}
|
|
2712
3396
|
hiddenInput.value = rid;
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
3397
|
+
const isVideo = file.type.startsWith("video/");
|
|
3398
|
+
if (!isVideo && deps) {
|
|
3399
|
+
renderSingleFileEditTile(container, rid, state, deps).catch(console.error);
|
|
3400
|
+
} else {
|
|
3401
|
+
renderFilePreview(container, rid, state, {
|
|
3402
|
+
fileName: file.name,
|
|
3403
|
+
isReadonly: false,
|
|
3404
|
+
deps
|
|
3405
|
+
}).catch(console.error);
|
|
3406
|
+
}
|
|
2718
3407
|
if (instance && !state.config.readonly) {
|
|
2719
3408
|
instance.triggerOnChange(fieldName, rid);
|
|
2720
3409
|
}
|
|
2721
3410
|
}
|
|
2722
|
-
function
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
const
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
}
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
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()"
|
|
3411
|
+
function filterAndSlice(allFiles, currentCount, constraints, state) {
|
|
3412
|
+
const rejectedByExt = allFiles.filter(
|
|
3413
|
+
(f) => !isFileExtensionAllowed(f.name, constraints.allowedExtensions)
|
|
3414
|
+
);
|
|
3415
|
+
const afterExt = allFiles.filter(
|
|
3416
|
+
(f) => isFileExtensionAllowed(f.name, constraints.allowedExtensions)
|
|
3417
|
+
);
|
|
3418
|
+
const rejectedByMime = afterExt.filter(
|
|
3419
|
+
(f) => !isMimeAllowed(f.type, constraints.allowedMimes)
|
|
3420
|
+
);
|
|
3421
|
+
const afterMime = afterExt.filter(
|
|
3422
|
+
(f) => isMimeAllowed(f.type, constraints.allowedMimes)
|
|
3423
|
+
);
|
|
3424
|
+
const rejectedBySize = afterMime.filter(
|
|
3425
|
+
(f) => !isFileSizeAllowed(f, constraints.maxSize)
|
|
3426
|
+
);
|
|
3427
|
+
const valid = afterMime.filter((f) => isFileSizeAllowed(f, constraints.maxSize));
|
|
3428
|
+
const remaining = constraints.maxCount === Infinity ? valid.length : Math.max(0, constraints.maxCount - currentCount);
|
|
3429
|
+
const accepted = valid.slice(0, remaining);
|
|
3430
|
+
const skippedByCount = valid.length - accepted.length;
|
|
3431
|
+
const errorParts = [];
|
|
3432
|
+
if (rejectedByExt.length > 0) {
|
|
3433
|
+
const formats = constraints.allowedExtensions.join(", ");
|
|
3434
|
+
const names = rejectedByExt.map((f) => f.name).join(", ");
|
|
3435
|
+
errorParts.push(t("invalidFileExtension", state, { name: names, formats }));
|
|
3436
|
+
}
|
|
3437
|
+
if (rejectedByMime.length > 0) {
|
|
3438
|
+
const mimes = constraints.allowedMimes.join(", ");
|
|
3439
|
+
const names = rejectedByMime.map((f) => f.name).join(", ");
|
|
3440
|
+
errorParts.push(t("invalidFileMime", state, { name: names, type: rejectedByMime.map((f) => f.type).join(", "), mimes }));
|
|
3441
|
+
}
|
|
3442
|
+
if (rejectedBySize.length > 0) {
|
|
3443
|
+
const names = rejectedBySize.map((f) => f.name).join(", ");
|
|
3444
|
+
errorParts.push(
|
|
3445
|
+
t("fileTooLarge", state, { name: names, maxSize: constraints.maxSize })
|
|
2774
3446
|
);
|
|
2775
3447
|
}
|
|
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}`);
|
|
3448
|
+
if (skippedByCount > 0) {
|
|
3449
|
+
errorParts.push(
|
|
3450
|
+
t("filesLimitExceeded", state, {
|
|
3451
|
+
skipped: skippedByCount,
|
|
3452
|
+
max: constraints.maxCount
|
|
3453
|
+
})
|
|
3454
|
+
);
|
|
2820
3455
|
}
|
|
2821
|
-
}
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
}
|
|
3456
|
+
return { accepted, errorMessage: errorParts.join(" \u2022 ") };
|
|
3457
|
+
}
|
|
3458
|
+
async function uploadBatch(accepted, resourceIds, listEl, state) {
|
|
3459
|
+
await Promise.all(
|
|
3460
|
+
accepted.map(async (file) => {
|
|
3461
|
+
const placeholder = createUploadingTile(file.name, state);
|
|
3462
|
+
if (listEl) {
|
|
3463
|
+
const tilesWrap = ensureTilesWrap(listEl);
|
|
3464
|
+
const addTile = tilesWrap.querySelector(".fb-tile-add");
|
|
3465
|
+
if (addTile) {
|
|
3466
|
+
tilesWrap.insertBefore(placeholder, addTile);
|
|
3467
|
+
} else {
|
|
3468
|
+
tilesWrap.appendChild(placeholder);
|
|
2835
3469
|
}
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
3470
|
+
}
|
|
3471
|
+
try {
|
|
3472
|
+
const rid = await uploadSingleFile(file, state);
|
|
3473
|
+
state.resourceIndex.set(rid, {
|
|
3474
|
+
name: file.name,
|
|
3475
|
+
type: file.type,
|
|
3476
|
+
size: file.size,
|
|
2840
3477
|
uploadedAt: /* @__PURE__ */ new Date(),
|
|
2841
3478
|
file: void 0
|
|
2842
3479
|
});
|
|
3480
|
+
resourceIds.push(rid);
|
|
3481
|
+
} finally {
|
|
3482
|
+
placeholder.remove();
|
|
2843
3483
|
}
|
|
2844
|
-
})
|
|
2845
|
-
|
|
2846
|
-
}
|
|
2847
|
-
function handleInitialFileData(initial, fileContainer, pathKey, fileWrapper, state, deps) {
|
|
2848
|
-
if (!state.resourceIndex.has(initial)) {
|
|
2849
|
-
const filename = initial.split("/").pop() || "file";
|
|
2850
|
-
const extension = filename.split(".").pop()?.toLowerCase();
|
|
2851
|
-
let fileType = "application/octet-stream";
|
|
2852
|
-
if (extension) {
|
|
2853
|
-
if (["jpg", "jpeg", "png", "gif", "webp"].includes(extension)) {
|
|
2854
|
-
fileType = `image/${extension === "jpg" ? "jpeg" : extension}`;
|
|
2855
|
-
} else if (["mp4", "webm", "mov", "avi"].includes(extension)) {
|
|
2856
|
-
fileType = `video/${extension === "mov" ? "quicktime" : extension}`;
|
|
2857
|
-
}
|
|
2858
|
-
}
|
|
2859
|
-
state.resourceIndex.set(initial, {
|
|
2860
|
-
name: filename,
|
|
2861
|
-
type: fileType,
|
|
2862
|
-
size: 0,
|
|
2863
|
-
uploadedAt: /* @__PURE__ */ new Date(),
|
|
2864
|
-
file: void 0
|
|
2865
|
-
});
|
|
2866
|
-
}
|
|
2867
|
-
renderFilePreview(fileContainer, initial, state, {
|
|
2868
|
-
fileName: initial,
|
|
2869
|
-
isReadonly: false,
|
|
2870
|
-
deps
|
|
2871
|
-
}).catch(console.error);
|
|
2872
|
-
const hiddenInput = document.createElement("input");
|
|
2873
|
-
hiddenInput.type = "hidden";
|
|
2874
|
-
hiddenInput.name = pathKey;
|
|
2875
|
-
hiddenInput.value = initial;
|
|
2876
|
-
fileWrapper.appendChild(hiddenInput);
|
|
3484
|
+
})
|
|
3485
|
+
);
|
|
2877
3486
|
}
|
|
2878
|
-
function setupFilesDropHandler(filesContainer,
|
|
3487
|
+
function setupFilesDropHandler(filesContainer, resourceIds, state, updateCallback, constraints, pathKey, instance) {
|
|
2879
3488
|
setupDragAndDrop(filesContainer, async (files) => {
|
|
2880
|
-
const
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
(f) => isFileExtensionAllowed(f.name, constraints.allowedExtensions)
|
|
2886
|
-
);
|
|
2887
|
-
const rejectedBySize = afterExtension.filter(
|
|
2888
|
-
(f) => !isFileSizeAllowed(f, constraints.maxSize)
|
|
2889
|
-
);
|
|
2890
|
-
const validFiles = afterExtension.filter(
|
|
2891
|
-
(f) => isFileSizeAllowed(f, constraints.maxSize)
|
|
3489
|
+
const { accepted, errorMessage } = filterAndSlice(
|
|
3490
|
+
Array.from(files),
|
|
3491
|
+
resourceIds.length,
|
|
3492
|
+
constraints,
|
|
3493
|
+
state
|
|
2892
3494
|
);
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
const skippedByCount = validFiles.length - arr.length;
|
|
2896
|
-
const errorParts = [];
|
|
2897
|
-
if (rejectedByExtension.length > 0) {
|
|
2898
|
-
const formats = constraints.allowedExtensions.join(", ");
|
|
2899
|
-
const names = rejectedByExtension.map((f) => f.name).join(", ");
|
|
2900
|
-
errorParts.push(
|
|
2901
|
-
t("invalidFileExtension", state, { name: names, formats })
|
|
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 "));
|
|
3495
|
+
if (errorMessage) {
|
|
3496
|
+
showFileError(filesContainer, errorMessage);
|
|
2920
3497
|
} else {
|
|
2921
3498
|
clearFileError(filesContainer);
|
|
2922
3499
|
}
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
state.resourceIndex.set(rid, {
|
|
2926
|
-
name: file.name,
|
|
2927
|
-
type: file.type,
|
|
2928
|
-
size: file.size,
|
|
2929
|
-
uploadedAt: /* @__PURE__ */ new Date(),
|
|
2930
|
-
file: void 0
|
|
2931
|
-
});
|
|
2932
|
-
initialFiles.push(rid);
|
|
2933
|
-
}
|
|
3500
|
+
const list = filesContainer.querySelector(".files-list") ?? filesContainer;
|
|
3501
|
+
await uploadBatch(accepted, resourceIds, list, state);
|
|
2934
3502
|
updateCallback();
|
|
2935
3503
|
if (instance && pathKey && !state.config.readonly) {
|
|
2936
|
-
instance.triggerOnChange(pathKey,
|
|
3504
|
+
instance.triggerOnChange(pathKey, resourceIds);
|
|
2937
3505
|
}
|
|
2938
3506
|
});
|
|
2939
3507
|
}
|
|
2940
|
-
function setupFilesPickerHandler(filesPicker,
|
|
3508
|
+
function setupFilesPickerHandler(filesPicker, resourceIds, state, updateCallback, constraints, pathKey, instance) {
|
|
2941
3509
|
filesPicker.onchange = async () => {
|
|
2942
|
-
if (filesPicker.files)
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
|
|
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
|
-
}
|
|
3510
|
+
if (!filesPicker.files) return;
|
|
3511
|
+
const wrapperEl = filesPicker.closest(".space-y-2") || filesPicker.parentElement;
|
|
3512
|
+
const { accepted, errorMessage } = filterAndSlice(
|
|
3513
|
+
Array.from(filesPicker.files),
|
|
3514
|
+
resourceIds.length,
|
|
3515
|
+
constraints,
|
|
3516
|
+
state
|
|
3517
|
+
);
|
|
3518
|
+
if (errorMessage && wrapperEl) {
|
|
3519
|
+
showFileError(wrapperEl, errorMessage);
|
|
3520
|
+
} else if (wrapperEl) {
|
|
3521
|
+
clearFileError(wrapperEl);
|
|
3001
3522
|
}
|
|
3523
|
+
const listEl = wrapperEl?.querySelector(".files-list");
|
|
3524
|
+
await uploadBatch(accepted, resourceIds, listEl ?? null, state);
|
|
3002
3525
|
updateCallback();
|
|
3003
3526
|
filesPicker.value = "";
|
|
3004
3527
|
if (instance && pathKey && !state.config.readonly) {
|
|
3005
|
-
instance.triggerOnChange(pathKey,
|
|
3528
|
+
instance.triggerOnChange(pathKey, resourceIds);
|
|
3006
3529
|
}
|
|
3007
3530
|
};
|
|
3008
3531
|
}
|
|
3009
|
-
|
|
3010
|
-
|
|
3011
|
-
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
|
|
3020
|
-
|
|
3021
|
-
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
|
|
3025
|
-
|
|
3026
|
-
|
|
3027
|
-
|
|
3028
|
-
|
|
3532
|
+
|
|
3533
|
+
// src/components/file/library.ts
|
|
3534
|
+
function buildAcceptContext(element) {
|
|
3535
|
+
if (!element.accept) return void 0;
|
|
3536
|
+
if (typeof element.accept === "string") {
|
|
3537
|
+
const exts2 = getAllowedExtensions(element.accept);
|
|
3538
|
+
return exts2.length > 0 ? { extensions: exts2 } : void 0;
|
|
3539
|
+
}
|
|
3540
|
+
const exts = element.accept.extensions ?? [];
|
|
3541
|
+
const mime = element.accept.mime ?? [];
|
|
3542
|
+
const normalizedExts = exts.map((e) => e.toLowerCase());
|
|
3543
|
+
if (normalizedExts.length === 0 && mime.length === 0) return void 0;
|
|
3544
|
+
const result = {};
|
|
3545
|
+
if (normalizedExts.length > 0) result.extensions = normalizedExts;
|
|
3546
|
+
if (mime.length > 0) result.mime = mime;
|
|
3547
|
+
return result;
|
|
3548
|
+
}
|
|
3549
|
+
function validatePickedResource(resource, allowedExtensions, allowedMimes, maxSizeMB, state) {
|
|
3550
|
+
if (!isFileExtensionAllowed(resource.name, allowedExtensions)) {
|
|
3551
|
+
const formats = allowedExtensions.join(", ");
|
|
3552
|
+
return t("invalidFileExtension", state, { name: resource.name, formats });
|
|
3553
|
+
}
|
|
3554
|
+
if (!isMimeAllowed(resource.type, allowedMimes)) {
|
|
3555
|
+
const mimes = allowedMimes.join(", ");
|
|
3556
|
+
return t("invalidFileMime", state, { name: resource.name, type: resource.type, mimes });
|
|
3557
|
+
}
|
|
3558
|
+
if (!isSizeWithinLimit(resource.size, maxSizeMB)) {
|
|
3559
|
+
return t("fileTooLarge", state, { name: resource.name, maxSize: maxSizeMB });
|
|
3560
|
+
}
|
|
3561
|
+
return null;
|
|
3562
|
+
}
|
|
3563
|
+
function readCurrentResourceIds(wrapper) {
|
|
3564
|
+
const raw = wrapper.dataset.resourceIds;
|
|
3565
|
+
if (!raw) return [];
|
|
3566
|
+
try {
|
|
3567
|
+
const parsed = JSON.parse(raw);
|
|
3568
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
3569
|
+
} catch {
|
|
3570
|
+
return [];
|
|
3571
|
+
}
|
|
3572
|
+
}
|
|
3573
|
+
function registerPickedResource(resource, state) {
|
|
3574
|
+
const existing = state.resourceIndex.get(resource.resourceId);
|
|
3575
|
+
state.resourceIndex.set(resource.resourceId, {
|
|
3576
|
+
name: resource.name,
|
|
3577
|
+
type: resource.type,
|
|
3578
|
+
size: resource.size,
|
|
3579
|
+
uploadedAt: existing?.uploadedAt ?? /* @__PURE__ */ new Date(),
|
|
3580
|
+
file: existing?.file
|
|
3581
|
+
});
|
|
3582
|
+
}
|
|
3583
|
+
function extractPickerError(error, state) {
|
|
3584
|
+
if (error instanceof Error && error.message) return error.message;
|
|
3585
|
+
return t("pickerError", state);
|
|
3586
|
+
}
|
|
3587
|
+
async function handleLibraryPickMulti(state, element, wrapper, fieldPath, resourceIds, maxCount, updateCallback, instance) {
|
|
3588
|
+
if (!state.config.pickExistingFiles) return;
|
|
3589
|
+
const allowedExtensions = getAllowedExtensions(element.accept);
|
|
3590
|
+
const allowedMimes = getAllowedMimes(element.accept);
|
|
3591
|
+
const maxSizeMB = element.maxSize ?? Infinity;
|
|
3592
|
+
const currentIds = readCurrentResourceIds(wrapper);
|
|
3593
|
+
const remaining = maxCount === Infinity ? Infinity : Math.max(0, maxCount - currentIds.length);
|
|
3594
|
+
let picked;
|
|
3595
|
+
try {
|
|
3596
|
+
picked = await state.config.pickExistingFiles({
|
|
3597
|
+
fieldPath,
|
|
3598
|
+
mode: "multiple",
|
|
3599
|
+
accept: buildAcceptContext(element),
|
|
3600
|
+
maxSizeMB: maxSizeMB === Infinity ? void 0 : maxSizeMB,
|
|
3601
|
+
remainingSlots: remaining === Infinity ? void 0 : remaining,
|
|
3602
|
+
selectedResourceIds: [...currentIds]
|
|
3603
|
+
});
|
|
3604
|
+
} catch (error) {
|
|
3605
|
+
showFileError(wrapper, extractPickerError(error, state));
|
|
3606
|
+
return;
|
|
3607
|
+
}
|
|
3608
|
+
if (picked.length === 0) return;
|
|
3609
|
+
const existingSet = new Set(currentIds);
|
|
3610
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3611
|
+
const deduped = picked.filter((r) => {
|
|
3612
|
+
if (existingSet.has(r.resourceId)) return false;
|
|
3613
|
+
if (seen.has(r.resourceId)) return false;
|
|
3614
|
+
seen.add(r.resourceId);
|
|
3615
|
+
return true;
|
|
3616
|
+
});
|
|
3617
|
+
const validItems = deduped.filter((r) => {
|
|
3618
|
+
const err = validatePickedResource(r, allowedExtensions, allowedMimes, maxSizeMB, state);
|
|
3619
|
+
return err === null;
|
|
3620
|
+
});
|
|
3621
|
+
const freshRemaining = maxCount === Infinity ? validItems.length : Math.max(0, maxCount - resourceIds.length);
|
|
3622
|
+
const accepted = validItems.slice(0, freshRemaining);
|
|
3623
|
+
const skipped = validItems.length - accepted.length;
|
|
3624
|
+
if (accepted.length === 0) return;
|
|
3625
|
+
clearFileError(wrapper);
|
|
3626
|
+
if (skipped > 0) {
|
|
3627
|
+
showFileError(
|
|
3628
|
+
wrapper,
|
|
3629
|
+
t("filesLimitExceeded", state, { skipped, max: maxCount })
|
|
3630
|
+
);
|
|
3631
|
+
}
|
|
3632
|
+
for (const resource of accepted) {
|
|
3633
|
+
registerPickedResource(resource, state);
|
|
3634
|
+
resourceIds.push(resource.resourceId);
|
|
3635
|
+
}
|
|
3636
|
+
wrapper.dataset.resourceIds = JSON.stringify(resourceIds);
|
|
3637
|
+
updateCallback();
|
|
3638
|
+
if (!state.config.readonly) {
|
|
3639
|
+
instance.triggerOnChange(fieldPath, resourceIds);
|
|
3640
|
+
}
|
|
3641
|
+
}
|
|
3642
|
+
async function handleLibraryPickSingle(state, element, container, fileWrapper, pathKey, fieldPath, renderCallback, instance) {
|
|
3643
|
+
if (!state.config.pickExistingFiles) return;
|
|
3644
|
+
const allowedExtensions = getAllowedExtensions(element.accept);
|
|
3645
|
+
const allowedMimes = getAllowedMimes(element.accept);
|
|
3646
|
+
const maxSizeMB = element.maxSize ?? Infinity;
|
|
3647
|
+
let picked;
|
|
3648
|
+
try {
|
|
3649
|
+
picked = await state.config.pickExistingFiles({
|
|
3650
|
+
fieldPath,
|
|
3651
|
+
mode: "single",
|
|
3652
|
+
accept: buildAcceptContext(element),
|
|
3653
|
+
maxSizeMB: maxSizeMB === Infinity ? void 0 : maxSizeMB,
|
|
3654
|
+
selectedResourceIds: []
|
|
3655
|
+
});
|
|
3656
|
+
} catch (error) {
|
|
3657
|
+
showFileError(container, extractPickerError(error, state));
|
|
3658
|
+
return;
|
|
3659
|
+
}
|
|
3660
|
+
if (picked.length === 0) return;
|
|
3661
|
+
const first = picked[0];
|
|
3662
|
+
const validationError = validatePickedResource(first, allowedExtensions, allowedMimes, maxSizeMB, state);
|
|
3663
|
+
if (validationError !== null) {
|
|
3664
|
+
showFileError(container, validationError);
|
|
3665
|
+
return;
|
|
3666
|
+
}
|
|
3667
|
+
clearFileError(container);
|
|
3668
|
+
registerPickedResource(first, state);
|
|
3669
|
+
let hiddenInput = fileWrapper.querySelector('input[type="hidden"]');
|
|
3670
|
+
if (!hiddenInput) {
|
|
3671
|
+
hiddenInput = document.createElement("input");
|
|
3672
|
+
hiddenInput.type = "hidden";
|
|
3673
|
+
hiddenInput.name = pathKey;
|
|
3674
|
+
fileWrapper.appendChild(hiddenInput);
|
|
3675
|
+
}
|
|
3676
|
+
hiddenInput.value = first.resourceId;
|
|
3677
|
+
await renderCallback(first.resourceId);
|
|
3678
|
+
if (!state.config.readonly) {
|
|
3679
|
+
instance.triggerOnChange(fieldPath, first.resourceId);
|
|
3680
|
+
}
|
|
3681
|
+
}
|
|
3682
|
+
|
|
3683
|
+
// src/components/file/render-edit.ts
|
|
3684
|
+
function handleInitialFileData(initial, fileContainer, pathKey, fileWrapper, state, deps) {
|
|
3685
|
+
seedInferredResource(initial, state.resourceIndex);
|
|
3686
|
+
const meta = state.resourceIndex.get(initial);
|
|
3687
|
+
const isVideo = meta?.type?.startsWith("video/");
|
|
3688
|
+
if (isVideo) {
|
|
3689
|
+
renderFilePreview(fileContainer, initial, state, {
|
|
3690
|
+
fileName: initial,
|
|
3691
|
+
isReadonly: false,
|
|
3692
|
+
deps
|
|
3693
|
+
}).catch(console.error);
|
|
3694
|
+
} else {
|
|
3695
|
+
renderSingleFileEditTile(fileContainer, initial, state, deps).catch(console.error);
|
|
3696
|
+
}
|
|
3697
|
+
const hiddenInput = document.createElement("input");
|
|
3698
|
+
hiddenInput.type = "hidden";
|
|
3699
|
+
hiddenInput.name = pathKey;
|
|
3700
|
+
hiddenInput.value = initial;
|
|
3701
|
+
fileWrapper.appendChild(hiddenInput);
|
|
3702
|
+
}
|
|
3703
|
+
var UPLOAD_SVG = `<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor" style="flex-shrink:0;color:var(--fb-file-upload-text-color,#9ca3af);">
|
|
3704
|
+
<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"/>
|
|
3705
|
+
</svg>`;
|
|
3706
|
+
function buildEmptyDropzone(state, primaryText, subHint, openPicker) {
|
|
3707
|
+
const dropzone = document.createElement("div");
|
|
3708
|
+
dropzone.className = "fb-file-dropzone";
|
|
3709
|
+
dropzone.innerHTML = `
|
|
3710
|
+
${UPLOAD_SVG}
|
|
3711
|
+
<div class="fb-dropzone-primary-text">${escapeHtml(primaryText)}</div>
|
|
3712
|
+
${subHint ? `<div class="fb-dropzone-hint-text">${escapeHtml(subHint)}</div>` : ""}
|
|
3713
|
+
`;
|
|
3714
|
+
dropzone.onclick = openPicker;
|
|
3715
|
+
return dropzone;
|
|
3716
|
+
}
|
|
3717
|
+
function buildLibraryButton(variant, state, onClick) {
|
|
3718
|
+
const btn = document.createElement("button");
|
|
3719
|
+
btn.type = "button";
|
|
3720
|
+
btn.className = variant === "card" ? "fb-file-library-card" : "fb-tile fb-tile-add-library";
|
|
3721
|
+
if (variant === "card") {
|
|
3722
|
+
btn.innerHTML = `
|
|
3723
|
+
<span class="fb-file-library-card-icon" aria-hidden="true">\u{1F4DA}</span>
|
|
3724
|
+
<span class="fb-file-library-card-label">${escapeHtml(t("fromLibrary", state))}</span>
|
|
3725
|
+
<span class="fb-file-library-card-hint">${escapeHtml(t("libraryHint", state))}</span>
|
|
3726
|
+
`;
|
|
3727
|
+
} else {
|
|
3728
|
+
btn.innerHTML = `<span aria-hidden="true">\u{1F4DA}</span>`;
|
|
3729
|
+
btn.title = t("fromLibrary", state);
|
|
3730
|
+
btn.setAttribute("aria-label", t("fromLibrary", state));
|
|
3731
|
+
}
|
|
3732
|
+
btn.addEventListener("click", onClick);
|
|
3733
|
+
return btn;
|
|
3734
|
+
}
|
|
3735
|
+
function renderResourcePills(opts) {
|
|
3736
|
+
const {
|
|
3737
|
+
container,
|
|
3738
|
+
rids,
|
|
3739
|
+
state,
|
|
3740
|
+
onRemove,
|
|
3741
|
+
hint,
|
|
3742
|
+
countInfo,
|
|
3743
|
+
maxCount,
|
|
3744
|
+
isReadonly = false,
|
|
3745
|
+
onLibraryPick
|
|
3746
|
+
} = opts;
|
|
3747
|
+
ensureFileStyles();
|
|
3748
|
+
const wrapper = container.closest("[data-files-wrapper]");
|
|
3749
|
+
if (wrapper) {
|
|
3750
|
+
wrapper.dataset.resourceIds = JSON.stringify(rids ?? []);
|
|
3751
|
+
}
|
|
3752
|
+
while (container.firstChild) container.removeChild(container.firstChild);
|
|
3753
|
+
const ridList = rids ?? [];
|
|
3754
|
+
const atMax = maxCount !== void 0 && ridList.length >= maxCount;
|
|
3755
|
+
const hasLibrary = !isReadonly && typeof onLibraryPick === "function";
|
|
3756
|
+
const buildSubHint = () => {
|
|
3757
|
+
const parts = [];
|
|
3758
|
+
if (hint) parts.push(hint);
|
|
3759
|
+
if (countInfo) parts.push(countInfo);
|
|
3760
|
+
return parts.join(" \u2022 ");
|
|
3761
|
+
};
|
|
3762
|
+
const openPicker = () => {
|
|
3763
|
+
const picker = findFilePicker(container);
|
|
3764
|
+
if (picker) picker.click();
|
|
3765
|
+
};
|
|
3766
|
+
if (ridList.length === 0) {
|
|
3767
|
+
if (isReadonly) {
|
|
3768
|
+
const emptyEl = document.createElement("div");
|
|
3769
|
+
emptyEl.className = "fb-tile-empty-text";
|
|
3770
|
+
emptyEl.textContent = t("noFilesSelected", state);
|
|
3771
|
+
container.appendChild(emptyEl);
|
|
3772
|
+
} else if (hasLibrary) {
|
|
3773
|
+
const row = document.createElement("div");
|
|
3774
|
+
row.className = "fb-file-card-row";
|
|
3775
|
+
const dropzone = buildEmptyDropzone(
|
|
3776
|
+
state,
|
|
3777
|
+
t("clickDragTextMultiple", state),
|
|
3778
|
+
buildSubHint(),
|
|
3779
|
+
openPicker
|
|
3780
|
+
);
|
|
3781
|
+
const libraryBtn = buildLibraryButton("card", state, onLibraryPick);
|
|
3782
|
+
row.appendChild(dropzone);
|
|
3783
|
+
row.appendChild(libraryBtn);
|
|
3784
|
+
container.appendChild(row);
|
|
3029
3785
|
} else {
|
|
3030
|
-
const
|
|
3031
|
-
|
|
3032
|
-
|
|
3033
|
-
|
|
3786
|
+
const dropzone = buildEmptyDropzone(
|
|
3787
|
+
state,
|
|
3788
|
+
t("clickDragTextMultiple", state),
|
|
3789
|
+
buildSubHint(),
|
|
3790
|
+
openPicker
|
|
3791
|
+
);
|
|
3792
|
+
container.appendChild(dropzone);
|
|
3034
3793
|
}
|
|
3035
|
-
|
|
3036
|
-
|
|
3037
|
-
|
|
3038
|
-
|
|
3039
|
-
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
const
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
3794
|
+
return;
|
|
3795
|
+
}
|
|
3796
|
+
const tilesWrap = document.createElement("div");
|
|
3797
|
+
tilesWrap.className = "fb-tiles-wrap";
|
|
3798
|
+
tilesWrap.style.cssText = "display:flex;flex-wrap:wrap;gap:6px;align-items:flex-start;";
|
|
3799
|
+
for (const rid of ridList) {
|
|
3800
|
+
const meta = state.resourceIndex.get(rid);
|
|
3801
|
+
const tile = createFileTile();
|
|
3802
|
+
tile.classList.add("fb-tile-resource", "resource-pill");
|
|
3803
|
+
tile.dataset.resourceId = rid;
|
|
3804
|
+
const actionsEl = createTileActions({
|
|
3805
|
+
canRemove: !isReadonly && onRemove !== null,
|
|
3806
|
+
removeHandler: onRemove ? () => onRemove(rid) : null,
|
|
3807
|
+
state,
|
|
3808
|
+
resourceId: rid,
|
|
3809
|
+
fileName: meta?.name ?? ""
|
|
3810
|
+
});
|
|
3811
|
+
fillTileContent(tile, rid, meta, state, actionsEl).catch((err) => {
|
|
3812
|
+
console.error("Failed to render tile:", err);
|
|
3813
|
+
});
|
|
3814
|
+
tilesWrap.appendChild(tile);
|
|
3815
|
+
}
|
|
3816
|
+
if (!isReadonly && !atMax) {
|
|
3817
|
+
const addTile = document.createElement("div");
|
|
3818
|
+
addTile.className = "fb-tile fb-tile-add";
|
|
3819
|
+
addTile.innerHTML = "+";
|
|
3820
|
+
addTile.onclick = openPicker;
|
|
3821
|
+
tilesWrap.appendChild(addTile);
|
|
3822
|
+
if (hasLibrary) {
|
|
3823
|
+
const libraryTile = buildLibraryButton("tile", state, onLibraryPick);
|
|
3824
|
+
tilesWrap.appendChild(libraryTile);
|
|
3825
|
+
}
|
|
3826
|
+
} else if (!isReadonly && atMax) {
|
|
3827
|
+
const chip = document.createElement("div");
|
|
3828
|
+
chip.className = "fb-tile-counter";
|
|
3829
|
+
chip.textContent = t("filesCounter", state, {
|
|
3830
|
+
count: ridList.length,
|
|
3831
|
+
max: maxCount
|
|
3832
|
+
});
|
|
3833
|
+
tilesWrap.appendChild(chip);
|
|
3834
|
+
}
|
|
3835
|
+
container.appendChild(tilesWrap);
|
|
3836
|
+
const subHint = buildSubHint();
|
|
3837
|
+
if (subHint) {
|
|
3838
|
+
const hintEl = document.createElement("div");
|
|
3839
|
+
hintEl.className = "fb-tile-hint";
|
|
3840
|
+
hintEl.textContent = subHint;
|
|
3841
|
+
container.appendChild(hintEl);
|
|
3842
|
+
}
|
|
3843
|
+
}
|
|
3844
|
+
function renderFileElementEdit(element, ctx, wrapper, pathKey) {
|
|
3845
|
+
const state = ctx.state;
|
|
3846
|
+
const fileWrapper = document.createElement("div");
|
|
3847
|
+
fileWrapper.className = "space-y-2";
|
|
3848
|
+
const picker = document.createElement("input");
|
|
3849
|
+
picker.type = "file";
|
|
3850
|
+
picker.name = pathKey;
|
|
3851
|
+
picker.style.display = "none";
|
|
3852
|
+
if (element.accept) {
|
|
3853
|
+
picker.accept = typeof element.accept === "string" ? element.accept : [
|
|
3854
|
+
...element.accept.extensions?.map((ext) => `.${ext}`) ?? [],
|
|
3855
|
+
...element.accept.mime ?? []
|
|
3856
|
+
].join(",") || "";
|
|
3857
|
+
}
|
|
3858
|
+
const fileContainer = document.createElement("div");
|
|
3859
|
+
fileContainer.className = "file-preview-container";
|
|
3860
|
+
const initial = ctx.prefill[element.key];
|
|
3861
|
+
const allowedExts = getAllowedExtensions(element.accept);
|
|
3862
|
+
const allowedMimes = getAllowedMimes(element.accept);
|
|
3863
|
+
const maxSizeMB = element.maxSize ?? Infinity;
|
|
3864
|
+
const handlers = {
|
|
3865
|
+
fileUploadHandler() {
|
|
3866
|
+
picker.click();
|
|
3867
|
+
},
|
|
3868
|
+
dragHandler(files) {
|
|
3052
3869
|
if (files.length > 0) {
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
pathKey,
|
|
3870
|
+
handleFileSelect({
|
|
3871
|
+
file: files[0],
|
|
3872
|
+
container: fileContainer,
|
|
3873
|
+
fieldName: pathKey,
|
|
3058
3874
|
state,
|
|
3059
|
-
deps,
|
|
3060
|
-
ctx.instance,
|
|
3061
|
-
allowedExts,
|
|
3875
|
+
deps: buildDeps(),
|
|
3876
|
+
instance: ctx.instance,
|
|
3877
|
+
allowedExtensions: allowedExts,
|
|
3878
|
+
allowedMimes,
|
|
3062
3879
|
maxSizeMB
|
|
3063
|
-
);
|
|
3880
|
+
});
|
|
3064
3881
|
}
|
|
3065
|
-
}
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
pathKey,
|
|
3071
|
-
fileWrapper,
|
|
3072
|
-
state,
|
|
3073
|
-
{
|
|
3074
|
-
picker,
|
|
3075
|
-
fileUploadHandler,
|
|
3076
|
-
dragHandler
|
|
3077
|
-
}
|
|
3078
|
-
);
|
|
3079
|
-
} else {
|
|
3882
|
+
},
|
|
3883
|
+
setupDrop(container) {
|
|
3884
|
+
setupDragAndDrop(container, handlers.dragHandler);
|
|
3885
|
+
},
|
|
3886
|
+
restoreDropzone() {
|
|
3080
3887
|
const hint = makeFieldHint(element, state);
|
|
3888
|
+
fileContainer.className = "file-preview-container w-full max-w-md bg-gray-100 rounded-lg overflow-hidden relative group cursor-pointer";
|
|
3889
|
+
fileContainer.style.height = "128px";
|
|
3081
3890
|
setEmptyFileContainer(fileContainer, state, hint);
|
|
3891
|
+
fileContainer.onclick = handlers.fileUploadHandler;
|
|
3892
|
+
setupDragAndDrop(fileContainer, handlers.dragHandler);
|
|
3893
|
+
},
|
|
3894
|
+
onRemove() {
|
|
3895
|
+
const hiddenInput = fileWrapper.querySelector('input[type="hidden"]');
|
|
3896
|
+
const currentRid = hiddenInput?.value;
|
|
3897
|
+
if (currentRid) {
|
|
3898
|
+
releaseLocalFileUrl(state.resourceIndex.get(currentRid)?.file);
|
|
3899
|
+
}
|
|
3900
|
+
if (hiddenInput) hiddenInput.value = "";
|
|
3901
|
+
handlers.restoreDropzone();
|
|
3082
3902
|
}
|
|
3083
|
-
|
|
3084
|
-
|
|
3085
|
-
picker
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
|
|
3903
|
+
};
|
|
3904
|
+
const buildDeps = () => ({
|
|
3905
|
+
picker,
|
|
3906
|
+
fileUploadHandler: handlers.fileUploadHandler,
|
|
3907
|
+
dragHandler: handlers.dragHandler,
|
|
3908
|
+
setupDrop: handlers.setupDrop,
|
|
3909
|
+
onRemove: handlers.onRemove
|
|
3910
|
+
});
|
|
3911
|
+
const renderEmptySingleState = () => {
|
|
3912
|
+
if (state.config.pickExistingFiles && !element.disableLibrary) {
|
|
3913
|
+
fileContainer.className = "file-preview-container";
|
|
3914
|
+
fileContainer.removeAttribute("style");
|
|
3915
|
+
fileContainer.onclick = null;
|
|
3916
|
+
const row = document.createElement("div");
|
|
3917
|
+
row.className = "fb-file-card-row";
|
|
3918
|
+
row.style.cssText = "display:flex;gap:8px;align-items:stretch;";
|
|
3919
|
+
const hint = makeFieldHint(element, state);
|
|
3920
|
+
const uploadCard = buildEmptyDropzone(
|
|
3921
|
+
state,
|
|
3922
|
+
t("clickDragText", state),
|
|
3923
|
+
hint,
|
|
3924
|
+
handlers.fileUploadHandler
|
|
3925
|
+
);
|
|
3926
|
+
uploadCard.style.cssText = "flex:1;min-width:0;height:128px;";
|
|
3927
|
+
setupDragAndDrop(uploadCard, handlers.dragHandler);
|
|
3928
|
+
const libraryBtn = buildLibraryButton("card", state, () => {
|
|
3929
|
+
handleLibraryPickSingle(
|
|
3930
|
+
state,
|
|
3931
|
+
element,
|
|
3090
3932
|
fileContainer,
|
|
3933
|
+
fileWrapper,
|
|
3091
3934
|
pathKey,
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
)
|
|
3098
|
-
|
|
3099
|
-
};
|
|
3100
|
-
fileWrapper.appendChild(fileContainer);
|
|
3101
|
-
fileWrapper.appendChild(picker);
|
|
3102
|
-
wrapper.appendChild(fileWrapper);
|
|
3103
|
-
}
|
|
3104
|
-
}
|
|
3105
|
-
function renderFilesElement(element, ctx, wrapper, pathKey) {
|
|
3106
|
-
const state = ctx.state;
|
|
3107
|
-
if (isElementReadonly(element, state, ctx)) {
|
|
3108
|
-
const rawPrefill = ctx.prefill[element.key];
|
|
3109
|
-
const initialFiles = Array.isArray(rawPrefill) ? rawPrefill : [];
|
|
3110
|
-
const filesWrapper = document.createElement("div");
|
|
3111
|
-
filesWrapper.className = "space-y-2";
|
|
3112
|
-
filesWrapper.dataset.filesWrapper = pathKey;
|
|
3113
|
-
const filesList = document.createElement("div");
|
|
3114
|
-
filesList.className = "files-list";
|
|
3115
|
-
initialFiles.forEach((resourceId) => {
|
|
3116
|
-
const pill = document.createElement("div");
|
|
3117
|
-
pill.className = "resource-pill";
|
|
3118
|
-
pill.dataset.resourceId = resourceId;
|
|
3119
|
-
filesList.appendChild(pill);
|
|
3120
|
-
});
|
|
3121
|
-
filesWrapper.appendChild(filesList);
|
|
3122
|
-
wrapper.appendChild(filesWrapper);
|
|
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);
|
|
3935
|
+
pathKey,
|
|
3936
|
+
async (rid) => {
|
|
3937
|
+
await renderSingleFileEditTile(fileContainer, rid, state, buildDeps());
|
|
3938
|
+
},
|
|
3939
|
+
ctx.instance
|
|
3940
|
+
).catch((err) => {
|
|
3941
|
+
console.error("Library pick failed:", err);
|
|
3131
3942
|
});
|
|
3132
3943
|
});
|
|
3944
|
+
libraryBtn.style.cssText = "flex:1;min-width:0;";
|
|
3945
|
+
row.appendChild(uploadCard);
|
|
3946
|
+
row.appendChild(libraryBtn);
|
|
3947
|
+
fileContainer.appendChild(row);
|
|
3133
3948
|
} else {
|
|
3134
|
-
|
|
3949
|
+
handlers.restoreDropzone();
|
|
3950
|
+
}
|
|
3951
|
+
};
|
|
3952
|
+
if (initial) {
|
|
3953
|
+
handleInitialFileData(
|
|
3954
|
+
initial,
|
|
3955
|
+
fileContainer,
|
|
3956
|
+
pathKey,
|
|
3957
|
+
fileWrapper,
|
|
3958
|
+
state,
|
|
3959
|
+
buildDeps()
|
|
3960
|
+
);
|
|
3961
|
+
const prefillMeta = state.resourceIndex.get(initial);
|
|
3962
|
+
if (prefillMeta?.type?.startsWith("video/")) {
|
|
3963
|
+
fileContainer.onclick = handlers.fileUploadHandler;
|
|
3964
|
+
setupDragAndDrop(fileContainer, handlers.dragHandler);
|
|
3135
3965
|
}
|
|
3136
|
-
wrapper.appendChild(resultsWrapper);
|
|
3137
3966
|
} else {
|
|
3138
|
-
|
|
3139
|
-
|
|
3140
|
-
|
|
3141
|
-
|
|
3967
|
+
renderEmptySingleState();
|
|
3968
|
+
}
|
|
3969
|
+
picker.onchange = () => {
|
|
3970
|
+
if (picker.files && picker.files.length > 0) {
|
|
3971
|
+
handleFileSelect({
|
|
3972
|
+
file: picker.files[0],
|
|
3973
|
+
container: fileContainer,
|
|
3974
|
+
fieldName: pathKey,
|
|
3142
3975
|
state,
|
|
3143
|
-
(
|
|
3144
|
-
|
|
3145
|
-
|
|
3146
|
-
|
|
3147
|
-
|
|
3148
|
-
|
|
3149
|
-
|
|
3150
|
-
|
|
3151
|
-
|
|
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
|
-
|
|
3976
|
+
deps: buildDeps(),
|
|
3977
|
+
instance: ctx.instance,
|
|
3978
|
+
allowedExtensions: allowedExts,
|
|
3979
|
+
allowedMimes,
|
|
3980
|
+
maxSizeMB
|
|
3981
|
+
});
|
|
3982
|
+
}
|
|
3983
|
+
};
|
|
3984
|
+
fileWrapper.appendChild(fileContainer);
|
|
3985
|
+
fileWrapper.appendChild(picker);
|
|
3986
|
+
wrapper.appendChild(fileWrapper);
|
|
3987
|
+
}
|
|
3988
|
+
function renderFilesElementEdit(element, ctx, wrapper, pathKey) {
|
|
3989
|
+
const state = ctx.state;
|
|
3990
|
+
const filesWrapper = document.createElement("div");
|
|
3991
|
+
filesWrapper.className = "space-y-2";
|
|
3992
|
+
filesWrapper.dataset.filesWrapper = pathKey;
|
|
3993
|
+
const filesPicker = document.createElement("input");
|
|
3994
|
+
filesPicker.type = "file";
|
|
3995
|
+
filesPicker.name = pathKey;
|
|
3996
|
+
filesPicker.multiple = true;
|
|
3997
|
+
filesPicker.style.display = "none";
|
|
3998
|
+
if (element.accept) {
|
|
3999
|
+
filesPicker.accept = typeof element.accept === "string" ? element.accept : [
|
|
4000
|
+
...element.accept.extensions?.map((ext) => `.${ext}`) ?? [],
|
|
4001
|
+
...element.accept.mime ?? []
|
|
4002
|
+
].join(",") || "";
|
|
4003
|
+
}
|
|
4004
|
+
const filesContainer = document.createElement("div");
|
|
4005
|
+
filesContainer.className = "files-list-wrapper";
|
|
4006
|
+
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);";
|
|
4007
|
+
const list = document.createElement("div");
|
|
4008
|
+
list.className = "files-list";
|
|
4009
|
+
const initialFiles = ctx.prefill[element.key] || [];
|
|
4010
|
+
addPrefillFilesToIndex(initialFiles, state.resourceIndex);
|
|
4011
|
+
filesWrapper.dataset.resourceIds = JSON.stringify(initialFiles);
|
|
4012
|
+
const filesFieldHint = makeFieldHint(element, state);
|
|
4013
|
+
const filesConstraints = {
|
|
4014
|
+
maxCount: Infinity,
|
|
4015
|
+
allowedExtensions: getAllowedExtensions(element.accept),
|
|
4016
|
+
allowedMimes: getAllowedMimes(element.accept),
|
|
4017
|
+
maxSize: element.maxSize ?? Infinity
|
|
4018
|
+
};
|
|
4019
|
+
filesContainer.appendChild(list);
|
|
4020
|
+
filesWrapper.appendChild(filesPicker);
|
|
4021
|
+
filesWrapper.appendChild(filesContainer);
|
|
4022
|
+
wrapper.appendChild(filesWrapper);
|
|
4023
|
+
const onLibraryPickFiles = state.config.pickExistingFiles && !element.disableLibrary ? () => {
|
|
4024
|
+
handleLibraryPickMulti(
|
|
3180
4025
|
state,
|
|
3181
|
-
|
|
3182
|
-
|
|
4026
|
+
element,
|
|
4027
|
+
filesWrapper,
|
|
3183
4028
|
pathKey,
|
|
3184
|
-
ctx.instance
|
|
3185
|
-
);
|
|
3186
|
-
setupFilesPickerHandler(
|
|
3187
|
-
filesPicker,
|
|
3188
4029
|
initialFiles,
|
|
3189
|
-
|
|
3190
|
-
|
|
3191
|
-
filesConstraints,
|
|
3192
|
-
pathKey,
|
|
4030
|
+
Infinity,
|
|
4031
|
+
updateFilesList,
|
|
3193
4032
|
ctx.instance
|
|
3194
|
-
)
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
|
|
4033
|
+
).catch((err) => {
|
|
4034
|
+
console.error("Library pick failed:", err);
|
|
4035
|
+
});
|
|
4036
|
+
} : null;
|
|
4037
|
+
function updateFilesList() {
|
|
4038
|
+
const currentlyReadonly = isElementReadonly(element, state);
|
|
4039
|
+
renderResourcePills({
|
|
4040
|
+
container: list,
|
|
4041
|
+
rids: initialFiles,
|
|
4042
|
+
state,
|
|
4043
|
+
onRemove: currentlyReadonly ? null : (ridToRemove) => {
|
|
4044
|
+
releaseLocalFileUrl(state.resourceIndex.get(ridToRemove)?.file);
|
|
4045
|
+
const index = initialFiles.indexOf(ridToRemove);
|
|
4046
|
+
if (index > -1) initialFiles.splice(index, 1);
|
|
4047
|
+
updateFilesList();
|
|
4048
|
+
},
|
|
4049
|
+
hint: filesFieldHint,
|
|
4050
|
+
isReadonly: currentlyReadonly,
|
|
4051
|
+
onLibraryPick: currentlyReadonly ? null : onLibraryPickFiles
|
|
4052
|
+
});
|
|
3199
4053
|
}
|
|
4054
|
+
updateFilesList();
|
|
4055
|
+
setupFilesDropHandler(
|
|
4056
|
+
filesContainer,
|
|
4057
|
+
initialFiles,
|
|
4058
|
+
state,
|
|
4059
|
+
updateFilesList,
|
|
4060
|
+
filesConstraints,
|
|
4061
|
+
pathKey,
|
|
4062
|
+
ctx.instance
|
|
4063
|
+
);
|
|
4064
|
+
setupFilesPickerHandler(
|
|
4065
|
+
filesPicker,
|
|
4066
|
+
initialFiles,
|
|
4067
|
+
state,
|
|
4068
|
+
updateFilesList,
|
|
4069
|
+
filesConstraints,
|
|
4070
|
+
pathKey,
|
|
4071
|
+
ctx.instance
|
|
4072
|
+
);
|
|
3200
4073
|
}
|
|
3201
|
-
function
|
|
4074
|
+
function renderMultipleFileElementEdit(element, ctx, wrapper, pathKey) {
|
|
3202
4075
|
const state = ctx.state;
|
|
3203
4076
|
const minFiles = element.minCount ?? 0;
|
|
3204
4077
|
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
|
-
|
|
3243
|
-
|
|
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,
|
|
3279
|
-
initialFiles,
|
|
4078
|
+
const filesWrapper = document.createElement("div");
|
|
4079
|
+
filesWrapper.className = "space-y-2";
|
|
4080
|
+
filesWrapper.dataset.filesWrapper = pathKey;
|
|
4081
|
+
const filesPicker = document.createElement("input");
|
|
4082
|
+
filesPicker.type = "file";
|
|
4083
|
+
filesPicker.name = pathKey;
|
|
4084
|
+
filesPicker.multiple = true;
|
|
4085
|
+
filesPicker.style.display = "none";
|
|
4086
|
+
if (element.accept) {
|
|
4087
|
+
filesPicker.accept = typeof element.accept === "string" ? element.accept : [
|
|
4088
|
+
...element.accept.extensions?.map((ext) => `.${ext}`) ?? [],
|
|
4089
|
+
...element.accept.mime ?? []
|
|
4090
|
+
].join(",") || "";
|
|
4091
|
+
}
|
|
4092
|
+
const filesContainer = document.createElement("div");
|
|
4093
|
+
filesContainer.className = "files-list-wrapper";
|
|
4094
|
+
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);";
|
|
4095
|
+
const list = document.createElement("div");
|
|
4096
|
+
list.className = "files-list";
|
|
4097
|
+
filesWrapper.appendChild(filesPicker);
|
|
4098
|
+
filesWrapper.appendChild(filesContainer);
|
|
4099
|
+
filesContainer.appendChild(list);
|
|
4100
|
+
const initialFiles = Array.isArray(ctx.prefill[element.key]) ? [...ctx.prefill[element.key]] : [];
|
|
4101
|
+
addPrefillFilesToIndex(initialFiles, state.resourceIndex);
|
|
4102
|
+
filesWrapper.dataset.resourceIds = JSON.stringify(initialFiles);
|
|
4103
|
+
const multipleFilesHint = makeFieldHint(element, state);
|
|
4104
|
+
const multipleConstraints = {
|
|
4105
|
+
maxCount: maxFiles,
|
|
4106
|
+
allowedExtensions: getAllowedExtensions(element.accept),
|
|
4107
|
+
allowedMimes: getAllowedMimes(element.accept),
|
|
4108
|
+
maxSize: element.maxSize ?? Infinity
|
|
4109
|
+
};
|
|
4110
|
+
const buildCountInfo = () => {
|
|
4111
|
+
const countText = initialFiles.length === 1 ? t("fileCountSingle", state, { count: initialFiles.length }) : t("fileCountPlural", state, { count: initialFiles.length });
|
|
4112
|
+
const minMaxText = minFiles > 0 || maxFiles < Infinity ? ` ${t("fileCountRange", state, { min: minFiles, max: maxFiles })}` : "";
|
|
4113
|
+
return countText + minMaxText;
|
|
4114
|
+
};
|
|
4115
|
+
const onLibraryPickMultiple = state.config.pickExistingFiles && !element.disableLibrary ? () => {
|
|
4116
|
+
handleLibraryPickMulti(
|
|
3280
4117
|
state,
|
|
3281
|
-
|
|
3282
|
-
|
|
4118
|
+
element,
|
|
4119
|
+
filesWrapper,
|
|
3283
4120
|
pathKey,
|
|
3284
|
-
ctx.instance
|
|
3285
|
-
);
|
|
3286
|
-
setupFilesPickerHandler(
|
|
3287
|
-
filesPicker,
|
|
3288
4121
|
initialFiles,
|
|
3289
|
-
|
|
4122
|
+
maxFiles,
|
|
3290
4123
|
updateFilesDisplay,
|
|
3291
|
-
multipleConstraints,
|
|
3292
|
-
pathKey,
|
|
3293
4124
|
ctx.instance
|
|
4125
|
+
).catch((err) => {
|
|
4126
|
+
console.error("Library pick failed:", err);
|
|
4127
|
+
});
|
|
4128
|
+
} : null;
|
|
4129
|
+
const updateFilesDisplay = () => {
|
|
4130
|
+
const currentlyReadonly = isElementReadonly(element, state);
|
|
4131
|
+
renderResourcePills({
|
|
4132
|
+
container: list,
|
|
4133
|
+
rids: initialFiles,
|
|
4134
|
+
state,
|
|
4135
|
+
onRemove: currentlyReadonly ? null : (index) => {
|
|
4136
|
+
releaseLocalFileUrl(state.resourceIndex.get(index)?.file);
|
|
4137
|
+
initialFiles.splice(initialFiles.indexOf(index), 1);
|
|
4138
|
+
updateFilesDisplay();
|
|
4139
|
+
},
|
|
4140
|
+
hint: multipleFilesHint,
|
|
4141
|
+
countInfo: buildCountInfo(),
|
|
4142
|
+
maxCount: maxFiles < Infinity ? maxFiles : void 0,
|
|
4143
|
+
isReadonly: currentlyReadonly,
|
|
4144
|
+
onLibraryPick: currentlyReadonly ? null : onLibraryPickMultiple
|
|
4145
|
+
});
|
|
4146
|
+
};
|
|
4147
|
+
setupFilesDropHandler(
|
|
4148
|
+
filesContainer,
|
|
4149
|
+
initialFiles,
|
|
4150
|
+
state,
|
|
4151
|
+
updateFilesDisplay,
|
|
4152
|
+
multipleConstraints,
|
|
4153
|
+
pathKey,
|
|
4154
|
+
ctx.instance
|
|
4155
|
+
);
|
|
4156
|
+
setupFilesPickerHandler(
|
|
4157
|
+
filesPicker,
|
|
4158
|
+
initialFiles,
|
|
4159
|
+
state,
|
|
4160
|
+
updateFilesDisplay,
|
|
4161
|
+
multipleConstraints,
|
|
4162
|
+
pathKey,
|
|
4163
|
+
ctx.instance
|
|
4164
|
+
);
|
|
4165
|
+
updateFilesDisplay();
|
|
4166
|
+
wrapper.appendChild(filesWrapper);
|
|
4167
|
+
}
|
|
4168
|
+
|
|
4169
|
+
// src/components/file/validate.ts
|
|
4170
|
+
function readMultiFileResourceIds(scopeRoot, fullKey) {
|
|
4171
|
+
const wrapper = scopeRoot.querySelector(
|
|
4172
|
+
`[data-files-wrapper="${fullKey}"]`
|
|
4173
|
+
);
|
|
4174
|
+
if (!wrapper) return [];
|
|
4175
|
+
const encoded = wrapper.dataset.resourceIds;
|
|
4176
|
+
if (encoded === void 0) {
|
|
4177
|
+
throw new Error(
|
|
4178
|
+
`readMultiFileResourceIds: [data-files-wrapper="${fullKey}"] is missing data-resource-ids attribute. This is a render bug.`
|
|
4179
|
+
);
|
|
4180
|
+
}
|
|
4181
|
+
const parsed = JSON.parse(encoded);
|
|
4182
|
+
if (!Array.isArray(parsed)) {
|
|
4183
|
+
throw new Error(
|
|
4184
|
+
`readMultiFileResourceIds: data-resource-ids on [data-files-wrapper="${fullKey}"] is not a JSON array. Got: ${encoded}`
|
|
3294
4185
|
);
|
|
3295
|
-
updateFilesDisplay();
|
|
3296
|
-
wrapper.appendChild(filesWrapper);
|
|
3297
4186
|
}
|
|
4187
|
+
return parsed;
|
|
3298
4188
|
}
|
|
3299
|
-
function
|
|
3300
|
-
const
|
|
3301
|
-
const
|
|
3302
|
-
|
|
3303
|
-
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
|
|
3307
|
-
|
|
3308
|
-
|
|
3309
|
-
|
|
3310
|
-
|
|
3311
|
-
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
|
|
4189
|
+
function validateFileCount(key, resourceIds, element, state, errors) {
|
|
4190
|
+
const minFiles = "minCount" in element ? element.minCount ?? 0 : 0;
|
|
4191
|
+
const maxFiles = "maxCount" in element ? element.maxCount ?? Infinity : Infinity;
|
|
4192
|
+
if (element.required && resourceIds.length === 0) {
|
|
4193
|
+
errors.push(`${key}: ${t("required", state)}`);
|
|
4194
|
+
}
|
|
4195
|
+
if (resourceIds.length < minFiles) {
|
|
4196
|
+
errors.push(`${key}: ${t("minFiles", state, { min: minFiles })}`);
|
|
4197
|
+
}
|
|
4198
|
+
if (resourceIds.length > maxFiles) {
|
|
4199
|
+
errors.push(`${key}: ${t("maxFiles", state, { max: maxFiles })}`);
|
|
4200
|
+
}
|
|
4201
|
+
}
|
|
4202
|
+
function validateFileTypes(key, resourceIds, element, state, errors) {
|
|
4203
|
+
const acceptField = "accept" in element ? element.accept : void 0;
|
|
4204
|
+
const allowedExtensions = getAllowedExtensions(acceptField);
|
|
4205
|
+
const allowedMimes = getAllowedMimes(acceptField);
|
|
4206
|
+
if (allowedExtensions.length === 0 && allowedMimes.length === 0) return;
|
|
4207
|
+
const formats = allowedExtensions.join(", ");
|
|
4208
|
+
const mimes = allowedMimes.join(", ");
|
|
4209
|
+
for (const rid of resourceIds) {
|
|
4210
|
+
const meta = state.resourceIndex.get(rid);
|
|
4211
|
+
const fileName = meta?.name ?? rid;
|
|
4212
|
+
if (allowedExtensions.length > 0 && !isFileExtensionAllowed(fileName, allowedExtensions)) {
|
|
4213
|
+
errors.push(
|
|
4214
|
+
`${key}: ${t("invalidFileExtension", state, { name: fileName, formats })}`
|
|
4215
|
+
);
|
|
4216
|
+
continue;
|
|
3316
4217
|
}
|
|
3317
|
-
|
|
3318
|
-
|
|
3319
|
-
|
|
3320
|
-
const { state } = context;
|
|
3321
|
-
const acceptField = "accept" in element2 ? element2.accept : void 0;
|
|
3322
|
-
const allowedExtensions = getAllowedExtensions(acceptField);
|
|
3323
|
-
if (allowedExtensions.length === 0) return;
|
|
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)) {
|
|
4218
|
+
if (allowedMimes.length > 0 && !meta?.inferredFromExtension) {
|
|
4219
|
+
const mimeType = meta?.type ?? "";
|
|
4220
|
+
if (!isMimeAllowed(mimeType, allowedMimes)) {
|
|
3329
4221
|
errors.push(
|
|
3330
|
-
`${
|
|
4222
|
+
`${key}: ${t("invalidFileMime", state, { name: fileName, type: mimeType, mimes })}`
|
|
3331
4223
|
);
|
|
3332
4224
|
}
|
|
3333
4225
|
}
|
|
3334
|
-
}
|
|
3335
|
-
|
|
3336
|
-
|
|
3337
|
-
|
|
3338
|
-
|
|
3339
|
-
|
|
3340
|
-
|
|
3341
|
-
|
|
3342
|
-
|
|
3343
|
-
|
|
3344
|
-
|
|
3345
|
-
|
|
3346
|
-
);
|
|
3347
|
-
}
|
|
4226
|
+
}
|
|
4227
|
+
}
|
|
4228
|
+
function validateFileSizes(key, resourceIds, element, state, errors) {
|
|
4229
|
+
const maxSizeMB = "maxSize" in element ? element.maxSize ?? Infinity : Infinity;
|
|
4230
|
+
if (maxSizeMB === Infinity) return;
|
|
4231
|
+
for (const rid of resourceIds) {
|
|
4232
|
+
const meta = state.resourceIndex.get(rid);
|
|
4233
|
+
if (!meta) continue;
|
|
4234
|
+
if (meta.size > maxSizeMB * 1024 * 1024) {
|
|
4235
|
+
errors.push(
|
|
4236
|
+
`${key}: ${t("fileTooLarge", state, { name: meta.name, maxSize: maxSizeMB })}`
|
|
4237
|
+
);
|
|
3348
4238
|
}
|
|
3349
|
-
}
|
|
4239
|
+
}
|
|
4240
|
+
}
|
|
4241
|
+
function validateMultiFile(element, key, context) {
|
|
4242
|
+
const { scopeRoot, skipValidation, path, state } = context;
|
|
4243
|
+
const errors = [];
|
|
4244
|
+
const fullKey = pathJoin(path, key);
|
|
4245
|
+
const resourceIds = readMultiFileResourceIds(scopeRoot, fullKey);
|
|
4246
|
+
if (!skipValidation) {
|
|
4247
|
+
validateFileCount(key, resourceIds, element, state, errors);
|
|
4248
|
+
validateFileTypes(key, resourceIds, element, state, errors);
|
|
4249
|
+
validateFileSizes(key, resourceIds, element, state, errors);
|
|
4250
|
+
}
|
|
4251
|
+
return { value: resourceIds, errors };
|
|
4252
|
+
}
|
|
4253
|
+
function validateSingleFile(element, key, context) {
|
|
4254
|
+
const { scopeRoot, skipValidation, state } = context;
|
|
4255
|
+
const errors = [];
|
|
4256
|
+
const input = scopeRoot.querySelector(
|
|
4257
|
+
`input[name$="${key}"][type="hidden"]`
|
|
4258
|
+
);
|
|
4259
|
+
const rid = input?.value ?? "";
|
|
4260
|
+
if (!skipValidation && element.required && rid === "") {
|
|
4261
|
+
errors.push(`${key}: ${t("required", state)}`);
|
|
4262
|
+
return { value: null, errors };
|
|
4263
|
+
}
|
|
4264
|
+
if (!skipValidation && rid !== "") {
|
|
4265
|
+
validateFileTypes(key, [rid], element, state, errors);
|
|
4266
|
+
validateFileSizes(key, [rid], element, state, errors);
|
|
4267
|
+
}
|
|
4268
|
+
return { value: rid || null, errors };
|
|
4269
|
+
}
|
|
4270
|
+
function validateFileElement(element, key, context) {
|
|
4271
|
+
const isMultipleField = element.type === "files" || "multiple" in element && Boolean(element.multiple);
|
|
3350
4272
|
if (isMultipleField) {
|
|
3351
|
-
|
|
3352
|
-
|
|
3353
|
-
|
|
3354
|
-
|
|
3355
|
-
|
|
3356
|
-
|
|
3357
|
-
|
|
3358
|
-
|
|
3359
|
-
|
|
3360
|
-
|
|
3361
|
-
|
|
3362
|
-
|
|
3363
|
-
|
|
3364
|
-
|
|
3365
|
-
|
|
3366
|
-
|
|
3367
|
-
|
|
3368
|
-
|
|
3369
|
-
|
|
4273
|
+
return validateMultiFile(element, key, context);
|
|
4274
|
+
}
|
|
4275
|
+
return validateSingleFile(element, key, context);
|
|
4276
|
+
}
|
|
4277
|
+
|
|
4278
|
+
// src/components/file/render-readonly.ts
|
|
4279
|
+
function renderFileElementReadonly(element, ctx, wrapper, pathKey) {
|
|
4280
|
+
const state = ctx.state;
|
|
4281
|
+
const rawInitial = ctx.prefill[element.key];
|
|
4282
|
+
const initial = typeof rawInitial === "string" ? rawInitial : "";
|
|
4283
|
+
if (initial) {
|
|
4284
|
+
addPrefillFilesToIndex([initial], state.resourceIndex);
|
|
4285
|
+
const hiddenInput = document.createElement("input");
|
|
4286
|
+
hiddenInput.type = "hidden";
|
|
4287
|
+
hiddenInput.name = pathKey;
|
|
4288
|
+
hiddenInput.value = initial;
|
|
4289
|
+
wrapper.appendChild(hiddenInput);
|
|
4290
|
+
renderFilePreviewReadonly(initial, state).then((filePreview) => {
|
|
4291
|
+
wrapper.appendChild(filePreview);
|
|
4292
|
+
}).catch((err) => {
|
|
4293
|
+
console.error("Failed to render file preview:", err);
|
|
4294
|
+
wrapper.appendChild(buildEmptyReadonlyTile(state));
|
|
4295
|
+
});
|
|
3370
4296
|
} else {
|
|
3371
|
-
|
|
3372
|
-
|
|
3373
|
-
|
|
3374
|
-
|
|
3375
|
-
|
|
3376
|
-
|
|
3377
|
-
|
|
3378
|
-
}
|
|
3379
|
-
|
|
3380
|
-
|
|
3381
|
-
|
|
3382
|
-
|
|
3383
|
-
|
|
4297
|
+
wrapper.appendChild(buildEmptyReadonlyTile(state));
|
|
4298
|
+
}
|
|
4299
|
+
}
|
|
4300
|
+
function buildEmptyReadonlyTile(state) {
|
|
4301
|
+
const emptyState = document.createElement("div");
|
|
4302
|
+
emptyState.style.cssText = `
|
|
4303
|
+
width:${TILE_SIZE};
|
|
4304
|
+
height:${TILE_SIZE};
|
|
4305
|
+
display:flex;
|
|
4306
|
+
align-items:center;
|
|
4307
|
+
justify-content:center;
|
|
4308
|
+
background:var(--fb-file-upload-bg-color,#f3f4f6);
|
|
4309
|
+
border-radius:var(--fb-border-radius,0.5rem);
|
|
4310
|
+
border:1px solid var(--fb-file-upload-border-color,#d1d5db);
|
|
4311
|
+
`;
|
|
4312
|
+
emptyState.innerHTML = `<div style="font-size:11px;text-align:center;color:var(--fb-text-secondary-color,#6b7280);">${escapeHtml(t("noFileSelected", state))}</div>`;
|
|
4313
|
+
return emptyState;
|
|
4314
|
+
}
|
|
4315
|
+
function renderMultiFileReadonly(rids, state, wrapper, pathKey, marginTop) {
|
|
4316
|
+
addPrefillFilesToIndex(rids, state.resourceIndex);
|
|
4317
|
+
const filesWrapper = document.createElement("div");
|
|
4318
|
+
filesWrapper.dataset.filesWrapper = pathKey;
|
|
4319
|
+
filesWrapper.dataset.resourceIds = JSON.stringify(rids);
|
|
4320
|
+
wrapper.appendChild(filesWrapper);
|
|
4321
|
+
if (rids.length === 0) {
|
|
4322
|
+
const emptyEl = document.createElement("div");
|
|
4323
|
+
emptyEl.className = "fb-tile-empty-text";
|
|
4324
|
+
emptyEl.textContent = t("noFilesSelected", state);
|
|
4325
|
+
filesWrapper.appendChild(emptyEl);
|
|
4326
|
+
return;
|
|
4327
|
+
}
|
|
4328
|
+
const tilesWrap = document.createElement("div");
|
|
4329
|
+
tilesWrap.style.cssText = `display:flex;flex-wrap:wrap;gap:6px;${marginTop ? `margin-top:${marginTop};` : ""}`;
|
|
4330
|
+
filesWrapper.appendChild(tilesWrap);
|
|
4331
|
+
const placeholders = rids.map(() => {
|
|
4332
|
+
const placeholder = document.createElement("div");
|
|
4333
|
+
placeholder.style.cssText = `width:${TILE_SIZE};height:${TILE_SIZE};`;
|
|
4334
|
+
tilesWrap.appendChild(placeholder);
|
|
4335
|
+
return placeholder;
|
|
4336
|
+
});
|
|
4337
|
+
for (let i = 0; i < rids.length; i++) {
|
|
4338
|
+
const resourceId = rids[i];
|
|
4339
|
+
const placeholder = placeholders[i];
|
|
4340
|
+
renderFilePreviewReadonly(resourceId, state).then((tileEl) => {
|
|
4341
|
+
placeholder.replaceWith(tileEl);
|
|
4342
|
+
}).catch((err) => {
|
|
4343
|
+
console.error("Failed to render readonly tile:", err);
|
|
4344
|
+
});
|
|
4345
|
+
}
|
|
4346
|
+
}
|
|
4347
|
+
function renderFilesElementReadonly(element, ctx, wrapper, pathKey) {
|
|
4348
|
+
const rawPrefill = ctx.prefill[element.key];
|
|
4349
|
+
const initialFiles = Array.isArray(rawPrefill) ? rawPrefill : [];
|
|
4350
|
+
renderMultiFileReadonly(initialFiles, ctx.state, wrapper, pathKey);
|
|
4351
|
+
}
|
|
4352
|
+
function renderMultipleFileElementReadonly(element, ctx, wrapper, pathKey) {
|
|
4353
|
+
const rawPrefill = ctx.prefill[element.key];
|
|
4354
|
+
const initialFiles = Array.isArray(rawPrefill) ? rawPrefill : [];
|
|
4355
|
+
renderMultiFileReadonly(initialFiles, ctx.state, wrapper, pathKey, "4px");
|
|
4356
|
+
}
|
|
4357
|
+
|
|
4358
|
+
// src/components/file.ts
|
|
4359
|
+
function renderFileElement(element, ctx, wrapper, pathKey) {
|
|
4360
|
+
if (isElementReadonly(element, ctx.state, ctx)) {
|
|
4361
|
+
renderFileElementReadonly(element, ctx, wrapper, pathKey);
|
|
4362
|
+
} else {
|
|
4363
|
+
renderFileElementEdit(element, ctx, wrapper, pathKey);
|
|
4364
|
+
}
|
|
4365
|
+
}
|
|
4366
|
+
function renderFilesElement(element, ctx, wrapper, pathKey) {
|
|
4367
|
+
if (isElementReadonly(element, ctx.state, ctx)) {
|
|
4368
|
+
renderFilesElementReadonly(element, ctx, wrapper, pathKey);
|
|
4369
|
+
} else {
|
|
4370
|
+
renderFilesElementEdit(element, ctx, wrapper, pathKey);
|
|
4371
|
+
}
|
|
4372
|
+
}
|
|
4373
|
+
function renderMultipleFileElement(element, ctx, wrapper, pathKey) {
|
|
4374
|
+
if (isElementReadonly(element, ctx.state, ctx)) {
|
|
4375
|
+
renderMultipleFileElementReadonly(element, ctx, wrapper, pathKey);
|
|
4376
|
+
} else {
|
|
4377
|
+
renderMultipleFileElementEdit(element, ctx, wrapper, pathKey);
|
|
3384
4378
|
}
|
|
3385
4379
|
}
|
|
3386
4380
|
function updateFileField(element, fieldPath, value, context) {
|
|
3387
4381
|
const { scopeRoot, state } = context;
|
|
3388
|
-
if ("multiple" in element && element.multiple) {
|
|
4382
|
+
if (element.type === "files" || "multiple" in element && element.multiple) {
|
|
3389
4383
|
if (!Array.isArray(value)) {
|
|
3390
4384
|
console.warn(
|
|
3391
4385
|
`updateFileField: Expected array for multiple file field "${fieldPath}", got ${typeof value}`
|
|
@@ -3394,30 +4388,19 @@ function updateFileField(element, fieldPath, value, context) {
|
|
|
3394
4388
|
}
|
|
3395
4389
|
value.forEach((resourceId) => {
|
|
3396
4390
|
if (resourceId && typeof resourceId === "string") {
|
|
3397
|
-
|
|
3398
|
-
const filename = resourceId.split("/").pop() || "file";
|
|
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
|
-
});
|
|
3415
|
-
}
|
|
4391
|
+
seedInferredResource(resourceId, state.resourceIndex);
|
|
3416
4392
|
}
|
|
3417
4393
|
});
|
|
3418
|
-
|
|
3419
|
-
`
|
|
4394
|
+
const filesWrapper = scopeRoot.querySelector(
|
|
4395
|
+
`[data-files-wrapper="${fieldPath}"]`
|
|
3420
4396
|
);
|
|
4397
|
+
if (filesWrapper) {
|
|
4398
|
+
filesWrapper.dataset.resourceIds = JSON.stringify(value);
|
|
4399
|
+
} else {
|
|
4400
|
+
console.warn(
|
|
4401
|
+
`updateFileField: [data-files-wrapper="${fieldPath}"] not found in DOM; data-resource-ids not updated`
|
|
4402
|
+
);
|
|
4403
|
+
}
|
|
3421
4404
|
} else {
|
|
3422
4405
|
const hiddenInput = scopeRoot.querySelector(
|
|
3423
4406
|
`input[name="${fieldPath}"][type="hidden"]`
|
|
@@ -3430,25 +4413,7 @@ function updateFileField(element, fieldPath, value, context) {
|
|
|
3430
4413
|
}
|
|
3431
4414
|
hiddenInput.value = value != null ? String(value) : "";
|
|
3432
4415
|
if (value && typeof value === "string") {
|
|
3433
|
-
|
|
3434
|
-
const filename = value.split("/").pop() || "file";
|
|
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
|
-
});
|
|
3451
|
-
}
|
|
4416
|
+
seedInferredResource(value, state.resourceIndex);
|
|
3452
4417
|
console.info(
|
|
3453
4418
|
`updateFileField: File field "${fieldPath}" updated. Preview update requires re-render.`
|
|
3454
4419
|
);
|
|
@@ -8098,6 +9063,7 @@ var defaultConfig = {
|
|
|
8098
9063
|
enableFilePreview: true,
|
|
8099
9064
|
maxPreviewSize: "200px",
|
|
8100
9065
|
readonly: false,
|
|
9066
|
+
pickExistingFiles: null,
|
|
8101
9067
|
parseTableFile: null,
|
|
8102
9068
|
locale: "en",
|
|
8103
9069
|
translations: {
|
|
@@ -8109,6 +9075,8 @@ var defaultConfig = {
|
|
|
8109
9075
|
noFileSelected: "No file selected",
|
|
8110
9076
|
noFilesSelected: "No files selected",
|
|
8111
9077
|
downloadButton: "Download",
|
|
9078
|
+
downloadFile: "Download",
|
|
9079
|
+
openInNewTab: "Open in new tab",
|
|
8112
9080
|
changeButton: "Change",
|
|
8113
9081
|
placeholderText: "Enter text",
|
|
8114
9082
|
previewAlt: "Preview",
|
|
@@ -8130,6 +9098,12 @@ var defaultConfig = {
|
|
|
8130
9098
|
fileCountSingle: "{count} file",
|
|
8131
9099
|
fileCountPlural: "{count} files",
|
|
8132
9100
|
fileCountRange: "({min}-{max})",
|
|
9101
|
+
uploadingFile: "Uploading\u2026",
|
|
9102
|
+
filesCounter: "{count}/{max}",
|
|
9103
|
+
fromLibrary: "From library",
|
|
9104
|
+
libraryEmpty: "Library is empty",
|
|
9105
|
+
libraryHint: "Choose from previously uploaded files",
|
|
9106
|
+
pickerError: "Failed to load files from library",
|
|
8133
9107
|
// Validation errors
|
|
8134
9108
|
required: "Required",
|
|
8135
9109
|
minItems: "Minimum {min} items required",
|
|
@@ -8145,6 +9119,7 @@ var defaultConfig = {
|
|
|
8145
9119
|
minFiles: "Minimum {min} files required",
|
|
8146
9120
|
maxFiles: "Maximum {max} files allowed",
|
|
8147
9121
|
invalidFileExtension: 'File "{name}" has unsupported format. Allowed: {formats}',
|
|
9122
|
+
invalidFileMime: 'File "{name}": file type {type} not allowed (allowed: {mimes})',
|
|
8148
9123
|
fileTooLarge: 'File "{name}" exceeds maximum size of {maxSize}MB',
|
|
8149
9124
|
filesLimitExceeded: "{skipped} file(s) skipped: maximum {max} files allowed",
|
|
8150
9125
|
unsupportedFieldType: "Unsupported field type: {type}",
|
|
@@ -8171,6 +9146,8 @@ var defaultConfig = {
|
|
|
8171
9146
|
noFileSelected: "\u0424\u0430\u0439\u043B \u043D\u0435 \u0432\u044B\u0431\u0440\u0430\u043D",
|
|
8172
9147
|
noFilesSelected: "\u041D\u0435\u0442 \u0444\u0430\u0439\u043B\u043E\u0432",
|
|
8173
9148
|
downloadButton: "\u0421\u043A\u0430\u0447\u0430\u0442\u044C",
|
|
9149
|
+
downloadFile: "\u0421\u043A\u0430\u0447\u0430\u0442\u044C",
|
|
9150
|
+
openInNewTab: "\u041E\u0442\u043A\u0440\u044B\u0442\u044C \u0432 \u043D\u043E\u0432\u043E\u0439 \u0432\u043A\u043B\u0430\u0434\u043A\u0435",
|
|
8174
9151
|
changeButton: "\u0418\u0437\u043C\u0435\u043D\u0438\u0442\u044C",
|
|
8175
9152
|
placeholderText: "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0442\u0435\u043A\u0441\u0442",
|
|
8176
9153
|
previewAlt: "\u041F\u0440\u0435\u0434\u043F\u0440\u043E\u0441\u043C\u043E\u0442\u0440",
|
|
@@ -8192,6 +9169,12 @@ var defaultConfig = {
|
|
|
8192
9169
|
fileCountSingle: "{count} \u0444\u0430\u0439\u043B",
|
|
8193
9170
|
fileCountPlural: "{count} \u0444\u0430\u0439\u043B\u043E\u0432",
|
|
8194
9171
|
fileCountRange: "({min}-{max})",
|
|
9172
|
+
uploadingFile: "\u0417\u0430\u0433\u0440\u0443\u0437\u043A\u0430\u2026",
|
|
9173
|
+
filesCounter: "{count}/{max}",
|
|
9174
|
+
fromLibrary: "\u0418\u0437 \u0431\u0438\u0431\u043B\u0438\u043E\u0442\u0435\u043A\u0438",
|
|
9175
|
+
libraryEmpty: "\u0411\u0438\u0431\u043B\u0438\u043E\u0442\u0435\u043A\u0430 \u043F\u0443\u0441\u0442\u0430",
|
|
9176
|
+
libraryHint: "\u0412\u044B\u0431\u0435\u0440\u0438\u0442\u0435 \u0438\u0437 \u0440\u0430\u043D\u0435\u0435 \u0437\u0430\u0433\u0440\u0443\u0436\u0435\u043D\u043D\u044B\u0445 \u0444\u0430\u0439\u043B\u043E\u0432",
|
|
9177
|
+
pickerError: "\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u0437\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044C \u0444\u0430\u0439\u043B\u044B \u0438\u0437 \u0431\u0438\u0431\u043B\u0438\u043E\u0442\u0435\u043A\u0438",
|
|
8195
9178
|
// Validation errors
|
|
8196
9179
|
required: "\u041E\u0431\u044F\u0437\u0430\u0442\u0435\u043B\u044C\u043D\u043E\u0435 \u043F\u043E\u043B\u0435",
|
|
8197
9180
|
minItems: "\u041C\u0438\u043D\u0438\u043C\u0443\u043C {min} \u044D\u043B\u0435\u043C\u0435\u043D\u0442\u043E\u0432",
|
|
@@ -8207,6 +9190,7 @@ var defaultConfig = {
|
|
|
8207
9190
|
minFiles: "\u041C\u0438\u043D\u0438\u043C\u0443\u043C {min} \u0444\u0430\u0439\u043B\u043E\u0432",
|
|
8208
9191
|
maxFiles: "\u041C\u0430\u043A\u0441\u0438\u043C\u0443\u043C {max} \u0444\u0430\u0439\u043B\u043E\u0432",
|
|
8209
9192
|
invalidFileExtension: '\u0424\u0430\u0439\u043B "{name}" \u0438\u043C\u0435\u0435\u0442 \u043D\u0435\u043F\u043E\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u043C\u044B\u0439 \u0444\u043E\u0440\u043C\u0430\u0442. \u0414\u043E\u043F\u0443\u0441\u0442\u0438\u043C\u044B\u0435: {formats}',
|
|
9193
|
+
invalidFileMime: '\u0424\u0430\u0439\u043B "{name}": \u0442\u0438\u043F \u0444\u0430\u0439\u043B\u0430 {type} \u043D\u0435 \u0440\u0430\u0437\u0440\u0435\u0448\u0451\u043D (\u0440\u0430\u0437\u0440\u0435\u0448\u0435\u043D\u044B: {mimes})',
|
|
8210
9194
|
fileTooLarge: '\u0424\u0430\u0439\u043B "{name}" \u043F\u0440\u0435\u0432\u044B\u0448\u0430\u0435\u0442 \u043C\u0430\u043A\u0441\u0438\u043C\u0430\u043B\u044C\u043D\u044B\u0439 \u0440\u0430\u0437\u043C\u0435\u0440 {maxSize}\u041C\u0411',
|
|
8211
9195
|
filesLimitExceeded: "{skipped} \u0444\u0430\u0439\u043B(\u043E\u0432) \u043F\u0440\u043E\u043F\u0443\u0449\u0435\u043D\u043E: \u043C\u0430\u043A\u0441\u0438\u043C\u0443\u043C {max} \u0444\u0430\u0439\u043B\u043E\u0432",
|
|
8212
9196
|
unsupportedFieldType: "\u041D\u0435\u043F\u043E\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u043C\u044B\u0439 \u0442\u0438\u043F \u043F\u043E\u043B\u044F: {type}",
|