@dmitryvim/form-builder 0.2.25 → 0.2.27
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +68 -14
- package/dist/browser/formbuilder.min.js +461 -193
- package/dist/browser/formbuilder.v0.2.27.min.js +874 -0
- package/dist/cjs/index.cjs +1793 -1098
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/esm/index.js +1773 -1091
- package/dist/esm/index.js.map +1 -1
- package/dist/form-builder.js +461 -193
- package/dist/types/components/file/constraints.d.ts +26 -0
- package/dist/types/components/file/dom.d.ts +44 -0
- package/dist/types/components/file/preview.d.ts +69 -0
- package/dist/types/components/file/render-edit.d.ts +15 -0
- package/dist/types/components/file/render-readonly.d.ts +23 -0
- package/dist/types/components/file/styles.d.ts +1 -0
- package/dist/types/components/file/upload.d.ts +13 -0
- package/dist/types/components/file/validate.d.ts +13 -0
- package/dist/types/components/file.d.ts +5 -27
- package/dist/types/types/config.d.ts +4 -0
- package/dist/types/types/schema.d.ts +2 -0
- package/dist/types/utils/helpers.d.ts +6 -0
- package/package.json +1 -1
- package/dist/browser/formbuilder.v0.2.25.min.js +0 -606
package/dist/esm/index.js
CHANGED
|
@@ -6,7 +6,10 @@ function t(key, state, params) {
|
|
|
6
6
|
let text = localeTranslations?.[key] || fallbackTranslations?.[key] || key;
|
|
7
7
|
if (params) {
|
|
8
8
|
for (const [paramKey, paramValue] of Object.entries(params)) {
|
|
9
|
-
text = text.replace(
|
|
9
|
+
text = text.replace(
|
|
10
|
+
new RegExp(`\\{${paramKey}\\}`, "g"),
|
|
11
|
+
String(paramValue)
|
|
12
|
+
);
|
|
10
13
|
}
|
|
11
14
|
}
|
|
12
15
|
return text;
|
|
@@ -249,6 +252,9 @@ function validateSchema(schema) {
|
|
|
249
252
|
}
|
|
250
253
|
|
|
251
254
|
// src/utils/helpers.ts
|
|
255
|
+
function isElementReadonly(element, state, ctx) {
|
|
256
|
+
return element.readonly === true || state.config.readonly === true || ctx?.inheritedReadonly === true;
|
|
257
|
+
}
|
|
252
258
|
function isPlainObject(obj) {
|
|
253
259
|
return obj && typeof obj === "object" && obj.constructor === Object;
|
|
254
260
|
}
|
|
@@ -407,6 +413,7 @@ function createCharCounter(element, input, isTextarea = false) {
|
|
|
407
413
|
}
|
|
408
414
|
function renderTextElement(element, ctx, wrapper, pathKey) {
|
|
409
415
|
const state = ctx.state;
|
|
416
|
+
const readonly = isElementReadonly(element, state, ctx);
|
|
410
417
|
const inputWrapper = document.createElement("div");
|
|
411
418
|
inputWrapper.style.cssText = "position: relative;";
|
|
412
419
|
const textInput = document.createElement("input");
|
|
@@ -417,7 +424,7 @@ function renderTextElement(element, ctx, wrapper, pathKey) {
|
|
|
417
424
|
padding-right: 60px;
|
|
418
425
|
border: var(--fb-border-width) solid var(--fb-border-color);
|
|
419
426
|
border-radius: var(--fb-border-radius);
|
|
420
|
-
background-color: ${
|
|
427
|
+
background-color: ${readonly ? "var(--fb-background-readonly-color)" : "var(--fb-background-color)"};
|
|
421
428
|
color: var(--fb-text-color);
|
|
422
429
|
font-size: var(--fb-font-size);
|
|
423
430
|
font-family: var(--fb-font-family);
|
|
@@ -428,8 +435,8 @@ function renderTextElement(element, ctx, wrapper, pathKey) {
|
|
|
428
435
|
textInput.name = pathKey;
|
|
429
436
|
textInput.placeholder = element.placeholder || "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0442\u0435\u043A\u0441\u0442";
|
|
430
437
|
textInput.value = ctx.prefill[element.key] || element.default || "";
|
|
431
|
-
textInput.readOnly =
|
|
432
|
-
if (!
|
|
438
|
+
textInput.readOnly = readonly;
|
|
439
|
+
if (!readonly) {
|
|
433
440
|
textInput.addEventListener("focus", () => {
|
|
434
441
|
textInput.style.borderColor = "var(--fb-border-focus-color)";
|
|
435
442
|
textInput.style.outline = `var(--fb-focus-ring-width) solid var(--fb-focus-ring-color)`;
|
|
@@ -450,7 +457,7 @@ function renderTextElement(element, ctx, wrapper, pathKey) {
|
|
|
450
457
|
}
|
|
451
458
|
});
|
|
452
459
|
}
|
|
453
|
-
if (!
|
|
460
|
+
if (!readonly && ctx.instance) {
|
|
454
461
|
const handleChange = () => {
|
|
455
462
|
const value = textInput.value === "" ? null : textInput.value;
|
|
456
463
|
ctx.instance.triggerOnChange(pathKey, value);
|
|
@@ -459,7 +466,7 @@ function renderTextElement(element, ctx, wrapper, pathKey) {
|
|
|
459
466
|
textInput.addEventListener("input", handleChange);
|
|
460
467
|
}
|
|
461
468
|
inputWrapper.appendChild(textInput);
|
|
462
|
-
if (!
|
|
469
|
+
if (!readonly && (element.minLength != null || element.maxLength != null)) {
|
|
463
470
|
const counter = createCharCounter(element, textInput, false);
|
|
464
471
|
inputWrapper.appendChild(counter);
|
|
465
472
|
}
|
|
@@ -467,6 +474,7 @@ function renderTextElement(element, ctx, wrapper, pathKey) {
|
|
|
467
474
|
}
|
|
468
475
|
function renderMultipleTextElement(element, ctx, wrapper, pathKey) {
|
|
469
476
|
const state = ctx.state;
|
|
477
|
+
const readonly = isElementReadonly(element, state, ctx);
|
|
470
478
|
const prefillValues = ctx.prefill[element.key] || [];
|
|
471
479
|
const values = Array.isArray(prefillValues) ? [...prefillValues] : [];
|
|
472
480
|
const minCount = element.minCount ?? 1;
|
|
@@ -498,7 +506,7 @@ function renderMultipleTextElement(element, ctx, wrapper, pathKey) {
|
|
|
498
506
|
padding-right: 60px;
|
|
499
507
|
border: var(--fb-border-width) solid var(--fb-border-color);
|
|
500
508
|
border-radius: var(--fb-border-radius);
|
|
501
|
-
background-color: ${
|
|
509
|
+
background-color: ${readonly ? "var(--fb-background-readonly-color)" : "var(--fb-background-color)"};
|
|
502
510
|
color: var(--fb-text-color);
|
|
503
511
|
font-size: var(--fb-font-size);
|
|
504
512
|
font-family: var(--fb-font-family);
|
|
@@ -508,8 +516,8 @@ function renderMultipleTextElement(element, ctx, wrapper, pathKey) {
|
|
|
508
516
|
`;
|
|
509
517
|
textInput.placeholder = element.placeholder || t("placeholderText", state);
|
|
510
518
|
textInput.value = value;
|
|
511
|
-
textInput.readOnly =
|
|
512
|
-
if (!
|
|
519
|
+
textInput.readOnly = readonly;
|
|
520
|
+
if (!readonly) {
|
|
513
521
|
textInput.addEventListener("focus", () => {
|
|
514
522
|
textInput.style.borderColor = "var(--fb-border-focus-color)";
|
|
515
523
|
textInput.style.outline = `var(--fb-focus-ring-width) solid var(--fb-focus-ring-color)`;
|
|
@@ -530,7 +538,7 @@ function renderMultipleTextElement(element, ctx, wrapper, pathKey) {
|
|
|
530
538
|
}
|
|
531
539
|
});
|
|
532
540
|
}
|
|
533
|
-
if (!
|
|
541
|
+
if (!readonly && ctx.instance) {
|
|
534
542
|
const handleChange = () => {
|
|
535
543
|
const value2 = textInput.value === "" ? null : textInput.value;
|
|
536
544
|
ctx.instance.triggerOnChange(textInput.name, value2);
|
|
@@ -539,7 +547,7 @@ function renderMultipleTextElement(element, ctx, wrapper, pathKey) {
|
|
|
539
547
|
textInput.addEventListener("input", handleChange);
|
|
540
548
|
}
|
|
541
549
|
inputContainer.appendChild(textInput);
|
|
542
|
-
if (!
|
|
550
|
+
if (!readonly && (element.minLength != null || element.maxLength != null)) {
|
|
543
551
|
const counter = createCharCounter(element, textInput, false);
|
|
544
552
|
inputContainer.appendChild(counter);
|
|
545
553
|
}
|
|
@@ -553,7 +561,7 @@ function renderMultipleTextElement(element, ctx, wrapper, pathKey) {
|
|
|
553
561
|
return itemWrapper;
|
|
554
562
|
}
|
|
555
563
|
function updateRemoveButtons() {
|
|
556
|
-
if (
|
|
564
|
+
if (readonly) return;
|
|
557
565
|
const items = container.querySelectorAll(".multiple-text-item");
|
|
558
566
|
const currentCount = items.length;
|
|
559
567
|
items.forEach((item) => {
|
|
@@ -598,7 +606,7 @@ function renderMultipleTextElement(element, ctx, wrapper, pathKey) {
|
|
|
598
606
|
}
|
|
599
607
|
let addRow = null;
|
|
600
608
|
let countDisplay = null;
|
|
601
|
-
if (!
|
|
609
|
+
if (!readonly) {
|
|
602
610
|
addRow = document.createElement("div");
|
|
603
611
|
addRow.className = "flex items-center gap-3 mt-2";
|
|
604
612
|
const addBtn = document.createElement("button");
|
|
@@ -806,6 +814,7 @@ function applyAutoExpand(textarea) {
|
|
|
806
814
|
}
|
|
807
815
|
function renderTextareaElement(element, ctx, wrapper, pathKey) {
|
|
808
816
|
const state = ctx.state;
|
|
817
|
+
const readonly = isElementReadonly(element, state, ctx);
|
|
809
818
|
const textareaWrapper = document.createElement("div");
|
|
810
819
|
textareaWrapper.style.cssText = "position: relative;";
|
|
811
820
|
const textareaInput = document.createElement("textarea");
|
|
@@ -815,8 +824,8 @@ function renderTextareaElement(element, ctx, wrapper, pathKey) {
|
|
|
815
824
|
textareaInput.placeholder = element.placeholder || "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0442\u0435\u043A\u0441\u0442";
|
|
816
825
|
textareaInput.rows = element.rows || 4;
|
|
817
826
|
textareaInput.value = ctx.prefill[element.key] || element.default || "";
|
|
818
|
-
textareaInput.readOnly =
|
|
819
|
-
if (!
|
|
827
|
+
textareaInput.readOnly = readonly;
|
|
828
|
+
if (!readonly && ctx.instance) {
|
|
820
829
|
const handleChange = () => {
|
|
821
830
|
const value = textareaInput.value === "" ? null : textareaInput.value;
|
|
822
831
|
ctx.instance.triggerOnChange(pathKey, value);
|
|
@@ -824,11 +833,11 @@ function renderTextareaElement(element, ctx, wrapper, pathKey) {
|
|
|
824
833
|
textareaInput.addEventListener("blur", handleChange);
|
|
825
834
|
textareaInput.addEventListener("input", handleChange);
|
|
826
835
|
}
|
|
827
|
-
if (element.autoExpand ||
|
|
836
|
+
if (element.autoExpand || readonly) {
|
|
828
837
|
applyAutoExpand(textareaInput);
|
|
829
838
|
}
|
|
830
839
|
textareaWrapper.appendChild(textareaInput);
|
|
831
|
-
if (!
|
|
840
|
+
if (!readonly && (element.minLength != null || element.maxLength != null)) {
|
|
832
841
|
const counter = createCharCounter(element, textareaInput, true);
|
|
833
842
|
textareaWrapper.appendChild(counter);
|
|
834
843
|
}
|
|
@@ -836,6 +845,7 @@ function renderTextareaElement(element, ctx, wrapper, pathKey) {
|
|
|
836
845
|
}
|
|
837
846
|
function renderMultipleTextareaElement(element, ctx, wrapper, pathKey) {
|
|
838
847
|
const state = ctx.state;
|
|
848
|
+
const readonly = isElementReadonly(element, state, ctx);
|
|
839
849
|
const prefillValues = ctx.prefill[element.key] || [];
|
|
840
850
|
const values = Array.isArray(prefillValues) ? [...prefillValues] : [];
|
|
841
851
|
const minCount = element.minCount ?? 1;
|
|
@@ -866,8 +876,8 @@ function renderMultipleTextareaElement(element, ctx, wrapper, pathKey) {
|
|
|
866
876
|
textareaInput.placeholder = element.placeholder || t("placeholderText", state);
|
|
867
877
|
textareaInput.rows = element.rows || 4;
|
|
868
878
|
textareaInput.value = value;
|
|
869
|
-
textareaInput.readOnly =
|
|
870
|
-
if (!
|
|
879
|
+
textareaInput.readOnly = readonly;
|
|
880
|
+
if (!readonly && ctx.instance) {
|
|
871
881
|
const handleChange = () => {
|
|
872
882
|
const value2 = textareaInput.value === "" ? null : textareaInput.value;
|
|
873
883
|
ctx.instance.triggerOnChange(textareaInput.name, value2);
|
|
@@ -875,11 +885,11 @@ function renderMultipleTextareaElement(element, ctx, wrapper, pathKey) {
|
|
|
875
885
|
textareaInput.addEventListener("blur", handleChange);
|
|
876
886
|
textareaInput.addEventListener("input", handleChange);
|
|
877
887
|
}
|
|
878
|
-
if (element.autoExpand ||
|
|
888
|
+
if (element.autoExpand || readonly) {
|
|
879
889
|
applyAutoExpand(textareaInput);
|
|
880
890
|
}
|
|
881
891
|
textareaContainer.appendChild(textareaInput);
|
|
882
|
-
if (!
|
|
892
|
+
if (!readonly && (element.minLength != null || element.maxLength != null)) {
|
|
883
893
|
const counter = createCharCounter(element, textareaInput, true);
|
|
884
894
|
textareaContainer.appendChild(counter);
|
|
885
895
|
}
|
|
@@ -893,7 +903,7 @@ function renderMultipleTextareaElement(element, ctx, wrapper, pathKey) {
|
|
|
893
903
|
return itemWrapper;
|
|
894
904
|
}
|
|
895
905
|
function updateRemoveButtons() {
|
|
896
|
-
if (
|
|
906
|
+
if (readonly) return;
|
|
897
907
|
const items = container.querySelectorAll(".multiple-textarea-item");
|
|
898
908
|
const currentCount = items.length;
|
|
899
909
|
items.forEach((item) => {
|
|
@@ -927,7 +937,7 @@ function renderMultipleTextareaElement(element, ctx, wrapper, pathKey) {
|
|
|
927
937
|
}
|
|
928
938
|
let addRow = null;
|
|
929
939
|
let countDisplay = null;
|
|
930
|
-
if (!
|
|
940
|
+
if (!readonly) {
|
|
931
941
|
addRow = document.createElement("div");
|
|
932
942
|
addRow.className = "flex items-center gap-3 mt-2";
|
|
933
943
|
const addBtn = document.createElement("button");
|
|
@@ -961,7 +971,9 @@ function renderMultipleTextareaElement(element, ctx, wrapper, pathKey) {
|
|
|
961
971
|
}
|
|
962
972
|
function updateAddButton() {
|
|
963
973
|
if (!addRow || !countDisplay) return;
|
|
964
|
-
const addBtn = addRow.querySelector(
|
|
974
|
+
const addBtn = addRow.querySelector(
|
|
975
|
+
".add-textarea-btn"
|
|
976
|
+
);
|
|
965
977
|
if (addBtn) {
|
|
966
978
|
const disabled = values.length >= maxCount;
|
|
967
979
|
addBtn.disabled = disabled;
|
|
@@ -980,7 +992,7 @@ function validateTextareaElement(element, key, context) {
|
|
|
980
992
|
function updateTextareaField(element, fieldPath, value, context) {
|
|
981
993
|
updateTextField(element, fieldPath, value, context);
|
|
982
994
|
const { scopeRoot, state } = context;
|
|
983
|
-
const shouldAutoExpand = element.autoExpand || state
|
|
995
|
+
const shouldAutoExpand = element.autoExpand || isElementReadonly(element, state);
|
|
984
996
|
if (!shouldAutoExpand) return;
|
|
985
997
|
if (element.multiple) {
|
|
986
998
|
const textareas = scopeRoot.querySelectorAll(
|
|
@@ -1041,6 +1053,7 @@ function createNumberRangeHint(element, input) {
|
|
|
1041
1053
|
}
|
|
1042
1054
|
function renderNumberElement(element, ctx, wrapper, pathKey) {
|
|
1043
1055
|
const state = ctx.state;
|
|
1056
|
+
const readonly = isElementReadonly(element, state, ctx);
|
|
1044
1057
|
const inputWrapper = document.createElement("div");
|
|
1045
1058
|
inputWrapper.style.cssText = "position: relative;";
|
|
1046
1059
|
const numberInput = document.createElement("input");
|
|
@@ -1053,8 +1066,8 @@ function renderNumberElement(element, ctx, wrapper, pathKey) {
|
|
|
1053
1066
|
if (element.max !== void 0) numberInput.max = element.max.toString();
|
|
1054
1067
|
if (element.step !== void 0) numberInput.step = element.step.toString();
|
|
1055
1068
|
numberInput.value = ctx.prefill[element.key] || element.default || "";
|
|
1056
|
-
numberInput.readOnly =
|
|
1057
|
-
if (!
|
|
1069
|
+
numberInput.readOnly = readonly;
|
|
1070
|
+
if (!readonly && ctx.instance) {
|
|
1058
1071
|
const handleChange = () => {
|
|
1059
1072
|
const value = numberInput.value ? parseFloat(numberInput.value) : null;
|
|
1060
1073
|
ctx.instance.triggerOnChange(pathKey, value);
|
|
@@ -1063,7 +1076,7 @@ function renderNumberElement(element, ctx, wrapper, pathKey) {
|
|
|
1063
1076
|
numberInput.addEventListener("input", handleChange);
|
|
1064
1077
|
}
|
|
1065
1078
|
inputWrapper.appendChild(numberInput);
|
|
1066
|
-
if (!
|
|
1079
|
+
if (!readonly && (element.min != null || element.max != null)) {
|
|
1067
1080
|
const counter = createNumberRangeHint(element, numberInput);
|
|
1068
1081
|
inputWrapper.appendChild(counter);
|
|
1069
1082
|
}
|
|
@@ -1071,6 +1084,7 @@ function renderNumberElement(element, ctx, wrapper, pathKey) {
|
|
|
1071
1084
|
}
|
|
1072
1085
|
function renderMultipleNumberElement(element, ctx, wrapper, pathKey) {
|
|
1073
1086
|
const state = ctx.state;
|
|
1087
|
+
const readonly = isElementReadonly(element, state, ctx);
|
|
1074
1088
|
const prefillValues = ctx.prefill[element.key] || [];
|
|
1075
1089
|
const values = Array.isArray(prefillValues) ? [...prefillValues] : [];
|
|
1076
1090
|
const minCount = element.minCount ?? 1;
|
|
@@ -1104,8 +1118,8 @@ function renderMultipleNumberElement(element, ctx, wrapper, pathKey) {
|
|
|
1104
1118
|
if (element.max !== void 0) numberInput.max = element.max.toString();
|
|
1105
1119
|
if (element.step !== void 0) numberInput.step = element.step.toString();
|
|
1106
1120
|
numberInput.value = value.toString();
|
|
1107
|
-
numberInput.readOnly =
|
|
1108
|
-
if (!
|
|
1121
|
+
numberInput.readOnly = readonly;
|
|
1122
|
+
if (!readonly && ctx.instance) {
|
|
1109
1123
|
const handleChange = () => {
|
|
1110
1124
|
const val = numberInput.value ? parseFloat(numberInput.value) : null;
|
|
1111
1125
|
ctx.instance.triggerOnChange(numberInput.name, val);
|
|
@@ -1114,7 +1128,7 @@ function renderMultipleNumberElement(element, ctx, wrapper, pathKey) {
|
|
|
1114
1128
|
numberInput.addEventListener("input", handleChange);
|
|
1115
1129
|
}
|
|
1116
1130
|
inputContainer.appendChild(numberInput);
|
|
1117
|
-
if (!
|
|
1131
|
+
if (!readonly && (element.min != null || element.max != null)) {
|
|
1118
1132
|
const counter = createNumberRangeHint(element, numberInput);
|
|
1119
1133
|
inputContainer.appendChild(counter);
|
|
1120
1134
|
}
|
|
@@ -1128,7 +1142,7 @@ function renderMultipleNumberElement(element, ctx, wrapper, pathKey) {
|
|
|
1128
1142
|
return itemWrapper;
|
|
1129
1143
|
}
|
|
1130
1144
|
function updateRemoveButtons() {
|
|
1131
|
-
if (
|
|
1145
|
+
if (readonly) return;
|
|
1132
1146
|
const items = container.querySelectorAll(".multiple-number-item");
|
|
1133
1147
|
const currentCount = items.length;
|
|
1134
1148
|
items.forEach((item) => {
|
|
@@ -1162,7 +1176,7 @@ function renderMultipleNumberElement(element, ctx, wrapper, pathKey) {
|
|
|
1162
1176
|
}
|
|
1163
1177
|
let addRow = null;
|
|
1164
1178
|
let countDisplay = null;
|
|
1165
|
-
if (!
|
|
1179
|
+
if (!readonly) {
|
|
1166
1180
|
addRow = document.createElement("div");
|
|
1167
1181
|
addRow.className = "flex items-center gap-3 mt-2";
|
|
1168
1182
|
const addBtn = document.createElement("button");
|
|
@@ -1367,10 +1381,11 @@ function updateNumberField(element, fieldPath, value, context) {
|
|
|
1367
1381
|
// src/components/select.ts
|
|
1368
1382
|
function renderSelectElement(element, ctx, wrapper, pathKey) {
|
|
1369
1383
|
const state = ctx.state;
|
|
1384
|
+
const readonly = isElementReadonly(element, state, ctx);
|
|
1370
1385
|
const selectInput = document.createElement("select");
|
|
1371
1386
|
selectInput.className = "w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500";
|
|
1372
1387
|
selectInput.name = pathKey;
|
|
1373
|
-
selectInput.disabled =
|
|
1388
|
+
selectInput.disabled = readonly;
|
|
1374
1389
|
(element.options || []).forEach((option) => {
|
|
1375
1390
|
const optionEl = document.createElement("option");
|
|
1376
1391
|
optionEl.value = option.value;
|
|
@@ -1380,14 +1395,14 @@ function renderSelectElement(element, ctx, wrapper, pathKey) {
|
|
|
1380
1395
|
}
|
|
1381
1396
|
selectInput.appendChild(optionEl);
|
|
1382
1397
|
});
|
|
1383
|
-
if (!
|
|
1398
|
+
if (!readonly && ctx.instance) {
|
|
1384
1399
|
const handleChange = () => {
|
|
1385
1400
|
ctx.instance.triggerOnChange(pathKey, selectInput.value);
|
|
1386
1401
|
};
|
|
1387
1402
|
selectInput.addEventListener("change", handleChange);
|
|
1388
1403
|
}
|
|
1389
1404
|
wrapper.appendChild(selectInput);
|
|
1390
|
-
if (!
|
|
1405
|
+
if (!readonly) {
|
|
1391
1406
|
const selectHint = document.createElement("p");
|
|
1392
1407
|
selectHint.className = "text-xs text-gray-500 mt-1";
|
|
1393
1408
|
selectHint.textContent = makeFieldHint(element, state);
|
|
@@ -1396,6 +1411,7 @@ function renderSelectElement(element, ctx, wrapper, pathKey) {
|
|
|
1396
1411
|
}
|
|
1397
1412
|
function renderMultipleSelectElement(element, ctx, wrapper, pathKey) {
|
|
1398
1413
|
const state = ctx.state;
|
|
1414
|
+
const readonly = isElementReadonly(element, state, ctx);
|
|
1399
1415
|
const prefillValues = ctx.prefill[element.key] || [];
|
|
1400
1416
|
const values = Array.isArray(prefillValues) ? [...prefillValues] : [];
|
|
1401
1417
|
const minCount = element.minCount ?? 1;
|
|
@@ -1420,7 +1436,7 @@ function renderMultipleSelectElement(element, ctx, wrapper, pathKey) {
|
|
|
1420
1436
|
itemWrapper.className = "multiple-select-item flex items-center gap-2";
|
|
1421
1437
|
const selectInput = document.createElement("select");
|
|
1422
1438
|
selectInput.className = "flex-1 px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500";
|
|
1423
|
-
selectInput.disabled =
|
|
1439
|
+
selectInput.disabled = readonly;
|
|
1424
1440
|
(element.options || []).forEach((option) => {
|
|
1425
1441
|
const optionElement = document.createElement("option");
|
|
1426
1442
|
optionElement.value = option.value;
|
|
@@ -1430,7 +1446,7 @@ function renderMultipleSelectElement(element, ctx, wrapper, pathKey) {
|
|
|
1430
1446
|
}
|
|
1431
1447
|
selectInput.appendChild(optionElement);
|
|
1432
1448
|
});
|
|
1433
|
-
if (!
|
|
1449
|
+
if (!readonly && ctx.instance) {
|
|
1434
1450
|
const handleChange = () => {
|
|
1435
1451
|
ctx.instance.triggerOnChange(selectInput.name, selectInput.value);
|
|
1436
1452
|
};
|
|
@@ -1446,7 +1462,7 @@ function renderMultipleSelectElement(element, ctx, wrapper, pathKey) {
|
|
|
1446
1462
|
return itemWrapper;
|
|
1447
1463
|
}
|
|
1448
1464
|
function updateRemoveButtons() {
|
|
1449
|
-
if (
|
|
1465
|
+
if (readonly) return;
|
|
1450
1466
|
const items = container.querySelectorAll(".multiple-select-item");
|
|
1451
1467
|
const currentCount = items.length;
|
|
1452
1468
|
items.forEach((item) => {
|
|
@@ -1478,7 +1494,7 @@ function renderMultipleSelectElement(element, ctx, wrapper, pathKey) {
|
|
|
1478
1494
|
}
|
|
1479
1495
|
let addRow = null;
|
|
1480
1496
|
let countDisplay = null;
|
|
1481
|
-
if (!
|
|
1497
|
+
if (!readonly) {
|
|
1482
1498
|
addRow = document.createElement("div");
|
|
1483
1499
|
addRow.className = "flex items-center gap-3 mt-2";
|
|
1484
1500
|
const addBtn = document.createElement("button");
|
|
@@ -1525,7 +1541,7 @@ function renderMultipleSelectElement(element, ctx, wrapper, pathKey) {
|
|
|
1525
1541
|
values.forEach((value) => addSelectItem(value));
|
|
1526
1542
|
updateAddButton();
|
|
1527
1543
|
updateRemoveButtons();
|
|
1528
|
-
if (!
|
|
1544
|
+
if (!readonly) {
|
|
1529
1545
|
const hint = document.createElement("p");
|
|
1530
1546
|
hint.className = "text-xs text-gray-500 mt-1";
|
|
1531
1547
|
hint.textContent = makeFieldHint(element, state);
|
|
@@ -1741,12 +1757,14 @@ function buildSegmentedGroup(element, currentValue, hiddenInput, readonly, onCha
|
|
|
1741
1757
|
}
|
|
1742
1758
|
function renderSwitcherElement(element, ctx, wrapper, pathKey) {
|
|
1743
1759
|
const state = ctx.state;
|
|
1744
|
-
const initialValue = String(
|
|
1760
|
+
const initialValue = String(
|
|
1761
|
+
ctx.prefill[element.key] ?? element.default ?? ""
|
|
1762
|
+
);
|
|
1745
1763
|
const hiddenInput = document.createElement("input");
|
|
1746
1764
|
hiddenInput.type = "hidden";
|
|
1747
1765
|
hiddenInput.name = pathKey;
|
|
1748
1766
|
hiddenInput.value = initialValue;
|
|
1749
|
-
const readonly = state
|
|
1767
|
+
const readonly = isElementReadonly(element, state, ctx);
|
|
1750
1768
|
const onChange = !readonly && ctx.instance ? (value) => {
|
|
1751
1769
|
ctx.instance.triggerOnChange(pathKey, value);
|
|
1752
1770
|
} : null;
|
|
@@ -1775,7 +1793,7 @@ function renderMultipleSwitcherElement(element, ctx, wrapper, pathKey) {
|
|
|
1775
1793
|
while (values.length < minCount) {
|
|
1776
1794
|
values.push(element.default || element.options?.[0]?.value || "");
|
|
1777
1795
|
}
|
|
1778
|
-
const readonly = state
|
|
1796
|
+
const readonly = isElementReadonly(element, state, ctx);
|
|
1779
1797
|
const container = document.createElement("div");
|
|
1780
1798
|
container.className = "space-y-2";
|
|
1781
1799
|
wrapper.appendChild(container);
|
|
@@ -2070,7 +2088,7 @@ function updateSwitcherField(element, fieldPath, value, context) {
|
|
|
2070
2088
|
}
|
|
2071
2089
|
}
|
|
2072
2090
|
|
|
2073
|
-
// src/components/file.ts
|
|
2091
|
+
// src/components/file/constraints.ts
|
|
2074
2092
|
function getAllowedExtensions(accept) {
|
|
2075
2093
|
if (!accept) return [];
|
|
2076
2094
|
if (typeof accept === "object" && Array.isArray(accept.extensions)) {
|
|
@@ -2083,16 +2101,662 @@ function getAllowedExtensions(accept) {
|
|
|
2083
2101
|
}
|
|
2084
2102
|
function isFileExtensionAllowed(fileName, allowedExtensions) {
|
|
2085
2103
|
if (allowedExtensions.length === 0) return true;
|
|
2086
|
-
const ext = fileName.split(".").pop()?.toLowerCase()
|
|
2104
|
+
const ext = fileName.split(".").pop()?.toLowerCase() ?? "";
|
|
2087
2105
|
return allowedExtensions.includes(ext);
|
|
2088
2106
|
}
|
|
2089
2107
|
function isFileSizeAllowed(file, maxSizeMB) {
|
|
2090
2108
|
if (maxSizeMB === Infinity) return true;
|
|
2091
2109
|
return file.size <= maxSizeMB * 1024 * 1024;
|
|
2092
2110
|
}
|
|
2111
|
+
function addPrefillFilesToIndex(initialFiles, resourceIndex) {
|
|
2112
|
+
for (const resourceId of initialFiles) {
|
|
2113
|
+
if (resourceIndex.has(resourceId)) continue;
|
|
2114
|
+
const filename = resourceId.split("/").pop() || "file";
|
|
2115
|
+
const extension = filename.split(".").pop()?.toLowerCase();
|
|
2116
|
+
let fileType = "application/octet-stream";
|
|
2117
|
+
if (extension) {
|
|
2118
|
+
if (["jpg", "jpeg", "png", "gif", "webp"].includes(extension)) {
|
|
2119
|
+
fileType = `image/${extension === "jpg" ? "jpeg" : extension}`;
|
|
2120
|
+
} else if (["mp4", "webm", "mov", "avi"].includes(extension)) {
|
|
2121
|
+
fileType = `video/${extension === "mov" ? "quicktime" : extension}`;
|
|
2122
|
+
}
|
|
2123
|
+
}
|
|
2124
|
+
resourceIndex.set(resourceId, {
|
|
2125
|
+
name: filename,
|
|
2126
|
+
type: fileType,
|
|
2127
|
+
size: 0,
|
|
2128
|
+
uploadedAt: /* @__PURE__ */ new Date(),
|
|
2129
|
+
file: void 0
|
|
2130
|
+
});
|
|
2131
|
+
}
|
|
2132
|
+
}
|
|
2133
|
+
|
|
2134
|
+
// src/components/file/styles.ts
|
|
2135
|
+
var STYLE_ID = "fb-file-styles";
|
|
2136
|
+
function ensureFileStyles() {
|
|
2137
|
+
if (typeof document === "undefined") return;
|
|
2138
|
+
if (document.getElementById(STYLE_ID)) return;
|
|
2139
|
+
const style = document.createElement("style");
|
|
2140
|
+
style.id = STYLE_ID;
|
|
2141
|
+
style.setAttribute("data-fb-file-styles", "true");
|
|
2142
|
+
style.textContent = `
|
|
2143
|
+
@keyframes fb-spin { to { transform: rotate(360deg); } }
|
|
2144
|
+
|
|
2145
|
+
/* Spinner used during single-file and multi-file upload */
|
|
2146
|
+
.fb-spinner {
|
|
2147
|
+
width: 36px;
|
|
2148
|
+
height: 36px;
|
|
2149
|
+
border: 3px solid rgba(0,0,0,0.12);
|
|
2150
|
+
border-top-color: var(--fb-text-secondary-color, #6b7280);
|
|
2151
|
+
border-radius: 50%;
|
|
2152
|
+
animation: fb-spin 0.7s linear infinite;
|
|
2153
|
+
flex-shrink: 0;
|
|
2154
|
+
}
|
|
2155
|
+
|
|
2156
|
+
/* Base tile: fixed 160\xD7160 square, theme-aware background */
|
|
2157
|
+
.fb-tile {
|
|
2158
|
+
width: var(--fb-tile-size, 160px);
|
|
2159
|
+
height: var(--fb-tile-size, 160px);
|
|
2160
|
+
flex-shrink: 0;
|
|
2161
|
+
position: relative;
|
|
2162
|
+
overflow: hidden;
|
|
2163
|
+
border-radius: var(--fb-border-radius, 0.5rem);
|
|
2164
|
+
background: var(--fb-file-upload-bg-color, #f3f4f6);
|
|
2165
|
+
}
|
|
2166
|
+
|
|
2167
|
+
/* Uploaded resource tile \u2014 adds a visible border */
|
|
2168
|
+
.fb-tile-resource {
|
|
2169
|
+
border: 1px solid var(--fb-file-upload-border-color, #d1d5db);
|
|
2170
|
+
}
|
|
2171
|
+
|
|
2172
|
+
/* Uploading placeholder tile \u2014 dashed border, uploading indicator */
|
|
2173
|
+
.fb-tile-uploading {
|
|
2174
|
+
border: 2px dashed var(--fb-file-upload-border-color, #d1d5db);
|
|
2175
|
+
}
|
|
2176
|
+
|
|
2177
|
+
/* "+" add-more tile */
|
|
2178
|
+
.fb-tile-add {
|
|
2179
|
+
border: 2px dashed var(--fb-file-upload-border-color, #d1d5db);
|
|
2180
|
+
display: flex;
|
|
2181
|
+
align-items: center;
|
|
2182
|
+
justify-content: center;
|
|
2183
|
+
cursor: pointer;
|
|
2184
|
+
font-size: 32px;
|
|
2185
|
+
color: var(--fb-file-upload-text-color, #9ca3af);
|
|
2186
|
+
transition:
|
|
2187
|
+
border-color var(--fb-transition-duration, 200ms),
|
|
2188
|
+
color var(--fb-transition-duration, 200ms);
|
|
2189
|
+
}
|
|
2190
|
+
.fb-tile-add:hover {
|
|
2191
|
+
border-color: var(--fb-file-upload-hover-border-color, #3b82f6);
|
|
2192
|
+
color: var(--fb-text-color, #1f2937);
|
|
2193
|
+
}
|
|
2194
|
+
|
|
2195
|
+
/* Count chip shown when at maxCount */
|
|
2196
|
+
.fb-tile-counter {
|
|
2197
|
+
font-size: 11px;
|
|
2198
|
+
color: var(--fb-text-secondary-color, #6b7280);
|
|
2199
|
+
background: var(--fb-file-upload-bg-color, #f3f4f6);
|
|
2200
|
+
border: 1px solid var(--fb-file-upload-border-color, #d1d5db);
|
|
2201
|
+
border-radius: 4px;
|
|
2202
|
+
padding: 2px 6px;
|
|
2203
|
+
align-self: flex-end;
|
|
2204
|
+
margin-bottom: 4px;
|
|
2205
|
+
}
|
|
2206
|
+
|
|
2207
|
+
/* Empty-state dropzone */
|
|
2208
|
+
.fb-file-dropzone {
|
|
2209
|
+
width: 100%;
|
|
2210
|
+
height: 128px;
|
|
2211
|
+
border: 2px dashed var(--fb-file-upload-border-color, #d1d5db);
|
|
2212
|
+
border-radius: var(--fb-border-radius, 0.5rem);
|
|
2213
|
+
display: flex;
|
|
2214
|
+
flex-direction: column;
|
|
2215
|
+
align-items: center;
|
|
2216
|
+
justify-content: center;
|
|
2217
|
+
gap: 4px;
|
|
2218
|
+
cursor: pointer;
|
|
2219
|
+
transition:
|
|
2220
|
+
border-color var(--fb-transition-duration, 200ms),
|
|
2221
|
+
background var(--fb-transition-duration, 200ms);
|
|
2222
|
+
}
|
|
2223
|
+
.fb-file-dropzone:hover {
|
|
2224
|
+
border-color: var(--fb-file-upload-hover-border-color, #3b82f6);
|
|
2225
|
+
background: var(--fb-background-hover-color, #f9fafb);
|
|
2226
|
+
}
|
|
2227
|
+
|
|
2228
|
+
/* Inline text inside tiles */
|
|
2229
|
+
.fb-tile-label {
|
|
2230
|
+
font-size: 9px;
|
|
2231
|
+
color: var(--fb-text-secondary-color, #6b7280);
|
|
2232
|
+
text-align: center;
|
|
2233
|
+
overflow: hidden;
|
|
2234
|
+
word-break: break-all;
|
|
2235
|
+
max-height: 28px;
|
|
2236
|
+
}
|
|
2237
|
+
.fb-tile-uploading-text {
|
|
2238
|
+
font-size: 8px;
|
|
2239
|
+
color: var(--fb-file-upload-text-color, #9ca3af);
|
|
2240
|
+
}
|
|
2241
|
+
.fb-tile-hint {
|
|
2242
|
+
font-size: 11px;
|
|
2243
|
+
color: var(--fb-file-upload-text-color, #9ca3af);
|
|
2244
|
+
margin-top: 4px;
|
|
2245
|
+
}
|
|
2246
|
+
.fb-tile-empty-text {
|
|
2247
|
+
font-size: 12px;
|
|
2248
|
+
color: var(--fb-text-secondary-color, #6b7280);
|
|
2249
|
+
padding: 4px 0;
|
|
2250
|
+
}
|
|
2251
|
+
.fb-dropzone-primary-text {
|
|
2252
|
+
font-size: 13px;
|
|
2253
|
+
color: var(--fb-text-secondary-color, #6b7280);
|
|
2254
|
+
}
|
|
2255
|
+
.fb-dropzone-hint-text {
|
|
2256
|
+
font-size: 11px;
|
|
2257
|
+
color: var(--fb-file-upload-text-color, #9ca3af);
|
|
2258
|
+
}
|
|
2259
|
+
|
|
2260
|
+
/* Hover overlay + X-button on resource tiles */
|
|
2261
|
+
.fb-tile-overlay {
|
|
2262
|
+
position: absolute;
|
|
2263
|
+
inset: 0;
|
|
2264
|
+
background: transparent;
|
|
2265
|
+
transition: background var(--fb-transition-duration, 200ms);
|
|
2266
|
+
display: flex;
|
|
2267
|
+
align-items: flex-start;
|
|
2268
|
+
justify-content: flex-end;
|
|
2269
|
+
}
|
|
2270
|
+
.fb-tile-resource:hover .fb-tile-overlay {
|
|
2271
|
+
background: var(--fb-tile-hover-overlay-color, rgba(0,0,0,0.4));
|
|
2272
|
+
}
|
|
2273
|
+
.fb-tile-x-btn {
|
|
2274
|
+
margin: 3px;
|
|
2275
|
+
width: 18px;
|
|
2276
|
+
height: 18px;
|
|
2277
|
+
background: var(--fb-error-color, #ef4444);
|
|
2278
|
+
color: var(--fb-file-bg-color, #fff);
|
|
2279
|
+
border: none;
|
|
2280
|
+
border-radius: 50%;
|
|
2281
|
+
font-size: 11px;
|
|
2282
|
+
line-height: 1;
|
|
2283
|
+
cursor: pointer;
|
|
2284
|
+
display: flex;
|
|
2285
|
+
align-items: center;
|
|
2286
|
+
justify-content: center;
|
|
2287
|
+
opacity: 0;
|
|
2288
|
+
transition: opacity var(--fb-transition-duration, 200ms);
|
|
2289
|
+
}
|
|
2290
|
+
.fb-tile-resource:hover .fb-tile-x-btn {
|
|
2291
|
+
opacity: 1;
|
|
2292
|
+
}
|
|
2293
|
+
|
|
2294
|
+
/* Video play button overlay (readonly tiles with video thumbnails) */
|
|
2295
|
+
.fb-video-overlay {
|
|
2296
|
+
position: absolute;
|
|
2297
|
+
inset: 0;
|
|
2298
|
+
display: flex;
|
|
2299
|
+
align-items: center;
|
|
2300
|
+
justify-content: center;
|
|
2301
|
+
background: var(--fb-tile-hover-overlay-color, rgba(0,0,0,0.25));
|
|
2302
|
+
}
|
|
2303
|
+
.fb-play-btn {
|
|
2304
|
+
background: var(--fb-file-bg-color, rgba(255,255,255,0.9));
|
|
2305
|
+
border-radius: 50%;
|
|
2306
|
+
display: flex;
|
|
2307
|
+
align-items: center;
|
|
2308
|
+
justify-content: center;
|
|
2309
|
+
}
|
|
2310
|
+
|
|
2311
|
+
/* Edit-mode local video preview wrapper */
|
|
2312
|
+
.fb-video-preview-wrap {
|
|
2313
|
+
position: relative;
|
|
2314
|
+
width: 100%;
|
|
2315
|
+
height: 100%;
|
|
2316
|
+
}
|
|
2317
|
+
|
|
2318
|
+
/* Hover overlay for edit-mode local video (Remove / Change buttons) */
|
|
2319
|
+
.fb-video-btn-overlay {
|
|
2320
|
+
position: absolute;
|
|
2321
|
+
top: 8px;
|
|
2322
|
+
right: 8px;
|
|
2323
|
+
z-index: 10;
|
|
2324
|
+
display: flex;
|
|
2325
|
+
gap: 4px;
|
|
2326
|
+
opacity: 0;
|
|
2327
|
+
transition: opacity var(--fb-transition-duration, 200ms);
|
|
2328
|
+
pointer-events: none;
|
|
2329
|
+
}
|
|
2330
|
+
.fb-video-preview-wrap:hover .fb-video-btn-overlay {
|
|
2331
|
+
opacity: 1;
|
|
2332
|
+
pointer-events: auto;
|
|
2333
|
+
}
|
|
2334
|
+
.fb-video-btn {
|
|
2335
|
+
border: none;
|
|
2336
|
+
border-radius: var(--fb-border-radius, 4px);
|
|
2337
|
+
font-size: 11px;
|
|
2338
|
+
padding: 4px 8px;
|
|
2339
|
+
cursor: pointer;
|
|
2340
|
+
color: #fff;
|
|
2341
|
+
line-height: 1.2;
|
|
2342
|
+
}
|
|
2343
|
+
.fb-video-btn-delete {
|
|
2344
|
+
background: rgba(220, 38, 38, 0.85);
|
|
2345
|
+
}
|
|
2346
|
+
.fb-video-btn-delete:hover {
|
|
2347
|
+
background: rgba(185, 28, 28, 0.95);
|
|
2348
|
+
}
|
|
2349
|
+
.fb-video-btn-change {
|
|
2350
|
+
background: rgba(31, 41, 55, 0.85);
|
|
2351
|
+
}
|
|
2352
|
+
.fb-video-btn-change:hover {
|
|
2353
|
+
background: rgba(17, 24, 39, 0.95);
|
|
2354
|
+
}
|
|
2355
|
+
|
|
2356
|
+
/* Tile action icon buttons (download / open / remove) \u2014 shown on tile hover */
|
|
2357
|
+
.fb-tile-actions {
|
|
2358
|
+
position: absolute;
|
|
2359
|
+
top: 3px;
|
|
2360
|
+
right: 3px;
|
|
2361
|
+
display: flex;
|
|
2362
|
+
flex-direction: row;
|
|
2363
|
+
gap: 3px;
|
|
2364
|
+
opacity: 0;
|
|
2365
|
+
transition: opacity var(--fb-transition-duration, 200ms);
|
|
2366
|
+
z-index: 10;
|
|
2367
|
+
}
|
|
2368
|
+
.fb-tile-resource:hover .fb-tile-actions {
|
|
2369
|
+
opacity: 1;
|
|
2370
|
+
}
|
|
2371
|
+
.fb-tile-action-btn {
|
|
2372
|
+
width: 28px;
|
|
2373
|
+
height: 28px;
|
|
2374
|
+
display: flex;
|
|
2375
|
+
align-items: center;
|
|
2376
|
+
justify-content: center;
|
|
2377
|
+
border: none;
|
|
2378
|
+
border-radius: 50%;
|
|
2379
|
+
cursor: pointer;
|
|
2380
|
+
background: rgba(31, 41, 55, 0.75);
|
|
2381
|
+
color: #fff;
|
|
2382
|
+
padding: 0;
|
|
2383
|
+
flex-shrink: 0;
|
|
2384
|
+
transition:
|
|
2385
|
+
background var(--fb-transition-duration, 200ms),
|
|
2386
|
+
opacity var(--fb-transition-duration, 200ms);
|
|
2387
|
+
}
|
|
2388
|
+
.fb-tile-action-btn:hover {
|
|
2389
|
+
background: rgba(17, 24, 39, 0.95);
|
|
2390
|
+
}
|
|
2391
|
+
.fb-tile-action-remove {
|
|
2392
|
+
background: rgba(220, 38, 38, 0.8);
|
|
2393
|
+
}
|
|
2394
|
+
.fb-tile-action-remove:hover {
|
|
2395
|
+
background: rgba(185, 28, 28, 0.95);
|
|
2396
|
+
}
|
|
2397
|
+
|
|
2398
|
+
/* Actions row inside zoom popup \u2014 always visible while popup is shown */
|
|
2399
|
+
.fb-tile-zoom-preview .fb-tile-actions {
|
|
2400
|
+
position: absolute;
|
|
2401
|
+
top: 6px;
|
|
2402
|
+
right: 6px;
|
|
2403
|
+
opacity: 1;
|
|
2404
|
+
z-index: 10000;
|
|
2405
|
+
}
|
|
2406
|
+
|
|
2407
|
+
/* Hover zoom preview popup for image tiles \u2014 appended to document.body (fixed) */
|
|
2408
|
+
.fb-tile-zoom-preview {
|
|
2409
|
+
position: fixed;
|
|
2410
|
+
z-index: 9999;
|
|
2411
|
+
background: var(--fb-background-color, #fff);
|
|
2412
|
+
border: 1px solid var(--fb-file-upload-border-color, #d1d5db);
|
|
2413
|
+
border-radius: var(--fb-border-radius, 0.5rem);
|
|
2414
|
+
box-shadow: 0 4px 16px rgba(0,0,0,0.18);
|
|
2415
|
+
padding: 4px;
|
|
2416
|
+
width: 350px;
|
|
2417
|
+
height: 350px;
|
|
2418
|
+
pointer-events: none;
|
|
2419
|
+
opacity: 0;
|
|
2420
|
+
transition: opacity 150ms ease;
|
|
2421
|
+
}
|
|
2422
|
+
.fb-tile-zoom-preview.fb-tile-zoom-preview--visible {
|
|
2423
|
+
opacity: 1;
|
|
2424
|
+
}
|
|
2425
|
+
.fb-tile-zoom-preview-img {
|
|
2426
|
+
width: 100%;
|
|
2427
|
+
height: 100%;
|
|
2428
|
+
object-fit: contain;
|
|
2429
|
+
display: block;
|
|
2430
|
+
background: var(--fb-file-upload-bg-color, #f3f4f6);
|
|
2431
|
+
border-radius: calc(var(--fb-border-radius, 0.5rem) - 2px);
|
|
2432
|
+
}
|
|
2433
|
+
`;
|
|
2434
|
+
document.head.appendChild(style);
|
|
2435
|
+
}
|
|
2436
|
+
|
|
2437
|
+
// src/components/file/dom.ts
|
|
2438
|
+
var TILE_SIZE = "160px";
|
|
2439
|
+
function createFileTile() {
|
|
2440
|
+
ensureFileStyles();
|
|
2441
|
+
const tile = document.createElement("div");
|
|
2442
|
+
tile.className = "fb-tile";
|
|
2443
|
+
return tile;
|
|
2444
|
+
}
|
|
2445
|
+
function showFileError(container, message) {
|
|
2446
|
+
const existing = container.closest(".space-y-2")?.querySelector(".file-error-message");
|
|
2447
|
+
if (existing) existing.remove();
|
|
2448
|
+
const errorEl = document.createElement("div");
|
|
2449
|
+
errorEl.className = "file-error-message error-message";
|
|
2450
|
+
errorEl.style.cssText = `
|
|
2451
|
+
color: var(--fb-error-color);
|
|
2452
|
+
font-size: var(--fb-font-size-small);
|
|
2453
|
+
margin-top: 0.25rem;
|
|
2454
|
+
`;
|
|
2455
|
+
errorEl.textContent = message;
|
|
2456
|
+
container.closest(".space-y-2")?.appendChild(errorEl);
|
|
2457
|
+
}
|
|
2458
|
+
function clearFileError(container) {
|
|
2459
|
+
const existing = container.closest(".space-y-2")?.querySelector(".file-error-message");
|
|
2460
|
+
if (existing) existing.remove();
|
|
2461
|
+
}
|
|
2462
|
+
function addDeleteButton(container, state, onDelete) {
|
|
2463
|
+
const existingOverlay = container.querySelector(".delete-overlay");
|
|
2464
|
+
if (existingOverlay) existingOverlay.remove();
|
|
2465
|
+
const overlay = document.createElement("div");
|
|
2466
|
+
overlay.className = "delete-overlay absolute inset-0 bg-black bg-opacity-50 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center";
|
|
2467
|
+
const deleteBtn = document.createElement("button");
|
|
2468
|
+
deleteBtn.className = "bg-red-600 text-white px-3 py-1 rounded text-sm hover:bg-red-700 transition-colors";
|
|
2469
|
+
deleteBtn.textContent = t("removeElement", state);
|
|
2470
|
+
deleteBtn.onclick = (e) => {
|
|
2471
|
+
e.stopPropagation();
|
|
2472
|
+
onDelete();
|
|
2473
|
+
};
|
|
2474
|
+
overlay.appendChild(deleteBtn);
|
|
2475
|
+
container.appendChild(overlay);
|
|
2476
|
+
}
|
|
2477
|
+
function findFilePicker(container) {
|
|
2478
|
+
let el = container.parentElement;
|
|
2479
|
+
while (el && !el.dataset.filesWrapper) {
|
|
2480
|
+
el = el.parentElement;
|
|
2481
|
+
}
|
|
2482
|
+
return el?.querySelector('input[type="file"]') ?? null;
|
|
2483
|
+
}
|
|
2484
|
+
function createUploadingTile(fileName, state) {
|
|
2485
|
+
ensureFileStyles();
|
|
2486
|
+
const tile = createFileTile();
|
|
2487
|
+
tile.classList.add("fb-tile-uploading");
|
|
2488
|
+
tile.className += " fb-uploading-tile";
|
|
2489
|
+
const label = fileName.length > 10 ? fileName.substring(0, 8) + "\u2026" : fileName;
|
|
2490
|
+
tile.innerHTML = `
|
|
2491
|
+
<div style="display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;gap:6px;padding:4px;">
|
|
2492
|
+
<div class="fb-spinner"></div>
|
|
2493
|
+
<div class="fb-tile-label">${escapeHtml(label)}</div>
|
|
2494
|
+
<div class="fb-tile-uploading-text">${escapeHtml(t("uploadingFile", state))}</div>
|
|
2495
|
+
</div>`;
|
|
2496
|
+
return tile;
|
|
2497
|
+
}
|
|
2498
|
+
function ensureTilesWrap(list) {
|
|
2499
|
+
const existing = list.querySelector(".fb-tiles-wrap");
|
|
2500
|
+
if (existing) return existing;
|
|
2501
|
+
const dropzone = list.querySelector(".fb-file-dropzone");
|
|
2502
|
+
if (dropzone) dropzone.remove();
|
|
2503
|
+
const tilesWrap = document.createElement("div");
|
|
2504
|
+
tilesWrap.className = "fb-tiles-wrap";
|
|
2505
|
+
tilesWrap.style.cssText = "display:flex;flex-wrap:wrap;gap:6px;align-items:flex-start;";
|
|
2506
|
+
const addTile = document.createElement("div");
|
|
2507
|
+
addTile.className = "fb-tile fb-tile-add";
|
|
2508
|
+
addTile.innerHTML = "+";
|
|
2509
|
+
tilesWrap.appendChild(addTile);
|
|
2510
|
+
list.appendChild(tilesWrap);
|
|
2511
|
+
return tilesWrap;
|
|
2512
|
+
}
|
|
2513
|
+
function setEmptyFileContainer(fileContainer, state, hint) {
|
|
2514
|
+
const hintHtml = hint ? `<div class="text-xs text-gray-500 mt-1">${escapeHtml(hint)}</div>` : "";
|
|
2515
|
+
fileContainer.innerHTML = `
|
|
2516
|
+
<div class="flex flex-col items-center justify-center h-full text-gray-400">
|
|
2517
|
+
<svg class="w-6 h-6 mb-2" fill="currentColor" viewBox="0 0 24 24">
|
|
2518
|
+
<path d="M21 19V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2zM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5l3.5-4.5z"/>
|
|
2519
|
+
</svg>
|
|
2520
|
+
<div class="text-sm text-center">${escapeHtml(t("clickDragText", state))}</div>
|
|
2521
|
+
${hintHtml}
|
|
2522
|
+
</div>
|
|
2523
|
+
`;
|
|
2524
|
+
}
|
|
2525
|
+
function setupDragAndDrop(element, dropHandler) {
|
|
2526
|
+
element.addEventListener("dragover", (e) => {
|
|
2527
|
+
e.preventDefault();
|
|
2528
|
+
element.classList.add("border-blue-500", "bg-blue-50");
|
|
2529
|
+
});
|
|
2530
|
+
element.addEventListener("dragleave", (e) => {
|
|
2531
|
+
e.preventDefault();
|
|
2532
|
+
element.classList.remove("border-blue-500", "bg-blue-50");
|
|
2533
|
+
});
|
|
2534
|
+
element.addEventListener("drop", (e) => {
|
|
2535
|
+
e.preventDefault();
|
|
2536
|
+
element.classList.remove("border-blue-500", "bg-blue-50");
|
|
2537
|
+
if (e.dataTransfer?.files) {
|
|
2538
|
+
dropHandler(e.dataTransfer.files);
|
|
2539
|
+
}
|
|
2540
|
+
});
|
|
2541
|
+
}
|
|
2542
|
+
|
|
2543
|
+
// src/components/file/preview.ts
|
|
2544
|
+
var ICON_DOWNLOAD = `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>`;
|
|
2545
|
+
var ICON_OPEN = `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/></svg>`;
|
|
2546
|
+
var ICON_REMOVE = `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/><path d="M10 11v6"/><path d="M14 11v6"/><path d="M9 6V4a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2v2"/></svg>`;
|
|
2547
|
+
function canDownload(state, meta) {
|
|
2548
|
+
return Boolean(
|
|
2549
|
+
state.config.downloadFile || state.config.getDownloadUrl || state.config.getThumbnail || meta?.file
|
|
2550
|
+
);
|
|
2551
|
+
}
|
|
2552
|
+
function canOpenInTab(state, meta) {
|
|
2553
|
+
return Boolean(
|
|
2554
|
+
state.config.getDownloadUrl || state.config.getThumbnail || meta?.file
|
|
2555
|
+
);
|
|
2556
|
+
}
|
|
2557
|
+
function createTileActions(options) {
|
|
2558
|
+
const { canRemove, removeHandler, state, resourceId, fileName, meta } = options;
|
|
2559
|
+
const group = document.createElement("div");
|
|
2560
|
+
group.className = "fb-tile-actions";
|
|
2561
|
+
const makeBtn = (icon, label, cls) => {
|
|
2562
|
+
const btn = document.createElement("button");
|
|
2563
|
+
btn.type = "button";
|
|
2564
|
+
btn.className = `fb-tile-action-btn ${cls}`;
|
|
2565
|
+
btn.innerHTML = icon;
|
|
2566
|
+
btn.title = label;
|
|
2567
|
+
btn.setAttribute("aria-label", label);
|
|
2568
|
+
btn.addEventListener("click", (e) => {
|
|
2569
|
+
e.stopPropagation();
|
|
2570
|
+
});
|
|
2571
|
+
return btn;
|
|
2572
|
+
};
|
|
2573
|
+
if (canDownload(state, meta)) {
|
|
2574
|
+
const dlBtn = makeBtn(ICON_DOWNLOAD, t("downloadFile", state), "fb-tile-action-download");
|
|
2575
|
+
dlBtn.addEventListener("click", () => {
|
|
2576
|
+
triggerTileDownload(resourceId, fileName, state, meta);
|
|
2577
|
+
});
|
|
2578
|
+
group.appendChild(dlBtn);
|
|
2579
|
+
}
|
|
2580
|
+
if (canOpenInTab(state, meta)) {
|
|
2581
|
+
const openBtn = makeBtn(ICON_OPEN, t("openInNewTab", state), "fb-tile-action-open");
|
|
2582
|
+
openBtn.addEventListener("click", () => {
|
|
2583
|
+
triggerTileOpen(resourceId, state, meta).catch((err) => {
|
|
2584
|
+
console.error("Open failed:", err);
|
|
2585
|
+
});
|
|
2586
|
+
});
|
|
2587
|
+
group.appendChild(openBtn);
|
|
2588
|
+
}
|
|
2589
|
+
if (canRemove && removeHandler) {
|
|
2590
|
+
const rmBtn = makeBtn(ICON_REMOVE, t("removeElement", state), "fb-tile-action-remove");
|
|
2591
|
+
rmBtn.addEventListener("click", () => {
|
|
2592
|
+
removeHandler();
|
|
2593
|
+
});
|
|
2594
|
+
group.appendChild(rmBtn);
|
|
2595
|
+
}
|
|
2596
|
+
return group;
|
|
2597
|
+
}
|
|
2598
|
+
var localFileUrlCache = /* @__PURE__ */ new WeakMap();
|
|
2599
|
+
function getLocalFileUrl(file) {
|
|
2600
|
+
let url = localFileUrlCache.get(file);
|
|
2601
|
+
if (!url) {
|
|
2602
|
+
url = URL.createObjectURL(file);
|
|
2603
|
+
localFileUrlCache.set(file, url);
|
|
2604
|
+
}
|
|
2605
|
+
return url;
|
|
2606
|
+
}
|
|
2607
|
+
function releaseLocalFileUrl(file) {
|
|
2608
|
+
if (!file) return;
|
|
2609
|
+
const url = localFileUrlCache.get(file);
|
|
2610
|
+
if (url) {
|
|
2611
|
+
URL.revokeObjectURL(url);
|
|
2612
|
+
localFileUrlCache.delete(file);
|
|
2613
|
+
}
|
|
2614
|
+
}
|
|
2615
|
+
function triggerTileDownload(resourceId, fileName, state, meta) {
|
|
2616
|
+
if (state.config.downloadFile) {
|
|
2617
|
+
state.config.downloadFile(resourceId, fileName);
|
|
2618
|
+
return;
|
|
2619
|
+
}
|
|
2620
|
+
if (meta?.file instanceof File) {
|
|
2621
|
+
downloadBlob(meta.file, fileName || meta.file.name);
|
|
2622
|
+
return;
|
|
2623
|
+
}
|
|
2624
|
+
forceDownload(resourceId, fileName, state).catch((err) => {
|
|
2625
|
+
console.error("Download failed:", err);
|
|
2626
|
+
});
|
|
2627
|
+
}
|
|
2628
|
+
async function triggerTileOpen(resourceId, state, meta) {
|
|
2629
|
+
let url = null;
|
|
2630
|
+
if (state.config.getDownloadUrl) {
|
|
2631
|
+
url = state.config.getDownloadUrl(resourceId);
|
|
2632
|
+
} else if (state.config.getThumbnail) {
|
|
2633
|
+
url = await state.config.getThumbnail(resourceId);
|
|
2634
|
+
} else if (meta?.file instanceof File) {
|
|
2635
|
+
url = getLocalFileUrl(meta.file);
|
|
2636
|
+
}
|
|
2637
|
+
if (url) {
|
|
2638
|
+
window.open(url, "_blank");
|
|
2639
|
+
}
|
|
2640
|
+
}
|
|
2641
|
+
var sharedZoomPopup = null;
|
|
2642
|
+
var zoomTimer = null;
|
|
2643
|
+
var zoomHideTimer = null;
|
|
2644
|
+
var zoomOwner = null;
|
|
2645
|
+
function getOrCreateZoomPopup() {
|
|
2646
|
+
if (!sharedZoomPopup) {
|
|
2647
|
+
sharedZoomPopup = document.createElement("div");
|
|
2648
|
+
sharedZoomPopup.className = "fb-tile-zoom-preview";
|
|
2649
|
+
const img = document.createElement("img");
|
|
2650
|
+
img.className = "fb-tile-zoom-preview-img";
|
|
2651
|
+
sharedZoomPopup.appendChild(img);
|
|
2652
|
+
sharedZoomPopup.addEventListener("mouseenter", cancelHideZoomPopup);
|
|
2653
|
+
sharedZoomPopup.addEventListener("mouseleave", scheduleHideZoomPopup);
|
|
2654
|
+
}
|
|
2655
|
+
return sharedZoomPopup;
|
|
2656
|
+
}
|
|
2657
|
+
function positionZoomPopup(popup, tile) {
|
|
2658
|
+
const tileRect = tile.getBoundingClientRect();
|
|
2659
|
+
const popupSize = 350;
|
|
2660
|
+
const margin = 6;
|
|
2661
|
+
const padding = 8;
|
|
2662
|
+
let top;
|
|
2663
|
+
if (tileRect.top - popupSize - margin >= padding) {
|
|
2664
|
+
top = tileRect.top - popupSize - margin;
|
|
2665
|
+
} else if (tileRect.bottom + margin + popupSize + padding <= window.innerHeight) {
|
|
2666
|
+
top = tileRect.bottom + margin;
|
|
2667
|
+
} else {
|
|
2668
|
+
top = Math.max(padding, Math.min(window.innerHeight - popupSize - padding, tileRect.top));
|
|
2669
|
+
}
|
|
2670
|
+
const tileCenterX = tileRect.left + tileRect.width / 2;
|
|
2671
|
+
let left = tileCenterX - popupSize / 2;
|
|
2672
|
+
left = Math.max(padding, Math.min(window.innerWidth - popupSize - padding, left));
|
|
2673
|
+
popup.style.top = `${top}px`;
|
|
2674
|
+
popup.style.left = `${left}px`;
|
|
2675
|
+
}
|
|
2676
|
+
function scheduleHideZoomPopup() {
|
|
2677
|
+
if (zoomHideTimer !== null) {
|
|
2678
|
+
clearTimeout(zoomHideTimer);
|
|
2679
|
+
}
|
|
2680
|
+
zoomHideTimer = setTimeout(() => {
|
|
2681
|
+
zoomHideTimer = null;
|
|
2682
|
+
removeZoomPopupNow();
|
|
2683
|
+
}, 100);
|
|
2684
|
+
}
|
|
2685
|
+
function cancelHideZoomPopup() {
|
|
2686
|
+
if (zoomHideTimer !== null) {
|
|
2687
|
+
clearTimeout(zoomHideTimer);
|
|
2688
|
+
zoomHideTimer = null;
|
|
2689
|
+
}
|
|
2690
|
+
}
|
|
2691
|
+
function removeZoomPopupNow() {
|
|
2692
|
+
if (zoomTimer !== null) {
|
|
2693
|
+
clearTimeout(zoomTimer);
|
|
2694
|
+
zoomTimer = null;
|
|
2695
|
+
}
|
|
2696
|
+
if (sharedZoomPopup && sharedZoomPopup.parentNode) {
|
|
2697
|
+
sharedZoomPopup.classList.remove("fb-tile-zoom-preview--visible");
|
|
2698
|
+
sharedZoomPopup.parentNode.removeChild(sharedZoomPopup);
|
|
2699
|
+
}
|
|
2700
|
+
zoomOwner = null;
|
|
2701
|
+
}
|
|
2702
|
+
function attachZoomHover(tile, src, alt, actionsEl) {
|
|
2703
|
+
tile.dataset.zoomSrc = src;
|
|
2704
|
+
tile.dataset.zoomAlt = alt;
|
|
2705
|
+
tile.addEventListener("mouseenter", () => {
|
|
2706
|
+
cancelHideZoomPopup();
|
|
2707
|
+
if (zoomOwner !== tile) {
|
|
2708
|
+
removeZoomPopupNow();
|
|
2709
|
+
}
|
|
2710
|
+
zoomOwner = tile;
|
|
2711
|
+
zoomTimer = setTimeout(() => {
|
|
2712
|
+
zoomTimer = null;
|
|
2713
|
+
const popup = getOrCreateZoomPopup();
|
|
2714
|
+
const existingActions = popup.querySelector(".fb-tile-actions");
|
|
2715
|
+
if (existingActions) existingActions.remove();
|
|
2716
|
+
const img = popup.querySelector(".fb-tile-zoom-preview-img");
|
|
2717
|
+
img.src = src;
|
|
2718
|
+
img.alt = alt;
|
|
2719
|
+
if (actionsEl) {
|
|
2720
|
+
popup.appendChild(actionsEl.cloneNode(true));
|
|
2721
|
+
attachClonedActionListeners(
|
|
2722
|
+
popup.querySelector(".fb-tile-actions"),
|
|
2723
|
+
actionsEl
|
|
2724
|
+
);
|
|
2725
|
+
}
|
|
2726
|
+
popup.style.pointerEvents = "auto";
|
|
2727
|
+
positionZoomPopup(popup, tile);
|
|
2728
|
+
document.body.appendChild(popup);
|
|
2729
|
+
popup.getBoundingClientRect();
|
|
2730
|
+
popup.classList.add("fb-tile-zoom-preview--visible");
|
|
2731
|
+
}, 200);
|
|
2732
|
+
});
|
|
2733
|
+
tile.addEventListener("mouseleave", () => {
|
|
2734
|
+
if (zoomTimer !== null) {
|
|
2735
|
+
clearTimeout(zoomTimer);
|
|
2736
|
+
zoomTimer = null;
|
|
2737
|
+
zoomOwner = null;
|
|
2738
|
+
} else {
|
|
2739
|
+
scheduleHideZoomPopup();
|
|
2740
|
+
}
|
|
2741
|
+
});
|
|
2742
|
+
}
|
|
2743
|
+
function attachClonedActionListeners(cloned, original) {
|
|
2744
|
+
const originalBtns = Array.from(original.querySelectorAll(".fb-tile-action-btn"));
|
|
2745
|
+
const clonedBtns = Array.from(cloned.querySelectorAll(".fb-tile-action-btn"));
|
|
2746
|
+
clonedBtns.forEach((clonedBtn, i) => {
|
|
2747
|
+
const origBtn = originalBtns[i];
|
|
2748
|
+
if (origBtn) {
|
|
2749
|
+
clonedBtn.addEventListener("click", (e) => {
|
|
2750
|
+
e.stopPropagation();
|
|
2751
|
+
origBtn.click();
|
|
2752
|
+
});
|
|
2753
|
+
}
|
|
2754
|
+
});
|
|
2755
|
+
}
|
|
2093
2756
|
function renderLocalImagePreview(container, file, fileName, state) {
|
|
2094
2757
|
const img = document.createElement("img");
|
|
2095
2758
|
img.className = "w-full h-full object-contain";
|
|
2759
|
+
img.style.background = "var(--fb-file-upload-bg-color,#f3f4f6)";
|
|
2096
2760
|
img.alt = fileName || t("previewAlt", state);
|
|
2097
2761
|
const reader = new FileReader();
|
|
2098
2762
|
reader.onload = (e) => {
|
|
@@ -2101,23 +2765,27 @@ function renderLocalImagePreview(container, file, fileName, state) {
|
|
|
2101
2765
|
reader.readAsDataURL(file);
|
|
2102
2766
|
container.appendChild(img);
|
|
2103
2767
|
}
|
|
2104
|
-
function
|
|
2105
|
-
const videoUrl = URL.createObjectURL(file);
|
|
2768
|
+
function setupDragDropless(container, _deps) {
|
|
2106
2769
|
container.onclick = null;
|
|
2107
2770
|
const newContainer = container.cloneNode(false);
|
|
2108
2771
|
if (container.parentNode) {
|
|
2109
2772
|
container.parentNode.replaceChild(newContainer, container);
|
|
2110
2773
|
}
|
|
2774
|
+
return newContainer;
|
|
2775
|
+
}
|
|
2776
|
+
function renderLocalVideoPreview(container, file, videoType, resourceId, state, deps) {
|
|
2777
|
+
const videoUrl = URL.createObjectURL(file);
|
|
2778
|
+
const newContainer = setupDragDropless(container);
|
|
2111
2779
|
newContainer.innerHTML = `
|
|
2112
|
-
<div class="
|
|
2780
|
+
<div class="fb-video-preview-wrap">
|
|
2113
2781
|
<video class="w-full h-full object-contain" controls preload="auto" muted src="${videoUrl}">
|
|
2114
2782
|
${escapeHtml(t("videoNotSupported", state))}
|
|
2115
2783
|
</video>
|
|
2116
|
-
<div class="
|
|
2117
|
-
<button class="
|
|
2784
|
+
<div class="fb-video-btn-overlay">
|
|
2785
|
+
<button class="fb-video-btn fb-video-btn-delete delete-file-btn">
|
|
2118
2786
|
${escapeHtml(t("removeElement", state))}
|
|
2119
2787
|
</button>
|
|
2120
|
-
<button class="
|
|
2788
|
+
<button class="fb-video-btn fb-video-btn-change change-file-btn">
|
|
2121
2789
|
${escapeHtml(t("changeButton", state))}
|
|
2122
2790
|
</button>
|
|
2123
2791
|
</div>
|
|
@@ -2127,20 +2795,14 @@ function renderLocalVideoPreview(container, file, videoType, resourceId, state,
|
|
|
2127
2795
|
return newContainer;
|
|
2128
2796
|
}
|
|
2129
2797
|
function attachVideoButtonHandlers(container, resourceId, state, deps) {
|
|
2130
|
-
const changeBtn = container.querySelector(
|
|
2131
|
-
".change-file-btn"
|
|
2132
|
-
);
|
|
2798
|
+
const changeBtn = container.querySelector(".change-file-btn");
|
|
2133
2799
|
if (changeBtn) {
|
|
2134
2800
|
changeBtn.onclick = (e) => {
|
|
2135
2801
|
e.stopPropagation();
|
|
2136
|
-
|
|
2137
|
-
deps.picker.click();
|
|
2138
|
-
}
|
|
2802
|
+
deps?.picker?.click();
|
|
2139
2803
|
};
|
|
2140
2804
|
}
|
|
2141
|
-
const deleteBtn = container.querySelector(
|
|
2142
|
-
".delete-file-btn"
|
|
2143
|
-
);
|
|
2805
|
+
const deleteBtn = container.querySelector(".delete-file-btn");
|
|
2144
2806
|
if (deleteBtn) {
|
|
2145
2807
|
deleteBtn.onclick = (e) => {
|
|
2146
2808
|
e.stopPropagation();
|
|
@@ -2159,9 +2821,6 @@ function handleVideoDelete(container, resourceId, state, deps) {
|
|
|
2159
2821
|
if (deps?.fileUploadHandler) {
|
|
2160
2822
|
container.onclick = deps.fileUploadHandler;
|
|
2161
2823
|
}
|
|
2162
|
-
if (deps?.dragHandler) {
|
|
2163
|
-
setupDragAndDrop(container, deps.dragHandler);
|
|
2164
|
-
}
|
|
2165
2824
|
container.innerHTML = `
|
|
2166
2825
|
<div class="flex flex-col items-center justify-center h-full text-gray-400">
|
|
2167
2826
|
<svg class="w-6 h-6 mb-2" fill="currentColor" viewBox="0 0 24 24">
|
|
@@ -2170,18 +2829,9 @@ function handleVideoDelete(container, resourceId, state, deps) {
|
|
|
2170
2829
|
<div class="text-sm text-center">${escapeHtml(t("clickDragText", state))}</div>
|
|
2171
2830
|
</div>
|
|
2172
2831
|
`;
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
video.className = "w-full h-full object-contain";
|
|
2177
|
-
video.controls = true;
|
|
2178
|
-
video.preload = "metadata";
|
|
2179
|
-
video.muted = true;
|
|
2180
|
-
video.src = thumbnailUrl;
|
|
2181
|
-
video.appendChild(
|
|
2182
|
-
document.createTextNode(t("videoNotSupported", state))
|
|
2183
|
-
);
|
|
2184
|
-
container.appendChild(video);
|
|
2832
|
+
if (deps?.setupDrop) {
|
|
2833
|
+
deps.setupDrop(container);
|
|
2834
|
+
}
|
|
2185
2835
|
}
|
|
2186
2836
|
function renderDeleteButton(container, resourceId, state) {
|
|
2187
2837
|
addDeleteButton(container, state, () => {
|
|
@@ -2203,13 +2853,11 @@ function renderDeleteButton(container, resourceId, state) {
|
|
|
2203
2853
|
});
|
|
2204
2854
|
}
|
|
2205
2855
|
async function renderLocalFilePreview(container, meta, fileName, resourceId, isReadonly, state, deps) {
|
|
2206
|
-
if (!meta.file || !(meta.file instanceof File))
|
|
2207
|
-
|
|
2208
|
-
}
|
|
2209
|
-
if (meta.type && meta.type.startsWith("image/")) {
|
|
2856
|
+
if (!meta.file || !(meta.file instanceof File)) return;
|
|
2857
|
+
if (meta.type?.startsWith("image/")) {
|
|
2210
2858
|
renderLocalImagePreview(container, meta.file, fileName, state);
|
|
2211
|
-
} else if (meta.type
|
|
2212
|
-
|
|
2859
|
+
} else if (meta.type?.startsWith("video/")) {
|
|
2860
|
+
container = renderLocalVideoPreview(
|
|
2213
2861
|
container,
|
|
2214
2862
|
meta.file,
|
|
2215
2863
|
meta.type,
|
|
@@ -2217,14 +2865,23 @@ async function renderLocalFilePreview(container, meta, fileName, resourceId, isR
|
|
|
2217
2865
|
state,
|
|
2218
2866
|
deps
|
|
2219
2867
|
);
|
|
2220
|
-
container = newContainer;
|
|
2221
2868
|
} else {
|
|
2222
|
-
container.innerHTML = `<div class="flex flex-col items-center justify-center h-full text-gray-400"><div
|
|
2869
|
+
container.innerHTML = `<div class="flex flex-col items-center justify-center h-full text-gray-400"><div style="font-size:36px;" class="mb-2">\u{1F4C1}</div><div class="text-sm">${escapeHtml(fileName)}</div></div>`;
|
|
2223
2870
|
}
|
|
2224
|
-
if (!isReadonly && !
|
|
2871
|
+
if (!isReadonly && !meta.type?.startsWith("video/")) {
|
|
2225
2872
|
renderDeleteButton(container, resourceId, state);
|
|
2226
2873
|
}
|
|
2227
2874
|
}
|
|
2875
|
+
function renderUploadedVideoPreview(container, thumbnailUrl, state) {
|
|
2876
|
+
const video = document.createElement("video");
|
|
2877
|
+
video.className = "w-full h-full object-contain";
|
|
2878
|
+
video.controls = true;
|
|
2879
|
+
video.preload = "metadata";
|
|
2880
|
+
video.muted = true;
|
|
2881
|
+
video.src = thumbnailUrl;
|
|
2882
|
+
video.appendChild(document.createTextNode(t("videoNotSupported", state)));
|
|
2883
|
+
container.appendChild(video);
|
|
2884
|
+
}
|
|
2228
2885
|
async function renderUploadedFilePreview(container, resourceId, fileName, meta, state) {
|
|
2229
2886
|
if (!state.config.getThumbnail) {
|
|
2230
2887
|
setEmptyFileContainer(container, state);
|
|
@@ -2234,11 +2891,12 @@ async function renderUploadedFilePreview(container, resourceId, fileName, meta,
|
|
|
2234
2891
|
const thumbnailUrl = await state.config.getThumbnail(resourceId);
|
|
2235
2892
|
if (thumbnailUrl) {
|
|
2236
2893
|
clear(container);
|
|
2237
|
-
if (meta
|
|
2238
|
-
renderUploadedVideoPreview(container, thumbnailUrl,
|
|
2894
|
+
if (meta?.type?.startsWith("video/")) {
|
|
2895
|
+
renderUploadedVideoPreview(container, thumbnailUrl, state);
|
|
2239
2896
|
} else {
|
|
2240
2897
|
const img = document.createElement("img");
|
|
2241
2898
|
img.className = "w-full h-full object-contain";
|
|
2899
|
+
img.style.background = "var(--fb-file-upload-bg-color,#f3f4f6)";
|
|
2242
2900
|
img.alt = fileName || t("previewAlt", state);
|
|
2243
2901
|
img.src = thumbnailUrl;
|
|
2244
2902
|
container.appendChild(img);
|
|
@@ -2266,9 +2924,6 @@ async function renderFilePreview(container, resourceId, state, options = {}) {
|
|
|
2266
2924
|
);
|
|
2267
2925
|
}
|
|
2268
2926
|
clear(container);
|
|
2269
|
-
if (isReadonly) {
|
|
2270
|
-
container.classList.add("cursor-pointer");
|
|
2271
|
-
}
|
|
2272
2927
|
const meta = state.resourceIndex.get(resourceId);
|
|
2273
2928
|
if (meta && meta.file && meta.file instanceof File) {
|
|
2274
2929
|
await renderLocalFilePreview(
|
|
@@ -2281,364 +2936,291 @@ async function renderFilePreview(container, resourceId, state, options = {}) {
|
|
|
2281
2936
|
deps
|
|
2282
2937
|
);
|
|
2283
2938
|
} else {
|
|
2284
|
-
await renderUploadedFilePreview(
|
|
2285
|
-
container,
|
|
2286
|
-
resourceId,
|
|
2287
|
-
fileName,
|
|
2288
|
-
meta,
|
|
2289
|
-
state
|
|
2290
|
-
);
|
|
2939
|
+
await renderUploadedFilePreview(container, resourceId, fileName, meta, state);
|
|
2291
2940
|
const isVideo = meta?.type?.startsWith("video/");
|
|
2292
2941
|
if (!isReadonly && !isVideo) {
|
|
2293
2942
|
renderDeleteButton(container, resourceId, state);
|
|
2294
2943
|
}
|
|
2295
2944
|
}
|
|
2296
2945
|
}
|
|
2297
|
-
|
|
2946
|
+
function resolveFileName(resourceId, meta, fileName) {
|
|
2947
|
+
if (fileName) return fileName;
|
|
2948
|
+
if (meta?.name?.includes(".")) return meta.name;
|
|
2949
|
+
const basename = resourceId.includes("/") ? resourceId.split("/").pop() : resourceId;
|
|
2950
|
+
return basename?.includes(".") ? basename : "";
|
|
2951
|
+
}
|
|
2952
|
+
async function renderFilePreviewReadonly(resourceId, state, fileName, options = {}) {
|
|
2298
2953
|
const meta = state.resourceIndex.get(resourceId);
|
|
2299
|
-
const actualFileName = meta
|
|
2300
|
-
const
|
|
2301
|
-
const
|
|
2302
|
-
|
|
2303
|
-
const
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
}
|
|
2309
|
-
const
|
|
2310
|
-
const
|
|
2311
|
-
|
|
2954
|
+
const actualFileName = resolveFileName(resourceId, meta, fileName);
|
|
2955
|
+
const { canRemove = false, removeHandler = null } = options;
|
|
2956
|
+
const isImage = meta?.type?.startsWith("image/") || Boolean(actualFileName.toLowerCase().match(/\.(jpg|jpeg|png|gif|webp)$/));
|
|
2957
|
+
const isVideo = meta?.type?.startsWith("video/") || Boolean(actualFileName.toLowerCase().match(/\.(mp4|webm|avi|mov)$/));
|
|
2958
|
+
const tile = createFileTile();
|
|
2959
|
+
tile.classList.add("fb-tile-resource");
|
|
2960
|
+
tile.style.cursor = "pointer";
|
|
2961
|
+
if (actualFileName) {
|
|
2962
|
+
tile.title = actualFileName;
|
|
2963
|
+
}
|
|
2964
|
+
const localFileUrl = meta?.file instanceof File ? getLocalFileUrl(meta.file) : null;
|
|
2965
|
+
const resolveOpenUrl = async () => {
|
|
2966
|
+
if (state.config.getDownloadUrl) return state.config.getDownloadUrl(resourceId);
|
|
2967
|
+
if (state.config.getThumbnail) return state.config.getThumbnail(resourceId);
|
|
2968
|
+
return localFileUrl;
|
|
2969
|
+
};
|
|
2970
|
+
tile.onclick = async () => {
|
|
2971
|
+
const url = await resolveOpenUrl();
|
|
2972
|
+
if (url) {
|
|
2973
|
+
window.open(url, "_blank");
|
|
2974
|
+
} else if (state.config.downloadFile) {
|
|
2975
|
+
state.config.downloadFile(resourceId, actualFileName);
|
|
2976
|
+
} else {
|
|
2977
|
+
forceDownload(resourceId, actualFileName, state).catch((err) => {
|
|
2978
|
+
console.error("Download failed:", err);
|
|
2979
|
+
});
|
|
2980
|
+
}
|
|
2981
|
+
};
|
|
2982
|
+
const actionsEl = createTileActions({
|
|
2983
|
+
canRemove,
|
|
2984
|
+
removeHandler,
|
|
2985
|
+
state,
|
|
2986
|
+
resourceId,
|
|
2987
|
+
fileName: actualFileName,
|
|
2988
|
+
meta
|
|
2989
|
+
});
|
|
2990
|
+
const resolveImageDisplayUrl = async () => {
|
|
2312
2991
|
if (state.config.getThumbnail) {
|
|
2313
2992
|
try {
|
|
2314
|
-
const
|
|
2315
|
-
if (
|
|
2316
|
-
|
|
2317
|
-
} else {
|
|
2318
|
-
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>`;
|
|
2319
|
-
}
|
|
2320
|
-
} catch (error) {
|
|
2321
|
-
console.warn("getThumbnail failed for", resourceId, error);
|
|
2322
|
-
previewContainer.innerHTML = `<div class="aspect-video flex items-center justify-center text-gray-400"><div class="text-center"><div class="text-4xl mb-2">\u{1F5BC}\uFE0F</div><div class="text-sm">${escapeHtml(actualFileName)}</div></div></div>`;
|
|
2993
|
+
const url = await state.config.getThumbnail(resourceId);
|
|
2994
|
+
if (url) return url;
|
|
2995
|
+
} catch {
|
|
2323
2996
|
}
|
|
2997
|
+
}
|
|
2998
|
+
return localFileUrl;
|
|
2999
|
+
};
|
|
3000
|
+
const renderImageFallback = () => {
|
|
3001
|
+
tile.innerHTML = `<div style="display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;font-size:36px;">\u{1F5BC}\uFE0F</div>`;
|
|
3002
|
+
tile.appendChild(actionsEl);
|
|
3003
|
+
};
|
|
3004
|
+
if (isImage) {
|
|
3005
|
+
const displayUrl = await resolveImageDisplayUrl();
|
|
3006
|
+
if (displayUrl) {
|
|
3007
|
+
const img = document.createElement("img");
|
|
3008
|
+
img.style.cssText = "width:100%;height:100%;object-fit:contain;background:var(--fb-file-upload-bg-color,#f3f4f6);";
|
|
3009
|
+
img.alt = actualFileName;
|
|
3010
|
+
img.src = displayUrl;
|
|
3011
|
+
tile.appendChild(img);
|
|
3012
|
+
tile.appendChild(actionsEl);
|
|
3013
|
+
attachZoomHover(tile, displayUrl, actualFileName, actionsEl);
|
|
2324
3014
|
} else {
|
|
2325
|
-
|
|
3015
|
+
renderImageFallback();
|
|
2326
3016
|
}
|
|
2327
3017
|
} else if (isVideo) {
|
|
2328
3018
|
if (state.config.getThumbnail) {
|
|
2329
3019
|
try {
|
|
2330
3020
|
const videoUrl = await state.config.getThumbnail(resourceId);
|
|
2331
3021
|
if (videoUrl) {
|
|
2332
|
-
|
|
2333
|
-
<
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
<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">
|
|
2338
|
-
<div class="bg-white bg-opacity-90 rounded-full p-3">
|
|
2339
|
-
<svg class="w-8 h-8 text-gray-800" fill="currentColor" viewBox="0 0 24 24">
|
|
2340
|
-
<path d="M8 5v14l11-7z"/>
|
|
2341
|
-
</svg>
|
|
2342
|
-
</div>
|
|
3022
|
+
tile.innerHTML = `
|
|
3023
|
+
<img style="width:100%;height:100%;object-fit:contain;background:var(--fb-file-upload-bg-color,#f3f4f6);" alt="${escapeHtml(actualFileName)}" src="${videoUrl}">
|
|
3024
|
+
<div class="fb-video-overlay">
|
|
3025
|
+
<div class="fb-play-btn" style="width:22px;height:22px;">
|
|
3026
|
+
<svg width="10" height="12" viewBox="0 0 10 12" fill="currentColor"><path d="M0 0l10 6-10 6z"/></svg>
|
|
2343
3027
|
</div>
|
|
2344
|
-
</div
|
|
2345
|
-
|
|
3028
|
+
</div>`;
|
|
3029
|
+
tile.appendChild(actionsEl);
|
|
2346
3030
|
} else {
|
|
2347
|
-
|
|
3031
|
+
tile.innerHTML = `<div style="display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;font-size:36px;">\u{1F3A5}</div>`;
|
|
3032
|
+
tile.appendChild(actionsEl);
|
|
2348
3033
|
}
|
|
2349
|
-
} catch
|
|
2350
|
-
|
|
2351
|
-
|
|
3034
|
+
} catch {
|
|
3035
|
+
tile.innerHTML = `<div style="display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;font-size:36px;">\u{1F3A5}</div>`;
|
|
3036
|
+
tile.appendChild(actionsEl);
|
|
2352
3037
|
}
|
|
2353
3038
|
} else {
|
|
2354
|
-
|
|
3039
|
+
tile.innerHTML = `<div style="display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;font-size:36px;">\u{1F3A5}</div>`;
|
|
3040
|
+
tile.appendChild(actionsEl);
|
|
2355
3041
|
}
|
|
2356
3042
|
} else {
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
3043
|
+
if (state.config.getThumbnail) {
|
|
3044
|
+
try {
|
|
3045
|
+
const thumbUrl = await state.config.getThumbnail(resourceId);
|
|
3046
|
+
if (thumbUrl) {
|
|
3047
|
+
const img = document.createElement("img");
|
|
3048
|
+
img.style.cssText = "width:100%;height:100%;object-fit:contain;background:var(--fb-file-upload-bg-color,#f3f4f6);";
|
|
3049
|
+
img.alt = actualFileName || resourceId;
|
|
3050
|
+
img.src = thumbUrl;
|
|
3051
|
+
tile.appendChild(img);
|
|
3052
|
+
tile.appendChild(actionsEl);
|
|
3053
|
+
return tile;
|
|
3054
|
+
}
|
|
3055
|
+
} catch {
|
|
3056
|
+
}
|
|
2371
3057
|
}
|
|
3058
|
+
const captionHtml = actualFileName ? `<div class="fb-tile-label">${escapeHtml(actualFileName.length > 10 ? actualFileName.substring(0, 8) + "\u2026" : actualFileName)}</div>
|
|
3059
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor"><path d="M19 9h-4V3H9v6H5l7 7 7-7zm-7 9H5v2h14v-2h-7z"/></svg>` : "";
|
|
3060
|
+
tile.innerHTML = `
|
|
3061
|
+
<div style="display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;padding:6px;gap:4px;">
|
|
3062
|
+
<div style="font-size:36px;">\u{1F4C1}</div>
|
|
3063
|
+
${captionHtml}
|
|
3064
|
+
</div>`;
|
|
3065
|
+
tile.appendChild(actionsEl);
|
|
2372
3066
|
}
|
|
2373
|
-
|
|
2374
|
-
fileNameElement.className = isPSD ? "hidden" : "text-sm font-medium text-gray-900 text-center";
|
|
2375
|
-
fileNameElement.textContent = actualFileName;
|
|
2376
|
-
const downloadButton = document.createElement("button");
|
|
2377
|
-
downloadButton.className = "w-full px-3 py-2 text-sm bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors";
|
|
2378
|
-
downloadButton.textContent = t("downloadButton", state);
|
|
2379
|
-
downloadButton.onclick = (e) => {
|
|
2380
|
-
e.preventDefault();
|
|
2381
|
-
e.stopPropagation();
|
|
2382
|
-
if (state.config.downloadFile) {
|
|
2383
|
-
state.config.downloadFile(resourceId, actualFileName);
|
|
2384
|
-
} else {
|
|
2385
|
-
forceDownload(resourceId, actualFileName, state);
|
|
2386
|
-
}
|
|
2387
|
-
};
|
|
2388
|
-
fileResult.appendChild(previewContainer);
|
|
2389
|
-
fileResult.appendChild(fileNameElement);
|
|
2390
|
-
fileResult.appendChild(downloadButton);
|
|
2391
|
-
return fileResult;
|
|
3067
|
+
return tile;
|
|
2392
3068
|
}
|
|
2393
|
-
function
|
|
2394
|
-
|
|
2395
|
-
const
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
};
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
for (let i = 0; i < 4; i++) {
|
|
2406
|
-
const slot = document.createElement("div");
|
|
2407
|
-
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";
|
|
2408
|
-
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
|
2409
|
-
svg.setAttribute("class", "w-12 h-12 text-gray-400");
|
|
2410
|
-
svg.setAttribute("fill", "currentColor");
|
|
2411
|
-
svg.setAttribute("viewBox", "0 0 24 24");
|
|
2412
|
-
const path = document.createElementNS(
|
|
2413
|
-
"http://www.w3.org/2000/svg",
|
|
2414
|
-
"path"
|
|
2415
|
-
);
|
|
2416
|
-
path.setAttribute(
|
|
2417
|
-
"d",
|
|
2418
|
-
"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"
|
|
2419
|
-
);
|
|
2420
|
-
svg.appendChild(path);
|
|
2421
|
-
slot.appendChild(svg);
|
|
2422
|
-
slot.onclick = () => {
|
|
2423
|
-
let filesWrapper = container.parentElement;
|
|
2424
|
-
while (filesWrapper && !filesWrapper.classList.contains("space-y-2")) {
|
|
2425
|
-
filesWrapper = filesWrapper.parentElement;
|
|
2426
|
-
}
|
|
2427
|
-
if (!filesWrapper && container.classList.contains("space-y-2")) {
|
|
2428
|
-
filesWrapper = container;
|
|
2429
|
-
}
|
|
2430
|
-
const fileInput = filesWrapper?.querySelector(
|
|
2431
|
-
'input[type="file"]'
|
|
2432
|
-
);
|
|
2433
|
-
if (fileInput) fileInput.click();
|
|
2434
|
-
};
|
|
2435
|
-
gridContainer2.appendChild(slot);
|
|
2436
|
-
}
|
|
2437
|
-
const hintText2 = document.createElement("div");
|
|
2438
|
-
hintText2.className = "text-center text-xs text-gray-500 mt-2";
|
|
2439
|
-
hintText2.textContent = buildHintLine();
|
|
2440
|
-
container.appendChild(gridContainer2);
|
|
2441
|
-
container.appendChild(hintText2);
|
|
2442
|
-
return;
|
|
2443
|
-
}
|
|
2444
|
-
const gridContainer = document.createElement("div");
|
|
2445
|
-
gridContainer.className = "files-list grid grid-cols-4 gap-3";
|
|
2446
|
-
const currentImagesCount = rids ? rids.length : 0;
|
|
2447
|
-
const rowsNeeded = Math.floor(currentImagesCount / 4) + 1;
|
|
2448
|
-
const slotsNeeded = rowsNeeded * 4;
|
|
2449
|
-
for (let i = 0; i < slotsNeeded; i++) {
|
|
2450
|
-
const slot = document.createElement("div");
|
|
2451
|
-
if (rids && i < rids.length) {
|
|
2452
|
-
const rid = rids[i];
|
|
2453
|
-
const meta = state.resourceIndex.get(rid);
|
|
2454
|
-
slot.className = "resource-pill aspect-square bg-gray-100 rounded-lg overflow-hidden relative group border border-gray-300";
|
|
2455
|
-
slot.dataset.resourceId = rid;
|
|
2456
|
-
renderThumbnailForResource(slot, rid, meta, state).catch((err) => {
|
|
2457
|
-
console.error("Failed to render thumbnail:", err);
|
|
2458
|
-
slot.innerHTML = `<div class="flex flex-col items-center justify-center h-full text-gray-400">
|
|
2459
|
-
<div class="text-2xl mb-1">\u{1F4C1}</div>
|
|
2460
|
-
<div class="text-xs">${escapeHtml(t("previewError", state))}</div>
|
|
2461
|
-
</div>`;
|
|
2462
|
-
});
|
|
2463
|
-
if (onRemove) {
|
|
2464
|
-
const overlay = document.createElement("div");
|
|
2465
|
-
overlay.className = "absolute inset-0 bg-black bg-opacity-50 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center";
|
|
2466
|
-
const removeBtn = document.createElement("button");
|
|
2467
|
-
removeBtn.className = "bg-red-600 text-white px-2 py-1 rounded text-xs";
|
|
2468
|
-
removeBtn.textContent = t("removeElement", state);
|
|
2469
|
-
removeBtn.onclick = (e) => {
|
|
2470
|
-
e.stopPropagation();
|
|
2471
|
-
onRemove(rid);
|
|
2472
|
-
};
|
|
2473
|
-
overlay.appendChild(removeBtn);
|
|
2474
|
-
slot.appendChild(overlay);
|
|
2475
|
-
}
|
|
2476
|
-
} else {
|
|
2477
|
-
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";
|
|
2478
|
-
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>';
|
|
2479
|
-
slot.onclick = () => {
|
|
2480
|
-
let filesWrapper = container.parentElement;
|
|
2481
|
-
while (filesWrapper && !filesWrapper.classList.contains("space-y-2")) {
|
|
2482
|
-
filesWrapper = filesWrapper.parentElement;
|
|
2483
|
-
}
|
|
2484
|
-
if (!filesWrapper && container.classList.contains("space-y-2")) {
|
|
2485
|
-
filesWrapper = container;
|
|
2486
|
-
}
|
|
2487
|
-
const fileInput = filesWrapper?.querySelector(
|
|
2488
|
-
'input[type="file"]'
|
|
2489
|
-
);
|
|
2490
|
-
if (fileInput) fileInput.click();
|
|
2491
|
-
};
|
|
2492
|
-
}
|
|
2493
|
-
gridContainer.appendChild(slot);
|
|
2494
|
-
}
|
|
2495
|
-
container.appendChild(gridContainer);
|
|
2496
|
-
const hintText = document.createElement("div");
|
|
2497
|
-
hintText.className = "text-center text-xs text-gray-500 mt-2";
|
|
2498
|
-
hintText.textContent = buildHintLine();
|
|
2499
|
-
container.appendChild(hintText);
|
|
2500
|
-
}
|
|
2501
|
-
function renderThumbnailError(slot, state, iconSize = "w-12 h-12") {
|
|
2502
|
-
slot.innerHTML = `<div class="flex flex-col items-center justify-center h-full text-gray-400">
|
|
2503
|
-
<svg class="${escapeHtml(iconSize)} text-red-400" fill="currentColor" viewBox="0 0 24 24">
|
|
2504
|
-
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z"/>
|
|
2505
|
-
</svg>
|
|
2506
|
-
<div class="text-xs mt-1 text-red-600">${escapeHtml(t("previewError", state))}</div>
|
|
2507
|
-
</div>`;
|
|
3069
|
+
async function renderSingleFileEditTile(fileContainer, resourceId, state, deps) {
|
|
3070
|
+
const meta = state.resourceIndex.get(resourceId);
|
|
3071
|
+
const fileName = meta?.name ?? resourceId.split("/").pop() ?? "";
|
|
3072
|
+
const removeHandler = deps.onRemove ?? null;
|
|
3073
|
+
const tile = await renderFilePreviewReadonly(resourceId, state, fileName, {
|
|
3074
|
+
canRemove: true,
|
|
3075
|
+
removeHandler
|
|
3076
|
+
});
|
|
3077
|
+
fileContainer.className = "file-preview-container";
|
|
3078
|
+
fileContainer.removeAttribute("style");
|
|
3079
|
+
clear(fileContainer);
|
|
3080
|
+
fileContainer.appendChild(tile);
|
|
2508
3081
|
}
|
|
2509
|
-
async function
|
|
2510
|
-
if (meta
|
|
3082
|
+
async function fillTileContent(tile, rid, meta, state, actionsEl) {
|
|
3083
|
+
if (meta?.type?.startsWith("image/")) {
|
|
2511
3084
|
if (meta.file && meta.file instanceof File) {
|
|
2512
3085
|
const img = document.createElement("img");
|
|
2513
|
-
img.
|
|
3086
|
+
img.style.cssText = "width:100%;height:100%;object-fit:contain;background:var(--fb-file-upload-bg-color,#f3f4f6);";
|
|
2514
3087
|
img.alt = meta.name;
|
|
2515
3088
|
const reader = new FileReader();
|
|
2516
3089
|
reader.onload = (e) => {
|
|
2517
3090
|
img.src = e.target?.result || "";
|
|
3091
|
+
attachZoomHover(tile, img.src, meta.name, actionsEl ?? null);
|
|
2518
3092
|
};
|
|
2519
3093
|
reader.readAsDataURL(meta.file);
|
|
2520
|
-
|
|
3094
|
+
tile.appendChild(img);
|
|
2521
3095
|
} else if (state.config.getThumbnail) {
|
|
2522
3096
|
try {
|
|
2523
3097
|
const url = await state.config.getThumbnail(rid);
|
|
2524
3098
|
if (url) {
|
|
2525
3099
|
const img = document.createElement("img");
|
|
2526
|
-
img.
|
|
3100
|
+
img.style.cssText = "width:100%;height:100%;object-fit:contain;background:var(--fb-file-upload-bg-color,#f3f4f6);";
|
|
2527
3101
|
img.alt = meta.name;
|
|
2528
3102
|
img.src = url;
|
|
2529
|
-
|
|
3103
|
+
tile.appendChild(img);
|
|
3104
|
+
attachZoomHover(tile, url, meta.name, actionsEl ?? null);
|
|
2530
3105
|
} else {
|
|
2531
|
-
|
|
2532
|
-
<svg class="w-12 h-12" fill="currentColor" viewBox="0 0 24 24">
|
|
2533
|
-
<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"/>
|
|
2534
|
-
</svg>
|
|
2535
|
-
</div>`;
|
|
3106
|
+
tile.innerHTML = `<div style="display:flex;align-items:center;justify-content:center;height:100%;font-size:36px;">\u{1F5BC}\uFE0F</div>`;
|
|
2536
3107
|
}
|
|
2537
3108
|
} catch (error) {
|
|
2538
3109
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
2539
|
-
if (state.config.onThumbnailError)
|
|
2540
|
-
|
|
2541
|
-
}
|
|
2542
|
-
renderThumbnailError(slot, state);
|
|
3110
|
+
if (state.config.onThumbnailError) state.config.onThumbnailError(err, rid);
|
|
3111
|
+
tile.innerHTML = `<div style="display:flex;align-items:center;justify-content:center;height:100%;font-size:16px;color:var(--fb-error-color,#ef4444);">\u2715</div>`;
|
|
2543
3112
|
}
|
|
2544
3113
|
} else {
|
|
2545
|
-
|
|
2546
|
-
<svg class="w-12 h-12" fill="currentColor" viewBox="0 0 24 24">
|
|
2547
|
-
<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"/>
|
|
2548
|
-
</svg>
|
|
2549
|
-
</div>`;
|
|
3114
|
+
tile.innerHTML = `<div style="display:flex;align-items:center;justify-content:center;height:100%;font-size:36px;">\u{1F5BC}\uFE0F</div>`;
|
|
2550
3115
|
}
|
|
2551
|
-
|
|
3116
|
+
if (actionsEl) tile.appendChild(actionsEl);
|
|
3117
|
+
} else if (meta?.type?.startsWith("video/")) {
|
|
2552
3118
|
if (meta.file && meta.file instanceof File) {
|
|
2553
3119
|
const videoUrl = URL.createObjectURL(meta.file);
|
|
2554
|
-
|
|
2555
|
-
<
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
<div class="bg-white bg-opacity-90 rounded-full p-1">
|
|
2560
|
-
<svg class="w-4 h-4 text-gray-800" fill="currentColor" viewBox="0 0 24 24">
|
|
2561
|
-
<path d="M8 5v14l11-7z"/>
|
|
2562
|
-
</svg>
|
|
2563
|
-
</div>
|
|
3120
|
+
tile.innerHTML = `
|
|
3121
|
+
<video style="width:100%;height:100%;" preload="metadata" muted src="${videoUrl}"></video>
|
|
3122
|
+
<div class="fb-video-overlay">
|
|
3123
|
+
<div class="fb-play-btn" style="width:20px;height:20px;">
|
|
3124
|
+
<svg width="8" height="10" viewBox="0 0 8 10" fill="currentColor"><path d="M0 0l8 5-8 5z"/></svg>
|
|
2564
3125
|
</div>
|
|
2565
|
-
</div
|
|
2566
|
-
`;
|
|
3126
|
+
</div>`;
|
|
2567
3127
|
} else if (state.config.getThumbnail) {
|
|
2568
3128
|
try {
|
|
2569
3129
|
const videoUrl = await state.config.getThumbnail(rid);
|
|
2570
3130
|
if (videoUrl) {
|
|
2571
|
-
|
|
2572
|
-
<
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
<div class="bg-white bg-opacity-90 rounded-full p-1">
|
|
2577
|
-
<svg class="w-4 h-4 text-gray-800" fill="currentColor" viewBox="0 0 24 24">
|
|
2578
|
-
<path d="M8 5v14l11-7z"/>
|
|
2579
|
-
</svg>
|
|
2580
|
-
</div>
|
|
3131
|
+
tile.innerHTML = `
|
|
3132
|
+
<img style="width:100%;height:100%;object-fit:contain;background:var(--fb-file-upload-bg-color,#f3f4f6);" alt="${escapeHtml(meta.name)}" src="${videoUrl}">
|
|
3133
|
+
<div class="fb-video-overlay">
|
|
3134
|
+
<div class="fb-play-btn" style="width:20px;height:20px;">
|
|
3135
|
+
<svg width="8" height="10" viewBox="0 0 8 10" fill="currentColor"><path d="M0 0l8 5-8 5z"/></svg>
|
|
2581
3136
|
</div>
|
|
2582
|
-
</div
|
|
2583
|
-
`;
|
|
3137
|
+
</div>`;
|
|
2584
3138
|
} else {
|
|
2585
|
-
|
|
2586
|
-
<svg class="w-8 h-8" fill="currentColor" viewBox="0 0 24 24">
|
|
2587
|
-
<path d="M8 5v14l11-7z"/>
|
|
2588
|
-
</svg>
|
|
2589
|
-
<div class="text-xs mt-1">${escapeHtml(meta?.name || "Video")}</div>
|
|
2590
|
-
</div>`;
|
|
3139
|
+
tile.innerHTML = `<div style="display:flex;align-items:center;justify-content:center;height:100%;font-size:36px;">\u{1F3A5}</div>`;
|
|
2591
3140
|
}
|
|
2592
3141
|
} catch (error) {
|
|
2593
3142
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
2594
|
-
if (state.config.onThumbnailError)
|
|
2595
|
-
|
|
2596
|
-
}
|
|
2597
|
-
renderThumbnailError(slot, state, "w-8 h-8");
|
|
3143
|
+
if (state.config.onThumbnailError) state.config.onThumbnailError(err, rid);
|
|
3144
|
+
tile.innerHTML = `<div style="display:flex;align-items:center;justify-content:center;height:100%;font-size:16px;color:var(--fb-error-color,#ef4444);">\u2715</div>`;
|
|
2598
3145
|
}
|
|
2599
3146
|
} else {
|
|
2600
|
-
|
|
2601
|
-
<svg class="w-8 h-8" fill="currentColor" viewBox="0 0 24 24">
|
|
2602
|
-
<path d="M8 5v14l11-7z"/>
|
|
2603
|
-
</svg>
|
|
2604
|
-
<div class="text-xs mt-1">${escapeHtml(meta?.name || "Video")}</div>
|
|
2605
|
-
</div>`;
|
|
3147
|
+
tile.innerHTML = `<div style="display:flex;align-items:center;justify-content:center;height:100%;font-size:36px;">\u{1F3A5}</div>`;
|
|
2606
3148
|
}
|
|
3149
|
+
if (actionsEl) tile.appendChild(actionsEl);
|
|
2607
3150
|
} else {
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
3151
|
+
const name = meta?.name ?? "";
|
|
3152
|
+
const hasExtension = name.includes(".");
|
|
3153
|
+
const captionHtml = hasExtension ? `<div class="fb-tile-label">${escapeHtml(name.length > 10 ? name.substring(0, 8) + "\u2026" : name)}</div>` : "";
|
|
3154
|
+
tile.innerHTML = `
|
|
3155
|
+
<div style="display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;padding:6px;gap:4px;">
|
|
3156
|
+
<div style="font-size:36px;">\u{1F4C1}</div>
|
|
3157
|
+
${captionHtml}
|
|
3158
|
+
</div>`;
|
|
3159
|
+
if (actionsEl) tile.appendChild(actionsEl);
|
|
2612
3160
|
}
|
|
2613
3161
|
}
|
|
2614
|
-
function
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
3162
|
+
async function forceDownload(resourceId, fileName, state) {
|
|
3163
|
+
try {
|
|
3164
|
+
let fileUrl = null;
|
|
3165
|
+
if (state.config.getDownloadUrl) {
|
|
3166
|
+
fileUrl = state.config.getDownloadUrl(resourceId);
|
|
3167
|
+
} else if (state.config.getThumbnail) {
|
|
3168
|
+
fileUrl = await state.config.getThumbnail(resourceId);
|
|
3169
|
+
}
|
|
3170
|
+
if (fileUrl) {
|
|
3171
|
+
const finalUrl = fileUrl.startsWith("http") ? fileUrl : new URL(fileUrl, window.location.href).href;
|
|
3172
|
+
const response = await fetch(finalUrl);
|
|
3173
|
+
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
|
|
3174
|
+
const blob = await response.blob();
|
|
3175
|
+
downloadBlob(blob, fileName);
|
|
3176
|
+
} else {
|
|
3177
|
+
throw new Error("No download URL available for resource");
|
|
3178
|
+
}
|
|
3179
|
+
} catch (error) {
|
|
3180
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
3181
|
+
if (state.config.onDownloadError) {
|
|
3182
|
+
state.config.onDownloadError(err, resourceId, fileName);
|
|
3183
|
+
}
|
|
3184
|
+
console.error(`File download failed for ${fileName}:`, err);
|
|
3185
|
+
throw err;
|
|
3186
|
+
}
|
|
2625
3187
|
}
|
|
2626
|
-
function
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
3188
|
+
function downloadBlob(blob, fileName) {
|
|
3189
|
+
try {
|
|
3190
|
+
const blobUrl = URL.createObjectURL(blob);
|
|
3191
|
+
const link = document.createElement("a");
|
|
3192
|
+
link.href = blobUrl;
|
|
3193
|
+
link.download = fileName;
|
|
3194
|
+
link.style.display = "none";
|
|
3195
|
+
document.body.appendChild(link);
|
|
3196
|
+
link.click();
|
|
3197
|
+
document.body.removeChild(link);
|
|
3198
|
+
setTimeout(() => {
|
|
3199
|
+
URL.revokeObjectURL(blobUrl);
|
|
3200
|
+
}, 100);
|
|
3201
|
+
} catch (error) {
|
|
3202
|
+
throw new Error(`Blob download failed: ${error.message}`);
|
|
3203
|
+
}
|
|
2638
3204
|
}
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
3205
|
+
|
|
3206
|
+
// src/components/file/upload.ts
|
|
3207
|
+
async function uploadSingleFile(file, state) {
|
|
3208
|
+
if (!state.config.uploadFile) {
|
|
3209
|
+
throw new Error(
|
|
3210
|
+
"No upload handler configured. Set uploadHandler via FormBuilder.setUploadHandler()"
|
|
3211
|
+
);
|
|
3212
|
+
}
|
|
3213
|
+
try {
|
|
3214
|
+
const rid = await state.config.uploadFile(file);
|
|
3215
|
+
if (typeof rid !== "string") {
|
|
3216
|
+
throw new Error("Upload handler must return a string resource ID");
|
|
3217
|
+
}
|
|
3218
|
+
return rid;
|
|
3219
|
+
} catch (error) {
|
|
3220
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
3221
|
+
if (state.config.onUploadError) state.config.onUploadError(err, file);
|
|
3222
|
+
throw new Error(`File upload failed: ${err.message}`);
|
|
3223
|
+
}
|
|
2642
3224
|
}
|
|
2643
3225
|
async function handleFileSelect(file, container, fieldName, state, deps = null, instance, allowedExtensions = [], maxSizeMB = Infinity) {
|
|
2644
3226
|
if (!isFileExtensionAllowed(file.name, allowedExtensions)) {
|
|
@@ -2657,24 +3239,18 @@ async function handleFileSelect(file, container, fieldName, state, deps = null,
|
|
|
2657
3239
|
return;
|
|
2658
3240
|
}
|
|
2659
3241
|
clearFileError(container);
|
|
3242
|
+
ensureFileStyles();
|
|
3243
|
+
container.innerHTML = `
|
|
3244
|
+
<div style="display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;gap:6px;padding:6px;">
|
|
3245
|
+
<div class="fb-spinner"></div>
|
|
3246
|
+
<div style="font-size:11px;color:var(--fb-text-secondary-color,#6b7280);text-align:center;">${escapeHtml(t("uploadingFile", state))}</div>
|
|
3247
|
+
</div>`;
|
|
2660
3248
|
let rid;
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
}
|
|
2667
|
-
} catch (error) {
|
|
2668
|
-
const err = error instanceof Error ? error : new Error(String(error));
|
|
2669
|
-
if (state.config.onUploadError) {
|
|
2670
|
-
state.config.onUploadError(err, file);
|
|
2671
|
-
}
|
|
2672
|
-
throw new Error(`File upload failed: ${err.message}`);
|
|
2673
|
-
}
|
|
2674
|
-
} else {
|
|
2675
|
-
throw new Error(
|
|
2676
|
-
"No upload handler configured. Set uploadHandler via FormBuilder.setUploadHandler()"
|
|
2677
|
-
);
|
|
3249
|
+
try {
|
|
3250
|
+
rid = await uploadSingleFile(file, state);
|
|
3251
|
+
} catch (error) {
|
|
3252
|
+
setEmptyFileContainer(container, state);
|
|
3253
|
+
throw error;
|
|
2678
3254
|
}
|
|
2679
3255
|
state.resourceIndex.set(rid, {
|
|
2680
3256
|
name: file.name,
|
|
@@ -2682,7 +3258,6 @@ async function handleFileSelect(file, container, fieldName, state, deps = null,
|
|
|
2682
3258
|
size: file.size,
|
|
2683
3259
|
uploadedAt: /* @__PURE__ */ new Date(),
|
|
2684
3260
|
file
|
|
2685
|
-
// Store the file object for local preview
|
|
2686
3261
|
});
|
|
2687
3262
|
let hiddenInput = container.parentElement?.querySelector(
|
|
2688
3263
|
'input[type="hidden"]'
|
|
@@ -2694,140 +3269,132 @@ async function handleFileSelect(file, container, fieldName, state, deps = null,
|
|
|
2694
3269
|
container.parentElement?.appendChild(hiddenInput);
|
|
2695
3270
|
}
|
|
2696
3271
|
hiddenInput.value = rid;
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
3272
|
+
const isVideo = file.type.startsWith("video/");
|
|
3273
|
+
if (!isVideo && deps) {
|
|
3274
|
+
renderSingleFileEditTile(container, rid, state, deps).catch(console.error);
|
|
3275
|
+
} else {
|
|
3276
|
+
renderFilePreview(container, rid, state, {
|
|
3277
|
+
fileName: file.name,
|
|
3278
|
+
isReadonly: false,
|
|
3279
|
+
deps
|
|
3280
|
+
}).catch(console.error);
|
|
3281
|
+
}
|
|
2702
3282
|
if (instance && !state.config.readonly) {
|
|
2703
3283
|
instance.triggerOnChange(fieldName, rid);
|
|
2704
3284
|
}
|
|
2705
3285
|
}
|
|
2706
|
-
function
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
const deleteBtn = document.createElement("button");
|
|
2731
|
-
deleteBtn.className = "bg-red-600 text-white px-3 py-1 rounded text-sm hover:bg-red-700 transition-colors";
|
|
2732
|
-
deleteBtn.textContent = t("removeElement", state);
|
|
2733
|
-
deleteBtn.onclick = (e) => {
|
|
2734
|
-
e.stopPropagation();
|
|
2735
|
-
onDelete();
|
|
2736
|
-
};
|
|
2737
|
-
overlay.appendChild(deleteBtn);
|
|
2738
|
-
container.appendChild(overlay);
|
|
2739
|
-
}
|
|
2740
|
-
async function uploadSingleFile(file, state) {
|
|
2741
|
-
if (state.config.uploadFile) {
|
|
2742
|
-
try {
|
|
2743
|
-
const rid = await state.config.uploadFile(file);
|
|
2744
|
-
if (typeof rid !== "string") {
|
|
2745
|
-
throw new Error("Upload handler must return a string resource ID");
|
|
2746
|
-
}
|
|
2747
|
-
return rid;
|
|
2748
|
-
} catch (error) {
|
|
2749
|
-
const err = error instanceof Error ? error : new Error(String(error));
|
|
2750
|
-
if (state.config.onUploadError) {
|
|
2751
|
-
state.config.onUploadError(err, file);
|
|
2752
|
-
}
|
|
2753
|
-
throw new Error(`File upload failed: ${err.message}`);
|
|
2754
|
-
}
|
|
2755
|
-
} else {
|
|
2756
|
-
throw new Error(
|
|
2757
|
-
"No upload handler configured. Set uploadHandler via FormBuilder.setUploadHandler()"
|
|
3286
|
+
function filterAndSlice(allFiles, currentCount, constraints, state) {
|
|
3287
|
+
const rejectedByExt = allFiles.filter(
|
|
3288
|
+
(f) => !isFileExtensionAllowed(f.name, constraints.allowedExtensions)
|
|
3289
|
+
);
|
|
3290
|
+
const afterExt = allFiles.filter(
|
|
3291
|
+
(f) => isFileExtensionAllowed(f.name, constraints.allowedExtensions)
|
|
3292
|
+
);
|
|
3293
|
+
const rejectedBySize = afterExt.filter(
|
|
3294
|
+
(f) => !isFileSizeAllowed(f, constraints.maxSize)
|
|
3295
|
+
);
|
|
3296
|
+
const valid = afterExt.filter((f) => isFileSizeAllowed(f, constraints.maxSize));
|
|
3297
|
+
const remaining = constraints.maxCount === Infinity ? valid.length : Math.max(0, constraints.maxCount - currentCount);
|
|
3298
|
+
const accepted = valid.slice(0, remaining);
|
|
3299
|
+
const skippedByCount = valid.length - accepted.length;
|
|
3300
|
+
const errorParts = [];
|
|
3301
|
+
if (rejectedByExt.length > 0) {
|
|
3302
|
+
const formats = constraints.allowedExtensions.join(", ");
|
|
3303
|
+
const names = rejectedByExt.map((f) => f.name).join(", ");
|
|
3304
|
+
errorParts.push(t("invalidFileExtension", state, { name: names, formats }));
|
|
3305
|
+
}
|
|
3306
|
+
if (rejectedBySize.length > 0) {
|
|
3307
|
+
const names = rejectedBySize.map((f) => f.name).join(", ");
|
|
3308
|
+
errorParts.push(
|
|
3309
|
+
t("fileTooLarge", state, { name: names, maxSize: constraints.maxSize })
|
|
2758
3310
|
);
|
|
2759
3311
|
}
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
fileUrl = await state.config.getThumbnail(resourceId);
|
|
2768
|
-
}
|
|
2769
|
-
if (fileUrl) {
|
|
2770
|
-
const finalUrl = fileUrl.startsWith("http") ? fileUrl : new URL(fileUrl, window.location.href).href;
|
|
2771
|
-
const response = await fetch(finalUrl);
|
|
2772
|
-
if (!response.ok) {
|
|
2773
|
-
throw new Error(`HTTP error! status: ${response.status}`);
|
|
2774
|
-
}
|
|
2775
|
-
const blob = await response.blob();
|
|
2776
|
-
downloadBlob(blob, fileName);
|
|
2777
|
-
} else {
|
|
2778
|
-
throw new Error("No download URL available for resource");
|
|
2779
|
-
}
|
|
2780
|
-
} catch (error) {
|
|
2781
|
-
const err = error instanceof Error ? error : new Error(String(error));
|
|
2782
|
-
if (state.config.onDownloadError) {
|
|
2783
|
-
state.config.onDownloadError(err, resourceId, fileName);
|
|
2784
|
-
}
|
|
2785
|
-
console.error(`File download failed for ${fileName}:`, err);
|
|
2786
|
-
throw err;
|
|
2787
|
-
}
|
|
2788
|
-
}
|
|
2789
|
-
function downloadBlob(blob, fileName) {
|
|
2790
|
-
try {
|
|
2791
|
-
const blobUrl = URL.createObjectURL(blob);
|
|
2792
|
-
const link = document.createElement("a");
|
|
2793
|
-
link.href = blobUrl;
|
|
2794
|
-
link.download = fileName;
|
|
2795
|
-
link.style.display = "none";
|
|
2796
|
-
document.body.appendChild(link);
|
|
2797
|
-
link.click();
|
|
2798
|
-
document.body.removeChild(link);
|
|
2799
|
-
setTimeout(() => {
|
|
2800
|
-
URL.revokeObjectURL(blobUrl);
|
|
2801
|
-
}, 100);
|
|
2802
|
-
} catch (error) {
|
|
2803
|
-
throw new Error(`Blob download failed: ${error.message}`);
|
|
3312
|
+
if (skippedByCount > 0) {
|
|
3313
|
+
errorParts.push(
|
|
3314
|
+
t("filesLimitExceeded", state, {
|
|
3315
|
+
skipped: skippedByCount,
|
|
3316
|
+
max: constraints.maxCount
|
|
3317
|
+
})
|
|
3318
|
+
);
|
|
2804
3319
|
}
|
|
2805
|
-
}
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
}
|
|
3320
|
+
return { accepted, errorMessage: errorParts.join(" \u2022 ") };
|
|
3321
|
+
}
|
|
3322
|
+
async function uploadBatch(accepted, resourceIds, listEl, state) {
|
|
3323
|
+
await Promise.all(
|
|
3324
|
+
accepted.map(async (file) => {
|
|
3325
|
+
const placeholder = createUploadingTile(file.name, state);
|
|
3326
|
+
if (listEl) {
|
|
3327
|
+
const tilesWrap = ensureTilesWrap(listEl);
|
|
3328
|
+
const addTile = tilesWrap.querySelector(".fb-tile-add");
|
|
3329
|
+
if (addTile) {
|
|
3330
|
+
tilesWrap.insertBefore(placeholder, addTile);
|
|
3331
|
+
} else {
|
|
3332
|
+
tilesWrap.appendChild(placeholder);
|
|
2819
3333
|
}
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
3334
|
+
}
|
|
3335
|
+
try {
|
|
3336
|
+
const rid = await uploadSingleFile(file, state);
|
|
3337
|
+
state.resourceIndex.set(rid, {
|
|
3338
|
+
name: file.name,
|
|
3339
|
+
type: file.type,
|
|
3340
|
+
size: file.size,
|
|
2824
3341
|
uploadedAt: /* @__PURE__ */ new Date(),
|
|
2825
3342
|
file: void 0
|
|
2826
3343
|
});
|
|
3344
|
+
resourceIds.push(rid);
|
|
3345
|
+
} finally {
|
|
3346
|
+
placeholder.remove();
|
|
2827
3347
|
}
|
|
2828
|
-
})
|
|
2829
|
-
|
|
3348
|
+
})
|
|
3349
|
+
);
|
|
3350
|
+
}
|
|
3351
|
+
function setupFilesDropHandler(filesContainer, resourceIds, state, updateCallback, constraints, pathKey, instance) {
|
|
3352
|
+
setupDragAndDrop(filesContainer, async (files) => {
|
|
3353
|
+
const { accepted, errorMessage } = filterAndSlice(
|
|
3354
|
+
Array.from(files),
|
|
3355
|
+
resourceIds.length,
|
|
3356
|
+
constraints,
|
|
3357
|
+
state
|
|
3358
|
+
);
|
|
3359
|
+
if (errorMessage) {
|
|
3360
|
+
showFileError(filesContainer, errorMessage);
|
|
3361
|
+
} else {
|
|
3362
|
+
clearFileError(filesContainer);
|
|
3363
|
+
}
|
|
3364
|
+
const list = filesContainer.querySelector(".files-list") ?? filesContainer;
|
|
3365
|
+
await uploadBatch(accepted, resourceIds, list, state);
|
|
3366
|
+
updateCallback();
|
|
3367
|
+
if (instance && pathKey && !state.config.readonly) {
|
|
3368
|
+
instance.triggerOnChange(pathKey, resourceIds);
|
|
3369
|
+
}
|
|
3370
|
+
});
|
|
3371
|
+
}
|
|
3372
|
+
function setupFilesPickerHandler(filesPicker, resourceIds, state, updateCallback, constraints, pathKey, instance) {
|
|
3373
|
+
filesPicker.onchange = async () => {
|
|
3374
|
+
if (!filesPicker.files) return;
|
|
3375
|
+
const wrapperEl = filesPicker.closest(".space-y-2") || filesPicker.parentElement;
|
|
3376
|
+
const { accepted, errorMessage } = filterAndSlice(
|
|
3377
|
+
Array.from(filesPicker.files),
|
|
3378
|
+
resourceIds.length,
|
|
3379
|
+
constraints,
|
|
3380
|
+
state
|
|
3381
|
+
);
|
|
3382
|
+
if (errorMessage && wrapperEl) {
|
|
3383
|
+
showFileError(wrapperEl, errorMessage);
|
|
3384
|
+
} else if (wrapperEl) {
|
|
3385
|
+
clearFileError(wrapperEl);
|
|
3386
|
+
}
|
|
3387
|
+
const listEl = wrapperEl?.querySelector(".files-list");
|
|
3388
|
+
await uploadBatch(accepted, resourceIds, listEl ?? null, state);
|
|
3389
|
+
updateCallback();
|
|
3390
|
+
filesPicker.value = "";
|
|
3391
|
+
if (instance && pathKey && !state.config.readonly) {
|
|
3392
|
+
instance.triggerOnChange(pathKey, resourceIds);
|
|
3393
|
+
}
|
|
3394
|
+
};
|
|
2830
3395
|
}
|
|
3396
|
+
|
|
3397
|
+
// src/components/file/render-edit.ts
|
|
2831
3398
|
function handleInitialFileData(initial, fileContainer, pathKey, fileWrapper, state, deps) {
|
|
2832
3399
|
if (!state.resourceIndex.has(initial)) {
|
|
2833
3400
|
const filename = initial.split("/").pop() || "file";
|
|
@@ -2848,493 +3415,553 @@ function handleInitialFileData(initial, fileContainer, pathKey, fileWrapper, sta
|
|
|
2848
3415
|
file: void 0
|
|
2849
3416
|
});
|
|
2850
3417
|
}
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
3418
|
+
const meta = state.resourceIndex.get(initial);
|
|
3419
|
+
const isVideo = meta?.type?.startsWith("video/");
|
|
3420
|
+
if (isVideo) {
|
|
3421
|
+
renderFilePreview(fileContainer, initial, state, {
|
|
3422
|
+
fileName: initial,
|
|
3423
|
+
isReadonly: false,
|
|
3424
|
+
deps
|
|
3425
|
+
}).catch(console.error);
|
|
3426
|
+
} else {
|
|
3427
|
+
renderSingleFileEditTile(fileContainer, initial, state, deps).catch(console.error);
|
|
3428
|
+
}
|
|
2856
3429
|
const hiddenInput = document.createElement("input");
|
|
2857
3430
|
hiddenInput.type = "hidden";
|
|
2858
3431
|
hiddenInput.name = pathKey;
|
|
2859
3432
|
hiddenInput.value = initial;
|
|
2860
3433
|
fileWrapper.appendChild(hiddenInput);
|
|
2861
3434
|
}
|
|
2862
|
-
function
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
);
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
);
|
|
2877
|
-
const remaining = constraints.maxCount === Infinity ? validFiles.length : Math.max(0, constraints.maxCount - initialFiles.length);
|
|
2878
|
-
const arr = validFiles.slice(0, remaining);
|
|
2879
|
-
const skippedByCount = validFiles.length - arr.length;
|
|
2880
|
-
const errorParts = [];
|
|
2881
|
-
if (rejectedByExtension.length > 0) {
|
|
2882
|
-
const formats = constraints.allowedExtensions.join(", ");
|
|
2883
|
-
const names = rejectedByExtension.map((f) => f.name).join(", ");
|
|
2884
|
-
errorParts.push(
|
|
2885
|
-
t("invalidFileExtension", state, { name: names, formats })
|
|
2886
|
-
);
|
|
2887
|
-
}
|
|
2888
|
-
if (rejectedBySize.length > 0) {
|
|
2889
|
-
const names = rejectedBySize.map((f) => f.name).join(", ");
|
|
2890
|
-
errorParts.push(
|
|
2891
|
-
t("fileTooLarge", state, { name: names, maxSize: constraints.maxSize })
|
|
2892
|
-
);
|
|
2893
|
-
}
|
|
2894
|
-
if (skippedByCount > 0) {
|
|
2895
|
-
errorParts.push(
|
|
2896
|
-
t("filesLimitExceeded", state, {
|
|
2897
|
-
skipped: skippedByCount,
|
|
2898
|
-
max: constraints.maxCount
|
|
2899
|
-
})
|
|
2900
|
-
);
|
|
2901
|
-
}
|
|
2902
|
-
if (errorParts.length > 0) {
|
|
2903
|
-
showFileError(filesContainer, errorParts.join(" \u2022 "));
|
|
2904
|
-
} else {
|
|
2905
|
-
clearFileError(filesContainer);
|
|
2906
|
-
}
|
|
2907
|
-
for (const file of arr) {
|
|
2908
|
-
const rid = await uploadSingleFile(file, state);
|
|
2909
|
-
state.resourceIndex.set(rid, {
|
|
2910
|
-
name: file.name,
|
|
2911
|
-
type: file.type,
|
|
2912
|
-
size: file.size,
|
|
2913
|
-
uploadedAt: /* @__PURE__ */ new Date(),
|
|
2914
|
-
file: void 0
|
|
2915
|
-
});
|
|
2916
|
-
initialFiles.push(rid);
|
|
2917
|
-
}
|
|
2918
|
-
updateCallback();
|
|
2919
|
-
if (instance && pathKey && !state.config.readonly) {
|
|
2920
|
-
instance.triggerOnChange(pathKey, initialFiles);
|
|
2921
|
-
}
|
|
2922
|
-
});
|
|
2923
|
-
}
|
|
2924
|
-
function setupFilesPickerHandler(filesPicker, initialFiles, state, updateCallback, constraints, pathKey, instance) {
|
|
2925
|
-
filesPicker.onchange = async () => {
|
|
2926
|
-
if (filesPicker.files) {
|
|
2927
|
-
const allFiles = Array.from(filesPicker.files);
|
|
2928
|
-
const rejectedByExtension = allFiles.filter(
|
|
2929
|
-
(f) => !isFileExtensionAllowed(f.name, constraints.allowedExtensions)
|
|
2930
|
-
);
|
|
2931
|
-
const afterExtension = allFiles.filter(
|
|
2932
|
-
(f) => isFileExtensionAllowed(f.name, constraints.allowedExtensions)
|
|
2933
|
-
);
|
|
2934
|
-
const rejectedBySize = afterExtension.filter(
|
|
2935
|
-
(f) => !isFileSizeAllowed(f, constraints.maxSize)
|
|
2936
|
-
);
|
|
2937
|
-
const validFiles = afterExtension.filter(
|
|
2938
|
-
(f) => isFileSizeAllowed(f, constraints.maxSize)
|
|
2939
|
-
);
|
|
2940
|
-
const remaining = constraints.maxCount === Infinity ? validFiles.length : Math.max(0, constraints.maxCount - initialFiles.length);
|
|
2941
|
-
const arr = validFiles.slice(0, remaining);
|
|
2942
|
-
const skippedByCount = validFiles.length - arr.length;
|
|
2943
|
-
const errorParts = [];
|
|
2944
|
-
if (rejectedByExtension.length > 0) {
|
|
2945
|
-
const formats = constraints.allowedExtensions.join(", ");
|
|
2946
|
-
const names = rejectedByExtension.map((f) => f.name).join(", ");
|
|
2947
|
-
errorParts.push(
|
|
2948
|
-
t("invalidFileExtension", state, { name: names, formats })
|
|
2949
|
-
);
|
|
2950
|
-
}
|
|
2951
|
-
if (rejectedBySize.length > 0) {
|
|
2952
|
-
const names = rejectedBySize.map((f) => f.name).join(", ");
|
|
2953
|
-
errorParts.push(
|
|
2954
|
-
t("fileTooLarge", state, {
|
|
2955
|
-
name: names,
|
|
2956
|
-
maxSize: constraints.maxSize
|
|
2957
|
-
})
|
|
2958
|
-
);
|
|
2959
|
-
}
|
|
2960
|
-
if (skippedByCount > 0) {
|
|
2961
|
-
errorParts.push(
|
|
2962
|
-
t("filesLimitExceeded", state, {
|
|
2963
|
-
skipped: skippedByCount,
|
|
2964
|
-
max: constraints.maxCount
|
|
2965
|
-
})
|
|
2966
|
-
);
|
|
2967
|
-
}
|
|
2968
|
-
const wrapper = filesPicker.closest(".space-y-2") || filesPicker.parentElement;
|
|
2969
|
-
if (errorParts.length > 0 && wrapper) {
|
|
2970
|
-
showFileError(wrapper, errorParts.join(" \u2022 "));
|
|
2971
|
-
} else if (wrapper) {
|
|
2972
|
-
clearFileError(wrapper);
|
|
2973
|
-
}
|
|
2974
|
-
for (const file of arr) {
|
|
2975
|
-
const rid = await uploadSingleFile(file, state);
|
|
2976
|
-
state.resourceIndex.set(rid, {
|
|
2977
|
-
name: file.name,
|
|
2978
|
-
type: file.type,
|
|
2979
|
-
size: file.size,
|
|
2980
|
-
uploadedAt: /* @__PURE__ */ new Date(),
|
|
2981
|
-
file: void 0
|
|
2982
|
-
});
|
|
2983
|
-
initialFiles.push(rid);
|
|
2984
|
-
}
|
|
2985
|
-
}
|
|
2986
|
-
updateCallback();
|
|
2987
|
-
filesPicker.value = "";
|
|
2988
|
-
if (instance && pathKey && !state.config.readonly) {
|
|
2989
|
-
instance.triggerOnChange(pathKey, initialFiles);
|
|
2990
|
-
}
|
|
3435
|
+
function renderResourcePills(container, rids, state, onRemove, hint, countInfo, maxCount, isReadonly = false) {
|
|
3436
|
+
ensureFileStyles();
|
|
3437
|
+
const wrapper = container.closest("[data-files-wrapper]");
|
|
3438
|
+
if (wrapper) {
|
|
3439
|
+
wrapper.dataset.resourceIds = JSON.stringify(rids ?? []);
|
|
3440
|
+
}
|
|
3441
|
+
while (container.firstChild) container.removeChild(container.firstChild);
|
|
3442
|
+
const ridList = rids ?? [];
|
|
3443
|
+
const atMax = maxCount !== void 0 && ridList.length >= maxCount;
|
|
3444
|
+
const buildSubHint = () => {
|
|
3445
|
+
const parts = [];
|
|
3446
|
+
if (hint) parts.push(hint);
|
|
3447
|
+
if (countInfo) parts.push(countInfo);
|
|
3448
|
+
return parts.join(" \u2022 ");
|
|
2991
3449
|
};
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
if (
|
|
2998
|
-
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
const emptyState = document.createElement("div");
|
|
3003
|
-
emptyState.className = "aspect-video bg-gray-100 rounded-lg flex items-center justify-center text-gray-500";
|
|
3004
|
-
emptyState.innerHTML = `<div class="text-center">${escapeHtml(t("previewUnavailable", state))}</div>`;
|
|
3005
|
-
wrapper.appendChild(emptyState);
|
|
3006
|
-
});
|
|
3007
|
-
} else {
|
|
3008
|
-
const emptyState = document.createElement("div");
|
|
3009
|
-
emptyState.className = "aspect-video bg-gray-100 rounded-lg flex items-center justify-center text-gray-500";
|
|
3010
|
-
emptyState.innerHTML = `<div class="text-center">${escapeHtml(t("noFileSelected", state))}</div>`;
|
|
3011
|
-
wrapper.appendChild(emptyState);
|
|
3012
|
-
}
|
|
3013
|
-
} else {
|
|
3014
|
-
const fileWrapper = document.createElement("div");
|
|
3015
|
-
fileWrapper.className = "space-y-2";
|
|
3016
|
-
const picker = document.createElement("input");
|
|
3017
|
-
picker.type = "file";
|
|
3018
|
-
picker.name = pathKey;
|
|
3019
|
-
picker.style.display = "none";
|
|
3020
|
-
if (element.accept) {
|
|
3021
|
-
picker.accept = typeof element.accept === "string" ? element.accept : element.accept.extensions?.map((ext) => `.${ext}`).join(",") || "";
|
|
3022
|
-
}
|
|
3023
|
-
const fileContainer = document.createElement("div");
|
|
3024
|
-
fileContainer.className = "file-preview-container w-full aspect-square max-w-xs bg-gray-100 rounded-lg overflow-hidden relative group cursor-pointer";
|
|
3025
|
-
const initial = ctx.prefill[element.key];
|
|
3026
|
-
const allowedExts = getAllowedExtensions(element.accept);
|
|
3027
|
-
const maxSizeMB = element.maxSize ?? Infinity;
|
|
3028
|
-
const fileUploadHandler = () => picker.click();
|
|
3029
|
-
const dragHandler = (files) => {
|
|
3030
|
-
if (files.length > 0) {
|
|
3031
|
-
const deps = { picker, fileUploadHandler, dragHandler };
|
|
3032
|
-
handleFileSelect(
|
|
3033
|
-
files[0],
|
|
3034
|
-
fileContainer,
|
|
3035
|
-
pathKey,
|
|
3036
|
-
state,
|
|
3037
|
-
deps,
|
|
3038
|
-
ctx.instance,
|
|
3039
|
-
allowedExts,
|
|
3040
|
-
maxSizeMB
|
|
3041
|
-
);
|
|
3042
|
-
}
|
|
3043
|
-
};
|
|
3044
|
-
if (initial) {
|
|
3045
|
-
handleInitialFileData(
|
|
3046
|
-
initial,
|
|
3047
|
-
fileContainer,
|
|
3048
|
-
pathKey,
|
|
3049
|
-
fileWrapper,
|
|
3050
|
-
state,
|
|
3051
|
-
{
|
|
3052
|
-
picker,
|
|
3053
|
-
fileUploadHandler,
|
|
3054
|
-
dragHandler
|
|
3055
|
-
}
|
|
3056
|
-
);
|
|
3450
|
+
const openPicker = () => {
|
|
3451
|
+
const picker = findFilePicker(container);
|
|
3452
|
+
if (picker) picker.click();
|
|
3453
|
+
};
|
|
3454
|
+
if (ridList.length === 0) {
|
|
3455
|
+
if (isReadonly) {
|
|
3456
|
+
const emptyEl = document.createElement("div");
|
|
3457
|
+
emptyEl.className = "fb-tile-empty-text";
|
|
3458
|
+
emptyEl.textContent = t("noFilesSelected", state);
|
|
3459
|
+
container.appendChild(emptyEl);
|
|
3057
3460
|
} else {
|
|
3058
|
-
const
|
|
3059
|
-
|
|
3461
|
+
const dropzone = document.createElement("div");
|
|
3462
|
+
dropzone.className = "fb-file-dropzone";
|
|
3463
|
+
const subHint2 = buildSubHint();
|
|
3464
|
+
dropzone.innerHTML = `
|
|
3465
|
+
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor" style="flex-shrink:0;color:var(--fb-file-upload-text-color,#9ca3af);">
|
|
3466
|
+
<path d="M19.35 10.04A7.49 7.49 0 0012 4C9.11 4 6.6 5.64 5.35 8.04A5.994 5.994 0 000 14c0 3.31 2.69 6 6 6h13c2.76 0 5-2.24 5-5 0-2.64-2.05-4.78-4.65-4.96zM14 13v4h-4v-4H7l5-5 5 5h-3z"/>
|
|
3467
|
+
</svg>
|
|
3468
|
+
<div class="fb-dropzone-primary-text">${escapeHtml(t("clickDragTextMultiple", state))}</div>
|
|
3469
|
+
${subHint2 ? `<div class="fb-dropzone-hint-text">${escapeHtml(subHint2)}</div>` : ""}
|
|
3470
|
+
`;
|
|
3471
|
+
dropzone.onclick = openPicker;
|
|
3472
|
+
container.appendChild(dropzone);
|
|
3060
3473
|
}
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
3474
|
+
return;
|
|
3475
|
+
}
|
|
3476
|
+
const tilesWrap = document.createElement("div");
|
|
3477
|
+
tilesWrap.className = "fb-tiles-wrap";
|
|
3478
|
+
tilesWrap.style.cssText = "display:flex;flex-wrap:wrap;gap:6px;align-items:flex-start;";
|
|
3479
|
+
for (const rid of ridList) {
|
|
3480
|
+
const meta = state.resourceIndex.get(rid);
|
|
3481
|
+
const tile = createFileTile();
|
|
3482
|
+
tile.classList.add("fb-tile-resource", "resource-pill");
|
|
3483
|
+
tile.dataset.resourceId = rid;
|
|
3484
|
+
const actionsEl = createTileActions({
|
|
3485
|
+
canRemove: !isReadonly && onRemove !== null,
|
|
3486
|
+
removeHandler: onRemove ? () => onRemove(rid) : null,
|
|
3487
|
+
state,
|
|
3488
|
+
resourceId: rid,
|
|
3489
|
+
fileName: meta?.name ?? ""
|
|
3490
|
+
});
|
|
3491
|
+
fillTileContent(tile, rid, meta, state, actionsEl).catch((err) => {
|
|
3492
|
+
console.error("Failed to render tile:", err);
|
|
3493
|
+
});
|
|
3494
|
+
tilesWrap.appendChild(tile);
|
|
3495
|
+
}
|
|
3496
|
+
if (!isReadonly && !atMax) {
|
|
3497
|
+
const addTile = document.createElement("div");
|
|
3498
|
+
addTile.className = "fb-tile fb-tile-add";
|
|
3499
|
+
addTile.innerHTML = "+";
|
|
3500
|
+
addTile.onclick = openPicker;
|
|
3501
|
+
tilesWrap.appendChild(addTile);
|
|
3502
|
+
} else if (!isReadonly && atMax) {
|
|
3503
|
+
const chip = document.createElement("div");
|
|
3504
|
+
chip.className = "fb-tile-counter";
|
|
3505
|
+
chip.textContent = t("filesCounter", state, {
|
|
3506
|
+
count: ridList.length,
|
|
3507
|
+
max: maxCount
|
|
3508
|
+
});
|
|
3509
|
+
tilesWrap.appendChild(chip);
|
|
3510
|
+
}
|
|
3511
|
+
container.appendChild(tilesWrap);
|
|
3512
|
+
const subHint = buildSubHint();
|
|
3513
|
+
if (subHint) {
|
|
3514
|
+
const hintEl = document.createElement("div");
|
|
3515
|
+
hintEl.className = "fb-tile-hint";
|
|
3516
|
+
hintEl.textContent = subHint;
|
|
3517
|
+
container.appendChild(hintEl);
|
|
3518
|
+
}
|
|
3519
|
+
}
|
|
3520
|
+
function renderFileElementEdit(element, ctx, wrapper, pathKey) {
|
|
3521
|
+
const state = ctx.state;
|
|
3522
|
+
const fileWrapper = document.createElement("div");
|
|
3523
|
+
fileWrapper.className = "space-y-2";
|
|
3524
|
+
const picker = document.createElement("input");
|
|
3525
|
+
picker.type = "file";
|
|
3526
|
+
picker.name = pathKey;
|
|
3527
|
+
picker.style.display = "none";
|
|
3528
|
+
if (element.accept) {
|
|
3529
|
+
picker.accept = typeof element.accept === "string" ? element.accept : element.accept.extensions?.map((ext) => `.${ext}`).join(",") || "";
|
|
3530
|
+
}
|
|
3531
|
+
const fileContainer = document.createElement("div");
|
|
3532
|
+
fileContainer.className = "file-preview-container";
|
|
3533
|
+
const initial = ctx.prefill[element.key];
|
|
3534
|
+
const allowedExts = getAllowedExtensions(element.accept);
|
|
3535
|
+
const maxSizeMB = element.maxSize ?? Infinity;
|
|
3536
|
+
const handlers = {
|
|
3537
|
+
fileUploadHandler() {
|
|
3538
|
+
picker.click();
|
|
3539
|
+
},
|
|
3540
|
+
dragHandler(files) {
|
|
3541
|
+
if (files.length > 0) {
|
|
3066
3542
|
handleFileSelect(
|
|
3067
|
-
|
|
3543
|
+
files[0],
|
|
3068
3544
|
fileContainer,
|
|
3069
3545
|
pathKey,
|
|
3070
3546
|
state,
|
|
3071
|
-
|
|
3547
|
+
buildDeps(),
|
|
3072
3548
|
ctx.instance,
|
|
3073
3549
|
allowedExts,
|
|
3074
3550
|
maxSizeMB
|
|
3075
3551
|
);
|
|
3076
3552
|
}
|
|
3077
|
-
}
|
|
3078
|
-
|
|
3079
|
-
|
|
3080
|
-
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
|
-
|
|
3084
|
-
|
|
3085
|
-
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
}
|
|
3098
|
-
|
|
3553
|
+
},
|
|
3554
|
+
setupDrop(container) {
|
|
3555
|
+
setupDragAndDrop(container, handlers.dragHandler);
|
|
3556
|
+
},
|
|
3557
|
+
restoreDropzone() {
|
|
3558
|
+
const hint = makeFieldHint(element, state);
|
|
3559
|
+
fileContainer.className = "file-preview-container w-full max-w-md bg-gray-100 rounded-lg overflow-hidden relative group cursor-pointer";
|
|
3560
|
+
fileContainer.style.height = "128px";
|
|
3561
|
+
setEmptyFileContainer(fileContainer, state, hint);
|
|
3562
|
+
fileContainer.onclick = handlers.fileUploadHandler;
|
|
3563
|
+
setupDragAndDrop(fileContainer, handlers.dragHandler);
|
|
3564
|
+
},
|
|
3565
|
+
onRemove() {
|
|
3566
|
+
const hiddenInput = fileWrapper.querySelector('input[type="hidden"]');
|
|
3567
|
+
const currentRid = hiddenInput?.value;
|
|
3568
|
+
if (currentRid) {
|
|
3569
|
+
releaseLocalFileUrl(state.resourceIndex.get(currentRid)?.file);
|
|
3570
|
+
}
|
|
3571
|
+
if (hiddenInput) hiddenInput.value = "";
|
|
3572
|
+
handlers.restoreDropzone();
|
|
3573
|
+
}
|
|
3574
|
+
};
|
|
3575
|
+
const buildDeps = () => ({
|
|
3576
|
+
picker,
|
|
3577
|
+
fileUploadHandler: handlers.fileUploadHandler,
|
|
3578
|
+
dragHandler: handlers.dragHandler,
|
|
3579
|
+
setupDrop: handlers.setupDrop,
|
|
3580
|
+
onRemove: handlers.onRemove
|
|
3581
|
+
});
|
|
3582
|
+
if (initial) {
|
|
3583
|
+
handleInitialFileData(
|
|
3584
|
+
initial,
|
|
3585
|
+
fileContainer,
|
|
3586
|
+
pathKey,
|
|
3587
|
+
fileWrapper,
|
|
3588
|
+
state,
|
|
3589
|
+
buildDeps()
|
|
3590
|
+
);
|
|
3591
|
+
const prefillMeta = state.resourceIndex.get(initial);
|
|
3592
|
+
if (prefillMeta?.type?.startsWith("video/")) {
|
|
3593
|
+
fileContainer.onclick = handlers.fileUploadHandler;
|
|
3594
|
+
setupDragAndDrop(fileContainer, handlers.dragHandler);
|
|
3099
3595
|
}
|
|
3100
|
-
wrapper.appendChild(resultsWrapper);
|
|
3101
3596
|
} else {
|
|
3102
|
-
|
|
3103
|
-
|
|
3104
|
-
|
|
3105
|
-
|
|
3597
|
+
handlers.restoreDropzone();
|
|
3598
|
+
}
|
|
3599
|
+
picker.onchange = () => {
|
|
3600
|
+
if (picker.files && picker.files.length > 0) {
|
|
3601
|
+
handleFileSelect(
|
|
3602
|
+
picker.files[0],
|
|
3603
|
+
fileContainer,
|
|
3604
|
+
pathKey,
|
|
3106
3605
|
state,
|
|
3107
|
-
(
|
|
3108
|
-
|
|
3109
|
-
|
|
3110
|
-
|
|
3111
|
-
}
|
|
3112
|
-
updateFilesList2();
|
|
3113
|
-
},
|
|
3114
|
-
filesFieldHint
|
|
3606
|
+
buildDeps(),
|
|
3607
|
+
ctx.instance,
|
|
3608
|
+
allowedExts,
|
|
3609
|
+
maxSizeMB
|
|
3115
3610
|
);
|
|
3116
|
-
}
|
|
3117
|
-
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
|
|
3126
|
-
|
|
3127
|
-
|
|
3128
|
-
|
|
3129
|
-
|
|
3130
|
-
|
|
3131
|
-
|
|
3132
|
-
|
|
3133
|
-
|
|
3134
|
-
|
|
3135
|
-
|
|
3136
|
-
|
|
3137
|
-
|
|
3138
|
-
|
|
3139
|
-
|
|
3140
|
-
|
|
3141
|
-
|
|
3142
|
-
|
|
3143
|
-
|
|
3144
|
-
|
|
3145
|
-
|
|
3146
|
-
|
|
3147
|
-
|
|
3148
|
-
|
|
3149
|
-
|
|
3150
|
-
|
|
3611
|
+
}
|
|
3612
|
+
};
|
|
3613
|
+
fileWrapper.appendChild(fileContainer);
|
|
3614
|
+
fileWrapper.appendChild(picker);
|
|
3615
|
+
wrapper.appendChild(fileWrapper);
|
|
3616
|
+
}
|
|
3617
|
+
function renderFilesElementEdit(element, ctx, wrapper, pathKey) {
|
|
3618
|
+
const state = ctx.state;
|
|
3619
|
+
const filesWrapper = document.createElement("div");
|
|
3620
|
+
filesWrapper.className = "space-y-2";
|
|
3621
|
+
filesWrapper.dataset.filesWrapper = pathKey;
|
|
3622
|
+
const filesPicker = document.createElement("input");
|
|
3623
|
+
filesPicker.type = "file";
|
|
3624
|
+
filesPicker.name = pathKey;
|
|
3625
|
+
filesPicker.multiple = true;
|
|
3626
|
+
filesPicker.style.display = "none";
|
|
3627
|
+
if (element.accept) {
|
|
3628
|
+
filesPicker.accept = typeof element.accept === "string" ? element.accept : element.accept.extensions?.map((ext) => `.${ext}`).join(",") || "";
|
|
3629
|
+
}
|
|
3630
|
+
const filesContainer = document.createElement("div");
|
|
3631
|
+
filesContainer.className = "files-list-wrapper";
|
|
3632
|
+
filesContainer.style.cssText = "border:2px dashed var(--fb-file-upload-border-color,#d1d5db);border-radius:var(--fb-border-radius,0.5rem);padding:8px;transition:border-color var(--fb-transition-duration,200ms),background var(--fb-transition-duration,200ms);";
|
|
3633
|
+
const list = document.createElement("div");
|
|
3634
|
+
list.className = "files-list";
|
|
3635
|
+
const initialFiles = ctx.prefill[element.key] || [];
|
|
3636
|
+
addPrefillFilesToIndex(initialFiles, state.resourceIndex);
|
|
3637
|
+
filesWrapper.dataset.resourceIds = JSON.stringify(initialFiles);
|
|
3638
|
+
const filesFieldHint = makeFieldHint(element, state);
|
|
3639
|
+
const filesConstraints = {
|
|
3640
|
+
maxCount: Infinity,
|
|
3641
|
+
allowedExtensions: getAllowedExtensions(element.accept),
|
|
3642
|
+
maxSize: element.maxSize ?? Infinity
|
|
3643
|
+
};
|
|
3644
|
+
filesContainer.appendChild(list);
|
|
3645
|
+
filesWrapper.appendChild(filesPicker);
|
|
3646
|
+
filesWrapper.appendChild(filesContainer);
|
|
3647
|
+
wrapper.appendChild(filesWrapper);
|
|
3648
|
+
function updateFilesList() {
|
|
3649
|
+
const currentlyReadonly = isElementReadonly(element, state);
|
|
3650
|
+
renderResourcePills(
|
|
3651
|
+
list,
|
|
3151
3652
|
initialFiles,
|
|
3152
3653
|
state,
|
|
3153
|
-
|
|
3154
|
-
|
|
3155
|
-
|
|
3156
|
-
|
|
3654
|
+
currentlyReadonly ? null : (ridToRemove) => {
|
|
3655
|
+
releaseLocalFileUrl(state.resourceIndex.get(ridToRemove)?.file);
|
|
3656
|
+
const index = initialFiles.indexOf(ridToRemove);
|
|
3657
|
+
if (index > -1) initialFiles.splice(index, 1);
|
|
3658
|
+
updateFilesList();
|
|
3659
|
+
},
|
|
3660
|
+
filesFieldHint,
|
|
3661
|
+
void 0,
|
|
3662
|
+
void 0,
|
|
3663
|
+
currentlyReadonly
|
|
3157
3664
|
);
|
|
3158
|
-
filesContainer.appendChild(list);
|
|
3159
|
-
filesWrapper.appendChild(filesContainer);
|
|
3160
|
-
filesWrapper.appendChild(filesPicker);
|
|
3161
|
-
wrapper.appendChild(filesWrapper);
|
|
3162
3665
|
}
|
|
3666
|
+
updateFilesList();
|
|
3667
|
+
setupFilesDropHandler(
|
|
3668
|
+
filesContainer,
|
|
3669
|
+
initialFiles,
|
|
3670
|
+
state,
|
|
3671
|
+
updateFilesList,
|
|
3672
|
+
filesConstraints,
|
|
3673
|
+
pathKey,
|
|
3674
|
+
ctx.instance
|
|
3675
|
+
);
|
|
3676
|
+
setupFilesPickerHandler(
|
|
3677
|
+
filesPicker,
|
|
3678
|
+
initialFiles,
|
|
3679
|
+
state,
|
|
3680
|
+
updateFilesList,
|
|
3681
|
+
filesConstraints,
|
|
3682
|
+
pathKey,
|
|
3683
|
+
ctx.instance
|
|
3684
|
+
);
|
|
3163
3685
|
}
|
|
3164
|
-
function
|
|
3686
|
+
function renderMultipleFileElementEdit(element, ctx, wrapper, pathKey) {
|
|
3165
3687
|
const state = ctx.state;
|
|
3166
3688
|
const minFiles = element.minCount ?? 0;
|
|
3167
3689
|
const maxFiles = element.maxCount ?? Infinity;
|
|
3168
|
-
|
|
3169
|
-
|
|
3170
|
-
|
|
3171
|
-
|
|
3172
|
-
|
|
3173
|
-
|
|
3174
|
-
|
|
3175
|
-
|
|
3176
|
-
|
|
3177
|
-
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
|
|
3182
|
-
|
|
3183
|
-
|
|
3184
|
-
|
|
3185
|
-
|
|
3186
|
-
|
|
3187
|
-
|
|
3188
|
-
|
|
3189
|
-
|
|
3190
|
-
|
|
3191
|
-
|
|
3192
|
-
|
|
3193
|
-
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
const
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
maxSize: element.maxSize ?? Infinity
|
|
3206
|
-
};
|
|
3207
|
-
const buildCountInfo = () => {
|
|
3208
|
-
const countText = initialFiles.length === 1 ? t("fileCountSingle", state, { count: initialFiles.length }) : t("fileCountPlural", state, { count: initialFiles.length });
|
|
3209
|
-
const minMaxText = minFiles > 0 || maxFiles < Infinity ? ` ${t("fileCountRange", state, { min: minFiles, max: maxFiles })}` : "";
|
|
3210
|
-
return countText + minMaxText;
|
|
3211
|
-
};
|
|
3212
|
-
const updateFilesDisplay = () => {
|
|
3213
|
-
renderResourcePills(
|
|
3214
|
-
filesContainer,
|
|
3215
|
-
initialFiles,
|
|
3216
|
-
state,
|
|
3217
|
-
(index) => {
|
|
3218
|
-
initialFiles.splice(initialFiles.indexOf(index), 1);
|
|
3219
|
-
updateFilesDisplay();
|
|
3220
|
-
},
|
|
3221
|
-
multipleFilesHint,
|
|
3222
|
-
buildCountInfo()
|
|
3223
|
-
);
|
|
3224
|
-
};
|
|
3225
|
-
setupFilesDropHandler(
|
|
3226
|
-
filesContainer,
|
|
3690
|
+
const filesWrapper = document.createElement("div");
|
|
3691
|
+
filesWrapper.className = "space-y-2";
|
|
3692
|
+
filesWrapper.dataset.filesWrapper = pathKey;
|
|
3693
|
+
const filesPicker = document.createElement("input");
|
|
3694
|
+
filesPicker.type = "file";
|
|
3695
|
+
filesPicker.name = pathKey;
|
|
3696
|
+
filesPicker.multiple = true;
|
|
3697
|
+
filesPicker.style.display = "none";
|
|
3698
|
+
if (element.accept) {
|
|
3699
|
+
filesPicker.accept = typeof element.accept === "string" ? element.accept : element.accept.extensions?.map((ext) => `.${ext}`).join(",") || "";
|
|
3700
|
+
}
|
|
3701
|
+
const filesContainer = document.createElement("div");
|
|
3702
|
+
filesContainer.className = "files-list-wrapper";
|
|
3703
|
+
filesContainer.style.cssText = "border:2px dashed var(--fb-file-upload-border-color,#d1d5db);border-radius:var(--fb-border-radius,0.5rem);padding:8px;transition:border-color var(--fb-transition-duration,200ms),background var(--fb-transition-duration,200ms);";
|
|
3704
|
+
const list = document.createElement("div");
|
|
3705
|
+
list.className = "files-list";
|
|
3706
|
+
filesWrapper.appendChild(filesPicker);
|
|
3707
|
+
filesWrapper.appendChild(filesContainer);
|
|
3708
|
+
filesContainer.appendChild(list);
|
|
3709
|
+
const initialFiles = Array.isArray(ctx.prefill[element.key]) ? [...ctx.prefill[element.key]] : [];
|
|
3710
|
+
addPrefillFilesToIndex(initialFiles, state.resourceIndex);
|
|
3711
|
+
filesWrapper.dataset.resourceIds = JSON.stringify(initialFiles);
|
|
3712
|
+
const multipleFilesHint = makeFieldHint(element, state);
|
|
3713
|
+
const multipleConstraints = {
|
|
3714
|
+
maxCount: maxFiles,
|
|
3715
|
+
allowedExtensions: getAllowedExtensions(element.accept),
|
|
3716
|
+
maxSize: element.maxSize ?? Infinity
|
|
3717
|
+
};
|
|
3718
|
+
const buildCountInfo = () => {
|
|
3719
|
+
const countText = initialFiles.length === 1 ? t("fileCountSingle", state, { count: initialFiles.length }) : t("fileCountPlural", state, { count: initialFiles.length });
|
|
3720
|
+
const minMaxText = minFiles > 0 || maxFiles < Infinity ? ` ${t("fileCountRange", state, { min: minFiles, max: maxFiles })}` : "";
|
|
3721
|
+
return countText + minMaxText;
|
|
3722
|
+
};
|
|
3723
|
+
const updateFilesDisplay = () => {
|
|
3724
|
+
const currentlyReadonly = isElementReadonly(element, state);
|
|
3725
|
+
renderResourcePills(
|
|
3726
|
+
list,
|
|
3227
3727
|
initialFiles,
|
|
3228
3728
|
state,
|
|
3229
|
-
|
|
3230
|
-
|
|
3231
|
-
|
|
3232
|
-
|
|
3729
|
+
currentlyReadonly ? null : (index) => {
|
|
3730
|
+
releaseLocalFileUrl(state.resourceIndex.get(index)?.file);
|
|
3731
|
+
initialFiles.splice(initialFiles.indexOf(index), 1);
|
|
3732
|
+
updateFilesDisplay();
|
|
3733
|
+
},
|
|
3734
|
+
multipleFilesHint,
|
|
3735
|
+
buildCountInfo(),
|
|
3736
|
+
maxFiles < Infinity ? maxFiles : void 0,
|
|
3737
|
+
currentlyReadonly
|
|
3233
3738
|
);
|
|
3234
|
-
|
|
3235
|
-
|
|
3236
|
-
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3739
|
+
};
|
|
3740
|
+
setupFilesDropHandler(
|
|
3741
|
+
filesContainer,
|
|
3742
|
+
initialFiles,
|
|
3743
|
+
state,
|
|
3744
|
+
updateFilesDisplay,
|
|
3745
|
+
multipleConstraints,
|
|
3746
|
+
pathKey,
|
|
3747
|
+
ctx.instance
|
|
3748
|
+
);
|
|
3749
|
+
setupFilesPickerHandler(
|
|
3750
|
+
filesPicker,
|
|
3751
|
+
initialFiles,
|
|
3752
|
+
state,
|
|
3753
|
+
updateFilesDisplay,
|
|
3754
|
+
multipleConstraints,
|
|
3755
|
+
pathKey,
|
|
3756
|
+
ctx.instance
|
|
3757
|
+
);
|
|
3758
|
+
updateFilesDisplay();
|
|
3759
|
+
wrapper.appendChild(filesWrapper);
|
|
3760
|
+
}
|
|
3761
|
+
|
|
3762
|
+
// src/components/file/validate.ts
|
|
3763
|
+
function readMultiFileResourceIds(scopeRoot, fullKey) {
|
|
3764
|
+
const wrapper = scopeRoot.querySelector(
|
|
3765
|
+
`[data-files-wrapper="${fullKey}"]`
|
|
3766
|
+
);
|
|
3767
|
+
if (!wrapper) return [];
|
|
3768
|
+
const encoded = wrapper.dataset.resourceIds;
|
|
3769
|
+
if (encoded === void 0) {
|
|
3770
|
+
throw new Error(
|
|
3771
|
+
`readMultiFileResourceIds: [data-files-wrapper="${fullKey}"] is missing data-resource-ids attribute. This is a render bug.`
|
|
3242
3772
|
);
|
|
3243
|
-
updateFilesDisplay();
|
|
3244
|
-
wrapper.appendChild(filesWrapper);
|
|
3245
3773
|
}
|
|
3774
|
+
const parsed = JSON.parse(encoded);
|
|
3775
|
+
if (!Array.isArray(parsed)) {
|
|
3776
|
+
throw new Error(
|
|
3777
|
+
`readMultiFileResourceIds: data-resource-ids on [data-files-wrapper="${fullKey}"] is not a JSON array. Got: ${encoded}`
|
|
3778
|
+
);
|
|
3779
|
+
}
|
|
3780
|
+
return parsed;
|
|
3246
3781
|
}
|
|
3247
|
-
function
|
|
3248
|
-
const
|
|
3249
|
-
const
|
|
3250
|
-
|
|
3251
|
-
|
|
3252
|
-
|
|
3253
|
-
|
|
3254
|
-
|
|
3255
|
-
|
|
3256
|
-
|
|
3257
|
-
|
|
3258
|
-
|
|
3259
|
-
|
|
3260
|
-
|
|
3261
|
-
|
|
3262
|
-
|
|
3263
|
-
|
|
3264
|
-
|
|
3265
|
-
|
|
3266
|
-
|
|
3267
|
-
|
|
3268
|
-
|
|
3269
|
-
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
const formats = allowedExtensions.join(", ");
|
|
3273
|
-
for (const rid of resourceIds) {
|
|
3274
|
-
const meta = state.resourceIndex.get(rid);
|
|
3275
|
-
const fileName = meta?.name ?? rid;
|
|
3276
|
-
if (!isFileExtensionAllowed(fileName, allowedExtensions)) {
|
|
3277
|
-
errors.push(
|
|
3278
|
-
`${key2}: ${t("invalidFileExtension", state, { name: fileName, formats })}`
|
|
3279
|
-
);
|
|
3280
|
-
}
|
|
3782
|
+
function validateFileCount(key, resourceIds, element, state, errors) {
|
|
3783
|
+
const minFiles = "minCount" in element ? element.minCount ?? 0 : 0;
|
|
3784
|
+
const maxFiles = "maxCount" in element ? element.maxCount ?? Infinity : Infinity;
|
|
3785
|
+
if (element.required && resourceIds.length === 0) {
|
|
3786
|
+
errors.push(`${key}: ${t("required", state)}`);
|
|
3787
|
+
}
|
|
3788
|
+
if (resourceIds.length < minFiles) {
|
|
3789
|
+
errors.push(`${key}: ${t("minFiles", state, { min: minFiles })}`);
|
|
3790
|
+
}
|
|
3791
|
+
if (resourceIds.length > maxFiles) {
|
|
3792
|
+
errors.push(`${key}: ${t("maxFiles", state, { max: maxFiles })}`);
|
|
3793
|
+
}
|
|
3794
|
+
}
|
|
3795
|
+
function validateFileExtensions(key, resourceIds, element, state, errors) {
|
|
3796
|
+
const acceptField = "accept" in element ? element.accept : void 0;
|
|
3797
|
+
const allowedExtensions = getAllowedExtensions(acceptField);
|
|
3798
|
+
if (allowedExtensions.length === 0) return;
|
|
3799
|
+
const formats = allowedExtensions.join(", ");
|
|
3800
|
+
for (const rid of resourceIds) {
|
|
3801
|
+
const meta = state.resourceIndex.get(rid);
|
|
3802
|
+
const fileName = meta?.name ?? rid;
|
|
3803
|
+
if (!isFileExtensionAllowed(fileName, allowedExtensions)) {
|
|
3804
|
+
errors.push(
|
|
3805
|
+
`${key}: ${t("invalidFileExtension", state, { name: fileName, formats })}`
|
|
3806
|
+
);
|
|
3281
3807
|
}
|
|
3282
|
-
}
|
|
3283
|
-
|
|
3284
|
-
|
|
3285
|
-
|
|
3286
|
-
|
|
3287
|
-
|
|
3288
|
-
|
|
3289
|
-
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
|
|
3293
|
-
|
|
3294
|
-
);
|
|
3295
|
-
}
|
|
3808
|
+
}
|
|
3809
|
+
}
|
|
3810
|
+
function validateFileSizes(key, resourceIds, element, state, errors) {
|
|
3811
|
+
const maxSizeMB = "maxSize" in element ? element.maxSize ?? Infinity : Infinity;
|
|
3812
|
+
if (maxSizeMB === Infinity) return;
|
|
3813
|
+
for (const rid of resourceIds) {
|
|
3814
|
+
const meta = state.resourceIndex.get(rid);
|
|
3815
|
+
if (!meta) continue;
|
|
3816
|
+
if (meta.size > maxSizeMB * 1024 * 1024) {
|
|
3817
|
+
errors.push(
|
|
3818
|
+
`${key}: ${t("fileTooLarge", state, { name: meta.name, maxSize: maxSizeMB })}`
|
|
3819
|
+
);
|
|
3296
3820
|
}
|
|
3297
|
-
}
|
|
3821
|
+
}
|
|
3822
|
+
}
|
|
3823
|
+
function validateMultiFile(element, key, context) {
|
|
3824
|
+
const { scopeRoot, skipValidation, path, state } = context;
|
|
3825
|
+
const errors = [];
|
|
3826
|
+
const fullKey = pathJoin(path, key);
|
|
3827
|
+
const resourceIds = readMultiFileResourceIds(scopeRoot, fullKey);
|
|
3828
|
+
if (!skipValidation) {
|
|
3829
|
+
validateFileCount(key, resourceIds, element, state, errors);
|
|
3830
|
+
validateFileExtensions(key, resourceIds, element, state, errors);
|
|
3831
|
+
validateFileSizes(key, resourceIds, element, state, errors);
|
|
3832
|
+
}
|
|
3833
|
+
return { value: resourceIds, errors };
|
|
3834
|
+
}
|
|
3835
|
+
function validateSingleFile(element, key, context) {
|
|
3836
|
+
const { scopeRoot, skipValidation, state } = context;
|
|
3837
|
+
const errors = [];
|
|
3838
|
+
const input = scopeRoot.querySelector(
|
|
3839
|
+
`input[name$="${key}"][type="hidden"]`
|
|
3840
|
+
);
|
|
3841
|
+
const rid = input?.value ?? "";
|
|
3842
|
+
if (!skipValidation && element.required && rid === "") {
|
|
3843
|
+
errors.push(`${key}: ${t("required", state)}`);
|
|
3844
|
+
return { value: null, errors };
|
|
3845
|
+
}
|
|
3846
|
+
if (!skipValidation && rid !== "") {
|
|
3847
|
+
validateFileExtensions(key, [rid], element, state, errors);
|
|
3848
|
+
validateFileSizes(key, [rid], element, state, errors);
|
|
3849
|
+
}
|
|
3850
|
+
return { value: rid || null, errors };
|
|
3851
|
+
}
|
|
3852
|
+
function validateFileElement(element, key, context) {
|
|
3853
|
+
const isMultipleField = element.type === "files" || "multiple" in element && Boolean(element.multiple);
|
|
3298
3854
|
if (isMultipleField) {
|
|
3299
|
-
|
|
3300
|
-
|
|
3301
|
-
|
|
3302
|
-
|
|
3303
|
-
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
|
|
3307
|
-
|
|
3308
|
-
|
|
3309
|
-
|
|
3310
|
-
|
|
3311
|
-
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
|
|
3317
|
-
|
|
3318
|
-
|
|
3855
|
+
return validateMultiFile(element, key, context);
|
|
3856
|
+
}
|
|
3857
|
+
return validateSingleFile(element, key, context);
|
|
3858
|
+
}
|
|
3859
|
+
|
|
3860
|
+
// src/components/file/render-readonly.ts
|
|
3861
|
+
function renderFileElementReadonly(element, ctx, wrapper, pathKey) {
|
|
3862
|
+
const state = ctx.state;
|
|
3863
|
+
const rawInitial = ctx.prefill[element.key];
|
|
3864
|
+
const initial = typeof rawInitial === "string" ? rawInitial : "";
|
|
3865
|
+
if (initial) {
|
|
3866
|
+
addPrefillFilesToIndex([initial], state.resourceIndex);
|
|
3867
|
+
const hiddenInput = document.createElement("input");
|
|
3868
|
+
hiddenInput.type = "hidden";
|
|
3869
|
+
hiddenInput.name = pathKey;
|
|
3870
|
+
hiddenInput.value = initial;
|
|
3871
|
+
wrapper.appendChild(hiddenInput);
|
|
3872
|
+
renderFilePreviewReadonly(initial, state).then((filePreview) => {
|
|
3873
|
+
wrapper.appendChild(filePreview);
|
|
3874
|
+
}).catch((err) => {
|
|
3875
|
+
console.error("Failed to render file preview:", err);
|
|
3876
|
+
wrapper.appendChild(buildEmptyReadonlyTile(state));
|
|
3877
|
+
});
|
|
3319
3878
|
} else {
|
|
3320
|
-
|
|
3321
|
-
|
|
3322
|
-
|
|
3323
|
-
|
|
3324
|
-
|
|
3325
|
-
|
|
3326
|
-
|
|
3327
|
-
}
|
|
3328
|
-
|
|
3329
|
-
|
|
3330
|
-
|
|
3331
|
-
|
|
3332
|
-
|
|
3879
|
+
wrapper.appendChild(buildEmptyReadonlyTile(state));
|
|
3880
|
+
}
|
|
3881
|
+
}
|
|
3882
|
+
function buildEmptyReadonlyTile(state) {
|
|
3883
|
+
const emptyState = document.createElement("div");
|
|
3884
|
+
emptyState.style.cssText = `
|
|
3885
|
+
width:${TILE_SIZE};
|
|
3886
|
+
height:${TILE_SIZE};
|
|
3887
|
+
display:flex;
|
|
3888
|
+
align-items:center;
|
|
3889
|
+
justify-content:center;
|
|
3890
|
+
background:var(--fb-file-upload-bg-color,#f3f4f6);
|
|
3891
|
+
border-radius:var(--fb-border-radius,0.5rem);
|
|
3892
|
+
border:1px solid var(--fb-file-upload-border-color,#d1d5db);
|
|
3893
|
+
`;
|
|
3894
|
+
emptyState.innerHTML = `<div style="font-size:11px;text-align:center;color:var(--fb-text-secondary-color,#6b7280);">${escapeHtml(t("noFileSelected", state))}</div>`;
|
|
3895
|
+
return emptyState;
|
|
3896
|
+
}
|
|
3897
|
+
function renderMultiFileReadonly(rids, state, wrapper, pathKey, marginTop) {
|
|
3898
|
+
addPrefillFilesToIndex(rids, state.resourceIndex);
|
|
3899
|
+
const filesWrapper = document.createElement("div");
|
|
3900
|
+
filesWrapper.dataset.filesWrapper = pathKey;
|
|
3901
|
+
filesWrapper.dataset.resourceIds = JSON.stringify(rids);
|
|
3902
|
+
wrapper.appendChild(filesWrapper);
|
|
3903
|
+
if (rids.length === 0) {
|
|
3904
|
+
const emptyEl = document.createElement("div");
|
|
3905
|
+
emptyEl.className = "fb-tile-empty-text";
|
|
3906
|
+
emptyEl.textContent = t("noFilesSelected", state);
|
|
3907
|
+
filesWrapper.appendChild(emptyEl);
|
|
3908
|
+
return;
|
|
3909
|
+
}
|
|
3910
|
+
const tilesWrap = document.createElement("div");
|
|
3911
|
+
tilesWrap.style.cssText = `display:flex;flex-wrap:wrap;gap:6px;${marginTop ? `margin-top:${marginTop};` : ""}`;
|
|
3912
|
+
filesWrapper.appendChild(tilesWrap);
|
|
3913
|
+
const placeholders = rids.map(() => {
|
|
3914
|
+
const placeholder = document.createElement("div");
|
|
3915
|
+
placeholder.style.cssText = `width:${TILE_SIZE};height:${TILE_SIZE};`;
|
|
3916
|
+
tilesWrap.appendChild(placeholder);
|
|
3917
|
+
return placeholder;
|
|
3918
|
+
});
|
|
3919
|
+
for (let i = 0; i < rids.length; i++) {
|
|
3920
|
+
const resourceId = rids[i];
|
|
3921
|
+
const placeholder = placeholders[i];
|
|
3922
|
+
renderFilePreviewReadonly(resourceId, state).then((tileEl) => {
|
|
3923
|
+
placeholder.replaceWith(tileEl);
|
|
3924
|
+
}).catch((err) => {
|
|
3925
|
+
console.error("Failed to render readonly tile:", err);
|
|
3926
|
+
});
|
|
3927
|
+
}
|
|
3928
|
+
}
|
|
3929
|
+
function renderFilesElementReadonly(element, ctx, wrapper, pathKey) {
|
|
3930
|
+
const rawPrefill = ctx.prefill[element.key];
|
|
3931
|
+
const initialFiles = Array.isArray(rawPrefill) ? rawPrefill : [];
|
|
3932
|
+
renderMultiFileReadonly(initialFiles, ctx.state, wrapper, pathKey);
|
|
3933
|
+
}
|
|
3934
|
+
function renderMultipleFileElementReadonly(element, ctx, wrapper, pathKey) {
|
|
3935
|
+
const rawPrefill = ctx.prefill[element.key];
|
|
3936
|
+
const initialFiles = Array.isArray(rawPrefill) ? rawPrefill : [];
|
|
3937
|
+
renderMultiFileReadonly(initialFiles, ctx.state, wrapper, pathKey, "4px");
|
|
3938
|
+
}
|
|
3939
|
+
|
|
3940
|
+
// src/components/file.ts
|
|
3941
|
+
function renderFileElement(element, ctx, wrapper, pathKey) {
|
|
3942
|
+
if (isElementReadonly(element, ctx.state, ctx)) {
|
|
3943
|
+
renderFileElementReadonly(element, ctx, wrapper, pathKey);
|
|
3944
|
+
} else {
|
|
3945
|
+
renderFileElementEdit(element, ctx, wrapper, pathKey);
|
|
3946
|
+
}
|
|
3947
|
+
}
|
|
3948
|
+
function renderFilesElement(element, ctx, wrapper, pathKey) {
|
|
3949
|
+
if (isElementReadonly(element, ctx.state, ctx)) {
|
|
3950
|
+
renderFilesElementReadonly(element, ctx, wrapper, pathKey);
|
|
3951
|
+
} else {
|
|
3952
|
+
renderFilesElementEdit(element, ctx, wrapper, pathKey);
|
|
3953
|
+
}
|
|
3954
|
+
}
|
|
3955
|
+
function renderMultipleFileElement(element, ctx, wrapper, pathKey) {
|
|
3956
|
+
if (isElementReadonly(element, ctx.state, ctx)) {
|
|
3957
|
+
renderMultipleFileElementReadonly(element, ctx, wrapper, pathKey);
|
|
3958
|
+
} else {
|
|
3959
|
+
renderMultipleFileElementEdit(element, ctx, wrapper, pathKey);
|
|
3333
3960
|
}
|
|
3334
3961
|
}
|
|
3335
3962
|
function updateFileField(element, fieldPath, value, context) {
|
|
3336
3963
|
const { scopeRoot, state } = context;
|
|
3337
|
-
if ("multiple" in element && element.multiple) {
|
|
3964
|
+
if (element.type === "files" || "multiple" in element && element.multiple) {
|
|
3338
3965
|
if (!Array.isArray(value)) {
|
|
3339
3966
|
console.warn(
|
|
3340
3967
|
`updateFileField: Expected array for multiple file field "${fieldPath}", got ${typeof value}`
|
|
@@ -3344,29 +3971,20 @@ function updateFileField(element, fieldPath, value, context) {
|
|
|
3344
3971
|
value.forEach((resourceId) => {
|
|
3345
3972
|
if (resourceId && typeof resourceId === "string") {
|
|
3346
3973
|
if (!state.resourceIndex.has(resourceId)) {
|
|
3347
|
-
|
|
3348
|
-
const extension = filename.split(".").pop()?.toLowerCase();
|
|
3349
|
-
let fileType = "application/octet-stream";
|
|
3350
|
-
if (extension) {
|
|
3351
|
-
if (["jpg", "jpeg", "png", "gif", "webp"].includes(extension)) {
|
|
3352
|
-
fileType = `image/${extension === "jpg" ? "jpeg" : extension}`;
|
|
3353
|
-
} else if (["mp4", "webm", "mov", "avi"].includes(extension)) {
|
|
3354
|
-
fileType = `video/${extension === "mov" ? "quicktime" : extension}`;
|
|
3355
|
-
}
|
|
3356
|
-
}
|
|
3357
|
-
state.resourceIndex.set(resourceId, {
|
|
3358
|
-
name: filename,
|
|
3359
|
-
type: fileType,
|
|
3360
|
-
size: 0,
|
|
3361
|
-
uploadedAt: /* @__PURE__ */ new Date(),
|
|
3362
|
-
file: void 0
|
|
3363
|
-
});
|
|
3974
|
+
addResourceToIndex(resourceId, state);
|
|
3364
3975
|
}
|
|
3365
3976
|
}
|
|
3366
3977
|
});
|
|
3367
|
-
|
|
3368
|
-
`
|
|
3978
|
+
const filesWrapper = scopeRoot.querySelector(
|
|
3979
|
+
`[data-files-wrapper="${fieldPath}"]`
|
|
3369
3980
|
);
|
|
3981
|
+
if (filesWrapper) {
|
|
3982
|
+
filesWrapper.dataset.resourceIds = JSON.stringify(value);
|
|
3983
|
+
} else {
|
|
3984
|
+
console.warn(
|
|
3985
|
+
`updateFileField: [data-files-wrapper="${fieldPath}"] not found in DOM; data-resource-ids not updated`
|
|
3986
|
+
);
|
|
3987
|
+
}
|
|
3370
3988
|
} else {
|
|
3371
3989
|
const hiddenInput = scopeRoot.querySelector(
|
|
3372
3990
|
`input[name="${fieldPath}"][type="hidden"]`
|
|
@@ -3380,23 +3998,7 @@ function updateFileField(element, fieldPath, value, context) {
|
|
|
3380
3998
|
hiddenInput.value = value != null ? String(value) : "";
|
|
3381
3999
|
if (value && typeof value === "string") {
|
|
3382
4000
|
if (!state.resourceIndex.has(value)) {
|
|
3383
|
-
|
|
3384
|
-
const extension = filename.split(".").pop()?.toLowerCase();
|
|
3385
|
-
let fileType = "application/octet-stream";
|
|
3386
|
-
if (extension) {
|
|
3387
|
-
if (["jpg", "jpeg", "png", "gif", "webp"].includes(extension)) {
|
|
3388
|
-
fileType = `image/${extension === "jpg" ? "jpeg" : extension}`;
|
|
3389
|
-
} else if (["mp4", "webm", "mov", "avi"].includes(extension)) {
|
|
3390
|
-
fileType = `video/${extension === "mov" ? "quicktime" : extension}`;
|
|
3391
|
-
}
|
|
3392
|
-
}
|
|
3393
|
-
state.resourceIndex.set(value, {
|
|
3394
|
-
name: filename,
|
|
3395
|
-
type: fileType,
|
|
3396
|
-
size: 0,
|
|
3397
|
-
uploadedAt: /* @__PURE__ */ new Date(),
|
|
3398
|
-
file: void 0
|
|
3399
|
-
});
|
|
4001
|
+
addResourceToIndex(value, state);
|
|
3400
4002
|
}
|
|
3401
4003
|
console.info(
|
|
3402
4004
|
`updateFileField: File field "${fieldPath}" updated. Preview update requires re-render.`
|
|
@@ -3404,6 +4006,25 @@ function updateFileField(element, fieldPath, value, context) {
|
|
|
3404
4006
|
}
|
|
3405
4007
|
}
|
|
3406
4008
|
}
|
|
4009
|
+
function addResourceToIndex(resourceId, state) {
|
|
4010
|
+
const filename = resourceId.split("/").pop() || "file";
|
|
4011
|
+
const extension = filename.split(".").pop()?.toLowerCase();
|
|
4012
|
+
let fileType = "application/octet-stream";
|
|
4013
|
+
if (extension) {
|
|
4014
|
+
if (["jpg", "jpeg", "png", "gif", "webp"].includes(extension)) {
|
|
4015
|
+
fileType = `image/${extension === "jpg" ? "jpeg" : extension}`;
|
|
4016
|
+
} else if (["mp4", "webm", "mov", "avi"].includes(extension)) {
|
|
4017
|
+
fileType = `video/${extension === "mov" ? "quicktime" : extension}`;
|
|
4018
|
+
}
|
|
4019
|
+
}
|
|
4020
|
+
state.resourceIndex.set(resourceId, {
|
|
4021
|
+
name: filename,
|
|
4022
|
+
type: fileType,
|
|
4023
|
+
size: 0,
|
|
4024
|
+
uploadedAt: /* @__PURE__ */ new Date(),
|
|
4025
|
+
file: void 0
|
|
4026
|
+
});
|
|
4027
|
+
}
|
|
3407
4028
|
|
|
3408
4029
|
// src/components/colour.ts
|
|
3409
4030
|
function normalizeColourValue(value) {
|
|
@@ -3559,15 +4180,16 @@ function createEditColourUI(value, pathKey, ctx) {
|
|
|
3559
4180
|
}
|
|
3560
4181
|
function renderColourElement(element, ctx, wrapper, pathKey) {
|
|
3561
4182
|
const state = ctx.state;
|
|
4183
|
+
const readonly = isElementReadonly(element, state, ctx);
|
|
3562
4184
|
const initialValue = ctx.prefill[element.key] || element.default || "#000000";
|
|
3563
|
-
if (
|
|
4185
|
+
if (readonly) {
|
|
3564
4186
|
const readonlyUI = createReadonlyColourUI(initialValue);
|
|
3565
4187
|
wrapper.appendChild(readonlyUI);
|
|
3566
4188
|
} else {
|
|
3567
4189
|
const editUI = createEditColourUI(initialValue, pathKey, ctx);
|
|
3568
4190
|
wrapper.appendChild(editUI);
|
|
3569
4191
|
}
|
|
3570
|
-
if (!
|
|
4192
|
+
if (!readonly) {
|
|
3571
4193
|
const colourHint = document.createElement("p");
|
|
3572
4194
|
colourHint.className = "mt-1";
|
|
3573
4195
|
colourHint.style.cssText = `
|
|
@@ -3580,6 +4202,7 @@ function renderColourElement(element, ctx, wrapper, pathKey) {
|
|
|
3580
4202
|
}
|
|
3581
4203
|
function renderMultipleColourElement(element, ctx, wrapper, pathKey) {
|
|
3582
4204
|
const state = ctx.state;
|
|
4205
|
+
const readonly = isElementReadonly(element, state, ctx);
|
|
3583
4206
|
const prefillValues = ctx.prefill[element.key] || [];
|
|
3584
4207
|
const values = Array.isArray(prefillValues) ? [...prefillValues] : [];
|
|
3585
4208
|
const minCount = element.minCount ?? 1;
|
|
@@ -3602,7 +4225,7 @@ function renderMultipleColourElement(element, ctx, wrapper, pathKey) {
|
|
|
3602
4225
|
function addColourItem(value = "#000000", index = -1) {
|
|
3603
4226
|
const itemWrapper = document.createElement("div");
|
|
3604
4227
|
itemWrapper.className = "multiple-colour-item flex items-center gap-2";
|
|
3605
|
-
if (
|
|
4228
|
+
if (readonly) {
|
|
3606
4229
|
const readonlyUI = createReadonlyColourUI(value);
|
|
3607
4230
|
while (readonlyUI.firstChild) {
|
|
3608
4231
|
itemWrapper.appendChild(readonlyUI.firstChild);
|
|
@@ -3622,7 +4245,7 @@ function renderMultipleColourElement(element, ctx, wrapper, pathKey) {
|
|
|
3622
4245
|
return itemWrapper;
|
|
3623
4246
|
}
|
|
3624
4247
|
function updateRemoveButtons() {
|
|
3625
|
-
if (
|
|
4248
|
+
if (readonly) return;
|
|
3626
4249
|
const items = container.querySelectorAll(".multiple-colour-item");
|
|
3627
4250
|
const currentCount = items.length;
|
|
3628
4251
|
items.forEach((item) => {
|
|
@@ -3667,7 +4290,7 @@ function renderMultipleColourElement(element, ctx, wrapper, pathKey) {
|
|
|
3667
4290
|
}
|
|
3668
4291
|
let addRow = null;
|
|
3669
4292
|
let countDisplay = null;
|
|
3670
|
-
if (!
|
|
4293
|
+
if (!readonly) {
|
|
3671
4294
|
addRow = document.createElement("div");
|
|
3672
4295
|
addRow.className = "flex items-center gap-3 mt-2";
|
|
3673
4296
|
const addBtn = document.createElement("button");
|
|
@@ -3714,7 +4337,7 @@ function renderMultipleColourElement(element, ctx, wrapper, pathKey) {
|
|
|
3714
4337
|
values.forEach((value) => addColourItem(value));
|
|
3715
4338
|
updateAddButton();
|
|
3716
4339
|
updateRemoveButtons();
|
|
3717
|
-
if (!
|
|
4340
|
+
if (!readonly) {
|
|
3718
4341
|
const hint = document.createElement("p");
|
|
3719
4342
|
hint.className = "mt-1";
|
|
3720
4343
|
hint.style.cssText = `
|
|
@@ -3783,7 +4406,9 @@ function validateColourElement(element, key, context) {
|
|
|
3783
4406
|
return normalized;
|
|
3784
4407
|
};
|
|
3785
4408
|
if (element.multiple) {
|
|
3786
|
-
const hexInputs = scopeRoot.querySelectorAll(
|
|
4409
|
+
const hexInputs = scopeRoot.querySelectorAll(
|
|
4410
|
+
`[name^="${key}["].colour-hex-input`
|
|
4411
|
+
);
|
|
3787
4412
|
const values = [];
|
|
3788
4413
|
hexInputs.forEach((input, index) => {
|
|
3789
4414
|
const val = input?.value ?? "";
|
|
@@ -3830,7 +4455,9 @@ function updateColourField(element, fieldPath, value, context) {
|
|
|
3830
4455
|
);
|
|
3831
4456
|
return;
|
|
3832
4457
|
}
|
|
3833
|
-
const hexInputs = scopeRoot.querySelectorAll(
|
|
4458
|
+
const hexInputs = scopeRoot.querySelectorAll(
|
|
4459
|
+
`[name^="${fieldPath}["].colour-hex-input`
|
|
4460
|
+
);
|
|
3834
4461
|
hexInputs.forEach((hexInput, index) => {
|
|
3835
4462
|
if (index < value.length) {
|
|
3836
4463
|
const normalized = normalizeColourValue(value[index]);
|
|
@@ -4025,6 +4652,7 @@ function renderSliderElement(element, ctx, wrapper, pathKey) {
|
|
|
4025
4652
|
);
|
|
4026
4653
|
}
|
|
4027
4654
|
const state = ctx.state;
|
|
4655
|
+
const readonly = isElementReadonly(element, state, ctx);
|
|
4028
4656
|
const defaultValue = element.default !== void 0 ? element.default : (element.min + element.max) / 2;
|
|
4029
4657
|
const initialValue = ctx.prefill[element.key] ?? defaultValue;
|
|
4030
4658
|
const sliderUI = createSliderUI(
|
|
@@ -4032,10 +4660,10 @@ function renderSliderElement(element, ctx, wrapper, pathKey) {
|
|
|
4032
4660
|
pathKey,
|
|
4033
4661
|
element,
|
|
4034
4662
|
ctx,
|
|
4035
|
-
|
|
4663
|
+
readonly
|
|
4036
4664
|
);
|
|
4037
4665
|
wrapper.appendChild(sliderUI);
|
|
4038
|
-
if (!
|
|
4666
|
+
if (!readonly) {
|
|
4039
4667
|
const hint = document.createElement("p");
|
|
4040
4668
|
hint.className = "mt-1";
|
|
4041
4669
|
hint.style.cssText = `
|
|
@@ -4059,6 +4687,7 @@ function renderMultipleSliderElement(element, ctx, wrapper, pathKey) {
|
|
|
4059
4687
|
);
|
|
4060
4688
|
}
|
|
4061
4689
|
const state = ctx.state;
|
|
4690
|
+
const readonly = isElementReadonly(element, state, ctx);
|
|
4062
4691
|
const prefillValues = ctx.prefill[element.key] || [];
|
|
4063
4692
|
const values = Array.isArray(prefillValues) ? [...prefillValues] : [];
|
|
4064
4693
|
const minCount = element.minCount ?? 1;
|
|
@@ -4083,13 +4712,7 @@ function renderMultipleSliderElement(element, ctx, wrapper, pathKey) {
|
|
|
4083
4712
|
const itemWrapper = document.createElement("div");
|
|
4084
4713
|
itemWrapper.className = "multiple-slider-item flex items-start gap-2";
|
|
4085
4714
|
const tempPathKey = `${pathKey}[${container.children.length}]`;
|
|
4086
|
-
const sliderUI = createSliderUI(
|
|
4087
|
-
value,
|
|
4088
|
-
tempPathKey,
|
|
4089
|
-
element,
|
|
4090
|
-
ctx,
|
|
4091
|
-
state.config.readonly
|
|
4092
|
-
);
|
|
4715
|
+
const sliderUI = createSliderUI(value, tempPathKey, element, ctx, readonly);
|
|
4093
4716
|
sliderUI.style.flex = "1";
|
|
4094
4717
|
itemWrapper.appendChild(sliderUI);
|
|
4095
4718
|
if (index === -1) {
|
|
@@ -4101,7 +4724,7 @@ function renderMultipleSliderElement(element, ctx, wrapper, pathKey) {
|
|
|
4101
4724
|
return itemWrapper;
|
|
4102
4725
|
}
|
|
4103
4726
|
function updateRemoveButtons() {
|
|
4104
|
-
if (
|
|
4727
|
+
if (readonly) return;
|
|
4105
4728
|
const items = container.querySelectorAll(".multiple-slider-item");
|
|
4106
4729
|
const currentCount = items.length;
|
|
4107
4730
|
items.forEach((item) => {
|
|
@@ -4147,7 +4770,7 @@ function renderMultipleSliderElement(element, ctx, wrapper, pathKey) {
|
|
|
4147
4770
|
}
|
|
4148
4771
|
let addRow = null;
|
|
4149
4772
|
let countDisplay = null;
|
|
4150
|
-
if (!
|
|
4773
|
+
if (!readonly) {
|
|
4151
4774
|
addRow = document.createElement("div");
|
|
4152
4775
|
addRow.className = "flex items-center gap-3 mt-2";
|
|
4153
4776
|
const addBtn = document.createElement("button");
|
|
@@ -4193,7 +4816,7 @@ function renderMultipleSliderElement(element, ctx, wrapper, pathKey) {
|
|
|
4193
4816
|
values.forEach((value) => addSliderItem(value));
|
|
4194
4817
|
updateAddButton();
|
|
4195
4818
|
updateRemoveButtons();
|
|
4196
|
-
if (!
|
|
4819
|
+
if (!readonly) {
|
|
4197
4820
|
const hint = document.createElement("p");
|
|
4198
4821
|
hint.className = "mt-1";
|
|
4199
4822
|
hint.style.cssText = `
|
|
@@ -4432,9 +5055,7 @@ function mergeWithDefaults(prefill, defaults) {
|
|
|
4432
5055
|
}
|
|
4433
5056
|
function extractRootFormData(formRoot) {
|
|
4434
5057
|
const data = {};
|
|
4435
|
-
const inputs = formRoot.querySelectorAll(
|
|
4436
|
-
"input, select, textarea"
|
|
4437
|
-
);
|
|
5058
|
+
const inputs = formRoot.querySelectorAll("input, select, textarea");
|
|
4438
5059
|
inputs.forEach((input) => {
|
|
4439
5060
|
const fieldName = input.getAttribute("name");
|
|
4440
5061
|
if (fieldName && !fieldName.includes("[") && !fieldName.includes(".")) {
|
|
@@ -4498,7 +5119,8 @@ function renderSingleContainerElement(element, ctx, wrapper, pathKey) {
|
|
|
4498
5119
|
} else {
|
|
4499
5120
|
itemsWrap.className = `grid grid-cols-${columns} gap-4`;
|
|
4500
5121
|
}
|
|
4501
|
-
|
|
5122
|
+
const containerIsReadonly = isElementReadonly(element, ctx.state, ctx);
|
|
5123
|
+
if (!containerIsReadonly) {
|
|
4502
5124
|
const hintsElement = createPrefillHints(element, pathKey);
|
|
4503
5125
|
if (hintsElement) {
|
|
4504
5126
|
containerWrap.appendChild(hintsElement);
|
|
@@ -4513,12 +5135,15 @@ function renderSingleContainerElement(element, ctx, wrapper, pathKey) {
|
|
|
4513
5135
|
// Merged prefill with defaults for enableIf evaluation
|
|
4514
5136
|
formData: ctx.formData ?? ctx.prefill,
|
|
4515
5137
|
// Complete root data for enableIf evaluation
|
|
4516
|
-
state: ctx.state
|
|
5138
|
+
state: ctx.state,
|
|
5139
|
+
inheritedReadonly: containerIsReadonly || ctx.inheritedReadonly
|
|
4517
5140
|
};
|
|
4518
5141
|
element.elements.forEach((child) => {
|
|
4519
5142
|
if (child.hidden || child.type === "hidden") {
|
|
4520
5143
|
const prefillVal = containerPrefill[child.key] ?? child.default ?? null;
|
|
4521
|
-
itemsWrap.appendChild(
|
|
5144
|
+
itemsWrap.appendChild(
|
|
5145
|
+
createHiddenInput(pathJoin(subCtx.path, child.key), prefillVal)
|
|
5146
|
+
);
|
|
4522
5147
|
} else {
|
|
4523
5148
|
itemsWrap.appendChild(renderElement(child, subCtx));
|
|
4524
5149
|
}
|
|
@@ -4528,13 +5153,15 @@ function renderSingleContainerElement(element, ctx, wrapper, pathKey) {
|
|
|
4528
5153
|
}
|
|
4529
5154
|
function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
|
|
4530
5155
|
const state = ctx.state;
|
|
5156
|
+
const containerIsReadonly = isElementReadonly(element, state, ctx);
|
|
5157
|
+
const childInheritedReadonly = containerIsReadonly || ctx.inheritedReadonly;
|
|
4531
5158
|
const containerWrap = document.createElement("div");
|
|
4532
5159
|
containerWrap.className = "border border-gray-200 rounded-lg p-4 bg-gray-50";
|
|
4533
5160
|
const countDisplay = document.createElement("span");
|
|
4534
5161
|
countDisplay.className = "text-sm text-gray-500";
|
|
4535
5162
|
const itemsWrap = document.createElement("div");
|
|
4536
5163
|
itemsWrap.className = "space-y-4";
|
|
4537
|
-
if (!
|
|
5164
|
+
if (!containerIsReadonly) {
|
|
4538
5165
|
const hintsElement = createPrefillHints(element, element.key);
|
|
4539
5166
|
if (hintsElement) {
|
|
4540
5167
|
containerWrap.appendChild(hintsElement);
|
|
@@ -4572,8 +5199,9 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
|
|
|
4572
5199
|
path: pathJoin(ctx.path, `${element.key}[${idx}]`),
|
|
4573
5200
|
prefill: childDefaults,
|
|
4574
5201
|
// Defaults for enableIf evaluation
|
|
4575
|
-
formData: currentFormData
|
|
5202
|
+
formData: currentFormData,
|
|
4576
5203
|
// Current root data from DOM for enableIf
|
|
5204
|
+
inheritedReadonly: childInheritedReadonly
|
|
4577
5205
|
};
|
|
4578
5206
|
const item = document.createElement("div");
|
|
4579
5207
|
item.className = "containerItem border border-gray-300 rounded-lg p-4 bg-white";
|
|
@@ -4587,13 +5215,18 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
|
|
|
4587
5215
|
}
|
|
4588
5216
|
element.elements.forEach((child) => {
|
|
4589
5217
|
if (child.hidden || child.type === "hidden") {
|
|
4590
|
-
childWrapper.appendChild(
|
|
5218
|
+
childWrapper.appendChild(
|
|
5219
|
+
createHiddenInput(
|
|
5220
|
+
pathJoin(subCtx.path, child.key),
|
|
5221
|
+
child.default ?? null
|
|
5222
|
+
)
|
|
5223
|
+
);
|
|
4591
5224
|
} else {
|
|
4592
5225
|
childWrapper.appendChild(renderElement(child, subCtx));
|
|
4593
5226
|
}
|
|
4594
5227
|
});
|
|
4595
5228
|
item.appendChild(childWrapper);
|
|
4596
|
-
if (!
|
|
5229
|
+
if (!containerIsReadonly) {
|
|
4597
5230
|
const rem = document.createElement("button");
|
|
4598
5231
|
rem.type = "button";
|
|
4599
5232
|
rem.className = "absolute top-2 right-2 px-2 py-1 rounded";
|
|
@@ -4643,8 +5276,9 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
|
|
|
4643
5276
|
path: pathJoin(ctx.path, `${element.key}[${idx}]`),
|
|
4644
5277
|
prefill: mergedPrefill,
|
|
4645
5278
|
// Merged prefill with defaults for enableIf
|
|
4646
|
-
formData: ctx.formData ?? ctx.prefill
|
|
5279
|
+
formData: ctx.formData ?? ctx.prefill,
|
|
4647
5280
|
// Complete root data for enableIf
|
|
5281
|
+
inheritedReadonly: childInheritedReadonly
|
|
4648
5282
|
};
|
|
4649
5283
|
const item = document.createElement("div");
|
|
4650
5284
|
item.className = "containerItem border border-gray-300 rounded-lg p-4 bg-white";
|
|
@@ -4659,13 +5293,15 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
|
|
|
4659
5293
|
element.elements.forEach((child) => {
|
|
4660
5294
|
if (child.hidden || child.type === "hidden") {
|
|
4661
5295
|
const prefillVal = prefillObj?.[child.key] ?? child.default ?? null;
|
|
4662
|
-
childWrapper.appendChild(
|
|
5296
|
+
childWrapper.appendChild(
|
|
5297
|
+
createHiddenInput(pathJoin(subCtx.path, child.key), prefillVal)
|
|
5298
|
+
);
|
|
4663
5299
|
} else {
|
|
4664
5300
|
childWrapper.appendChild(renderElement(child, subCtx));
|
|
4665
5301
|
}
|
|
4666
5302
|
});
|
|
4667
5303
|
item.appendChild(childWrapper);
|
|
4668
|
-
if (!
|
|
5304
|
+
if (!containerIsReadonly) {
|
|
4669
5305
|
const rem = document.createElement("button");
|
|
4670
5306
|
rem.type = "button";
|
|
4671
5307
|
rem.className = "absolute top-2 right-2 px-2 py-1 rounded";
|
|
@@ -4688,7 +5324,7 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
|
|
|
4688
5324
|
itemsWrap.appendChild(item);
|
|
4689
5325
|
});
|
|
4690
5326
|
}
|
|
4691
|
-
if (!
|
|
5327
|
+
if (!containerIsReadonly) {
|
|
4692
5328
|
while (countItems() < min) {
|
|
4693
5329
|
const idx = countItems();
|
|
4694
5330
|
const subCtx = {
|
|
@@ -4696,8 +5332,9 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
|
|
|
4696
5332
|
path: pathJoin(ctx.path, `${element.key}[${idx}]`),
|
|
4697
5333
|
prefill: childDefaults,
|
|
4698
5334
|
// Defaults for enableIf evaluation
|
|
4699
|
-
formData: ctx.formData ?? ctx.prefill
|
|
5335
|
+
formData: ctx.formData ?? ctx.prefill,
|
|
4700
5336
|
// Complete root data for enableIf
|
|
5337
|
+
inheritedReadonly: childInheritedReadonly
|
|
4701
5338
|
};
|
|
4702
5339
|
const item = document.createElement("div");
|
|
4703
5340
|
item.className = "containerItem border border-gray-300 rounded-lg p-4 bg-white";
|
|
@@ -4711,7 +5348,12 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
|
|
|
4711
5348
|
}
|
|
4712
5349
|
element.elements.forEach((child) => {
|
|
4713
5350
|
if (child.hidden || child.type === "hidden") {
|
|
4714
|
-
childWrapper.appendChild(
|
|
5351
|
+
childWrapper.appendChild(
|
|
5352
|
+
createHiddenInput(
|
|
5353
|
+
pathJoin(subCtx.path, child.key),
|
|
5354
|
+
child.default ?? null
|
|
5355
|
+
)
|
|
5356
|
+
);
|
|
4715
5357
|
} else {
|
|
4716
5358
|
childWrapper.appendChild(renderElement(child, subCtx));
|
|
4717
5359
|
}
|
|
@@ -4743,7 +5385,7 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
|
|
|
4743
5385
|
}
|
|
4744
5386
|
}
|
|
4745
5387
|
containerWrap.appendChild(itemsWrap);
|
|
4746
|
-
if (!
|
|
5388
|
+
if (!containerIsReadonly) {
|
|
4747
5389
|
const addRow = document.createElement("div");
|
|
4748
5390
|
addRow.className = "flex items-center gap-3 mt-2";
|
|
4749
5391
|
addRow.appendChild(createAddButton());
|
|
@@ -4902,8 +5544,10 @@ function updateContainerField(element, fieldPath, value, context) {
|
|
|
4902
5544
|
const filesKey = richChild.filesKey ?? "files";
|
|
4903
5545
|
const containerValue = itemValue;
|
|
4904
5546
|
const compositeValue = {};
|
|
4905
|
-
if (textKey in containerValue)
|
|
4906
|
-
|
|
5547
|
+
if (textKey in containerValue)
|
|
5548
|
+
compositeValue[textKey] = containerValue[textKey];
|
|
5549
|
+
if (filesKey in containerValue)
|
|
5550
|
+
compositeValue[filesKey] = containerValue[filesKey];
|
|
4907
5551
|
if (Object.keys(compositeValue).length > 0) {
|
|
4908
5552
|
instance.updateField(childPath, compositeValue);
|
|
4909
5553
|
}
|
|
@@ -4939,8 +5583,10 @@ function updateContainerField(element, fieldPath, value, context) {
|
|
|
4939
5583
|
const filesKey = richChild.filesKey ?? "files";
|
|
4940
5584
|
const containerValue = value;
|
|
4941
5585
|
const compositeValue = {};
|
|
4942
|
-
if (textKey in containerValue)
|
|
4943
|
-
|
|
5586
|
+
if (textKey in containerValue)
|
|
5587
|
+
compositeValue[textKey] = containerValue[textKey];
|
|
5588
|
+
if (filesKey in containerValue)
|
|
5589
|
+
compositeValue[filesKey] = containerValue[filesKey];
|
|
4944
5590
|
if (Object.keys(compositeValue).length > 0) {
|
|
4945
5591
|
instance.updateField(childPath, compositeValue);
|
|
4946
5592
|
}
|
|
@@ -5278,12 +5924,21 @@ function renderEditTable(element, initialData, pathKey, ctx, wrapper) {
|
|
|
5278
5924
|
const hiddenInput = document.createElement("input");
|
|
5279
5925
|
hiddenInput.type = "hidden";
|
|
5280
5926
|
hiddenInput.name = pathKey;
|
|
5281
|
-
hiddenInput.value = JSON.stringify({
|
|
5927
|
+
hiddenInput.value = JSON.stringify({
|
|
5928
|
+
[cellsKey]: cells,
|
|
5929
|
+
[mergesKey]: merges
|
|
5930
|
+
});
|
|
5282
5931
|
wrapper.appendChild(hiddenInput);
|
|
5283
5932
|
function persistValue() {
|
|
5284
|
-
hiddenInput.value = JSON.stringify({
|
|
5933
|
+
hiddenInput.value = JSON.stringify({
|
|
5934
|
+
[cellsKey]: cells,
|
|
5935
|
+
[mergesKey]: merges
|
|
5936
|
+
});
|
|
5285
5937
|
if (instance) {
|
|
5286
|
-
instance.triggerOnChange(pathKey, {
|
|
5938
|
+
instance.triggerOnChange(pathKey, {
|
|
5939
|
+
[cellsKey]: cells,
|
|
5940
|
+
[mergesKey]: merges
|
|
5941
|
+
});
|
|
5287
5942
|
}
|
|
5288
5943
|
}
|
|
5289
5944
|
hiddenInput._applyExternalUpdate = (data) => {
|
|
@@ -5342,9 +5997,7 @@ function renderEditTable(element, initialData, pathKey, ctx, wrapper) {
|
|
|
5342
5997
|
rebuild();
|
|
5343
5998
|
} catch (e) {
|
|
5344
5999
|
const errMsg = e instanceof Error ? e.message : String(e);
|
|
5345
|
-
console.error(
|
|
5346
|
-
t("tableImportError", state).replace("{error}", errMsg)
|
|
5347
|
-
);
|
|
6000
|
+
console.error(t("tableImportError", state).replace("{error}", errMsg));
|
|
5348
6001
|
} finally {
|
|
5349
6002
|
overlay.remove();
|
|
5350
6003
|
}
|
|
@@ -5492,15 +6145,19 @@ function renderEditTable(element, initialData, pathKey, ctx, wrapper) {
|
|
|
5492
6145
|
contextMenu.style.display = "none";
|
|
5493
6146
|
}
|
|
5494
6147
|
const menuDismissCtrl = new AbortController();
|
|
5495
|
-
document.addEventListener(
|
|
5496
|
-
|
|
5497
|
-
|
|
5498
|
-
|
|
5499
|
-
|
|
5500
|
-
|
|
5501
|
-
|
|
5502
|
-
|
|
5503
|
-
|
|
6148
|
+
document.addEventListener(
|
|
6149
|
+
"mousedown",
|
|
6150
|
+
(e) => {
|
|
6151
|
+
if (!wrapper.isConnected) {
|
|
6152
|
+
menuDismissCtrl.abort();
|
|
6153
|
+
return;
|
|
6154
|
+
}
|
|
6155
|
+
if (!contextMenu.contains(e.target)) {
|
|
6156
|
+
hideContextMenu();
|
|
6157
|
+
}
|
|
6158
|
+
},
|
|
6159
|
+
{ signal: menuDismissCtrl.signal }
|
|
6160
|
+
);
|
|
5504
6161
|
function applySelectionStyles() {
|
|
5505
6162
|
const range = selectionRange(sel);
|
|
5506
6163
|
const allTds = tableEl.querySelectorAll("td[data-row]");
|
|
@@ -5846,7 +6503,9 @@ function renderEditTable(element, initialData, pathKey, ctx, wrapper) {
|
|
|
5846
6503
|
if (!anchor) return;
|
|
5847
6504
|
const text = e.clipboardData?.getData("text/plain") ?? "";
|
|
5848
6505
|
const isMultiCell = text.includes(" ") || text.split(/\r?\n/).filter((l) => l).length > 1;
|
|
5849
|
-
const editing = tableEl.querySelector(
|
|
6506
|
+
const editing = tableEl.querySelector(
|
|
6507
|
+
"[contenteditable='true']"
|
|
6508
|
+
);
|
|
5850
6509
|
if (editing && !isMultiCell) return;
|
|
5851
6510
|
e.preventDefault();
|
|
5852
6511
|
if (editing) {
|
|
@@ -6002,7 +6661,11 @@ function renderEditTable(element, initialData, pathKey, ctx, wrapper) {
|
|
|
6002
6661
|
}
|
|
6003
6662
|
}
|
|
6004
6663
|
function updateTopZoneOverlays(mx, wr, tblR, scrollL, active) {
|
|
6005
|
-
const headerCells = active ? Array.from(
|
|
6664
|
+
const headerCells = active ? Array.from(
|
|
6665
|
+
tableEl.querySelectorAll(
|
|
6666
|
+
"thead td[data-col]"
|
|
6667
|
+
)
|
|
6668
|
+
) : [];
|
|
6006
6669
|
let closestColIdx = -1;
|
|
6007
6670
|
let closestColDist = Infinity;
|
|
6008
6671
|
let closestBorderX = -1;
|
|
@@ -6020,7 +6683,10 @@ function renderEditTable(element, initialData, pathKey, ctx, wrapper) {
|
|
|
6020
6683
|
if (borderDist < closestBorderDist && borderDist < 20) {
|
|
6021
6684
|
closestBorderDist = borderDist;
|
|
6022
6685
|
closestBorderX = cellRect.right - wr.left + scrollL;
|
|
6023
|
-
closestAfterCol = parseInt(
|
|
6686
|
+
closestAfterCol = parseInt(
|
|
6687
|
+
headerCells[i].getAttribute("data-col") ?? "0",
|
|
6688
|
+
10
|
|
6689
|
+
);
|
|
6024
6690
|
}
|
|
6025
6691
|
}
|
|
6026
6692
|
colRemoveBtns.forEach((btn, idx) => {
|
|
@@ -6075,7 +6741,10 @@ function renderEditTable(element, initialData, pathKey, ctx, wrapper) {
|
|
|
6075
6741
|
closestRowBorderDist = borderDist;
|
|
6076
6742
|
closestBorderY = trRect.bottom - wr.top;
|
|
6077
6743
|
const firstTd = allRowEls[i].querySelector("td[data-row]");
|
|
6078
|
-
closestAfterRow = parseInt(
|
|
6744
|
+
closestAfterRow = parseInt(
|
|
6745
|
+
firstTd?.getAttribute("data-row") ?? "0",
|
|
6746
|
+
10
|
|
6747
|
+
);
|
|
6079
6748
|
}
|
|
6080
6749
|
}
|
|
6081
6750
|
rowRemoveBtns.forEach((btn, idx) => {
|
|
@@ -6101,7 +6770,8 @@ function renderEditTable(element, initialData, pathKey, ctx, wrapper) {
|
|
|
6101
6770
|
let rafPending = false;
|
|
6102
6771
|
tableWrapper.onmousemove = (e) => {
|
|
6103
6772
|
const target = e.target;
|
|
6104
|
-
if (target.tagName === "BUTTON" && target.parentElement === tableWrapper)
|
|
6773
|
+
if (target.tagName === "BUTTON" && target.parentElement === tableWrapper)
|
|
6774
|
+
return;
|
|
6105
6775
|
if (rafPending) return;
|
|
6106
6776
|
rafPending = true;
|
|
6107
6777
|
const mx = e.clientX;
|
|
@@ -6160,6 +6830,7 @@ function isTableDataWithFieldNames(v, cellsKey) {
|
|
|
6160
6830
|
}
|
|
6161
6831
|
function renderTableElement(element, ctx, wrapper, pathKey) {
|
|
6162
6832
|
const state = ctx.state;
|
|
6833
|
+
const readonly = isElementReadonly(element, state, ctx);
|
|
6163
6834
|
const rawPrefill = ctx.prefill[element.key];
|
|
6164
6835
|
const cellsKey = element.fieldNames?.cells ?? "cells";
|
|
6165
6836
|
const mergesKey = element.fieldNames?.merges ?? "merges";
|
|
@@ -6194,11 +6865,13 @@ function renderTableElement(element, ctx, wrapper, pathKey) {
|
|
|
6194
6865
|
const cols = rows > 0 ? initialData.cells[0].length : 0;
|
|
6195
6866
|
const err = validateMerges(initialData.merges, rows, cols);
|
|
6196
6867
|
if (err) {
|
|
6197
|
-
console.warn(
|
|
6868
|
+
console.warn(
|
|
6869
|
+
`Table "${element.key}": invalid prefill merges stripped (${err})`
|
|
6870
|
+
);
|
|
6198
6871
|
initialData = { ...initialData, merges: [] };
|
|
6199
6872
|
}
|
|
6200
6873
|
}
|
|
6201
|
-
if (
|
|
6874
|
+
if (readonly) {
|
|
6202
6875
|
renderReadonlyTable(initialData, wrapper);
|
|
6203
6876
|
} else {
|
|
6204
6877
|
renderEditTable(element, initialData, pathKey, ctx, wrapper);
|
|
@@ -6875,20 +7548,26 @@ function renderEditMode(element, ctx, wrapper, pathKey, initialValue) {
|
|
|
6875
7548
|
});
|
|
6876
7549
|
let mentionTooltip = null;
|
|
6877
7550
|
backdrop.addEventListener("mouseover", (e) => {
|
|
6878
|
-
const mark = e.target.closest?.(
|
|
7551
|
+
const mark = e.target.closest?.(
|
|
7552
|
+
"mark"
|
|
7553
|
+
);
|
|
6879
7554
|
if (!mark?.dataset.rid) return;
|
|
6880
7555
|
mentionTooltip = removePortalTooltip(mentionTooltip);
|
|
6881
7556
|
mentionTooltip = showMentionTooltip(mark, mark.dataset.rid, state);
|
|
6882
7557
|
});
|
|
6883
7558
|
backdrop.addEventListener("mouseout", (e) => {
|
|
6884
|
-
const mark = e.target.closest?.(
|
|
7559
|
+
const mark = e.target.closest?.(
|
|
7560
|
+
"mark"
|
|
7561
|
+
);
|
|
6885
7562
|
if (!mark) return;
|
|
6886
7563
|
const related = e.relatedTarget;
|
|
6887
7564
|
if (related?.closest?.("mark")) return;
|
|
6888
7565
|
mentionTooltip = removePortalTooltip(mentionTooltip);
|
|
6889
7566
|
});
|
|
6890
7567
|
backdrop.addEventListener("mousedown", (e) => {
|
|
6891
|
-
const mark = e.target.closest?.(
|
|
7568
|
+
const mark = e.target.closest?.(
|
|
7569
|
+
"mark"
|
|
7570
|
+
);
|
|
6892
7571
|
if (!mark) return;
|
|
6893
7572
|
mentionTooltip = removePortalTooltip(mentionTooltip);
|
|
6894
7573
|
const marks = backdrop.querySelectorAll("mark");
|
|
@@ -7132,11 +7811,7 @@ function renderEditMode(element, ctx, wrapper, pathKey, initialValue) {
|
|
|
7132
7811
|
textarea.addEventListener("keydown", (e) => {
|
|
7133
7812
|
if (!dropdownState.open) return;
|
|
7134
7813
|
const labels = buildFileLabelsFromClosure();
|
|
7135
|
-
const filtered = filterFilesForDropdown(
|
|
7136
|
-
dropdownState.query,
|
|
7137
|
-
files,
|
|
7138
|
-
labels
|
|
7139
|
-
);
|
|
7814
|
+
const filtered = filterFilesForDropdown(dropdownState.query, files, labels);
|
|
7140
7815
|
if (e.key === "ArrowDown") {
|
|
7141
7816
|
e.preventDefault();
|
|
7142
7817
|
dropdownState.selectedIndex = Math.min(
|
|
@@ -7463,6 +8138,7 @@ function renderReadonlyMode(_element, ctx, wrapper, _pathKey, value) {
|
|
|
7463
8138
|
}
|
|
7464
8139
|
function renderRichInputElement(element, ctx, wrapper, pathKey) {
|
|
7465
8140
|
const state = ctx.state;
|
|
8141
|
+
const readonly = isElementReadonly(element, state, ctx);
|
|
7466
8142
|
const textKey = element.textKey ?? "text";
|
|
7467
8143
|
const filesKey = element.filesKey ?? "files";
|
|
7468
8144
|
let initialValue;
|
|
@@ -7500,7 +8176,7 @@ function renderRichInputElement(element, ctx, wrapper, pathKey) {
|
|
|
7500
8176
|
});
|
|
7501
8177
|
}
|
|
7502
8178
|
}
|
|
7503
|
-
if (
|
|
8179
|
+
if (readonly) {
|
|
7504
8180
|
renderReadonlyMode(element, ctx, wrapper, pathKey, initialValue);
|
|
7505
8181
|
} else {
|
|
7506
8182
|
if (!state.config.uploadFile) {
|
|
@@ -7562,9 +8238,7 @@ function validateRichInputElement(element, key, context) {
|
|
|
7562
8238
|
}
|
|
7563
8239
|
}
|
|
7564
8240
|
if (element.maxFiles != null && files.length > element.maxFiles) {
|
|
7565
|
-
errors.push(
|
|
7566
|
-
`${key}: ${t("maxFiles", state, { max: element.maxFiles })}`
|
|
7567
|
-
);
|
|
8241
|
+
errors.push(`${key}: ${t("maxFiles", state, { max: element.maxFiles })}`);
|
|
7568
8242
|
}
|
|
7569
8243
|
}
|
|
7570
8244
|
return { value, errors, spread: !!element.flatOutput };
|
|
@@ -7680,9 +8354,7 @@ function shouldDisableElement(element, ctx) {
|
|
|
7680
8354
|
return false;
|
|
7681
8355
|
}
|
|
7682
8356
|
function extractDOMValue(fieldPath, formRoot) {
|
|
7683
|
-
const input = formRoot.querySelector(
|
|
7684
|
-
`[name="${fieldPath}"]`
|
|
7685
|
-
);
|
|
8357
|
+
const input = formRoot.querySelector(`[name="${fieldPath}"]`);
|
|
7686
8358
|
if (!input) {
|
|
7687
8359
|
return void 0;
|
|
7688
8360
|
}
|
|
@@ -7727,9 +8399,7 @@ function reevaluateEnableIf(wrapper, element, ctx) {
|
|
|
7727
8399
|
`[data-container-item="${containerKey}[${containerIndex}]"]`
|
|
7728
8400
|
);
|
|
7729
8401
|
if (containerItemElement) {
|
|
7730
|
-
const inputs = containerItemElement.querySelectorAll(
|
|
7731
|
-
"input, select, textarea"
|
|
7732
|
-
);
|
|
8402
|
+
const inputs = containerItemElement.querySelectorAll("input, select, textarea");
|
|
7733
8403
|
inputs.forEach((input) => {
|
|
7734
8404
|
const fieldName = input.getAttribute("name");
|
|
7735
8405
|
if (fieldName) {
|
|
@@ -7781,7 +8451,10 @@ function reevaluateEnableIf(wrapper, element, ctx) {
|
|
|
7781
8451
|
wrapper.setAttribute("data-conditionally-disabled", "true");
|
|
7782
8452
|
}
|
|
7783
8453
|
} catch (error) {
|
|
7784
|
-
console.error(
|
|
8454
|
+
console.error(
|
|
8455
|
+
`Error re-evaluating enableIf for field "${element.key}":`,
|
|
8456
|
+
error
|
|
8457
|
+
);
|
|
7785
8458
|
}
|
|
7786
8459
|
}
|
|
7787
8460
|
function setupEnableIfListeners(wrapper, element, ctx) {
|
|
@@ -7802,14 +8475,10 @@ function setupEnableIfListeners(wrapper, element, ctx) {
|
|
|
7802
8475
|
} else {
|
|
7803
8476
|
dependencyFieldPath = dependencyKey;
|
|
7804
8477
|
}
|
|
7805
|
-
const dependencyInput = formRoot.querySelector(
|
|
7806
|
-
`[name="${dependencyFieldPath}"]`
|
|
7807
|
-
);
|
|
8478
|
+
const dependencyInput = formRoot.querySelector(`[name="${dependencyFieldPath}"]`);
|
|
7808
8479
|
if (!dependencyInput) {
|
|
7809
8480
|
const observer = new MutationObserver(() => {
|
|
7810
|
-
const input = formRoot.querySelector(
|
|
7811
|
-
`[name="${dependencyFieldPath}"]`
|
|
7812
|
-
);
|
|
8481
|
+
const input = formRoot.querySelector(`[name="${dependencyFieldPath}"]`);
|
|
7813
8482
|
if (input) {
|
|
7814
8483
|
input.addEventListener("change", () => {
|
|
7815
8484
|
reevaluateEnableIf(wrapper, element, ctx);
|
|
@@ -7956,7 +8625,9 @@ function dispatchToRenderer(element, ctx, wrapper, pathKey) {
|
|
|
7956
8625
|
default: {
|
|
7957
8626
|
const unsupported = document.createElement("div");
|
|
7958
8627
|
unsupported.className = "text-red-500 text-sm";
|
|
7959
|
-
unsupported.textContent = t("unsupportedFieldType", ctx.state, {
|
|
8628
|
+
unsupported.textContent = t("unsupportedFieldType", ctx.state, {
|
|
8629
|
+
type: element.type
|
|
8630
|
+
});
|
|
7960
8631
|
wrapper.appendChild(unsupported);
|
|
7961
8632
|
}
|
|
7962
8633
|
}
|
|
@@ -8008,6 +8679,8 @@ var defaultConfig = {
|
|
|
8008
8679
|
noFileSelected: "No file selected",
|
|
8009
8680
|
noFilesSelected: "No files selected",
|
|
8010
8681
|
downloadButton: "Download",
|
|
8682
|
+
downloadFile: "Download",
|
|
8683
|
+
openInNewTab: "Open in new tab",
|
|
8011
8684
|
changeButton: "Change",
|
|
8012
8685
|
placeholderText: "Enter text",
|
|
8013
8686
|
previewAlt: "Preview",
|
|
@@ -8029,6 +8702,8 @@ var defaultConfig = {
|
|
|
8029
8702
|
fileCountSingle: "{count} file",
|
|
8030
8703
|
fileCountPlural: "{count} files",
|
|
8031
8704
|
fileCountRange: "({min}-{max})",
|
|
8705
|
+
uploadingFile: "Uploading\u2026",
|
|
8706
|
+
filesCounter: "{count}/{max}",
|
|
8032
8707
|
// Validation errors
|
|
8033
8708
|
required: "Required",
|
|
8034
8709
|
minItems: "Minimum {min} items required",
|
|
@@ -8070,6 +8745,8 @@ var defaultConfig = {
|
|
|
8070
8745
|
noFileSelected: "\u0424\u0430\u0439\u043B \u043D\u0435 \u0432\u044B\u0431\u0440\u0430\u043D",
|
|
8071
8746
|
noFilesSelected: "\u041D\u0435\u0442 \u0444\u0430\u0439\u043B\u043E\u0432",
|
|
8072
8747
|
downloadButton: "\u0421\u043A\u0430\u0447\u0430\u0442\u044C",
|
|
8748
|
+
downloadFile: "\u0421\u043A\u0430\u0447\u0430\u0442\u044C",
|
|
8749
|
+
openInNewTab: "\u041E\u0442\u043A\u0440\u044B\u0442\u044C \u0432 \u043D\u043E\u0432\u043E\u0439 \u0432\u043A\u043B\u0430\u0434\u043A\u0435",
|
|
8073
8750
|
changeButton: "\u0418\u0437\u043C\u0435\u043D\u0438\u0442\u044C",
|
|
8074
8751
|
placeholderText: "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0442\u0435\u043A\u0441\u0442",
|
|
8075
8752
|
previewAlt: "\u041F\u0440\u0435\u0434\u043F\u0440\u043E\u0441\u043C\u043E\u0442\u0440",
|
|
@@ -8091,6 +8768,8 @@ var defaultConfig = {
|
|
|
8091
8768
|
fileCountSingle: "{count} \u0444\u0430\u0439\u043B",
|
|
8092
8769
|
fileCountPlural: "{count} \u0444\u0430\u0439\u043B\u043E\u0432",
|
|
8093
8770
|
fileCountRange: "({min}-{max})",
|
|
8771
|
+
uploadingFile: "\u0417\u0430\u0433\u0440\u0443\u0437\u043A\u0430\u2026",
|
|
8772
|
+
filesCounter: "{count}/{max}",
|
|
8094
8773
|
// Validation errors
|
|
8095
8774
|
required: "\u041E\u0431\u044F\u0437\u0430\u0442\u0435\u043B\u044C\u043D\u043E\u0435 \u043F\u043E\u043B\u0435",
|
|
8096
8775
|
minItems: "\u041C\u0438\u043D\u0438\u043C\u0443\u043C {min} \u044D\u043B\u0435\u043C\u0435\u043D\u0442\u043E\u0432",
|
|
@@ -8908,7 +9587,10 @@ var FormBuilderInstance = class {
|
|
|
8908
9587
|
);
|
|
8909
9588
|
if (componentResult !== null) {
|
|
8910
9589
|
errors.push(...componentResult.errors);
|
|
8911
|
-
return {
|
|
9590
|
+
return {
|
|
9591
|
+
value: componentResult.value,
|
|
9592
|
+
spread: !!componentResult.spread
|
|
9593
|
+
};
|
|
8912
9594
|
}
|
|
8913
9595
|
console.warn(`Unknown field type "${element.type}" for key "${key}"`);
|
|
8914
9596
|
return { value: null, spread: false };
|
|
@@ -8917,10 +9599,7 @@ var FormBuilderInstance = class {
|
|
|
8917
9599
|
this.state.schema.elements.forEach((element) => {
|
|
8918
9600
|
if (element.enableIf) {
|
|
8919
9601
|
try {
|
|
8920
|
-
const shouldEnable = evaluateEnableCondition(
|
|
8921
|
-
element.enableIf,
|
|
8922
|
-
data
|
|
8923
|
-
);
|
|
9602
|
+
const shouldEnable = evaluateEnableCondition(element.enableIf, data);
|
|
8924
9603
|
if (!shouldEnable) {
|
|
8925
9604
|
return;
|
|
8926
9605
|
}
|
|
@@ -9215,7 +9894,10 @@ var FormBuilderInstance = class {
|
|
|
9215
9894
|
disabledWrapper.className = "fb-field-wrapper-disabled";
|
|
9216
9895
|
disabledWrapper.style.display = "none";
|
|
9217
9896
|
disabledWrapper.setAttribute("data-field-key", element.key);
|
|
9218
|
-
disabledWrapper.setAttribute(
|
|
9897
|
+
disabledWrapper.setAttribute(
|
|
9898
|
+
"data-conditionally-disabled",
|
|
9899
|
+
"true"
|
|
9900
|
+
);
|
|
9219
9901
|
wrapper.parentNode?.replaceChild(disabledWrapper, wrapper);
|
|
9220
9902
|
}
|
|
9221
9903
|
} catch (error) {
|