@dmitryvim/form-builder 0.2.11 → 0.2.13
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 +150 -110
- package/dist/browser/formbuilder.v0.2.13.min.js +362 -0
- package/dist/cjs/index.cjs +831 -459
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/esm/index.js +830 -457
- package/dist/esm/index.js.map +1 -1
- package/dist/form-builder.js +150 -110
- 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,75 @@
|
|
|
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 addPatternHint(element, parts) {
|
|
37
|
-
if (element.pattern
|
|
38
|
-
parts.push("
|
|
39
|
-
} else if (element.pattern?.includes("\u0410-\u042F")) {
|
|
40
|
-
parts.push("text with punctuation");
|
|
59
|
+
function addPatternHint(element, parts, state) {
|
|
60
|
+
if (element.pattern) {
|
|
61
|
+
parts.push(t("hintPattern", state, { pattern: element.pattern }));
|
|
41
62
|
}
|
|
42
63
|
}
|
|
43
|
-
function makeFieldHint(element) {
|
|
64
|
+
function makeFieldHint(element, state) {
|
|
44
65
|
const parts = [];
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
66
|
+
addLengthHint(element, parts, state);
|
|
67
|
+
if (element.type !== "slider") {
|
|
68
|
+
addRangeHint(element, parts, state);
|
|
69
|
+
}
|
|
70
|
+
addFileSizeHint(element, parts, state);
|
|
71
|
+
addFormatHint(element, parts, state);
|
|
72
|
+
addPatternHint(element, parts, state);
|
|
51
73
|
return parts.join(" \u2022 ");
|
|
52
74
|
}
|
|
53
75
|
function validateSchema(schema) {
|
|
@@ -185,6 +207,11 @@ function validateSchema(schema) {
|
|
|
185
207
|
function isPlainObject(obj) {
|
|
186
208
|
return obj && typeof obj === "object" && obj.constructor === Object;
|
|
187
209
|
}
|
|
210
|
+
function escapeHtml(text) {
|
|
211
|
+
const div = document.createElement("div");
|
|
212
|
+
div.textContent = text;
|
|
213
|
+
return div.innerHTML;
|
|
214
|
+
}
|
|
188
215
|
function pathJoin(base, key) {
|
|
189
216
|
return base ? `${base}.${key}` : key;
|
|
190
217
|
}
|
|
@@ -262,13 +289,62 @@ function deepEqual(a, b) {
|
|
|
262
289
|
}
|
|
263
290
|
|
|
264
291
|
// src/components/text.ts
|
|
292
|
+
function createCharCounter(element, input, isTextarea = false) {
|
|
293
|
+
const counter = document.createElement("span");
|
|
294
|
+
counter.className = "char-counter";
|
|
295
|
+
counter.style.cssText = `
|
|
296
|
+
position: absolute;
|
|
297
|
+
${isTextarea ? "bottom: 8px" : "top: 50%; transform: translateY(-50%)"};
|
|
298
|
+
right: 10px;
|
|
299
|
+
font-size: var(--fb-font-size-small);
|
|
300
|
+
color: var(--fb-text-secondary-color);
|
|
301
|
+
pointer-events: none;
|
|
302
|
+
background: var(--fb-background-color);
|
|
303
|
+
padding: 0 4px;
|
|
304
|
+
`;
|
|
305
|
+
const updateCounter = () => {
|
|
306
|
+
const len = input.value.length;
|
|
307
|
+
const min = element.minLength;
|
|
308
|
+
const max = element.maxLength;
|
|
309
|
+
if (min == null && max == null) {
|
|
310
|
+
counter.textContent = "";
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
if (len === 0 || min != null && len < min) {
|
|
314
|
+
if (min != null && max != null) {
|
|
315
|
+
counter.textContent = `${min}-${max}`;
|
|
316
|
+
} else if (max != null) {
|
|
317
|
+
counter.textContent = `\u2264${max}`;
|
|
318
|
+
} else if (min != null) {
|
|
319
|
+
counter.textContent = `\u2265${min}`;
|
|
320
|
+
}
|
|
321
|
+
counter.style.color = "var(--fb-text-secondary-color)";
|
|
322
|
+
} else if (max != null && len > max) {
|
|
323
|
+
counter.textContent = `${len}/${max}`;
|
|
324
|
+
counter.style.color = "var(--fb-error-color)";
|
|
325
|
+
} else {
|
|
326
|
+
if (max != null) {
|
|
327
|
+
counter.textContent = `${len}/${max}`;
|
|
328
|
+
} else {
|
|
329
|
+
counter.textContent = `${len}`;
|
|
330
|
+
}
|
|
331
|
+
counter.style.color = "var(--fb-text-secondary-color)";
|
|
332
|
+
}
|
|
333
|
+
};
|
|
334
|
+
input.addEventListener("input", updateCounter);
|
|
335
|
+
updateCounter();
|
|
336
|
+
return counter;
|
|
337
|
+
}
|
|
265
338
|
function renderTextElement(element, ctx, wrapper, pathKey) {
|
|
266
339
|
const state = ctx.state;
|
|
340
|
+
const inputWrapper = document.createElement("div");
|
|
341
|
+
inputWrapper.style.cssText = "position: relative;";
|
|
267
342
|
const textInput = document.createElement("input");
|
|
268
343
|
textInput.type = "text";
|
|
269
344
|
textInput.className = "w-full rounded-lg";
|
|
270
345
|
textInput.style.cssText = `
|
|
271
346
|
padding: var(--fb-input-padding-y) var(--fb-input-padding-x);
|
|
347
|
+
padding-right: 60px;
|
|
272
348
|
border: var(--fb-border-width) solid var(--fb-border-color);
|
|
273
349
|
border-radius: var(--fb-border-radius);
|
|
274
350
|
background-color: ${state.config.readonly ? "var(--fb-background-readonly-color)" : "var(--fb-background-color)"};
|
|
@@ -276,6 +352,8 @@ function renderTextElement(element, ctx, wrapper, pathKey) {
|
|
|
276
352
|
font-size: var(--fb-font-size);
|
|
277
353
|
font-family: var(--fb-font-family);
|
|
278
354
|
transition: all var(--fb-transition-duration) ease-in-out;
|
|
355
|
+
width: 100%;
|
|
356
|
+
box-sizing: border-box;
|
|
279
357
|
`;
|
|
280
358
|
textInput.name = pathKey;
|
|
281
359
|
textInput.placeholder = element.placeholder || "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0442\u0435\u043A\u0441\u0442";
|
|
@@ -310,15 +388,12 @@ function renderTextElement(element, ctx, wrapper, pathKey) {
|
|
|
310
388
|
textInput.addEventListener("blur", handleChange);
|
|
311
389
|
textInput.addEventListener("input", handleChange);
|
|
312
390
|
}
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
`;
|
|
320
|
-
textHint.textContent = makeFieldHint(element);
|
|
321
|
-
wrapper.appendChild(textHint);
|
|
391
|
+
inputWrapper.appendChild(textInput);
|
|
392
|
+
if (!state.config.readonly && (element.minLength != null || element.maxLength != null)) {
|
|
393
|
+
const counter = createCharCounter(element, textInput, false);
|
|
394
|
+
inputWrapper.appendChild(counter);
|
|
395
|
+
}
|
|
396
|
+
wrapper.appendChild(inputWrapper);
|
|
322
397
|
}
|
|
323
398
|
function renderMultipleTextElement(element, ctx, wrapper, pathKey) {
|
|
324
399
|
const state = ctx.state;
|
|
@@ -344,11 +419,13 @@ function renderMultipleTextElement(element, ctx, wrapper, pathKey) {
|
|
|
344
419
|
function addTextItem(value = "", index = -1) {
|
|
345
420
|
const itemWrapper = document.createElement("div");
|
|
346
421
|
itemWrapper.className = "multiple-text-item flex items-center gap-2";
|
|
422
|
+
const inputContainer = document.createElement("div");
|
|
423
|
+
inputContainer.style.cssText = "position: relative; flex: 1;";
|
|
347
424
|
const textInput = document.createElement("input");
|
|
348
425
|
textInput.type = "text";
|
|
349
|
-
textInput.className = "flex-1";
|
|
350
426
|
textInput.style.cssText = `
|
|
351
427
|
padding: var(--fb-input-padding-y) var(--fb-input-padding-x);
|
|
428
|
+
padding-right: 60px;
|
|
352
429
|
border: var(--fb-border-width) solid var(--fb-border-color);
|
|
353
430
|
border-radius: var(--fb-border-radius);
|
|
354
431
|
background-color: ${state.config.readonly ? "var(--fb-background-readonly-color)" : "var(--fb-background-color)"};
|
|
@@ -356,8 +433,10 @@ function renderMultipleTextElement(element, ctx, wrapper, pathKey) {
|
|
|
356
433
|
font-size: var(--fb-font-size);
|
|
357
434
|
font-family: var(--fb-font-family);
|
|
358
435
|
transition: all var(--fb-transition-duration) ease-in-out;
|
|
436
|
+
width: 100%;
|
|
437
|
+
box-sizing: border-box;
|
|
359
438
|
`;
|
|
360
|
-
textInput.placeholder = element.placeholder || "
|
|
439
|
+
textInput.placeholder = element.placeholder || t("placeholderText", state);
|
|
361
440
|
textInput.value = value;
|
|
362
441
|
textInput.readOnly = state.config.readonly;
|
|
363
442
|
if (!state.config.readonly) {
|
|
@@ -389,7 +468,12 @@ function renderMultipleTextElement(element, ctx, wrapper, pathKey) {
|
|
|
389
468
|
textInput.addEventListener("blur", handleChange);
|
|
390
469
|
textInput.addEventListener("input", handleChange);
|
|
391
470
|
}
|
|
392
|
-
|
|
471
|
+
inputContainer.appendChild(textInput);
|
|
472
|
+
if (!state.config.readonly && (element.minLength != null || element.maxLength != null)) {
|
|
473
|
+
const counter = createCharCounter(element, textInput, false);
|
|
474
|
+
inputContainer.appendChild(counter);
|
|
475
|
+
}
|
|
476
|
+
itemWrapper.appendChild(inputContainer);
|
|
393
477
|
if (index === -1) {
|
|
394
478
|
container.appendChild(itemWrapper);
|
|
395
479
|
} else {
|
|
@@ -442,47 +526,54 @@ function renderMultipleTextElement(element, ctx, wrapper, pathKey) {
|
|
|
442
526
|
removeBtn.style.pointerEvents = disabled ? "none" : "auto";
|
|
443
527
|
});
|
|
444
528
|
}
|
|
529
|
+
let addRow = null;
|
|
530
|
+
let countDisplay = null;
|
|
531
|
+
if (!state.config.readonly) {
|
|
532
|
+
addRow = document.createElement("div");
|
|
533
|
+
addRow.className = "flex items-center gap-3 mt-2";
|
|
534
|
+
const addBtn = document.createElement("button");
|
|
535
|
+
addBtn.type = "button";
|
|
536
|
+
addBtn.className = "add-text-btn px-3 py-1 rounded";
|
|
537
|
+
addBtn.style.cssText = `
|
|
538
|
+
color: var(--fb-primary-color);
|
|
539
|
+
border: var(--fb-border-width) solid var(--fb-primary-color);
|
|
540
|
+
background-color: transparent;
|
|
541
|
+
font-size: var(--fb-font-size);
|
|
542
|
+
transition: all var(--fb-transition-duration);
|
|
543
|
+
`;
|
|
544
|
+
addBtn.textContent = "+";
|
|
545
|
+
addBtn.addEventListener("mouseenter", () => {
|
|
546
|
+
addBtn.style.backgroundColor = "var(--fb-background-hover-color)";
|
|
547
|
+
});
|
|
548
|
+
addBtn.addEventListener("mouseleave", () => {
|
|
549
|
+
addBtn.style.backgroundColor = "transparent";
|
|
550
|
+
});
|
|
551
|
+
addBtn.onclick = () => {
|
|
552
|
+
values.push(element.default || "");
|
|
553
|
+
addTextItem(element.default || "");
|
|
554
|
+
updateAddButton();
|
|
555
|
+
updateRemoveButtons();
|
|
556
|
+
};
|
|
557
|
+
countDisplay = document.createElement("span");
|
|
558
|
+
countDisplay.className = "text-sm text-gray-500";
|
|
559
|
+
addRow.appendChild(addBtn);
|
|
560
|
+
addRow.appendChild(countDisplay);
|
|
561
|
+
wrapper.appendChild(addRow);
|
|
562
|
+
}
|
|
445
563
|
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);
|
|
564
|
+
if (!addRow || !countDisplay) return;
|
|
565
|
+
const addBtn = addRow.querySelector(".add-text-btn");
|
|
566
|
+
if (addBtn) {
|
|
567
|
+
const disabled = values.length >= maxCount;
|
|
568
|
+
addBtn.disabled = disabled;
|
|
569
|
+
addBtn.style.opacity = disabled ? "0.5" : "1";
|
|
570
|
+
addBtn.style.pointerEvents = disabled ? "none" : "auto";
|
|
473
571
|
}
|
|
572
|
+
countDisplay.textContent = `${values.length}/${maxCount === Infinity ? "\u221E" : maxCount}`;
|
|
474
573
|
}
|
|
475
574
|
values.forEach((value) => addTextItem(value));
|
|
476
575
|
updateAddButton();
|
|
477
576
|
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
577
|
}
|
|
487
578
|
function validateTextElement(element, key, context) {
|
|
488
579
|
const errors = [];
|
|
@@ -521,26 +612,31 @@ function validateTextElement(element, key, context) {
|
|
|
521
612
|
};
|
|
522
613
|
const validateTextInput = (input, val, fieldKey) => {
|
|
523
614
|
let hasError = false;
|
|
615
|
+
const { state } = context;
|
|
524
616
|
if (!skipValidation && val) {
|
|
525
617
|
if (element.minLength !== void 0 && element.minLength !== null && val.length < element.minLength) {
|
|
526
|
-
|
|
527
|
-
|
|
618
|
+
const msg = t("minLength", state, { min: element.minLength });
|
|
619
|
+
errors.push(`${fieldKey}: ${msg}`);
|
|
620
|
+
markValidity(input, msg);
|
|
528
621
|
hasError = true;
|
|
529
622
|
} else if (element.maxLength !== void 0 && element.maxLength !== null && val.length > element.maxLength) {
|
|
530
|
-
|
|
531
|
-
|
|
623
|
+
const msg = t("maxLength", state, { max: element.maxLength });
|
|
624
|
+
errors.push(`${fieldKey}: ${msg}`);
|
|
625
|
+
markValidity(input, msg);
|
|
532
626
|
hasError = true;
|
|
533
627
|
} else if (element.pattern) {
|
|
534
628
|
try {
|
|
535
629
|
const re = new RegExp(element.pattern);
|
|
536
630
|
if (!re.test(val)) {
|
|
537
|
-
|
|
538
|
-
|
|
631
|
+
const msg = t("patternMismatch", state);
|
|
632
|
+
errors.push(`${fieldKey}: ${msg}`);
|
|
633
|
+
markValidity(input, msg);
|
|
539
634
|
hasError = true;
|
|
540
635
|
}
|
|
541
636
|
} catch {
|
|
542
|
-
|
|
543
|
-
|
|
637
|
+
const msg = t("invalidPattern", state);
|
|
638
|
+
errors.push(`${fieldKey}: ${msg}`);
|
|
639
|
+
markValidity(input, msg);
|
|
544
640
|
hasError = true;
|
|
545
641
|
}
|
|
546
642
|
}
|
|
@@ -560,17 +656,18 @@ function validateTextElement(element, key, context) {
|
|
|
560
656
|
validateTextInput(input, val, `${key}[${index}]`);
|
|
561
657
|
});
|
|
562
658
|
if (!skipValidation) {
|
|
659
|
+
const { state } = context;
|
|
563
660
|
const minCount = element.minCount ?? 1;
|
|
564
661
|
const maxCount = element.maxCount ?? Infinity;
|
|
565
662
|
const filteredValues = rawValues.filter((v) => v.trim() !== "");
|
|
566
663
|
if (element.required && filteredValues.length === 0) {
|
|
567
|
-
errors.push(`${key}: required`);
|
|
664
|
+
errors.push(`${key}: ${t("required", state)}`);
|
|
568
665
|
}
|
|
569
666
|
if (filteredValues.length < minCount) {
|
|
570
|
-
errors.push(`${key}:
|
|
667
|
+
errors.push(`${key}: ${t("minItems", state, { min: minCount })}`);
|
|
571
668
|
}
|
|
572
669
|
if (filteredValues.length > maxCount) {
|
|
573
|
-
errors.push(`${key}:
|
|
670
|
+
errors.push(`${key}: ${t("maxItems", state, { max: maxCount })}`);
|
|
574
671
|
}
|
|
575
672
|
}
|
|
576
673
|
return { value: values, errors };
|
|
@@ -578,8 +675,9 @@ function validateTextElement(element, key, context) {
|
|
|
578
675
|
const input = scopeRoot.querySelector(`[name$="${key}"]`);
|
|
579
676
|
const val = input?.value ?? "";
|
|
580
677
|
if (!skipValidation && element.required && val === "") {
|
|
581
|
-
|
|
582
|
-
|
|
678
|
+
const msg = t("required", context.state);
|
|
679
|
+
errors.push(`${key}: ${msg}`);
|
|
680
|
+
markValidity(input, msg);
|
|
583
681
|
return { value: null, errors };
|
|
584
682
|
}
|
|
585
683
|
if (input) {
|
|
@@ -623,8 +721,11 @@ function updateTextField(element, fieldPath, value, context) {
|
|
|
623
721
|
// src/components/textarea.ts
|
|
624
722
|
function renderTextareaElement(element, ctx, wrapper, pathKey) {
|
|
625
723
|
const state = ctx.state;
|
|
724
|
+
const textareaWrapper = document.createElement("div");
|
|
725
|
+
textareaWrapper.style.cssText = "position: relative;";
|
|
626
726
|
const textareaInput = document.createElement("textarea");
|
|
627
727
|
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";
|
|
728
|
+
textareaInput.style.cssText = "padding-bottom: 24px;";
|
|
628
729
|
textareaInput.name = pathKey;
|
|
629
730
|
textareaInput.placeholder = element.placeholder || "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0442\u0435\u043A\u0441\u0442";
|
|
630
731
|
textareaInput.rows = element.rows || 4;
|
|
@@ -638,11 +739,12 @@ function renderTextareaElement(element, ctx, wrapper, pathKey) {
|
|
|
638
739
|
textareaInput.addEventListener("blur", handleChange);
|
|
639
740
|
textareaInput.addEventListener("input", handleChange);
|
|
640
741
|
}
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
742
|
+
textareaWrapper.appendChild(textareaInput);
|
|
743
|
+
if (!state.config.readonly && (element.minLength != null || element.maxLength != null)) {
|
|
744
|
+
const counter = createCharCounter(element, textareaInput, true);
|
|
745
|
+
textareaWrapper.appendChild(counter);
|
|
746
|
+
}
|
|
747
|
+
wrapper.appendChild(textareaWrapper);
|
|
646
748
|
}
|
|
647
749
|
function renderMultipleTextareaElement(element, ctx, wrapper, pathKey) {
|
|
648
750
|
const state = ctx.state;
|
|
@@ -668,9 +770,12 @@ function renderMultipleTextareaElement(element, ctx, wrapper, pathKey) {
|
|
|
668
770
|
function addTextareaItem(value = "", index = -1) {
|
|
669
771
|
const itemWrapper = document.createElement("div");
|
|
670
772
|
itemWrapper.className = "multiple-textarea-item";
|
|
773
|
+
const textareaContainer = document.createElement("div");
|
|
774
|
+
textareaContainer.style.cssText = "position: relative;";
|
|
671
775
|
const textareaInput = document.createElement("textarea");
|
|
672
776
|
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.
|
|
777
|
+
textareaInput.style.cssText = "padding-bottom: 24px;";
|
|
778
|
+
textareaInput.placeholder = element.placeholder || t("placeholderText", state);
|
|
674
779
|
textareaInput.rows = element.rows || 4;
|
|
675
780
|
textareaInput.value = value;
|
|
676
781
|
textareaInput.readOnly = state.config.readonly;
|
|
@@ -682,7 +787,12 @@ function renderMultipleTextareaElement(element, ctx, wrapper, pathKey) {
|
|
|
682
787
|
textareaInput.addEventListener("blur", handleChange);
|
|
683
788
|
textareaInput.addEventListener("input", handleChange);
|
|
684
789
|
}
|
|
685
|
-
|
|
790
|
+
textareaContainer.appendChild(textareaInput);
|
|
791
|
+
if (!state.config.readonly && (element.minLength != null || element.maxLength != null)) {
|
|
792
|
+
const counter = createCharCounter(element, textareaInput, true);
|
|
793
|
+
textareaContainer.appendChild(counter);
|
|
794
|
+
}
|
|
795
|
+
itemWrapper.appendChild(textareaContainer);
|
|
686
796
|
if (index === -1) {
|
|
687
797
|
container.appendChild(itemWrapper);
|
|
688
798
|
} else {
|
|
@@ -724,30 +834,54 @@ function renderMultipleTextareaElement(element, ctx, wrapper, pathKey) {
|
|
|
724
834
|
removeBtn.style.pointerEvents = disabled ? "none" : "auto";
|
|
725
835
|
});
|
|
726
836
|
}
|
|
837
|
+
let addRow = null;
|
|
838
|
+
let countDisplay = null;
|
|
839
|
+
if (!state.config.readonly) {
|
|
840
|
+
addRow = document.createElement("div");
|
|
841
|
+
addRow.className = "flex items-center gap-3 mt-2";
|
|
842
|
+
const addBtn = document.createElement("button");
|
|
843
|
+
addBtn.type = "button";
|
|
844
|
+
addBtn.className = "add-textarea-btn px-3 py-1 rounded";
|
|
845
|
+
addBtn.style.cssText = `
|
|
846
|
+
color: var(--fb-primary-color);
|
|
847
|
+
border: var(--fb-border-width) solid var(--fb-primary-color);
|
|
848
|
+
background-color: transparent;
|
|
849
|
+
font-size: var(--fb-font-size);
|
|
850
|
+
transition: all var(--fb-transition-duration);
|
|
851
|
+
`;
|
|
852
|
+
addBtn.textContent = "+";
|
|
853
|
+
addBtn.addEventListener("mouseenter", () => {
|
|
854
|
+
addBtn.style.backgroundColor = "var(--fb-background-hover-color)";
|
|
855
|
+
});
|
|
856
|
+
addBtn.addEventListener("mouseleave", () => {
|
|
857
|
+
addBtn.style.backgroundColor = "transparent";
|
|
858
|
+
});
|
|
859
|
+
addBtn.onclick = () => {
|
|
860
|
+
values.push(element.default || "");
|
|
861
|
+
addTextareaItem(element.default || "");
|
|
862
|
+
updateAddButton();
|
|
863
|
+
updateRemoveButtons();
|
|
864
|
+
};
|
|
865
|
+
countDisplay = document.createElement("span");
|
|
866
|
+
countDisplay.className = "text-sm text-gray-500";
|
|
867
|
+
addRow.appendChild(addBtn);
|
|
868
|
+
addRow.appendChild(countDisplay);
|
|
869
|
+
wrapper.appendChild(addRow);
|
|
870
|
+
}
|
|
727
871
|
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);
|
|
872
|
+
if (!addRow || !countDisplay) return;
|
|
873
|
+
const addBtn = addRow.querySelector(".add-textarea-btn");
|
|
874
|
+
if (addBtn) {
|
|
875
|
+
const disabled = values.length >= maxCount;
|
|
876
|
+
addBtn.disabled = disabled;
|
|
877
|
+
addBtn.style.opacity = disabled ? "0.5" : "1";
|
|
878
|
+
addBtn.style.pointerEvents = disabled ? "none" : "auto";
|
|
742
879
|
}
|
|
880
|
+
countDisplay.textContent = `${values.length}/${maxCount === Infinity ? "\u221E" : maxCount}`;
|
|
743
881
|
}
|
|
744
882
|
values.forEach((value) => addTextareaItem(value));
|
|
745
883
|
updateAddButton();
|
|
746
884
|
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
885
|
}
|
|
752
886
|
function validateTextareaElement(element, key, context) {
|
|
753
887
|
return validateTextElement(element, key, context);
|
|
@@ -757,11 +891,61 @@ function updateTextareaField(element, fieldPath, value, context) {
|
|
|
757
891
|
}
|
|
758
892
|
|
|
759
893
|
// src/components/number.ts
|
|
894
|
+
function createNumberCounter(element, input) {
|
|
895
|
+
const counter = document.createElement("span");
|
|
896
|
+
counter.className = "number-counter";
|
|
897
|
+
counter.style.cssText = `
|
|
898
|
+
position: absolute;
|
|
899
|
+
top: 50%;
|
|
900
|
+
transform: translateY(-50%);
|
|
901
|
+
right: 10px;
|
|
902
|
+
font-size: var(--fb-font-size-small);
|
|
903
|
+
color: var(--fb-text-secondary-color);
|
|
904
|
+
pointer-events: none;
|
|
905
|
+
background: var(--fb-background-color);
|
|
906
|
+
padding: 0 4px;
|
|
907
|
+
`;
|
|
908
|
+
const updateCounter = () => {
|
|
909
|
+
const val = input.value ? parseFloat(input.value) : null;
|
|
910
|
+
const min = element.min;
|
|
911
|
+
const max = element.max;
|
|
912
|
+
if (min == null && max == null) {
|
|
913
|
+
counter.textContent = "";
|
|
914
|
+
return;
|
|
915
|
+
}
|
|
916
|
+
if (val == null || min != null && val < min) {
|
|
917
|
+
if (min != null && max != null) {
|
|
918
|
+
counter.textContent = `${min}-${max}`;
|
|
919
|
+
} else if (max != null) {
|
|
920
|
+
counter.textContent = `\u2264${max}`;
|
|
921
|
+
} else if (min != null) {
|
|
922
|
+
counter.textContent = `\u2265${min}`;
|
|
923
|
+
}
|
|
924
|
+
counter.style.color = "var(--fb-text-secondary-color)";
|
|
925
|
+
} else if (max != null && val > max) {
|
|
926
|
+
counter.textContent = `${val}/${max}`;
|
|
927
|
+
counter.style.color = "var(--fb-error-color)";
|
|
928
|
+
} else {
|
|
929
|
+
if (max != null) {
|
|
930
|
+
counter.textContent = `${val}/${max}`;
|
|
931
|
+
} else {
|
|
932
|
+
counter.textContent = `${val}`;
|
|
933
|
+
}
|
|
934
|
+
counter.style.color = "var(--fb-text-secondary-color)";
|
|
935
|
+
}
|
|
936
|
+
};
|
|
937
|
+
input.addEventListener("input", updateCounter);
|
|
938
|
+
updateCounter();
|
|
939
|
+
return counter;
|
|
940
|
+
}
|
|
760
941
|
function renderNumberElement(element, ctx, wrapper, pathKey) {
|
|
761
942
|
const state = ctx.state;
|
|
943
|
+
const inputWrapper = document.createElement("div");
|
|
944
|
+
inputWrapper.style.cssText = "position: relative;";
|
|
762
945
|
const numberInput = document.createElement("input");
|
|
763
946
|
numberInput.type = "number";
|
|
764
947
|
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";
|
|
948
|
+
numberInput.style.cssText = "padding-right: 60px; width: 100%; box-sizing: border-box;";
|
|
765
949
|
numberInput.name = pathKey;
|
|
766
950
|
numberInput.placeholder = element.placeholder || "0";
|
|
767
951
|
if (element.min !== void 0) numberInput.min = element.min.toString();
|
|
@@ -777,11 +961,12 @@ function renderNumberElement(element, ctx, wrapper, pathKey) {
|
|
|
777
961
|
numberInput.addEventListener("blur", handleChange);
|
|
778
962
|
numberInput.addEventListener("input", handleChange);
|
|
779
963
|
}
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
964
|
+
inputWrapper.appendChild(numberInput);
|
|
965
|
+
if (!state.config.readonly && (element.min != null || element.max != null)) {
|
|
966
|
+
const counter = createNumberCounter(element, numberInput);
|
|
967
|
+
inputWrapper.appendChild(counter);
|
|
968
|
+
}
|
|
969
|
+
wrapper.appendChild(inputWrapper);
|
|
785
970
|
}
|
|
786
971
|
function renderMultipleNumberElement(element, ctx, wrapper, pathKey) {
|
|
787
972
|
const state = ctx.state;
|
|
@@ -807,9 +992,12 @@ function renderMultipleNumberElement(element, ctx, wrapper, pathKey) {
|
|
|
807
992
|
function addNumberItem(value = "", index = -1) {
|
|
808
993
|
const itemWrapper = document.createElement("div");
|
|
809
994
|
itemWrapper.className = "multiple-number-item flex items-center gap-2";
|
|
995
|
+
const inputContainer = document.createElement("div");
|
|
996
|
+
inputContainer.style.cssText = "position: relative; flex: 1;";
|
|
810
997
|
const numberInput = document.createElement("input");
|
|
811
998
|
numberInput.type = "number";
|
|
812
|
-
numberInput.className = "
|
|
999
|
+
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";
|
|
1000
|
+
numberInput.style.cssText = "padding-right: 60px; width: 100%; box-sizing: border-box;";
|
|
813
1001
|
numberInput.placeholder = element.placeholder || "0";
|
|
814
1002
|
if (element.min !== void 0) numberInput.min = element.min.toString();
|
|
815
1003
|
if (element.max !== void 0) numberInput.max = element.max.toString();
|
|
@@ -824,7 +1012,12 @@ function renderMultipleNumberElement(element, ctx, wrapper, pathKey) {
|
|
|
824
1012
|
numberInput.addEventListener("blur", handleChange);
|
|
825
1013
|
numberInput.addEventListener("input", handleChange);
|
|
826
1014
|
}
|
|
827
|
-
|
|
1015
|
+
inputContainer.appendChild(numberInput);
|
|
1016
|
+
if (!state.config.readonly && (element.min != null || element.max != null)) {
|
|
1017
|
+
const counter = createNumberCounter(element, numberInput);
|
|
1018
|
+
inputContainer.appendChild(counter);
|
|
1019
|
+
}
|
|
1020
|
+
itemWrapper.appendChild(inputContainer);
|
|
828
1021
|
if (index === -1) {
|
|
829
1022
|
container.appendChild(itemWrapper);
|
|
830
1023
|
} else {
|
|
@@ -866,30 +1059,54 @@ function renderMultipleNumberElement(element, ctx, wrapper, pathKey) {
|
|
|
866
1059
|
removeBtn.style.pointerEvents = disabled ? "none" : "auto";
|
|
867
1060
|
});
|
|
868
1061
|
}
|
|
1062
|
+
let addRow = null;
|
|
1063
|
+
let countDisplay = null;
|
|
1064
|
+
if (!state.config.readonly) {
|
|
1065
|
+
addRow = document.createElement("div");
|
|
1066
|
+
addRow.className = "flex items-center gap-3 mt-2";
|
|
1067
|
+
const addBtn = document.createElement("button");
|
|
1068
|
+
addBtn.type = "button";
|
|
1069
|
+
addBtn.className = "add-number-btn px-3 py-1 rounded";
|
|
1070
|
+
addBtn.style.cssText = `
|
|
1071
|
+
color: var(--fb-primary-color);
|
|
1072
|
+
border: var(--fb-border-width) solid var(--fb-primary-color);
|
|
1073
|
+
background-color: transparent;
|
|
1074
|
+
font-size: var(--fb-font-size);
|
|
1075
|
+
transition: all var(--fb-transition-duration);
|
|
1076
|
+
`;
|
|
1077
|
+
addBtn.textContent = "+";
|
|
1078
|
+
addBtn.addEventListener("mouseenter", () => {
|
|
1079
|
+
addBtn.style.backgroundColor = "var(--fb-background-hover-color)";
|
|
1080
|
+
});
|
|
1081
|
+
addBtn.addEventListener("mouseleave", () => {
|
|
1082
|
+
addBtn.style.backgroundColor = "transparent";
|
|
1083
|
+
});
|
|
1084
|
+
addBtn.onclick = () => {
|
|
1085
|
+
values.push(element.default || "");
|
|
1086
|
+
addNumberItem(element.default || "");
|
|
1087
|
+
updateAddButton();
|
|
1088
|
+
updateRemoveButtons();
|
|
1089
|
+
};
|
|
1090
|
+
countDisplay = document.createElement("span");
|
|
1091
|
+
countDisplay.className = "text-sm text-gray-500";
|
|
1092
|
+
addRow.appendChild(addBtn);
|
|
1093
|
+
addRow.appendChild(countDisplay);
|
|
1094
|
+
wrapper.appendChild(addRow);
|
|
1095
|
+
}
|
|
869
1096
|
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);
|
|
1097
|
+
if (!addRow || !countDisplay) return;
|
|
1098
|
+
const addBtn = addRow.querySelector(".add-number-btn");
|
|
1099
|
+
if (addBtn) {
|
|
1100
|
+
const disabled = values.length >= maxCount;
|
|
1101
|
+
addBtn.disabled = disabled;
|
|
1102
|
+
addBtn.style.opacity = disabled ? "0.5" : "1";
|
|
1103
|
+
addBtn.style.pointerEvents = disabled ? "none" : "auto";
|
|
884
1104
|
}
|
|
1105
|
+
countDisplay.textContent = `${values.length}/${maxCount === Infinity ? "\u221E" : maxCount}`;
|
|
885
1106
|
}
|
|
886
1107
|
values.forEach((value) => addNumberItem(value));
|
|
887
1108
|
updateAddButton();
|
|
888
1109
|
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
1110
|
}
|
|
894
1111
|
function validateNumberElement(element, key, context) {
|
|
895
1112
|
const errors = [];
|
|
@@ -928,13 +1145,16 @@ function validateNumberElement(element, key, context) {
|
|
|
928
1145
|
};
|
|
929
1146
|
const validateNumberInput = (input, v, fieldKey) => {
|
|
930
1147
|
let hasError = false;
|
|
1148
|
+
const { state } = context;
|
|
931
1149
|
if (!skipValidation && element.min !== void 0 && element.min !== null && v < element.min) {
|
|
932
|
-
|
|
933
|
-
|
|
1150
|
+
const msg = t("minValue", state, { min: element.min });
|
|
1151
|
+
errors.push(`${fieldKey}: ${msg}`);
|
|
1152
|
+
markValidity(input, msg);
|
|
934
1153
|
hasError = true;
|
|
935
1154
|
} else if (!skipValidation && element.max !== void 0 && element.max !== null && v > element.max) {
|
|
936
|
-
|
|
937
|
-
|
|
1155
|
+
const msg = t("maxValue", state, { max: element.max });
|
|
1156
|
+
errors.push(`${fieldKey}: ${msg}`);
|
|
1157
|
+
markValidity(input, msg);
|
|
938
1158
|
hasError = true;
|
|
939
1159
|
}
|
|
940
1160
|
if (!hasError) {
|
|
@@ -955,8 +1175,9 @@ function validateNumberElement(element, key, context) {
|
|
|
955
1175
|
}
|
|
956
1176
|
const v = parseFloat(raw);
|
|
957
1177
|
if (!skipValidation && !Number.isFinite(v)) {
|
|
958
|
-
|
|
959
|
-
|
|
1178
|
+
const msg = t("notANumber", context.state);
|
|
1179
|
+
errors.push(`${key}[${index}]: ${msg}`);
|
|
1180
|
+
markValidity(input, msg);
|
|
960
1181
|
values.push(null);
|
|
961
1182
|
return;
|
|
962
1183
|
}
|
|
@@ -965,26 +1186,29 @@ function validateNumberElement(element, key, context) {
|
|
|
965
1186
|
values.push(Number(v.toFixed(d)));
|
|
966
1187
|
});
|
|
967
1188
|
if (!skipValidation) {
|
|
1189
|
+
const { state } = context;
|
|
968
1190
|
const minCount = element.minCount ?? 1;
|
|
969
1191
|
const maxCount = element.maxCount ?? Infinity;
|
|
970
1192
|
const filteredValues = values.filter((v) => v !== null);
|
|
971
1193
|
if (element.required && filteredValues.length === 0) {
|
|
972
|
-
errors.push(`${key}: required`);
|
|
1194
|
+
errors.push(`${key}: ${t("required", state)}`);
|
|
973
1195
|
}
|
|
974
1196
|
if (filteredValues.length < minCount) {
|
|
975
|
-
errors.push(`${key}:
|
|
1197
|
+
errors.push(`${key}: ${t("minItems", state, { min: minCount })}`);
|
|
976
1198
|
}
|
|
977
1199
|
if (filteredValues.length > maxCount) {
|
|
978
|
-
errors.push(`${key}:
|
|
1200
|
+
errors.push(`${key}: ${t("maxItems", state, { max: maxCount })}`);
|
|
979
1201
|
}
|
|
980
1202
|
}
|
|
981
1203
|
return { value: values, errors };
|
|
982
1204
|
} else {
|
|
983
1205
|
const input = scopeRoot.querySelector(`[name$="${key}"]`);
|
|
984
1206
|
const raw = input?.value ?? "";
|
|
1207
|
+
const { state } = context;
|
|
985
1208
|
if (!skipValidation && element.required && raw === "") {
|
|
986
|
-
|
|
987
|
-
|
|
1209
|
+
const msg = t("required", state);
|
|
1210
|
+
errors.push(`${key}: ${msg}`);
|
|
1211
|
+
markValidity(input, msg);
|
|
988
1212
|
return { value: null, errors };
|
|
989
1213
|
}
|
|
990
1214
|
if (raw === "") {
|
|
@@ -993,8 +1217,9 @@ function validateNumberElement(element, key, context) {
|
|
|
993
1217
|
}
|
|
994
1218
|
const v = parseFloat(raw);
|
|
995
1219
|
if (!skipValidation && !Number.isFinite(v)) {
|
|
996
|
-
|
|
997
|
-
|
|
1220
|
+
const msg = t("notANumber", state);
|
|
1221
|
+
errors.push(`${key}: ${msg}`);
|
|
1222
|
+
markValidity(input, msg);
|
|
998
1223
|
return { value: null, errors };
|
|
999
1224
|
}
|
|
1000
1225
|
validateNumberInput(input, v, key);
|
|
@@ -1061,10 +1286,12 @@ function renderSelectElement(element, ctx, wrapper, pathKey) {
|
|
|
1061
1286
|
selectInput.addEventListener("change", handleChange);
|
|
1062
1287
|
}
|
|
1063
1288
|
wrapper.appendChild(selectInput);
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1289
|
+
if (!state.config.readonly) {
|
|
1290
|
+
const selectHint = document.createElement("p");
|
|
1291
|
+
selectHint.className = "text-xs text-gray-500 mt-1";
|
|
1292
|
+
selectHint.textContent = makeFieldHint(element, state);
|
|
1293
|
+
wrapper.appendChild(selectHint);
|
|
1294
|
+
}
|
|
1068
1295
|
}
|
|
1069
1296
|
function renderMultipleSelectElement(element, ctx, wrapper, pathKey) {
|
|
1070
1297
|
const state = ctx.state;
|
|
@@ -1148,31 +1375,61 @@ function renderMultipleSelectElement(element, ctx, wrapper, pathKey) {
|
|
|
1148
1375
|
removeBtn.style.pointerEvents = disabled ? "none" : "auto";
|
|
1149
1376
|
});
|
|
1150
1377
|
}
|
|
1378
|
+
let addRow = null;
|
|
1379
|
+
let countDisplay = null;
|
|
1380
|
+
if (!state.config.readonly) {
|
|
1381
|
+
addRow = document.createElement("div");
|
|
1382
|
+
addRow.className = "flex items-center gap-3 mt-2";
|
|
1383
|
+
const addBtn = document.createElement("button");
|
|
1384
|
+
addBtn.type = "button";
|
|
1385
|
+
addBtn.className = "add-select-btn px-3 py-1 rounded";
|
|
1386
|
+
addBtn.style.cssText = `
|
|
1387
|
+
color: var(--fb-primary-color);
|
|
1388
|
+
border: var(--fb-border-width) solid var(--fb-primary-color);
|
|
1389
|
+
background-color: transparent;
|
|
1390
|
+
font-size: var(--fb-font-size);
|
|
1391
|
+
transition: all var(--fb-transition-duration);
|
|
1392
|
+
`;
|
|
1393
|
+
addBtn.textContent = "+";
|
|
1394
|
+
addBtn.addEventListener("mouseenter", () => {
|
|
1395
|
+
addBtn.style.backgroundColor = "var(--fb-background-hover-color)";
|
|
1396
|
+
});
|
|
1397
|
+
addBtn.addEventListener("mouseleave", () => {
|
|
1398
|
+
addBtn.style.backgroundColor = "transparent";
|
|
1399
|
+
});
|
|
1400
|
+
addBtn.onclick = () => {
|
|
1401
|
+
const defaultValue = element.default || element.options?.[0]?.value || "";
|
|
1402
|
+
values.push(defaultValue);
|
|
1403
|
+
addSelectItem(defaultValue);
|
|
1404
|
+
updateAddButton();
|
|
1405
|
+
updateRemoveButtons();
|
|
1406
|
+
};
|
|
1407
|
+
countDisplay = document.createElement("span");
|
|
1408
|
+
countDisplay.className = "text-sm text-gray-500";
|
|
1409
|
+
addRow.appendChild(addBtn);
|
|
1410
|
+
addRow.appendChild(countDisplay);
|
|
1411
|
+
wrapper.appendChild(addRow);
|
|
1412
|
+
}
|
|
1151
1413
|
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);
|
|
1414
|
+
if (!addRow || !countDisplay) return;
|
|
1415
|
+
const addBtn = addRow.querySelector(".add-select-btn");
|
|
1416
|
+
if (addBtn) {
|
|
1417
|
+
const disabled = values.length >= maxCount;
|
|
1418
|
+
addBtn.disabled = disabled;
|
|
1419
|
+
addBtn.style.opacity = disabled ? "0.5" : "1";
|
|
1420
|
+
addBtn.style.pointerEvents = disabled ? "none" : "auto";
|
|
1167
1421
|
}
|
|
1422
|
+
countDisplay.textContent = `${values.length}/${maxCount === Infinity ? "\u221E" : maxCount}`;
|
|
1168
1423
|
}
|
|
1169
1424
|
values.forEach((value) => addSelectItem(value));
|
|
1170
1425
|
updateAddButton();
|
|
1171
1426
|
updateRemoveButtons();
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1427
|
+
if (!state.config.readonly) {
|
|
1428
|
+
const hint = document.createElement("p");
|
|
1429
|
+
hint.className = "text-xs text-gray-500 mt-1";
|
|
1430
|
+
hint.textContent = makeFieldHint(element, state);
|
|
1431
|
+
wrapper.appendChild(hint);
|
|
1432
|
+
}
|
|
1176
1433
|
}
|
|
1177
1434
|
function validateSelectElement(element, key, context) {
|
|
1178
1435
|
const errors = [];
|
|
@@ -1211,17 +1468,18 @@ function validateSelectElement(element, key, context) {
|
|
|
1211
1468
|
};
|
|
1212
1469
|
const validateMultipleCount = (key2, values, element2, filterFn) => {
|
|
1213
1470
|
if (skipValidation) return;
|
|
1471
|
+
const { state } = context;
|
|
1214
1472
|
const filteredValues = values.filter(filterFn);
|
|
1215
1473
|
const minCount = "minCount" in element2 ? element2.minCount ?? 1 : 1;
|
|
1216
1474
|
const maxCount = "maxCount" in element2 ? element2.maxCount ?? Infinity : Infinity;
|
|
1217
1475
|
if (element2.required && filteredValues.length === 0) {
|
|
1218
|
-
errors.push(`${key2}: required`);
|
|
1476
|
+
errors.push(`${key2}: ${t("required", state)}`);
|
|
1219
1477
|
}
|
|
1220
1478
|
if (filteredValues.length < minCount) {
|
|
1221
|
-
errors.push(`${key2}:
|
|
1479
|
+
errors.push(`${key2}: ${t("minItems", state, { min: minCount })}`);
|
|
1222
1480
|
}
|
|
1223
1481
|
if (filteredValues.length > maxCount) {
|
|
1224
|
-
errors.push(`${key2}:
|
|
1482
|
+
errors.push(`${key2}: ${t("maxItems", state, { max: maxCount })}`);
|
|
1225
1483
|
}
|
|
1226
1484
|
};
|
|
1227
1485
|
if ("multiple" in element && element.multiple) {
|
|
@@ -1242,8 +1500,9 @@ function validateSelectElement(element, key, context) {
|
|
|
1242
1500
|
);
|
|
1243
1501
|
const val = input?.value ?? "";
|
|
1244
1502
|
if (!skipValidation && element.required && val === "") {
|
|
1245
|
-
|
|
1246
|
-
|
|
1503
|
+
const msg = t("required", context.state);
|
|
1504
|
+
errors.push(`${key}: ${msg}`);
|
|
1505
|
+
markValidity(input, msg);
|
|
1247
1506
|
return { value: null, errors };
|
|
1248
1507
|
} else {
|
|
1249
1508
|
markValidity(input, null);
|
|
@@ -1295,18 +1554,11 @@ function updateSelectField(element, fieldPath, value, context) {
|
|
|
1295
1554
|
}
|
|
1296
1555
|
}
|
|
1297
1556
|
|
|
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
1557
|
// src/components/file.ts
|
|
1306
|
-
function renderLocalImagePreview(container, file, fileName) {
|
|
1558
|
+
function renderLocalImagePreview(container, file, fileName, state) {
|
|
1307
1559
|
const img = document.createElement("img");
|
|
1308
1560
|
img.className = "w-full h-full object-contain";
|
|
1309
|
-
img.alt = fileName || "
|
|
1561
|
+
img.alt = fileName || t("previewAlt", state);
|
|
1310
1562
|
const reader = new FileReader();
|
|
1311
1563
|
reader.onload = (e) => {
|
|
1312
1564
|
img.src = e.target?.result || "";
|
|
@@ -1325,14 +1577,14 @@ function renderLocalVideoPreview(container, file, videoType, resourceId, state,
|
|
|
1325
1577
|
<div class="relative group h-full">
|
|
1326
1578
|
<video class="w-full h-full object-contain" controls preload="auto" muted>
|
|
1327
1579
|
<source src="${videoUrl}" type="${videoType}">
|
|
1328
|
-
|
|
1580
|
+
${escapeHtml(t("videoNotSupported", state))}
|
|
1329
1581
|
</video>
|
|
1330
1582
|
<div class="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity z-10 flex gap-1">
|
|
1331
1583
|
<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)}
|
|
1584
|
+
${escapeHtml(t("removeElement", state))}
|
|
1333
1585
|
</button>
|
|
1334
1586
|
<button class="bg-gray-800 bg-opacity-75 hover:bg-opacity-90 text-white p-1 rounded text-xs change-file-btn">
|
|
1335
|
-
|
|
1587
|
+
${escapeHtml(t("changeButton", state))}
|
|
1336
1588
|
</button>
|
|
1337
1589
|
</div>
|
|
1338
1590
|
</div>
|
|
@@ -1381,11 +1633,11 @@ function handleVideoDelete(container, resourceId, state, deps) {
|
|
|
1381
1633
|
<svg class="w-6 h-6 mb-2" fill="currentColor" viewBox="0 0 24 24">
|
|
1382
1634
|
<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
1635
|
</svg>
|
|
1384
|
-
<div class="text-sm text-center">${t("clickDragText", state)}</div>
|
|
1636
|
+
<div class="text-sm text-center">${escapeHtml(t("clickDragText", state))}</div>
|
|
1385
1637
|
</div>
|
|
1386
1638
|
`;
|
|
1387
1639
|
}
|
|
1388
|
-
function renderUploadedVideoPreview(container, thumbnailUrl, videoType) {
|
|
1640
|
+
function renderUploadedVideoPreview(container, thumbnailUrl, videoType, state) {
|
|
1389
1641
|
const video = document.createElement("video");
|
|
1390
1642
|
video.className = "w-full h-full object-contain";
|
|
1391
1643
|
video.controls = true;
|
|
@@ -1396,7 +1648,7 @@ function renderUploadedVideoPreview(container, thumbnailUrl, videoType) {
|
|
|
1396
1648
|
source.type = videoType;
|
|
1397
1649
|
video.appendChild(source);
|
|
1398
1650
|
video.appendChild(
|
|
1399
|
-
document.createTextNode("
|
|
1651
|
+
document.createTextNode(t("videoNotSupported", state))
|
|
1400
1652
|
);
|
|
1401
1653
|
container.appendChild(video);
|
|
1402
1654
|
}
|
|
@@ -1414,7 +1666,7 @@ function renderDeleteButton(container, resourceId, state) {
|
|
|
1414
1666
|
<svg class="w-6 h-6 mb-2" fill="currentColor" viewBox="0 0 24 24">
|
|
1415
1667
|
<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
1668
|
</svg>
|
|
1417
|
-
<div class="text-sm text-center">${t("clickDragText", state)}</div>
|
|
1669
|
+
<div class="text-sm text-center">${escapeHtml(t("clickDragText", state))}</div>
|
|
1418
1670
|
</div>
|
|
1419
1671
|
`;
|
|
1420
1672
|
});
|
|
@@ -1424,7 +1676,7 @@ async function renderLocalFilePreview(container, meta, fileName, resourceId, isR
|
|
|
1424
1676
|
return;
|
|
1425
1677
|
}
|
|
1426
1678
|
if (meta.type && meta.type.startsWith("image/")) {
|
|
1427
|
-
renderLocalImagePreview(container, meta.file, fileName);
|
|
1679
|
+
renderLocalImagePreview(container, meta.file, fileName, state);
|
|
1428
1680
|
} else if (meta.type && meta.type.startsWith("video/")) {
|
|
1429
1681
|
const newContainer = renderLocalVideoPreview(
|
|
1430
1682
|
container,
|
|
@@ -1436,7 +1688,7 @@ async function renderLocalFilePreview(container, meta, fileName, resourceId, isR
|
|
|
1436
1688
|
);
|
|
1437
1689
|
container = newContainer;
|
|
1438
1690
|
} 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>`;
|
|
1691
|
+
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
1692
|
}
|
|
1441
1693
|
if (!isReadonly && !(meta.type && meta.type.startsWith("video/"))) {
|
|
1442
1694
|
renderDeleteButton(container, resourceId, state);
|
|
@@ -1452,11 +1704,11 @@ async function renderUploadedFilePreview(container, resourceId, fileName, meta,
|
|
|
1452
1704
|
if (thumbnailUrl) {
|
|
1453
1705
|
clear(container);
|
|
1454
1706
|
if (meta && meta.type && meta.type.startsWith("video/")) {
|
|
1455
|
-
renderUploadedVideoPreview(container, thumbnailUrl, meta.type);
|
|
1707
|
+
renderUploadedVideoPreview(container, thumbnailUrl, meta.type, state);
|
|
1456
1708
|
} else {
|
|
1457
1709
|
const img = document.createElement("img");
|
|
1458
1710
|
img.className = "w-full h-full object-contain";
|
|
1459
|
-
img.alt = fileName || "
|
|
1711
|
+
img.alt = fileName || t("previewAlt", state);
|
|
1460
1712
|
img.src = thumbnailUrl;
|
|
1461
1713
|
container.appendChild(img);
|
|
1462
1714
|
}
|
|
@@ -1470,7 +1722,7 @@ async function renderUploadedFilePreview(container, resourceId, fileName, meta,
|
|
|
1470
1722
|
<svg class="w-6 h-6 mb-2" fill="currentColor" viewBox="0 0 24 24">
|
|
1471
1723
|
<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
1724
|
</svg>
|
|
1473
|
-
<div class="text-sm text-center">${fileName || "
|
|
1725
|
+
<div class="text-sm text-center">${escapeHtml(fileName || t("previewUnavailable", state))}</div>
|
|
1474
1726
|
</div>
|
|
1475
1727
|
`;
|
|
1476
1728
|
}
|
|
@@ -1526,16 +1778,16 @@ async function renderFilePreviewReadonly(resourceId, state, fileName) {
|
|
|
1526
1778
|
try {
|
|
1527
1779
|
const thumbnailUrl = await state.config.getThumbnail(resourceId);
|
|
1528
1780
|
if (thumbnailUrl) {
|
|
1529
|
-
previewContainer.innerHTML = `<img src="${thumbnailUrl}" alt="${actualFileName}" class="w-full h-auto">`;
|
|
1781
|
+
previewContainer.innerHTML = `<img src="${thumbnailUrl}" alt="${escapeHtml(actualFileName)}" class="w-full h-auto">`;
|
|
1530
1782
|
} 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>`;
|
|
1783
|
+
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
1784
|
}
|
|
1533
1785
|
} catch (error) {
|
|
1534
1786
|
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>`;
|
|
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>`;
|
|
1536
1788
|
}
|
|
1537
1789
|
} 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>`;
|
|
1790
|
+
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
1791
|
}
|
|
1540
1792
|
} else if (isVideo) {
|
|
1541
1793
|
if (state.config.getThumbnail) {
|
|
@@ -1546,7 +1798,7 @@ async function renderFilePreviewReadonly(resourceId, state, fileName) {
|
|
|
1546
1798
|
<div class="relative group">
|
|
1547
1799
|
<video class="w-full h-auto" controls preload="auto" muted>
|
|
1548
1800
|
<source src="${videoUrl}" type="${meta?.type || "video/mp4"}">
|
|
1549
|
-
|
|
1801
|
+
${escapeHtml(t("videoNotSupported", state))}
|
|
1550
1802
|
</video>
|
|
1551
1803
|
<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
1804
|
<div class="bg-white bg-opacity-90 rounded-full p-3">
|
|
@@ -1558,14 +1810,14 @@ async function renderFilePreviewReadonly(resourceId, state, fileName) {
|
|
|
1558
1810
|
</div>
|
|
1559
1811
|
`;
|
|
1560
1812
|
} 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>`;
|
|
1813
|
+
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
1814
|
}
|
|
1563
1815
|
} catch (error) {
|
|
1564
1816
|
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>`;
|
|
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>`;
|
|
1566
1818
|
}
|
|
1567
1819
|
} 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>`;
|
|
1820
|
+
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
1821
|
}
|
|
1570
1822
|
} else {
|
|
1571
1823
|
const fileIcon = isPSD ? "\u{1F3A8}" : "\u{1F4C1}";
|
|
@@ -1575,13 +1827,13 @@ async function renderFilePreviewReadonly(resourceId, state, fileName) {
|
|
|
1575
1827
|
<div class="flex items-center space-x-3">
|
|
1576
1828
|
<div class="text-3xl text-gray-400">${fileIcon}</div>
|
|
1577
1829
|
<div class="flex-1 min-w-0">
|
|
1578
|
-
<div class="text-sm font-medium text-gray-900 truncate">${actualFileName}</div>
|
|
1830
|
+
<div class="text-sm font-medium text-gray-900 truncate">${escapeHtml(actualFileName)}</div>
|
|
1579
1831
|
<div class="text-xs text-gray-500">${fileDescription}</div>
|
|
1580
1832
|
</div>
|
|
1581
1833
|
</div>
|
|
1582
1834
|
`;
|
|
1583
1835
|
} 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>`;
|
|
1836
|
+
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
1837
|
}
|
|
1586
1838
|
}
|
|
1587
1839
|
const fileNameElement = document.createElement("p");
|
|
@@ -1604,12 +1856,18 @@ async function renderFilePreviewReadonly(resourceId, state, fileName) {
|
|
|
1604
1856
|
fileResult.appendChild(downloadButton);
|
|
1605
1857
|
return fileResult;
|
|
1606
1858
|
}
|
|
1607
|
-
function renderResourcePills(container, rids, state, onRemove) {
|
|
1859
|
+
function renderResourcePills(container, rids, state, onRemove, hint, countInfo) {
|
|
1608
1860
|
clear(container);
|
|
1861
|
+
const buildHintLine = () => {
|
|
1862
|
+
const parts = [t("clickDragTextMultiple", state)];
|
|
1863
|
+
if (hint) parts.push(hint);
|
|
1864
|
+
if (countInfo) parts.push(countInfo);
|
|
1865
|
+
return parts.join(" \u2022 ");
|
|
1866
|
+
};
|
|
1609
1867
|
const isInitialRender = !container.classList.contains("grid");
|
|
1610
1868
|
if ((!rids || rids.length === 0) && isInitialRender) {
|
|
1611
|
-
const
|
|
1612
|
-
|
|
1869
|
+
const gridContainer2 = document.createElement("div");
|
|
1870
|
+
gridContainer2.className = "grid grid-cols-4 gap-3 mb-3";
|
|
1613
1871
|
for (let i = 0; i < 4; i++) {
|
|
1614
1872
|
const slot = document.createElement("div");
|
|
1615
1873
|
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 +1898,17 @@ function renderResourcePills(container, rids, state, onRemove) {
|
|
|
1640
1898
|
);
|
|
1641
1899
|
if (fileInput) fileInput.click();
|
|
1642
1900
|
};
|
|
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);
|
|
1901
|
+
gridContainer2.appendChild(slot);
|
|
1902
|
+
}
|
|
1903
|
+
const hintText2 = document.createElement("div");
|
|
1904
|
+
hintText2.className = "text-center text-xs text-gray-500 mt-2";
|
|
1905
|
+
hintText2.textContent = buildHintLine();
|
|
1906
|
+
container.appendChild(gridContainer2);
|
|
1907
|
+
container.appendChild(hintText2);
|
|
1670
1908
|
return;
|
|
1671
1909
|
}
|
|
1672
|
-
|
|
1910
|
+
const gridContainer = document.createElement("div");
|
|
1911
|
+
gridContainer.className = "files-list grid grid-cols-4 gap-3";
|
|
1673
1912
|
const currentImagesCount = rids ? rids.length : 0;
|
|
1674
1913
|
const rowsNeeded = Math.floor(currentImagesCount / 4) + 1;
|
|
1675
1914
|
const slotsNeeded = rowsNeeded * 4;
|
|
@@ -1684,7 +1923,7 @@ function renderResourcePills(container, rids, state, onRemove) {
|
|
|
1684
1923
|
console.error("Failed to render thumbnail:", err);
|
|
1685
1924
|
slot.innerHTML = `<div class="flex flex-col items-center justify-center h-full text-gray-400">
|
|
1686
1925
|
<div class="text-2xl mb-1">\u{1F4C1}</div>
|
|
1687
|
-
<div class="text-xs"
|
|
1926
|
+
<div class="text-xs">${escapeHtml(t("previewError", state))}</div>
|
|
1688
1927
|
</div>`;
|
|
1689
1928
|
});
|
|
1690
1929
|
if (onRemove) {
|
|
@@ -1717,15 +1956,20 @@ function renderResourcePills(container, rids, state, onRemove) {
|
|
|
1717
1956
|
if (fileInput) fileInput.click();
|
|
1718
1957
|
};
|
|
1719
1958
|
}
|
|
1720
|
-
|
|
1959
|
+
gridContainer.appendChild(slot);
|
|
1721
1960
|
}
|
|
1961
|
+
container.appendChild(gridContainer);
|
|
1962
|
+
const hintText = document.createElement("div");
|
|
1963
|
+
hintText.className = "text-center text-xs text-gray-500 mt-2";
|
|
1964
|
+
hintText.textContent = buildHintLine();
|
|
1965
|
+
container.appendChild(hintText);
|
|
1722
1966
|
}
|
|
1723
|
-
function renderThumbnailError(slot, iconSize = "w-12 h-12") {
|
|
1967
|
+
function renderThumbnailError(slot, state, iconSize = "w-12 h-12") {
|
|
1724
1968
|
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">
|
|
1969
|
+
<svg class="${escapeHtml(iconSize)} text-red-400" fill="currentColor" viewBox="0 0 24 24">
|
|
1726
1970
|
<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
1971
|
</svg>
|
|
1728
|
-
<div class="text-xs mt-1 text-red-600"
|
|
1972
|
+
<div class="text-xs mt-1 text-red-600">${escapeHtml(t("previewError", state))}</div>
|
|
1729
1973
|
</div>`;
|
|
1730
1974
|
}
|
|
1731
1975
|
async function renderThumbnailForResource(slot, rid, meta, state) {
|
|
@@ -1761,7 +2005,7 @@ async function renderThumbnailForResource(slot, rid, meta, state) {
|
|
|
1761
2005
|
if (state.config.onThumbnailError) {
|
|
1762
2006
|
state.config.onThumbnailError(err, rid);
|
|
1763
2007
|
}
|
|
1764
|
-
renderThumbnailError(slot);
|
|
2008
|
+
renderThumbnailError(slot, state);
|
|
1765
2009
|
}
|
|
1766
2010
|
} else {
|
|
1767
2011
|
slot.innerHTML = `<div class="flex flex-col items-center justify-center h-full text-gray-400">
|
|
@@ -1810,7 +2054,7 @@ async function renderThumbnailForResource(slot, rid, meta, state) {
|
|
|
1810
2054
|
<svg class="w-8 h-8" fill="currentColor" viewBox="0 0 24 24">
|
|
1811
2055
|
<path d="M8 5v14l11-7z"/>
|
|
1812
2056
|
</svg>
|
|
1813
|
-
<div class="text-xs mt-1">${meta?.name || "Video"}</div>
|
|
2057
|
+
<div class="text-xs mt-1">${escapeHtml(meta?.name || "Video")}</div>
|
|
1814
2058
|
</div>`;
|
|
1815
2059
|
}
|
|
1816
2060
|
} catch (error) {
|
|
@@ -1818,30 +2062,32 @@ async function renderThumbnailForResource(slot, rid, meta, state) {
|
|
|
1818
2062
|
if (state.config.onThumbnailError) {
|
|
1819
2063
|
state.config.onThumbnailError(err, rid);
|
|
1820
2064
|
}
|
|
1821
|
-
renderThumbnailError(slot, "w-8 h-8");
|
|
2065
|
+
renderThumbnailError(slot, state, "w-8 h-8");
|
|
1822
2066
|
}
|
|
1823
2067
|
} else {
|
|
1824
2068
|
slot.innerHTML = `<div class="flex flex-col items-center justify-center h-full text-gray-400">
|
|
1825
2069
|
<svg class="w-8 h-8" fill="currentColor" viewBox="0 0 24 24">
|
|
1826
2070
|
<path d="M8 5v14l11-7z"/>
|
|
1827
2071
|
</svg>
|
|
1828
|
-
<div class="text-xs mt-1">${meta?.name || "Video"}</div>
|
|
2072
|
+
<div class="text-xs mt-1">${escapeHtml(meta?.name || "Video")}</div>
|
|
1829
2073
|
</div>`;
|
|
1830
2074
|
}
|
|
1831
2075
|
} else {
|
|
1832
2076
|
slot.innerHTML = `<div class="flex flex-col items-center justify-center h-full text-gray-400">
|
|
1833
2077
|
<div class="text-2xl mb-1">\u{1F4C1}</div>
|
|
1834
|
-
<div class="text-xs">${meta?.name || "File"}</div>
|
|
2078
|
+
<div class="text-xs">${escapeHtml(meta?.name || "File")}</div>
|
|
1835
2079
|
</div>`;
|
|
1836
2080
|
}
|
|
1837
2081
|
}
|
|
1838
|
-
function setEmptyFileContainer(fileContainer, state) {
|
|
2082
|
+
function setEmptyFileContainer(fileContainer, state, hint) {
|
|
2083
|
+
const hintHtml = hint ? `<div class="text-xs text-gray-500 mt-1">${escapeHtml(hint)}</div>` : "";
|
|
1839
2084
|
fileContainer.innerHTML = `
|
|
1840
2085
|
<div class="flex flex-col items-center justify-center h-full text-gray-400">
|
|
1841
2086
|
<svg class="w-6 h-6 mb-2" fill="currentColor" viewBox="0 0 24 24">
|
|
1842
2087
|
<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
2088
|
</svg>
|
|
1844
|
-
<div class="text-sm text-center">${t("clickDragText", state)}</div>
|
|
2089
|
+
<div class="text-sm text-center">${escapeHtml(t("clickDragText", state))}</div>
|
|
2090
|
+
${hintHtml}
|
|
1845
2091
|
</div>
|
|
1846
2092
|
`;
|
|
1847
2093
|
}
|
|
@@ -2101,13 +2347,13 @@ function renderFileElement(element, ctx, wrapper, pathKey) {
|
|
|
2101
2347
|
console.error("Failed to render file preview:", err);
|
|
2102
2348
|
const emptyState = document.createElement("div");
|
|
2103
2349
|
emptyState.className = "aspect-video bg-gray-100 rounded-lg flex items-center justify-center text-gray-500";
|
|
2104
|
-
emptyState.innerHTML = `<div class="text-center"
|
|
2350
|
+
emptyState.innerHTML = `<div class="text-center">${escapeHtml(t("previewUnavailable", state))}</div>`;
|
|
2105
2351
|
wrapper.appendChild(emptyState);
|
|
2106
2352
|
});
|
|
2107
2353
|
} else {
|
|
2108
2354
|
const emptyState = document.createElement("div");
|
|
2109
2355
|
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>`;
|
|
2356
|
+
emptyState.innerHTML = `<div class="text-center">${escapeHtml(t("noFileSelected", state))}</div>`;
|
|
2111
2357
|
wrapper.appendChild(emptyState);
|
|
2112
2358
|
}
|
|
2113
2359
|
} else {
|
|
@@ -2151,7 +2397,8 @@ function renderFileElement(element, ctx, wrapper, pathKey) {
|
|
|
2151
2397
|
}
|
|
2152
2398
|
);
|
|
2153
2399
|
} else {
|
|
2154
|
-
|
|
2400
|
+
const hint = makeFieldHint(element, state);
|
|
2401
|
+
setEmptyFileContainer(fileContainer, state, hint);
|
|
2155
2402
|
}
|
|
2156
2403
|
fileContainer.onclick = fileUploadHandler;
|
|
2157
2404
|
setupDragAndDrop(fileContainer, dragHandler);
|
|
@@ -2170,18 +2417,6 @@ function renderFileElement(element, ctx, wrapper, pathKey) {
|
|
|
2170
2417
|
};
|
|
2171
2418
|
fileWrapper.appendChild(fileContainer);
|
|
2172
2419
|
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
2420
|
wrapper.appendChild(fileWrapper);
|
|
2186
2421
|
}
|
|
2187
2422
|
}
|
|
@@ -2200,18 +2435,24 @@ function renderFilesElement(element, ctx, wrapper, pathKey) {
|
|
|
2200
2435
|
});
|
|
2201
2436
|
});
|
|
2202
2437
|
} 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>`;
|
|
2438
|
+
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
2439
|
}
|
|
2205
2440
|
wrapper.appendChild(resultsWrapper);
|
|
2206
2441
|
} else {
|
|
2207
2442
|
let updateFilesList2 = function() {
|
|
2208
|
-
renderResourcePills(
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2443
|
+
renderResourcePills(
|
|
2444
|
+
list,
|
|
2445
|
+
initialFiles,
|
|
2446
|
+
state,
|
|
2447
|
+
(ridToRemove) => {
|
|
2448
|
+
const index = initialFiles.indexOf(ridToRemove);
|
|
2449
|
+
if (index > -1) {
|
|
2450
|
+
initialFiles.splice(index, 1);
|
|
2451
|
+
}
|
|
2452
|
+
updateFilesList2();
|
|
2453
|
+
},
|
|
2454
|
+
filesFieldHint
|
|
2455
|
+
);
|
|
2215
2456
|
};
|
|
2216
2457
|
const filesWrapper = document.createElement("div");
|
|
2217
2458
|
filesWrapper.className = "space-y-2";
|
|
@@ -2229,6 +2470,7 @@ function renderFilesElement(element, ctx, wrapper, pathKey) {
|
|
|
2229
2470
|
list.className = "files-list";
|
|
2230
2471
|
const initialFiles = ctx.prefill[element.key] || [];
|
|
2231
2472
|
addPrefillFilesToIndex(initialFiles, state);
|
|
2473
|
+
const filesFieldHint = makeFieldHint(element, state);
|
|
2232
2474
|
updateFilesList2();
|
|
2233
2475
|
setupFilesDropHandler(
|
|
2234
2476
|
filesContainer,
|
|
@@ -2249,10 +2491,6 @@ function renderFilesElement(element, ctx, wrapper, pathKey) {
|
|
|
2249
2491
|
filesContainer.appendChild(list);
|
|
2250
2492
|
filesWrapper.appendChild(filesContainer);
|
|
2251
2493
|
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
2494
|
wrapper.appendChild(filesWrapper);
|
|
2257
2495
|
}
|
|
2258
2496
|
}
|
|
@@ -2273,7 +2511,7 @@ function renderMultipleFileElement(element, ctx, wrapper, pathKey) {
|
|
|
2273
2511
|
});
|
|
2274
2512
|
});
|
|
2275
2513
|
} 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>`;
|
|
2514
|
+
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
2515
|
}
|
|
2278
2516
|
wrapper.appendChild(resultsWrapper);
|
|
2279
2517
|
} else {
|
|
@@ -2293,19 +2531,24 @@ function renderMultipleFileElement(element, ctx, wrapper, pathKey) {
|
|
|
2293
2531
|
filesWrapper.appendChild(filesContainer);
|
|
2294
2532
|
const initialFiles = Array.isArray(ctx.prefill[element.key]) ? [...ctx.prefill[element.key]] : [];
|
|
2295
2533
|
addPrefillFilesToIndex(initialFiles, state);
|
|
2534
|
+
const multipleFilesHint = makeFieldHint(element, state);
|
|
2535
|
+
const buildCountInfo = () => {
|
|
2536
|
+
const countText = initialFiles.length === 1 ? t("fileCountSingle", state, { count: initialFiles.length }) : t("fileCountPlural", state, { count: initialFiles.length });
|
|
2537
|
+
const minMaxText = minFiles > 0 || maxFiles < Infinity ? ` ${t("fileCountRange", state, { min: minFiles, max: maxFiles })}` : "";
|
|
2538
|
+
return countText + minMaxText;
|
|
2539
|
+
};
|
|
2296
2540
|
const updateFilesDisplay = () => {
|
|
2297
|
-
renderResourcePills(
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
filesWrapper.appendChild(countInfo);
|
|
2541
|
+
renderResourcePills(
|
|
2542
|
+
filesContainer,
|
|
2543
|
+
initialFiles,
|
|
2544
|
+
state,
|
|
2545
|
+
(index) => {
|
|
2546
|
+
initialFiles.splice(initialFiles.indexOf(index), 1);
|
|
2547
|
+
updateFilesDisplay();
|
|
2548
|
+
},
|
|
2549
|
+
multipleFilesHint,
|
|
2550
|
+
buildCountInfo()
|
|
2551
|
+
);
|
|
2309
2552
|
};
|
|
2310
2553
|
setupFilesDropHandler(
|
|
2311
2554
|
filesContainer,
|
|
@@ -2333,16 +2576,17 @@ function validateFileElement(element, key, context) {
|
|
|
2333
2576
|
const isMultipleField = element.type === "files" || "multiple" in element && Boolean(element.multiple);
|
|
2334
2577
|
const validateFileCount = (key2, resourceIds, element2) => {
|
|
2335
2578
|
if (skipValidation) return;
|
|
2579
|
+
const { state } = context;
|
|
2336
2580
|
const minFiles = "minCount" in element2 ? element2.minCount ?? 0 : 0;
|
|
2337
2581
|
const maxFiles = "maxCount" in element2 ? element2.maxCount ?? Infinity : Infinity;
|
|
2338
2582
|
if (element2.required && resourceIds.length === 0) {
|
|
2339
|
-
errors.push(`${key2}: required`);
|
|
2583
|
+
errors.push(`${key2}: ${t("required", state)}`);
|
|
2340
2584
|
}
|
|
2341
2585
|
if (resourceIds.length < minFiles) {
|
|
2342
|
-
errors.push(`${key2}:
|
|
2586
|
+
errors.push(`${key2}: ${t("minFiles", state, { min: minFiles })}`);
|
|
2343
2587
|
}
|
|
2344
2588
|
if (resourceIds.length > maxFiles) {
|
|
2345
|
-
errors.push(`${key2}:
|
|
2589
|
+
errors.push(`${key2}: ${t("maxFiles", state, { max: maxFiles })}`);
|
|
2346
2590
|
}
|
|
2347
2591
|
};
|
|
2348
2592
|
if (isMultipleField) {
|
|
@@ -2370,7 +2614,7 @@ function validateFileElement(element, key, context) {
|
|
|
2370
2614
|
);
|
|
2371
2615
|
const rid = input?.value ?? "";
|
|
2372
2616
|
if (!skipValidation && element.required && rid === "") {
|
|
2373
|
-
errors.push(`${key}: required`);
|
|
2617
|
+
errors.push(`${key}: ${t("required", context.state)}`);
|
|
2374
2618
|
return { value: null, errors };
|
|
2375
2619
|
}
|
|
2376
2620
|
return { value: rid || null, errors };
|
|
@@ -2611,14 +2855,16 @@ function renderColourElement(element, ctx, wrapper, pathKey) {
|
|
|
2611
2855
|
const editUI = createEditColourUI(initialValue, pathKey, ctx);
|
|
2612
2856
|
wrapper.appendChild(editUI);
|
|
2613
2857
|
}
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2858
|
+
if (!state.config.readonly) {
|
|
2859
|
+
const colourHint = document.createElement("p");
|
|
2860
|
+
colourHint.className = "mt-1";
|
|
2861
|
+
colourHint.style.cssText = `
|
|
2862
|
+
font-size: var(--fb-font-size-small);
|
|
2863
|
+
color: var(--fb-text-secondary-color);
|
|
2864
|
+
`;
|
|
2865
|
+
colourHint.textContent = makeFieldHint(element, state);
|
|
2866
|
+
wrapper.appendChild(colourHint);
|
|
2867
|
+
}
|
|
2622
2868
|
}
|
|
2623
2869
|
function renderMultipleColourElement(element, ctx, wrapper, pathKey) {
|
|
2624
2870
|
const state = ctx.state;
|
|
@@ -2707,48 +2953,65 @@ function renderMultipleColourElement(element, ctx, wrapper, pathKey) {
|
|
|
2707
2953
|
removeBtn.style.pointerEvents = disabled ? "none" : "auto";
|
|
2708
2954
|
});
|
|
2709
2955
|
}
|
|
2956
|
+
let addRow = null;
|
|
2957
|
+
let countDisplay = null;
|
|
2958
|
+
if (!state.config.readonly) {
|
|
2959
|
+
addRow = document.createElement("div");
|
|
2960
|
+
addRow.className = "flex items-center gap-3 mt-2";
|
|
2961
|
+
const addBtn = document.createElement("button");
|
|
2962
|
+
addBtn.type = "button";
|
|
2963
|
+
addBtn.className = "add-colour-btn px-3 py-1 rounded";
|
|
2964
|
+
addBtn.style.cssText = `
|
|
2965
|
+
color: var(--fb-primary-color);
|
|
2966
|
+
border: var(--fb-border-width) solid var(--fb-primary-color);
|
|
2967
|
+
background-color: transparent;
|
|
2968
|
+
font-size: var(--fb-font-size);
|
|
2969
|
+
transition: all var(--fb-transition-duration);
|
|
2970
|
+
`;
|
|
2971
|
+
addBtn.textContent = "+";
|
|
2972
|
+
addBtn.addEventListener("mouseenter", () => {
|
|
2973
|
+
addBtn.style.backgroundColor = "var(--fb-background-hover-color)";
|
|
2974
|
+
});
|
|
2975
|
+
addBtn.addEventListener("mouseleave", () => {
|
|
2976
|
+
addBtn.style.backgroundColor = "transparent";
|
|
2977
|
+
});
|
|
2978
|
+
addBtn.onclick = () => {
|
|
2979
|
+
const defaultColour = element.default || "#000000";
|
|
2980
|
+
values.push(defaultColour);
|
|
2981
|
+
addColourItem(defaultColour);
|
|
2982
|
+
updateAddButton();
|
|
2983
|
+
updateRemoveButtons();
|
|
2984
|
+
};
|
|
2985
|
+
countDisplay = document.createElement("span");
|
|
2986
|
+
countDisplay.className = "text-sm text-gray-500";
|
|
2987
|
+
addRow.appendChild(addBtn);
|
|
2988
|
+
addRow.appendChild(countDisplay);
|
|
2989
|
+
wrapper.appendChild(addRow);
|
|
2990
|
+
}
|
|
2710
2991
|
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);
|
|
2992
|
+
if (!addRow || !countDisplay) return;
|
|
2993
|
+
const addBtn = addRow.querySelector(".add-colour-btn");
|
|
2994
|
+
if (addBtn) {
|
|
2995
|
+
const disabled = values.length >= maxCount;
|
|
2996
|
+
addBtn.disabled = disabled;
|
|
2997
|
+
addBtn.style.opacity = disabled ? "0.5" : "1";
|
|
2998
|
+
addBtn.style.pointerEvents = disabled ? "none" : "auto";
|
|
2739
2999
|
}
|
|
3000
|
+
countDisplay.textContent = `${values.length}/${maxCount === Infinity ? "\u221E" : maxCount}`;
|
|
2740
3001
|
}
|
|
2741
3002
|
values.forEach((value) => addColourItem(value));
|
|
2742
3003
|
updateAddButton();
|
|
2743
3004
|
updateRemoveButtons();
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
3005
|
+
if (!state.config.readonly) {
|
|
3006
|
+
const hint = document.createElement("p");
|
|
3007
|
+
hint.className = "mt-1";
|
|
3008
|
+
hint.style.cssText = `
|
|
3009
|
+
font-size: var(--fb-font-size-small);
|
|
3010
|
+
color: var(--fb-text-secondary-color);
|
|
3011
|
+
`;
|
|
3012
|
+
hint.textContent = makeFieldHint(element, state);
|
|
3013
|
+
wrapper.appendChild(hint);
|
|
3014
|
+
}
|
|
2752
3015
|
}
|
|
2753
3016
|
function validateColourElement(element, key, context) {
|
|
2754
3017
|
const errors = [];
|
|
@@ -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;
|
|
@@ -3050,14 +3323,16 @@ function renderSliderElement(element, ctx, wrapper, pathKey) {
|
|
|
3050
3323
|
state.config.readonly
|
|
3051
3324
|
);
|
|
3052
3325
|
wrapper.appendChild(sliderUI);
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3326
|
+
if (!state.config.readonly) {
|
|
3327
|
+
const hint = document.createElement("p");
|
|
3328
|
+
hint.className = "mt-1";
|
|
3329
|
+
hint.style.cssText = `
|
|
3330
|
+
font-size: var(--fb-font-size-small);
|
|
3331
|
+
color: var(--fb-text-secondary-color);
|
|
3332
|
+
`;
|
|
3333
|
+
hint.textContent = makeFieldHint(element, state);
|
|
3334
|
+
wrapper.appendChild(hint);
|
|
3335
|
+
}
|
|
3061
3336
|
}
|
|
3062
3337
|
function renderMultipleSliderElement(element, ctx, wrapper, pathKey) {
|
|
3063
3338
|
if (element.min === void 0 || element.min === null) {
|
|
@@ -3158,47 +3433,64 @@ function renderMultipleSliderElement(element, ctx, wrapper, pathKey) {
|
|
|
3158
3433
|
removeBtn.style.pointerEvents = disabled ? "none" : "auto";
|
|
3159
3434
|
});
|
|
3160
3435
|
}
|
|
3436
|
+
let addRow = null;
|
|
3437
|
+
let countDisplay = null;
|
|
3438
|
+
if (!state.config.readonly) {
|
|
3439
|
+
addRow = document.createElement("div");
|
|
3440
|
+
addRow.className = "flex items-center gap-3 mt-2";
|
|
3441
|
+
const addBtn = document.createElement("button");
|
|
3442
|
+
addBtn.type = "button";
|
|
3443
|
+
addBtn.className = "add-slider-btn px-3 py-1 rounded";
|
|
3444
|
+
addBtn.style.cssText = `
|
|
3445
|
+
color: var(--fb-primary-color);
|
|
3446
|
+
border: var(--fb-border-width) solid var(--fb-primary-color);
|
|
3447
|
+
background-color: transparent;
|
|
3448
|
+
font-size: var(--fb-font-size);
|
|
3449
|
+
transition: all var(--fb-transition-duration);
|
|
3450
|
+
`;
|
|
3451
|
+
addBtn.textContent = "+";
|
|
3452
|
+
addBtn.addEventListener("mouseenter", () => {
|
|
3453
|
+
addBtn.style.backgroundColor = "var(--fb-background-hover-color)";
|
|
3454
|
+
});
|
|
3455
|
+
addBtn.addEventListener("mouseleave", () => {
|
|
3456
|
+
addBtn.style.backgroundColor = "transparent";
|
|
3457
|
+
});
|
|
3458
|
+
addBtn.onclick = () => {
|
|
3459
|
+
values.push(defaultValue);
|
|
3460
|
+
addSliderItem(defaultValue);
|
|
3461
|
+
updateAddButton();
|
|
3462
|
+
updateRemoveButtons();
|
|
3463
|
+
};
|
|
3464
|
+
countDisplay = document.createElement("span");
|
|
3465
|
+
countDisplay.className = "text-sm text-gray-500";
|
|
3466
|
+
addRow.appendChild(addBtn);
|
|
3467
|
+
addRow.appendChild(countDisplay);
|
|
3468
|
+
wrapper.appendChild(addRow);
|
|
3469
|
+
}
|
|
3161
3470
|
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);
|
|
3471
|
+
if (!addRow || !countDisplay) return;
|
|
3472
|
+
const addBtn = addRow.querySelector(".add-slider-btn");
|
|
3473
|
+
if (addBtn) {
|
|
3474
|
+
const disabled = values.length >= maxCount;
|
|
3475
|
+
addBtn.disabled = disabled;
|
|
3476
|
+
addBtn.style.opacity = disabled ? "0.5" : "1";
|
|
3477
|
+
addBtn.style.pointerEvents = disabled ? "none" : "auto";
|
|
3189
3478
|
}
|
|
3479
|
+
countDisplay.textContent = `${values.length}/${maxCount === Infinity ? "\u221E" : maxCount}`;
|
|
3190
3480
|
}
|
|
3191
3481
|
values.forEach((value) => addSliderItem(value));
|
|
3192
3482
|
updateAddButton();
|
|
3193
3483
|
updateRemoveButtons();
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
|
|
3484
|
+
if (!state.config.readonly) {
|
|
3485
|
+
const hint = document.createElement("p");
|
|
3486
|
+
hint.className = "mt-1";
|
|
3487
|
+
hint.style.cssText = `
|
|
3488
|
+
font-size: var(--fb-font-size-small);
|
|
3489
|
+
color: var(--fb-text-secondary-color);
|
|
3490
|
+
`;
|
|
3491
|
+
hint.textContent = makeFieldHint(element, state);
|
|
3492
|
+
wrapper.appendChild(hint);
|
|
3493
|
+
}
|
|
3202
3494
|
}
|
|
3203
3495
|
function validateSliderElement(element, key, context) {
|
|
3204
3496
|
const errors = [];
|
|
@@ -3254,11 +3546,13 @@ function validateSliderElement(element, key, context) {
|
|
|
3254
3546
|
}
|
|
3255
3547
|
};
|
|
3256
3548
|
const validateSliderValue = (slider, fieldKey) => {
|
|
3549
|
+
const { state } = context;
|
|
3257
3550
|
const rawValue = slider.value;
|
|
3258
3551
|
if (!rawValue) {
|
|
3259
3552
|
if (!skipValidation && element.required) {
|
|
3260
|
-
|
|
3261
|
-
|
|
3553
|
+
const msg = t("required", state);
|
|
3554
|
+
errors.push(`${fieldKey}: ${msg}`);
|
|
3555
|
+
markValidity(slider, msg);
|
|
3262
3556
|
return null;
|
|
3263
3557
|
}
|
|
3264
3558
|
markValidity(slider, null);
|
|
@@ -3275,13 +3569,15 @@ function validateSliderElement(element, key, context) {
|
|
|
3275
3569
|
}
|
|
3276
3570
|
if (!skipValidation) {
|
|
3277
3571
|
if (value < min) {
|
|
3278
|
-
|
|
3279
|
-
|
|
3572
|
+
const msg = t("minValue", state, { min });
|
|
3573
|
+
errors.push(`${fieldKey}: ${msg}`);
|
|
3574
|
+
markValidity(slider, msg);
|
|
3280
3575
|
return value;
|
|
3281
3576
|
}
|
|
3282
3577
|
if (value > max) {
|
|
3283
|
-
|
|
3284
|
-
|
|
3578
|
+
const msg = t("maxValue", state, { max });
|
|
3579
|
+
errors.push(`${fieldKey}: ${msg}`);
|
|
3580
|
+
markValidity(slider, msg);
|
|
3285
3581
|
return value;
|
|
3286
3582
|
}
|
|
3287
3583
|
}
|
|
@@ -3298,17 +3594,18 @@ function validateSliderElement(element, key, context) {
|
|
|
3298
3594
|
values.push(value);
|
|
3299
3595
|
});
|
|
3300
3596
|
if (!skipValidation) {
|
|
3597
|
+
const { state } = context;
|
|
3301
3598
|
const minCount = element.minCount ?? 1;
|
|
3302
3599
|
const maxCount = element.maxCount ?? Infinity;
|
|
3303
3600
|
const filteredValues = values.filter((v) => v !== null);
|
|
3304
3601
|
if (element.required && filteredValues.length === 0) {
|
|
3305
|
-
errors.push(`${key}: required`);
|
|
3602
|
+
errors.push(`${key}: ${t("required", state)}`);
|
|
3306
3603
|
}
|
|
3307
3604
|
if (filteredValues.length < minCount) {
|
|
3308
|
-
errors.push(`${key}:
|
|
3605
|
+
errors.push(`${key}: ${t("minItems", state, { min: minCount })}`);
|
|
3309
3606
|
}
|
|
3310
3607
|
if (filteredValues.length > maxCount) {
|
|
3311
|
-
errors.push(`${key}:
|
|
3608
|
+
errors.push(`${key}: ${t("maxItems", state, { max: maxCount })}`);
|
|
3312
3609
|
}
|
|
3313
3610
|
}
|
|
3314
3611
|
return { value: values, errors };
|
|
@@ -3318,7 +3615,7 @@ function validateSliderElement(element, key, context) {
|
|
|
3318
3615
|
);
|
|
3319
3616
|
if (!slider) {
|
|
3320
3617
|
if (!skipValidation && element.required) {
|
|
3321
|
-
errors.push(`${key}: required`);
|
|
3618
|
+
errors.push(`${key}: ${t("required", context.state)}`);
|
|
3322
3619
|
}
|
|
3323
3620
|
return { value: null, errors };
|
|
3324
3621
|
}
|
|
@@ -3470,10 +3767,6 @@ function renderSingleContainerElement(element, ctx, wrapper, pathKey) {
|
|
|
3470
3767
|
const containerWrap = document.createElement("div");
|
|
3471
3768
|
containerWrap.className = "border border-gray-200 rounded-lg p-4 bg-gray-50";
|
|
3472
3769
|
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
3770
|
const itemsWrap = document.createElement("div");
|
|
3478
3771
|
const columns = element.columns || 1;
|
|
3479
3772
|
if (columns === 1) {
|
|
@@ -3481,8 +3774,6 @@ function renderSingleContainerElement(element, ctx, wrapper, pathKey) {
|
|
|
3481
3774
|
} else {
|
|
3482
3775
|
itemsWrap.className = `grid grid-cols-${columns} gap-4`;
|
|
3483
3776
|
}
|
|
3484
|
-
containerWrap.appendChild(header);
|
|
3485
|
-
header.appendChild(left);
|
|
3486
3777
|
if (!ctx.state.config.readonly) {
|
|
3487
3778
|
const hintsElement = createPrefillHints(element, pathKey);
|
|
3488
3779
|
if (hintsElement) {
|
|
@@ -3503,21 +3794,16 @@ function renderSingleContainerElement(element, ctx, wrapper, pathKey) {
|
|
|
3503
3794
|
}
|
|
3504
3795
|
});
|
|
3505
3796
|
containerWrap.appendChild(itemsWrap);
|
|
3506
|
-
left.innerHTML = `<span>${element.label || element.key}</span>`;
|
|
3507
3797
|
wrapper.appendChild(containerWrap);
|
|
3508
3798
|
}
|
|
3509
3799
|
function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
|
|
3510
3800
|
const state = ctx.state;
|
|
3511
3801
|
const containerWrap = document.createElement("div");
|
|
3512
3802
|
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";
|
|
3803
|
+
const countDisplay = document.createElement("span");
|
|
3804
|
+
countDisplay.className = "text-sm text-gray-500";
|
|
3517
3805
|
const itemsWrap = document.createElement("div");
|
|
3518
3806
|
itemsWrap.className = "space-y-4";
|
|
3519
|
-
containerWrap.appendChild(header);
|
|
3520
|
-
header.appendChild(left);
|
|
3521
3807
|
if (!ctx.state.config.readonly) {
|
|
3522
3808
|
const hintsElement = createPrefillHints(element, element.key);
|
|
3523
3809
|
if (hintsElement) {
|
|
@@ -3531,7 +3817,7 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
|
|
|
3531
3817
|
const createAddButton = () => {
|
|
3532
3818
|
const add = document.createElement("button");
|
|
3533
3819
|
add.type = "button";
|
|
3534
|
-
add.className = "add-container-btn
|
|
3820
|
+
add.className = "add-container-btn px-3 py-1 rounded";
|
|
3535
3821
|
add.style.cssText = `
|
|
3536
3822
|
color: var(--fb-primary-color);
|
|
3537
3823
|
border: var(--fb-border-width) solid var(--fb-primary-color);
|
|
@@ -3612,7 +3898,7 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
|
|
|
3612
3898
|
existingAddBtn.style.opacity = currentCount >= max ? "0.5" : "1";
|
|
3613
3899
|
existingAddBtn.style.pointerEvents = currentCount >= max ? "none" : "auto";
|
|
3614
3900
|
}
|
|
3615
|
-
|
|
3901
|
+
countDisplay.textContent = `${currentCount}/${max === Infinity ? "\u221E" : max}`;
|
|
3616
3902
|
};
|
|
3617
3903
|
if (pre && Array.isArray(pre)) {
|
|
3618
3904
|
pre.forEach((prefillObj, idx) => {
|
|
@@ -3719,7 +4005,11 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
|
|
|
3719
4005
|
}
|
|
3720
4006
|
containerWrap.appendChild(itemsWrap);
|
|
3721
4007
|
if (!state.config.readonly) {
|
|
3722
|
-
|
|
4008
|
+
const addRow = document.createElement("div");
|
|
4009
|
+
addRow.className = "flex items-center gap-3 mt-2";
|
|
4010
|
+
addRow.appendChild(createAddButton());
|
|
4011
|
+
addRow.appendChild(countDisplay);
|
|
4012
|
+
containerWrap.appendChild(addRow);
|
|
3723
4013
|
}
|
|
3724
4014
|
updateAddButton();
|
|
3725
4015
|
wrapper.appendChild(containerWrap);
|
|
@@ -3744,16 +4034,17 @@ function validateContainerElement(element, key, context) {
|
|
|
3744
4034
|
}
|
|
3745
4035
|
const validateContainerCount = (key2, items, element2) => {
|
|
3746
4036
|
if (skipValidation) return;
|
|
4037
|
+
const { state } = context;
|
|
3747
4038
|
const minItems = "minCount" in element2 ? element2.minCount ?? 0 : 0;
|
|
3748
4039
|
const maxItems = "maxCount" in element2 ? element2.maxCount ?? Infinity : Infinity;
|
|
3749
4040
|
if (element2.required && items.length === 0) {
|
|
3750
|
-
errors.push(`${key2}: required`);
|
|
4041
|
+
errors.push(`${key2}: ${t("required", state)}`);
|
|
3751
4042
|
}
|
|
3752
4043
|
if (items.length < minItems) {
|
|
3753
|
-
errors.push(`${key2}:
|
|
4044
|
+
errors.push(`${key2}: ${t("minItems", state, { min: minItems })}`);
|
|
3754
4045
|
}
|
|
3755
4046
|
if (items.length > maxItems) {
|
|
3756
|
-
errors.push(`${key2}:
|
|
4047
|
+
errors.push(`${key2}: ${t("maxItems", state, { max: maxItems })}`);
|
|
3757
4048
|
}
|
|
3758
4049
|
};
|
|
3759
4050
|
if ("multiple" in element && element.multiple) {
|
|
@@ -4300,7 +4591,7 @@ function dispatchToRenderer(element, ctx, wrapper, pathKey) {
|
|
|
4300
4591
|
default: {
|
|
4301
4592
|
const unsupported = document.createElement("div");
|
|
4302
4593
|
unsupported.className = "text-red-500 text-sm";
|
|
4303
|
-
unsupported.textContent =
|
|
4594
|
+
unsupported.textContent = t("unsupportedFieldType", ctx.state, { type: element.type });
|
|
4304
4595
|
wrapper.appendChild(unsupported);
|
|
4305
4596
|
}
|
|
4306
4597
|
}
|
|
@@ -4344,31 +4635,112 @@ var defaultConfig = {
|
|
|
4344
4635
|
locale: "en",
|
|
4345
4636
|
translations: {
|
|
4346
4637
|
en: {
|
|
4347
|
-
|
|
4638
|
+
// UI texts
|
|
4348
4639
|
removeElement: "Remove",
|
|
4349
|
-
uploadText: "Upload",
|
|
4350
|
-
dragDropText: "or drag and drop files",
|
|
4351
|
-
dragDropTextSingle: "or drag and drop file",
|
|
4352
4640
|
clickDragText: "Click or drag file",
|
|
4641
|
+
clickDragTextMultiple: "Click or drag files",
|
|
4353
4642
|
noFileSelected: "No file selected",
|
|
4354
4643
|
noFilesSelected: "No files selected",
|
|
4355
|
-
downloadButton: "Download"
|
|
4644
|
+
downloadButton: "Download",
|
|
4645
|
+
changeButton: "Change",
|
|
4646
|
+
placeholderText: "Enter text",
|
|
4647
|
+
previewAlt: "Preview",
|
|
4648
|
+
previewUnavailable: "Preview unavailable",
|
|
4649
|
+
previewError: "Preview error",
|
|
4650
|
+
videoNotSupported: "Your browser does not support the video tag.",
|
|
4651
|
+
// Field hints
|
|
4652
|
+
hintLengthRange: "{min}-{max} chars",
|
|
4653
|
+
hintMaxLength: "\u2264{max} chars",
|
|
4654
|
+
hintMinLength: "\u2265{min} chars",
|
|
4655
|
+
hintValueRange: "{min}-{max}",
|
|
4656
|
+
hintMaxValue: "\u2264{max}",
|
|
4657
|
+
hintMinValue: "\u2265{min}",
|
|
4658
|
+
hintMaxSize: "\u2264{size}MB",
|
|
4659
|
+
hintFormats: "{formats}",
|
|
4660
|
+
hintRequired: "Required",
|
|
4661
|
+
hintOptional: "Optional",
|
|
4662
|
+
hintPattern: "Format: {pattern}",
|
|
4663
|
+
fileCountSingle: "{count} file",
|
|
4664
|
+
fileCountPlural: "{count} files",
|
|
4665
|
+
fileCountRange: "({min}-{max})",
|
|
4666
|
+
// Validation errors
|
|
4667
|
+
required: "Required",
|
|
4668
|
+
minItems: "Minimum {min} items required",
|
|
4669
|
+
maxItems: "Maximum {max} items allowed",
|
|
4670
|
+
minLength: "Minimum {min} characters",
|
|
4671
|
+
maxLength: "Maximum {max} characters",
|
|
4672
|
+
minValue: "Must be at least {min}",
|
|
4673
|
+
maxValue: "Must be at most {max}",
|
|
4674
|
+
patternMismatch: "Invalid format",
|
|
4675
|
+
invalidPattern: "Invalid pattern in schema",
|
|
4676
|
+
notANumber: "Must be a number",
|
|
4677
|
+
invalidHexColour: "Invalid hex color",
|
|
4678
|
+
minFiles: "Minimum {min} files required",
|
|
4679
|
+
maxFiles: "Maximum {max} files allowed",
|
|
4680
|
+
unsupportedFieldType: "Unsupported field type: {type}"
|
|
4356
4681
|
},
|
|
4357
4682
|
ru: {
|
|
4358
|
-
|
|
4683
|
+
// UI texts
|
|
4359
4684
|
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
4685
|
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",
|
|
4686
|
+
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
4687
|
noFileSelected: "\u0424\u0430\u0439\u043B \u043D\u0435 \u0432\u044B\u0431\u0440\u0430\u043D",
|
|
4365
4688
|
noFilesSelected: "\u041D\u0435\u0442 \u0444\u0430\u0439\u043B\u043E\u0432",
|
|
4366
|
-
downloadButton: "\u0421\u043A\u0430\u0447\u0430\u0442\u044C"
|
|
4689
|
+
downloadButton: "\u0421\u043A\u0430\u0447\u0430\u0442\u044C",
|
|
4690
|
+
changeButton: "\u0418\u0437\u043C\u0435\u043D\u0438\u0442\u044C",
|
|
4691
|
+
placeholderText: "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0442\u0435\u043A\u0441\u0442",
|
|
4692
|
+
previewAlt: "\u041F\u0440\u0435\u0434\u043F\u0440\u043E\u0441\u043C\u043E\u0442\u0440",
|
|
4693
|
+
previewUnavailable: "\u041F\u0440\u0435\u0434\u043F\u0440\u043E\u0441\u043C\u043E\u0442\u0440 \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D",
|
|
4694
|
+
previewError: "\u041E\u0448\u0438\u0431\u043A\u0430 \u043F\u0440\u0435\u0434\u043F\u0440\u043E\u0441\u043C\u043E\u0442\u0440\u0430",
|
|
4695
|
+
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.",
|
|
4696
|
+
// Field hints
|
|
4697
|
+
hintLengthRange: "{min}-{max} \u0441\u0438\u043C\u0432\u043E\u043B\u043E\u0432",
|
|
4698
|
+
hintMaxLength: "\u2264{max} \u0441\u0438\u043C\u0432\u043E\u043B\u043E\u0432",
|
|
4699
|
+
hintMinLength: "\u2265{min} \u0441\u0438\u043C\u0432\u043E\u043B\u043E\u0432",
|
|
4700
|
+
hintValueRange: "{min}-{max}",
|
|
4701
|
+
hintMaxValue: "\u2264{max}",
|
|
4702
|
+
hintMinValue: "\u2265{min}",
|
|
4703
|
+
hintMaxSize: "\u2264{size}\u041C\u0411",
|
|
4704
|
+
hintFormats: "{formats}",
|
|
4705
|
+
hintRequired: "\u041E\u0431\u044F\u0437\u0430\u0442\u0435\u043B\u044C\u043D\u043E\u0435",
|
|
4706
|
+
hintOptional: "\u041D\u0435\u043E\u0431\u044F\u0437\u0430\u0442\u0435\u043B\u044C\u043D\u043E\u0435",
|
|
4707
|
+
hintPattern: "\u0424\u043E\u0440\u043C\u0430\u0442: {pattern}",
|
|
4708
|
+
fileCountSingle: "{count} \u0444\u0430\u0439\u043B",
|
|
4709
|
+
fileCountPlural: "{count} \u0444\u0430\u0439\u043B\u043E\u0432",
|
|
4710
|
+
fileCountRange: "({min}-{max})",
|
|
4711
|
+
// Validation errors
|
|
4712
|
+
required: "\u041E\u0431\u044F\u0437\u0430\u0442\u0435\u043B\u044C\u043D\u043E\u0435 \u043F\u043E\u043B\u0435",
|
|
4713
|
+
minItems: "\u041C\u0438\u043D\u0438\u043C\u0443\u043C {min} \u044D\u043B\u0435\u043C\u0435\u043D\u0442\u043E\u0432",
|
|
4714
|
+
maxItems: "\u041C\u0430\u043A\u0441\u0438\u043C\u0443\u043C {max} \u044D\u043B\u0435\u043C\u0435\u043D\u0442\u043E\u0432",
|
|
4715
|
+
minLength: "\u041C\u0438\u043D\u0438\u043C\u0443\u043C {min} \u0441\u0438\u043C\u0432\u043E\u043B\u043E\u0432",
|
|
4716
|
+
maxLength: "\u041C\u0430\u043A\u0441\u0438\u043C\u0443\u043C {max} \u0441\u0438\u043C\u0432\u043E\u043B\u043E\u0432",
|
|
4717
|
+
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}",
|
|
4718
|
+
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}",
|
|
4719
|
+
patternMismatch: "\u041D\u0435\u0432\u0435\u0440\u043D\u044B\u0439 \u0444\u043E\u0440\u043C\u0430\u0442",
|
|
4720
|
+
invalidPattern: "\u041D\u0435\u0432\u0435\u0440\u043D\u044B\u0439 \u043F\u0430\u0442\u0442\u0435\u0440\u043D \u0432 \u0441\u0445\u0435\u043C\u0435",
|
|
4721
|
+
notANumber: "\u0414\u043E\u043B\u0436\u043D\u043E \u0431\u044B\u0442\u044C \u0447\u0438\u0441\u043B\u043E\u043C",
|
|
4722
|
+
invalidHexColour: "\u041D\u0435\u0432\u0435\u0440\u043D\u044B\u0439 \u0444\u043E\u0440\u043C\u0430\u0442 \u0446\u0432\u0435\u0442\u0430",
|
|
4723
|
+
minFiles: "\u041C\u0438\u043D\u0438\u043C\u0443\u043C {min} \u0444\u0430\u0439\u043B\u043E\u0432",
|
|
4724
|
+
maxFiles: "\u041C\u0430\u043A\u0441\u0438\u043C\u0443\u043C {max} \u0444\u0430\u0439\u043B\u043E\u0432",
|
|
4725
|
+
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
4726
|
}
|
|
4368
4727
|
},
|
|
4369
4728
|
theme: {}
|
|
4370
4729
|
};
|
|
4371
4730
|
function createInstanceState(config) {
|
|
4731
|
+
const mergedTranslations = {
|
|
4732
|
+
...defaultConfig.translations
|
|
4733
|
+
};
|
|
4734
|
+
if (config?.translations) {
|
|
4735
|
+
for (const [locale, userTranslations] of Object.entries(
|
|
4736
|
+
config.translations
|
|
4737
|
+
)) {
|
|
4738
|
+
mergedTranslations[locale] = {
|
|
4739
|
+
...defaultConfig.translations[locale] || {},
|
|
4740
|
+
...userTranslations
|
|
4741
|
+
};
|
|
4742
|
+
}
|
|
4743
|
+
}
|
|
4372
4744
|
return {
|
|
4373
4745
|
schema: null,
|
|
4374
4746
|
formRoot: null,
|
|
@@ -4377,7 +4749,8 @@ function createInstanceState(config) {
|
|
|
4377
4749
|
version: "1.0.0",
|
|
4378
4750
|
config: {
|
|
4379
4751
|
...defaultConfig,
|
|
4380
|
-
...config
|
|
4752
|
+
...config,
|
|
4753
|
+
translations: mergedTranslations
|
|
4381
4754
|
},
|
|
4382
4755
|
debounceTimer: null
|
|
4383
4756
|
};
|