@dmitryvim/form-builder 0.2.34 → 0.3.1
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 +453 -174
- package/dist/browser/formbuilder.v0.3.1.min.js +1441 -0
- package/dist/cjs/index.cjs +1331 -505
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/esm/index.js +1317 -502
- package/dist/esm/index.js.map +1 -1
- package/dist/form-builder.js +453 -174
- package/dist/types/components/boolean.d.ts +4 -0
- package/dist/types/components/file/library.d.ts +40 -16
- package/dist/types/components/file/upload.d.ts +111 -2
- package/dist/types/components/text.d.ts +12 -1
- package/dist/types/index.d.ts +1 -1
- package/dist/types/instance/FormBuilderInstance.d.ts +14 -1
- package/dist/types/styles/theme.d.ts +47 -0
- package/dist/types/types/component-operations.d.ts +7 -0
- package/dist/types/types/index.d.ts +1 -1
- package/dist/types/types/schema.d.ts +40 -1
- package/dist/types/utils/styles.d.ts +47 -0
- package/package.json +1 -1
- package/dist/browser/formbuilder.v0.2.34.min.js +0 -1162
package/dist/esm/index.js
CHANGED
|
@@ -399,6 +399,113 @@ function deepEqual(a, b) {
|
|
|
399
399
|
}
|
|
400
400
|
|
|
401
401
|
// src/utils/styles.ts
|
|
402
|
+
function clearFieldError(input) {
|
|
403
|
+
const name = input.getAttribute("name");
|
|
404
|
+
if (!name) return;
|
|
405
|
+
const doc = input.ownerDocument || document;
|
|
406
|
+
const errorNode = doc.getElementById(`error-${name}`);
|
|
407
|
+
if (errorNode) errorNode.remove();
|
|
408
|
+
}
|
|
409
|
+
var BIN_ICON_SVG = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"/><path d="M10 11v6"/><path d="M14 11v6"/><path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>';
|
|
410
|
+
function ensureThemingHooks(doc) {
|
|
411
|
+
if (doc.head.querySelector("[data-fb-theming-hooks]")) return;
|
|
412
|
+
const style = doc.createElement("style");
|
|
413
|
+
style.setAttribute("data-fb-theming-hooks", "");
|
|
414
|
+
style.textContent = `
|
|
415
|
+
[data-fb-slide-card] {
|
|
416
|
+
background: var(--fb-slide-card-bg);
|
|
417
|
+
box-shadow: var(--fb-slide-card-shadow);
|
|
418
|
+
border-radius: var(--fb-slide-card-radius);
|
|
419
|
+
min-height: var(--fb-slide-card-min-height);
|
|
420
|
+
padding: var(--fb-slide-card-padding);
|
|
421
|
+
}
|
|
422
|
+
[data-fb-label-row] > label {
|
|
423
|
+
font-size: var(--fb-label-section-font-size);
|
|
424
|
+
letter-spacing: var(--fb-label-section-letter-spacing);
|
|
425
|
+
text-transform: var(--fb-label-section-text-transform);
|
|
426
|
+
}
|
|
427
|
+
/* Per-item remove (trash) button used by multi-container items. Shares the
|
|
428
|
+
same faint-on-rest, error-on-hover palette as .fb-chip-remove. */
|
|
429
|
+
.fb-item-remove {
|
|
430
|
+
color: var(--fb-text-faint-color, #94a3b8);
|
|
431
|
+
background-color: transparent;
|
|
432
|
+
transition: color var(--fb-transition-duration), background-color var(--fb-transition-duration);
|
|
433
|
+
}
|
|
434
|
+
.fb-item-remove:hover {
|
|
435
|
+
color: var(--fb-error-color);
|
|
436
|
+
background-color: var(--fb-background-hover-color);
|
|
437
|
+
}
|
|
438
|
+
/* Prefill-suggestion pills rendered by createPrefillHints. Outline pill at rest,
|
|
439
|
+
soft-fill on hover, solid-fill when selected. All colors flow from the active
|
|
440
|
+
theme \u2014 consumers don't need to ship their own CSS. */
|
|
441
|
+
.fb-prefill-hint {
|
|
442
|
+
padding: 0.25rem 0.625rem;
|
|
443
|
+
border: var(--fb-border-width) solid var(--fb-primary-color);
|
|
444
|
+
border-radius: 9999px;
|
|
445
|
+
background: var(--fb-background-color);
|
|
446
|
+
color: var(--fb-primary-color);
|
|
447
|
+
font-size: var(--fb-font-size-small);
|
|
448
|
+
font-weight: var(--fb-font-weight-medium);
|
|
449
|
+
font-family: var(--fb-font-family);
|
|
450
|
+
cursor: pointer;
|
|
451
|
+
transition: background-color var(--fb-transition-duration), border-color var(--fb-transition-duration), color var(--fb-transition-duration);
|
|
452
|
+
}
|
|
453
|
+
.fb-prefill-hint:hover {
|
|
454
|
+
background: var(--fb-primary-soft-color);
|
|
455
|
+
border-color: var(--fb-primary-hover-color);
|
|
456
|
+
color: var(--fb-primary-hover-color);
|
|
457
|
+
}
|
|
458
|
+
.fb-prefill-hint:focus-visible {
|
|
459
|
+
outline: var(--fb-focus-ring-width) solid var(--fb-focus-ring-color);
|
|
460
|
+
outline-offset: 2px;
|
|
461
|
+
}
|
|
462
|
+
.fb-prefill-hint[aria-pressed="true"],
|
|
463
|
+
.fb-prefill-hint.active {
|
|
464
|
+
background: var(--fb-primary-color);
|
|
465
|
+
color: #ffffff;
|
|
466
|
+
border-color: var(--fb-primary-color);
|
|
467
|
+
}
|
|
468
|
+
`;
|
|
469
|
+
doc.head.appendChild(style);
|
|
470
|
+
}
|
|
471
|
+
function applyAutoExpand(textarea) {
|
|
472
|
+
textarea.style.overflow = "hidden";
|
|
473
|
+
textarea.style.resize = "none";
|
|
474
|
+
const lineCount = (textarea.value.match(/\n/g) || []).length + 1;
|
|
475
|
+
textarea.rows = Math.max(1, lineCount);
|
|
476
|
+
const resize = () => {
|
|
477
|
+
if (!textarea.isConnected) return;
|
|
478
|
+
textarea.style.height = "0";
|
|
479
|
+
const cs = getComputedStyle(textarea);
|
|
480
|
+
const borderY = parseFloat(cs.borderTopWidth || "0") + parseFloat(cs.borderBottomWidth || "0");
|
|
481
|
+
textarea.style.height = `${textarea.scrollHeight + borderY}px`;
|
|
482
|
+
};
|
|
483
|
+
textarea.addEventListener("input", resize);
|
|
484
|
+
setTimeout(() => {
|
|
485
|
+
if (textarea.isConnected) resize();
|
|
486
|
+
}, 0);
|
|
487
|
+
}
|
|
488
|
+
function applySingleLineMode(textarea) {
|
|
489
|
+
textarea.addEventListener("keydown", (e) => {
|
|
490
|
+
if (e.key === "Enter") {
|
|
491
|
+
e.preventDefault();
|
|
492
|
+
}
|
|
493
|
+
});
|
|
494
|
+
textarea.addEventListener("paste", (e) => {
|
|
495
|
+
const pasted = e.clipboardData?.getData("text") ?? "";
|
|
496
|
+
if (!/[\r\n]/.test(pasted)) return;
|
|
497
|
+
e.preventDefault();
|
|
498
|
+
const cleaned = pasted.replace(/[\r\n]+/g, " ");
|
|
499
|
+
const start = textarea.selectionStart ?? textarea.value.length;
|
|
500
|
+
const end = textarea.selectionEnd ?? textarea.value.length;
|
|
501
|
+
const before = textarea.value.slice(0, start);
|
|
502
|
+
const after = textarea.value.slice(end);
|
|
503
|
+
textarea.value = before + cleaned + after;
|
|
504
|
+
const pos = start + cleaned.length;
|
|
505
|
+
textarea.setSelectionRange(pos, pos);
|
|
506
|
+
textarea.dispatchEvent(new Event("input", { bubbles: true }));
|
|
507
|
+
});
|
|
508
|
+
}
|
|
402
509
|
function mountCounterInLabel(wrapper, counter) {
|
|
403
510
|
const labelRow = wrapper.querySelector(
|
|
404
511
|
":scope > [data-fb-label-row]"
|
|
@@ -560,46 +667,94 @@ function applyActionButtonStyles(button, isFormLevel = false) {
|
|
|
560
667
|
}
|
|
561
668
|
|
|
562
669
|
// src/components/text.ts
|
|
563
|
-
function
|
|
670
|
+
function ensureChipStyles(doc) {
|
|
671
|
+
if (doc.head.querySelector("[data-fb-chip-styles]")) return;
|
|
672
|
+
const style = doc.createElement("style");
|
|
673
|
+
style.setAttribute("data-fb-chip-styles", "");
|
|
674
|
+
style.textContent = `
|
|
675
|
+
.fb-chip-list { display: flex; flex-direction: column; gap: 4px; }
|
|
676
|
+
.fb-chip {
|
|
677
|
+
display: flex;
|
|
678
|
+
align-items: center;
|
|
679
|
+
gap: 8px;
|
|
680
|
+
padding: 5px 6px 5px 10px;
|
|
681
|
+
background: var(--fb-chip-bg, var(--fb-background-color, #fff));
|
|
682
|
+
border: 1px solid var(--fb-chip-border, var(--fb-border-color, #e2e8f0));
|
|
683
|
+
border-radius: 6px;
|
|
684
|
+
position: relative;
|
|
685
|
+
transition: border-color var(--fb-transition-duration, 0.15s);
|
|
686
|
+
}
|
|
687
|
+
.fb-chip:hover { border-color: var(--fb-border-hover-color, var(--fb-border-color, #cbd5e1)); }
|
|
688
|
+
.fb-chip:focus-within { border-color: var(--fb-border-focus-color, var(--fb-primary-color, #2f5bea)); }
|
|
689
|
+
.fb-chip-dot {
|
|
690
|
+
flex: 0 0 6px;
|
|
691
|
+
width: 6px;
|
|
692
|
+
height: 6px;
|
|
693
|
+
border-radius: 50%;
|
|
694
|
+
background: var(--fb-chip-dot, var(--fb-primary-color, #2f5bea));
|
|
695
|
+
}
|
|
696
|
+
.fb-chip-input {
|
|
697
|
+
flex: 1;
|
|
698
|
+
min-width: 0;
|
|
699
|
+
padding: 2px 0;
|
|
700
|
+
border: 0;
|
|
701
|
+
outline: none;
|
|
702
|
+
background: transparent;
|
|
703
|
+
color: var(--fb-chip-text, var(--fb-text-color, inherit));
|
|
704
|
+
font-size: var(--fb-font-size, 14px);
|
|
705
|
+
font-family: var(--fb-font-family, inherit);
|
|
706
|
+
line-height: 1.4;
|
|
707
|
+
}
|
|
708
|
+
.fb-chip-input::placeholder { color: var(--fb-text-placeholder-color, #94a3b8); }
|
|
709
|
+
.fb-chip-input:read-only { color: var(--fb-text-secondary-color, #475569); }
|
|
710
|
+
.fb-chip-remove {
|
|
711
|
+
flex: 0 0 auto;
|
|
712
|
+
width: 22px;
|
|
713
|
+
height: 22px;
|
|
714
|
+
display: inline-flex;
|
|
715
|
+
align-items: center;
|
|
716
|
+
justify-content: center;
|
|
717
|
+
padding: 0;
|
|
718
|
+
border: 0;
|
|
719
|
+
border-radius: 4px;
|
|
720
|
+
background: transparent;
|
|
721
|
+
color: var(--fb-text-faint-color, #94a3b8);
|
|
722
|
+
cursor: pointer;
|
|
723
|
+
opacity: 0;
|
|
724
|
+
transition: opacity 0.12s, color 0.12s, background-color 0.12s;
|
|
725
|
+
}
|
|
726
|
+
.fb-chip:hover .fb-chip-remove,
|
|
727
|
+
.fb-chip-remove:focus-visible { opacity: 1; }
|
|
728
|
+
.fb-chip-remove:hover {
|
|
729
|
+
color: var(--fb-error-color, #dc2626);
|
|
730
|
+
background: var(--fb-background-hover-color, #f1f5f9);
|
|
731
|
+
}
|
|
732
|
+
.fb-chip-remove:disabled { opacity: 0 !important; pointer-events: none; }
|
|
733
|
+
`;
|
|
734
|
+
doc.head.appendChild(style);
|
|
735
|
+
}
|
|
736
|
+
function createCharCounter(element, input) {
|
|
564
737
|
const counter = document.createElement("span");
|
|
565
738
|
counter.className = "char-counter";
|
|
566
739
|
counter.style.cssText = `
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
740
|
+
margin-top: 4px;
|
|
741
|
+
padding-right: 12px;
|
|
742
|
+
text-align: right;
|
|
570
743
|
font-size: var(--fb-font-size-small);
|
|
571
|
-
|
|
744
|
+
line-height: 1;
|
|
745
|
+
color: var(--fb-error-color);
|
|
572
746
|
pointer-events: none;
|
|
573
|
-
|
|
574
|
-
padding: 0 4px;
|
|
747
|
+
display: none;
|
|
575
748
|
`;
|
|
576
749
|
const updateCounter = () => {
|
|
577
750
|
const len = input.value.length;
|
|
578
|
-
const min = element.minLength;
|
|
579
751
|
const max = element.maxLength;
|
|
580
|
-
if (
|
|
581
|
-
counter.textContent = "";
|
|
582
|
-
return;
|
|
583
|
-
}
|
|
584
|
-
if (len === 0 || min != null && len < min) {
|
|
585
|
-
if (min != null && max != null) {
|
|
586
|
-
counter.textContent = `${min}-${max}`;
|
|
587
|
-
} else if (max != null) {
|
|
588
|
-
counter.textContent = `\u2264${max}`;
|
|
589
|
-
} else if (min != null) {
|
|
590
|
-
counter.textContent = `\u2265${min}`;
|
|
591
|
-
}
|
|
592
|
-
counter.style.color = "var(--fb-text-secondary-color)";
|
|
593
|
-
} else if (max != null && len > max) {
|
|
752
|
+
if (max != null && len > max) {
|
|
594
753
|
counter.textContent = `${len}/${max}`;
|
|
595
|
-
counter.style.
|
|
754
|
+
counter.style.display = "block";
|
|
596
755
|
} else {
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
} else {
|
|
600
|
-
counter.textContent = `${len}`;
|
|
601
|
-
}
|
|
602
|
-
counter.style.color = "var(--fb-text-secondary-color)";
|
|
756
|
+
counter.textContent = "";
|
|
757
|
+
counter.style.display = "none";
|
|
603
758
|
}
|
|
604
759
|
};
|
|
605
760
|
input.addEventListener("input", updateCounter);
|
|
@@ -612,26 +767,32 @@ function renderTextElement(element, ctx, wrapper, pathKey) {
|
|
|
612
767
|
const inputWrapper = document.createElement("div");
|
|
613
768
|
inputWrapper.style.cssText = "position: relative;";
|
|
614
769
|
const hasCharCounter = !readonly && (element.minLength != null || element.maxLength != null);
|
|
615
|
-
const textInput = document.createElement("
|
|
616
|
-
textInput.
|
|
770
|
+
const textInput = document.createElement("textarea");
|
|
771
|
+
textInput.rows = 1;
|
|
617
772
|
textInput.className = "w-full rounded-lg";
|
|
618
773
|
textInput.style.cssText = `
|
|
619
774
|
padding: var(--fb-input-padding-y) var(--fb-input-padding-x);
|
|
620
|
-
${hasCharCounter ? "padding-right: 60px;" : ""}
|
|
621
775
|
border: var(--fb-border-width) solid var(--fb-border-color);
|
|
622
776
|
border-radius: var(--fb-border-radius);
|
|
623
777
|
background-color: ${readonly ? "var(--fb-background-readonly-color)" : "var(--fb-background-color)"};
|
|
624
778
|
color: var(--fb-text-color);
|
|
625
779
|
font-size: var(--fb-font-size);
|
|
626
780
|
font-family: var(--fb-font-family);
|
|
781
|
+
line-height: var(--fb-line-height, 1.5);
|
|
627
782
|
transition: all var(--fb-transition-duration) ease-in-out;
|
|
628
783
|
width: 100%;
|
|
629
784
|
box-sizing: border-box;
|
|
785
|
+
resize: none;
|
|
786
|
+
overflow: hidden;
|
|
787
|
+
word-break: break-word;
|
|
788
|
+
overflow-wrap: anywhere;
|
|
630
789
|
`;
|
|
631
790
|
textInput.name = pathKey;
|
|
632
791
|
textInput.placeholder = element.placeholder || "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0442\u0435\u043A\u0441\u0442";
|
|
633
792
|
textInput.value = ctx.prefill[element.key] || element.default || "";
|
|
634
793
|
textInput.readOnly = readonly;
|
|
794
|
+
applySingleLineMode(textInput);
|
|
795
|
+
applyAutoExpand(textInput);
|
|
635
796
|
if (!readonly) {
|
|
636
797
|
textInput.addEventListener("focus", () => {
|
|
637
798
|
textInput.style.borderColor = "var(--fb-border-focus-color)";
|
|
@@ -663,7 +824,7 @@ function renderTextElement(element, ctx, wrapper, pathKey) {
|
|
|
663
824
|
}
|
|
664
825
|
inputWrapper.appendChild(textInput);
|
|
665
826
|
if (hasCharCounter) {
|
|
666
|
-
const counter = createCharCounter(element, textInput
|
|
827
|
+
const counter = createCharCounter(element, textInput);
|
|
667
828
|
inputWrapper.appendChild(counter);
|
|
668
829
|
}
|
|
669
830
|
wrapper.appendChild(inputWrapper);
|
|
@@ -673,132 +834,83 @@ function renderMultipleTextElement(element, ctx, wrapper, pathKey) {
|
|
|
673
834
|
const readonly = isElementReadonly(element, state, ctx);
|
|
674
835
|
const prefillValues = ctx.prefill[element.key] || [];
|
|
675
836
|
const values = Array.isArray(prefillValues) ? [...prefillValues] : [];
|
|
676
|
-
const hasCharCounter = !readonly && (element.minLength != null || element.maxLength != null);
|
|
677
837
|
const minCount = element.minCount ?? 1;
|
|
678
838
|
const maxCount = element.maxCount ?? Infinity;
|
|
679
839
|
while (values.length < minCount) {
|
|
680
840
|
values.push(element.default || "");
|
|
681
841
|
}
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
842
|
+
ensureChipStyles(document);
|
|
843
|
+
const list = document.createElement("div");
|
|
844
|
+
list.className = "fb-chip-list";
|
|
845
|
+
wrapper.appendChild(list);
|
|
685
846
|
function updateIndices() {
|
|
686
|
-
const items =
|
|
687
|
-
items.forEach((
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
847
|
+
const items = list.querySelectorAll(".fb-chip-input");
|
|
848
|
+
items.forEach((input, index) => {
|
|
849
|
+
input.name = `${pathKey}[${index}]`;
|
|
850
|
+
const chip = input.closest(".fb-chip");
|
|
851
|
+
const sib = chip?.nextElementSibling;
|
|
852
|
+
if (sib && sib.classList.contains("error-message")) {
|
|
853
|
+
sib.id = `error-${input.name}`;
|
|
691
854
|
}
|
|
692
855
|
});
|
|
693
856
|
}
|
|
694
|
-
function
|
|
695
|
-
const
|
|
696
|
-
|
|
697
|
-
const
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
font-size: var(--fb-font-size);
|
|
709
|
-
font-family: var(--fb-font-family);
|
|
710
|
-
transition: all var(--fb-transition-duration) ease-in-out;
|
|
711
|
-
width: 100%;
|
|
712
|
-
box-sizing: border-box;
|
|
713
|
-
`;
|
|
714
|
-
textInput.placeholder = element.placeholder || t("placeholderText", state);
|
|
715
|
-
textInput.value = value;
|
|
716
|
-
textInput.readOnly = readonly;
|
|
717
|
-
if (!readonly) {
|
|
718
|
-
textInput.addEventListener("focus", () => {
|
|
719
|
-
textInput.style.borderColor = "var(--fb-border-focus-color)";
|
|
720
|
-
textInput.style.outline = `var(--fb-focus-ring-width) solid var(--fb-focus-ring-color)`;
|
|
721
|
-
textInput.style.outlineOffset = "0";
|
|
722
|
-
});
|
|
723
|
-
textInput.addEventListener("blur", () => {
|
|
724
|
-
textInput.style.borderColor = "var(--fb-border-color)";
|
|
725
|
-
textInput.style.outline = "none";
|
|
726
|
-
});
|
|
727
|
-
textInput.addEventListener("mouseenter", () => {
|
|
728
|
-
if (document.activeElement !== textInput) {
|
|
729
|
-
textInput.style.borderColor = "var(--fb-border-hover-color)";
|
|
730
|
-
}
|
|
731
|
-
});
|
|
732
|
-
textInput.addEventListener("mouseleave", () => {
|
|
733
|
-
if (document.activeElement !== textInput) {
|
|
734
|
-
textInput.style.borderColor = "var(--fb-border-color)";
|
|
735
|
-
}
|
|
736
|
-
});
|
|
737
|
-
}
|
|
857
|
+
function addChip(value = "") {
|
|
858
|
+
const chip = document.createElement("div");
|
|
859
|
+
chip.className = "fb-chip";
|
|
860
|
+
const dot = document.createElement("span");
|
|
861
|
+
dot.className = "fb-chip-dot";
|
|
862
|
+
dot.setAttribute("aria-hidden", "true");
|
|
863
|
+
chip.appendChild(dot);
|
|
864
|
+
const input = document.createElement("input");
|
|
865
|
+
input.type = "text";
|
|
866
|
+
input.className = "fb-chip-input";
|
|
867
|
+
input.value = value;
|
|
868
|
+
input.placeholder = element.placeholder || t("placeholderText", state);
|
|
869
|
+
input.readOnly = readonly;
|
|
870
|
+
chip.appendChild(input);
|
|
738
871
|
if (!readonly && ctx.instance) {
|
|
739
872
|
const handleChange = () => {
|
|
740
|
-
|
|
741
|
-
|
|
873
|
+
ctx.instance.triggerOnChange(
|
|
874
|
+
input.name,
|
|
875
|
+
input.value === "" ? null : input.value
|
|
876
|
+
);
|
|
742
877
|
};
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
}
|
|
746
|
-
inputContainer.appendChild(textInput);
|
|
747
|
-
if (hasCharCounter) {
|
|
748
|
-
const counter = createCharCounter(element, textInput, false);
|
|
749
|
-
inputContainer.appendChild(counter);
|
|
878
|
+
input.addEventListener("blur", handleChange);
|
|
879
|
+
input.addEventListener("input", handleChange);
|
|
750
880
|
}
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
881
|
+
if (!readonly) {
|
|
882
|
+
const rem = document.createElement("button");
|
|
883
|
+
rem.type = "button";
|
|
884
|
+
rem.className = "fb-chip-remove";
|
|
885
|
+
rem.setAttribute("aria-label", t("removeElement", state));
|
|
886
|
+
rem.innerHTML = BIN_ICON_SVG;
|
|
887
|
+
rem.onclick = () => {
|
|
888
|
+
const chips = list.querySelectorAll(".fb-chip");
|
|
889
|
+
const idx = Array.prototype.indexOf.call(chips, chip);
|
|
890
|
+
if (idx < 0) return;
|
|
891
|
+
if (chips.length <= minCount) return;
|
|
892
|
+
values.splice(idx, 1);
|
|
893
|
+
const trailingError = chip.nextElementSibling;
|
|
894
|
+
if (trailingError && trailingError.classList.contains("error-message")) {
|
|
895
|
+
trailingError.remove();
|
|
896
|
+
}
|
|
897
|
+
chip.remove();
|
|
898
|
+
updateIndices();
|
|
899
|
+
updateAddButton();
|
|
900
|
+
updateRemoveButtons();
|
|
901
|
+
};
|
|
902
|
+
chip.appendChild(rem);
|
|
756
903
|
}
|
|
904
|
+
list.appendChild(chip);
|
|
757
905
|
updateIndices();
|
|
758
|
-
return
|
|
906
|
+
return chip;
|
|
759
907
|
}
|
|
760
908
|
function updateRemoveButtons() {
|
|
761
909
|
if (readonly) return;
|
|
762
|
-
const
|
|
763
|
-
const
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
".remove-item-btn"
|
|
767
|
-
);
|
|
768
|
-
if (!removeBtn) {
|
|
769
|
-
removeBtn = document.createElement("button");
|
|
770
|
-
removeBtn.type = "button";
|
|
771
|
-
removeBtn.className = "remove-item-btn px-2 py-1 rounded";
|
|
772
|
-
removeBtn.style.cssText = `
|
|
773
|
-
color: var(--fb-error-color);
|
|
774
|
-
background-color: transparent;
|
|
775
|
-
transition: background-color var(--fb-transition-duration);
|
|
776
|
-
`;
|
|
777
|
-
removeBtn.innerHTML = "\u2715";
|
|
778
|
-
removeBtn.addEventListener("mouseenter", () => {
|
|
779
|
-
removeBtn.style.backgroundColor = "var(--fb-background-hover-color)";
|
|
780
|
-
});
|
|
781
|
-
removeBtn.addEventListener("mouseleave", () => {
|
|
782
|
-
removeBtn.style.backgroundColor = "transparent";
|
|
783
|
-
});
|
|
784
|
-
removeBtn.onclick = () => {
|
|
785
|
-
const currentIndex = Array.from(container.children).indexOf(
|
|
786
|
-
item
|
|
787
|
-
);
|
|
788
|
-
if (container.children.length > minCount) {
|
|
789
|
-
values.splice(currentIndex, 1);
|
|
790
|
-
item.remove();
|
|
791
|
-
updateIndices();
|
|
792
|
-
updateAddButton();
|
|
793
|
-
updateRemoveButtons();
|
|
794
|
-
}
|
|
795
|
-
};
|
|
796
|
-
item.appendChild(removeBtn);
|
|
797
|
-
}
|
|
798
|
-
const disabled = currentCount <= minCount;
|
|
799
|
-
removeBtn.disabled = disabled;
|
|
800
|
-
removeBtn.style.opacity = disabled ? "0.5" : "1";
|
|
801
|
-
removeBtn.style.pointerEvents = disabled ? "none" : "auto";
|
|
910
|
+
const chipCount = list.querySelectorAll(".fb-chip").length;
|
|
911
|
+
const disabled = chipCount <= minCount;
|
|
912
|
+
list.querySelectorAll(".fb-chip-remove").forEach((btn) => {
|
|
913
|
+
btn.disabled = disabled;
|
|
802
914
|
});
|
|
803
915
|
}
|
|
804
916
|
let addUpdate = null;
|
|
@@ -807,7 +919,7 @@ function renderMultipleTextElement(element, ctx, wrapper, pathKey) {
|
|
|
807
919
|
"text",
|
|
808
920
|
() => {
|
|
809
921
|
values.push(element.default || "");
|
|
810
|
-
|
|
922
|
+
addChip(element.default || "");
|
|
811
923
|
updateAddButton();
|
|
812
924
|
updateRemoveButtons();
|
|
813
925
|
},
|
|
@@ -820,7 +932,7 @@ function renderMultipleTextElement(element, ctx, wrapper, pathKey) {
|
|
|
820
932
|
function updateAddButton() {
|
|
821
933
|
if (addUpdate) addUpdate(values.length, maxCount);
|
|
822
934
|
}
|
|
823
|
-
values.forEach((value) =>
|
|
935
|
+
values.forEach((value) => addChip(value));
|
|
824
936
|
updateAddButton();
|
|
825
937
|
updateRemoveButtons();
|
|
826
938
|
}
|
|
@@ -843,10 +955,12 @@ function validateTextElement(element, key, context) {
|
|
|
843
955
|
font-size: var(--fb-font-size-small);
|
|
844
956
|
margin-top: 0.25rem;
|
|
845
957
|
`;
|
|
846
|
-
|
|
847
|
-
|
|
958
|
+
const chipAncestor = input.closest?.(".fb-chip");
|
|
959
|
+
const anchor = chipAncestor || input;
|
|
960
|
+
if (anchor.nextSibling) {
|
|
961
|
+
anchor.parentNode?.insertBefore(errorElement, anchor.nextSibling);
|
|
848
962
|
} else {
|
|
849
|
-
|
|
963
|
+
anchor.parentNode?.appendChild(errorElement);
|
|
850
964
|
}
|
|
851
965
|
}
|
|
852
966
|
errorElement.textContent = errorMessage;
|
|
@@ -895,7 +1009,7 @@ function validateTextElement(element, key, context) {
|
|
|
895
1009
|
}
|
|
896
1010
|
};
|
|
897
1011
|
if (element.multiple) {
|
|
898
|
-
const inputs = scopeRoot.querySelectorAll(`[name^="${key}["]`);
|
|
1012
|
+
const inputs = scopeRoot.querySelectorAll(`[name^="${key}\\["]`);
|
|
899
1013
|
const values = [];
|
|
900
1014
|
const rawValues = [];
|
|
901
1015
|
inputs.forEach((input, index) => {
|
|
@@ -944,12 +1058,14 @@ function updateTextField(element, fieldPath, value, context) {
|
|
|
944
1058
|
);
|
|
945
1059
|
return;
|
|
946
1060
|
}
|
|
947
|
-
const inputs = scopeRoot.querySelectorAll(`[name^="${fieldPath}["]`);
|
|
1061
|
+
const inputs = scopeRoot.querySelectorAll(`[name^="${fieldPath}\\["]`);
|
|
948
1062
|
inputs.forEach((input, index) => {
|
|
949
1063
|
if (index < value.length) {
|
|
950
1064
|
input.value = value[index] != null ? String(value[index]) : "";
|
|
951
1065
|
input.classList.remove("invalid");
|
|
952
1066
|
input.title = "";
|
|
1067
|
+
clearFieldError(input);
|
|
1068
|
+
input.dispatchEvent(new Event("input", { bubbles: true }));
|
|
953
1069
|
}
|
|
954
1070
|
});
|
|
955
1071
|
if (value.length !== inputs.length) {
|
|
@@ -963,26 +1079,15 @@ function updateTextField(element, fieldPath, value, context) {
|
|
|
963
1079
|
input.value = value != null ? String(value) : "";
|
|
964
1080
|
input.classList.remove("invalid");
|
|
965
1081
|
input.title = "";
|
|
1082
|
+
clearFieldError(input);
|
|
1083
|
+
if (input instanceof HTMLTextAreaElement) {
|
|
1084
|
+
input.dispatchEvent(new Event("input", { bubbles: true }));
|
|
1085
|
+
}
|
|
966
1086
|
}
|
|
967
1087
|
}
|
|
968
1088
|
}
|
|
969
1089
|
|
|
970
1090
|
// src/components/textarea.ts
|
|
971
|
-
function applyAutoExpand(textarea) {
|
|
972
|
-
textarea.style.overflow = "hidden";
|
|
973
|
-
textarea.style.resize = "none";
|
|
974
|
-
const lineCount = (textarea.value.match(/\n/g) || []).length + 1;
|
|
975
|
-
textarea.rows = Math.max(1, lineCount);
|
|
976
|
-
const resize = () => {
|
|
977
|
-
if (!textarea.isConnected) return;
|
|
978
|
-
textarea.style.height = "0";
|
|
979
|
-
textarea.style.height = `${textarea.scrollHeight}px`;
|
|
980
|
-
};
|
|
981
|
-
textarea.addEventListener("input", resize);
|
|
982
|
-
setTimeout(() => {
|
|
983
|
-
if (textarea.isConnected) resize();
|
|
984
|
-
}, 0);
|
|
985
|
-
}
|
|
986
1091
|
function renderTextareaElement(element, ctx, wrapper, pathKey) {
|
|
987
1092
|
const state = ctx.state;
|
|
988
1093
|
const readonly = isElementReadonly(element, state, ctx);
|
|
@@ -1013,7 +1118,7 @@ function renderTextareaElement(element, ctx, wrapper, pathKey) {
|
|
|
1013
1118
|
}
|
|
1014
1119
|
textareaWrapper.appendChild(textareaInput);
|
|
1015
1120
|
if (!readonly && (element.minLength != null || element.maxLength != null)) {
|
|
1016
|
-
const counter = createCharCounter(element, textareaInput
|
|
1121
|
+
const counter = createCharCounter(element, textareaInput);
|
|
1017
1122
|
textareaWrapper.appendChild(counter);
|
|
1018
1123
|
}
|
|
1019
1124
|
wrapper.appendChild(textareaWrapper);
|
|
@@ -1069,7 +1174,7 @@ function renderMultipleTextareaElement(element, ctx, wrapper, pathKey) {
|
|
|
1069
1174
|
}
|
|
1070
1175
|
textareaContainer.appendChild(textareaInput);
|
|
1071
1176
|
if (!readonly && (element.minLength != null || element.maxLength != null)) {
|
|
1072
|
-
const counter = createCharCounter(element, textareaInput
|
|
1177
|
+
const counter = createCharCounter(element, textareaInput);
|
|
1073
1178
|
textareaContainer.appendChild(counter);
|
|
1074
1179
|
}
|
|
1075
1180
|
itemWrapper.appendChild(textareaContainer);
|
|
@@ -1163,6 +1268,89 @@ function updateTextareaField(element, fieldPath, value, context) {
|
|
|
1163
1268
|
}
|
|
1164
1269
|
|
|
1165
1270
|
// src/components/number.ts
|
|
1271
|
+
function ensureStepperStyles(doc) {
|
|
1272
|
+
const ID = "fb-number-stepper-styles";
|
|
1273
|
+
if (doc.getElementById(ID)) return;
|
|
1274
|
+
const style = doc.createElement("style");
|
|
1275
|
+
style.id = ID;
|
|
1276
|
+
style.textContent = `
|
|
1277
|
+
.fb-stepper-input::-webkit-outer-spin-button,
|
|
1278
|
+
.fb-stepper-input::-webkit-inner-spin-button {
|
|
1279
|
+
-webkit-appearance: none;
|
|
1280
|
+
margin: 0;
|
|
1281
|
+
}
|
|
1282
|
+
.fb-stepper-input { -moz-appearance: textfield; }
|
|
1283
|
+
`;
|
|
1284
|
+
doc.head.appendChild(style);
|
|
1285
|
+
}
|
|
1286
|
+
function buildStepper(input, element, readonly) {
|
|
1287
|
+
ensureStepperStyles(input.ownerDocument);
|
|
1288
|
+
const step = element.step ?? 1;
|
|
1289
|
+
const min = element.min;
|
|
1290
|
+
const max = element.max;
|
|
1291
|
+
const wrap = document.createElement("div");
|
|
1292
|
+
wrap.className = "fb-stepper";
|
|
1293
|
+
wrap.style.cssText = `
|
|
1294
|
+
display: inline-flex;
|
|
1295
|
+
align-items: stretch;
|
|
1296
|
+
border: var(--fb-border-width) solid var(--fb-border-color);
|
|
1297
|
+
border-radius: var(--fb-border-radius);
|
|
1298
|
+
overflow: hidden;
|
|
1299
|
+
background: var(--fb-background-color);
|
|
1300
|
+
`;
|
|
1301
|
+
const makeBtn = (label, delta) => {
|
|
1302
|
+
const b = document.createElement("button");
|
|
1303
|
+
b.type = "button";
|
|
1304
|
+
b.textContent = label;
|
|
1305
|
+
b.tabIndex = -1;
|
|
1306
|
+
b.style.cssText = `
|
|
1307
|
+
width: 32px;
|
|
1308
|
+
border: none;
|
|
1309
|
+
background: transparent;
|
|
1310
|
+
color: var(--fb-text-color);
|
|
1311
|
+
font-size: var(--fb-font-size);
|
|
1312
|
+
font-family: var(--fb-font-family);
|
|
1313
|
+
cursor: ${readonly ? "default" : "pointer"};
|
|
1314
|
+
user-select: none;
|
|
1315
|
+
`;
|
|
1316
|
+
if (readonly) {
|
|
1317
|
+
b.disabled = true;
|
|
1318
|
+
b.style.opacity = "0.5";
|
|
1319
|
+
} else {
|
|
1320
|
+
b.addEventListener("click", (e) => {
|
|
1321
|
+
e.preventDefault();
|
|
1322
|
+
const current = parseFloat(input.value);
|
|
1323
|
+
const base = Number.isFinite(current) ? current : min ?? element.default ?? 0;
|
|
1324
|
+
let next = parseFloat((base + delta * step).toPrecision(12));
|
|
1325
|
+
if (min != null) next = Math.max(min, next);
|
|
1326
|
+
if (max != null) next = Math.min(max, next);
|
|
1327
|
+
input.value = String(next);
|
|
1328
|
+
input.dispatchEvent(new Event("input", { bubbles: true }));
|
|
1329
|
+
input.dispatchEvent(new Event("change", { bubbles: true }));
|
|
1330
|
+
});
|
|
1331
|
+
}
|
|
1332
|
+
return b;
|
|
1333
|
+
};
|
|
1334
|
+
input.classList.add("fb-stepper-input");
|
|
1335
|
+
input.style.cssText = `
|
|
1336
|
+
width: 56px;
|
|
1337
|
+
border: none;
|
|
1338
|
+
border-left: var(--fb-border-width) solid var(--fb-border-color);
|
|
1339
|
+
border-right: var(--fb-border-width) solid var(--fb-border-color);
|
|
1340
|
+
padding: var(--fb-input-padding-y) 0;
|
|
1341
|
+
font-size: var(--fb-font-size);
|
|
1342
|
+
font-family: var(--fb-font-family);
|
|
1343
|
+
text-align: center;
|
|
1344
|
+
background: transparent;
|
|
1345
|
+
color: var(--fb-text-color);
|
|
1346
|
+
-moz-appearance: textfield;
|
|
1347
|
+
box-sizing: border-box;
|
|
1348
|
+
`;
|
|
1349
|
+
wrap.appendChild(makeBtn("\u2212", -1));
|
|
1350
|
+
wrap.appendChild(input);
|
|
1351
|
+
wrap.appendChild(makeBtn("+", 1));
|
|
1352
|
+
return wrap;
|
|
1353
|
+
}
|
|
1166
1354
|
function createNumberRangeHint(element, input) {
|
|
1167
1355
|
const hint = document.createElement("span");
|
|
1168
1356
|
hint.className = "number-range-hint";
|
|
@@ -1209,14 +1397,6 @@ function renderNumberElement(element, ctx, wrapper, pathKey) {
|
|
|
1209
1397
|
inputWrapper.style.cssText = "position: relative;";
|
|
1210
1398
|
const numberInput = document.createElement("input");
|
|
1211
1399
|
numberInput.type = "number";
|
|
1212
|
-
numberInput.className = "w-full border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500";
|
|
1213
|
-
numberInput.style.cssText = `
|
|
1214
|
-
padding: var(--fb-input-padding-y) 60px var(--fb-input-padding-y) var(--fb-input-padding-x);
|
|
1215
|
-
font-size: var(--fb-font-size);
|
|
1216
|
-
font-family: var(--fb-font-family);
|
|
1217
|
-
width: 100%;
|
|
1218
|
-
box-sizing: border-box;
|
|
1219
|
-
`;
|
|
1220
1400
|
numberInput.name = pathKey;
|
|
1221
1401
|
numberInput.placeholder = element.placeholder || "0";
|
|
1222
1402
|
if (element.min !== void 0) numberInput.min = element.min.toString();
|
|
@@ -1224,6 +1404,16 @@ function renderNumberElement(element, ctx, wrapper, pathKey) {
|
|
|
1224
1404
|
if (element.step !== void 0) numberInput.step = element.step.toString();
|
|
1225
1405
|
numberInput.value = ctx.prefill[element.key] || element.default || "";
|
|
1226
1406
|
numberInput.readOnly = readonly;
|
|
1407
|
+
if (!element.stepper) {
|
|
1408
|
+
numberInput.className = "w-full border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500";
|
|
1409
|
+
numberInput.style.cssText = `
|
|
1410
|
+
padding: var(--fb-input-padding-y) 60px var(--fb-input-padding-y) var(--fb-input-padding-x);
|
|
1411
|
+
font-size: var(--fb-font-size);
|
|
1412
|
+
font-family: var(--fb-font-family);
|
|
1413
|
+
width: 100%;
|
|
1414
|
+
box-sizing: border-box;
|
|
1415
|
+
`;
|
|
1416
|
+
}
|
|
1227
1417
|
if (!readonly && ctx.instance) {
|
|
1228
1418
|
const handleChange = () => {
|
|
1229
1419
|
const value = numberInput.value ? parseFloat(numberInput.value) : null;
|
|
@@ -1232,10 +1422,14 @@ function renderNumberElement(element, ctx, wrapper, pathKey) {
|
|
|
1232
1422
|
numberInput.addEventListener("blur", handleChange);
|
|
1233
1423
|
numberInput.addEventListener("input", handleChange);
|
|
1234
1424
|
}
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
inputWrapper.appendChild(
|
|
1425
|
+
if (element.stepper) {
|
|
1426
|
+
inputWrapper.appendChild(buildStepper(numberInput, element, readonly));
|
|
1427
|
+
} else {
|
|
1428
|
+
inputWrapper.appendChild(numberInput);
|
|
1429
|
+
if (!readonly && (element.min != null || element.max != null)) {
|
|
1430
|
+
const counter = createNumberRangeHint(element, numberInput);
|
|
1431
|
+
inputWrapper.appendChild(counter);
|
|
1432
|
+
}
|
|
1239
1433
|
}
|
|
1240
1434
|
wrapper.appendChild(inputWrapper);
|
|
1241
1435
|
}
|
|
@@ -1489,13 +1683,14 @@ function updateNumberField(element, fieldPath, value, context) {
|
|
|
1489
1683
|
return;
|
|
1490
1684
|
}
|
|
1491
1685
|
const inputs = scopeRoot.querySelectorAll(
|
|
1492
|
-
`[name^="${fieldPath}["]`
|
|
1686
|
+
`[name^="${fieldPath}\\["]`
|
|
1493
1687
|
);
|
|
1494
1688
|
inputs.forEach((input, index) => {
|
|
1495
1689
|
if (index < value.length) {
|
|
1496
1690
|
input.value = value[index] != null ? String(value[index]) : "";
|
|
1497
1691
|
input.classList.remove("invalid");
|
|
1498
1692
|
input.title = "";
|
|
1693
|
+
clearFieldError(input);
|
|
1499
1694
|
}
|
|
1500
1695
|
});
|
|
1501
1696
|
if (value.length !== inputs.length) {
|
|
@@ -1511,6 +1706,7 @@ function updateNumberField(element, fieldPath, value, context) {
|
|
|
1511
1706
|
input.value = value != null ? String(value) : "";
|
|
1512
1707
|
input.classList.remove("invalid");
|
|
1513
1708
|
input.title = "";
|
|
1709
|
+
clearFieldError(input);
|
|
1514
1710
|
}
|
|
1515
1711
|
}
|
|
1516
1712
|
}
|
|
@@ -1722,7 +1918,7 @@ function validateSelectElement(element, key, context) {
|
|
|
1722
1918
|
};
|
|
1723
1919
|
if ("multiple" in element && element.multiple) {
|
|
1724
1920
|
const inputs = scopeRoot.querySelectorAll(
|
|
1725
|
-
`[name^="${key}["]`
|
|
1921
|
+
`[name^="${key}\\["]`
|
|
1726
1922
|
);
|
|
1727
1923
|
const values = [];
|
|
1728
1924
|
inputs.forEach((input) => {
|
|
@@ -1758,7 +1954,7 @@ function updateSelectField(element, fieldPath, value, context) {
|
|
|
1758
1954
|
return;
|
|
1759
1955
|
}
|
|
1760
1956
|
const selects = scopeRoot.querySelectorAll(
|
|
1761
|
-
`[name^="${fieldPath}["]`
|
|
1957
|
+
`[name^="${fieldPath}\\["]`
|
|
1762
1958
|
);
|
|
1763
1959
|
selects.forEach((select, index) => {
|
|
1764
1960
|
if (index < value.length) {
|
|
@@ -1769,6 +1965,7 @@ function updateSelectField(element, fieldPath, value, context) {
|
|
|
1769
1965
|
});
|
|
1770
1966
|
select.classList.remove("invalid");
|
|
1771
1967
|
select.title = "";
|
|
1968
|
+
clearFieldError(select);
|
|
1772
1969
|
}
|
|
1773
1970
|
});
|
|
1774
1971
|
if (value.length !== selects.length) {
|
|
@@ -1788,72 +1985,147 @@ function updateSelectField(element, fieldPath, value, context) {
|
|
|
1788
1985
|
});
|
|
1789
1986
|
select.classList.remove("invalid");
|
|
1790
1987
|
select.title = "";
|
|
1988
|
+
clearFieldError(select);
|
|
1791
1989
|
}
|
|
1792
1990
|
}
|
|
1793
1991
|
}
|
|
1794
1992
|
|
|
1795
1993
|
// src/components/switcher.ts
|
|
1796
|
-
function applySelectedStyle(btn) {
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1994
|
+
function applySelectedStyle(btn, isPreset) {
|
|
1995
|
+
if (isPreset) {
|
|
1996
|
+
btn.style.backgroundColor = "var(--fb-primary-soft-color)";
|
|
1997
|
+
btn.style.color = "var(--fb-primary-color)";
|
|
1998
|
+
btn.style.borderColor = "var(--fb-primary-color)";
|
|
1999
|
+
} else {
|
|
2000
|
+
btn.style.backgroundColor = "var(--fb-primary-color)";
|
|
2001
|
+
btn.style.color = "#ffffff";
|
|
2002
|
+
btn.style.borderColor = "var(--fb-primary-color)";
|
|
2003
|
+
}
|
|
1800
2004
|
}
|
|
1801
|
-
function applyUnselectedStyle(btn) {
|
|
1802
|
-
btn.style.backgroundColor = "transparent";
|
|
2005
|
+
function applyUnselectedStyle(btn, isPreset) {
|
|
2006
|
+
btn.style.backgroundColor = isPreset ? "var(--fb-background-color)" : "transparent";
|
|
1803
2007
|
btn.style.color = "var(--fb-text-color)";
|
|
1804
2008
|
btn.style.borderColor = "var(--fb-border-color)";
|
|
1805
2009
|
}
|
|
2010
|
+
function isPresetButton(btn) {
|
|
2011
|
+
return btn.classList.contains("fb-switcher-preset");
|
|
2012
|
+
}
|
|
2013
|
+
function buildPresetCard(option, readonly) {
|
|
2014
|
+
const btn = document.createElement("button");
|
|
2015
|
+
btn.type = "button";
|
|
2016
|
+
btn.className = "fb-switcher-btn fb-switcher-preset";
|
|
2017
|
+
btn.dataset.value = option.value;
|
|
2018
|
+
btn.style.cssText = `
|
|
2019
|
+
display: inline-flex;
|
|
2020
|
+
align-items: center;
|
|
2021
|
+
gap: 8px;
|
|
2022
|
+
padding: 7px 12px 7px 10px;
|
|
2023
|
+
border-width: var(--fb-border-width);
|
|
2024
|
+
border-style: solid;
|
|
2025
|
+
border-radius: 999px;
|
|
2026
|
+
background: var(--fb-background-color);
|
|
2027
|
+
font-size: var(--fb-font-size);
|
|
2028
|
+
font-family: var(--fb-font-family);
|
|
2029
|
+
line-height: 1.25;
|
|
2030
|
+
cursor: ${readonly ? "default" : "pointer"};
|
|
2031
|
+
transition: background-color var(--fb-transition-duration), color var(--fb-transition-duration), border-color var(--fb-transition-duration);
|
|
2032
|
+
outline: none;
|
|
2033
|
+
`;
|
|
2034
|
+
if (option.iconUrl) {
|
|
2035
|
+
const icon = document.createElement("img");
|
|
2036
|
+
icon.className = "fb-switcher-icon";
|
|
2037
|
+
icon.src = option.iconUrl;
|
|
2038
|
+
icon.alt = "";
|
|
2039
|
+
icon.setAttribute("aria-hidden", "true");
|
|
2040
|
+
icon.style.cssText = `
|
|
2041
|
+
display: block;
|
|
2042
|
+
flex: 0 0 auto;
|
|
2043
|
+
width: 20px;
|
|
2044
|
+
height: 20px;
|
|
2045
|
+
object-fit: contain;
|
|
2046
|
+
`;
|
|
2047
|
+
btn.appendChild(icon);
|
|
2048
|
+
}
|
|
2049
|
+
const name = document.createElement("span");
|
|
2050
|
+
name.className = "fb-switcher-name";
|
|
2051
|
+
name.textContent = option.label;
|
|
2052
|
+
name.style.cssText = "font-weight: 600;";
|
|
2053
|
+
btn.appendChild(name);
|
|
2054
|
+
if (option.subtitle) {
|
|
2055
|
+
const sub = document.createElement("span");
|
|
2056
|
+
sub.className = "fb-switcher-subtitle";
|
|
2057
|
+
sub.textContent = option.subtitle;
|
|
2058
|
+
sub.style.cssText = `
|
|
2059
|
+
font-size: var(--fb-font-size-small);
|
|
2060
|
+
opacity: 0.7;
|
|
2061
|
+
font-variant-numeric: tabular-nums;
|
|
2062
|
+
`;
|
|
2063
|
+
btn.appendChild(sub);
|
|
2064
|
+
}
|
|
2065
|
+
return btn;
|
|
2066
|
+
}
|
|
1806
2067
|
function buildSegmentedGroup(element, currentValue, hiddenInput, readonly, onChange) {
|
|
1807
2068
|
const options = element.options || [];
|
|
2069
|
+
const isPresetMode = options.some((o) => o.subtitle || o.iconUrl);
|
|
1808
2070
|
const group = document.createElement("div");
|
|
1809
2071
|
group.className = "fb-switcher-group";
|
|
1810
|
-
group.style.cssText = `
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
2072
|
+
group.style.cssText = isPresetMode ? `
|
|
2073
|
+
display: flex;
|
|
2074
|
+
flex-direction: row;
|
|
2075
|
+
flex-wrap: wrap;
|
|
2076
|
+
gap: 6px;
|
|
2077
|
+
` : `
|
|
2078
|
+
display: inline-flex;
|
|
2079
|
+
flex-direction: row;
|
|
2080
|
+
flex-wrap: nowrap;
|
|
2081
|
+
`;
|
|
1815
2082
|
const buttons = [];
|
|
1816
2083
|
options.forEach((option, index) => {
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
btn.dataset.value = option.value;
|
|
1821
|
-
btn.textContent = option.label;
|
|
1822
|
-
btn.style.cssText = `
|
|
1823
|
-
padding: var(--fb-input-padding-y) var(--fb-input-padding-x);
|
|
1824
|
-
font-size: var(--fb-font-size);
|
|
1825
|
-
border-width: var(--fb-border-width);
|
|
1826
|
-
border-style: solid;
|
|
1827
|
-
cursor: ${readonly ? "default" : "pointer"};
|
|
1828
|
-
transition: background-color var(--fb-transition-duration), color var(--fb-transition-duration), border-color var(--fb-transition-duration);
|
|
1829
|
-
white-space: nowrap;
|
|
1830
|
-
line-height: 1.25;
|
|
1831
|
-
outline: none;
|
|
1832
|
-
`;
|
|
1833
|
-
if (options.length === 1) {
|
|
1834
|
-
btn.style.borderRadius = "var(--fb-border-radius)";
|
|
1835
|
-
} else if (index === 0) {
|
|
1836
|
-
btn.style.borderRadius = "var(--fb-border-radius) 0 0 var(--fb-border-radius)";
|
|
1837
|
-
btn.style.borderRightWidth = "0";
|
|
1838
|
-
} else if (index === options.length - 1) {
|
|
1839
|
-
btn.style.borderRadius = "0 var(--fb-border-radius) var(--fb-border-radius) 0";
|
|
2084
|
+
let btn;
|
|
2085
|
+
if (isPresetMode) {
|
|
2086
|
+
btn = buildPresetCard(option, readonly);
|
|
1840
2087
|
} else {
|
|
1841
|
-
btn
|
|
1842
|
-
btn.
|
|
2088
|
+
btn = document.createElement("button");
|
|
2089
|
+
btn.type = "button";
|
|
2090
|
+
btn.className = "fb-switcher-btn";
|
|
2091
|
+
btn.dataset.value = option.value;
|
|
2092
|
+
btn.textContent = option.label;
|
|
2093
|
+
btn.style.cssText = `
|
|
2094
|
+
padding: var(--fb-input-padding-y) var(--fb-input-padding-x);
|
|
2095
|
+
font-size: var(--fb-font-size);
|
|
2096
|
+
border-width: var(--fb-border-width);
|
|
2097
|
+
border-style: solid;
|
|
2098
|
+
cursor: ${readonly ? "default" : "pointer"};
|
|
2099
|
+
transition: background-color var(--fb-transition-duration), color var(--fb-transition-duration), border-color var(--fb-transition-duration);
|
|
2100
|
+
white-space: nowrap;
|
|
2101
|
+
line-height: 1.25;
|
|
2102
|
+
outline: none;
|
|
2103
|
+
`;
|
|
2104
|
+
if (options.length === 1) {
|
|
2105
|
+
btn.style.borderRadius = "var(--fb-border-radius)";
|
|
2106
|
+
} else if (index === 0) {
|
|
2107
|
+
btn.style.borderRadius = "var(--fb-border-radius) 0 0 var(--fb-border-radius)";
|
|
2108
|
+
btn.style.borderRightWidth = "0";
|
|
2109
|
+
} else if (index === options.length - 1) {
|
|
2110
|
+
btn.style.borderRadius = "0 var(--fb-border-radius) var(--fb-border-radius) 0";
|
|
2111
|
+
} else {
|
|
2112
|
+
btn.style.borderRadius = "0";
|
|
2113
|
+
btn.style.borderRightWidth = "0";
|
|
2114
|
+
}
|
|
1843
2115
|
}
|
|
1844
2116
|
if (option.value === currentValue) {
|
|
1845
|
-
applySelectedStyle(btn);
|
|
2117
|
+
applySelectedStyle(btn, isPresetMode);
|
|
1846
2118
|
} else {
|
|
1847
|
-
applyUnselectedStyle(btn);
|
|
2119
|
+
applyUnselectedStyle(btn, isPresetMode);
|
|
1848
2120
|
}
|
|
1849
2121
|
if (!readonly) {
|
|
1850
2122
|
btn.addEventListener("click", () => {
|
|
1851
2123
|
hiddenInput.value = option.value;
|
|
1852
2124
|
buttons.forEach((b) => {
|
|
1853
2125
|
if (b.dataset.value === option.value) {
|
|
1854
|
-
applySelectedStyle(b);
|
|
2126
|
+
applySelectedStyle(b, isPresetMode);
|
|
1855
2127
|
} else {
|
|
1856
|
-
applyUnselectedStyle(b);
|
|
2128
|
+
applyUnselectedStyle(b, isPresetMode);
|
|
1857
2129
|
}
|
|
1858
2130
|
});
|
|
1859
2131
|
if (onChange) {
|
|
@@ -1867,7 +2139,7 @@ function buildSegmentedGroup(element, currentValue, hiddenInput, readonly, onCha
|
|
|
1867
2139
|
});
|
|
1868
2140
|
btn.addEventListener("mouseleave", () => {
|
|
1869
2141
|
if (hiddenInput.value !== option.value) {
|
|
1870
|
-
btn.style.backgroundColor = "transparent";
|
|
2142
|
+
btn.style.backgroundColor = isPresetMode ? "var(--fb-background-color)" : "transparent";
|
|
1871
2143
|
}
|
|
1872
2144
|
});
|
|
1873
2145
|
}
|
|
@@ -2086,7 +2358,7 @@ function validateSwitcherElement(element, key, context) {
|
|
|
2086
2358
|
);
|
|
2087
2359
|
if ("multiple" in element && element.multiple) {
|
|
2088
2360
|
const inputs = scopeRoot.querySelectorAll(
|
|
2089
|
-
`input[type="hidden"][name^="${key}["]`
|
|
2361
|
+
`input[type="hidden"][name^="${key}\\["]`
|
|
2090
2362
|
);
|
|
2091
2363
|
const values = [];
|
|
2092
2364
|
inputs.forEach((input) => {
|
|
@@ -2133,7 +2405,7 @@ function updateSwitcherField(element, fieldPath, value, context) {
|
|
|
2133
2405
|
return;
|
|
2134
2406
|
}
|
|
2135
2407
|
const inputs = scopeRoot.querySelectorAll(
|
|
2136
|
-
`input[type="hidden"][name^="${fieldPath}["]`
|
|
2408
|
+
`input[type="hidden"][name^="${fieldPath}\\["]`
|
|
2137
2409
|
);
|
|
2138
2410
|
inputs.forEach((input, index) => {
|
|
2139
2411
|
if (index < value.length) {
|
|
@@ -2142,15 +2414,17 @@ function updateSwitcherField(element, fieldPath, value, context) {
|
|
|
2142
2414
|
const group = input.parentElement?.querySelector(".fb-switcher-group");
|
|
2143
2415
|
if (group) {
|
|
2144
2416
|
group.querySelectorAll(".fb-switcher-btn").forEach((btn) => {
|
|
2417
|
+
const isPreset = isPresetButton(btn);
|
|
2145
2418
|
if (btn.dataset.value === newVal) {
|
|
2146
|
-
applySelectedStyle(btn);
|
|
2419
|
+
applySelectedStyle(btn, isPreset);
|
|
2147
2420
|
} else {
|
|
2148
|
-
applyUnselectedStyle(btn);
|
|
2421
|
+
applyUnselectedStyle(btn, isPreset);
|
|
2149
2422
|
}
|
|
2150
2423
|
});
|
|
2151
2424
|
}
|
|
2152
2425
|
input.classList.remove("invalid");
|
|
2153
2426
|
input.title = "";
|
|
2427
|
+
clearFieldError(input);
|
|
2154
2428
|
}
|
|
2155
2429
|
});
|
|
2156
2430
|
if (value.length !== inputs.length) {
|
|
@@ -2168,16 +2442,208 @@ function updateSwitcherField(element, fieldPath, value, context) {
|
|
|
2168
2442
|
const group = input.parentElement?.querySelector(".fb-switcher-group");
|
|
2169
2443
|
if (group) {
|
|
2170
2444
|
group.querySelectorAll(".fb-switcher-btn").forEach((btn) => {
|
|
2445
|
+
const isPreset = isPresetButton(btn);
|
|
2171
2446
|
if (btn.dataset.value === newVal) {
|
|
2172
|
-
applySelectedStyle(btn);
|
|
2447
|
+
applySelectedStyle(btn, isPreset);
|
|
2173
2448
|
} else {
|
|
2174
|
-
applyUnselectedStyle(btn);
|
|
2449
|
+
applyUnselectedStyle(btn, isPreset);
|
|
2175
2450
|
}
|
|
2176
2451
|
});
|
|
2177
2452
|
}
|
|
2178
2453
|
input.classList.remove("invalid");
|
|
2179
2454
|
input.title = "";
|
|
2455
|
+
clearFieldError(input);
|
|
2456
|
+
}
|
|
2457
|
+
}
|
|
2458
|
+
}
|
|
2459
|
+
|
|
2460
|
+
// src/components/boolean.ts
|
|
2461
|
+
var TOGGLE_W = 36;
|
|
2462
|
+
var TOGGLE_H = 20;
|
|
2463
|
+
var KNOB = 16;
|
|
2464
|
+
function ensureStyles(doc) {
|
|
2465
|
+
const ID = "fb-boolean-styles";
|
|
2466
|
+
if (doc.getElementById(ID)) return;
|
|
2467
|
+
const style = doc.createElement("style");
|
|
2468
|
+
style.id = ID;
|
|
2469
|
+
style.textContent = `
|
|
2470
|
+
.fb-toggle {
|
|
2471
|
+
position: relative;
|
|
2472
|
+
display: inline-block;
|
|
2473
|
+
width: ${TOGGLE_W}px;
|
|
2474
|
+
height: ${TOGGLE_H}px;
|
|
2475
|
+
border-radius: ${TOGGLE_H}px;
|
|
2476
|
+
background: var(--fb-border-color);
|
|
2477
|
+
transition: background-color var(--fb-transition-duration);
|
|
2478
|
+
flex-shrink: 0;
|
|
2479
|
+
}
|
|
2480
|
+
.fb-toggle::after {
|
|
2481
|
+
content: "";
|
|
2482
|
+
position: absolute;
|
|
2483
|
+
top: ${(TOGGLE_H - KNOB) / 2}px;
|
|
2484
|
+
left: ${(TOGGLE_H - KNOB) / 2}px;
|
|
2485
|
+
width: ${KNOB}px;
|
|
2486
|
+
height: ${KNOB}px;
|
|
2487
|
+
border-radius: 50%;
|
|
2488
|
+
background: #ffffff;
|
|
2489
|
+
box-shadow: 0 1px 3px rgba(0,0,0,0.2);
|
|
2490
|
+
transition: transform var(--fb-transition-duration);
|
|
2491
|
+
}
|
|
2492
|
+
.fb-toggle.fb-on {
|
|
2493
|
+
background: var(--fb-primary-color);
|
|
2494
|
+
}
|
|
2495
|
+
.fb-toggle.fb-on::after {
|
|
2496
|
+
transform: translateX(${TOGGLE_W - KNOB - (TOGGLE_H - KNOB)}px);
|
|
2497
|
+
}
|
|
2498
|
+
.fb-toggle-row {
|
|
2499
|
+
display: flex;
|
|
2500
|
+
align-items: center;
|
|
2501
|
+
gap: 12px;
|
|
2502
|
+
padding: 12px 14px;
|
|
2503
|
+
background: var(--fb-surface-soft-color);
|
|
2504
|
+
border: var(--fb-border-width) solid var(--fb-border-color);
|
|
2505
|
+
border-radius: var(--fb-border-radius);
|
|
2506
|
+
cursor: pointer;
|
|
2507
|
+
user-select: none;
|
|
2180
2508
|
}
|
|
2509
|
+
.fb-toggle-row[aria-disabled="true"] {
|
|
2510
|
+
cursor: default;
|
|
2511
|
+
opacity: 0.7;
|
|
2512
|
+
}
|
|
2513
|
+
.fb-toggle-row:focus-visible {
|
|
2514
|
+
outline: var(--fb-focus-ring-width) solid var(--fb-focus-ring-color);
|
|
2515
|
+
outline-offset: var(--fb-focus-ring-offset);
|
|
2516
|
+
}
|
|
2517
|
+
.fb-toggle-text { flex: 1; min-width: 0; }
|
|
2518
|
+
.fb-toggle-title {
|
|
2519
|
+
display: flex;
|
|
2520
|
+
align-items: center;
|
|
2521
|
+
gap: 4px;
|
|
2522
|
+
font-size: var(--fb-font-size);
|
|
2523
|
+
font-weight: 500;
|
|
2524
|
+
color: var(--fb-text-color);
|
|
2525
|
+
line-height: 1.3;
|
|
2526
|
+
}
|
|
2527
|
+
.fb-toggle-subtitle {
|
|
2528
|
+
font-size: var(--fb-font-size-small);
|
|
2529
|
+
color: var(--fb-text-secondary-color);
|
|
2530
|
+
margin-top: 2px;
|
|
2531
|
+
line-height: 1.35;
|
|
2532
|
+
}
|
|
2533
|
+
.fb-toggle-info {
|
|
2534
|
+
flex: 0 0 14px;
|
|
2535
|
+
display: inline-flex;
|
|
2536
|
+
align-items: center;
|
|
2537
|
+
justify-content: center;
|
|
2538
|
+
width: 14px;
|
|
2539
|
+
height: 14px;
|
|
2540
|
+
border-radius: 50%;
|
|
2541
|
+
background: var(--fb-border-color);
|
|
2542
|
+
color: #fff;
|
|
2543
|
+
font-size: 10px;
|
|
2544
|
+
font-weight: 700;
|
|
2545
|
+
font-style: italic;
|
|
2546
|
+
font-family: serif;
|
|
2547
|
+
cursor: help;
|
|
2548
|
+
}
|
|
2549
|
+
`;
|
|
2550
|
+
doc.head.appendChild(style);
|
|
2551
|
+
}
|
|
2552
|
+
function parseBool(v) {
|
|
2553
|
+
if (typeof v === "boolean") return v;
|
|
2554
|
+
if (typeof v === "string") return v === "true" || v === "on" || v === "1";
|
|
2555
|
+
return false;
|
|
2556
|
+
}
|
|
2557
|
+
function renderBooleanElement(element, ctx, wrapper, pathKey) {
|
|
2558
|
+
ensureStyles(document);
|
|
2559
|
+
const state = ctx.state;
|
|
2560
|
+
const readonly = isElementReadonly(element, state, ctx);
|
|
2561
|
+
const prefillRaw = ctx.prefill[element.key];
|
|
2562
|
+
const initial = prefillRaw !== void 0 ? parseBool(prefillRaw) : parseBool(element.default);
|
|
2563
|
+
const hiddenInput = document.createElement("input");
|
|
2564
|
+
hiddenInput.type = "hidden";
|
|
2565
|
+
hiddenInput.name = pathKey;
|
|
2566
|
+
hiddenInput.value = initial ? "true" : "false";
|
|
2567
|
+
const row = document.createElement("div");
|
|
2568
|
+
row.className = "fb-toggle-row";
|
|
2569
|
+
row.setAttribute("role", "switch");
|
|
2570
|
+
row.setAttribute("aria-checked", initial ? "true" : "false");
|
|
2571
|
+
if (readonly) {
|
|
2572
|
+
row.setAttribute("aria-disabled", "true");
|
|
2573
|
+
} else {
|
|
2574
|
+
row.tabIndex = 0;
|
|
2575
|
+
}
|
|
2576
|
+
const pill = document.createElement("span");
|
|
2577
|
+
pill.className = "fb-toggle" + (initial ? " fb-on" : "");
|
|
2578
|
+
pill.setAttribute("aria-hidden", "true");
|
|
2579
|
+
row.appendChild(pill);
|
|
2580
|
+
const textBlock = document.createElement("div");
|
|
2581
|
+
textBlock.className = "fb-toggle-text";
|
|
2582
|
+
const titleEl = document.createElement("div");
|
|
2583
|
+
titleEl.className = "fb-toggle-title";
|
|
2584
|
+
titleEl.appendChild(document.createTextNode(element.label ?? ""));
|
|
2585
|
+
if (element.description) {
|
|
2586
|
+
const info = document.createElement("span");
|
|
2587
|
+
info.className = "fb-toggle-info";
|
|
2588
|
+
info.textContent = "i";
|
|
2589
|
+
info.title = element.description;
|
|
2590
|
+
titleEl.appendChild(info);
|
|
2591
|
+
}
|
|
2592
|
+
textBlock.appendChild(titleEl);
|
|
2593
|
+
if (element.hint) {
|
|
2594
|
+
const subtitle = document.createElement("div");
|
|
2595
|
+
subtitle.className = "fb-toggle-subtitle";
|
|
2596
|
+
subtitle.textContent = element.hint;
|
|
2597
|
+
textBlock.appendChild(subtitle);
|
|
2598
|
+
}
|
|
2599
|
+
row.appendChild(textBlock);
|
|
2600
|
+
if (!readonly) {
|
|
2601
|
+
const toggle = () => {
|
|
2602
|
+
const next = hiddenInput.value !== "true";
|
|
2603
|
+
hiddenInput.value = next ? "true" : "false";
|
|
2604
|
+
pill.classList.toggle("fb-on", next);
|
|
2605
|
+
row.setAttribute("aria-checked", next ? "true" : "false");
|
|
2606
|
+
if (ctx.instance) ctx.instance.triggerOnChange(pathKey, next);
|
|
2607
|
+
};
|
|
2608
|
+
row.addEventListener("click", (e) => {
|
|
2609
|
+
if (e.target?.classList.contains("fb-toggle-info")) {
|
|
2610
|
+
return;
|
|
2611
|
+
}
|
|
2612
|
+
toggle();
|
|
2613
|
+
});
|
|
2614
|
+
row.addEventListener("keydown", (e) => {
|
|
2615
|
+
if (e.key === " " || e.key === "Enter") {
|
|
2616
|
+
e.preventDefault();
|
|
2617
|
+
toggle();
|
|
2618
|
+
}
|
|
2619
|
+
});
|
|
2620
|
+
}
|
|
2621
|
+
wrapper.appendChild(hiddenInput);
|
|
2622
|
+
wrapper.appendChild(row);
|
|
2623
|
+
}
|
|
2624
|
+
function validateBooleanElement(element, key, context) {
|
|
2625
|
+
const { scopeRoot } = context;
|
|
2626
|
+
const input = scopeRoot.querySelector(
|
|
2627
|
+
`input[type="hidden"][name="${key}"]`
|
|
2628
|
+
);
|
|
2629
|
+
const raw = input?.value ?? "";
|
|
2630
|
+
const value = parseBool(raw);
|
|
2631
|
+
const errors = [];
|
|
2632
|
+
return { value, errors };
|
|
2633
|
+
}
|
|
2634
|
+
function updateBooleanField(_element, fieldPath, value, context) {
|
|
2635
|
+
const { scopeRoot } = context;
|
|
2636
|
+
const input = scopeRoot.querySelector(
|
|
2637
|
+
`input[type="hidden"][name="${fieldPath}"]`
|
|
2638
|
+
);
|
|
2639
|
+
if (!input) return;
|
|
2640
|
+
const bool = parseBool(value);
|
|
2641
|
+
input.value = bool ? "true" : "false";
|
|
2642
|
+
const row = input.parentElement?.querySelector(".fb-toggle-row");
|
|
2643
|
+
if (row) {
|
|
2644
|
+
row.setAttribute("aria-checked", bool ? "true" : "false");
|
|
2645
|
+
const pill = row.querySelector(".fb-toggle");
|
|
2646
|
+
if (pill) pill.classList.toggle("fb-on", bool);
|
|
2181
2647
|
}
|
|
2182
2648
|
}
|
|
2183
2649
|
|
|
@@ -2281,14 +2747,20 @@ function ensureFileStyles() {
|
|
|
2281
2747
|
}
|
|
2282
2748
|
|
|
2283
2749
|
/* \u2500\u2500\u2500 Wide single-file add tile (empty state) \u2500\u2500\u2500 */
|
|
2750
|
+
/* Flex-wraps: side-by-side when wide enough, stacks upload/library
|
|
2751
|
+
vertically when narrow (e.g. inside a 50/50 container column). */
|
|
2284
2752
|
.fb-wide-tile {
|
|
2285
2753
|
width: 100%;
|
|
2754
|
+
box-sizing: border-box;
|
|
2286
2755
|
border-radius: 0.75rem;
|
|
2287
2756
|
border: 1px dashed #60a5fa;
|
|
2288
2757
|
background: rgba(239,246,255,0.5);
|
|
2289
2758
|
display: flex;
|
|
2759
|
+
flex-wrap: wrap;
|
|
2760
|
+
align-items: stretch;
|
|
2761
|
+
gap: 0;
|
|
2290
2762
|
overflow: hidden;
|
|
2291
|
-
height: 180px;
|
|
2763
|
+
min-height: 180px;
|
|
2292
2764
|
transition: border-color 150ms, background 150ms, box-shadow 150ms;
|
|
2293
2765
|
cursor: pointer;
|
|
2294
2766
|
}
|
|
@@ -2302,9 +2774,12 @@ function ensureFileStyles() {
|
|
|
2302
2774
|
box-shadow: 0 0 0 4px rgba(191,219,254,0.7);
|
|
2303
2775
|
}
|
|
2304
2776
|
|
|
2305
|
-
/* Upload zone inside wide tile
|
|
2777
|
+
/* Upload zone inside wide tile.
|
|
2778
|
+
flex: 1 1 220px \u2014 wants at least 220px; if the container can't fit
|
|
2779
|
+
upload + library on one row (~220 + 176), library wraps below. */
|
|
2306
2780
|
.fb-wide-tile-upload {
|
|
2307
|
-
flex: 1;
|
|
2781
|
+
flex: 1 1 220px;
|
|
2782
|
+
min-height: 140px;
|
|
2308
2783
|
display: flex;
|
|
2309
2784
|
flex-direction: column;
|
|
2310
2785
|
align-items: center;
|
|
@@ -2317,24 +2792,21 @@ function ensureFileStyles() {
|
|
|
2317
2792
|
background: transparent;
|
|
2318
2793
|
border: none;
|
|
2319
2794
|
font-family: inherit;
|
|
2795
|
+
/* Dashed separator from library: right side when in a row, bottom when
|
|
2796
|
+
wrapped (the line then sits between the two stacked cards). */
|
|
2797
|
+
border-right: 1px dashed rgba(96,165,250,0.5);
|
|
2320
2798
|
}
|
|
2321
2799
|
.fb-wide-tile-upload:hover {
|
|
2322
2800
|
background: rgba(191,219,254,0.25);
|
|
2323
2801
|
}
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
.
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
border-left: 1px dashed rgba(96,165,250,0.5);
|
|
2330
|
-
background: transparent;
|
|
2331
|
-
flex-shrink: 0;
|
|
2332
|
-
}
|
|
2333
|
-
|
|
2334
|
-
/* Library zone inside wide tile */
|
|
2802
|
+
/* Library zone inside wide tile.
|
|
2803
|
+
flex: 0 0 176px \u2014 fixed 176px, never grows. Upload fills the rest in
|
|
2804
|
+
row layout. When the tile wraps to two rows on narrow containers,
|
|
2805
|
+
library stays 176px wide on its own row (left-aligned), preserving the
|
|
2806
|
+
visual hierarchy "upload > library" in both layouts. */
|
|
2335
2807
|
.fb-wide-tile-library {
|
|
2336
|
-
|
|
2337
|
-
|
|
2808
|
+
flex: 0 0 176px;
|
|
2809
|
+
min-height: 120px;
|
|
2338
2810
|
display: flex;
|
|
2339
2811
|
flex-direction: column;
|
|
2340
2812
|
align-items: center;
|
|
@@ -2351,6 +2823,10 @@ function ensureFileStyles() {
|
|
|
2351
2823
|
.fb-wide-tile-library:hover {
|
|
2352
2824
|
background: rgba(191,219,254,0.25);
|
|
2353
2825
|
}
|
|
2826
|
+
/* Narrow-tile mode lives in a separate <style> tag (see below) \u2014 the
|
|
2827
|
+
@container rule is appended only when the runtime actually supports
|
|
2828
|
+
container queries, so jsdom (which doesn't) never sees it and stays
|
|
2829
|
+
quiet in test logs. */
|
|
2354
2830
|
|
|
2355
2831
|
/* \u2500\u2500\u2500 Multi-file outer grid container \u2500\u2500\u2500 */
|
|
2356
2832
|
.fb-multi-outer {
|
|
@@ -2729,6 +3205,39 @@ function ensureFileStyles() {
|
|
|
2729
3205
|
}
|
|
2730
3206
|
`;
|
|
2731
3207
|
document.head.appendChild(style);
|
|
3208
|
+
if (typeof CSS !== "undefined" && typeof CSS.supports === "function" && CSS.supports("container-type", "inline-size")) {
|
|
3209
|
+
const cq = document.createElement("style");
|
|
3210
|
+
cq.setAttribute("data-fb-file-styles-cq", "true");
|
|
3211
|
+
cq.textContent = `
|
|
3212
|
+
.fb-wide-tile { container-type: inline-size; }
|
|
3213
|
+
@container (max-width: 408px) {
|
|
3214
|
+
.fb-wide-tile-upload {
|
|
3215
|
+
border-right: none;
|
|
3216
|
+
border-bottom: 1px dashed rgba(96,165,250,0.5);
|
|
3217
|
+
}
|
|
3218
|
+
.fb-wide-tile-library {
|
|
3219
|
+
flex: 1 0 100%;
|
|
3220
|
+
min-height: 0;
|
|
3221
|
+
flex-direction: row;
|
|
3222
|
+
gap: 6px;
|
|
3223
|
+
padding: 8px 12px;
|
|
3224
|
+
font-size: 12px;
|
|
3225
|
+
}
|
|
3226
|
+
.fb-wide-tile-library .fb-wide-tile-library-icon {
|
|
3227
|
+
width: 16px;
|
|
3228
|
+
height: 16px;
|
|
3229
|
+
}
|
|
3230
|
+
.fb-wide-tile-library .fb-wide-tile-library-label {
|
|
3231
|
+
font-size: 12px;
|
|
3232
|
+
font-weight: 500;
|
|
3233
|
+
}
|
|
3234
|
+
.fb-wide-tile-library .fb-wide-tile-library-hint {
|
|
3235
|
+
display: none;
|
|
3236
|
+
}
|
|
3237
|
+
}
|
|
3238
|
+
`;
|
|
3239
|
+
document.head.appendChild(cq);
|
|
3240
|
+
}
|
|
2732
3241
|
}
|
|
2733
3242
|
|
|
2734
3243
|
// src/components/file/dom.ts
|
|
@@ -3738,15 +4247,25 @@ function filterAndSlice(allFiles, currentCount, constraints, state) {
|
|
|
3738
4247
|
}
|
|
3739
4248
|
return { accepted, errorMessage: errorParts.join(" \u2022 ") };
|
|
3740
4249
|
}
|
|
3741
|
-
async function uploadBatch(
|
|
3742
|
-
|
|
4250
|
+
async function uploadBatch(opts) {
|
|
4251
|
+
const {
|
|
4252
|
+
accepted,
|
|
4253
|
+
listEl,
|
|
4254
|
+
state,
|
|
4255
|
+
shouldHideAddTile,
|
|
4256
|
+
buildSuccessTile,
|
|
4257
|
+
prepareForUpload
|
|
4258
|
+
} = opts;
|
|
4259
|
+
if (listEl && shouldHideAddTile) {
|
|
3743
4260
|
const tilesWrap = ensureTilesWrap(listEl);
|
|
3744
4261
|
const addTile = tilesWrap.querySelector(".fb-multi-add-tile-js") ?? tilesWrap.querySelector(".fb-tile-add");
|
|
3745
4262
|
if (addTile) addTile.style.display = "none";
|
|
3746
4263
|
}
|
|
4264
|
+
prepareForUpload?.();
|
|
4265
|
+
const orderedIds = new Array(accepted.length).fill(null);
|
|
3747
4266
|
const failures = [];
|
|
3748
4267
|
await Promise.allSettled(
|
|
3749
|
-
accepted.map(async (file) => {
|
|
4268
|
+
accepted.map(async (file, index) => {
|
|
3750
4269
|
const placeholder = createUploadingTile(file.name, state);
|
|
3751
4270
|
if (listEl) {
|
|
3752
4271
|
const tilesWrap = ensureTilesWrap(listEl);
|
|
@@ -3759,20 +4278,24 @@ async function uploadBatch(accepted, resourceIds, listEl, state) {
|
|
|
3759
4278
|
type: file.type,
|
|
3760
4279
|
size: file.size,
|
|
3761
4280
|
uploadedAt: /* @__PURE__ */ new Date(),
|
|
3762
|
-
file
|
|
4281
|
+
file
|
|
3763
4282
|
});
|
|
3764
|
-
|
|
4283
|
+
orderedIds[index] = rid;
|
|
4284
|
+
if (buildSuccessTile && placeholder.parentNode) {
|
|
4285
|
+
placeholder.replaceWith(buildSuccessTile(rid));
|
|
4286
|
+
} else {
|
|
4287
|
+
placeholder.remove();
|
|
4288
|
+
}
|
|
3765
4289
|
} catch (err) {
|
|
3766
4290
|
const wrapped = err instanceof Error ? err : new Error(String(err));
|
|
3767
4291
|
const cause = wrapped.cause;
|
|
3768
4292
|
const root = cause instanceof Error ? cause : cause !== void 0 ? new Error(String(cause)) : wrapped;
|
|
3769
4293
|
failures.push({ file, error: root });
|
|
3770
|
-
} finally {
|
|
3771
4294
|
placeholder.remove();
|
|
3772
4295
|
}
|
|
3773
4296
|
})
|
|
3774
4297
|
);
|
|
3775
|
-
return { failures };
|
|
4298
|
+
return { failures, orderedIds };
|
|
3776
4299
|
}
|
|
3777
4300
|
function buildBatchErrorMessage(filterError, failures, state) {
|
|
3778
4301
|
if (failures.length === 0) return filterError;
|
|
@@ -3784,68 +4307,70 @@ function buildBatchErrorMessage(filterError, failures, state) {
|
|
|
3784
4307
|
).join(" \u2022 ");
|
|
3785
4308
|
return filterError ? `${filterError} \u2022 ${uploadMsg}` : uploadMsg;
|
|
3786
4309
|
}
|
|
3787
|
-
function
|
|
4310
|
+
async function runMultiFileBatch(opts, files, listEl, errorTarget) {
|
|
4311
|
+
const {
|
|
4312
|
+
resourceIds,
|
|
4313
|
+
state,
|
|
4314
|
+
updateCallback,
|
|
4315
|
+
constraints,
|
|
4316
|
+
pathKey,
|
|
4317
|
+
instance,
|
|
4318
|
+
buildSuccessTile,
|
|
4319
|
+
prepareForUpload,
|
|
4320
|
+
coordinator
|
|
4321
|
+
} = opts;
|
|
4322
|
+
const { accepted, errorMessage } = filterAndSlice(
|
|
4323
|
+
files,
|
|
4324
|
+
coordinator.getOccupiedCount(),
|
|
4325
|
+
constraints,
|
|
4326
|
+
state
|
|
4327
|
+
);
|
|
4328
|
+
if (errorTarget) {
|
|
4329
|
+
if (errorMessage) showFileError(errorTarget, errorMessage);
|
|
4330
|
+
else clearFileError(errorTarget);
|
|
4331
|
+
}
|
|
4332
|
+
const handle = coordinator.beginBatch(accepted.length);
|
|
4333
|
+
const shouldHideAddTile = coordinator.getOccupiedCount() >= constraints.maxCount;
|
|
4334
|
+
const { failures, orderedIds } = await uploadBatch({
|
|
4335
|
+
accepted,
|
|
4336
|
+
listEl,
|
|
4337
|
+
state,
|
|
4338
|
+
shouldHideAddTile,
|
|
4339
|
+
buildSuccessTile,
|
|
4340
|
+
prepareForUpload
|
|
4341
|
+
});
|
|
4342
|
+
handle.setResults(orderedIds);
|
|
4343
|
+
if (instance && pathKey && !state.config.readonly) {
|
|
4344
|
+
instance.triggerOnChange(pathKey, resourceIds);
|
|
4345
|
+
}
|
|
4346
|
+
const { wasLast } = handle.end();
|
|
4347
|
+
if (wasLast) updateCallback();
|
|
4348
|
+
if (errorTarget) {
|
|
4349
|
+
const combined = buildBatchErrorMessage(errorMessage, failures, state);
|
|
4350
|
+
if (combined) showFileError(errorTarget, combined);
|
|
4351
|
+
else clearFileError(errorTarget);
|
|
4352
|
+
}
|
|
4353
|
+
}
|
|
4354
|
+
function setupFilesDropHandler(opts) {
|
|
4355
|
+
const { filesContainer } = opts;
|
|
3788
4356
|
setupDragAndDrop(filesContainer, async (files) => {
|
|
3789
|
-
const { accepted, errorMessage } = filterAndSlice(
|
|
3790
|
-
Array.from(files),
|
|
3791
|
-
resourceIds.length,
|
|
3792
|
-
constraints,
|
|
3793
|
-
state
|
|
3794
|
-
);
|
|
3795
|
-
if (errorMessage) {
|
|
3796
|
-
showFileError(filesContainer, errorMessage);
|
|
3797
|
-
} else {
|
|
3798
|
-
clearFileError(filesContainer);
|
|
3799
|
-
}
|
|
3800
4357
|
const list = filesContainer.querySelector(".files-list") ?? filesContainer;
|
|
3801
|
-
|
|
3802
|
-
const combined = buildBatchErrorMessage(errorMessage, failures, state);
|
|
3803
|
-
if (combined) {
|
|
3804
|
-
showFileError(filesContainer, combined);
|
|
3805
|
-
} else {
|
|
3806
|
-
clearFileError(filesContainer);
|
|
3807
|
-
}
|
|
3808
|
-
updateCallback();
|
|
3809
|
-
if (instance && pathKey && !state.config.readonly) {
|
|
3810
|
-
instance.triggerOnChange(pathKey, resourceIds);
|
|
3811
|
-
}
|
|
4358
|
+
await runMultiFileBatch(opts, Array.from(files), list, filesContainer);
|
|
3812
4359
|
});
|
|
3813
4360
|
}
|
|
3814
|
-
function setupFilesPickerHandler(
|
|
4361
|
+
function setupFilesPickerHandler(opts) {
|
|
4362
|
+
const { filesPicker } = opts;
|
|
3815
4363
|
filesPicker.onchange = async () => {
|
|
3816
4364
|
if (!filesPicker.files) return;
|
|
3817
|
-
const wrapperEl = filesPicker.closest("[data-files-wrapper]")
|
|
3818
|
-
const
|
|
4365
|
+
const wrapperEl = filesPicker.closest("[data-files-wrapper]") ?? filesPicker.parentElement;
|
|
4366
|
+
const listEl = wrapperEl?.querySelector(".files-list") ?? null;
|
|
4367
|
+
await runMultiFileBatch(
|
|
4368
|
+
opts,
|
|
3819
4369
|
Array.from(filesPicker.files),
|
|
3820
|
-
|
|
3821
|
-
|
|
3822
|
-
state
|
|
4370
|
+
listEl,
|
|
4371
|
+
wrapperEl
|
|
3823
4372
|
);
|
|
3824
|
-
if (errorMessage && wrapperEl) {
|
|
3825
|
-
showFileError(wrapperEl, errorMessage);
|
|
3826
|
-
} else if (wrapperEl) {
|
|
3827
|
-
clearFileError(wrapperEl);
|
|
3828
|
-
}
|
|
3829
|
-
const listEl = wrapperEl?.querySelector(".files-list");
|
|
3830
|
-
const { failures } = await uploadBatch(
|
|
3831
|
-
accepted,
|
|
3832
|
-
resourceIds,
|
|
3833
|
-
listEl ?? null,
|
|
3834
|
-
state
|
|
3835
|
-
);
|
|
3836
|
-
if (wrapperEl) {
|
|
3837
|
-
const combined = buildBatchErrorMessage(errorMessage, failures, state);
|
|
3838
|
-
if (combined) {
|
|
3839
|
-
showFileError(wrapperEl, combined);
|
|
3840
|
-
} else {
|
|
3841
|
-
clearFileError(wrapperEl);
|
|
3842
|
-
}
|
|
3843
|
-
}
|
|
3844
|
-
updateCallback();
|
|
3845
4373
|
filesPicker.value = "";
|
|
3846
|
-
if (instance && pathKey && !state.config.readonly) {
|
|
3847
|
-
instance.triggerOnChange(pathKey, resourceIds);
|
|
3848
|
-
}
|
|
3849
4374
|
};
|
|
3850
4375
|
}
|
|
3851
4376
|
|
|
@@ -3886,16 +4411,6 @@ function validatePickedResource(resource, allowedExtensions, allowedMimes, maxSi
|
|
|
3886
4411
|
}
|
|
3887
4412
|
return null;
|
|
3888
4413
|
}
|
|
3889
|
-
function readCurrentResourceIds(wrapper) {
|
|
3890
|
-
const raw = wrapper.dataset.resourceIds;
|
|
3891
|
-
if (!raw) return [];
|
|
3892
|
-
try {
|
|
3893
|
-
const parsed = JSON.parse(raw);
|
|
3894
|
-
return Array.isArray(parsed) ? parsed : [];
|
|
3895
|
-
} catch {
|
|
3896
|
-
return [];
|
|
3897
|
-
}
|
|
3898
|
-
}
|
|
3899
4414
|
function registerPickedResource(resource, state) {
|
|
3900
4415
|
const existing = state.resourceIndex.get(resource.resourceId);
|
|
3901
4416
|
state.resourceIndex.set(resource.resourceId, {
|
|
@@ -3910,13 +4425,27 @@ function extractPickerError(error, state) {
|
|
|
3910
4425
|
if (error instanceof Error && error.message) return error.message;
|
|
3911
4426
|
return t("pickerError", state);
|
|
3912
4427
|
}
|
|
3913
|
-
async function handleLibraryPickMulti(
|
|
4428
|
+
async function handleLibraryPickMulti(opts) {
|
|
4429
|
+
const {
|
|
4430
|
+
state,
|
|
4431
|
+
element,
|
|
4432
|
+
wrapper,
|
|
4433
|
+
fieldPath,
|
|
4434
|
+
resourceIds,
|
|
4435
|
+
maxCount,
|
|
4436
|
+
updateCallback,
|
|
4437
|
+
instance,
|
|
4438
|
+
coordinator,
|
|
4439
|
+
list,
|
|
4440
|
+
buildSuccessTile
|
|
4441
|
+
} = opts;
|
|
3914
4442
|
if (!state.config.pickExistingFiles) return;
|
|
3915
4443
|
const allowedExtensions = getAllowedExtensions(element.accept);
|
|
3916
4444
|
const allowedMimes = getAllowedMimes(element.accept);
|
|
3917
4445
|
const maxSizeMB = element.maxSize ?? Infinity;
|
|
3918
|
-
const
|
|
3919
|
-
const
|
|
4446
|
+
const knownRids = coordinator.getAllKnownRids();
|
|
4447
|
+
const existingSet = new Set(knownRids);
|
|
4448
|
+
const remaining = maxCount === Infinity ? Infinity : Math.max(0, maxCount - coordinator.getOccupiedCount());
|
|
3920
4449
|
let picked;
|
|
3921
4450
|
try {
|
|
3922
4451
|
picked = await state.config.pickExistingFiles({
|
|
@@ -3925,14 +4454,15 @@ async function handleLibraryPickMulti(state, element, wrapper, fieldPath, resour
|
|
|
3925
4454
|
accept: buildAcceptContext(element),
|
|
3926
4455
|
maxSizeMB: maxSizeMB === Infinity ? void 0 : maxSizeMB,
|
|
3927
4456
|
remainingSlots: remaining === Infinity ? void 0 : remaining,
|
|
3928
|
-
|
|
4457
|
+
// Hand the host every rid that's already selected (committed or
|
|
4458
|
+
// staged) so it can grey them out or filter them.
|
|
4459
|
+
selectedResourceIds: knownRids
|
|
3929
4460
|
});
|
|
3930
4461
|
} catch (error) {
|
|
3931
4462
|
showFileError(wrapper, extractPickerError(error, state));
|
|
3932
4463
|
return;
|
|
3933
4464
|
}
|
|
3934
4465
|
if (picked.length === 0) return;
|
|
3935
|
-
const existingSet = new Set(currentIds);
|
|
3936
4466
|
const seen = /* @__PURE__ */ new Set();
|
|
3937
4467
|
const deduped = picked.filter((r) => {
|
|
3938
4468
|
if (existingSet.has(r.resourceId)) return false;
|
|
@@ -3950,10 +4480,18 @@ async function handleLibraryPickMulti(state, element, wrapper, fieldPath, resour
|
|
|
3950
4480
|
);
|
|
3951
4481
|
return err === null;
|
|
3952
4482
|
});
|
|
3953
|
-
const freshRemaining = maxCount === Infinity ? validItems.length : Math.max(0, maxCount -
|
|
4483
|
+
const freshRemaining = maxCount === Infinity ? validItems.length : Math.max(0, maxCount - coordinator.getOccupiedCount());
|
|
3954
4484
|
const accepted = validItems.slice(0, freshRemaining);
|
|
3955
4485
|
const skipped = validItems.length - accepted.length;
|
|
3956
|
-
if (accepted.length === 0)
|
|
4486
|
+
if (accepted.length === 0) {
|
|
4487
|
+
if (skipped > 0) {
|
|
4488
|
+
showFileError(
|
|
4489
|
+
wrapper,
|
|
4490
|
+
t("filesLimitExceeded", state, { skipped, max: maxCount })
|
|
4491
|
+
);
|
|
4492
|
+
}
|
|
4493
|
+
return;
|
|
4494
|
+
}
|
|
3957
4495
|
clearFileError(wrapper);
|
|
3958
4496
|
if (skipped > 0) {
|
|
3959
4497
|
showFileError(
|
|
@@ -3963,13 +4501,22 @@ async function handleLibraryPickMulti(state, element, wrapper, fieldPath, resour
|
|
|
3963
4501
|
}
|
|
3964
4502
|
for (const resource of accepted) {
|
|
3965
4503
|
registerPickedResource(resource, state);
|
|
3966
|
-
resourceIds.push(resource.resourceId);
|
|
3967
4504
|
}
|
|
3968
|
-
|
|
3969
|
-
|
|
4505
|
+
const acceptedIds = accepted.map((r) => r.resourceId);
|
|
4506
|
+
const handle = coordinator.beginBatch(accepted.length);
|
|
4507
|
+
handle.setResults(acceptedIds);
|
|
3970
4508
|
if (!state.config.readonly) {
|
|
3971
4509
|
instance.triggerOnChange(fieldPath, resourceIds);
|
|
3972
4510
|
}
|
|
4511
|
+
const { wasLast } = handle.end();
|
|
4512
|
+
if (wasLast) {
|
|
4513
|
+
updateCallback();
|
|
4514
|
+
} else {
|
|
4515
|
+
const tilesWrap = ensureTilesWrap(list);
|
|
4516
|
+
for (const rid of acceptedIds) {
|
|
4517
|
+
tilesWrap.appendChild(buildSuccessTile(rid));
|
|
4518
|
+
}
|
|
4519
|
+
}
|
|
3973
4520
|
}
|
|
3974
4521
|
async function handleLibraryPickSingle(state, element, container, fileWrapper, pathKey, fieldPath, renderCallback, instance) {
|
|
3975
4522
|
if (!state.config.pickExistingFiles) return;
|
|
@@ -4074,21 +4621,21 @@ function buildWideTile(state, hasLibrary, onUploadClick, onLibraryClick, isDragO
|
|
|
4074
4621
|
};
|
|
4075
4622
|
outer.appendChild(uploadBtn);
|
|
4076
4623
|
if (hasLibrary && onLibraryClick) {
|
|
4077
|
-
const divider = document.createElement("div");
|
|
4078
|
-
divider.className = "fb-wide-tile-divider";
|
|
4079
|
-
outer.appendChild(divider);
|
|
4080
4624
|
const libBtn = document.createElement("button");
|
|
4081
4625
|
libBtn.type = "button";
|
|
4082
4626
|
libBtn.className = "fb-wide-tile-library fb-file-library-card";
|
|
4083
4627
|
const libIcon = document.createElement("span");
|
|
4628
|
+
libIcon.className = "fb-wide-tile-library-icon";
|
|
4084
4629
|
libIcon.style.cssText = "width:28px;height:28px;display:block;flex-shrink:0;";
|
|
4085
4630
|
libIcon.innerHTML = ICON_LIBRARY2;
|
|
4086
4631
|
libBtn.appendChild(libIcon);
|
|
4087
4632
|
const libLabel = document.createElement("div");
|
|
4633
|
+
libLabel.className = "fb-wide-tile-library-label";
|
|
4088
4634
|
libLabel.style.cssText = "font-size:13px;font-weight:600;text-align:center;";
|
|
4089
4635
|
libLabel.textContent = t("fromLibrary", state);
|
|
4090
4636
|
libBtn.appendChild(libLabel);
|
|
4091
4637
|
const libHint = document.createElement("div");
|
|
4638
|
+
libHint.className = "fb-wide-tile-library-hint";
|
|
4092
4639
|
libHint.style.cssText = "font-size:11px;opacity:0.75;text-align:center;";
|
|
4093
4640
|
libHint.textContent = t("libraryHint", state);
|
|
4094
4641
|
libBtn.appendChild(libHint);
|
|
@@ -4290,6 +4837,14 @@ function buildMetaDot() {
|
|
|
4290
4837
|
return dot;
|
|
4291
4838
|
}
|
|
4292
4839
|
var gridResizeObservers = /* @__PURE__ */ new WeakMap();
|
|
4840
|
+
function disposePlaceholdersForUpload(container) {
|
|
4841
|
+
const observer = gridResizeObservers.get(container);
|
|
4842
|
+
if (observer) {
|
|
4843
|
+
observer.disconnect();
|
|
4844
|
+
gridResizeObservers.delete(container);
|
|
4845
|
+
}
|
|
4846
|
+
container.querySelectorAll(".fb-multi-placeholder").forEach((p) => p.remove());
|
|
4847
|
+
}
|
|
4293
4848
|
function renderResourcePills(opts) {
|
|
4294
4849
|
const {
|
|
4295
4850
|
container,
|
|
@@ -4637,16 +5192,19 @@ function setupMultiFileEditMode(element, ctx, wrapper, pathKey, maxFiles) {
|
|
|
4637
5192
|
filesPicker.click();
|
|
4638
5193
|
};
|
|
4639
5194
|
const onLibraryPick = state.config.pickExistingFiles && !element.disableLibrary ? () => {
|
|
4640
|
-
handleLibraryPickMulti(
|
|
5195
|
+
handleLibraryPickMulti({
|
|
4641
5196
|
state,
|
|
4642
5197
|
element,
|
|
4643
|
-
filesWrapper,
|
|
4644
|
-
pathKey,
|
|
4645
|
-
initialFiles,
|
|
4646
|
-
maxFiles,
|
|
4647
|
-
updateFilesDisplay,
|
|
4648
|
-
ctx.instance
|
|
4649
|
-
|
|
5198
|
+
wrapper: filesWrapper,
|
|
5199
|
+
fieldPath: pathKey,
|
|
5200
|
+
resourceIds: initialFiles,
|
|
5201
|
+
maxCount: maxFiles,
|
|
5202
|
+
updateCallback: updateFilesDisplay,
|
|
5203
|
+
instance: ctx.instance,
|
|
5204
|
+
coordinator,
|
|
5205
|
+
list,
|
|
5206
|
+
buildSuccessTile
|
|
5207
|
+
}).catch((err) => {
|
|
4650
5208
|
console.error("Library pick failed:", err);
|
|
4651
5209
|
});
|
|
4652
5210
|
} : null;
|
|
@@ -4660,37 +5218,153 @@ function setupMultiFileEditMode(element, ctx, wrapper, pathKey, maxFiles) {
|
|
|
4660
5218
|
releaseLocalFileUrl(state.resourceIndex.get(ridToRemove)?.file);
|
|
4661
5219
|
const index = initialFiles.indexOf(ridToRemove);
|
|
4662
5220
|
if (index > -1) initialFiles.splice(index, 1);
|
|
4663
|
-
|
|
5221
|
+
if (coordinator.hasInFlightBatches()) {
|
|
5222
|
+
pendingRemovals.add(ridToRemove);
|
|
5223
|
+
list.querySelector(`[data-resource-id="${ridToRemove}"]`)?.remove();
|
|
5224
|
+
filesWrapper.dataset.resourceIds = JSON.stringify(initialFiles);
|
|
5225
|
+
} else {
|
|
5226
|
+
updateFilesDisplay();
|
|
5227
|
+
}
|
|
5228
|
+
if (ctx.instance && pathKey && !state.config.readonly) {
|
|
5229
|
+
ctx.instance.triggerOnChange(pathKey, initialFiles);
|
|
5230
|
+
}
|
|
4664
5231
|
},
|
|
4665
5232
|
maxCount: maxFiles < Infinity ? maxFiles : void 0,
|
|
4666
5233
|
isReadonly: currentlyReadonly,
|
|
4667
5234
|
onLibraryPick: currentlyReadonly ? null : onLibraryPick,
|
|
4668
5235
|
element,
|
|
4669
5236
|
onClearAll: currentlyReadonly ? void 0 : () => {
|
|
5237
|
+
for (const rid of initialFiles) {
|
|
5238
|
+
releaseLocalFileUrl(state.resourceIndex.get(rid)?.file);
|
|
5239
|
+
}
|
|
4670
5240
|
initialFiles.splice(0);
|
|
4671
|
-
|
|
5241
|
+
if (coordinator.hasInFlightBatches()) {
|
|
5242
|
+
const visibleTiles = list.querySelectorAll("[data-resource-id]");
|
|
5243
|
+
for (const tile of visibleTiles) {
|
|
5244
|
+
const rid = tile.dataset.resourceId;
|
|
5245
|
+
if (rid) {
|
|
5246
|
+
releaseLocalFileUrl(state.resourceIndex.get(rid)?.file);
|
|
5247
|
+
pendingRemovals.add(rid);
|
|
5248
|
+
}
|
|
5249
|
+
tile.remove();
|
|
5250
|
+
}
|
|
5251
|
+
filesWrapper.dataset.resourceIds = JSON.stringify(initialFiles);
|
|
5252
|
+
} else {
|
|
5253
|
+
updateFilesDisplay();
|
|
5254
|
+
}
|
|
5255
|
+
if (ctx.instance && pathKey && !state.config.readonly) {
|
|
5256
|
+
ctx.instance.triggerOnChange(pathKey, initialFiles);
|
|
5257
|
+
}
|
|
4672
5258
|
},
|
|
4673
5259
|
openPicker
|
|
4674
5260
|
});
|
|
4675
5261
|
}
|
|
4676
|
-
|
|
4677
|
-
|
|
4678
|
-
|
|
4679
|
-
|
|
4680
|
-
|
|
4681
|
-
|
|
4682
|
-
|
|
4683
|
-
|
|
4684
|
-
|
|
4685
|
-
|
|
4686
|
-
|
|
4687
|
-
|
|
5262
|
+
let inFlightFiles = 0;
|
|
5263
|
+
let activeBatches = 0;
|
|
5264
|
+
let nextBatchOrdinal = 0;
|
|
5265
|
+
let nextCommitOrdinal = 0;
|
|
5266
|
+
const stagedResults = /* @__PURE__ */ new Map();
|
|
5267
|
+
const batchReservations = /* @__PURE__ */ new Map();
|
|
5268
|
+
const pendingRemovals = /* @__PURE__ */ new Set();
|
|
5269
|
+
const drainContiguousStagedResults = () => {
|
|
5270
|
+
while (stagedResults.has(nextCommitOrdinal)) {
|
|
5271
|
+
const ordinal = nextCommitOrdinal;
|
|
5272
|
+
const ids = stagedResults.get(ordinal);
|
|
5273
|
+
stagedResults.delete(ordinal);
|
|
5274
|
+
nextCommitOrdinal += 1;
|
|
5275
|
+
const reservation = batchReservations.get(ordinal) ?? 0;
|
|
5276
|
+
batchReservations.delete(ordinal);
|
|
5277
|
+
inFlightFiles -= reservation;
|
|
5278
|
+
for (const rid of ids) {
|
|
5279
|
+
if (rid === null) continue;
|
|
5280
|
+
if (pendingRemovals.has(rid)) {
|
|
5281
|
+
pendingRemovals.delete(rid);
|
|
5282
|
+
continue;
|
|
5283
|
+
}
|
|
5284
|
+
initialFiles.push(rid);
|
|
5285
|
+
}
|
|
5286
|
+
}
|
|
5287
|
+
filesWrapper.dataset.resourceIds = JSON.stringify(initialFiles);
|
|
5288
|
+
};
|
|
5289
|
+
const coordinator = {
|
|
5290
|
+
getOccupiedCount: () => initialFiles.length + inFlightFiles,
|
|
5291
|
+
getAllKnownRids: () => {
|
|
5292
|
+
const out = [...initialFiles];
|
|
5293
|
+
for (const ids of stagedResults.values()) {
|
|
5294
|
+
for (const rid of ids) {
|
|
5295
|
+
if (rid !== null) out.push(rid);
|
|
5296
|
+
}
|
|
5297
|
+
}
|
|
5298
|
+
return out;
|
|
5299
|
+
},
|
|
5300
|
+
hasInFlightBatches: () => activeBatches > 0 || batchReservations.size > 0,
|
|
5301
|
+
wasRemovedDuringBatch: (rid) => pendingRemovals.has(rid),
|
|
5302
|
+
beginBatch: (count) => {
|
|
5303
|
+
inFlightFiles += count;
|
|
5304
|
+
activeBatches += 1;
|
|
5305
|
+
const ordinal = nextBatchOrdinal++;
|
|
5306
|
+
batchReservations.set(ordinal, count);
|
|
5307
|
+
return {
|
|
5308
|
+
setResults: (orderedIds) => {
|
|
5309
|
+
stagedResults.set(ordinal, orderedIds);
|
|
5310
|
+
drainContiguousStagedResults();
|
|
5311
|
+
},
|
|
5312
|
+
end: () => {
|
|
5313
|
+
activeBatches -= 1;
|
|
5314
|
+
const wasLast = activeBatches === 0 && batchReservations.size === 0;
|
|
5315
|
+
if (wasLast) {
|
|
5316
|
+
pendingRemovals.clear();
|
|
5317
|
+
}
|
|
5318
|
+
return { wasLast };
|
|
5319
|
+
}
|
|
5320
|
+
};
|
|
5321
|
+
}
|
|
5322
|
+
};
|
|
5323
|
+
const buildSuccessTile = (rid) => {
|
|
5324
|
+
const currentlyReadonly = isElementReadonly(element, state);
|
|
5325
|
+
return buildPreviewTile(
|
|
5326
|
+
rid,
|
|
5327
|
+
state,
|
|
5328
|
+
!currentlyReadonly,
|
|
5329
|
+
currentlyReadonly ? null : () => {
|
|
5330
|
+
releaseLocalFileUrl(state.resourceIndex.get(rid)?.file);
|
|
5331
|
+
const idx = initialFiles.indexOf(rid);
|
|
5332
|
+
if (idx > -1) {
|
|
5333
|
+
initialFiles.splice(idx, 1);
|
|
5334
|
+
if (coordinator.hasInFlightBatches()) {
|
|
5335
|
+
pendingRemovals.add(rid);
|
|
5336
|
+
list.querySelector(`[data-resource-id="${rid}"]`)?.remove();
|
|
5337
|
+
filesWrapper.dataset.resourceIds = JSON.stringify(initialFiles);
|
|
5338
|
+
} else {
|
|
5339
|
+
updateFilesDisplay();
|
|
5340
|
+
}
|
|
5341
|
+
if (ctx.instance && pathKey && !state.config.readonly) {
|
|
5342
|
+
ctx.instance.triggerOnChange(pathKey, initialFiles);
|
|
5343
|
+
}
|
|
5344
|
+
return;
|
|
5345
|
+
}
|
|
5346
|
+
pendingRemovals.add(rid);
|
|
5347
|
+
list.querySelector(`[data-resource-id="${rid}"]`)?.remove();
|
|
5348
|
+
if (ctx.instance && pathKey && !state.config.readonly) {
|
|
5349
|
+
ctx.instance.triggerOnChange(pathKey, initialFiles);
|
|
5350
|
+
}
|
|
5351
|
+
}
|
|
5352
|
+
);
|
|
5353
|
+
};
|
|
5354
|
+
const prepareForUpload = () => disposePlaceholdersForUpload(list);
|
|
5355
|
+
const sharedHandlerOpts = {
|
|
5356
|
+
resourceIds: initialFiles,
|
|
4688
5357
|
state,
|
|
4689
|
-
updateFilesDisplay,
|
|
5358
|
+
updateCallback: updateFilesDisplay,
|
|
4690
5359
|
constraints,
|
|
4691
5360
|
pathKey,
|
|
4692
|
-
ctx.instance
|
|
4693
|
-
|
|
5361
|
+
instance: ctx.instance,
|
|
5362
|
+
buildSuccessTile,
|
|
5363
|
+
prepareForUpload,
|
|
5364
|
+
coordinator
|
|
5365
|
+
};
|
|
5366
|
+
setupFilesDropHandler({ ...sharedHandlerOpts, filesContainer });
|
|
5367
|
+
setupFilesPickerHandler({ ...sharedHandlerOpts, filesPicker });
|
|
4694
5368
|
updateFilesDisplay();
|
|
4695
5369
|
wrapper.appendChild(filesWrapper);
|
|
4696
5370
|
}
|
|
@@ -5330,7 +6004,7 @@ function validateColourElement(element, key, context) {
|
|
|
5330
6004
|
};
|
|
5331
6005
|
if (element.multiple) {
|
|
5332
6006
|
const hexInputs = scopeRoot.querySelectorAll(
|
|
5333
|
-
`[name^="${key}["].colour-hex-input`
|
|
6007
|
+
`[name^="${key}\\["].colour-hex-input`
|
|
5334
6008
|
);
|
|
5335
6009
|
const values = [];
|
|
5336
6010
|
hexInputs.forEach((input, index) => {
|
|
@@ -5379,7 +6053,7 @@ function updateColourField(element, fieldPath, value, context) {
|
|
|
5379
6053
|
return;
|
|
5380
6054
|
}
|
|
5381
6055
|
const hexInputs = scopeRoot.querySelectorAll(
|
|
5382
|
-
`[name^="${fieldPath}["].colour-hex-input`
|
|
6056
|
+
`[name^="${fieldPath}\\["].colour-hex-input`
|
|
5383
6057
|
);
|
|
5384
6058
|
hexInputs.forEach((hexInput, index) => {
|
|
5385
6059
|
if (index < value.length) {
|
|
@@ -5387,6 +6061,7 @@ function updateColourField(element, fieldPath, value, context) {
|
|
|
5387
6061
|
hexInput.value = normalized;
|
|
5388
6062
|
hexInput.classList.remove("invalid");
|
|
5389
6063
|
hexInput.title = "";
|
|
6064
|
+
clearFieldError(hexInput);
|
|
5390
6065
|
const wrapper = hexInput.closest(".colour-picker-wrapper");
|
|
5391
6066
|
if (wrapper) {
|
|
5392
6067
|
const swatch = wrapper.querySelector(".colour-swatch");
|
|
@@ -5416,6 +6091,7 @@ function updateColourField(element, fieldPath, value, context) {
|
|
|
5416
6091
|
hexInput.value = normalized;
|
|
5417
6092
|
hexInput.classList.remove("invalid");
|
|
5418
6093
|
hexInput.title = "";
|
|
6094
|
+
clearFieldError(hexInput);
|
|
5419
6095
|
const wrapper = hexInput.closest(".colour-picker-wrapper");
|
|
5420
6096
|
if (wrapper) {
|
|
5421
6097
|
const swatch = wrapper.querySelector(".colour-swatch");
|
|
@@ -5818,7 +6494,7 @@ function validateSliderElement(element, key, context) {
|
|
|
5818
6494
|
};
|
|
5819
6495
|
if (element.multiple) {
|
|
5820
6496
|
const sliders = scopeRoot.querySelectorAll(
|
|
5821
|
-
`input[type="range"][name^="${key}["]`
|
|
6497
|
+
`input[type="range"][name^="${key}\\["]`
|
|
5822
6498
|
);
|
|
5823
6499
|
const values = [];
|
|
5824
6500
|
sliders.forEach((slider, index) => {
|
|
@@ -5869,7 +6545,7 @@ function updateSliderField(element, fieldPath, value, context) {
|
|
|
5869
6545
|
return;
|
|
5870
6546
|
}
|
|
5871
6547
|
const sliders = scopeRoot.querySelectorAll(
|
|
5872
|
-
`input[type="range"][name^="${fieldPath}["]`
|
|
6548
|
+
`input[type="range"][name^="${fieldPath}\\["]`
|
|
5873
6549
|
);
|
|
5874
6550
|
sliders.forEach((slider, index) => {
|
|
5875
6551
|
if (index < value.length && value[index] !== null) {
|
|
@@ -5897,6 +6573,7 @@ function updateSliderField(element, fieldPath, value, context) {
|
|
|
5897
6573
|
}
|
|
5898
6574
|
slider.classList.remove("invalid");
|
|
5899
6575
|
slider.title = "";
|
|
6576
|
+
clearFieldError(slider);
|
|
5900
6577
|
}
|
|
5901
6578
|
});
|
|
5902
6579
|
if (value.length !== sliders.length) {
|
|
@@ -5933,6 +6610,7 @@ function updateSliderField(element, fieldPath, value, context) {
|
|
|
5933
6610
|
}
|
|
5934
6611
|
slider.classList.remove("invalid");
|
|
5935
6612
|
slider.title = "";
|
|
6613
|
+
clearFieldError(slider);
|
|
5936
6614
|
}
|
|
5937
6615
|
}
|
|
5938
6616
|
}
|
|
@@ -6055,34 +6733,24 @@ function getChildWrapperClass(isSlides, columns) {
|
|
|
6055
6733
|
const cols = columns || 1;
|
|
6056
6734
|
return cols === 1 ? "space-y-2" : `grid grid-cols-${cols} gap-2`;
|
|
6057
6735
|
}
|
|
6058
|
-
function mountRemoveButton(item, onRemove) {
|
|
6736
|
+
function mountRemoveButton(item, onRemove, state) {
|
|
6059
6737
|
const rem = document.createElement("button");
|
|
6060
6738
|
rem.type = "button";
|
|
6061
6739
|
rem.className = "fb-item-remove";
|
|
6740
|
+
rem.setAttribute("aria-label", t("removeElement", state));
|
|
6062
6741
|
rem.style.cssText = `
|
|
6063
|
-
width:
|
|
6064
|
-
height:
|
|
6742
|
+
width: 24px;
|
|
6743
|
+
height: 24px;
|
|
6065
6744
|
display: inline-flex;
|
|
6066
6745
|
align-items: center;
|
|
6067
6746
|
justify-content: center;
|
|
6068
6747
|
padding: 0;
|
|
6069
|
-
line-height: 1;
|
|
6070
|
-
font-size: 14px;
|
|
6071
|
-
color: var(--fb-error-color);
|
|
6072
|
-
background-color: transparent;
|
|
6073
6748
|
border: 0;
|
|
6074
6749
|
border-radius: 4px;
|
|
6075
6750
|
cursor: pointer;
|
|
6076
6751
|
flex-shrink: 0;
|
|
6077
|
-
transition: background-color var(--fb-transition-duration);
|
|
6078
6752
|
`;
|
|
6079
|
-
rem.
|
|
6080
|
-
rem.addEventListener("mouseenter", () => {
|
|
6081
|
-
rem.style.backgroundColor = "var(--fb-background-hover-color)";
|
|
6082
|
-
});
|
|
6083
|
-
rem.addEventListener("mouseleave", () => {
|
|
6084
|
-
rem.style.backgroundColor = "transparent";
|
|
6085
|
-
});
|
|
6753
|
+
rem.innerHTML = BIN_ICON_SVG;
|
|
6086
6754
|
rem.onclick = onRemove;
|
|
6087
6755
|
const labelRow = item.querySelector("[data-fb-label-row]");
|
|
6088
6756
|
if (labelRow) {
|
|
@@ -6108,7 +6776,7 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
|
|
|
6108
6776
|
itemsWrap.className = "fb-container-slides";
|
|
6109
6777
|
const slideCols = element.columns;
|
|
6110
6778
|
const gridTemplateColumns = typeof slideCols === "number" && slideCols > 0 ? `repeat(${slideCols}, 1fr)` : "repeat(auto-fit, minmax(280px, 1fr))";
|
|
6111
|
-
itemsWrap.style.cssText = `display:grid;grid-template-columns:${gridTemplateColumns};gap:
|
|
6779
|
+
itemsWrap.style.cssText = `display:grid;grid-template-columns:${gridTemplateColumns};gap:var(--fb-slides-gap, 14px);align-items:start;`;
|
|
6112
6780
|
} else {
|
|
6113
6781
|
itemsWrap.className = "space-y-2";
|
|
6114
6782
|
}
|
|
@@ -6139,6 +6807,9 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
|
|
|
6139
6807
|
const item = document.createElement("div");
|
|
6140
6808
|
item.className = "containerItem border border-gray-300 rounded-lg p-2 bg-white";
|
|
6141
6809
|
item.setAttribute("data-container-item", `${element.key}[${idx}]`);
|
|
6810
|
+
if (isSlides) {
|
|
6811
|
+
item.setAttribute("data-fb-slide-card", "");
|
|
6812
|
+
}
|
|
6142
6813
|
const childWrapper = document.createElement("div");
|
|
6143
6814
|
childWrapper.className = getChildWrapperClass(isSlides, element.columns);
|
|
6144
6815
|
element.elements.forEach((child) => {
|
|
@@ -6155,7 +6826,7 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
|
|
|
6155
6826
|
});
|
|
6156
6827
|
item.appendChild(childWrapper);
|
|
6157
6828
|
if (!containerIsReadonly) {
|
|
6158
|
-
mountRemoveButton(item, () => handleRemoveItem(item));
|
|
6829
|
+
mountRemoveButton(item, () => handleRemoveItem(item), state);
|
|
6159
6830
|
}
|
|
6160
6831
|
if (slideAddTile && slideAddTile.parentElement === itemsWrap) {
|
|
6161
6832
|
itemsWrap.insertBefore(item, slideAddTile);
|
|
@@ -6201,6 +6872,9 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
|
|
|
6201
6872
|
const item = document.createElement("div");
|
|
6202
6873
|
item.className = "containerItem border border-gray-300 rounded-lg p-2 bg-white";
|
|
6203
6874
|
item.setAttribute("data-container-item", `${element.key}[${idx}]`);
|
|
6875
|
+
if (isSlides) {
|
|
6876
|
+
item.setAttribute("data-fb-slide-card", "");
|
|
6877
|
+
}
|
|
6204
6878
|
const childWrapper = document.createElement("div");
|
|
6205
6879
|
if (isSlides) {
|
|
6206
6880
|
childWrapper.className = "space-y-2";
|
|
@@ -6224,7 +6898,7 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
|
|
|
6224
6898
|
});
|
|
6225
6899
|
item.appendChild(childWrapper);
|
|
6226
6900
|
if (!containerIsReadonly) {
|
|
6227
|
-
mountRemoveButton(item, () => handleRemoveItem(item));
|
|
6901
|
+
mountRemoveButton(item, () => handleRemoveItem(item), ctx.state);
|
|
6228
6902
|
}
|
|
6229
6903
|
itemsWrap.appendChild(item);
|
|
6230
6904
|
});
|
|
@@ -6244,6 +6918,9 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
|
|
|
6244
6918
|
const item = document.createElement("div");
|
|
6245
6919
|
item.className = "containerItem border border-gray-300 rounded-lg p-2 bg-white";
|
|
6246
6920
|
item.setAttribute("data-container-item", `${element.key}[${idx}]`);
|
|
6921
|
+
if (isSlides) {
|
|
6922
|
+
item.setAttribute("data-fb-slide-card", "");
|
|
6923
|
+
}
|
|
6247
6924
|
const childWrapper = document.createElement("div");
|
|
6248
6925
|
if (isSlides) {
|
|
6249
6926
|
childWrapper.className = "space-y-2";
|
|
@@ -6268,11 +6945,15 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
|
|
|
6268
6945
|
}
|
|
6269
6946
|
});
|
|
6270
6947
|
item.appendChild(childWrapper);
|
|
6271
|
-
mountRemoveButton(
|
|
6272
|
-
|
|
6273
|
-
|
|
6274
|
-
|
|
6275
|
-
|
|
6948
|
+
mountRemoveButton(
|
|
6949
|
+
item,
|
|
6950
|
+
() => {
|
|
6951
|
+
if (countItems() > min) {
|
|
6952
|
+
handleRemoveItem(item);
|
|
6953
|
+
}
|
|
6954
|
+
},
|
|
6955
|
+
ctx.state
|
|
6956
|
+
);
|
|
6276
6957
|
itemsWrap.appendChild(item);
|
|
6277
6958
|
}
|
|
6278
6959
|
}
|
|
@@ -8897,7 +9578,7 @@ function renderEditMode(element, ctx, wrapper, pathKey, initialValue) {
|
|
|
8897
9578
|
if (element.minLength != null || element.maxLength != null) {
|
|
8898
9579
|
const counterRow = document.createElement("div");
|
|
8899
9580
|
counterRow.style.cssText = "position: relative; padding: 2px 10px 4px; text-align: right;";
|
|
8900
|
-
const counter = createCharCounter(element, textarea
|
|
9581
|
+
const counter = createCharCounter(element, textarea);
|
|
8901
9582
|
counter.style.cssText = `
|
|
8902
9583
|
position: static;
|
|
8903
9584
|
display: inline-block;
|
|
@@ -9422,6 +10103,117 @@ function validateMarkdown(_element, _key, _context) {
|
|
|
9422
10103
|
function updateMarkdown(_element, _fieldPath, _value, _context) {
|
|
9423
10104
|
}
|
|
9424
10105
|
|
|
10106
|
+
// src/components/registry.ts
|
|
10107
|
+
function validateHiddenElement(element, key, context) {
|
|
10108
|
+
const { scopeRoot } = context;
|
|
10109
|
+
const input = scopeRoot.querySelector(
|
|
10110
|
+
`input[type="hidden"][data-hidden-field="true"][name="${key}"]`
|
|
10111
|
+
);
|
|
10112
|
+
const raw = input?.value ?? "";
|
|
10113
|
+
if (raw === "") {
|
|
10114
|
+
const defaultVal = "default" in element ? element.default : null;
|
|
10115
|
+
return { value: defaultVal !== void 0 ? defaultVal : null, errors: [] };
|
|
10116
|
+
}
|
|
10117
|
+
return { value: deserializeHiddenValue(raw), errors: [] };
|
|
10118
|
+
}
|
|
10119
|
+
function updateHiddenField(_element, fieldPath, value, context) {
|
|
10120
|
+
const { scopeRoot } = context;
|
|
10121
|
+
const input = scopeRoot.querySelector(
|
|
10122
|
+
`input[type="hidden"][data-hidden-field="true"][name="${fieldPath}"]`
|
|
10123
|
+
);
|
|
10124
|
+
if (!input) return;
|
|
10125
|
+
input.value = serializeHiddenValue(value);
|
|
10126
|
+
}
|
|
10127
|
+
var componentRegistry = {
|
|
10128
|
+
text: {
|
|
10129
|
+
validate: validateTextElement,
|
|
10130
|
+
update: updateTextField
|
|
10131
|
+
},
|
|
10132
|
+
textarea: {
|
|
10133
|
+
validate: validateTextareaElement,
|
|
10134
|
+
update: updateTextareaField
|
|
10135
|
+
},
|
|
10136
|
+
number: {
|
|
10137
|
+
validate: validateNumberElement,
|
|
10138
|
+
update: updateNumberField
|
|
10139
|
+
},
|
|
10140
|
+
select: {
|
|
10141
|
+
validate: validateSelectElement,
|
|
10142
|
+
update: updateSelectField
|
|
10143
|
+
},
|
|
10144
|
+
switcher: {
|
|
10145
|
+
validate: validateSwitcherElement,
|
|
10146
|
+
update: updateSwitcherField
|
|
10147
|
+
},
|
|
10148
|
+
boolean: {
|
|
10149
|
+
validate: validateBooleanElement,
|
|
10150
|
+
update: updateBooleanField,
|
|
10151
|
+
ownsLabel: true
|
|
10152
|
+
},
|
|
10153
|
+
file: {
|
|
10154
|
+
validate: validateFileElement,
|
|
10155
|
+
update: updateFileField
|
|
10156
|
+
},
|
|
10157
|
+
files: {
|
|
10158
|
+
// Legacy type - delegates to file
|
|
10159
|
+
validate: validateFileElement,
|
|
10160
|
+
update: updateFileField
|
|
10161
|
+
},
|
|
10162
|
+
colour: {
|
|
10163
|
+
validate: validateColourElement,
|
|
10164
|
+
update: updateColourField
|
|
10165
|
+
},
|
|
10166
|
+
slider: {
|
|
10167
|
+
validate: validateSliderElement,
|
|
10168
|
+
update: updateSliderField
|
|
10169
|
+
},
|
|
10170
|
+
container: {
|
|
10171
|
+
validate: validateContainerElement,
|
|
10172
|
+
update: updateContainerField
|
|
10173
|
+
},
|
|
10174
|
+
group: {
|
|
10175
|
+
// Deprecated type - delegates to container
|
|
10176
|
+
validate: validateGroupElement,
|
|
10177
|
+
update: updateGroupField
|
|
10178
|
+
},
|
|
10179
|
+
table: {
|
|
10180
|
+
validate: validateTableElement,
|
|
10181
|
+
update: updateTableField
|
|
10182
|
+
},
|
|
10183
|
+
richinput: {
|
|
10184
|
+
validate: validateRichInputElement,
|
|
10185
|
+
update: updateRichInputField
|
|
10186
|
+
},
|
|
10187
|
+
hidden: {
|
|
10188
|
+
// Legacy type: `type: "hidden"` — reads/writes DOM <input type="hidden"> element
|
|
10189
|
+
validate: validateHiddenElement,
|
|
10190
|
+
update: updateHiddenField
|
|
10191
|
+
},
|
|
10192
|
+
markdown: {
|
|
10193
|
+
// Display-only element — no value, no errors, skip from form data
|
|
10194
|
+
validate: validateMarkdown,
|
|
10195
|
+
update: updateMarkdown
|
|
10196
|
+
}
|
|
10197
|
+
};
|
|
10198
|
+
function getComponentOperations(elementType) {
|
|
10199
|
+
return componentRegistry[elementType] || null;
|
|
10200
|
+
}
|
|
10201
|
+
function validateElementWithComponent(element, key, context) {
|
|
10202
|
+
const ops = getComponentOperations(element.type);
|
|
10203
|
+
if (ops && ops.validate) {
|
|
10204
|
+
return ops.validate(element, key, context);
|
|
10205
|
+
}
|
|
10206
|
+
return null;
|
|
10207
|
+
}
|
|
10208
|
+
function updateElementWithComponent(element, fieldPath, value, context) {
|
|
10209
|
+
const ops = getComponentOperations(element.type);
|
|
10210
|
+
if (ops && ops.update) {
|
|
10211
|
+
ops.update(element, fieldPath, value, context);
|
|
10212
|
+
return true;
|
|
10213
|
+
}
|
|
10214
|
+
return false;
|
|
10215
|
+
}
|
|
10216
|
+
|
|
9425
10217
|
// src/components/index.ts
|
|
9426
10218
|
function showTooltip(tooltipId, button) {
|
|
9427
10219
|
const tooltip = document.getElementById(tooltipId);
|
|
@@ -9730,6 +10522,9 @@ function dispatchToRenderer(element, ctx, wrapper, pathKey) {
|
|
|
9730
10522
|
renderSwitcherElement(element, ctx, wrapper, pathKey);
|
|
9731
10523
|
}
|
|
9732
10524
|
break;
|
|
10525
|
+
case "boolean":
|
|
10526
|
+
renderBooleanElement(element, ctx, wrapper, pathKey);
|
|
10527
|
+
break;
|
|
9733
10528
|
case "file":
|
|
9734
10529
|
if (isMultiple) {
|
|
9735
10530
|
renderMultipleFileElement(element, ctx, wrapper, pathKey);
|
|
@@ -9808,8 +10603,11 @@ function renderElement2(element, ctx) {
|
|
|
9808
10603
|
const wrapper = document.createElement("div");
|
|
9809
10604
|
wrapper.className = "mb-2 fb-field-wrapper";
|
|
9810
10605
|
wrapper.setAttribute("data-field-key", element.key);
|
|
9811
|
-
const
|
|
9812
|
-
|
|
10606
|
+
const ops = getComponentOperations(element.type);
|
|
10607
|
+
if (!ops?.ownsLabel) {
|
|
10608
|
+
const label = createLabelContainer(element);
|
|
10609
|
+
wrapper.appendChild(label);
|
|
10610
|
+
}
|
|
9813
10611
|
const pathKey = pathJoin(ctx.path, element.key);
|
|
9814
10612
|
dispatchToRenderer(element, ctx, wrapper, pathKey);
|
|
9815
10613
|
if (initiallyDisabled) {
|
|
@@ -10042,28 +10840,52 @@ var defaultTheme = {
|
|
|
10042
10840
|
// blue-500
|
|
10043
10841
|
primaryHoverColor: "#2563eb",
|
|
10044
10842
|
// blue-600
|
|
10843
|
+
primarySoftColor: "#dbeafe",
|
|
10844
|
+
// blue-100
|
|
10845
|
+
primarySoftHoverColor: "#bfdbfe",
|
|
10846
|
+
// blue-200
|
|
10045
10847
|
errorColor: "#ef4444",
|
|
10046
10848
|
// red-500
|
|
10047
10849
|
errorHoverColor: "#dc2626",
|
|
10048
10850
|
// red-600
|
|
10049
10851
|
successColor: "#10b981",
|
|
10050
10852
|
// green-500
|
|
10853
|
+
accentColor: "#f59e0b",
|
|
10854
|
+
// amber-500
|
|
10855
|
+
accentSoftColor: "#fef3c7",
|
|
10856
|
+
// amber-100
|
|
10857
|
+
accentBorderColor: "#fde68a",
|
|
10858
|
+
// amber-200
|
|
10859
|
+
accentTextColor: "#92400e",
|
|
10860
|
+
// amber-800
|
|
10051
10861
|
borderColor: "#d1d5db",
|
|
10052
10862
|
// gray-300
|
|
10053
10863
|
borderHoverColor: "#9ca3af",
|
|
10054
10864
|
// gray-400
|
|
10055
10865
|
borderFocusColor: "#3b82f6",
|
|
10056
10866
|
// blue-500
|
|
10867
|
+
borderStrongColor: "#9ca3af",
|
|
10868
|
+
// gray-400
|
|
10057
10869
|
backgroundColor: "#ffffff",
|
|
10058
10870
|
// white
|
|
10059
10871
|
backgroundHoverColor: "#f9fafb",
|
|
10060
10872
|
// gray-50
|
|
10061
10873
|
backgroundReadonlyColor: "#f3f4f6",
|
|
10062
10874
|
// gray-100
|
|
10875
|
+
pageBackgroundColor: "#f9fafb",
|
|
10876
|
+
// gray-50
|
|
10877
|
+
surfaceSoftColor: "#eff6ff",
|
|
10878
|
+
// blue-50
|
|
10879
|
+
surfaceTintColor: "#f8fafc",
|
|
10880
|
+
// slate-50
|
|
10063
10881
|
textColor: "#1f2937",
|
|
10064
10882
|
// gray-800
|
|
10065
10883
|
textSecondaryColor: "#6b7280",
|
|
10066
10884
|
// gray-500
|
|
10885
|
+
textMutedColor: "#9ca3af",
|
|
10886
|
+
// gray-400
|
|
10887
|
+
textFaintColor: "#cbd5e1",
|
|
10888
|
+
// slate-300
|
|
10067
10889
|
textPlaceholderColor: "#9ca3af",
|
|
10068
10890
|
// gray-400
|
|
10069
10891
|
textDisabledColor: "#d1d5db",
|
|
@@ -10106,6 +10928,12 @@ var defaultTheme = {
|
|
|
10106
10928
|
// 4px (compact density v2)
|
|
10107
10929
|
borderRadius: "0.5rem",
|
|
10108
10930
|
// rounded-lg (8px)
|
|
10931
|
+
borderRadiusSmall: "0.375rem",
|
|
10932
|
+
// 6px
|
|
10933
|
+
borderRadiusLarge: "0.75rem",
|
|
10934
|
+
// 12px
|
|
10935
|
+
borderRadiusXLarge: "1rem",
|
|
10936
|
+
// 16px
|
|
10109
10937
|
borderWidth: "1px",
|
|
10110
10938
|
// Typography
|
|
10111
10939
|
fontSize: "0.875rem",
|
|
@@ -10117,13 +10945,31 @@ var defaultTheme = {
|
|
|
10117
10945
|
fontFamily: 'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
|
|
10118
10946
|
fontWeightNormal: "400",
|
|
10119
10947
|
fontWeightMedium: "500",
|
|
10948
|
+
lineHeight: "1.5",
|
|
10949
|
+
// Shadows
|
|
10950
|
+
shadowCard: "0 1px 2px rgba(15,23,42,.04), 0 1px 0 rgba(15,23,42,.02)",
|
|
10951
|
+
shadowPopover: "0 12px 32px -12px rgba(15,23,42,.18), 0 4px 12px -6px rgba(15,23,42,.08)",
|
|
10120
10952
|
// Focus ring
|
|
10121
10953
|
focusRingWidth: "2px",
|
|
10122
10954
|
focusRingColor: "#3b82f6",
|
|
10123
10955
|
// blue-500
|
|
10124
10956
|
focusRingOpacity: "0.5",
|
|
10125
10957
|
// Transitions
|
|
10126
|
-
transitionDuration: "200ms"
|
|
10958
|
+
transitionDuration: "200ms",
|
|
10959
|
+
// Slide-card defaults — flat-white to match every other item card. The
|
|
10960
|
+
// Picaz theme overrides this with a gradient + shadow to lift the slides.
|
|
10961
|
+
slideCardBg: "#ffffff",
|
|
10962
|
+
slideCardShadow: "0 1px 2px rgba(15,23,42,.04), 0 1px 0 rgba(15,23,42,.02)",
|
|
10963
|
+
slideCardRadius: "0.5rem",
|
|
10964
|
+
// matches borderRadius
|
|
10965
|
+
slideCardMinHeight: "0",
|
|
10966
|
+
slideCardPadding: "12px",
|
|
10967
|
+
// Section-label defaults — same look as the regular field label. Themes
|
|
10968
|
+
// that want the "ПРЕИМУЩЕСТВА" caps style override these three vars.
|
|
10969
|
+
labelSectionFontSize: "0.875rem",
|
|
10970
|
+
// matches fontSize
|
|
10971
|
+
labelSectionLetterSpacing: "normal",
|
|
10972
|
+
labelSectionTextTransform: "none"
|
|
10127
10973
|
};
|
|
10128
10974
|
function generateCSSVariables(theme) {
|
|
10129
10975
|
const mergedTheme = { ...defaultTheme, ...theme };
|
|
@@ -10135,6 +10981,7 @@ function generateCSSVariables(theme) {
|
|
|
10135
10981
|
return cssVars.join("\n");
|
|
10136
10982
|
}
|
|
10137
10983
|
function injectThemeVariables(container, theme) {
|
|
10984
|
+
ensureThemingHooks(container.ownerDocument || document);
|
|
10138
10985
|
const cssVariables = generateCSSVariables(theme);
|
|
10139
10986
|
let styleTag = container.querySelector(
|
|
10140
10987
|
"style[data-fb-theme]"
|
|
@@ -10197,114 +11044,64 @@ var exampleThemes = {
|
|
|
10197
11044
|
fontSize: "16px",
|
|
10198
11045
|
fontSizeSmall: "14px",
|
|
10199
11046
|
fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif'
|
|
10200
|
-
}
|
|
10201
|
-
};
|
|
10202
|
-
|
|
10203
|
-
// src/components/registry.ts
|
|
10204
|
-
function validateHiddenElement(element, key, context) {
|
|
10205
|
-
const { scopeRoot } = context;
|
|
10206
|
-
const input = scopeRoot.querySelector(
|
|
10207
|
-
`input[type="hidden"][data-hidden-field="true"][name="${key}"]`
|
|
10208
|
-
);
|
|
10209
|
-
const raw = input?.value ?? "";
|
|
10210
|
-
if (raw === "") {
|
|
10211
|
-
const defaultVal = "default" in element ? element.default : null;
|
|
10212
|
-
return { value: defaultVal !== void 0 ? defaultVal : null, errors: [] };
|
|
10213
|
-
}
|
|
10214
|
-
return { value: deserializeHiddenValue(raw), errors: [] };
|
|
10215
|
-
}
|
|
10216
|
-
function updateHiddenField(_element, fieldPath, value, context) {
|
|
10217
|
-
const { scopeRoot } = context;
|
|
10218
|
-
const input = scopeRoot.querySelector(
|
|
10219
|
-
`input[type="hidden"][data-hidden-field="true"][name="${fieldPath}"]`
|
|
10220
|
-
);
|
|
10221
|
-
if (!input) return;
|
|
10222
|
-
input.value = serializeHiddenValue(value);
|
|
10223
|
-
}
|
|
10224
|
-
var componentRegistry = {
|
|
10225
|
-
text: {
|
|
10226
|
-
validate: validateTextElement,
|
|
10227
|
-
update: updateTextField
|
|
10228
|
-
},
|
|
10229
|
-
textarea: {
|
|
10230
|
-
validate: validateTextareaElement,
|
|
10231
|
-
update: updateTextareaField
|
|
10232
|
-
},
|
|
10233
|
-
number: {
|
|
10234
|
-
validate: validateNumberElement,
|
|
10235
|
-
update: updateNumberField
|
|
10236
|
-
},
|
|
10237
|
-
select: {
|
|
10238
|
-
validate: validateSelectElement,
|
|
10239
|
-
update: updateSelectField
|
|
10240
|
-
},
|
|
10241
|
-
switcher: {
|
|
10242
|
-
validate: validateSwitcherElement,
|
|
10243
|
-
update: updateSwitcherField
|
|
10244
|
-
},
|
|
10245
|
-
file: {
|
|
10246
|
-
validate: validateFileElement,
|
|
10247
|
-
update: updateFileField
|
|
10248
|
-
},
|
|
10249
|
-
files: {
|
|
10250
|
-
// Legacy type - delegates to file
|
|
10251
|
-
validate: validateFileElement,
|
|
10252
|
-
update: updateFileField
|
|
10253
|
-
},
|
|
10254
|
-
colour: {
|
|
10255
|
-
validate: validateColourElement,
|
|
10256
|
-
update: updateColourField
|
|
10257
|
-
},
|
|
10258
|
-
slider: {
|
|
10259
|
-
validate: validateSliderElement,
|
|
10260
|
-
update: updateSliderField
|
|
10261
|
-
},
|
|
10262
|
-
container: {
|
|
10263
|
-
validate: validateContainerElement,
|
|
10264
|
-
update: updateContainerField
|
|
10265
|
-
},
|
|
10266
|
-
group: {
|
|
10267
|
-
// Deprecated type - delegates to container
|
|
10268
|
-
validate: validateGroupElement,
|
|
10269
|
-
update: updateGroupField
|
|
10270
|
-
},
|
|
10271
|
-
table: {
|
|
10272
|
-
validate: validateTableElement,
|
|
10273
|
-
update: updateTableField
|
|
10274
|
-
},
|
|
10275
|
-
richinput: {
|
|
10276
|
-
validate: validateRichInputElement,
|
|
10277
|
-
update: updateRichInputField
|
|
10278
|
-
},
|
|
10279
|
-
hidden: {
|
|
10280
|
-
// Legacy type: `type: "hidden"` — reads/writes DOM <input type="hidden"> element
|
|
10281
|
-
validate: validateHiddenElement,
|
|
10282
|
-
update: updateHiddenField
|
|
10283
11047
|
},
|
|
10284
|
-
|
|
10285
|
-
|
|
10286
|
-
|
|
10287
|
-
|
|
11048
|
+
// Picaz wizard design tokens — derived from the Picaz Wizard mockups.
|
|
11049
|
+
// Pairs with the host-side .card / .section-num / .lede chrome that wraps the form.
|
|
11050
|
+
// Assumes Inter is loaded by the host (e.g. via Google Fonts in index.html).
|
|
11051
|
+
picaz: {
|
|
11052
|
+
...defaultTheme,
|
|
11053
|
+
primaryColor: "#2f5bea",
|
|
11054
|
+
primaryHoverColor: "#2349c8",
|
|
11055
|
+
primarySoftColor: "#eaf0ff",
|
|
11056
|
+
primarySoftHoverColor: "#d6e0ff",
|
|
11057
|
+
errorColor: "#ef4444",
|
|
11058
|
+
successColor: "#16a34a",
|
|
11059
|
+
accentColor: "#ffb020",
|
|
11060
|
+
accentSoftColor: "#fff7e6",
|
|
11061
|
+
accentBorderColor: "#fde7b5",
|
|
11062
|
+
accentTextColor: "#92400e",
|
|
11063
|
+
borderColor: "#e3e8f0",
|
|
11064
|
+
borderHoverColor: "#cdd6e3",
|
|
11065
|
+
borderFocusColor: "#2f5bea",
|
|
11066
|
+
borderStrongColor: "#cdd6e3",
|
|
11067
|
+
backgroundColor: "#ffffff",
|
|
11068
|
+
backgroundHoverColor: "#f3f7ff",
|
|
11069
|
+
pageBackgroundColor: "#f6f8fb",
|
|
11070
|
+
surfaceSoftColor: "#eef4ff",
|
|
11071
|
+
surfaceTintColor: "#f3f7ff",
|
|
11072
|
+
textColor: "#0f172a",
|
|
11073
|
+
textSecondaryColor: "#334155",
|
|
11074
|
+
textMutedColor: "#64748b",
|
|
11075
|
+
textFaintColor: "#94a3b8",
|
|
11076
|
+
textPlaceholderColor: "#94a3b8",
|
|
11077
|
+
buttonBgColor: "#2f5bea",
|
|
11078
|
+
buttonHoverBgColor: "#2349c8",
|
|
11079
|
+
fileUploadBgColor: "#fafcff",
|
|
11080
|
+
fileUploadBorderColor: "#cdd6e3",
|
|
11081
|
+
fileUploadHoverBorderColor: "#2f5bea",
|
|
11082
|
+
// Picaz uses roomier inputs (11/14px) than the defaultTheme compact density.
|
|
11083
|
+
inputPaddingX: "14px",
|
|
11084
|
+
inputPaddingY: "11px",
|
|
11085
|
+
borderRadius: "12px",
|
|
11086
|
+
borderRadiusSmall: "8px",
|
|
11087
|
+
borderRadiusLarge: "16px",
|
|
11088
|
+
borderRadiusXLarge: "22px",
|
|
11089
|
+
fontFamily: '"Inter", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif',
|
|
11090
|
+
shadowCard: "0 1px 2px rgba(15,23,42,.04), 0 1px 0 rgba(15,23,42,.02)",
|
|
11091
|
+
shadowPopover: "0 12px 32px -12px rgba(15,23,42,.18), 0 4px 12px -6px rgba(15,23,42,.08)",
|
|
11092
|
+
focusRingColor: "#2f5bea",
|
|
11093
|
+
// Slide cards: subtle gradient lift, no rest-state shadow (mockup adds it
|
|
11094
|
+
// only on hover, which form-builder doesn't yet differentiate).
|
|
11095
|
+
slideCardBg: "linear-gradient(180deg, #f7f9fc 0%, #dde3ee 100%)",
|
|
11096
|
+
slideCardShadow: "none",
|
|
11097
|
+
slideCardRadius: "16px",
|
|
11098
|
+
// Tiny uppercase captions above grouped lists ("ПРЕИМУЩЕСТВА" in the mockup).
|
|
11099
|
+
labelSectionFontSize: "0.625rem",
|
|
11100
|
+
// 10px
|
|
11101
|
+
labelSectionLetterSpacing: "0.07em",
|
|
11102
|
+
labelSectionTextTransform: "uppercase"
|
|
10288
11103
|
}
|
|
10289
11104
|
};
|
|
10290
|
-
function getComponentOperations(elementType) {
|
|
10291
|
-
return componentRegistry[elementType] || null;
|
|
10292
|
-
}
|
|
10293
|
-
function validateElementWithComponent(element, key, context) {
|
|
10294
|
-
const ops = getComponentOperations(element.type);
|
|
10295
|
-
if (ops && ops.validate) {
|
|
10296
|
-
return ops.validate(element, key, context);
|
|
10297
|
-
}
|
|
10298
|
-
return null;
|
|
10299
|
-
}
|
|
10300
|
-
function updateElementWithComponent(element, fieldPath, value, context) {
|
|
10301
|
-
const ops = getComponentOperations(element.type);
|
|
10302
|
-
if (ops && ops.update) {
|
|
10303
|
-
ops.update(element, fieldPath, value, context);
|
|
10304
|
-
return true;
|
|
10305
|
-
}
|
|
10306
|
-
return false;
|
|
10307
|
-
}
|
|
10308
11105
|
|
|
10309
11106
|
// src/instance/FormBuilderInstance.ts
|
|
10310
11107
|
var FormBuilderInstance = class {
|
|
@@ -10429,26 +11226,37 @@ var FormBuilderInstance = class {
|
|
|
10429
11226
|
}
|
|
10430
11227
|
}
|
|
10431
11228
|
/**
|
|
10432
|
-
* Find the DOM element corresponding to a field path (instance-scoped)
|
|
11229
|
+
* Find the DOM element corresponding to a field path (instance-scoped).
|
|
11230
|
+
*
|
|
11231
|
+
* Strategy:
|
|
11232
|
+
* 1. Try a `[name="…"]` lookup first — works for any field that renders
|
|
11233
|
+
* an input/hidden with the path as its name, in either mode. Some
|
|
11234
|
+
* readonly renderers still emit a hidden input (boolean, switcher),
|
|
11235
|
+
* so this path must run regardless of `state.config.readonly`. A
|
|
11236
|
+
* prior version gated this on edit mode only, which made
|
|
11237
|
+
* `updateField` / `setFormData` silently miss readonly boolean
|
|
11238
|
+
* fields whose component also opts out of the standard label row
|
|
11239
|
+
* (`ownsLabel: true`).
|
|
11240
|
+
* 2. If no input matched, fall back to locating the field wrapper by
|
|
11241
|
+
* its visible label text — needed for readonly previews that don't
|
|
11242
|
+
* emit any `name=` attribute (e.g. file/markdown previews).
|
|
10433
11243
|
*/
|
|
10434
11244
|
findFormElementByFieldPath(fieldPath) {
|
|
10435
11245
|
if (!this.state.formRoot) return null;
|
|
10436
|
-
|
|
10437
|
-
|
|
10438
|
-
|
|
11246
|
+
let element = this.state.formRoot.querySelector(
|
|
11247
|
+
`[name="${fieldPath}"]`
|
|
11248
|
+
);
|
|
11249
|
+
if (element) return element;
|
|
11250
|
+
const variations = [
|
|
11251
|
+
fieldPath,
|
|
11252
|
+
fieldPath.replace(/\[(\d+)\]/g, "[$1]"),
|
|
11253
|
+
fieldPath.replace(/\./g, "[") + "]".repeat((fieldPath.match(/\./g) || []).length)
|
|
11254
|
+
];
|
|
11255
|
+
for (const variation of variations) {
|
|
11256
|
+
element = this.state.formRoot.querySelector(
|
|
11257
|
+
`[name="${variation}"]`
|
|
10439
11258
|
);
|
|
10440
11259
|
if (element) return element;
|
|
10441
|
-
const variations = [
|
|
10442
|
-
fieldPath,
|
|
10443
|
-
fieldPath.replace(/\[(\d+)\]/g, "[$1]"),
|
|
10444
|
-
fieldPath.replace(/\./g, "[") + "]".repeat((fieldPath.match(/\./g) || []).length)
|
|
10445
|
-
];
|
|
10446
|
-
for (const variation of variations) {
|
|
10447
|
-
element = this.state.formRoot.querySelector(
|
|
10448
|
-
`[name="${variation}"]`
|
|
10449
|
-
);
|
|
10450
|
-
if (element) return element;
|
|
10451
|
-
}
|
|
10452
11260
|
}
|
|
10453
11261
|
const schemaElement = this.findSchemaElement(fieldPath);
|
|
10454
11262
|
if (!schemaElement) return null;
|
|
@@ -10657,6 +11465,13 @@ var FormBuilderInstance = class {
|
|
|
10657
11465
|
const value = hintValues[fieldKey];
|
|
10658
11466
|
this.updateField(fullPath, value);
|
|
10659
11467
|
}
|
|
11468
|
+
const group = target.closest(".fb-prefill-hints");
|
|
11469
|
+
if (group) {
|
|
11470
|
+
group.querySelectorAll(
|
|
11471
|
+
'.fb-prefill-hint[aria-pressed="true"]'
|
|
11472
|
+
).forEach((el) => el.removeAttribute("aria-pressed"));
|
|
11473
|
+
}
|
|
11474
|
+
target.setAttribute("aria-pressed", "true");
|
|
10660
11475
|
} catch (error) {
|
|
10661
11476
|
console.error("Error parsing prefill hint values:", error);
|
|
10662
11477
|
}
|