@dmitryvim/form-builder 0.2.27 → 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 +57 -1
- package/dist/browser/formbuilder.min.js +207 -125
- package/dist/browser/formbuilder.v0.2.28.min.js +956 -0
- package/dist/cjs/index.cjs +542 -133
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/esm/index.js +528 -122
- 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/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;
|
|
@@ -3222,7 +3328,18 @@ async function uploadSingleFile(file, state) {
|
|
|
3222
3328
|
throw new Error(`File upload failed: ${err.message}`);
|
|
3223
3329
|
}
|
|
3224
3330
|
}
|
|
3225
|
-
async function handleFileSelect(
|
|
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;
|
|
3226
3343
|
if (!isFileExtensionAllowed(file.name, allowedExtensions)) {
|
|
3227
3344
|
const formats = allowedExtensions.join(", ");
|
|
3228
3345
|
showFileError(
|
|
@@ -3231,6 +3348,14 @@ async function handleFileSelect(file, container, fieldName, state, deps = null,
|
|
|
3231
3348
|
);
|
|
3232
3349
|
return;
|
|
3233
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
|
+
}
|
|
3234
3359
|
if (!isFileSizeAllowed(file, maxSizeMB)) {
|
|
3235
3360
|
showFileError(
|
|
3236
3361
|
container,
|
|
@@ -3290,10 +3415,16 @@ function filterAndSlice(allFiles, currentCount, constraints, state) {
|
|
|
3290
3415
|
const afterExt = allFiles.filter(
|
|
3291
3416
|
(f) => isFileExtensionAllowed(f.name, constraints.allowedExtensions)
|
|
3292
3417
|
);
|
|
3293
|
-
const
|
|
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(
|
|
3294
3425
|
(f) => !isFileSizeAllowed(f, constraints.maxSize)
|
|
3295
3426
|
);
|
|
3296
|
-
const valid =
|
|
3427
|
+
const valid = afterMime.filter((f) => isFileSizeAllowed(f, constraints.maxSize));
|
|
3297
3428
|
const remaining = constraints.maxCount === Infinity ? valid.length : Math.max(0, constraints.maxCount - currentCount);
|
|
3298
3429
|
const accepted = valid.slice(0, remaining);
|
|
3299
3430
|
const skippedByCount = valid.length - accepted.length;
|
|
@@ -3303,6 +3434,11 @@ function filterAndSlice(allFiles, currentCount, constraints, state) {
|
|
|
3303
3434
|
const names = rejectedByExt.map((f) => f.name).join(", ");
|
|
3304
3435
|
errorParts.push(t("invalidFileExtension", state, { name: names, formats }));
|
|
3305
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
|
+
}
|
|
3306
3442
|
if (rejectedBySize.length > 0) {
|
|
3307
3443
|
const names = rejectedBySize.map((f) => f.name).join(", ");
|
|
3308
3444
|
errorParts.push(
|
|
@@ -3394,27 +3530,159 @@ function setupFilesPickerHandler(filesPicker, resourceIds, state, updateCallback
|
|
|
3394
3530
|
};
|
|
3395
3531
|
}
|
|
3396
3532
|
|
|
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
|
-
|
|
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: []
|
|
3416
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);
|
|
3417
3680
|
}
|
|
3681
|
+
}
|
|
3682
|
+
|
|
3683
|
+
// src/components/file/render-edit.ts
|
|
3684
|
+
function handleInitialFileData(initial, fileContainer, pathKey, fileWrapper, state, deps) {
|
|
3685
|
+
seedInferredResource(initial, state.resourceIndex);
|
|
3418
3686
|
const meta = state.resourceIndex.get(initial);
|
|
3419
3687
|
const isVideo = meta?.type?.startsWith("video/");
|
|
3420
3688
|
if (isVideo) {
|
|
@@ -3432,7 +3700,50 @@ function handleInitialFileData(initial, fileContainer, pathKey, fileWrapper, sta
|
|
|
3432
3700
|
hiddenInput.value = initial;
|
|
3433
3701
|
fileWrapper.appendChild(hiddenInput);
|
|
3434
3702
|
}
|
|
3435
|
-
|
|
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;
|
|
3436
3747
|
ensureFileStyles();
|
|
3437
3748
|
const wrapper = container.closest("[data-files-wrapper]");
|
|
3438
3749
|
if (wrapper) {
|
|
@@ -3441,6 +3752,7 @@ function renderResourcePills(container, rids, state, onRemove, hint, countInfo,
|
|
|
3441
3752
|
while (container.firstChild) container.removeChild(container.firstChild);
|
|
3442
3753
|
const ridList = rids ?? [];
|
|
3443
3754
|
const atMax = maxCount !== void 0 && ridList.length >= maxCount;
|
|
3755
|
+
const hasLibrary = !isReadonly && typeof onLibraryPick === "function";
|
|
3444
3756
|
const buildSubHint = () => {
|
|
3445
3757
|
const parts = [];
|
|
3446
3758
|
if (hint) parts.push(hint);
|
|
@@ -3457,18 +3769,26 @@ function renderResourcePills(container, rids, state, onRemove, hint, countInfo,
|
|
|
3457
3769
|
emptyEl.className = "fb-tile-empty-text";
|
|
3458
3770
|
emptyEl.textContent = t("noFilesSelected", state);
|
|
3459
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);
|
|
3460
3785
|
} 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;
|
|
3786
|
+
const dropzone = buildEmptyDropzone(
|
|
3787
|
+
state,
|
|
3788
|
+
t("clickDragTextMultiple", state),
|
|
3789
|
+
buildSubHint(),
|
|
3790
|
+
openPicker
|
|
3791
|
+
);
|
|
3472
3792
|
container.appendChild(dropzone);
|
|
3473
3793
|
}
|
|
3474
3794
|
return;
|
|
@@ -3499,6 +3819,10 @@ function renderResourcePills(container, rids, state, onRemove, hint, countInfo,
|
|
|
3499
3819
|
addTile.innerHTML = "+";
|
|
3500
3820
|
addTile.onclick = openPicker;
|
|
3501
3821
|
tilesWrap.appendChild(addTile);
|
|
3822
|
+
if (hasLibrary) {
|
|
3823
|
+
const libraryTile = buildLibraryButton("tile", state, onLibraryPick);
|
|
3824
|
+
tilesWrap.appendChild(libraryTile);
|
|
3825
|
+
}
|
|
3502
3826
|
} else if (!isReadonly && atMax) {
|
|
3503
3827
|
const chip = document.createElement("div");
|
|
3504
3828
|
chip.className = "fb-tile-counter";
|
|
@@ -3526,12 +3850,16 @@ function renderFileElementEdit(element, ctx, wrapper, pathKey) {
|
|
|
3526
3850
|
picker.name = pathKey;
|
|
3527
3851
|
picker.style.display = "none";
|
|
3528
3852
|
if (element.accept) {
|
|
3529
|
-
picker.accept = typeof element.accept === "string" ? element.accept :
|
|
3853
|
+
picker.accept = typeof element.accept === "string" ? element.accept : [
|
|
3854
|
+
...element.accept.extensions?.map((ext) => `.${ext}`) ?? [],
|
|
3855
|
+
...element.accept.mime ?? []
|
|
3856
|
+
].join(",") || "";
|
|
3530
3857
|
}
|
|
3531
3858
|
const fileContainer = document.createElement("div");
|
|
3532
3859
|
fileContainer.className = "file-preview-container";
|
|
3533
3860
|
const initial = ctx.prefill[element.key];
|
|
3534
3861
|
const allowedExts = getAllowedExtensions(element.accept);
|
|
3862
|
+
const allowedMimes = getAllowedMimes(element.accept);
|
|
3535
3863
|
const maxSizeMB = element.maxSize ?? Infinity;
|
|
3536
3864
|
const handlers = {
|
|
3537
3865
|
fileUploadHandler() {
|
|
@@ -3539,16 +3867,17 @@ function renderFileElementEdit(element, ctx, wrapper, pathKey) {
|
|
|
3539
3867
|
},
|
|
3540
3868
|
dragHandler(files) {
|
|
3541
3869
|
if (files.length > 0) {
|
|
3542
|
-
handleFileSelect(
|
|
3543
|
-
files[0],
|
|
3544
|
-
fileContainer,
|
|
3545
|
-
pathKey,
|
|
3870
|
+
handleFileSelect({
|
|
3871
|
+
file: files[0],
|
|
3872
|
+
container: fileContainer,
|
|
3873
|
+
fieldName: pathKey,
|
|
3546
3874
|
state,
|
|
3547
|
-
buildDeps(),
|
|
3548
|
-
ctx.instance,
|
|
3549
|
-
allowedExts,
|
|
3875
|
+
deps: buildDeps(),
|
|
3876
|
+
instance: ctx.instance,
|
|
3877
|
+
allowedExtensions: allowedExts,
|
|
3878
|
+
allowedMimes,
|
|
3550
3879
|
maxSizeMB
|
|
3551
|
-
);
|
|
3880
|
+
});
|
|
3552
3881
|
}
|
|
3553
3882
|
},
|
|
3554
3883
|
setupDrop(container) {
|
|
@@ -3579,6 +3908,47 @@ function renderFileElementEdit(element, ctx, wrapper, pathKey) {
|
|
|
3579
3908
|
setupDrop: handlers.setupDrop,
|
|
3580
3909
|
onRemove: handlers.onRemove
|
|
3581
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,
|
|
3932
|
+
fileContainer,
|
|
3933
|
+
fileWrapper,
|
|
3934
|
+
pathKey,
|
|
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);
|
|
3942
|
+
});
|
|
3943
|
+
});
|
|
3944
|
+
libraryBtn.style.cssText = "flex:1;min-width:0;";
|
|
3945
|
+
row.appendChild(uploadCard);
|
|
3946
|
+
row.appendChild(libraryBtn);
|
|
3947
|
+
fileContainer.appendChild(row);
|
|
3948
|
+
} else {
|
|
3949
|
+
handlers.restoreDropzone();
|
|
3950
|
+
}
|
|
3951
|
+
};
|
|
3582
3952
|
if (initial) {
|
|
3583
3953
|
handleInitialFileData(
|
|
3584
3954
|
initial,
|
|
@@ -3594,20 +3964,21 @@ function renderFileElementEdit(element, ctx, wrapper, pathKey) {
|
|
|
3594
3964
|
setupDragAndDrop(fileContainer, handlers.dragHandler);
|
|
3595
3965
|
}
|
|
3596
3966
|
} else {
|
|
3597
|
-
|
|
3967
|
+
renderEmptySingleState();
|
|
3598
3968
|
}
|
|
3599
3969
|
picker.onchange = () => {
|
|
3600
3970
|
if (picker.files && picker.files.length > 0) {
|
|
3601
|
-
handleFileSelect(
|
|
3602
|
-
picker.files[0],
|
|
3603
|
-
fileContainer,
|
|
3604
|
-
pathKey,
|
|
3971
|
+
handleFileSelect({
|
|
3972
|
+
file: picker.files[0],
|
|
3973
|
+
container: fileContainer,
|
|
3974
|
+
fieldName: pathKey,
|
|
3605
3975
|
state,
|
|
3606
|
-
buildDeps(),
|
|
3607
|
-
ctx.instance,
|
|
3608
|
-
allowedExts,
|
|
3976
|
+
deps: buildDeps(),
|
|
3977
|
+
instance: ctx.instance,
|
|
3978
|
+
allowedExtensions: allowedExts,
|
|
3979
|
+
allowedMimes,
|
|
3609
3980
|
maxSizeMB
|
|
3610
|
-
);
|
|
3981
|
+
});
|
|
3611
3982
|
}
|
|
3612
3983
|
};
|
|
3613
3984
|
fileWrapper.appendChild(fileContainer);
|
|
@@ -3625,7 +3996,10 @@ function renderFilesElementEdit(element, ctx, wrapper, pathKey) {
|
|
|
3625
3996
|
filesPicker.multiple = true;
|
|
3626
3997
|
filesPicker.style.display = "none";
|
|
3627
3998
|
if (element.accept) {
|
|
3628
|
-
filesPicker.accept = typeof element.accept === "string" ? element.accept :
|
|
3999
|
+
filesPicker.accept = typeof element.accept === "string" ? element.accept : [
|
|
4000
|
+
...element.accept.extensions?.map((ext) => `.${ext}`) ?? [],
|
|
4001
|
+
...element.accept.mime ?? []
|
|
4002
|
+
].join(",") || "";
|
|
3629
4003
|
}
|
|
3630
4004
|
const filesContainer = document.createElement("div");
|
|
3631
4005
|
filesContainer.className = "files-list-wrapper";
|
|
@@ -3639,29 +4013,43 @@ function renderFilesElementEdit(element, ctx, wrapper, pathKey) {
|
|
|
3639
4013
|
const filesConstraints = {
|
|
3640
4014
|
maxCount: Infinity,
|
|
3641
4015
|
allowedExtensions: getAllowedExtensions(element.accept),
|
|
4016
|
+
allowedMimes: getAllowedMimes(element.accept),
|
|
3642
4017
|
maxSize: element.maxSize ?? Infinity
|
|
3643
4018
|
};
|
|
3644
4019
|
filesContainer.appendChild(list);
|
|
3645
4020
|
filesWrapper.appendChild(filesPicker);
|
|
3646
4021
|
filesWrapper.appendChild(filesContainer);
|
|
3647
4022
|
wrapper.appendChild(filesWrapper);
|
|
4023
|
+
const onLibraryPickFiles = state.config.pickExistingFiles && !element.disableLibrary ? () => {
|
|
4024
|
+
handleLibraryPickMulti(
|
|
4025
|
+
state,
|
|
4026
|
+
element,
|
|
4027
|
+
filesWrapper,
|
|
4028
|
+
pathKey,
|
|
4029
|
+
initialFiles,
|
|
4030
|
+
Infinity,
|
|
4031
|
+
updateFilesList,
|
|
4032
|
+
ctx.instance
|
|
4033
|
+
).catch((err) => {
|
|
4034
|
+
console.error("Library pick failed:", err);
|
|
4035
|
+
});
|
|
4036
|
+
} : null;
|
|
3648
4037
|
function updateFilesList() {
|
|
3649
4038
|
const currentlyReadonly = isElementReadonly(element, state);
|
|
3650
|
-
renderResourcePills(
|
|
3651
|
-
list,
|
|
3652
|
-
initialFiles,
|
|
4039
|
+
renderResourcePills({
|
|
4040
|
+
container: list,
|
|
4041
|
+
rids: initialFiles,
|
|
3653
4042
|
state,
|
|
3654
|
-
currentlyReadonly ? null : (ridToRemove) => {
|
|
4043
|
+
onRemove: currentlyReadonly ? null : (ridToRemove) => {
|
|
3655
4044
|
releaseLocalFileUrl(state.resourceIndex.get(ridToRemove)?.file);
|
|
3656
4045
|
const index = initialFiles.indexOf(ridToRemove);
|
|
3657
4046
|
if (index > -1) initialFiles.splice(index, 1);
|
|
3658
4047
|
updateFilesList();
|
|
3659
4048
|
},
|
|
3660
|
-
filesFieldHint,
|
|
3661
|
-
|
|
3662
|
-
|
|
3663
|
-
|
|
3664
|
-
);
|
|
4049
|
+
hint: filesFieldHint,
|
|
4050
|
+
isReadonly: currentlyReadonly,
|
|
4051
|
+
onLibraryPick: currentlyReadonly ? null : onLibraryPickFiles
|
|
4052
|
+
});
|
|
3665
4053
|
}
|
|
3666
4054
|
updateFilesList();
|
|
3667
4055
|
setupFilesDropHandler(
|
|
@@ -3696,7 +4084,10 @@ function renderMultipleFileElementEdit(element, ctx, wrapper, pathKey) {
|
|
|
3696
4084
|
filesPicker.multiple = true;
|
|
3697
4085
|
filesPicker.style.display = "none";
|
|
3698
4086
|
if (element.accept) {
|
|
3699
|
-
filesPicker.accept = typeof element.accept === "string" ? element.accept :
|
|
4087
|
+
filesPicker.accept = typeof element.accept === "string" ? element.accept : [
|
|
4088
|
+
...element.accept.extensions?.map((ext) => `.${ext}`) ?? [],
|
|
4089
|
+
...element.accept.mime ?? []
|
|
4090
|
+
].join(",") || "";
|
|
3700
4091
|
}
|
|
3701
4092
|
const filesContainer = document.createElement("div");
|
|
3702
4093
|
filesContainer.className = "files-list-wrapper";
|
|
@@ -3713,6 +4104,7 @@ function renderMultipleFileElementEdit(element, ctx, wrapper, pathKey) {
|
|
|
3713
4104
|
const multipleConstraints = {
|
|
3714
4105
|
maxCount: maxFiles,
|
|
3715
4106
|
allowedExtensions: getAllowedExtensions(element.accept),
|
|
4107
|
+
allowedMimes: getAllowedMimes(element.accept),
|
|
3716
4108
|
maxSize: element.maxSize ?? Infinity
|
|
3717
4109
|
};
|
|
3718
4110
|
const buildCountInfo = () => {
|
|
@@ -3720,22 +4112,37 @@ function renderMultipleFileElementEdit(element, ctx, wrapper, pathKey) {
|
|
|
3720
4112
|
const minMaxText = minFiles > 0 || maxFiles < Infinity ? ` ${t("fileCountRange", state, { min: minFiles, max: maxFiles })}` : "";
|
|
3721
4113
|
return countText + minMaxText;
|
|
3722
4114
|
};
|
|
4115
|
+
const onLibraryPickMultiple = state.config.pickExistingFiles && !element.disableLibrary ? () => {
|
|
4116
|
+
handleLibraryPickMulti(
|
|
4117
|
+
state,
|
|
4118
|
+
element,
|
|
4119
|
+
filesWrapper,
|
|
4120
|
+
pathKey,
|
|
4121
|
+
initialFiles,
|
|
4122
|
+
maxFiles,
|
|
4123
|
+
updateFilesDisplay,
|
|
4124
|
+
ctx.instance
|
|
4125
|
+
).catch((err) => {
|
|
4126
|
+
console.error("Library pick failed:", err);
|
|
4127
|
+
});
|
|
4128
|
+
} : null;
|
|
3723
4129
|
const updateFilesDisplay = () => {
|
|
3724
4130
|
const currentlyReadonly = isElementReadonly(element, state);
|
|
3725
|
-
renderResourcePills(
|
|
3726
|
-
list,
|
|
3727
|
-
initialFiles,
|
|
4131
|
+
renderResourcePills({
|
|
4132
|
+
container: list,
|
|
4133
|
+
rids: initialFiles,
|
|
3728
4134
|
state,
|
|
3729
|
-
currentlyReadonly ? null : (index) => {
|
|
4135
|
+
onRemove: currentlyReadonly ? null : (index) => {
|
|
3730
4136
|
releaseLocalFileUrl(state.resourceIndex.get(index)?.file);
|
|
3731
4137
|
initialFiles.splice(initialFiles.indexOf(index), 1);
|
|
3732
4138
|
updateFilesDisplay();
|
|
3733
4139
|
},
|
|
3734
|
-
multipleFilesHint,
|
|
3735
|
-
buildCountInfo(),
|
|
3736
|
-
maxFiles < Infinity ? maxFiles : void 0,
|
|
3737
|
-
currentlyReadonly
|
|
3738
|
-
|
|
4140
|
+
hint: multipleFilesHint,
|
|
4141
|
+
countInfo: buildCountInfo(),
|
|
4142
|
+
maxCount: maxFiles < Infinity ? maxFiles : void 0,
|
|
4143
|
+
isReadonly: currentlyReadonly,
|
|
4144
|
+
onLibraryPick: currentlyReadonly ? null : onLibraryPickMultiple
|
|
4145
|
+
});
|
|
3739
4146
|
};
|
|
3740
4147
|
setupFilesDropHandler(
|
|
3741
4148
|
filesContainer,
|
|
@@ -3792,18 +4199,29 @@ function validateFileCount(key, resourceIds, element, state, errors) {
|
|
|
3792
4199
|
errors.push(`${key}: ${t("maxFiles", state, { max: maxFiles })}`);
|
|
3793
4200
|
}
|
|
3794
4201
|
}
|
|
3795
|
-
function
|
|
4202
|
+
function validateFileTypes(key, resourceIds, element, state, errors) {
|
|
3796
4203
|
const acceptField = "accept" in element ? element.accept : void 0;
|
|
3797
4204
|
const allowedExtensions = getAllowedExtensions(acceptField);
|
|
3798
|
-
|
|
4205
|
+
const allowedMimes = getAllowedMimes(acceptField);
|
|
4206
|
+
if (allowedExtensions.length === 0 && allowedMimes.length === 0) return;
|
|
3799
4207
|
const formats = allowedExtensions.join(", ");
|
|
4208
|
+
const mimes = allowedMimes.join(", ");
|
|
3800
4209
|
for (const rid of resourceIds) {
|
|
3801
4210
|
const meta = state.resourceIndex.get(rid);
|
|
3802
4211
|
const fileName = meta?.name ?? rid;
|
|
3803
|
-
if (!isFileExtensionAllowed(fileName, allowedExtensions)) {
|
|
4212
|
+
if (allowedExtensions.length > 0 && !isFileExtensionAllowed(fileName, allowedExtensions)) {
|
|
3804
4213
|
errors.push(
|
|
3805
4214
|
`${key}: ${t("invalidFileExtension", state, { name: fileName, formats })}`
|
|
3806
4215
|
);
|
|
4216
|
+
continue;
|
|
4217
|
+
}
|
|
4218
|
+
if (allowedMimes.length > 0 && !meta?.inferredFromExtension) {
|
|
4219
|
+
const mimeType = meta?.type ?? "";
|
|
4220
|
+
if (!isMimeAllowed(mimeType, allowedMimes)) {
|
|
4221
|
+
errors.push(
|
|
4222
|
+
`${key}: ${t("invalidFileMime", state, { name: fileName, type: mimeType, mimes })}`
|
|
4223
|
+
);
|
|
4224
|
+
}
|
|
3807
4225
|
}
|
|
3808
4226
|
}
|
|
3809
4227
|
}
|
|
@@ -3827,7 +4245,7 @@ function validateMultiFile(element, key, context) {
|
|
|
3827
4245
|
const resourceIds = readMultiFileResourceIds(scopeRoot, fullKey);
|
|
3828
4246
|
if (!skipValidation) {
|
|
3829
4247
|
validateFileCount(key, resourceIds, element, state, errors);
|
|
3830
|
-
|
|
4248
|
+
validateFileTypes(key, resourceIds, element, state, errors);
|
|
3831
4249
|
validateFileSizes(key, resourceIds, element, state, errors);
|
|
3832
4250
|
}
|
|
3833
4251
|
return { value: resourceIds, errors };
|
|
@@ -3844,7 +4262,7 @@ function validateSingleFile(element, key, context) {
|
|
|
3844
4262
|
return { value: null, errors };
|
|
3845
4263
|
}
|
|
3846
4264
|
if (!skipValidation && rid !== "") {
|
|
3847
|
-
|
|
4265
|
+
validateFileTypes(key, [rid], element, state, errors);
|
|
3848
4266
|
validateFileSizes(key, [rid], element, state, errors);
|
|
3849
4267
|
}
|
|
3850
4268
|
return { value: rid || null, errors };
|
|
@@ -3970,9 +4388,7 @@ function updateFileField(element, fieldPath, value, context) {
|
|
|
3970
4388
|
}
|
|
3971
4389
|
value.forEach((resourceId) => {
|
|
3972
4390
|
if (resourceId && typeof resourceId === "string") {
|
|
3973
|
-
|
|
3974
|
-
addResourceToIndex(resourceId, state);
|
|
3975
|
-
}
|
|
4391
|
+
seedInferredResource(resourceId, state.resourceIndex);
|
|
3976
4392
|
}
|
|
3977
4393
|
});
|
|
3978
4394
|
const filesWrapper = scopeRoot.querySelector(
|
|
@@ -3997,34 +4413,13 @@ function updateFileField(element, fieldPath, value, context) {
|
|
|
3997
4413
|
}
|
|
3998
4414
|
hiddenInput.value = value != null ? String(value) : "";
|
|
3999
4415
|
if (value && typeof value === "string") {
|
|
4000
|
-
|
|
4001
|
-
addResourceToIndex(value, state);
|
|
4002
|
-
}
|
|
4416
|
+
seedInferredResource(value, state.resourceIndex);
|
|
4003
4417
|
console.info(
|
|
4004
4418
|
`updateFileField: File field "${fieldPath}" updated. Preview update requires re-render.`
|
|
4005
4419
|
);
|
|
4006
4420
|
}
|
|
4007
4421
|
}
|
|
4008
4422
|
}
|
|
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
4423
|
|
|
4029
4424
|
// src/components/colour.ts
|
|
4030
4425
|
function normalizeColourValue(value) {
|
|
@@ -8668,6 +9063,7 @@ var defaultConfig = {
|
|
|
8668
9063
|
enableFilePreview: true,
|
|
8669
9064
|
maxPreviewSize: "200px",
|
|
8670
9065
|
readonly: false,
|
|
9066
|
+
pickExistingFiles: null,
|
|
8671
9067
|
parseTableFile: null,
|
|
8672
9068
|
locale: "en",
|
|
8673
9069
|
translations: {
|
|
@@ -8704,6 +9100,10 @@ var defaultConfig = {
|
|
|
8704
9100
|
fileCountRange: "({min}-{max})",
|
|
8705
9101
|
uploadingFile: "Uploading\u2026",
|
|
8706
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",
|
|
8707
9107
|
// Validation errors
|
|
8708
9108
|
required: "Required",
|
|
8709
9109
|
minItems: "Minimum {min} items required",
|
|
@@ -8719,6 +9119,7 @@ var defaultConfig = {
|
|
|
8719
9119
|
minFiles: "Minimum {min} files required",
|
|
8720
9120
|
maxFiles: "Maximum {max} files allowed",
|
|
8721
9121
|
invalidFileExtension: 'File "{name}" has unsupported format. Allowed: {formats}',
|
|
9122
|
+
invalidFileMime: 'File "{name}": file type {type} not allowed (allowed: {mimes})',
|
|
8722
9123
|
fileTooLarge: 'File "{name}" exceeds maximum size of {maxSize}MB',
|
|
8723
9124
|
filesLimitExceeded: "{skipped} file(s) skipped: maximum {max} files allowed",
|
|
8724
9125
|
unsupportedFieldType: "Unsupported field type: {type}",
|
|
@@ -8770,6 +9171,10 @@ var defaultConfig = {
|
|
|
8770
9171
|
fileCountRange: "({min}-{max})",
|
|
8771
9172
|
uploadingFile: "\u0417\u0430\u0433\u0440\u0443\u0437\u043A\u0430\u2026",
|
|
8772
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",
|
|
8773
9178
|
// Validation errors
|
|
8774
9179
|
required: "\u041E\u0431\u044F\u0437\u0430\u0442\u0435\u043B\u044C\u043D\u043E\u0435 \u043F\u043E\u043B\u0435",
|
|
8775
9180
|
minItems: "\u041C\u0438\u043D\u0438\u043C\u0443\u043C {min} \u044D\u043B\u0435\u043C\u0435\u043D\u0442\u043E\u0432",
|
|
@@ -8785,6 +9190,7 @@ var defaultConfig = {
|
|
|
8785
9190
|
minFiles: "\u041C\u0438\u043D\u0438\u043C\u0443\u043C {min} \u0444\u0430\u0439\u043B\u043E\u0432",
|
|
8786
9191
|
maxFiles: "\u041C\u0430\u043A\u0441\u0438\u043C\u0443\u043C {max} \u0444\u0430\u0439\u043B\u043E\u0432",
|
|
8787
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})',
|
|
8788
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',
|
|
8789
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",
|
|
8790
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}",
|