@dmitryvim/form-builder 0.2.11 → 0.2.12
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/dist/browser/formbuilder.min.js +141 -101
- package/dist/browser/formbuilder.v0.2.12.min.js +362 -0
- package/dist/cjs/index.cjs +793 -425
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/esm/index.js +792 -423
- package/dist/esm/index.js.map +1 -1
- package/dist/form-builder.js +141 -101
- package/dist/types/components/file.d.ts +1 -1
- package/dist/types/components/text.d.ts +1 -0
- package/dist/types/instance/state.d.ts +1 -0
- package/dist/types/types/config.d.ts +37 -6
- package/dist/types/utils/helpers.d.ts +5 -0
- package/dist/types/utils/translation.d.ts +8 -3
- package/dist/types/utils/validation.d.ts +2 -1
- package/package.json +1 -1
- package/dist/browser/formbuilder.v0.2.11.min.js +0 -322
package/dist/esm/index.js
CHANGED
|
@@ -1,53 +1,83 @@
|
|
|
1
|
+
// src/utils/translation.ts
|
|
2
|
+
function t(key, state, params) {
|
|
3
|
+
const locale = state.config.locale || "en";
|
|
4
|
+
const localeTranslations = state.config.translations[locale];
|
|
5
|
+
const fallbackTranslations = state.config.translations.en;
|
|
6
|
+
let text = localeTranslations?.[key] || fallbackTranslations?.[key] || key;
|
|
7
|
+
if (params) {
|
|
8
|
+
for (const [paramKey, paramValue] of Object.entries(params)) {
|
|
9
|
+
text = text.replace(new RegExp(`\\{${paramKey}\\}`, "g"), String(paramValue));
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
return text;
|
|
13
|
+
}
|
|
14
|
+
|
|
1
15
|
// src/utils/validation.ts
|
|
2
|
-
function addLengthHint(element, parts) {
|
|
3
|
-
if (element.minLength
|
|
4
|
-
if (element.minLength
|
|
5
|
-
parts.push(
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
16
|
+
function addLengthHint(element, parts, state) {
|
|
17
|
+
if (element.minLength != null || element.maxLength != null) {
|
|
18
|
+
if (element.minLength != null && element.maxLength != null) {
|
|
19
|
+
parts.push(
|
|
20
|
+
t("hintLengthRange", state, {
|
|
21
|
+
min: element.minLength,
|
|
22
|
+
max: element.maxLength
|
|
23
|
+
})
|
|
24
|
+
);
|
|
25
|
+
} else if (element.maxLength != null) {
|
|
26
|
+
parts.push(t("hintMaxLength", state, { max: element.maxLength }));
|
|
27
|
+
} else if (element.minLength != null) {
|
|
28
|
+
parts.push(t("hintMinLength", state, { min: element.minLength }));
|
|
10
29
|
}
|
|
11
30
|
}
|
|
12
31
|
}
|
|
13
|
-
function addRangeHint(element, parts) {
|
|
14
|
-
if (element.min
|
|
15
|
-
if (element.min
|
|
16
|
-
parts.push(
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
} else if (element.
|
|
20
|
-
parts.push(
|
|
32
|
+
function addRangeHint(element, parts, state) {
|
|
33
|
+
if (element.min != null || element.max != null) {
|
|
34
|
+
if (element.min != null && element.max != null) {
|
|
35
|
+
parts.push(
|
|
36
|
+
t("hintValueRange", state, { min: element.min, max: element.max })
|
|
37
|
+
);
|
|
38
|
+
} else if (element.max != null) {
|
|
39
|
+
parts.push(t("hintMaxValue", state, { max: element.max }));
|
|
40
|
+
} else if (element.min != null) {
|
|
41
|
+
parts.push(t("hintMinValue", state, { min: element.min }));
|
|
21
42
|
}
|
|
22
43
|
}
|
|
23
44
|
}
|
|
24
|
-
function addFileSizeHint(element, parts) {
|
|
45
|
+
function addFileSizeHint(element, parts, state) {
|
|
25
46
|
if (element.maxSizeMB) {
|
|
26
|
-
parts.push(
|
|
47
|
+
parts.push(t("hintMaxSize", state, { size: element.maxSizeMB }));
|
|
27
48
|
}
|
|
28
49
|
}
|
|
29
|
-
function addFormatHint(element, parts) {
|
|
50
|
+
function addFormatHint(element, parts, state) {
|
|
30
51
|
if (element.accept?.extensions) {
|
|
31
52
|
parts.push(
|
|
32
|
-
|
|
53
|
+
t("hintFormats", state, {
|
|
54
|
+
formats: element.accept.extensions.map((ext) => ext.toUpperCase()).join(",")
|
|
55
|
+
})
|
|
33
56
|
);
|
|
34
57
|
}
|
|
35
58
|
}
|
|
36
|
-
function
|
|
37
|
-
if (element.
|
|
38
|
-
parts.push("
|
|
39
|
-
} else
|
|
40
|
-
parts.push("
|
|
59
|
+
function addRequiredHint(element, parts, state) {
|
|
60
|
+
if (element.required) {
|
|
61
|
+
parts.push(t("hintRequired", state));
|
|
62
|
+
} else {
|
|
63
|
+
parts.push(t("hintOptional", state));
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
function addPatternHint(element, parts, state) {
|
|
67
|
+
if (element.pattern) {
|
|
68
|
+
parts.push(t("hintPattern", state, { pattern: element.pattern }));
|
|
41
69
|
}
|
|
42
70
|
}
|
|
43
|
-
function makeFieldHint(element) {
|
|
71
|
+
function makeFieldHint(element, state) {
|
|
44
72
|
const parts = [];
|
|
45
|
-
|
|
46
|
-
addLengthHint(element, parts);
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
73
|
+
addRequiredHint(element, parts, state);
|
|
74
|
+
addLengthHint(element, parts, state);
|
|
75
|
+
if (element.type !== "slider") {
|
|
76
|
+
addRangeHint(element, parts, state);
|
|
77
|
+
}
|
|
78
|
+
addFileSizeHint(element, parts, state);
|
|
79
|
+
addFormatHint(element, parts, state);
|
|
80
|
+
addPatternHint(element, parts, state);
|
|
51
81
|
return parts.join(" \u2022 ");
|
|
52
82
|
}
|
|
53
83
|
function validateSchema(schema) {
|
|
@@ -185,6 +215,11 @@ function validateSchema(schema) {
|
|
|
185
215
|
function isPlainObject(obj) {
|
|
186
216
|
return obj && typeof obj === "object" && obj.constructor === Object;
|
|
187
217
|
}
|
|
218
|
+
function escapeHtml(text) {
|
|
219
|
+
const div = document.createElement("div");
|
|
220
|
+
div.textContent = text;
|
|
221
|
+
return div.innerHTML;
|
|
222
|
+
}
|
|
188
223
|
function pathJoin(base, key) {
|
|
189
224
|
return base ? `${base}.${key}` : key;
|
|
190
225
|
}
|
|
@@ -262,13 +297,62 @@ function deepEqual(a, b) {
|
|
|
262
297
|
}
|
|
263
298
|
|
|
264
299
|
// src/components/text.ts
|
|
300
|
+
function createCharCounter(element, input, isTextarea = false) {
|
|
301
|
+
const counter = document.createElement("span");
|
|
302
|
+
counter.className = "char-counter";
|
|
303
|
+
counter.style.cssText = `
|
|
304
|
+
position: absolute;
|
|
305
|
+
${isTextarea ? "bottom: 8px" : "top: 50%; transform: translateY(-50%)"};
|
|
306
|
+
right: 10px;
|
|
307
|
+
font-size: var(--fb-font-size-small);
|
|
308
|
+
color: var(--fb-text-secondary-color);
|
|
309
|
+
pointer-events: none;
|
|
310
|
+
background: var(--fb-background-color);
|
|
311
|
+
padding: 0 4px;
|
|
312
|
+
`;
|
|
313
|
+
const updateCounter = () => {
|
|
314
|
+
const len = input.value.length;
|
|
315
|
+
const min = element.minLength;
|
|
316
|
+
const max = element.maxLength;
|
|
317
|
+
if (min == null && max == null) {
|
|
318
|
+
counter.textContent = "";
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
if (len === 0 || min != null && len < min) {
|
|
322
|
+
if (min != null && max != null) {
|
|
323
|
+
counter.textContent = `${min}-${max}`;
|
|
324
|
+
} else if (max != null) {
|
|
325
|
+
counter.textContent = `\u2264${max}`;
|
|
326
|
+
} else if (min != null) {
|
|
327
|
+
counter.textContent = `\u2265${min}`;
|
|
328
|
+
}
|
|
329
|
+
counter.style.color = "var(--fb-text-secondary-color)";
|
|
330
|
+
} else if (max != null && len > max) {
|
|
331
|
+
counter.textContent = `${len}/${max}`;
|
|
332
|
+
counter.style.color = "var(--fb-error-color)";
|
|
333
|
+
} else {
|
|
334
|
+
if (max != null) {
|
|
335
|
+
counter.textContent = `${len}/${max}`;
|
|
336
|
+
} else {
|
|
337
|
+
counter.textContent = `${len}`;
|
|
338
|
+
}
|
|
339
|
+
counter.style.color = "var(--fb-text-secondary-color)";
|
|
340
|
+
}
|
|
341
|
+
};
|
|
342
|
+
input.addEventListener("input", updateCounter);
|
|
343
|
+
updateCounter();
|
|
344
|
+
return counter;
|
|
345
|
+
}
|
|
265
346
|
function renderTextElement(element, ctx, wrapper, pathKey) {
|
|
266
347
|
const state = ctx.state;
|
|
348
|
+
const inputWrapper = document.createElement("div");
|
|
349
|
+
inputWrapper.style.cssText = "position: relative;";
|
|
267
350
|
const textInput = document.createElement("input");
|
|
268
351
|
textInput.type = "text";
|
|
269
352
|
textInput.className = "w-full rounded-lg";
|
|
270
353
|
textInput.style.cssText = `
|
|
271
354
|
padding: var(--fb-input-padding-y) var(--fb-input-padding-x);
|
|
355
|
+
padding-right: 60px;
|
|
272
356
|
border: var(--fb-border-width) solid var(--fb-border-color);
|
|
273
357
|
border-radius: var(--fb-border-radius);
|
|
274
358
|
background-color: ${state.config.readonly ? "var(--fb-background-readonly-color)" : "var(--fb-background-color)"};
|
|
@@ -276,6 +360,8 @@ function renderTextElement(element, ctx, wrapper, pathKey) {
|
|
|
276
360
|
font-size: var(--fb-font-size);
|
|
277
361
|
font-family: var(--fb-font-family);
|
|
278
362
|
transition: all var(--fb-transition-duration) ease-in-out;
|
|
363
|
+
width: 100%;
|
|
364
|
+
box-sizing: border-box;
|
|
279
365
|
`;
|
|
280
366
|
textInput.name = pathKey;
|
|
281
367
|
textInput.placeholder = element.placeholder || "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0442\u0435\u043A\u0441\u0442";
|
|
@@ -310,15 +396,12 @@ function renderTextElement(element, ctx, wrapper, pathKey) {
|
|
|
310
396
|
textInput.addEventListener("blur", handleChange);
|
|
311
397
|
textInput.addEventListener("input", handleChange);
|
|
312
398
|
}
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
`;
|
|
320
|
-
textHint.textContent = makeFieldHint(element);
|
|
321
|
-
wrapper.appendChild(textHint);
|
|
399
|
+
inputWrapper.appendChild(textInput);
|
|
400
|
+
if (!state.config.readonly && (element.minLength != null || element.maxLength != null)) {
|
|
401
|
+
const counter = createCharCounter(element, textInput, false);
|
|
402
|
+
inputWrapper.appendChild(counter);
|
|
403
|
+
}
|
|
404
|
+
wrapper.appendChild(inputWrapper);
|
|
322
405
|
}
|
|
323
406
|
function renderMultipleTextElement(element, ctx, wrapper, pathKey) {
|
|
324
407
|
const state = ctx.state;
|
|
@@ -344,11 +427,13 @@ function renderMultipleTextElement(element, ctx, wrapper, pathKey) {
|
|
|
344
427
|
function addTextItem(value = "", index = -1) {
|
|
345
428
|
const itemWrapper = document.createElement("div");
|
|
346
429
|
itemWrapper.className = "multiple-text-item flex items-center gap-2";
|
|
430
|
+
const inputContainer = document.createElement("div");
|
|
431
|
+
inputContainer.style.cssText = "position: relative; flex: 1;";
|
|
347
432
|
const textInput = document.createElement("input");
|
|
348
433
|
textInput.type = "text";
|
|
349
|
-
textInput.className = "flex-1";
|
|
350
434
|
textInput.style.cssText = `
|
|
351
435
|
padding: var(--fb-input-padding-y) var(--fb-input-padding-x);
|
|
436
|
+
padding-right: 60px;
|
|
352
437
|
border: var(--fb-border-width) solid var(--fb-border-color);
|
|
353
438
|
border-radius: var(--fb-border-radius);
|
|
354
439
|
background-color: ${state.config.readonly ? "var(--fb-background-readonly-color)" : "var(--fb-background-color)"};
|
|
@@ -356,8 +441,10 @@ function renderMultipleTextElement(element, ctx, wrapper, pathKey) {
|
|
|
356
441
|
font-size: var(--fb-font-size);
|
|
357
442
|
font-family: var(--fb-font-family);
|
|
358
443
|
transition: all var(--fb-transition-duration) ease-in-out;
|
|
444
|
+
width: 100%;
|
|
445
|
+
box-sizing: border-box;
|
|
359
446
|
`;
|
|
360
|
-
textInput.placeholder = element.placeholder || "
|
|
447
|
+
textInput.placeholder = element.placeholder || t("placeholderText", state);
|
|
361
448
|
textInput.value = value;
|
|
362
449
|
textInput.readOnly = state.config.readonly;
|
|
363
450
|
if (!state.config.readonly) {
|
|
@@ -389,7 +476,12 @@ function renderMultipleTextElement(element, ctx, wrapper, pathKey) {
|
|
|
389
476
|
textInput.addEventListener("blur", handleChange);
|
|
390
477
|
textInput.addEventListener("input", handleChange);
|
|
391
478
|
}
|
|
392
|
-
|
|
479
|
+
inputContainer.appendChild(textInput);
|
|
480
|
+
if (!state.config.readonly && (element.minLength != null || element.maxLength != null)) {
|
|
481
|
+
const counter = createCharCounter(element, textInput, false);
|
|
482
|
+
inputContainer.appendChild(counter);
|
|
483
|
+
}
|
|
484
|
+
itemWrapper.appendChild(inputContainer);
|
|
393
485
|
if (index === -1) {
|
|
394
486
|
container.appendChild(itemWrapper);
|
|
395
487
|
} else {
|
|
@@ -442,47 +534,54 @@ function renderMultipleTextElement(element, ctx, wrapper, pathKey) {
|
|
|
442
534
|
removeBtn.style.pointerEvents = disabled ? "none" : "auto";
|
|
443
535
|
});
|
|
444
536
|
}
|
|
537
|
+
let addRow = null;
|
|
538
|
+
let countDisplay = null;
|
|
539
|
+
if (!state.config.readonly) {
|
|
540
|
+
addRow = document.createElement("div");
|
|
541
|
+
addRow.className = "flex items-center gap-3 mt-2";
|
|
542
|
+
const addBtn = document.createElement("button");
|
|
543
|
+
addBtn.type = "button";
|
|
544
|
+
addBtn.className = "add-text-btn px-3 py-1 rounded";
|
|
545
|
+
addBtn.style.cssText = `
|
|
546
|
+
color: var(--fb-primary-color);
|
|
547
|
+
border: var(--fb-border-width) solid var(--fb-primary-color);
|
|
548
|
+
background-color: transparent;
|
|
549
|
+
font-size: var(--fb-font-size);
|
|
550
|
+
transition: all var(--fb-transition-duration);
|
|
551
|
+
`;
|
|
552
|
+
addBtn.textContent = "+";
|
|
553
|
+
addBtn.addEventListener("mouseenter", () => {
|
|
554
|
+
addBtn.style.backgroundColor = "var(--fb-background-hover-color)";
|
|
555
|
+
});
|
|
556
|
+
addBtn.addEventListener("mouseleave", () => {
|
|
557
|
+
addBtn.style.backgroundColor = "transparent";
|
|
558
|
+
});
|
|
559
|
+
addBtn.onclick = () => {
|
|
560
|
+
values.push(element.default || "");
|
|
561
|
+
addTextItem(element.default || "");
|
|
562
|
+
updateAddButton();
|
|
563
|
+
updateRemoveButtons();
|
|
564
|
+
};
|
|
565
|
+
countDisplay = document.createElement("span");
|
|
566
|
+
countDisplay.className = "text-sm text-gray-500";
|
|
567
|
+
addRow.appendChild(addBtn);
|
|
568
|
+
addRow.appendChild(countDisplay);
|
|
569
|
+
wrapper.appendChild(addRow);
|
|
570
|
+
}
|
|
445
571
|
function updateAddButton() {
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
if (
|
|
449
|
-
const
|
|
450
|
-
addBtn.
|
|
451
|
-
addBtn.
|
|
452
|
-
addBtn.style.
|
|
453
|
-
color: var(--fb-primary-color);
|
|
454
|
-
border: var(--fb-border-width) solid var(--fb-primary-color);
|
|
455
|
-
background-color: transparent;
|
|
456
|
-
font-size: var(--fb-font-size);
|
|
457
|
-
transition: all var(--fb-transition-duration);
|
|
458
|
-
`;
|
|
459
|
-
addBtn.textContent = "+";
|
|
460
|
-
addBtn.addEventListener("mouseenter", () => {
|
|
461
|
-
addBtn.style.backgroundColor = "var(--fb-background-hover-color)";
|
|
462
|
-
});
|
|
463
|
-
addBtn.addEventListener("mouseleave", () => {
|
|
464
|
-
addBtn.style.backgroundColor = "transparent";
|
|
465
|
-
});
|
|
466
|
-
addBtn.onclick = () => {
|
|
467
|
-
values.push(element.default || "");
|
|
468
|
-
addTextItem(element.default || "");
|
|
469
|
-
updateAddButton();
|
|
470
|
-
updateRemoveButtons();
|
|
471
|
-
};
|
|
472
|
-
wrapper.appendChild(addBtn);
|
|
572
|
+
if (!addRow || !countDisplay) return;
|
|
573
|
+
const addBtn = addRow.querySelector(".add-text-btn");
|
|
574
|
+
if (addBtn) {
|
|
575
|
+
const disabled = values.length >= maxCount;
|
|
576
|
+
addBtn.disabled = disabled;
|
|
577
|
+
addBtn.style.opacity = disabled ? "0.5" : "1";
|
|
578
|
+
addBtn.style.pointerEvents = disabled ? "none" : "auto";
|
|
473
579
|
}
|
|
580
|
+
countDisplay.textContent = `${values.length}/${maxCount === Infinity ? "\u221E" : maxCount}`;
|
|
474
581
|
}
|
|
475
582
|
values.forEach((value) => addTextItem(value));
|
|
476
583
|
updateAddButton();
|
|
477
584
|
updateRemoveButtons();
|
|
478
|
-
const hint = document.createElement("p");
|
|
479
|
-
hint.className = "mt-1";
|
|
480
|
-
hint.style.cssText = `
|
|
481
|
-
font-size: var(--fb-font-size-small);
|
|
482
|
-
color: var(--fb-text-secondary-color);
|
|
483
|
-
`;
|
|
484
|
-
hint.textContent = makeFieldHint(element);
|
|
485
|
-
wrapper.appendChild(hint);
|
|
486
585
|
}
|
|
487
586
|
function validateTextElement(element, key, context) {
|
|
488
587
|
const errors = [];
|
|
@@ -521,26 +620,31 @@ function validateTextElement(element, key, context) {
|
|
|
521
620
|
};
|
|
522
621
|
const validateTextInput = (input, val, fieldKey) => {
|
|
523
622
|
let hasError = false;
|
|
623
|
+
const { state } = context;
|
|
524
624
|
if (!skipValidation && val) {
|
|
525
625
|
if (element.minLength !== void 0 && element.minLength !== null && val.length < element.minLength) {
|
|
526
|
-
|
|
527
|
-
|
|
626
|
+
const msg = t("minLength", state, { min: element.minLength });
|
|
627
|
+
errors.push(`${fieldKey}: ${msg}`);
|
|
628
|
+
markValidity(input, msg);
|
|
528
629
|
hasError = true;
|
|
529
630
|
} else if (element.maxLength !== void 0 && element.maxLength !== null && val.length > element.maxLength) {
|
|
530
|
-
|
|
531
|
-
|
|
631
|
+
const msg = t("maxLength", state, { max: element.maxLength });
|
|
632
|
+
errors.push(`${fieldKey}: ${msg}`);
|
|
633
|
+
markValidity(input, msg);
|
|
532
634
|
hasError = true;
|
|
533
635
|
} else if (element.pattern) {
|
|
534
636
|
try {
|
|
535
637
|
const re = new RegExp(element.pattern);
|
|
536
638
|
if (!re.test(val)) {
|
|
537
|
-
|
|
538
|
-
|
|
639
|
+
const msg = t("patternMismatch", state);
|
|
640
|
+
errors.push(`${fieldKey}: ${msg}`);
|
|
641
|
+
markValidity(input, msg);
|
|
539
642
|
hasError = true;
|
|
540
643
|
}
|
|
541
644
|
} catch {
|
|
542
|
-
|
|
543
|
-
|
|
645
|
+
const msg = t("invalidPattern", state);
|
|
646
|
+
errors.push(`${fieldKey}: ${msg}`);
|
|
647
|
+
markValidity(input, msg);
|
|
544
648
|
hasError = true;
|
|
545
649
|
}
|
|
546
650
|
}
|
|
@@ -560,17 +664,18 @@ function validateTextElement(element, key, context) {
|
|
|
560
664
|
validateTextInput(input, val, `${key}[${index}]`);
|
|
561
665
|
});
|
|
562
666
|
if (!skipValidation) {
|
|
667
|
+
const { state } = context;
|
|
563
668
|
const minCount = element.minCount ?? 1;
|
|
564
669
|
const maxCount = element.maxCount ?? Infinity;
|
|
565
670
|
const filteredValues = rawValues.filter((v) => v.trim() !== "");
|
|
566
671
|
if (element.required && filteredValues.length === 0) {
|
|
567
|
-
errors.push(`${key}: required`);
|
|
672
|
+
errors.push(`${key}: ${t("required", state)}`);
|
|
568
673
|
}
|
|
569
674
|
if (filteredValues.length < minCount) {
|
|
570
|
-
errors.push(`${key}:
|
|
675
|
+
errors.push(`${key}: ${t("minItems", state, { min: minCount })}`);
|
|
571
676
|
}
|
|
572
677
|
if (filteredValues.length > maxCount) {
|
|
573
|
-
errors.push(`${key}:
|
|
678
|
+
errors.push(`${key}: ${t("maxItems", state, { max: maxCount })}`);
|
|
574
679
|
}
|
|
575
680
|
}
|
|
576
681
|
return { value: values, errors };
|
|
@@ -578,8 +683,9 @@ function validateTextElement(element, key, context) {
|
|
|
578
683
|
const input = scopeRoot.querySelector(`[name$="${key}"]`);
|
|
579
684
|
const val = input?.value ?? "";
|
|
580
685
|
if (!skipValidation && element.required && val === "") {
|
|
581
|
-
|
|
582
|
-
|
|
686
|
+
const msg = t("required", context.state);
|
|
687
|
+
errors.push(`${key}: ${msg}`);
|
|
688
|
+
markValidity(input, msg);
|
|
583
689
|
return { value: null, errors };
|
|
584
690
|
}
|
|
585
691
|
if (input) {
|
|
@@ -623,8 +729,11 @@ function updateTextField(element, fieldPath, value, context) {
|
|
|
623
729
|
// src/components/textarea.ts
|
|
624
730
|
function renderTextareaElement(element, ctx, wrapper, pathKey) {
|
|
625
731
|
const state = ctx.state;
|
|
732
|
+
const textareaWrapper = document.createElement("div");
|
|
733
|
+
textareaWrapper.style.cssText = "position: relative;";
|
|
626
734
|
const textareaInput = document.createElement("textarea");
|
|
627
735
|
textareaInput.className = "w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 resize-none";
|
|
736
|
+
textareaInput.style.cssText = "padding-bottom: 24px;";
|
|
628
737
|
textareaInput.name = pathKey;
|
|
629
738
|
textareaInput.placeholder = element.placeholder || "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0442\u0435\u043A\u0441\u0442";
|
|
630
739
|
textareaInput.rows = element.rows || 4;
|
|
@@ -638,11 +747,12 @@ function renderTextareaElement(element, ctx, wrapper, pathKey) {
|
|
|
638
747
|
textareaInput.addEventListener("blur", handleChange);
|
|
639
748
|
textareaInput.addEventListener("input", handleChange);
|
|
640
749
|
}
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
750
|
+
textareaWrapper.appendChild(textareaInput);
|
|
751
|
+
if (!state.config.readonly && (element.minLength != null || element.maxLength != null)) {
|
|
752
|
+
const counter = createCharCounter(element, textareaInput, true);
|
|
753
|
+
textareaWrapper.appendChild(counter);
|
|
754
|
+
}
|
|
755
|
+
wrapper.appendChild(textareaWrapper);
|
|
646
756
|
}
|
|
647
757
|
function renderMultipleTextareaElement(element, ctx, wrapper, pathKey) {
|
|
648
758
|
const state = ctx.state;
|
|
@@ -668,9 +778,12 @@ function renderMultipleTextareaElement(element, ctx, wrapper, pathKey) {
|
|
|
668
778
|
function addTextareaItem(value = "", index = -1) {
|
|
669
779
|
const itemWrapper = document.createElement("div");
|
|
670
780
|
itemWrapper.className = "multiple-textarea-item";
|
|
781
|
+
const textareaContainer = document.createElement("div");
|
|
782
|
+
textareaContainer.style.cssText = "position: relative;";
|
|
671
783
|
const textareaInput = document.createElement("textarea");
|
|
672
784
|
textareaInput.className = "w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 resize-none";
|
|
673
|
-
textareaInput.
|
|
785
|
+
textareaInput.style.cssText = "padding-bottom: 24px;";
|
|
786
|
+
textareaInput.placeholder = element.placeholder || t("placeholderText", state);
|
|
674
787
|
textareaInput.rows = element.rows || 4;
|
|
675
788
|
textareaInput.value = value;
|
|
676
789
|
textareaInput.readOnly = state.config.readonly;
|
|
@@ -682,7 +795,12 @@ function renderMultipleTextareaElement(element, ctx, wrapper, pathKey) {
|
|
|
682
795
|
textareaInput.addEventListener("blur", handleChange);
|
|
683
796
|
textareaInput.addEventListener("input", handleChange);
|
|
684
797
|
}
|
|
685
|
-
|
|
798
|
+
textareaContainer.appendChild(textareaInput);
|
|
799
|
+
if (!state.config.readonly && (element.minLength != null || element.maxLength != null)) {
|
|
800
|
+
const counter = createCharCounter(element, textareaInput, true);
|
|
801
|
+
textareaContainer.appendChild(counter);
|
|
802
|
+
}
|
|
803
|
+
itemWrapper.appendChild(textareaContainer);
|
|
686
804
|
if (index === -1) {
|
|
687
805
|
container.appendChild(itemWrapper);
|
|
688
806
|
} else {
|
|
@@ -724,30 +842,54 @@ function renderMultipleTextareaElement(element, ctx, wrapper, pathKey) {
|
|
|
724
842
|
removeBtn.style.pointerEvents = disabled ? "none" : "auto";
|
|
725
843
|
});
|
|
726
844
|
}
|
|
845
|
+
let addRow = null;
|
|
846
|
+
let countDisplay = null;
|
|
847
|
+
if (!state.config.readonly) {
|
|
848
|
+
addRow = document.createElement("div");
|
|
849
|
+
addRow.className = "flex items-center gap-3 mt-2";
|
|
850
|
+
const addBtn = document.createElement("button");
|
|
851
|
+
addBtn.type = "button";
|
|
852
|
+
addBtn.className = "add-textarea-btn px-3 py-1 rounded";
|
|
853
|
+
addBtn.style.cssText = `
|
|
854
|
+
color: var(--fb-primary-color);
|
|
855
|
+
border: var(--fb-border-width) solid var(--fb-primary-color);
|
|
856
|
+
background-color: transparent;
|
|
857
|
+
font-size: var(--fb-font-size);
|
|
858
|
+
transition: all var(--fb-transition-duration);
|
|
859
|
+
`;
|
|
860
|
+
addBtn.textContent = "+";
|
|
861
|
+
addBtn.addEventListener("mouseenter", () => {
|
|
862
|
+
addBtn.style.backgroundColor = "var(--fb-background-hover-color)";
|
|
863
|
+
});
|
|
864
|
+
addBtn.addEventListener("mouseleave", () => {
|
|
865
|
+
addBtn.style.backgroundColor = "transparent";
|
|
866
|
+
});
|
|
867
|
+
addBtn.onclick = () => {
|
|
868
|
+
values.push(element.default || "");
|
|
869
|
+
addTextareaItem(element.default || "");
|
|
870
|
+
updateAddButton();
|
|
871
|
+
updateRemoveButtons();
|
|
872
|
+
};
|
|
873
|
+
countDisplay = document.createElement("span");
|
|
874
|
+
countDisplay.className = "text-sm text-gray-500";
|
|
875
|
+
addRow.appendChild(addBtn);
|
|
876
|
+
addRow.appendChild(countDisplay);
|
|
877
|
+
wrapper.appendChild(addRow);
|
|
878
|
+
}
|
|
727
879
|
function updateAddButton() {
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
if (
|
|
731
|
-
const
|
|
732
|
-
addBtn.
|
|
733
|
-
addBtn.
|
|
734
|
-
addBtn.
|
|
735
|
-
addBtn.onclick = () => {
|
|
736
|
-
values.push(element.default || "");
|
|
737
|
-
addTextareaItem(element.default || "");
|
|
738
|
-
updateAddButton();
|
|
739
|
-
updateRemoveButtons();
|
|
740
|
-
};
|
|
741
|
-
wrapper.appendChild(addBtn);
|
|
880
|
+
if (!addRow || !countDisplay) return;
|
|
881
|
+
const addBtn = addRow.querySelector(".add-textarea-btn");
|
|
882
|
+
if (addBtn) {
|
|
883
|
+
const disabled = values.length >= maxCount;
|
|
884
|
+
addBtn.disabled = disabled;
|
|
885
|
+
addBtn.style.opacity = disabled ? "0.5" : "1";
|
|
886
|
+
addBtn.style.pointerEvents = disabled ? "none" : "auto";
|
|
742
887
|
}
|
|
888
|
+
countDisplay.textContent = `${values.length}/${maxCount === Infinity ? "\u221E" : maxCount}`;
|
|
743
889
|
}
|
|
744
890
|
values.forEach((value) => addTextareaItem(value));
|
|
745
891
|
updateAddButton();
|
|
746
892
|
updateRemoveButtons();
|
|
747
|
-
const hint = document.createElement("p");
|
|
748
|
-
hint.className = "text-xs text-gray-500 mt-1";
|
|
749
|
-
hint.textContent = makeFieldHint(element);
|
|
750
|
-
wrapper.appendChild(hint);
|
|
751
893
|
}
|
|
752
894
|
function validateTextareaElement(element, key, context) {
|
|
753
895
|
return validateTextElement(element, key, context);
|
|
@@ -757,11 +899,61 @@ function updateTextareaField(element, fieldPath, value, context) {
|
|
|
757
899
|
}
|
|
758
900
|
|
|
759
901
|
// src/components/number.ts
|
|
902
|
+
function createNumberCounter(element, input) {
|
|
903
|
+
const counter = document.createElement("span");
|
|
904
|
+
counter.className = "number-counter";
|
|
905
|
+
counter.style.cssText = `
|
|
906
|
+
position: absolute;
|
|
907
|
+
top: 50%;
|
|
908
|
+
transform: translateY(-50%);
|
|
909
|
+
right: 10px;
|
|
910
|
+
font-size: var(--fb-font-size-small);
|
|
911
|
+
color: var(--fb-text-secondary-color);
|
|
912
|
+
pointer-events: none;
|
|
913
|
+
background: var(--fb-background-color);
|
|
914
|
+
padding: 0 4px;
|
|
915
|
+
`;
|
|
916
|
+
const updateCounter = () => {
|
|
917
|
+
const val = input.value ? parseFloat(input.value) : null;
|
|
918
|
+
const min = element.min;
|
|
919
|
+
const max = element.max;
|
|
920
|
+
if (min == null && max == null) {
|
|
921
|
+
counter.textContent = "";
|
|
922
|
+
return;
|
|
923
|
+
}
|
|
924
|
+
if (val == null || min != null && val < min) {
|
|
925
|
+
if (min != null && max != null) {
|
|
926
|
+
counter.textContent = `${min}-${max}`;
|
|
927
|
+
} else if (max != null) {
|
|
928
|
+
counter.textContent = `\u2264${max}`;
|
|
929
|
+
} else if (min != null) {
|
|
930
|
+
counter.textContent = `\u2265${min}`;
|
|
931
|
+
}
|
|
932
|
+
counter.style.color = "var(--fb-text-secondary-color)";
|
|
933
|
+
} else if (max != null && val > max) {
|
|
934
|
+
counter.textContent = `${val}/${max}`;
|
|
935
|
+
counter.style.color = "var(--fb-error-color)";
|
|
936
|
+
} else {
|
|
937
|
+
if (max != null) {
|
|
938
|
+
counter.textContent = `${val}/${max}`;
|
|
939
|
+
} else {
|
|
940
|
+
counter.textContent = `${val}`;
|
|
941
|
+
}
|
|
942
|
+
counter.style.color = "var(--fb-text-secondary-color)";
|
|
943
|
+
}
|
|
944
|
+
};
|
|
945
|
+
input.addEventListener("input", updateCounter);
|
|
946
|
+
updateCounter();
|
|
947
|
+
return counter;
|
|
948
|
+
}
|
|
760
949
|
function renderNumberElement(element, ctx, wrapper, pathKey) {
|
|
761
950
|
const state = ctx.state;
|
|
951
|
+
const inputWrapper = document.createElement("div");
|
|
952
|
+
inputWrapper.style.cssText = "position: relative;";
|
|
762
953
|
const numberInput = document.createElement("input");
|
|
763
954
|
numberInput.type = "number";
|
|
764
955
|
numberInput.className = "w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500";
|
|
956
|
+
numberInput.style.cssText = "padding-right: 60px; width: 100%; box-sizing: border-box;";
|
|
765
957
|
numberInput.name = pathKey;
|
|
766
958
|
numberInput.placeholder = element.placeholder || "0";
|
|
767
959
|
if (element.min !== void 0) numberInput.min = element.min.toString();
|
|
@@ -777,11 +969,12 @@ function renderNumberElement(element, ctx, wrapper, pathKey) {
|
|
|
777
969
|
numberInput.addEventListener("blur", handleChange);
|
|
778
970
|
numberInput.addEventListener("input", handleChange);
|
|
779
971
|
}
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
972
|
+
inputWrapper.appendChild(numberInput);
|
|
973
|
+
if (!state.config.readonly && (element.min != null || element.max != null)) {
|
|
974
|
+
const counter = createNumberCounter(element, numberInput);
|
|
975
|
+
inputWrapper.appendChild(counter);
|
|
976
|
+
}
|
|
977
|
+
wrapper.appendChild(inputWrapper);
|
|
785
978
|
}
|
|
786
979
|
function renderMultipleNumberElement(element, ctx, wrapper, pathKey) {
|
|
787
980
|
const state = ctx.state;
|
|
@@ -807,9 +1000,12 @@ function renderMultipleNumberElement(element, ctx, wrapper, pathKey) {
|
|
|
807
1000
|
function addNumberItem(value = "", index = -1) {
|
|
808
1001
|
const itemWrapper = document.createElement("div");
|
|
809
1002
|
itemWrapper.className = "multiple-number-item flex items-center gap-2";
|
|
1003
|
+
const inputContainer = document.createElement("div");
|
|
1004
|
+
inputContainer.style.cssText = "position: relative; flex: 1;";
|
|
810
1005
|
const numberInput = document.createElement("input");
|
|
811
1006
|
numberInput.type = "number";
|
|
812
|
-
numberInput.className = "
|
|
1007
|
+
numberInput.className = "w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500";
|
|
1008
|
+
numberInput.style.cssText = "padding-right: 60px; width: 100%; box-sizing: border-box;";
|
|
813
1009
|
numberInput.placeholder = element.placeholder || "0";
|
|
814
1010
|
if (element.min !== void 0) numberInput.min = element.min.toString();
|
|
815
1011
|
if (element.max !== void 0) numberInput.max = element.max.toString();
|
|
@@ -824,7 +1020,12 @@ function renderMultipleNumberElement(element, ctx, wrapper, pathKey) {
|
|
|
824
1020
|
numberInput.addEventListener("blur", handleChange);
|
|
825
1021
|
numberInput.addEventListener("input", handleChange);
|
|
826
1022
|
}
|
|
827
|
-
|
|
1023
|
+
inputContainer.appendChild(numberInput);
|
|
1024
|
+
if (!state.config.readonly && (element.min != null || element.max != null)) {
|
|
1025
|
+
const counter = createNumberCounter(element, numberInput);
|
|
1026
|
+
inputContainer.appendChild(counter);
|
|
1027
|
+
}
|
|
1028
|
+
itemWrapper.appendChild(inputContainer);
|
|
828
1029
|
if (index === -1) {
|
|
829
1030
|
container.appendChild(itemWrapper);
|
|
830
1031
|
} else {
|
|
@@ -866,30 +1067,54 @@ function renderMultipleNumberElement(element, ctx, wrapper, pathKey) {
|
|
|
866
1067
|
removeBtn.style.pointerEvents = disabled ? "none" : "auto";
|
|
867
1068
|
});
|
|
868
1069
|
}
|
|
1070
|
+
let addRow = null;
|
|
1071
|
+
let countDisplay = null;
|
|
1072
|
+
if (!state.config.readonly) {
|
|
1073
|
+
addRow = document.createElement("div");
|
|
1074
|
+
addRow.className = "flex items-center gap-3 mt-2";
|
|
1075
|
+
const addBtn = document.createElement("button");
|
|
1076
|
+
addBtn.type = "button";
|
|
1077
|
+
addBtn.className = "add-number-btn px-3 py-1 rounded";
|
|
1078
|
+
addBtn.style.cssText = `
|
|
1079
|
+
color: var(--fb-primary-color);
|
|
1080
|
+
border: var(--fb-border-width) solid var(--fb-primary-color);
|
|
1081
|
+
background-color: transparent;
|
|
1082
|
+
font-size: var(--fb-font-size);
|
|
1083
|
+
transition: all var(--fb-transition-duration);
|
|
1084
|
+
`;
|
|
1085
|
+
addBtn.textContent = "+";
|
|
1086
|
+
addBtn.addEventListener("mouseenter", () => {
|
|
1087
|
+
addBtn.style.backgroundColor = "var(--fb-background-hover-color)";
|
|
1088
|
+
});
|
|
1089
|
+
addBtn.addEventListener("mouseleave", () => {
|
|
1090
|
+
addBtn.style.backgroundColor = "transparent";
|
|
1091
|
+
});
|
|
1092
|
+
addBtn.onclick = () => {
|
|
1093
|
+
values.push(element.default || "");
|
|
1094
|
+
addNumberItem(element.default || "");
|
|
1095
|
+
updateAddButton();
|
|
1096
|
+
updateRemoveButtons();
|
|
1097
|
+
};
|
|
1098
|
+
countDisplay = document.createElement("span");
|
|
1099
|
+
countDisplay.className = "text-sm text-gray-500";
|
|
1100
|
+
addRow.appendChild(addBtn);
|
|
1101
|
+
addRow.appendChild(countDisplay);
|
|
1102
|
+
wrapper.appendChild(addRow);
|
|
1103
|
+
}
|
|
869
1104
|
function updateAddButton() {
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
if (
|
|
873
|
-
const
|
|
874
|
-
addBtn.
|
|
875
|
-
addBtn.
|
|
876
|
-
addBtn.
|
|
877
|
-
addBtn.onclick = () => {
|
|
878
|
-
values.push(element.default || "");
|
|
879
|
-
addNumberItem(element.default || "");
|
|
880
|
-
updateAddButton();
|
|
881
|
-
updateRemoveButtons();
|
|
882
|
-
};
|
|
883
|
-
wrapper.appendChild(addBtn);
|
|
1105
|
+
if (!addRow || !countDisplay) return;
|
|
1106
|
+
const addBtn = addRow.querySelector(".add-number-btn");
|
|
1107
|
+
if (addBtn) {
|
|
1108
|
+
const disabled = values.length >= maxCount;
|
|
1109
|
+
addBtn.disabled = disabled;
|
|
1110
|
+
addBtn.style.opacity = disabled ? "0.5" : "1";
|
|
1111
|
+
addBtn.style.pointerEvents = disabled ? "none" : "auto";
|
|
884
1112
|
}
|
|
1113
|
+
countDisplay.textContent = `${values.length}/${maxCount === Infinity ? "\u221E" : maxCount}`;
|
|
885
1114
|
}
|
|
886
1115
|
values.forEach((value) => addNumberItem(value));
|
|
887
1116
|
updateAddButton();
|
|
888
1117
|
updateRemoveButtons();
|
|
889
|
-
const hint = document.createElement("p");
|
|
890
|
-
hint.className = "text-xs text-gray-500 mt-1";
|
|
891
|
-
hint.textContent = makeFieldHint(element);
|
|
892
|
-
wrapper.appendChild(hint);
|
|
893
1118
|
}
|
|
894
1119
|
function validateNumberElement(element, key, context) {
|
|
895
1120
|
const errors = [];
|
|
@@ -928,13 +1153,16 @@ function validateNumberElement(element, key, context) {
|
|
|
928
1153
|
};
|
|
929
1154
|
const validateNumberInput = (input, v, fieldKey) => {
|
|
930
1155
|
let hasError = false;
|
|
1156
|
+
const { state } = context;
|
|
931
1157
|
if (!skipValidation && element.min !== void 0 && element.min !== null && v < element.min) {
|
|
932
|
-
|
|
933
|
-
|
|
1158
|
+
const msg = t("minValue", state, { min: element.min });
|
|
1159
|
+
errors.push(`${fieldKey}: ${msg}`);
|
|
1160
|
+
markValidity(input, msg);
|
|
934
1161
|
hasError = true;
|
|
935
1162
|
} else if (!skipValidation && element.max !== void 0 && element.max !== null && v > element.max) {
|
|
936
|
-
|
|
937
|
-
|
|
1163
|
+
const msg = t("maxValue", state, { max: element.max });
|
|
1164
|
+
errors.push(`${fieldKey}: ${msg}`);
|
|
1165
|
+
markValidity(input, msg);
|
|
938
1166
|
hasError = true;
|
|
939
1167
|
}
|
|
940
1168
|
if (!hasError) {
|
|
@@ -955,8 +1183,9 @@ function validateNumberElement(element, key, context) {
|
|
|
955
1183
|
}
|
|
956
1184
|
const v = parseFloat(raw);
|
|
957
1185
|
if (!skipValidation && !Number.isFinite(v)) {
|
|
958
|
-
|
|
959
|
-
|
|
1186
|
+
const msg = t("notANumber", context.state);
|
|
1187
|
+
errors.push(`${key}[${index}]: ${msg}`);
|
|
1188
|
+
markValidity(input, msg);
|
|
960
1189
|
values.push(null);
|
|
961
1190
|
return;
|
|
962
1191
|
}
|
|
@@ -965,26 +1194,29 @@ function validateNumberElement(element, key, context) {
|
|
|
965
1194
|
values.push(Number(v.toFixed(d)));
|
|
966
1195
|
});
|
|
967
1196
|
if (!skipValidation) {
|
|
1197
|
+
const { state } = context;
|
|
968
1198
|
const minCount = element.minCount ?? 1;
|
|
969
1199
|
const maxCount = element.maxCount ?? Infinity;
|
|
970
1200
|
const filteredValues = values.filter((v) => v !== null);
|
|
971
1201
|
if (element.required && filteredValues.length === 0) {
|
|
972
|
-
errors.push(`${key}: required`);
|
|
1202
|
+
errors.push(`${key}: ${t("required", state)}`);
|
|
973
1203
|
}
|
|
974
1204
|
if (filteredValues.length < minCount) {
|
|
975
|
-
errors.push(`${key}:
|
|
1205
|
+
errors.push(`${key}: ${t("minItems", state, { min: minCount })}`);
|
|
976
1206
|
}
|
|
977
1207
|
if (filteredValues.length > maxCount) {
|
|
978
|
-
errors.push(`${key}:
|
|
1208
|
+
errors.push(`${key}: ${t("maxItems", state, { max: maxCount })}`);
|
|
979
1209
|
}
|
|
980
1210
|
}
|
|
981
1211
|
return { value: values, errors };
|
|
982
1212
|
} else {
|
|
983
1213
|
const input = scopeRoot.querySelector(`[name$="${key}"]`);
|
|
984
1214
|
const raw = input?.value ?? "";
|
|
1215
|
+
const { state } = context;
|
|
985
1216
|
if (!skipValidation && element.required && raw === "") {
|
|
986
|
-
|
|
987
|
-
|
|
1217
|
+
const msg = t("required", state);
|
|
1218
|
+
errors.push(`${key}: ${msg}`);
|
|
1219
|
+
markValidity(input, msg);
|
|
988
1220
|
return { value: null, errors };
|
|
989
1221
|
}
|
|
990
1222
|
if (raw === "") {
|
|
@@ -993,8 +1225,9 @@ function validateNumberElement(element, key, context) {
|
|
|
993
1225
|
}
|
|
994
1226
|
const v = parseFloat(raw);
|
|
995
1227
|
if (!skipValidation && !Number.isFinite(v)) {
|
|
996
|
-
|
|
997
|
-
|
|
1228
|
+
const msg = t("notANumber", state);
|
|
1229
|
+
errors.push(`${key}: ${msg}`);
|
|
1230
|
+
markValidity(input, msg);
|
|
998
1231
|
return { value: null, errors };
|
|
999
1232
|
}
|
|
1000
1233
|
validateNumberInput(input, v, key);
|
|
@@ -1063,7 +1296,7 @@ function renderSelectElement(element, ctx, wrapper, pathKey) {
|
|
|
1063
1296
|
wrapper.appendChild(selectInput);
|
|
1064
1297
|
const selectHint = document.createElement("p");
|
|
1065
1298
|
selectHint.className = "text-xs text-gray-500 mt-1";
|
|
1066
|
-
selectHint.textContent = makeFieldHint(element);
|
|
1299
|
+
selectHint.textContent = makeFieldHint(element, state);
|
|
1067
1300
|
wrapper.appendChild(selectHint);
|
|
1068
1301
|
}
|
|
1069
1302
|
function renderMultipleSelectElement(element, ctx, wrapper, pathKey) {
|
|
@@ -1148,30 +1381,58 @@ function renderMultipleSelectElement(element, ctx, wrapper, pathKey) {
|
|
|
1148
1381
|
removeBtn.style.pointerEvents = disabled ? "none" : "auto";
|
|
1149
1382
|
});
|
|
1150
1383
|
}
|
|
1384
|
+
let addRow = null;
|
|
1385
|
+
let countDisplay = null;
|
|
1386
|
+
if (!state.config.readonly) {
|
|
1387
|
+
addRow = document.createElement("div");
|
|
1388
|
+
addRow.className = "flex items-center gap-3 mt-2";
|
|
1389
|
+
const addBtn = document.createElement("button");
|
|
1390
|
+
addBtn.type = "button";
|
|
1391
|
+
addBtn.className = "add-select-btn px-3 py-1 rounded";
|
|
1392
|
+
addBtn.style.cssText = `
|
|
1393
|
+
color: var(--fb-primary-color);
|
|
1394
|
+
border: var(--fb-border-width) solid var(--fb-primary-color);
|
|
1395
|
+
background-color: transparent;
|
|
1396
|
+
font-size: var(--fb-font-size);
|
|
1397
|
+
transition: all var(--fb-transition-duration);
|
|
1398
|
+
`;
|
|
1399
|
+
addBtn.textContent = "+";
|
|
1400
|
+
addBtn.addEventListener("mouseenter", () => {
|
|
1401
|
+
addBtn.style.backgroundColor = "var(--fb-background-hover-color)";
|
|
1402
|
+
});
|
|
1403
|
+
addBtn.addEventListener("mouseleave", () => {
|
|
1404
|
+
addBtn.style.backgroundColor = "transparent";
|
|
1405
|
+
});
|
|
1406
|
+
addBtn.onclick = () => {
|
|
1407
|
+
const defaultValue = element.default || element.options?.[0]?.value || "";
|
|
1408
|
+
values.push(defaultValue);
|
|
1409
|
+
addSelectItem(defaultValue);
|
|
1410
|
+
updateAddButton();
|
|
1411
|
+
updateRemoveButtons();
|
|
1412
|
+
};
|
|
1413
|
+
countDisplay = document.createElement("span");
|
|
1414
|
+
countDisplay.className = "text-sm text-gray-500";
|
|
1415
|
+
addRow.appendChild(addBtn);
|
|
1416
|
+
addRow.appendChild(countDisplay);
|
|
1417
|
+
wrapper.appendChild(addRow);
|
|
1418
|
+
}
|
|
1151
1419
|
function updateAddButton() {
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
if (
|
|
1155
|
-
const
|
|
1156
|
-
addBtn.
|
|
1157
|
-
addBtn.
|
|
1158
|
-
addBtn.
|
|
1159
|
-
addBtn.onclick = () => {
|
|
1160
|
-
const defaultValue = element.default || element.options?.[0]?.value || "";
|
|
1161
|
-
values.push(defaultValue);
|
|
1162
|
-
addSelectItem(defaultValue);
|
|
1163
|
-
updateAddButton();
|
|
1164
|
-
updateRemoveButtons();
|
|
1165
|
-
};
|
|
1166
|
-
wrapper.appendChild(addBtn);
|
|
1420
|
+
if (!addRow || !countDisplay) return;
|
|
1421
|
+
const addBtn = addRow.querySelector(".add-select-btn");
|
|
1422
|
+
if (addBtn) {
|
|
1423
|
+
const disabled = values.length >= maxCount;
|
|
1424
|
+
addBtn.disabled = disabled;
|
|
1425
|
+
addBtn.style.opacity = disabled ? "0.5" : "1";
|
|
1426
|
+
addBtn.style.pointerEvents = disabled ? "none" : "auto";
|
|
1167
1427
|
}
|
|
1428
|
+
countDisplay.textContent = `${values.length}/${maxCount === Infinity ? "\u221E" : maxCount}`;
|
|
1168
1429
|
}
|
|
1169
1430
|
values.forEach((value) => addSelectItem(value));
|
|
1170
1431
|
updateAddButton();
|
|
1171
1432
|
updateRemoveButtons();
|
|
1172
1433
|
const hint = document.createElement("p");
|
|
1173
1434
|
hint.className = "text-xs text-gray-500 mt-1";
|
|
1174
|
-
hint.textContent = makeFieldHint(element);
|
|
1435
|
+
hint.textContent = makeFieldHint(element, state);
|
|
1175
1436
|
wrapper.appendChild(hint);
|
|
1176
1437
|
}
|
|
1177
1438
|
function validateSelectElement(element, key, context) {
|
|
@@ -1211,17 +1472,18 @@ function validateSelectElement(element, key, context) {
|
|
|
1211
1472
|
};
|
|
1212
1473
|
const validateMultipleCount = (key2, values, element2, filterFn) => {
|
|
1213
1474
|
if (skipValidation) return;
|
|
1475
|
+
const { state } = context;
|
|
1214
1476
|
const filteredValues = values.filter(filterFn);
|
|
1215
1477
|
const minCount = "minCount" in element2 ? element2.minCount ?? 1 : 1;
|
|
1216
1478
|
const maxCount = "maxCount" in element2 ? element2.maxCount ?? Infinity : Infinity;
|
|
1217
1479
|
if (element2.required && filteredValues.length === 0) {
|
|
1218
|
-
errors.push(`${key2}: required`);
|
|
1480
|
+
errors.push(`${key2}: ${t("required", state)}`);
|
|
1219
1481
|
}
|
|
1220
1482
|
if (filteredValues.length < minCount) {
|
|
1221
|
-
errors.push(`${key2}:
|
|
1483
|
+
errors.push(`${key2}: ${t("minItems", state, { min: minCount })}`);
|
|
1222
1484
|
}
|
|
1223
1485
|
if (filteredValues.length > maxCount) {
|
|
1224
|
-
errors.push(`${key2}:
|
|
1486
|
+
errors.push(`${key2}: ${t("maxItems", state, { max: maxCount })}`);
|
|
1225
1487
|
}
|
|
1226
1488
|
};
|
|
1227
1489
|
if ("multiple" in element && element.multiple) {
|
|
@@ -1242,8 +1504,9 @@ function validateSelectElement(element, key, context) {
|
|
|
1242
1504
|
);
|
|
1243
1505
|
const val = input?.value ?? "";
|
|
1244
1506
|
if (!skipValidation && element.required && val === "") {
|
|
1245
|
-
|
|
1246
|
-
|
|
1507
|
+
const msg = t("required", context.state);
|
|
1508
|
+
errors.push(`${key}: ${msg}`);
|
|
1509
|
+
markValidity(input, msg);
|
|
1247
1510
|
return { value: null, errors };
|
|
1248
1511
|
} else {
|
|
1249
1512
|
markValidity(input, null);
|
|
@@ -1295,18 +1558,11 @@ function updateSelectField(element, fieldPath, value, context) {
|
|
|
1295
1558
|
}
|
|
1296
1559
|
}
|
|
1297
1560
|
|
|
1298
|
-
// src/utils/translation.ts
|
|
1299
|
-
function t(key, state) {
|
|
1300
|
-
const locale = state.config.locale || "en";
|
|
1301
|
-
const translations = state.config.translations[locale] || state.config.translations.en;
|
|
1302
|
-
return translations[key] || key;
|
|
1303
|
-
}
|
|
1304
|
-
|
|
1305
1561
|
// src/components/file.ts
|
|
1306
|
-
function renderLocalImagePreview(container, file, fileName) {
|
|
1562
|
+
function renderLocalImagePreview(container, file, fileName, state) {
|
|
1307
1563
|
const img = document.createElement("img");
|
|
1308
1564
|
img.className = "w-full h-full object-contain";
|
|
1309
|
-
img.alt = fileName || "
|
|
1565
|
+
img.alt = fileName || t("previewAlt", state);
|
|
1310
1566
|
const reader = new FileReader();
|
|
1311
1567
|
reader.onload = (e) => {
|
|
1312
1568
|
img.src = e.target?.result || "";
|
|
@@ -1325,14 +1581,14 @@ function renderLocalVideoPreview(container, file, videoType, resourceId, state,
|
|
|
1325
1581
|
<div class="relative group h-full">
|
|
1326
1582
|
<video class="w-full h-full object-contain" controls preload="auto" muted>
|
|
1327
1583
|
<source src="${videoUrl}" type="${videoType}">
|
|
1328
|
-
|
|
1584
|
+
${escapeHtml(t("videoNotSupported", state))}
|
|
1329
1585
|
</video>
|
|
1330
1586
|
<div class="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity z-10 flex gap-1">
|
|
1331
1587
|
<button class="bg-red-600 bg-opacity-75 hover:bg-opacity-90 text-white p-1 rounded text-xs delete-file-btn">
|
|
1332
|
-
${t("removeElement", state)}
|
|
1588
|
+
${escapeHtml(t("removeElement", state))}
|
|
1333
1589
|
</button>
|
|
1334
1590
|
<button class="bg-gray-800 bg-opacity-75 hover:bg-opacity-90 text-white p-1 rounded text-xs change-file-btn">
|
|
1335
|
-
|
|
1591
|
+
${escapeHtml(t("changeButton", state))}
|
|
1336
1592
|
</button>
|
|
1337
1593
|
</div>
|
|
1338
1594
|
</div>
|
|
@@ -1381,11 +1637,11 @@ function handleVideoDelete(container, resourceId, state, deps) {
|
|
|
1381
1637
|
<svg class="w-6 h-6 mb-2" fill="currentColor" viewBox="0 0 24 24">
|
|
1382
1638
|
<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"/>
|
|
1383
1639
|
</svg>
|
|
1384
|
-
<div class="text-sm text-center">${t("clickDragText", state)}</div>
|
|
1640
|
+
<div class="text-sm text-center">${escapeHtml(t("clickDragText", state))}</div>
|
|
1385
1641
|
</div>
|
|
1386
1642
|
`;
|
|
1387
1643
|
}
|
|
1388
|
-
function renderUploadedVideoPreview(container, thumbnailUrl, videoType) {
|
|
1644
|
+
function renderUploadedVideoPreview(container, thumbnailUrl, videoType, state) {
|
|
1389
1645
|
const video = document.createElement("video");
|
|
1390
1646
|
video.className = "w-full h-full object-contain";
|
|
1391
1647
|
video.controls = true;
|
|
@@ -1396,7 +1652,7 @@ function renderUploadedVideoPreview(container, thumbnailUrl, videoType) {
|
|
|
1396
1652
|
source.type = videoType;
|
|
1397
1653
|
video.appendChild(source);
|
|
1398
1654
|
video.appendChild(
|
|
1399
|
-
document.createTextNode("
|
|
1655
|
+
document.createTextNode(t("videoNotSupported", state))
|
|
1400
1656
|
);
|
|
1401
1657
|
container.appendChild(video);
|
|
1402
1658
|
}
|
|
@@ -1414,7 +1670,7 @@ function renderDeleteButton(container, resourceId, state) {
|
|
|
1414
1670
|
<svg class="w-6 h-6 mb-2" fill="currentColor" viewBox="0 0 24 24">
|
|
1415
1671
|
<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"/>
|
|
1416
1672
|
</svg>
|
|
1417
|
-
<div class="text-sm text-center">${t("clickDragText", state)}</div>
|
|
1673
|
+
<div class="text-sm text-center">${escapeHtml(t("clickDragText", state))}</div>
|
|
1418
1674
|
</div>
|
|
1419
1675
|
`;
|
|
1420
1676
|
});
|
|
@@ -1424,7 +1680,7 @@ async function renderLocalFilePreview(container, meta, fileName, resourceId, isR
|
|
|
1424
1680
|
return;
|
|
1425
1681
|
}
|
|
1426
1682
|
if (meta.type && meta.type.startsWith("image/")) {
|
|
1427
|
-
renderLocalImagePreview(container, meta.file, fileName);
|
|
1683
|
+
renderLocalImagePreview(container, meta.file, fileName, state);
|
|
1428
1684
|
} else if (meta.type && meta.type.startsWith("video/")) {
|
|
1429
1685
|
const newContainer = renderLocalVideoPreview(
|
|
1430
1686
|
container,
|
|
@@ -1436,7 +1692,7 @@ async function renderLocalFilePreview(container, meta, fileName, resourceId, isR
|
|
|
1436
1692
|
);
|
|
1437
1693
|
container = newContainer;
|
|
1438
1694
|
} else {
|
|
1439
|
-
container.innerHTML = `<div class="flex flex-col items-center justify-center h-full text-gray-400"><div class="text-2xl mb-2">\u{1F4C1}</div><div class="text-sm">${fileName}</div></div>`;
|
|
1695
|
+
container.innerHTML = `<div class="flex flex-col items-center justify-center h-full text-gray-400"><div class="text-2xl mb-2">\u{1F4C1}</div><div class="text-sm">${escapeHtml(fileName)}</div></div>`;
|
|
1440
1696
|
}
|
|
1441
1697
|
if (!isReadonly && !(meta.type && meta.type.startsWith("video/"))) {
|
|
1442
1698
|
renderDeleteButton(container, resourceId, state);
|
|
@@ -1452,11 +1708,11 @@ async function renderUploadedFilePreview(container, resourceId, fileName, meta,
|
|
|
1452
1708
|
if (thumbnailUrl) {
|
|
1453
1709
|
clear(container);
|
|
1454
1710
|
if (meta && meta.type && meta.type.startsWith("video/")) {
|
|
1455
|
-
renderUploadedVideoPreview(container, thumbnailUrl, meta.type);
|
|
1711
|
+
renderUploadedVideoPreview(container, thumbnailUrl, meta.type, state);
|
|
1456
1712
|
} else {
|
|
1457
1713
|
const img = document.createElement("img");
|
|
1458
1714
|
img.className = "w-full h-full object-contain";
|
|
1459
|
-
img.alt = fileName || "
|
|
1715
|
+
img.alt = fileName || t("previewAlt", state);
|
|
1460
1716
|
img.src = thumbnailUrl;
|
|
1461
1717
|
container.appendChild(img);
|
|
1462
1718
|
}
|
|
@@ -1470,7 +1726,7 @@ async function renderUploadedFilePreview(container, resourceId, fileName, meta,
|
|
|
1470
1726
|
<svg class="w-6 h-6 mb-2" fill="currentColor" viewBox="0 0 24 24">
|
|
1471
1727
|
<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"/>
|
|
1472
1728
|
</svg>
|
|
1473
|
-
<div class="text-sm text-center">${fileName || "
|
|
1729
|
+
<div class="text-sm text-center">${escapeHtml(fileName || t("previewUnavailable", state))}</div>
|
|
1474
1730
|
</div>
|
|
1475
1731
|
`;
|
|
1476
1732
|
}
|
|
@@ -1526,16 +1782,16 @@ async function renderFilePreviewReadonly(resourceId, state, fileName) {
|
|
|
1526
1782
|
try {
|
|
1527
1783
|
const thumbnailUrl = await state.config.getThumbnail(resourceId);
|
|
1528
1784
|
if (thumbnailUrl) {
|
|
1529
|
-
previewContainer.innerHTML = `<img src="${thumbnailUrl}" alt="${actualFileName}" class="w-full h-auto">`;
|
|
1785
|
+
previewContainer.innerHTML = `<img src="${thumbnailUrl}" alt="${escapeHtml(actualFileName)}" class="w-full h-auto">`;
|
|
1530
1786
|
} else {
|
|
1531
|
-
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">${actualFileName}</div></div></div>`;
|
|
1787
|
+
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>`;
|
|
1532
1788
|
}
|
|
1533
1789
|
} catch (error) {
|
|
1534
1790
|
console.warn("getThumbnail failed for", resourceId, error);
|
|
1535
|
-
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">${actualFileName}</div></div></div>`;
|
|
1791
|
+
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>`;
|
|
1536
1792
|
}
|
|
1537
1793
|
} else {
|
|
1538
|
-
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">${actualFileName}</div></div></div>`;
|
|
1794
|
+
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>`;
|
|
1539
1795
|
}
|
|
1540
1796
|
} else if (isVideo) {
|
|
1541
1797
|
if (state.config.getThumbnail) {
|
|
@@ -1546,7 +1802,7 @@ async function renderFilePreviewReadonly(resourceId, state, fileName) {
|
|
|
1546
1802
|
<div class="relative group">
|
|
1547
1803
|
<video class="w-full h-auto" controls preload="auto" muted>
|
|
1548
1804
|
<source src="${videoUrl}" type="${meta?.type || "video/mp4"}">
|
|
1549
|
-
|
|
1805
|
+
${escapeHtml(t("videoNotSupported", state))}
|
|
1550
1806
|
</video>
|
|
1551
1807
|
<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">
|
|
1552
1808
|
<div class="bg-white bg-opacity-90 rounded-full p-3">
|
|
@@ -1558,14 +1814,14 @@ async function renderFilePreviewReadonly(resourceId, state, fileName) {
|
|
|
1558
1814
|
</div>
|
|
1559
1815
|
`;
|
|
1560
1816
|
} else {
|
|
1561
|
-
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{1F3A5}</div><div class="text-sm">${actualFileName}</div></div></div>`;
|
|
1817
|
+
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{1F3A5}</div><div class="text-sm">${escapeHtml(actualFileName)}</div></div></div>`;
|
|
1562
1818
|
}
|
|
1563
1819
|
} catch (error) {
|
|
1564
1820
|
console.warn("getThumbnail failed for video", resourceId, error);
|
|
1565
|
-
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{1F3A5}</div><div class="text-sm">${actualFileName}</div></div></div>`;
|
|
1821
|
+
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{1F3A5}</div><div class="text-sm">${escapeHtml(actualFileName)}</div></div></div>`;
|
|
1566
1822
|
}
|
|
1567
1823
|
} else {
|
|
1568
|
-
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{1F3A5}</div><div class="text-sm">${actualFileName}</div></div></div>`;
|
|
1824
|
+
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{1F3A5}</div><div class="text-sm">${escapeHtml(actualFileName)}</div></div></div>`;
|
|
1569
1825
|
}
|
|
1570
1826
|
} else {
|
|
1571
1827
|
const fileIcon = isPSD ? "\u{1F3A8}" : "\u{1F4C1}";
|
|
@@ -1575,13 +1831,13 @@ async function renderFilePreviewReadonly(resourceId, state, fileName) {
|
|
|
1575
1831
|
<div class="flex items-center space-x-3">
|
|
1576
1832
|
<div class="text-3xl text-gray-400">${fileIcon}</div>
|
|
1577
1833
|
<div class="flex-1 min-w-0">
|
|
1578
|
-
<div class="text-sm font-medium text-gray-900 truncate">${actualFileName}</div>
|
|
1834
|
+
<div class="text-sm font-medium text-gray-900 truncate">${escapeHtml(actualFileName)}</div>
|
|
1579
1835
|
<div class="text-xs text-gray-500">${fileDescription}</div>
|
|
1580
1836
|
</div>
|
|
1581
1837
|
</div>
|
|
1582
1838
|
`;
|
|
1583
1839
|
} else {
|
|
1584
|
-
previewContainer.innerHTML = `<div class="aspect-video flex items-center justify-center text-gray-400"><div class="text-center"><div class="text-4xl mb-2">${fileIcon}</div><div class="text-sm">${actualFileName}</div><div class="text-xs text-gray-500 mt-1">${fileDescription}</div></div></div>`;
|
|
1840
|
+
previewContainer.innerHTML = `<div class="aspect-video flex items-center justify-center text-gray-400"><div class="text-center"><div class="text-4xl mb-2">${fileIcon}</div><div class="text-sm">${escapeHtml(actualFileName)}</div><div class="text-xs text-gray-500 mt-1">${fileDescription}</div></div></div>`;
|
|
1585
1841
|
}
|
|
1586
1842
|
}
|
|
1587
1843
|
const fileNameElement = document.createElement("p");
|
|
@@ -1604,12 +1860,18 @@ async function renderFilePreviewReadonly(resourceId, state, fileName) {
|
|
|
1604
1860
|
fileResult.appendChild(downloadButton);
|
|
1605
1861
|
return fileResult;
|
|
1606
1862
|
}
|
|
1607
|
-
function renderResourcePills(container, rids, state, onRemove) {
|
|
1863
|
+
function renderResourcePills(container, rids, state, onRemove, hint, countInfo) {
|
|
1608
1864
|
clear(container);
|
|
1865
|
+
const buildHintLine = () => {
|
|
1866
|
+
const parts = [t("clickDragTextMultiple", state)];
|
|
1867
|
+
if (hint) parts.push(hint);
|
|
1868
|
+
if (countInfo) parts.push(countInfo);
|
|
1869
|
+
return parts.join(" \u2022 ");
|
|
1870
|
+
};
|
|
1609
1871
|
const isInitialRender = !container.classList.contains("grid");
|
|
1610
1872
|
if ((!rids || rids.length === 0) && isInitialRender) {
|
|
1611
|
-
const
|
|
1612
|
-
|
|
1873
|
+
const gridContainer2 = document.createElement("div");
|
|
1874
|
+
gridContainer2.className = "grid grid-cols-4 gap-3 mb-3";
|
|
1613
1875
|
for (let i = 0; i < 4; i++) {
|
|
1614
1876
|
const slot = document.createElement("div");
|
|
1615
1877
|
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";
|
|
@@ -1640,36 +1902,17 @@ function renderResourcePills(container, rids, state, onRemove) {
|
|
|
1640
1902
|
);
|
|
1641
1903
|
if (fileInput) fileInput.click();
|
|
1642
1904
|
};
|
|
1643
|
-
|
|
1644
|
-
}
|
|
1645
|
-
const
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
uploadLink.onclick = (e) => {
|
|
1651
|
-
e.stopPropagation();
|
|
1652
|
-
let filesWrapper = container.parentElement;
|
|
1653
|
-
while (filesWrapper && !filesWrapper.classList.contains("space-y-2")) {
|
|
1654
|
-
filesWrapper = filesWrapper.parentElement;
|
|
1655
|
-
}
|
|
1656
|
-
if (!filesWrapper && container.classList.contains("space-y-2")) {
|
|
1657
|
-
filesWrapper = container;
|
|
1658
|
-
}
|
|
1659
|
-
const fileInput = filesWrapper?.querySelector(
|
|
1660
|
-
'input[type="file"]'
|
|
1661
|
-
);
|
|
1662
|
-
if (fileInput) fileInput.click();
|
|
1663
|
-
};
|
|
1664
|
-
textContainer.appendChild(uploadLink);
|
|
1665
|
-
textContainer.appendChild(
|
|
1666
|
-
document.createTextNode(` ${t("dragDropText", state)}`)
|
|
1667
|
-
);
|
|
1668
|
-
container.appendChild(gridContainer);
|
|
1669
|
-
container.appendChild(textContainer);
|
|
1905
|
+
gridContainer2.appendChild(slot);
|
|
1906
|
+
}
|
|
1907
|
+
const hintText2 = document.createElement("div");
|
|
1908
|
+
hintText2.className = "text-center text-xs text-gray-500 mt-2";
|
|
1909
|
+
hintText2.textContent = buildHintLine();
|
|
1910
|
+
container.appendChild(gridContainer2);
|
|
1911
|
+
container.appendChild(hintText2);
|
|
1670
1912
|
return;
|
|
1671
1913
|
}
|
|
1672
|
-
|
|
1914
|
+
const gridContainer = document.createElement("div");
|
|
1915
|
+
gridContainer.className = "files-list grid grid-cols-4 gap-3";
|
|
1673
1916
|
const currentImagesCount = rids ? rids.length : 0;
|
|
1674
1917
|
const rowsNeeded = Math.floor(currentImagesCount / 4) + 1;
|
|
1675
1918
|
const slotsNeeded = rowsNeeded * 4;
|
|
@@ -1684,7 +1927,7 @@ function renderResourcePills(container, rids, state, onRemove) {
|
|
|
1684
1927
|
console.error("Failed to render thumbnail:", err);
|
|
1685
1928
|
slot.innerHTML = `<div class="flex flex-col items-center justify-center h-full text-gray-400">
|
|
1686
1929
|
<div class="text-2xl mb-1">\u{1F4C1}</div>
|
|
1687
|
-
<div class="text-xs"
|
|
1930
|
+
<div class="text-xs">${escapeHtml(t("previewError", state))}</div>
|
|
1688
1931
|
</div>`;
|
|
1689
1932
|
});
|
|
1690
1933
|
if (onRemove) {
|
|
@@ -1717,15 +1960,20 @@ function renderResourcePills(container, rids, state, onRemove) {
|
|
|
1717
1960
|
if (fileInput) fileInput.click();
|
|
1718
1961
|
};
|
|
1719
1962
|
}
|
|
1720
|
-
|
|
1963
|
+
gridContainer.appendChild(slot);
|
|
1721
1964
|
}
|
|
1965
|
+
container.appendChild(gridContainer);
|
|
1966
|
+
const hintText = document.createElement("div");
|
|
1967
|
+
hintText.className = "text-center text-xs text-gray-500 mt-2";
|
|
1968
|
+
hintText.textContent = buildHintLine();
|
|
1969
|
+
container.appendChild(hintText);
|
|
1722
1970
|
}
|
|
1723
|
-
function renderThumbnailError(slot, iconSize = "w-12 h-12") {
|
|
1971
|
+
function renderThumbnailError(slot, state, iconSize = "w-12 h-12") {
|
|
1724
1972
|
slot.innerHTML = `<div class="flex flex-col items-center justify-center h-full text-gray-400">
|
|
1725
|
-
<svg class="${iconSize} text-red-400" fill="currentColor" viewBox="0 0 24 24">
|
|
1973
|
+
<svg class="${escapeHtml(iconSize)} text-red-400" fill="currentColor" viewBox="0 0 24 24">
|
|
1726
1974
|
<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"/>
|
|
1727
1975
|
</svg>
|
|
1728
|
-
<div class="text-xs mt-1 text-red-600"
|
|
1976
|
+
<div class="text-xs mt-1 text-red-600">${escapeHtml(t("previewError", state))}</div>
|
|
1729
1977
|
</div>`;
|
|
1730
1978
|
}
|
|
1731
1979
|
async function renderThumbnailForResource(slot, rid, meta, state) {
|
|
@@ -1761,7 +2009,7 @@ async function renderThumbnailForResource(slot, rid, meta, state) {
|
|
|
1761
2009
|
if (state.config.onThumbnailError) {
|
|
1762
2010
|
state.config.onThumbnailError(err, rid);
|
|
1763
2011
|
}
|
|
1764
|
-
renderThumbnailError(slot);
|
|
2012
|
+
renderThumbnailError(slot, state);
|
|
1765
2013
|
}
|
|
1766
2014
|
} else {
|
|
1767
2015
|
slot.innerHTML = `<div class="flex flex-col items-center justify-center h-full text-gray-400">
|
|
@@ -1810,7 +2058,7 @@ async function renderThumbnailForResource(slot, rid, meta, state) {
|
|
|
1810
2058
|
<svg class="w-8 h-8" fill="currentColor" viewBox="0 0 24 24">
|
|
1811
2059
|
<path d="M8 5v14l11-7z"/>
|
|
1812
2060
|
</svg>
|
|
1813
|
-
<div class="text-xs mt-1">${meta?.name || "Video"}</div>
|
|
2061
|
+
<div class="text-xs mt-1">${escapeHtml(meta?.name || "Video")}</div>
|
|
1814
2062
|
</div>`;
|
|
1815
2063
|
}
|
|
1816
2064
|
} catch (error) {
|
|
@@ -1818,30 +2066,32 @@ async function renderThumbnailForResource(slot, rid, meta, state) {
|
|
|
1818
2066
|
if (state.config.onThumbnailError) {
|
|
1819
2067
|
state.config.onThumbnailError(err, rid);
|
|
1820
2068
|
}
|
|
1821
|
-
renderThumbnailError(slot, "w-8 h-8");
|
|
2069
|
+
renderThumbnailError(slot, state, "w-8 h-8");
|
|
1822
2070
|
}
|
|
1823
2071
|
} else {
|
|
1824
2072
|
slot.innerHTML = `<div class="flex flex-col items-center justify-center h-full text-gray-400">
|
|
1825
2073
|
<svg class="w-8 h-8" fill="currentColor" viewBox="0 0 24 24">
|
|
1826
2074
|
<path d="M8 5v14l11-7z"/>
|
|
1827
2075
|
</svg>
|
|
1828
|
-
<div class="text-xs mt-1">${meta?.name || "Video"}</div>
|
|
2076
|
+
<div class="text-xs mt-1">${escapeHtml(meta?.name || "Video")}</div>
|
|
1829
2077
|
</div>`;
|
|
1830
2078
|
}
|
|
1831
2079
|
} else {
|
|
1832
2080
|
slot.innerHTML = `<div class="flex flex-col items-center justify-center h-full text-gray-400">
|
|
1833
2081
|
<div class="text-2xl mb-1">\u{1F4C1}</div>
|
|
1834
|
-
<div class="text-xs">${meta?.name || "File"}</div>
|
|
2082
|
+
<div class="text-xs">${escapeHtml(meta?.name || "File")}</div>
|
|
1835
2083
|
</div>`;
|
|
1836
2084
|
}
|
|
1837
2085
|
}
|
|
1838
|
-
function setEmptyFileContainer(fileContainer, state) {
|
|
2086
|
+
function setEmptyFileContainer(fileContainer, state, hint) {
|
|
2087
|
+
const hintHtml = hint ? `<div class="text-xs text-gray-500 mt-1">${escapeHtml(hint)}</div>` : "";
|
|
1839
2088
|
fileContainer.innerHTML = `
|
|
1840
2089
|
<div class="flex flex-col items-center justify-center h-full text-gray-400">
|
|
1841
2090
|
<svg class="w-6 h-6 mb-2" fill="currentColor" viewBox="0 0 24 24">
|
|
1842
2091
|
<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"/>
|
|
1843
2092
|
</svg>
|
|
1844
|
-
<div class="text-sm text-center">${t("clickDragText", state)}</div>
|
|
2093
|
+
<div class="text-sm text-center">${escapeHtml(t("clickDragText", state))}</div>
|
|
2094
|
+
${hintHtml}
|
|
1845
2095
|
</div>
|
|
1846
2096
|
`;
|
|
1847
2097
|
}
|
|
@@ -2101,13 +2351,13 @@ function renderFileElement(element, ctx, wrapper, pathKey) {
|
|
|
2101
2351
|
console.error("Failed to render file preview:", err);
|
|
2102
2352
|
const emptyState = document.createElement("div");
|
|
2103
2353
|
emptyState.className = "aspect-video bg-gray-100 rounded-lg flex items-center justify-center text-gray-500";
|
|
2104
|
-
emptyState.innerHTML = `<div class="text-center"
|
|
2354
|
+
emptyState.innerHTML = `<div class="text-center">${escapeHtml(t("previewUnavailable", state))}</div>`;
|
|
2105
2355
|
wrapper.appendChild(emptyState);
|
|
2106
2356
|
});
|
|
2107
2357
|
} else {
|
|
2108
2358
|
const emptyState = document.createElement("div");
|
|
2109
2359
|
emptyState.className = "aspect-video bg-gray-100 rounded-lg flex items-center justify-center text-gray-500";
|
|
2110
|
-
emptyState.innerHTML = `<div class="text-center">${t("noFileSelected", state)}</div>`;
|
|
2360
|
+
emptyState.innerHTML = `<div class="text-center">${escapeHtml(t("noFileSelected", state))}</div>`;
|
|
2111
2361
|
wrapper.appendChild(emptyState);
|
|
2112
2362
|
}
|
|
2113
2363
|
} else {
|
|
@@ -2151,7 +2401,8 @@ function renderFileElement(element, ctx, wrapper, pathKey) {
|
|
|
2151
2401
|
}
|
|
2152
2402
|
);
|
|
2153
2403
|
} else {
|
|
2154
|
-
|
|
2404
|
+
const hint = makeFieldHint(element, state);
|
|
2405
|
+
setEmptyFileContainer(fileContainer, state, hint);
|
|
2155
2406
|
}
|
|
2156
2407
|
fileContainer.onclick = fileUploadHandler;
|
|
2157
2408
|
setupDragAndDrop(fileContainer, dragHandler);
|
|
@@ -2170,18 +2421,6 @@ function renderFileElement(element, ctx, wrapper, pathKey) {
|
|
|
2170
2421
|
};
|
|
2171
2422
|
fileWrapper.appendChild(fileContainer);
|
|
2172
2423
|
fileWrapper.appendChild(picker);
|
|
2173
|
-
const uploadText = document.createElement("p");
|
|
2174
|
-
uploadText.className = "text-xs text-gray-600 mt-2 text-center";
|
|
2175
|
-
uploadText.innerHTML = `<span class="underline cursor-pointer">${t("uploadText", state)}</span> ${t("dragDropTextSingle", state)}`;
|
|
2176
|
-
const uploadSpan = uploadText.querySelector("span");
|
|
2177
|
-
if (uploadSpan) {
|
|
2178
|
-
uploadSpan.onclick = () => picker.click();
|
|
2179
|
-
}
|
|
2180
|
-
fileWrapper.appendChild(uploadText);
|
|
2181
|
-
const fileHint = document.createElement("p");
|
|
2182
|
-
fileHint.className = "text-xs text-gray-500 mt-1 text-center";
|
|
2183
|
-
fileHint.textContent = makeFieldHint(element);
|
|
2184
|
-
fileWrapper.appendChild(fileHint);
|
|
2185
2424
|
wrapper.appendChild(fileWrapper);
|
|
2186
2425
|
}
|
|
2187
2426
|
}
|
|
@@ -2200,18 +2439,24 @@ function renderFilesElement(element, ctx, wrapper, pathKey) {
|
|
|
2200
2439
|
});
|
|
2201
2440
|
});
|
|
2202
2441
|
} else {
|
|
2203
|
-
resultsWrapper.innerHTML = `<div class="aspect-video bg-gray-100 rounded-lg flex items-center justify-center text-gray-500"><div class="text-center">${t("noFilesSelected", state)}</div></div>`;
|
|
2442
|
+
resultsWrapper.innerHTML = `<div class="aspect-video bg-gray-100 rounded-lg flex items-center justify-center text-gray-500"><div class="text-center">${escapeHtml(t("noFilesSelected", state))}</div></div>`;
|
|
2204
2443
|
}
|
|
2205
2444
|
wrapper.appendChild(resultsWrapper);
|
|
2206
2445
|
} else {
|
|
2207
2446
|
let updateFilesList2 = function() {
|
|
2208
|
-
renderResourcePills(
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2447
|
+
renderResourcePills(
|
|
2448
|
+
list,
|
|
2449
|
+
initialFiles,
|
|
2450
|
+
state,
|
|
2451
|
+
(ridToRemove) => {
|
|
2452
|
+
const index = initialFiles.indexOf(ridToRemove);
|
|
2453
|
+
if (index > -1) {
|
|
2454
|
+
initialFiles.splice(index, 1);
|
|
2455
|
+
}
|
|
2456
|
+
updateFilesList2();
|
|
2457
|
+
},
|
|
2458
|
+
filesFieldHint
|
|
2459
|
+
);
|
|
2215
2460
|
};
|
|
2216
2461
|
const filesWrapper = document.createElement("div");
|
|
2217
2462
|
filesWrapper.className = "space-y-2";
|
|
@@ -2229,6 +2474,7 @@ function renderFilesElement(element, ctx, wrapper, pathKey) {
|
|
|
2229
2474
|
list.className = "files-list";
|
|
2230
2475
|
const initialFiles = ctx.prefill[element.key] || [];
|
|
2231
2476
|
addPrefillFilesToIndex(initialFiles, state);
|
|
2477
|
+
const filesFieldHint = makeFieldHint(element, state);
|
|
2232
2478
|
updateFilesList2();
|
|
2233
2479
|
setupFilesDropHandler(
|
|
2234
2480
|
filesContainer,
|
|
@@ -2249,10 +2495,6 @@ function renderFilesElement(element, ctx, wrapper, pathKey) {
|
|
|
2249
2495
|
filesContainer.appendChild(list);
|
|
2250
2496
|
filesWrapper.appendChild(filesContainer);
|
|
2251
2497
|
filesWrapper.appendChild(filesPicker);
|
|
2252
|
-
const filesHint = document.createElement("p");
|
|
2253
|
-
filesHint.className = "text-xs text-gray-500 mt-1 text-center";
|
|
2254
|
-
filesHint.textContent = makeFieldHint(element);
|
|
2255
|
-
filesWrapper.appendChild(filesHint);
|
|
2256
2498
|
wrapper.appendChild(filesWrapper);
|
|
2257
2499
|
}
|
|
2258
2500
|
}
|
|
@@ -2273,7 +2515,7 @@ function renderMultipleFileElement(element, ctx, wrapper, pathKey) {
|
|
|
2273
2515
|
});
|
|
2274
2516
|
});
|
|
2275
2517
|
} else {
|
|
2276
|
-
resultsWrapper.innerHTML = `<div class="aspect-video bg-gray-100 rounded-lg flex items-center justify-center text-gray-500"><div class="text-center">${t("noFilesSelected", state)}</div></div>`;
|
|
2518
|
+
resultsWrapper.innerHTML = `<div class="aspect-video bg-gray-100 rounded-lg flex items-center justify-center text-gray-500"><div class="text-center">${escapeHtml(t("noFilesSelected", state))}</div></div>`;
|
|
2277
2519
|
}
|
|
2278
2520
|
wrapper.appendChild(resultsWrapper);
|
|
2279
2521
|
} else {
|
|
@@ -2293,19 +2535,24 @@ function renderMultipleFileElement(element, ctx, wrapper, pathKey) {
|
|
|
2293
2535
|
filesWrapper.appendChild(filesContainer);
|
|
2294
2536
|
const initialFiles = Array.isArray(ctx.prefill[element.key]) ? [...ctx.prefill[element.key]] : [];
|
|
2295
2537
|
addPrefillFilesToIndex(initialFiles, state);
|
|
2538
|
+
const multipleFilesHint = makeFieldHint(element, state);
|
|
2539
|
+
const buildCountInfo = () => {
|
|
2540
|
+
const countText = initialFiles.length === 1 ? t("fileCountSingle", state, { count: initialFiles.length }) : t("fileCountPlural", state, { count: initialFiles.length });
|
|
2541
|
+
const minMaxText = minFiles > 0 || maxFiles < Infinity ? ` ${t("fileCountRange", state, { min: minFiles, max: maxFiles })}` : "";
|
|
2542
|
+
return countText + minMaxText;
|
|
2543
|
+
};
|
|
2296
2544
|
const updateFilesDisplay = () => {
|
|
2297
|
-
renderResourcePills(
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
filesWrapper.appendChild(countInfo);
|
|
2545
|
+
renderResourcePills(
|
|
2546
|
+
filesContainer,
|
|
2547
|
+
initialFiles,
|
|
2548
|
+
state,
|
|
2549
|
+
(index) => {
|
|
2550
|
+
initialFiles.splice(initialFiles.indexOf(index), 1);
|
|
2551
|
+
updateFilesDisplay();
|
|
2552
|
+
},
|
|
2553
|
+
multipleFilesHint,
|
|
2554
|
+
buildCountInfo()
|
|
2555
|
+
);
|
|
2309
2556
|
};
|
|
2310
2557
|
setupFilesDropHandler(
|
|
2311
2558
|
filesContainer,
|
|
@@ -2333,16 +2580,17 @@ function validateFileElement(element, key, context) {
|
|
|
2333
2580
|
const isMultipleField = element.type === "files" || "multiple" in element && Boolean(element.multiple);
|
|
2334
2581
|
const validateFileCount = (key2, resourceIds, element2) => {
|
|
2335
2582
|
if (skipValidation) return;
|
|
2583
|
+
const { state } = context;
|
|
2336
2584
|
const minFiles = "minCount" in element2 ? element2.minCount ?? 0 : 0;
|
|
2337
2585
|
const maxFiles = "maxCount" in element2 ? element2.maxCount ?? Infinity : Infinity;
|
|
2338
2586
|
if (element2.required && resourceIds.length === 0) {
|
|
2339
|
-
errors.push(`${key2}: required`);
|
|
2587
|
+
errors.push(`${key2}: ${t("required", state)}`);
|
|
2340
2588
|
}
|
|
2341
2589
|
if (resourceIds.length < minFiles) {
|
|
2342
|
-
errors.push(`${key2}:
|
|
2590
|
+
errors.push(`${key2}: ${t("minFiles", state, { min: minFiles })}`);
|
|
2343
2591
|
}
|
|
2344
2592
|
if (resourceIds.length > maxFiles) {
|
|
2345
|
-
errors.push(`${key2}:
|
|
2593
|
+
errors.push(`${key2}: ${t("maxFiles", state, { max: maxFiles })}`);
|
|
2346
2594
|
}
|
|
2347
2595
|
};
|
|
2348
2596
|
if (isMultipleField) {
|
|
@@ -2370,7 +2618,7 @@ function validateFileElement(element, key, context) {
|
|
|
2370
2618
|
);
|
|
2371
2619
|
const rid = input?.value ?? "";
|
|
2372
2620
|
if (!skipValidation && element.required && rid === "") {
|
|
2373
|
-
errors.push(`${key}: required`);
|
|
2621
|
+
errors.push(`${key}: ${t("required", context.state)}`);
|
|
2374
2622
|
return { value: null, errors };
|
|
2375
2623
|
}
|
|
2376
2624
|
return { value: rid || null, errors };
|
|
@@ -2617,7 +2865,7 @@ function renderColourElement(element, ctx, wrapper, pathKey) {
|
|
|
2617
2865
|
font-size: var(--fb-font-size-small);
|
|
2618
2866
|
color: var(--fb-text-secondary-color);
|
|
2619
2867
|
`;
|
|
2620
|
-
colourHint.textContent = makeFieldHint(element);
|
|
2868
|
+
colourHint.textContent = makeFieldHint(element, state);
|
|
2621
2869
|
wrapper.appendChild(colourHint);
|
|
2622
2870
|
}
|
|
2623
2871
|
function renderMultipleColourElement(element, ctx, wrapper, pathKey) {
|
|
@@ -2707,36 +2955,51 @@ function renderMultipleColourElement(element, ctx, wrapper, pathKey) {
|
|
|
2707
2955
|
removeBtn.style.pointerEvents = disabled ? "none" : "auto";
|
|
2708
2956
|
});
|
|
2709
2957
|
}
|
|
2958
|
+
let addRow = null;
|
|
2959
|
+
let countDisplay = null;
|
|
2960
|
+
if (!state.config.readonly) {
|
|
2961
|
+
addRow = document.createElement("div");
|
|
2962
|
+
addRow.className = "flex items-center gap-3 mt-2";
|
|
2963
|
+
const addBtn = document.createElement("button");
|
|
2964
|
+
addBtn.type = "button";
|
|
2965
|
+
addBtn.className = "add-colour-btn px-3 py-1 rounded";
|
|
2966
|
+
addBtn.style.cssText = `
|
|
2967
|
+
color: var(--fb-primary-color);
|
|
2968
|
+
border: var(--fb-border-width) solid var(--fb-primary-color);
|
|
2969
|
+
background-color: transparent;
|
|
2970
|
+
font-size: var(--fb-font-size);
|
|
2971
|
+
transition: all var(--fb-transition-duration);
|
|
2972
|
+
`;
|
|
2973
|
+
addBtn.textContent = "+";
|
|
2974
|
+
addBtn.addEventListener("mouseenter", () => {
|
|
2975
|
+
addBtn.style.backgroundColor = "var(--fb-background-hover-color)";
|
|
2976
|
+
});
|
|
2977
|
+
addBtn.addEventListener("mouseleave", () => {
|
|
2978
|
+
addBtn.style.backgroundColor = "transparent";
|
|
2979
|
+
});
|
|
2980
|
+
addBtn.onclick = () => {
|
|
2981
|
+
const defaultColour = element.default || "#000000";
|
|
2982
|
+
values.push(defaultColour);
|
|
2983
|
+
addColourItem(defaultColour);
|
|
2984
|
+
updateAddButton();
|
|
2985
|
+
updateRemoveButtons();
|
|
2986
|
+
};
|
|
2987
|
+
countDisplay = document.createElement("span");
|
|
2988
|
+
countDisplay.className = "text-sm text-gray-500";
|
|
2989
|
+
addRow.appendChild(addBtn);
|
|
2990
|
+
addRow.appendChild(countDisplay);
|
|
2991
|
+
wrapper.appendChild(addRow);
|
|
2992
|
+
}
|
|
2710
2993
|
function updateAddButton() {
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
if (
|
|
2714
|
-
const
|
|
2715
|
-
addBtn.
|
|
2716
|
-
addBtn.
|
|
2717
|
-
addBtn.style.
|
|
2718
|
-
color: var(--fb-primary-color);
|
|
2719
|
-
border: var(--fb-border-width) solid var(--fb-primary-color);
|
|
2720
|
-
background-color: transparent;
|
|
2721
|
-
font-size: var(--fb-font-size);
|
|
2722
|
-
transition: all var(--fb-transition-duration);
|
|
2723
|
-
`;
|
|
2724
|
-
addBtn.textContent = "+";
|
|
2725
|
-
addBtn.addEventListener("mouseenter", () => {
|
|
2726
|
-
addBtn.style.backgroundColor = "var(--fb-background-hover-color)";
|
|
2727
|
-
});
|
|
2728
|
-
addBtn.addEventListener("mouseleave", () => {
|
|
2729
|
-
addBtn.style.backgroundColor = "transparent";
|
|
2730
|
-
});
|
|
2731
|
-
addBtn.onclick = () => {
|
|
2732
|
-
const defaultColour = element.default || "#000000";
|
|
2733
|
-
values.push(defaultColour);
|
|
2734
|
-
addColourItem(defaultColour);
|
|
2735
|
-
updateAddButton();
|
|
2736
|
-
updateRemoveButtons();
|
|
2737
|
-
};
|
|
2738
|
-
wrapper.appendChild(addBtn);
|
|
2994
|
+
if (!addRow || !countDisplay) return;
|
|
2995
|
+
const addBtn = addRow.querySelector(".add-colour-btn");
|
|
2996
|
+
if (addBtn) {
|
|
2997
|
+
const disabled = values.length >= maxCount;
|
|
2998
|
+
addBtn.disabled = disabled;
|
|
2999
|
+
addBtn.style.opacity = disabled ? "0.5" : "1";
|
|
3000
|
+
addBtn.style.pointerEvents = disabled ? "none" : "auto";
|
|
2739
3001
|
}
|
|
3002
|
+
countDisplay.textContent = `${values.length}/${maxCount === Infinity ? "\u221E" : maxCount}`;
|
|
2740
3003
|
}
|
|
2741
3004
|
values.forEach((value) => addColourItem(value));
|
|
2742
3005
|
updateAddButton();
|
|
@@ -2747,7 +3010,7 @@ function renderMultipleColourElement(element, ctx, wrapper, pathKey) {
|
|
|
2747
3010
|
font-size: var(--fb-font-size-small);
|
|
2748
3011
|
color: var(--fb-text-secondary-color);
|
|
2749
3012
|
`;
|
|
2750
|
-
hint.textContent = makeFieldHint(element);
|
|
3013
|
+
hint.textContent = makeFieldHint(element, state);
|
|
2751
3014
|
wrapper.appendChild(hint);
|
|
2752
3015
|
}
|
|
2753
3016
|
function validateColourElement(element, key, context) {
|
|
@@ -2786,10 +3049,12 @@ function validateColourElement(element, key, context) {
|
|
|
2786
3049
|
}
|
|
2787
3050
|
};
|
|
2788
3051
|
const validateColourValue = (input, val, fieldKey) => {
|
|
3052
|
+
const { state } = context;
|
|
2789
3053
|
if (!val) {
|
|
2790
3054
|
if (!skipValidation && element.required) {
|
|
2791
|
-
|
|
2792
|
-
|
|
3055
|
+
const msg = t("required", state);
|
|
3056
|
+
errors.push(`${fieldKey}: ${msg}`);
|
|
3057
|
+
markValidity(input, msg);
|
|
2793
3058
|
return "";
|
|
2794
3059
|
}
|
|
2795
3060
|
markValidity(input, null);
|
|
@@ -2797,8 +3062,9 @@ function validateColourElement(element, key, context) {
|
|
|
2797
3062
|
}
|
|
2798
3063
|
const normalized = normalizeColourValue(val);
|
|
2799
3064
|
if (!skipValidation && !isValidHexColour(normalized)) {
|
|
2800
|
-
|
|
2801
|
-
|
|
3065
|
+
const msg = t("invalidHexColour", state);
|
|
3066
|
+
errors.push(`${fieldKey}: ${msg}`);
|
|
3067
|
+
markValidity(input, msg);
|
|
2802
3068
|
return val;
|
|
2803
3069
|
}
|
|
2804
3070
|
markValidity(input, null);
|
|
@@ -2813,17 +3079,18 @@ function validateColourElement(element, key, context) {
|
|
|
2813
3079
|
values.push(validated);
|
|
2814
3080
|
});
|
|
2815
3081
|
if (!skipValidation) {
|
|
3082
|
+
const { state } = context;
|
|
2816
3083
|
const minCount = element.minCount ?? 1;
|
|
2817
3084
|
const maxCount = element.maxCount ?? Infinity;
|
|
2818
3085
|
const filteredValues = values.filter((v) => v !== "");
|
|
2819
3086
|
if (element.required && filteredValues.length === 0) {
|
|
2820
|
-
errors.push(`${key}: required`);
|
|
3087
|
+
errors.push(`${key}: ${t("required", state)}`);
|
|
2821
3088
|
}
|
|
2822
3089
|
if (filteredValues.length < minCount) {
|
|
2823
|
-
errors.push(`${key}:
|
|
3090
|
+
errors.push(`${key}: ${t("minItems", state, { min: minCount })}`);
|
|
2824
3091
|
}
|
|
2825
3092
|
if (filteredValues.length > maxCount) {
|
|
2826
|
-
errors.push(`${key}:
|
|
3093
|
+
errors.push(`${key}: ${t("maxItems", state, { max: maxCount })}`);
|
|
2827
3094
|
}
|
|
2828
3095
|
}
|
|
2829
3096
|
return { value: values, errors };
|
|
@@ -2833,8 +3100,9 @@ function validateColourElement(element, key, context) {
|
|
|
2833
3100
|
);
|
|
2834
3101
|
const val = hexInput?.value ?? "";
|
|
2835
3102
|
if (!skipValidation && element.required && val === "") {
|
|
2836
|
-
|
|
2837
|
-
|
|
3103
|
+
const msg = t("required", context.state);
|
|
3104
|
+
errors.push(`${key}: ${msg}`);
|
|
3105
|
+
markValidity(hexInput, msg);
|
|
2838
3106
|
return { value: "", errors };
|
|
2839
3107
|
}
|
|
2840
3108
|
const validated = validateColourValue(hexInput, val, key);
|
|
@@ -2926,13 +3194,15 @@ function alignToStep(value, step) {
|
|
|
2926
3194
|
}
|
|
2927
3195
|
function createSliderUI(value, pathKey, element, ctx, readonly) {
|
|
2928
3196
|
const container = document.createElement("div");
|
|
2929
|
-
container.className = "slider-container
|
|
2930
|
-
const
|
|
2931
|
-
|
|
3197
|
+
container.className = "slider-container";
|
|
3198
|
+
const mainRow = document.createElement("div");
|
|
3199
|
+
mainRow.className = "flex items-start gap-3";
|
|
3200
|
+
const sliderSection = document.createElement("div");
|
|
3201
|
+
sliderSection.className = "flex-1";
|
|
2932
3202
|
const slider = document.createElement("input");
|
|
2933
3203
|
slider.type = "range";
|
|
2934
3204
|
slider.name = pathKey;
|
|
2935
|
-
slider.className = "slider-input
|
|
3205
|
+
slider.className = "slider-input w-full";
|
|
2936
3206
|
slider.disabled = readonly;
|
|
2937
3207
|
const scale = element.scale || "linear";
|
|
2938
3208
|
const min = element.min;
|
|
@@ -2970,25 +3240,13 @@ function createSliderUI(value, pathKey, element, ctx, readonly) {
|
|
|
2970
3240
|
cursor: ${readonly ? "not-allowed" : "pointer"};
|
|
2971
3241
|
opacity: ${readonly ? "0.6" : "1"};
|
|
2972
3242
|
`;
|
|
2973
|
-
|
|
2974
|
-
valueDisplay.className = "slider-value";
|
|
2975
|
-
valueDisplay.style.cssText = `
|
|
2976
|
-
min-width: 60px;
|
|
2977
|
-
text-align: right;
|
|
2978
|
-
font-size: var(--fb-font-size);
|
|
2979
|
-
color: var(--fb-text-color);
|
|
2980
|
-
font-family: var(--fb-font-family-mono, monospace);
|
|
2981
|
-
font-weight: 500;
|
|
2982
|
-
`;
|
|
2983
|
-
valueDisplay.textContent = value.toFixed(step < 1 ? 2 : 0);
|
|
2984
|
-
sliderRow.appendChild(slider);
|
|
2985
|
-
sliderRow.appendChild(valueDisplay);
|
|
2986
|
-
container.appendChild(sliderRow);
|
|
3243
|
+
sliderSection.appendChild(slider);
|
|
2987
3244
|
const labelsRow = document.createElement("div");
|
|
2988
3245
|
labelsRow.className = "flex justify-between";
|
|
2989
3246
|
labelsRow.style.cssText = `
|
|
2990
3247
|
font-size: var(--fb-font-size-small);
|
|
2991
3248
|
color: var(--fb-text-secondary-color);
|
|
3249
|
+
margin-top: 4px;
|
|
2992
3250
|
`;
|
|
2993
3251
|
const minLabel = document.createElement("span");
|
|
2994
3252
|
minLabel.textContent = min.toString();
|
|
@@ -2996,7 +3254,22 @@ function createSliderUI(value, pathKey, element, ctx, readonly) {
|
|
|
2996
3254
|
maxLabel.textContent = max.toString();
|
|
2997
3255
|
labelsRow.appendChild(minLabel);
|
|
2998
3256
|
labelsRow.appendChild(maxLabel);
|
|
2999
|
-
|
|
3257
|
+
sliderSection.appendChild(labelsRow);
|
|
3258
|
+
const valueDisplay = document.createElement("span");
|
|
3259
|
+
valueDisplay.className = "slider-value";
|
|
3260
|
+
valueDisplay.style.cssText = `
|
|
3261
|
+
min-width: 60px;
|
|
3262
|
+
text-align: right;
|
|
3263
|
+
font-size: var(--fb-font-size);
|
|
3264
|
+
color: var(--fb-text-color);
|
|
3265
|
+
font-family: var(--fb-font-family-mono, monospace);
|
|
3266
|
+
font-weight: 500;
|
|
3267
|
+
padding-top: 2px;
|
|
3268
|
+
`;
|
|
3269
|
+
valueDisplay.textContent = value.toFixed(step < 1 ? 2 : 0);
|
|
3270
|
+
mainRow.appendChild(sliderSection);
|
|
3271
|
+
mainRow.appendChild(valueDisplay);
|
|
3272
|
+
container.appendChild(mainRow);
|
|
3000
3273
|
if (!readonly) {
|
|
3001
3274
|
const updateValue = () => {
|
|
3002
3275
|
let displayValue;
|
|
@@ -3056,7 +3329,7 @@ function renderSliderElement(element, ctx, wrapper, pathKey) {
|
|
|
3056
3329
|
font-size: var(--fb-font-size-small);
|
|
3057
3330
|
color: var(--fb-text-secondary-color);
|
|
3058
3331
|
`;
|
|
3059
|
-
hint.textContent = makeFieldHint(element);
|
|
3332
|
+
hint.textContent = makeFieldHint(element, state);
|
|
3060
3333
|
wrapper.appendChild(hint);
|
|
3061
3334
|
}
|
|
3062
3335
|
function renderMultipleSliderElement(element, ctx, wrapper, pathKey) {
|
|
@@ -3158,35 +3431,50 @@ function renderMultipleSliderElement(element, ctx, wrapper, pathKey) {
|
|
|
3158
3431
|
removeBtn.style.pointerEvents = disabled ? "none" : "auto";
|
|
3159
3432
|
});
|
|
3160
3433
|
}
|
|
3434
|
+
let addRow = null;
|
|
3435
|
+
let countDisplay = null;
|
|
3436
|
+
if (!state.config.readonly) {
|
|
3437
|
+
addRow = document.createElement("div");
|
|
3438
|
+
addRow.className = "flex items-center gap-3 mt-2";
|
|
3439
|
+
const addBtn = document.createElement("button");
|
|
3440
|
+
addBtn.type = "button";
|
|
3441
|
+
addBtn.className = "add-slider-btn px-3 py-1 rounded";
|
|
3442
|
+
addBtn.style.cssText = `
|
|
3443
|
+
color: var(--fb-primary-color);
|
|
3444
|
+
border: var(--fb-border-width) solid var(--fb-primary-color);
|
|
3445
|
+
background-color: transparent;
|
|
3446
|
+
font-size: var(--fb-font-size);
|
|
3447
|
+
transition: all var(--fb-transition-duration);
|
|
3448
|
+
`;
|
|
3449
|
+
addBtn.textContent = "+";
|
|
3450
|
+
addBtn.addEventListener("mouseenter", () => {
|
|
3451
|
+
addBtn.style.backgroundColor = "var(--fb-background-hover-color)";
|
|
3452
|
+
});
|
|
3453
|
+
addBtn.addEventListener("mouseleave", () => {
|
|
3454
|
+
addBtn.style.backgroundColor = "transparent";
|
|
3455
|
+
});
|
|
3456
|
+
addBtn.onclick = () => {
|
|
3457
|
+
values.push(defaultValue);
|
|
3458
|
+
addSliderItem(defaultValue);
|
|
3459
|
+
updateAddButton();
|
|
3460
|
+
updateRemoveButtons();
|
|
3461
|
+
};
|
|
3462
|
+
countDisplay = document.createElement("span");
|
|
3463
|
+
countDisplay.className = "text-sm text-gray-500";
|
|
3464
|
+
addRow.appendChild(addBtn);
|
|
3465
|
+
addRow.appendChild(countDisplay);
|
|
3466
|
+
wrapper.appendChild(addRow);
|
|
3467
|
+
}
|
|
3161
3468
|
function updateAddButton() {
|
|
3162
|
-
|
|
3163
|
-
|
|
3164
|
-
if (
|
|
3165
|
-
const
|
|
3166
|
-
addBtn.
|
|
3167
|
-
addBtn.
|
|
3168
|
-
addBtn.style.
|
|
3169
|
-
color: var(--fb-primary-color);
|
|
3170
|
-
border: var(--fb-border-width) solid var(--fb-primary-color);
|
|
3171
|
-
background-color: transparent;
|
|
3172
|
-
font-size: var(--fb-font-size);
|
|
3173
|
-
transition: all var(--fb-transition-duration);
|
|
3174
|
-
`;
|
|
3175
|
-
addBtn.textContent = "+";
|
|
3176
|
-
addBtn.addEventListener("mouseenter", () => {
|
|
3177
|
-
addBtn.style.backgroundColor = "var(--fb-background-hover-color)";
|
|
3178
|
-
});
|
|
3179
|
-
addBtn.addEventListener("mouseleave", () => {
|
|
3180
|
-
addBtn.style.backgroundColor = "transparent";
|
|
3181
|
-
});
|
|
3182
|
-
addBtn.onclick = () => {
|
|
3183
|
-
values.push(defaultValue);
|
|
3184
|
-
addSliderItem(defaultValue);
|
|
3185
|
-
updateAddButton();
|
|
3186
|
-
updateRemoveButtons();
|
|
3187
|
-
};
|
|
3188
|
-
wrapper.appendChild(addBtn);
|
|
3469
|
+
if (!addRow || !countDisplay) return;
|
|
3470
|
+
const addBtn = addRow.querySelector(".add-slider-btn");
|
|
3471
|
+
if (addBtn) {
|
|
3472
|
+
const disabled = values.length >= maxCount;
|
|
3473
|
+
addBtn.disabled = disabled;
|
|
3474
|
+
addBtn.style.opacity = disabled ? "0.5" : "1";
|
|
3475
|
+
addBtn.style.pointerEvents = disabled ? "none" : "auto";
|
|
3189
3476
|
}
|
|
3477
|
+
countDisplay.textContent = `${values.length}/${maxCount === Infinity ? "\u221E" : maxCount}`;
|
|
3190
3478
|
}
|
|
3191
3479
|
values.forEach((value) => addSliderItem(value));
|
|
3192
3480
|
updateAddButton();
|
|
@@ -3197,7 +3485,7 @@ function renderMultipleSliderElement(element, ctx, wrapper, pathKey) {
|
|
|
3197
3485
|
font-size: var(--fb-font-size-small);
|
|
3198
3486
|
color: var(--fb-text-secondary-color);
|
|
3199
3487
|
`;
|
|
3200
|
-
hint.textContent = makeFieldHint(element);
|
|
3488
|
+
hint.textContent = makeFieldHint(element, state);
|
|
3201
3489
|
wrapper.appendChild(hint);
|
|
3202
3490
|
}
|
|
3203
3491
|
function validateSliderElement(element, key, context) {
|
|
@@ -3254,11 +3542,13 @@ function validateSliderElement(element, key, context) {
|
|
|
3254
3542
|
}
|
|
3255
3543
|
};
|
|
3256
3544
|
const validateSliderValue = (slider, fieldKey) => {
|
|
3545
|
+
const { state } = context;
|
|
3257
3546
|
const rawValue = slider.value;
|
|
3258
3547
|
if (!rawValue) {
|
|
3259
3548
|
if (!skipValidation && element.required) {
|
|
3260
|
-
|
|
3261
|
-
|
|
3549
|
+
const msg = t("required", state);
|
|
3550
|
+
errors.push(`${fieldKey}: ${msg}`);
|
|
3551
|
+
markValidity(slider, msg);
|
|
3262
3552
|
return null;
|
|
3263
3553
|
}
|
|
3264
3554
|
markValidity(slider, null);
|
|
@@ -3275,13 +3565,15 @@ function validateSliderElement(element, key, context) {
|
|
|
3275
3565
|
}
|
|
3276
3566
|
if (!skipValidation) {
|
|
3277
3567
|
if (value < min) {
|
|
3278
|
-
|
|
3279
|
-
|
|
3568
|
+
const msg = t("minValue", state, { min });
|
|
3569
|
+
errors.push(`${fieldKey}: ${msg}`);
|
|
3570
|
+
markValidity(slider, msg);
|
|
3280
3571
|
return value;
|
|
3281
3572
|
}
|
|
3282
3573
|
if (value > max) {
|
|
3283
|
-
|
|
3284
|
-
|
|
3574
|
+
const msg = t("maxValue", state, { max });
|
|
3575
|
+
errors.push(`${fieldKey}: ${msg}`);
|
|
3576
|
+
markValidity(slider, msg);
|
|
3285
3577
|
return value;
|
|
3286
3578
|
}
|
|
3287
3579
|
}
|
|
@@ -3298,17 +3590,18 @@ function validateSliderElement(element, key, context) {
|
|
|
3298
3590
|
values.push(value);
|
|
3299
3591
|
});
|
|
3300
3592
|
if (!skipValidation) {
|
|
3593
|
+
const { state } = context;
|
|
3301
3594
|
const minCount = element.minCount ?? 1;
|
|
3302
3595
|
const maxCount = element.maxCount ?? Infinity;
|
|
3303
3596
|
const filteredValues = values.filter((v) => v !== null);
|
|
3304
3597
|
if (element.required && filteredValues.length === 0) {
|
|
3305
|
-
errors.push(`${key}: required`);
|
|
3598
|
+
errors.push(`${key}: ${t("required", state)}`);
|
|
3306
3599
|
}
|
|
3307
3600
|
if (filteredValues.length < minCount) {
|
|
3308
|
-
errors.push(`${key}:
|
|
3601
|
+
errors.push(`${key}: ${t("minItems", state, { min: minCount })}`);
|
|
3309
3602
|
}
|
|
3310
3603
|
if (filteredValues.length > maxCount) {
|
|
3311
|
-
errors.push(`${key}:
|
|
3604
|
+
errors.push(`${key}: ${t("maxItems", state, { max: maxCount })}`);
|
|
3312
3605
|
}
|
|
3313
3606
|
}
|
|
3314
3607
|
return { value: values, errors };
|
|
@@ -3318,7 +3611,7 @@ function validateSliderElement(element, key, context) {
|
|
|
3318
3611
|
);
|
|
3319
3612
|
if (!slider) {
|
|
3320
3613
|
if (!skipValidation && element.required) {
|
|
3321
|
-
errors.push(`${key}: required`);
|
|
3614
|
+
errors.push(`${key}: ${t("required", context.state)}`);
|
|
3322
3615
|
}
|
|
3323
3616
|
return { value: null, errors };
|
|
3324
3617
|
}
|
|
@@ -3470,10 +3763,6 @@ function renderSingleContainerElement(element, ctx, wrapper, pathKey) {
|
|
|
3470
3763
|
const containerWrap = document.createElement("div");
|
|
3471
3764
|
containerWrap.className = "border border-gray-200 rounded-lg p-4 bg-gray-50";
|
|
3472
3765
|
containerWrap.setAttribute("data-container", pathKey);
|
|
3473
|
-
const header = document.createElement("div");
|
|
3474
|
-
header.className = "flex justify-between items-center mb-4";
|
|
3475
|
-
const left = document.createElement("div");
|
|
3476
|
-
left.className = "flex-1";
|
|
3477
3766
|
const itemsWrap = document.createElement("div");
|
|
3478
3767
|
const columns = element.columns || 1;
|
|
3479
3768
|
if (columns === 1) {
|
|
@@ -3481,8 +3770,6 @@ function renderSingleContainerElement(element, ctx, wrapper, pathKey) {
|
|
|
3481
3770
|
} else {
|
|
3482
3771
|
itemsWrap.className = `grid grid-cols-${columns} gap-4`;
|
|
3483
3772
|
}
|
|
3484
|
-
containerWrap.appendChild(header);
|
|
3485
|
-
header.appendChild(left);
|
|
3486
3773
|
if (!ctx.state.config.readonly) {
|
|
3487
3774
|
const hintsElement = createPrefillHints(element, pathKey);
|
|
3488
3775
|
if (hintsElement) {
|
|
@@ -3503,21 +3790,16 @@ function renderSingleContainerElement(element, ctx, wrapper, pathKey) {
|
|
|
3503
3790
|
}
|
|
3504
3791
|
});
|
|
3505
3792
|
containerWrap.appendChild(itemsWrap);
|
|
3506
|
-
left.innerHTML = `<span>${element.label || element.key}</span>`;
|
|
3507
3793
|
wrapper.appendChild(containerWrap);
|
|
3508
3794
|
}
|
|
3509
3795
|
function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
|
|
3510
3796
|
const state = ctx.state;
|
|
3511
3797
|
const containerWrap = document.createElement("div");
|
|
3512
3798
|
containerWrap.className = "border border-gray-200 rounded-lg p-4 bg-gray-50";
|
|
3513
|
-
const
|
|
3514
|
-
|
|
3515
|
-
const left = document.createElement("div");
|
|
3516
|
-
left.className = "flex-1";
|
|
3799
|
+
const countDisplay = document.createElement("span");
|
|
3800
|
+
countDisplay.className = "text-sm text-gray-500";
|
|
3517
3801
|
const itemsWrap = document.createElement("div");
|
|
3518
3802
|
itemsWrap.className = "space-y-4";
|
|
3519
|
-
containerWrap.appendChild(header);
|
|
3520
|
-
header.appendChild(left);
|
|
3521
3803
|
if (!ctx.state.config.readonly) {
|
|
3522
3804
|
const hintsElement = createPrefillHints(element, element.key);
|
|
3523
3805
|
if (hintsElement) {
|
|
@@ -3531,7 +3813,7 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
|
|
|
3531
3813
|
const createAddButton = () => {
|
|
3532
3814
|
const add = document.createElement("button");
|
|
3533
3815
|
add.type = "button";
|
|
3534
|
-
add.className = "add-container-btn
|
|
3816
|
+
add.className = "add-container-btn px-3 py-1 rounded";
|
|
3535
3817
|
add.style.cssText = `
|
|
3536
3818
|
color: var(--fb-primary-color);
|
|
3537
3819
|
border: var(--fb-border-width) solid var(--fb-primary-color);
|
|
@@ -3612,7 +3894,7 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
|
|
|
3612
3894
|
existingAddBtn.style.opacity = currentCount >= max ? "0.5" : "1";
|
|
3613
3895
|
existingAddBtn.style.pointerEvents = currentCount >= max ? "none" : "auto";
|
|
3614
3896
|
}
|
|
3615
|
-
|
|
3897
|
+
countDisplay.textContent = `${currentCount}/${max === Infinity ? "\u221E" : max}`;
|
|
3616
3898
|
};
|
|
3617
3899
|
if (pre && Array.isArray(pre)) {
|
|
3618
3900
|
pre.forEach((prefillObj, idx) => {
|
|
@@ -3719,7 +4001,11 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
|
|
|
3719
4001
|
}
|
|
3720
4002
|
containerWrap.appendChild(itemsWrap);
|
|
3721
4003
|
if (!state.config.readonly) {
|
|
3722
|
-
|
|
4004
|
+
const addRow = document.createElement("div");
|
|
4005
|
+
addRow.className = "flex items-center gap-3 mt-2";
|
|
4006
|
+
addRow.appendChild(createAddButton());
|
|
4007
|
+
addRow.appendChild(countDisplay);
|
|
4008
|
+
containerWrap.appendChild(addRow);
|
|
3723
4009
|
}
|
|
3724
4010
|
updateAddButton();
|
|
3725
4011
|
wrapper.appendChild(containerWrap);
|
|
@@ -3744,16 +4030,17 @@ function validateContainerElement(element, key, context) {
|
|
|
3744
4030
|
}
|
|
3745
4031
|
const validateContainerCount = (key2, items, element2) => {
|
|
3746
4032
|
if (skipValidation) return;
|
|
4033
|
+
const { state } = context;
|
|
3747
4034
|
const minItems = "minCount" in element2 ? element2.minCount ?? 0 : 0;
|
|
3748
4035
|
const maxItems = "maxCount" in element2 ? element2.maxCount ?? Infinity : Infinity;
|
|
3749
4036
|
if (element2.required && items.length === 0) {
|
|
3750
|
-
errors.push(`${key2}: required`);
|
|
4037
|
+
errors.push(`${key2}: ${t("required", state)}`);
|
|
3751
4038
|
}
|
|
3752
4039
|
if (items.length < minItems) {
|
|
3753
|
-
errors.push(`${key2}:
|
|
4040
|
+
errors.push(`${key2}: ${t("minItems", state, { min: minItems })}`);
|
|
3754
4041
|
}
|
|
3755
4042
|
if (items.length > maxItems) {
|
|
3756
|
-
errors.push(`${key2}:
|
|
4043
|
+
errors.push(`${key2}: ${t("maxItems", state, { max: maxItems })}`);
|
|
3757
4044
|
}
|
|
3758
4045
|
};
|
|
3759
4046
|
if ("multiple" in element && element.multiple) {
|
|
@@ -4300,7 +4587,7 @@ function dispatchToRenderer(element, ctx, wrapper, pathKey) {
|
|
|
4300
4587
|
default: {
|
|
4301
4588
|
const unsupported = document.createElement("div");
|
|
4302
4589
|
unsupported.className = "text-red-500 text-sm";
|
|
4303
|
-
unsupported.textContent =
|
|
4590
|
+
unsupported.textContent = t("unsupportedFieldType", ctx.state, { type: element.type });
|
|
4304
4591
|
wrapper.appendChild(unsupported);
|
|
4305
4592
|
}
|
|
4306
4593
|
}
|
|
@@ -4344,31 +4631,112 @@ var defaultConfig = {
|
|
|
4344
4631
|
locale: "en",
|
|
4345
4632
|
translations: {
|
|
4346
4633
|
en: {
|
|
4347
|
-
|
|
4634
|
+
// UI texts
|
|
4348
4635
|
removeElement: "Remove",
|
|
4349
|
-
uploadText: "Upload",
|
|
4350
|
-
dragDropText: "or drag and drop files",
|
|
4351
|
-
dragDropTextSingle: "or drag and drop file",
|
|
4352
4636
|
clickDragText: "Click or drag file",
|
|
4637
|
+
clickDragTextMultiple: "Click or drag files",
|
|
4353
4638
|
noFileSelected: "No file selected",
|
|
4354
4639
|
noFilesSelected: "No files selected",
|
|
4355
|
-
downloadButton: "Download"
|
|
4640
|
+
downloadButton: "Download",
|
|
4641
|
+
changeButton: "Change",
|
|
4642
|
+
placeholderText: "Enter text",
|
|
4643
|
+
previewAlt: "Preview",
|
|
4644
|
+
previewUnavailable: "Preview unavailable",
|
|
4645
|
+
previewError: "Preview error",
|
|
4646
|
+
videoNotSupported: "Your browser does not support the video tag.",
|
|
4647
|
+
// Field hints
|
|
4648
|
+
hintLengthRange: "{min}-{max} chars",
|
|
4649
|
+
hintMaxLength: "\u2264{max} chars",
|
|
4650
|
+
hintMinLength: "\u2265{min} chars",
|
|
4651
|
+
hintValueRange: "{min}-{max}",
|
|
4652
|
+
hintMaxValue: "\u2264{max}",
|
|
4653
|
+
hintMinValue: "\u2265{min}",
|
|
4654
|
+
hintMaxSize: "\u2264{size}MB",
|
|
4655
|
+
hintFormats: "{formats}",
|
|
4656
|
+
hintRequired: "Required",
|
|
4657
|
+
hintOptional: "Optional",
|
|
4658
|
+
hintPattern: "Format: {pattern}",
|
|
4659
|
+
fileCountSingle: "{count} file",
|
|
4660
|
+
fileCountPlural: "{count} files",
|
|
4661
|
+
fileCountRange: "({min}-{max})",
|
|
4662
|
+
// Validation errors
|
|
4663
|
+
required: "Required",
|
|
4664
|
+
minItems: "Minimum {min} items required",
|
|
4665
|
+
maxItems: "Maximum {max} items allowed",
|
|
4666
|
+
minLength: "Minimum {min} characters",
|
|
4667
|
+
maxLength: "Maximum {max} characters",
|
|
4668
|
+
minValue: "Must be at least {min}",
|
|
4669
|
+
maxValue: "Must be at most {max}",
|
|
4670
|
+
patternMismatch: "Invalid format",
|
|
4671
|
+
invalidPattern: "Invalid pattern in schema",
|
|
4672
|
+
notANumber: "Must be a number",
|
|
4673
|
+
invalidHexColour: "Invalid hex color",
|
|
4674
|
+
minFiles: "Minimum {min} files required",
|
|
4675
|
+
maxFiles: "Maximum {max} files allowed",
|
|
4676
|
+
unsupportedFieldType: "Unsupported field type: {type}"
|
|
4356
4677
|
},
|
|
4357
4678
|
ru: {
|
|
4358
|
-
|
|
4679
|
+
// UI texts
|
|
4359
4680
|
removeElement: "\u0423\u0434\u0430\u043B\u0438\u0442\u044C",
|
|
4360
|
-
uploadText: "\u0417\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u0435",
|
|
4361
|
-
dragDropText: "\u0438\u043B\u0438 \u043F\u0435\u0440\u0435\u0442\u0430\u0449\u0438\u0442\u0435 \u0444\u0430\u0439\u043B\u044B",
|
|
4362
|
-
dragDropTextSingle: "\u0438\u043B\u0438 \u043F\u0435\u0440\u0435\u0442\u0430\u0449\u0438\u0442\u0435 \u0444\u0430\u0439\u043B",
|
|
4363
4681
|
clickDragText: "\u041D\u0430\u0436\u043C\u0438\u0442\u0435 \u0438\u043B\u0438 \u043F\u0435\u0440\u0435\u0442\u0430\u0449\u0438\u0442\u0435 \u0444\u0430\u0439\u043B",
|
|
4682
|
+
clickDragTextMultiple: "\u041D\u0430\u0436\u043C\u0438\u0442\u0435 \u0438\u043B\u0438 \u043F\u0435\u0440\u0435\u0442\u0430\u0449\u0438\u0442\u0435 \u0444\u0430\u0439\u043B\u044B",
|
|
4364
4683
|
noFileSelected: "\u0424\u0430\u0439\u043B \u043D\u0435 \u0432\u044B\u0431\u0440\u0430\u043D",
|
|
4365
4684
|
noFilesSelected: "\u041D\u0435\u0442 \u0444\u0430\u0439\u043B\u043E\u0432",
|
|
4366
|
-
downloadButton: "\u0421\u043A\u0430\u0447\u0430\u0442\u044C"
|
|
4685
|
+
downloadButton: "\u0421\u043A\u0430\u0447\u0430\u0442\u044C",
|
|
4686
|
+
changeButton: "\u0418\u0437\u043C\u0435\u043D\u0438\u0442\u044C",
|
|
4687
|
+
placeholderText: "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0442\u0435\u043A\u0441\u0442",
|
|
4688
|
+
previewAlt: "\u041F\u0440\u0435\u0434\u043F\u0440\u043E\u0441\u043C\u043E\u0442\u0440",
|
|
4689
|
+
previewUnavailable: "\u041F\u0440\u0435\u0434\u043F\u0440\u043E\u0441\u043C\u043E\u0442\u0440 \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D",
|
|
4690
|
+
previewError: "\u041E\u0448\u0438\u0431\u043A\u0430 \u043F\u0440\u0435\u0434\u043F\u0440\u043E\u0441\u043C\u043E\u0442\u0440\u0430",
|
|
4691
|
+
videoNotSupported: "\u0412\u0430\u0448 \u0431\u0440\u0430\u0443\u0437\u0435\u0440 \u043D\u0435 \u043F\u043E\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442 \u0432\u0438\u0434\u0435\u043E.",
|
|
4692
|
+
// Field hints
|
|
4693
|
+
hintLengthRange: "{min}-{max} \u0441\u0438\u043C\u0432\u043E\u043B\u043E\u0432",
|
|
4694
|
+
hintMaxLength: "\u2264{max} \u0441\u0438\u043C\u0432\u043E\u043B\u043E\u0432",
|
|
4695
|
+
hintMinLength: "\u2265{min} \u0441\u0438\u043C\u0432\u043E\u043B\u043E\u0432",
|
|
4696
|
+
hintValueRange: "{min}-{max}",
|
|
4697
|
+
hintMaxValue: "\u2264{max}",
|
|
4698
|
+
hintMinValue: "\u2265{min}",
|
|
4699
|
+
hintMaxSize: "\u2264{size}\u041C\u0411",
|
|
4700
|
+
hintFormats: "{formats}",
|
|
4701
|
+
hintRequired: "\u041E\u0431\u044F\u0437\u0430\u0442\u0435\u043B\u044C\u043D\u043E\u0435",
|
|
4702
|
+
hintOptional: "\u041D\u0435\u043E\u0431\u044F\u0437\u0430\u0442\u0435\u043B\u044C\u043D\u043E\u0435",
|
|
4703
|
+
hintPattern: "\u0424\u043E\u0440\u043C\u0430\u0442: {pattern}",
|
|
4704
|
+
fileCountSingle: "{count} \u0444\u0430\u0439\u043B",
|
|
4705
|
+
fileCountPlural: "{count} \u0444\u0430\u0439\u043B\u043E\u0432",
|
|
4706
|
+
fileCountRange: "({min}-{max})",
|
|
4707
|
+
// Validation errors
|
|
4708
|
+
required: "\u041E\u0431\u044F\u0437\u0430\u0442\u0435\u043B\u044C\u043D\u043E\u0435 \u043F\u043E\u043B\u0435",
|
|
4709
|
+
minItems: "\u041C\u0438\u043D\u0438\u043C\u0443\u043C {min} \u044D\u043B\u0435\u043C\u0435\u043D\u0442\u043E\u0432",
|
|
4710
|
+
maxItems: "\u041C\u0430\u043A\u0441\u0438\u043C\u0443\u043C {max} \u044D\u043B\u0435\u043C\u0435\u043D\u0442\u043E\u0432",
|
|
4711
|
+
minLength: "\u041C\u0438\u043D\u0438\u043C\u0443\u043C {min} \u0441\u0438\u043C\u0432\u043E\u043B\u043E\u0432",
|
|
4712
|
+
maxLength: "\u041C\u0430\u043A\u0441\u0438\u043C\u0443\u043C {max} \u0441\u0438\u043C\u0432\u043E\u043B\u043E\u0432",
|
|
4713
|
+
minValue: "\u0417\u043D\u0430\u0447\u0435\u043D\u0438\u0435 \u0434\u043E\u043B\u0436\u043D\u043E \u0431\u044B\u0442\u044C \u043D\u0435 \u043C\u0435\u043D\u0435\u0435 {min}",
|
|
4714
|
+
maxValue: "\u0417\u043D\u0430\u0447\u0435\u043D\u0438\u0435 \u0434\u043E\u043B\u0436\u043D\u043E \u0431\u044B\u0442\u044C \u043D\u0435 \u0431\u043E\u043B\u0435\u0435 {max}",
|
|
4715
|
+
patternMismatch: "\u041D\u0435\u0432\u0435\u0440\u043D\u044B\u0439 \u0444\u043E\u0440\u043C\u0430\u0442",
|
|
4716
|
+
invalidPattern: "\u041D\u0435\u0432\u0435\u0440\u043D\u044B\u0439 \u043F\u0430\u0442\u0442\u0435\u0440\u043D \u0432 \u0441\u0445\u0435\u043C\u0435",
|
|
4717
|
+
notANumber: "\u0414\u043E\u043B\u0436\u043D\u043E \u0431\u044B\u0442\u044C \u0447\u0438\u0441\u043B\u043E\u043C",
|
|
4718
|
+
invalidHexColour: "\u041D\u0435\u0432\u0435\u0440\u043D\u044B\u0439 \u0444\u043E\u0440\u043C\u0430\u0442 \u0446\u0432\u0435\u0442\u0430",
|
|
4719
|
+
minFiles: "\u041C\u0438\u043D\u0438\u043C\u0443\u043C {min} \u0444\u0430\u0439\u043B\u043E\u0432",
|
|
4720
|
+
maxFiles: "\u041C\u0430\u043A\u0441\u0438\u043C\u0443\u043C {max} \u0444\u0430\u0439\u043B\u043E\u0432",
|
|
4721
|
+
unsupportedFieldType: "\u041D\u0435\u043F\u043E\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u043C\u044B\u0439 \u0442\u0438\u043F \u043F\u043E\u043B\u044F: {type}"
|
|
4367
4722
|
}
|
|
4368
4723
|
},
|
|
4369
4724
|
theme: {}
|
|
4370
4725
|
};
|
|
4371
4726
|
function createInstanceState(config) {
|
|
4727
|
+
const mergedTranslations = {
|
|
4728
|
+
...defaultConfig.translations
|
|
4729
|
+
};
|
|
4730
|
+
if (config?.translations) {
|
|
4731
|
+
for (const [locale, userTranslations] of Object.entries(
|
|
4732
|
+
config.translations
|
|
4733
|
+
)) {
|
|
4734
|
+
mergedTranslations[locale] = {
|
|
4735
|
+
...defaultConfig.translations[locale] || {},
|
|
4736
|
+
...userTranslations
|
|
4737
|
+
};
|
|
4738
|
+
}
|
|
4739
|
+
}
|
|
4372
4740
|
return {
|
|
4373
4741
|
schema: null,
|
|
4374
4742
|
formRoot: null,
|
|
@@ -4377,7 +4745,8 @@ function createInstanceState(config) {
|
|
|
4377
4745
|
version: "1.0.0",
|
|
4378
4746
|
config: {
|
|
4379
4747
|
...defaultConfig,
|
|
4380
|
-
...config
|
|
4748
|
+
...config,
|
|
4749
|
+
translations: mergedTranslations
|
|
4381
4750
|
},
|
|
4382
4751
|
debounceTimer: null
|
|
4383
4752
|
};
|