@dmitryvim/form-builder 0.2.27 → 0.2.29
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 +57 -1
- package/dist/browser/formbuilder.min.js +207 -125
- package/dist/browser/formbuilder.v0.2.29.min.js +956 -0
- package/dist/cjs/index.cjs +566 -142
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/esm/index.js +552 -131
- package/dist/esm/index.js.map +1 -1
- package/dist/form-builder.js +207 -125
- package/dist/types/components/file/constraints.d.ts +39 -3
- package/dist/types/components/file/dom.d.ts +2 -0
- package/dist/types/components/file/library.d.ts +49 -0
- package/dist/types/components/file/render-edit.d.ts +12 -9
- package/dist/types/components/file/upload.d.ts +15 -3
- package/dist/types/index.d.ts +1 -1
- package/dist/types/types/config.d.ts +33 -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.27.min.js +0 -874
package/dist/esm/index.js
CHANGED
|
@@ -2104,30 +2104,58 @@ function isFileExtensionAllowed(fileName, allowedExtensions) {
|
|
|
2104
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
|
+
});
|
|
2110
2155
|
}
|
|
2111
2156
|
function addPrefillFilesToIndex(initialFiles, resourceIndex) {
|
|
2112
2157
|
for (const resourceId of initialFiles) {
|
|
2113
|
-
|
|
2114
|
-
const filename = resourceId.split("/").pop() || "file";
|
|
2115
|
-
const extension = filename.split(".").pop()?.toLowerCase();
|
|
2116
|
-
let fileType = "application/octet-stream";
|
|
2117
|
-
if (extension) {
|
|
2118
|
-
if (["jpg", "jpeg", "png", "gif", "webp"].includes(extension)) {
|
|
2119
|
-
fileType = `image/${extension === "jpg" ? "jpeg" : extension}`;
|
|
2120
|
-
} else if (["mp4", "webm", "mov", "avi"].includes(extension)) {
|
|
2121
|
-
fileType = `video/${extension === "mov" ? "quicktime" : extension}`;
|
|
2122
|
-
}
|
|
2123
|
-
}
|
|
2124
|
-
resourceIndex.set(resourceId, {
|
|
2125
|
-
name: filename,
|
|
2126
|
-
type: fileType,
|
|
2127
|
-
size: 0,
|
|
2128
|
-
uploadedAt: /* @__PURE__ */ new Date(),
|
|
2129
|
-
file: void 0
|
|
2130
|
-
});
|
|
2158
|
+
seedInferredResource(resourceId, resourceIndex);
|
|
2131
2159
|
}
|
|
2132
2160
|
}
|
|
2133
2161
|
|
|
@@ -2404,6 +2432,84 @@ function ensureFileStyles() {
|
|
|
2404
2432
|
z-index: 10000;
|
|
2405
2433
|
}
|
|
2406
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
|
+
|
|
2407
2513
|
/* Hover zoom preview popup for image tiles \u2014 appended to document.body (fixed) */
|
|
2408
2514
|
.fb-tile-zoom-preview {
|
|
2409
2515
|
position: fixed;
|
|
@@ -2522,22 +2628,34 @@ function setEmptyFileContainer(fileContainer, state, hint) {
|
|
|
2522
2628
|
</div>
|
|
2523
2629
|
`;
|
|
2524
2630
|
}
|
|
2631
|
+
var dragDropHandlers = /* @__PURE__ */ new WeakMap();
|
|
2525
2632
|
function setupDragAndDrop(element, dropHandler) {
|
|
2526
|
-
|
|
2633
|
+
const prev = dragDropHandlers.get(element);
|
|
2634
|
+
if (prev) {
|
|
2635
|
+
element.removeEventListener("dragover", prev.dragover);
|
|
2636
|
+
element.removeEventListener("dragleave", prev.dragleave);
|
|
2637
|
+
element.removeEventListener("drop", prev.drop);
|
|
2638
|
+
}
|
|
2639
|
+
const dragover = (e) => {
|
|
2527
2640
|
e.preventDefault();
|
|
2528
2641
|
element.classList.add("border-blue-500", "bg-blue-50");
|
|
2529
|
-
}
|
|
2530
|
-
|
|
2642
|
+
};
|
|
2643
|
+
const dragleave = (e) => {
|
|
2531
2644
|
e.preventDefault();
|
|
2532
2645
|
element.classList.remove("border-blue-500", "bg-blue-50");
|
|
2533
|
-
}
|
|
2534
|
-
|
|
2646
|
+
};
|
|
2647
|
+
const drop = (e) => {
|
|
2535
2648
|
e.preventDefault();
|
|
2536
2649
|
element.classList.remove("border-blue-500", "bg-blue-50");
|
|
2537
|
-
|
|
2538
|
-
|
|
2650
|
+
const files = e.dataTransfer?.files;
|
|
2651
|
+
if (files) {
|
|
2652
|
+
dropHandler(files);
|
|
2539
2653
|
}
|
|
2540
|
-
}
|
|
2654
|
+
};
|
|
2655
|
+
element.addEventListener("dragover", dragover);
|
|
2656
|
+
element.addEventListener("dragleave", dragleave);
|
|
2657
|
+
element.addEventListener("drop", drop);
|
|
2658
|
+
dragDropHandlers.set(element, { dragover, dragleave, drop });
|
|
2541
2659
|
}
|
|
2542
2660
|
|
|
2543
2661
|
// src/components/file/preview.ts
|
|
@@ -3222,7 +3340,18 @@ async function uploadSingleFile(file, state) {
|
|
|
3222
3340
|
throw new Error(`File upload failed: ${err.message}`);
|
|
3223
3341
|
}
|
|
3224
3342
|
}
|
|
3225
|
-
async function handleFileSelect(
|
|
3343
|
+
async function handleFileSelect(opts) {
|
|
3344
|
+
const {
|
|
3345
|
+
file,
|
|
3346
|
+
container,
|
|
3347
|
+
fieldName,
|
|
3348
|
+
state,
|
|
3349
|
+
deps = null,
|
|
3350
|
+
instance = null,
|
|
3351
|
+
allowedExtensions = [],
|
|
3352
|
+
allowedMimes = [],
|
|
3353
|
+
maxSizeMB = Infinity
|
|
3354
|
+
} = opts;
|
|
3226
3355
|
if (!isFileExtensionAllowed(file.name, allowedExtensions)) {
|
|
3227
3356
|
const formats = allowedExtensions.join(", ");
|
|
3228
3357
|
showFileError(
|
|
@@ -3231,6 +3360,14 @@ async function handleFileSelect(file, container, fieldName, state, deps = null,
|
|
|
3231
3360
|
);
|
|
3232
3361
|
return;
|
|
3233
3362
|
}
|
|
3363
|
+
if (!isMimeAllowed(file.type, allowedMimes)) {
|
|
3364
|
+
const mimes = allowedMimes.join(", ");
|
|
3365
|
+
showFileError(
|
|
3366
|
+
container,
|
|
3367
|
+
t("invalidFileMime", state, { name: file.name, type: file.type, mimes })
|
|
3368
|
+
);
|
|
3369
|
+
return;
|
|
3370
|
+
}
|
|
3234
3371
|
if (!isFileSizeAllowed(file, maxSizeMB)) {
|
|
3235
3372
|
showFileError(
|
|
3236
3373
|
container,
|
|
@@ -3290,10 +3427,16 @@ function filterAndSlice(allFiles, currentCount, constraints, state) {
|
|
|
3290
3427
|
const afterExt = allFiles.filter(
|
|
3291
3428
|
(f) => isFileExtensionAllowed(f.name, constraints.allowedExtensions)
|
|
3292
3429
|
);
|
|
3293
|
-
const
|
|
3430
|
+
const rejectedByMime = afterExt.filter(
|
|
3431
|
+
(f) => !isMimeAllowed(f.type, constraints.allowedMimes)
|
|
3432
|
+
);
|
|
3433
|
+
const afterMime = afterExt.filter(
|
|
3434
|
+
(f) => isMimeAllowed(f.type, constraints.allowedMimes)
|
|
3435
|
+
);
|
|
3436
|
+
const rejectedBySize = afterMime.filter(
|
|
3294
3437
|
(f) => !isFileSizeAllowed(f, constraints.maxSize)
|
|
3295
3438
|
);
|
|
3296
|
-
const valid =
|
|
3439
|
+
const valid = afterMime.filter((f) => isFileSizeAllowed(f, constraints.maxSize));
|
|
3297
3440
|
const remaining = constraints.maxCount === Infinity ? valid.length : Math.max(0, constraints.maxCount - currentCount);
|
|
3298
3441
|
const accepted = valid.slice(0, remaining);
|
|
3299
3442
|
const skippedByCount = valid.length - accepted.length;
|
|
@@ -3303,6 +3446,11 @@ function filterAndSlice(allFiles, currentCount, constraints, state) {
|
|
|
3303
3446
|
const names = rejectedByExt.map((f) => f.name).join(", ");
|
|
3304
3447
|
errorParts.push(t("invalidFileExtension", state, { name: names, formats }));
|
|
3305
3448
|
}
|
|
3449
|
+
if (rejectedByMime.length > 0) {
|
|
3450
|
+
const mimes = constraints.allowedMimes.join(", ");
|
|
3451
|
+
const names = rejectedByMime.map((f) => f.name).join(", ");
|
|
3452
|
+
errorParts.push(t("invalidFileMime", state, { name: names, type: rejectedByMime.map((f) => f.type).join(", "), mimes }));
|
|
3453
|
+
}
|
|
3306
3454
|
if (rejectedBySize.length > 0) {
|
|
3307
3455
|
const names = rejectedBySize.map((f) => f.name).join(", ");
|
|
3308
3456
|
errorParts.push(
|
|
@@ -3394,27 +3542,159 @@ function setupFilesPickerHandler(filesPicker, resourceIds, state, updateCallback
|
|
|
3394
3542
|
};
|
|
3395
3543
|
}
|
|
3396
3544
|
|
|
3397
|
-
// src/components/file/
|
|
3398
|
-
function
|
|
3399
|
-
if (!
|
|
3400
|
-
|
|
3401
|
-
const
|
|
3402
|
-
|
|
3403
|
-
|
|
3404
|
-
|
|
3405
|
-
|
|
3406
|
-
|
|
3407
|
-
|
|
3408
|
-
|
|
3409
|
-
|
|
3410
|
-
|
|
3411
|
-
|
|
3412
|
-
|
|
3413
|
-
|
|
3414
|
-
|
|
3415
|
-
|
|
3545
|
+
// src/components/file/library.ts
|
|
3546
|
+
function buildAcceptContext(element) {
|
|
3547
|
+
if (!element.accept) return void 0;
|
|
3548
|
+
if (typeof element.accept === "string") {
|
|
3549
|
+
const exts2 = getAllowedExtensions(element.accept);
|
|
3550
|
+
return exts2.length > 0 ? { extensions: exts2 } : void 0;
|
|
3551
|
+
}
|
|
3552
|
+
const exts = element.accept.extensions ?? [];
|
|
3553
|
+
const mime = element.accept.mime ?? [];
|
|
3554
|
+
const normalizedExts = exts.map((e) => e.toLowerCase());
|
|
3555
|
+
if (normalizedExts.length === 0 && mime.length === 0) return void 0;
|
|
3556
|
+
const result = {};
|
|
3557
|
+
if (normalizedExts.length > 0) result.extensions = normalizedExts;
|
|
3558
|
+
if (mime.length > 0) result.mime = mime;
|
|
3559
|
+
return result;
|
|
3560
|
+
}
|
|
3561
|
+
function validatePickedResource(resource, allowedExtensions, allowedMimes, maxSizeMB, state) {
|
|
3562
|
+
if (!isFileExtensionAllowed(resource.name, allowedExtensions)) {
|
|
3563
|
+
const formats = allowedExtensions.join(", ");
|
|
3564
|
+
return t("invalidFileExtension", state, { name: resource.name, formats });
|
|
3565
|
+
}
|
|
3566
|
+
if (!isMimeAllowed(resource.type, allowedMimes)) {
|
|
3567
|
+
const mimes = allowedMimes.join(", ");
|
|
3568
|
+
return t("invalidFileMime", state, { name: resource.name, type: resource.type, mimes });
|
|
3569
|
+
}
|
|
3570
|
+
if (!isSizeWithinLimit(resource.size, maxSizeMB)) {
|
|
3571
|
+
return t("fileTooLarge", state, { name: resource.name, maxSize: maxSizeMB });
|
|
3572
|
+
}
|
|
3573
|
+
return null;
|
|
3574
|
+
}
|
|
3575
|
+
function readCurrentResourceIds(wrapper) {
|
|
3576
|
+
const raw = wrapper.dataset.resourceIds;
|
|
3577
|
+
if (!raw) return [];
|
|
3578
|
+
try {
|
|
3579
|
+
const parsed = JSON.parse(raw);
|
|
3580
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
3581
|
+
} catch {
|
|
3582
|
+
return [];
|
|
3583
|
+
}
|
|
3584
|
+
}
|
|
3585
|
+
function registerPickedResource(resource, state) {
|
|
3586
|
+
const existing = state.resourceIndex.get(resource.resourceId);
|
|
3587
|
+
state.resourceIndex.set(resource.resourceId, {
|
|
3588
|
+
name: resource.name,
|
|
3589
|
+
type: resource.type,
|
|
3590
|
+
size: resource.size,
|
|
3591
|
+
uploadedAt: existing?.uploadedAt ?? /* @__PURE__ */ new Date(),
|
|
3592
|
+
file: existing?.file
|
|
3593
|
+
});
|
|
3594
|
+
}
|
|
3595
|
+
function extractPickerError(error, state) {
|
|
3596
|
+
if (error instanceof Error && error.message) return error.message;
|
|
3597
|
+
return t("pickerError", state);
|
|
3598
|
+
}
|
|
3599
|
+
async function handleLibraryPickMulti(state, element, wrapper, fieldPath, resourceIds, maxCount, updateCallback, instance) {
|
|
3600
|
+
if (!state.config.pickExistingFiles) return;
|
|
3601
|
+
const allowedExtensions = getAllowedExtensions(element.accept);
|
|
3602
|
+
const allowedMimes = getAllowedMimes(element.accept);
|
|
3603
|
+
const maxSizeMB = element.maxSize ?? Infinity;
|
|
3604
|
+
const currentIds = readCurrentResourceIds(wrapper);
|
|
3605
|
+
const remaining = maxCount === Infinity ? Infinity : Math.max(0, maxCount - currentIds.length);
|
|
3606
|
+
let picked;
|
|
3607
|
+
try {
|
|
3608
|
+
picked = await state.config.pickExistingFiles({
|
|
3609
|
+
fieldPath,
|
|
3610
|
+
mode: "multiple",
|
|
3611
|
+
accept: buildAcceptContext(element),
|
|
3612
|
+
maxSizeMB: maxSizeMB === Infinity ? void 0 : maxSizeMB,
|
|
3613
|
+
remainingSlots: remaining === Infinity ? void 0 : remaining,
|
|
3614
|
+
selectedResourceIds: [...currentIds]
|
|
3416
3615
|
});
|
|
3616
|
+
} catch (error) {
|
|
3617
|
+
showFileError(wrapper, extractPickerError(error, state));
|
|
3618
|
+
return;
|
|
3619
|
+
}
|
|
3620
|
+
if (picked.length === 0) return;
|
|
3621
|
+
const existingSet = new Set(currentIds);
|
|
3622
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3623
|
+
const deduped = picked.filter((r) => {
|
|
3624
|
+
if (existingSet.has(r.resourceId)) return false;
|
|
3625
|
+
if (seen.has(r.resourceId)) return false;
|
|
3626
|
+
seen.add(r.resourceId);
|
|
3627
|
+
return true;
|
|
3628
|
+
});
|
|
3629
|
+
const validItems = deduped.filter((r) => {
|
|
3630
|
+
const err = validatePickedResource(r, allowedExtensions, allowedMimes, maxSizeMB, state);
|
|
3631
|
+
return err === null;
|
|
3632
|
+
});
|
|
3633
|
+
const freshRemaining = maxCount === Infinity ? validItems.length : Math.max(0, maxCount - resourceIds.length);
|
|
3634
|
+
const accepted = validItems.slice(0, freshRemaining);
|
|
3635
|
+
const skipped = validItems.length - accepted.length;
|
|
3636
|
+
if (accepted.length === 0) return;
|
|
3637
|
+
clearFileError(wrapper);
|
|
3638
|
+
if (skipped > 0) {
|
|
3639
|
+
showFileError(
|
|
3640
|
+
wrapper,
|
|
3641
|
+
t("filesLimitExceeded", state, { skipped, max: maxCount })
|
|
3642
|
+
);
|
|
3643
|
+
}
|
|
3644
|
+
for (const resource of accepted) {
|
|
3645
|
+
registerPickedResource(resource, state);
|
|
3646
|
+
resourceIds.push(resource.resourceId);
|
|
3647
|
+
}
|
|
3648
|
+
wrapper.dataset.resourceIds = JSON.stringify(resourceIds);
|
|
3649
|
+
updateCallback();
|
|
3650
|
+
if (!state.config.readonly) {
|
|
3651
|
+
instance.triggerOnChange(fieldPath, resourceIds);
|
|
3652
|
+
}
|
|
3653
|
+
}
|
|
3654
|
+
async function handleLibraryPickSingle(state, element, container, fileWrapper, pathKey, fieldPath, renderCallback, instance) {
|
|
3655
|
+
if (!state.config.pickExistingFiles) return;
|
|
3656
|
+
const allowedExtensions = getAllowedExtensions(element.accept);
|
|
3657
|
+
const allowedMimes = getAllowedMimes(element.accept);
|
|
3658
|
+
const maxSizeMB = element.maxSize ?? Infinity;
|
|
3659
|
+
let picked;
|
|
3660
|
+
try {
|
|
3661
|
+
picked = await state.config.pickExistingFiles({
|
|
3662
|
+
fieldPath,
|
|
3663
|
+
mode: "single",
|
|
3664
|
+
accept: buildAcceptContext(element),
|
|
3665
|
+
maxSizeMB: maxSizeMB === Infinity ? void 0 : maxSizeMB,
|
|
3666
|
+
selectedResourceIds: []
|
|
3667
|
+
});
|
|
3668
|
+
} catch (error) {
|
|
3669
|
+
showFileError(container, extractPickerError(error, state));
|
|
3670
|
+
return;
|
|
3671
|
+
}
|
|
3672
|
+
if (picked.length === 0) return;
|
|
3673
|
+
const first = picked[0];
|
|
3674
|
+
const validationError = validatePickedResource(first, allowedExtensions, allowedMimes, maxSizeMB, state);
|
|
3675
|
+
if (validationError !== null) {
|
|
3676
|
+
showFileError(container, validationError);
|
|
3677
|
+
return;
|
|
3678
|
+
}
|
|
3679
|
+
clearFileError(container);
|
|
3680
|
+
registerPickedResource(first, state);
|
|
3681
|
+
let hiddenInput = fileWrapper.querySelector('input[type="hidden"]');
|
|
3682
|
+
if (!hiddenInput) {
|
|
3683
|
+
hiddenInput = document.createElement("input");
|
|
3684
|
+
hiddenInput.type = "hidden";
|
|
3685
|
+
hiddenInput.name = pathKey;
|
|
3686
|
+
fileWrapper.appendChild(hiddenInput);
|
|
3687
|
+
}
|
|
3688
|
+
hiddenInput.value = first.resourceId;
|
|
3689
|
+
await renderCallback(first.resourceId);
|
|
3690
|
+
if (!state.config.readonly) {
|
|
3691
|
+
instance.triggerOnChange(fieldPath, first.resourceId);
|
|
3417
3692
|
}
|
|
3693
|
+
}
|
|
3694
|
+
|
|
3695
|
+
// src/components/file/render-edit.ts
|
|
3696
|
+
function handleInitialFileData(initial, fileContainer, pathKey, fileWrapper, state, deps) {
|
|
3697
|
+
seedInferredResource(initial, state.resourceIndex);
|
|
3418
3698
|
const meta = state.resourceIndex.get(initial);
|
|
3419
3699
|
const isVideo = meta?.type?.startsWith("video/");
|
|
3420
3700
|
if (isVideo) {
|
|
@@ -3432,7 +3712,50 @@ function handleInitialFileData(initial, fileContainer, pathKey, fileWrapper, sta
|
|
|
3432
3712
|
hiddenInput.value = initial;
|
|
3433
3713
|
fileWrapper.appendChild(hiddenInput);
|
|
3434
3714
|
}
|
|
3435
|
-
|
|
3715
|
+
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);">
|
|
3716
|
+
<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"/>
|
|
3717
|
+
</svg>`;
|
|
3718
|
+
function buildEmptyDropzone(state, primaryText, subHint, openPicker) {
|
|
3719
|
+
const dropzone = document.createElement("div");
|
|
3720
|
+
dropzone.className = "fb-file-dropzone";
|
|
3721
|
+
dropzone.innerHTML = `
|
|
3722
|
+
${UPLOAD_SVG}
|
|
3723
|
+
<div class="fb-dropzone-primary-text">${escapeHtml(primaryText)}</div>
|
|
3724
|
+
${subHint ? `<div class="fb-dropzone-hint-text">${escapeHtml(subHint)}</div>` : ""}
|
|
3725
|
+
`;
|
|
3726
|
+
dropzone.onclick = openPicker;
|
|
3727
|
+
return dropzone;
|
|
3728
|
+
}
|
|
3729
|
+
function buildLibraryButton(variant, state, onClick) {
|
|
3730
|
+
const btn = document.createElement("button");
|
|
3731
|
+
btn.type = "button";
|
|
3732
|
+
btn.className = variant === "card" ? "fb-file-library-card" : "fb-tile fb-tile-add-library";
|
|
3733
|
+
if (variant === "card") {
|
|
3734
|
+
btn.innerHTML = `
|
|
3735
|
+
<span class="fb-file-library-card-icon" aria-hidden="true">\u{1F4DA}</span>
|
|
3736
|
+
<span class="fb-file-library-card-label">${escapeHtml(t("fromLibrary", state))}</span>
|
|
3737
|
+
<span class="fb-file-library-card-hint">${escapeHtml(t("libraryHint", state))}</span>
|
|
3738
|
+
`;
|
|
3739
|
+
} else {
|
|
3740
|
+
btn.innerHTML = `<span aria-hidden="true">\u{1F4DA}</span>`;
|
|
3741
|
+
btn.title = t("fromLibrary", state);
|
|
3742
|
+
btn.setAttribute("aria-label", t("fromLibrary", state));
|
|
3743
|
+
}
|
|
3744
|
+
btn.addEventListener("click", onClick);
|
|
3745
|
+
return btn;
|
|
3746
|
+
}
|
|
3747
|
+
function renderResourcePills(opts) {
|
|
3748
|
+
const {
|
|
3749
|
+
container,
|
|
3750
|
+
rids,
|
|
3751
|
+
state,
|
|
3752
|
+
onRemove,
|
|
3753
|
+
hint,
|
|
3754
|
+
countInfo,
|
|
3755
|
+
maxCount,
|
|
3756
|
+
isReadonly = false,
|
|
3757
|
+
onLibraryPick
|
|
3758
|
+
} = opts;
|
|
3436
3759
|
ensureFileStyles();
|
|
3437
3760
|
const wrapper = container.closest("[data-files-wrapper]");
|
|
3438
3761
|
if (wrapper) {
|
|
@@ -3441,6 +3764,7 @@ function renderResourcePills(container, rids, state, onRemove, hint, countInfo,
|
|
|
3441
3764
|
while (container.firstChild) container.removeChild(container.firstChild);
|
|
3442
3765
|
const ridList = rids ?? [];
|
|
3443
3766
|
const atMax = maxCount !== void 0 && ridList.length >= maxCount;
|
|
3767
|
+
const hasLibrary = !isReadonly && typeof onLibraryPick === "function";
|
|
3444
3768
|
const buildSubHint = () => {
|
|
3445
3769
|
const parts = [];
|
|
3446
3770
|
if (hint) parts.push(hint);
|
|
@@ -3457,18 +3781,26 @@ function renderResourcePills(container, rids, state, onRemove, hint, countInfo,
|
|
|
3457
3781
|
emptyEl.className = "fb-tile-empty-text";
|
|
3458
3782
|
emptyEl.textContent = t("noFilesSelected", state);
|
|
3459
3783
|
container.appendChild(emptyEl);
|
|
3784
|
+
} else if (hasLibrary) {
|
|
3785
|
+
const row = document.createElement("div");
|
|
3786
|
+
row.className = "fb-file-card-row";
|
|
3787
|
+
const dropzone = buildEmptyDropzone(
|
|
3788
|
+
state,
|
|
3789
|
+
t("clickDragTextMultiple", state),
|
|
3790
|
+
buildSubHint(),
|
|
3791
|
+
openPicker
|
|
3792
|
+
);
|
|
3793
|
+
const libraryBtn = buildLibraryButton("card", state, onLibraryPick);
|
|
3794
|
+
row.appendChild(dropzone);
|
|
3795
|
+
row.appendChild(libraryBtn);
|
|
3796
|
+
container.appendChild(row);
|
|
3460
3797
|
} else {
|
|
3461
|
-
const dropzone =
|
|
3462
|
-
|
|
3463
|
-
|
|
3464
|
-
|
|
3465
|
-
|
|
3466
|
-
|
|
3467
|
-
</svg>
|
|
3468
|
-
<div class="fb-dropzone-primary-text">${escapeHtml(t("clickDragTextMultiple", state))}</div>
|
|
3469
|
-
${subHint2 ? `<div class="fb-dropzone-hint-text">${escapeHtml(subHint2)}</div>` : ""}
|
|
3470
|
-
`;
|
|
3471
|
-
dropzone.onclick = openPicker;
|
|
3798
|
+
const dropzone = buildEmptyDropzone(
|
|
3799
|
+
state,
|
|
3800
|
+
t("clickDragTextMultiple", state),
|
|
3801
|
+
buildSubHint(),
|
|
3802
|
+
openPicker
|
|
3803
|
+
);
|
|
3472
3804
|
container.appendChild(dropzone);
|
|
3473
3805
|
}
|
|
3474
3806
|
return;
|
|
@@ -3499,6 +3831,10 @@ function renderResourcePills(container, rids, state, onRemove, hint, countInfo,
|
|
|
3499
3831
|
addTile.innerHTML = "+";
|
|
3500
3832
|
addTile.onclick = openPicker;
|
|
3501
3833
|
tilesWrap.appendChild(addTile);
|
|
3834
|
+
if (hasLibrary) {
|
|
3835
|
+
const libraryTile = buildLibraryButton("tile", state, onLibraryPick);
|
|
3836
|
+
tilesWrap.appendChild(libraryTile);
|
|
3837
|
+
}
|
|
3502
3838
|
} else if (!isReadonly && atMax) {
|
|
3503
3839
|
const chip = document.createElement("div");
|
|
3504
3840
|
chip.className = "fb-tile-counter";
|
|
@@ -3526,12 +3862,16 @@ function renderFileElementEdit(element, ctx, wrapper, pathKey) {
|
|
|
3526
3862
|
picker.name = pathKey;
|
|
3527
3863
|
picker.style.display = "none";
|
|
3528
3864
|
if (element.accept) {
|
|
3529
|
-
picker.accept = typeof element.accept === "string" ? element.accept :
|
|
3865
|
+
picker.accept = typeof element.accept === "string" ? element.accept : [
|
|
3866
|
+
...element.accept.extensions?.map((ext) => `.${ext}`) ?? [],
|
|
3867
|
+
...element.accept.mime ?? []
|
|
3868
|
+
].join(",") || "";
|
|
3530
3869
|
}
|
|
3531
3870
|
const fileContainer = document.createElement("div");
|
|
3532
3871
|
fileContainer.className = "file-preview-container";
|
|
3533
3872
|
const initial = ctx.prefill[element.key];
|
|
3534
3873
|
const allowedExts = getAllowedExtensions(element.accept);
|
|
3874
|
+
const allowedMimes = getAllowedMimes(element.accept);
|
|
3535
3875
|
const maxSizeMB = element.maxSize ?? Infinity;
|
|
3536
3876
|
const handlers = {
|
|
3537
3877
|
fileUploadHandler() {
|
|
@@ -3539,16 +3879,17 @@ function renderFileElementEdit(element, ctx, wrapper, pathKey) {
|
|
|
3539
3879
|
},
|
|
3540
3880
|
dragHandler(files) {
|
|
3541
3881
|
if (files.length > 0) {
|
|
3542
|
-
handleFileSelect(
|
|
3543
|
-
files[0],
|
|
3544
|
-
fileContainer,
|
|
3545
|
-
pathKey,
|
|
3882
|
+
handleFileSelect({
|
|
3883
|
+
file: files[0],
|
|
3884
|
+
container: fileContainer,
|
|
3885
|
+
fieldName: pathKey,
|
|
3546
3886
|
state,
|
|
3547
|
-
buildDeps(),
|
|
3548
|
-
ctx.instance,
|
|
3549
|
-
allowedExts,
|
|
3887
|
+
deps: buildDeps(),
|
|
3888
|
+
instance: ctx.instance,
|
|
3889
|
+
allowedExtensions: allowedExts,
|
|
3890
|
+
allowedMimes,
|
|
3550
3891
|
maxSizeMB
|
|
3551
|
-
);
|
|
3892
|
+
});
|
|
3552
3893
|
}
|
|
3553
3894
|
},
|
|
3554
3895
|
setupDrop(container) {
|
|
@@ -3569,7 +3910,7 @@ function renderFileElementEdit(element, ctx, wrapper, pathKey) {
|
|
|
3569
3910
|
releaseLocalFileUrl(state.resourceIndex.get(currentRid)?.file);
|
|
3570
3911
|
}
|
|
3571
3912
|
if (hiddenInput) hiddenInput.value = "";
|
|
3572
|
-
|
|
3913
|
+
renderEmptySingleState();
|
|
3573
3914
|
}
|
|
3574
3915
|
};
|
|
3575
3916
|
const buildDeps = () => ({
|
|
@@ -3579,6 +3920,50 @@ function renderFileElementEdit(element, ctx, wrapper, pathKey) {
|
|
|
3579
3920
|
setupDrop: handlers.setupDrop,
|
|
3580
3921
|
onRemove: handlers.onRemove
|
|
3581
3922
|
});
|
|
3923
|
+
const renderEmptySingleState = () => {
|
|
3924
|
+
if (state.config.pickExistingFiles && !element.disableLibrary) {
|
|
3925
|
+
fileContainer.className = "file-preview-container";
|
|
3926
|
+
fileContainer.removeAttribute("style");
|
|
3927
|
+
fileContainer.onclick = null;
|
|
3928
|
+
while (fileContainer.firstChild) {
|
|
3929
|
+
fileContainer.removeChild(fileContainer.firstChild);
|
|
3930
|
+
}
|
|
3931
|
+
const row = document.createElement("div");
|
|
3932
|
+
row.className = "fb-file-card-row";
|
|
3933
|
+
row.style.cssText = "display:flex;gap:8px;align-items:stretch;";
|
|
3934
|
+
const hint = makeFieldHint(element, state);
|
|
3935
|
+
const uploadCard = buildEmptyDropzone(
|
|
3936
|
+
state,
|
|
3937
|
+
t("clickDragText", state),
|
|
3938
|
+
hint,
|
|
3939
|
+
handlers.fileUploadHandler
|
|
3940
|
+
);
|
|
3941
|
+
uploadCard.style.cssText = "flex:1;min-width:0;height:128px;";
|
|
3942
|
+
setupDragAndDrop(uploadCard, handlers.dragHandler);
|
|
3943
|
+
const libraryBtn = buildLibraryButton("card", state, () => {
|
|
3944
|
+
handleLibraryPickSingle(
|
|
3945
|
+
state,
|
|
3946
|
+
element,
|
|
3947
|
+
fileContainer,
|
|
3948
|
+
fileWrapper,
|
|
3949
|
+
pathKey,
|
|
3950
|
+
pathKey,
|
|
3951
|
+
async (rid) => {
|
|
3952
|
+
await renderSingleFileEditTile(fileContainer, rid, state, buildDeps());
|
|
3953
|
+
},
|
|
3954
|
+
ctx.instance
|
|
3955
|
+
).catch((err) => {
|
|
3956
|
+
console.error("Library pick failed:", err);
|
|
3957
|
+
});
|
|
3958
|
+
});
|
|
3959
|
+
libraryBtn.style.cssText = "flex:1;min-width:0;";
|
|
3960
|
+
row.appendChild(uploadCard);
|
|
3961
|
+
row.appendChild(libraryBtn);
|
|
3962
|
+
fileContainer.appendChild(row);
|
|
3963
|
+
} else {
|
|
3964
|
+
handlers.restoreDropzone();
|
|
3965
|
+
}
|
|
3966
|
+
};
|
|
3582
3967
|
if (initial) {
|
|
3583
3968
|
handleInitialFileData(
|
|
3584
3969
|
initial,
|
|
@@ -3594,20 +3979,21 @@ function renderFileElementEdit(element, ctx, wrapper, pathKey) {
|
|
|
3594
3979
|
setupDragAndDrop(fileContainer, handlers.dragHandler);
|
|
3595
3980
|
}
|
|
3596
3981
|
} else {
|
|
3597
|
-
|
|
3982
|
+
renderEmptySingleState();
|
|
3598
3983
|
}
|
|
3599
3984
|
picker.onchange = () => {
|
|
3600
3985
|
if (picker.files && picker.files.length > 0) {
|
|
3601
|
-
handleFileSelect(
|
|
3602
|
-
picker.files[0],
|
|
3603
|
-
fileContainer,
|
|
3604
|
-
pathKey,
|
|
3986
|
+
handleFileSelect({
|
|
3987
|
+
file: picker.files[0],
|
|
3988
|
+
container: fileContainer,
|
|
3989
|
+
fieldName: pathKey,
|
|
3605
3990
|
state,
|
|
3606
|
-
buildDeps(),
|
|
3607
|
-
ctx.instance,
|
|
3608
|
-
allowedExts,
|
|
3991
|
+
deps: buildDeps(),
|
|
3992
|
+
instance: ctx.instance,
|
|
3993
|
+
allowedExtensions: allowedExts,
|
|
3994
|
+
allowedMimes,
|
|
3609
3995
|
maxSizeMB
|
|
3610
|
-
);
|
|
3996
|
+
});
|
|
3611
3997
|
}
|
|
3612
3998
|
};
|
|
3613
3999
|
fileWrapper.appendChild(fileContainer);
|
|
@@ -3625,7 +4011,10 @@ function renderFilesElementEdit(element, ctx, wrapper, pathKey) {
|
|
|
3625
4011
|
filesPicker.multiple = true;
|
|
3626
4012
|
filesPicker.style.display = "none";
|
|
3627
4013
|
if (element.accept) {
|
|
3628
|
-
filesPicker.accept = typeof element.accept === "string" ? element.accept :
|
|
4014
|
+
filesPicker.accept = typeof element.accept === "string" ? element.accept : [
|
|
4015
|
+
...element.accept.extensions?.map((ext) => `.${ext}`) ?? [],
|
|
4016
|
+
...element.accept.mime ?? []
|
|
4017
|
+
].join(",") || "";
|
|
3629
4018
|
}
|
|
3630
4019
|
const filesContainer = document.createElement("div");
|
|
3631
4020
|
filesContainer.className = "files-list-wrapper";
|
|
@@ -3639,29 +4028,43 @@ function renderFilesElementEdit(element, ctx, wrapper, pathKey) {
|
|
|
3639
4028
|
const filesConstraints = {
|
|
3640
4029
|
maxCount: Infinity,
|
|
3641
4030
|
allowedExtensions: getAllowedExtensions(element.accept),
|
|
4031
|
+
allowedMimes: getAllowedMimes(element.accept),
|
|
3642
4032
|
maxSize: element.maxSize ?? Infinity
|
|
3643
4033
|
};
|
|
3644
4034
|
filesContainer.appendChild(list);
|
|
3645
4035
|
filesWrapper.appendChild(filesPicker);
|
|
3646
4036
|
filesWrapper.appendChild(filesContainer);
|
|
3647
4037
|
wrapper.appendChild(filesWrapper);
|
|
4038
|
+
const onLibraryPickFiles = state.config.pickExistingFiles && !element.disableLibrary ? () => {
|
|
4039
|
+
handleLibraryPickMulti(
|
|
4040
|
+
state,
|
|
4041
|
+
element,
|
|
4042
|
+
filesWrapper,
|
|
4043
|
+
pathKey,
|
|
4044
|
+
initialFiles,
|
|
4045
|
+
Infinity,
|
|
4046
|
+
updateFilesList,
|
|
4047
|
+
ctx.instance
|
|
4048
|
+
).catch((err) => {
|
|
4049
|
+
console.error("Library pick failed:", err);
|
|
4050
|
+
});
|
|
4051
|
+
} : null;
|
|
3648
4052
|
function updateFilesList() {
|
|
3649
4053
|
const currentlyReadonly = isElementReadonly(element, state);
|
|
3650
|
-
renderResourcePills(
|
|
3651
|
-
list,
|
|
3652
|
-
initialFiles,
|
|
4054
|
+
renderResourcePills({
|
|
4055
|
+
container: list,
|
|
4056
|
+
rids: initialFiles,
|
|
3653
4057
|
state,
|
|
3654
|
-
currentlyReadonly ? null : (ridToRemove) => {
|
|
4058
|
+
onRemove: currentlyReadonly ? null : (ridToRemove) => {
|
|
3655
4059
|
releaseLocalFileUrl(state.resourceIndex.get(ridToRemove)?.file);
|
|
3656
4060
|
const index = initialFiles.indexOf(ridToRemove);
|
|
3657
4061
|
if (index > -1) initialFiles.splice(index, 1);
|
|
3658
4062
|
updateFilesList();
|
|
3659
4063
|
},
|
|
3660
|
-
filesFieldHint,
|
|
3661
|
-
|
|
3662
|
-
|
|
3663
|
-
|
|
3664
|
-
);
|
|
4064
|
+
hint: filesFieldHint,
|
|
4065
|
+
isReadonly: currentlyReadonly,
|
|
4066
|
+
onLibraryPick: currentlyReadonly ? null : onLibraryPickFiles
|
|
4067
|
+
});
|
|
3665
4068
|
}
|
|
3666
4069
|
updateFilesList();
|
|
3667
4070
|
setupFilesDropHandler(
|
|
@@ -3696,7 +4099,10 @@ function renderMultipleFileElementEdit(element, ctx, wrapper, pathKey) {
|
|
|
3696
4099
|
filesPicker.multiple = true;
|
|
3697
4100
|
filesPicker.style.display = "none";
|
|
3698
4101
|
if (element.accept) {
|
|
3699
|
-
filesPicker.accept = typeof element.accept === "string" ? element.accept :
|
|
4102
|
+
filesPicker.accept = typeof element.accept === "string" ? element.accept : [
|
|
4103
|
+
...element.accept.extensions?.map((ext) => `.${ext}`) ?? [],
|
|
4104
|
+
...element.accept.mime ?? []
|
|
4105
|
+
].join(",") || "";
|
|
3700
4106
|
}
|
|
3701
4107
|
const filesContainer = document.createElement("div");
|
|
3702
4108
|
filesContainer.className = "files-list-wrapper";
|
|
@@ -3713,6 +4119,7 @@ function renderMultipleFileElementEdit(element, ctx, wrapper, pathKey) {
|
|
|
3713
4119
|
const multipleConstraints = {
|
|
3714
4120
|
maxCount: maxFiles,
|
|
3715
4121
|
allowedExtensions: getAllowedExtensions(element.accept),
|
|
4122
|
+
allowedMimes: getAllowedMimes(element.accept),
|
|
3716
4123
|
maxSize: element.maxSize ?? Infinity
|
|
3717
4124
|
};
|
|
3718
4125
|
const buildCountInfo = () => {
|
|
@@ -3720,22 +4127,37 @@ function renderMultipleFileElementEdit(element, ctx, wrapper, pathKey) {
|
|
|
3720
4127
|
const minMaxText = minFiles > 0 || maxFiles < Infinity ? ` ${t("fileCountRange", state, { min: minFiles, max: maxFiles })}` : "";
|
|
3721
4128
|
return countText + minMaxText;
|
|
3722
4129
|
};
|
|
4130
|
+
const onLibraryPickMultiple = state.config.pickExistingFiles && !element.disableLibrary ? () => {
|
|
4131
|
+
handleLibraryPickMulti(
|
|
4132
|
+
state,
|
|
4133
|
+
element,
|
|
4134
|
+
filesWrapper,
|
|
4135
|
+
pathKey,
|
|
4136
|
+
initialFiles,
|
|
4137
|
+
maxFiles,
|
|
4138
|
+
updateFilesDisplay,
|
|
4139
|
+
ctx.instance
|
|
4140
|
+
).catch((err) => {
|
|
4141
|
+
console.error("Library pick failed:", err);
|
|
4142
|
+
});
|
|
4143
|
+
} : null;
|
|
3723
4144
|
const updateFilesDisplay = () => {
|
|
3724
4145
|
const currentlyReadonly = isElementReadonly(element, state);
|
|
3725
|
-
renderResourcePills(
|
|
3726
|
-
list,
|
|
3727
|
-
initialFiles,
|
|
4146
|
+
renderResourcePills({
|
|
4147
|
+
container: list,
|
|
4148
|
+
rids: initialFiles,
|
|
3728
4149
|
state,
|
|
3729
|
-
currentlyReadonly ? null : (index) => {
|
|
4150
|
+
onRemove: currentlyReadonly ? null : (index) => {
|
|
3730
4151
|
releaseLocalFileUrl(state.resourceIndex.get(index)?.file);
|
|
3731
4152
|
initialFiles.splice(initialFiles.indexOf(index), 1);
|
|
3732
4153
|
updateFilesDisplay();
|
|
3733
4154
|
},
|
|
3734
|
-
multipleFilesHint,
|
|
3735
|
-
buildCountInfo(),
|
|
3736
|
-
maxFiles < Infinity ? maxFiles : void 0,
|
|
3737
|
-
currentlyReadonly
|
|
3738
|
-
|
|
4155
|
+
hint: multipleFilesHint,
|
|
4156
|
+
countInfo: buildCountInfo(),
|
|
4157
|
+
maxCount: maxFiles < Infinity ? maxFiles : void 0,
|
|
4158
|
+
isReadonly: currentlyReadonly,
|
|
4159
|
+
onLibraryPick: currentlyReadonly ? null : onLibraryPickMultiple
|
|
4160
|
+
});
|
|
3739
4161
|
};
|
|
3740
4162
|
setupFilesDropHandler(
|
|
3741
4163
|
filesContainer,
|
|
@@ -3792,18 +4214,29 @@ function validateFileCount(key, resourceIds, element, state, errors) {
|
|
|
3792
4214
|
errors.push(`${key}: ${t("maxFiles", state, { max: maxFiles })}`);
|
|
3793
4215
|
}
|
|
3794
4216
|
}
|
|
3795
|
-
function
|
|
4217
|
+
function validateFileTypes(key, resourceIds, element, state, errors) {
|
|
3796
4218
|
const acceptField = "accept" in element ? element.accept : void 0;
|
|
3797
4219
|
const allowedExtensions = getAllowedExtensions(acceptField);
|
|
3798
|
-
|
|
4220
|
+
const allowedMimes = getAllowedMimes(acceptField);
|
|
4221
|
+
if (allowedExtensions.length === 0 && allowedMimes.length === 0) return;
|
|
3799
4222
|
const formats = allowedExtensions.join(", ");
|
|
4223
|
+
const mimes = allowedMimes.join(", ");
|
|
3800
4224
|
for (const rid of resourceIds) {
|
|
3801
4225
|
const meta = state.resourceIndex.get(rid);
|
|
3802
4226
|
const fileName = meta?.name ?? rid;
|
|
3803
|
-
if (!isFileExtensionAllowed(fileName, allowedExtensions)) {
|
|
4227
|
+
if (allowedExtensions.length > 0 && !isFileExtensionAllowed(fileName, allowedExtensions)) {
|
|
3804
4228
|
errors.push(
|
|
3805
4229
|
`${key}: ${t("invalidFileExtension", state, { name: fileName, formats })}`
|
|
3806
4230
|
);
|
|
4231
|
+
continue;
|
|
4232
|
+
}
|
|
4233
|
+
if (allowedMimes.length > 0 && !meta?.inferredFromExtension) {
|
|
4234
|
+
const mimeType = meta?.type ?? "";
|
|
4235
|
+
if (!isMimeAllowed(mimeType, allowedMimes)) {
|
|
4236
|
+
errors.push(
|
|
4237
|
+
`${key}: ${t("invalidFileMime", state, { name: fileName, type: mimeType, mimes })}`
|
|
4238
|
+
);
|
|
4239
|
+
}
|
|
3807
4240
|
}
|
|
3808
4241
|
}
|
|
3809
4242
|
}
|
|
@@ -3827,7 +4260,7 @@ function validateMultiFile(element, key, context) {
|
|
|
3827
4260
|
const resourceIds = readMultiFileResourceIds(scopeRoot, fullKey);
|
|
3828
4261
|
if (!skipValidation) {
|
|
3829
4262
|
validateFileCount(key, resourceIds, element, state, errors);
|
|
3830
|
-
|
|
4263
|
+
validateFileTypes(key, resourceIds, element, state, errors);
|
|
3831
4264
|
validateFileSizes(key, resourceIds, element, state, errors);
|
|
3832
4265
|
}
|
|
3833
4266
|
return { value: resourceIds, errors };
|
|
@@ -3844,7 +4277,7 @@ function validateSingleFile(element, key, context) {
|
|
|
3844
4277
|
return { value: null, errors };
|
|
3845
4278
|
}
|
|
3846
4279
|
if (!skipValidation && rid !== "") {
|
|
3847
|
-
|
|
4280
|
+
validateFileTypes(key, [rid], element, state, errors);
|
|
3848
4281
|
validateFileSizes(key, [rid], element, state, errors);
|
|
3849
4282
|
}
|
|
3850
4283
|
return { value: rid || null, errors };
|
|
@@ -3970,9 +4403,7 @@ function updateFileField(element, fieldPath, value, context) {
|
|
|
3970
4403
|
}
|
|
3971
4404
|
value.forEach((resourceId) => {
|
|
3972
4405
|
if (resourceId && typeof resourceId === "string") {
|
|
3973
|
-
|
|
3974
|
-
addResourceToIndex(resourceId, state);
|
|
3975
|
-
}
|
|
4406
|
+
seedInferredResource(resourceId, state.resourceIndex);
|
|
3976
4407
|
}
|
|
3977
4408
|
});
|
|
3978
4409
|
const filesWrapper = scopeRoot.querySelector(
|
|
@@ -3997,34 +4428,13 @@ function updateFileField(element, fieldPath, value, context) {
|
|
|
3997
4428
|
}
|
|
3998
4429
|
hiddenInput.value = value != null ? String(value) : "";
|
|
3999
4430
|
if (value && typeof value === "string") {
|
|
4000
|
-
|
|
4001
|
-
addResourceToIndex(value, state);
|
|
4002
|
-
}
|
|
4431
|
+
seedInferredResource(value, state.resourceIndex);
|
|
4003
4432
|
console.info(
|
|
4004
4433
|
`updateFileField: File field "${fieldPath}" updated. Preview update requires re-render.`
|
|
4005
4434
|
);
|
|
4006
4435
|
}
|
|
4007
4436
|
}
|
|
4008
4437
|
}
|
|
4009
|
-
function addResourceToIndex(resourceId, state) {
|
|
4010
|
-
const filename = resourceId.split("/").pop() || "file";
|
|
4011
|
-
const extension = filename.split(".").pop()?.toLowerCase();
|
|
4012
|
-
let fileType = "application/octet-stream";
|
|
4013
|
-
if (extension) {
|
|
4014
|
-
if (["jpg", "jpeg", "png", "gif", "webp"].includes(extension)) {
|
|
4015
|
-
fileType = `image/${extension === "jpg" ? "jpeg" : extension}`;
|
|
4016
|
-
} else if (["mp4", "webm", "mov", "avi"].includes(extension)) {
|
|
4017
|
-
fileType = `video/${extension === "mov" ? "quicktime" : extension}`;
|
|
4018
|
-
}
|
|
4019
|
-
}
|
|
4020
|
-
state.resourceIndex.set(resourceId, {
|
|
4021
|
-
name: filename,
|
|
4022
|
-
type: fileType,
|
|
4023
|
-
size: 0,
|
|
4024
|
-
uploadedAt: /* @__PURE__ */ new Date(),
|
|
4025
|
-
file: void 0
|
|
4026
|
-
});
|
|
4027
|
-
}
|
|
4028
4438
|
|
|
4029
4439
|
// src/components/colour.ts
|
|
4030
4440
|
function normalizeColourValue(value) {
|
|
@@ -8668,6 +9078,7 @@ var defaultConfig = {
|
|
|
8668
9078
|
enableFilePreview: true,
|
|
8669
9079
|
maxPreviewSize: "200px",
|
|
8670
9080
|
readonly: false,
|
|
9081
|
+
pickExistingFiles: null,
|
|
8671
9082
|
parseTableFile: null,
|
|
8672
9083
|
locale: "en",
|
|
8673
9084
|
translations: {
|
|
@@ -8704,6 +9115,10 @@ var defaultConfig = {
|
|
|
8704
9115
|
fileCountRange: "({min}-{max})",
|
|
8705
9116
|
uploadingFile: "Uploading\u2026",
|
|
8706
9117
|
filesCounter: "{count}/{max}",
|
|
9118
|
+
fromLibrary: "From library",
|
|
9119
|
+
libraryEmpty: "Library is empty",
|
|
9120
|
+
libraryHint: "Choose from previously uploaded files",
|
|
9121
|
+
pickerError: "Failed to load files from library",
|
|
8707
9122
|
// Validation errors
|
|
8708
9123
|
required: "Required",
|
|
8709
9124
|
minItems: "Minimum {min} items required",
|
|
@@ -8719,6 +9134,7 @@ var defaultConfig = {
|
|
|
8719
9134
|
minFiles: "Minimum {min} files required",
|
|
8720
9135
|
maxFiles: "Maximum {max} files allowed",
|
|
8721
9136
|
invalidFileExtension: 'File "{name}" has unsupported format. Allowed: {formats}',
|
|
9137
|
+
invalidFileMime: 'File "{name}": file type {type} not allowed (allowed: {mimes})',
|
|
8722
9138
|
fileTooLarge: 'File "{name}" exceeds maximum size of {maxSize}MB',
|
|
8723
9139
|
filesLimitExceeded: "{skipped} file(s) skipped: maximum {max} files allowed",
|
|
8724
9140
|
unsupportedFieldType: "Unsupported field type: {type}",
|
|
@@ -8770,6 +9186,10 @@ var defaultConfig = {
|
|
|
8770
9186
|
fileCountRange: "({min}-{max})",
|
|
8771
9187
|
uploadingFile: "\u0417\u0430\u0433\u0440\u0443\u0437\u043A\u0430\u2026",
|
|
8772
9188
|
filesCounter: "{count}/{max}",
|
|
9189
|
+
fromLibrary: "\u0418\u0437 \u0431\u0438\u0431\u043B\u0438\u043E\u0442\u0435\u043A\u0438",
|
|
9190
|
+
libraryEmpty: "\u0411\u0438\u0431\u043B\u0438\u043E\u0442\u0435\u043A\u0430 \u043F\u0443\u0441\u0442\u0430",
|
|
9191
|
+
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",
|
|
9192
|
+
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",
|
|
8773
9193
|
// Validation errors
|
|
8774
9194
|
required: "\u041E\u0431\u044F\u0437\u0430\u0442\u0435\u043B\u044C\u043D\u043E\u0435 \u043F\u043E\u043B\u0435",
|
|
8775
9195
|
minItems: "\u041C\u0438\u043D\u0438\u043C\u0443\u043C {min} \u044D\u043B\u0435\u043C\u0435\u043D\u0442\u043E\u0432",
|
|
@@ -8785,6 +9205,7 @@ var defaultConfig = {
|
|
|
8785
9205
|
minFiles: "\u041C\u0438\u043D\u0438\u043C\u0443\u043C {min} \u0444\u0430\u0439\u043B\u043E\u0432",
|
|
8786
9206
|
maxFiles: "\u041C\u0430\u043A\u0441\u0438\u043C\u0443\u043C {max} \u0444\u0430\u0439\u043B\u043E\u0432",
|
|
8787
9207
|
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}',
|
|
9208
|
+
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})',
|
|
8788
9209
|
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',
|
|
8789
9210
|
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",
|
|
8790
9211
|
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}",
|