form-builder-pro 1.4.1 → 1.4.3

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/index.js CHANGED
@@ -4749,8 +4749,31 @@ function transformField(field) {
4749
4749
  transformed.repeatIncrementEnabled = field.repeatIncrementEnabled;
4750
4750
  if (field.dateConstraints !== void 0)
4751
4751
  transformed.dateConstraints = field.dateConstraints;
4752
- if (field.formulaConfig !== void 0)
4752
+ if (field.formulaConfig !== void 0) {
4753
4753
  transformed.formulaConfig = field.formulaConfig;
4754
+ } else if (field.type === "formula" && field.formulaType) {
4755
+ const dp = field.floatingPrecision ?? 2;
4756
+ if (field.formulaType === "SIMPLE") {
4757
+ transformed.formulaConfig = {
4758
+ mode: "single",
4759
+ single: { expression: field.formulaExpression ?? "" },
4760
+ multiple: { compareField: "", conditions: [], fallbackExpression: "" },
4761
+ decimalPlaces: dp
4762
+ };
4763
+ } else if (field.formulaType === "SWITCH") {
4764
+ const cases = Array.isArray(field.switchCases) ? field.switchCases.map((c) => ({ value: c.matchValue ?? c.value ?? "", expression: c.formulaExpression ?? c.expression ?? "" })) : [];
4765
+ transformed.formulaConfig = {
4766
+ mode: "multiple",
4767
+ single: { expression: "" },
4768
+ multiple: {
4769
+ compareField: field.switchField ?? "",
4770
+ conditions: cases,
4771
+ fallbackExpression: field.switchDefaultFormula ?? ""
4772
+ },
4773
+ decimalPlaces: dp
4774
+ };
4775
+ }
4776
+ }
4754
4777
  if (field.nameGeneratorFormat !== void 0)
4755
4778
  transformed.nameGeneratorFormat = field.nameGeneratorFormat;
4756
4779
  if (field.nameGeneratorText !== void 0)
@@ -4761,6 +4784,13 @@ function transformField(field) {
4761
4784
  transformed.nameGeneratorSuffix = field.nameGeneratorSuffix;
4762
4785
  if (field.nameGeneratorIdPadding !== void 0)
4763
4786
  transformed.nameGeneratorIdPadding = field.nameGeneratorIdPadding;
4787
+ if (field.autoPopulateFields !== void 0 && field.autoPopulateFields !== null) {
4788
+ const apf = field.autoPopulateFields;
4789
+ transformed.autoPopulateFields = {
4790
+ enabled: typeof apf.enabled === "boolean" ? apf.enabled : false,
4791
+ fields: Array.isArray(apf.fields) ? apf.fields : []
4792
+ };
4793
+ }
4764
4794
  if (field.css !== void 0)
4765
4795
  transformed.css = field.css;
4766
4796
  if (field.optionsSource !== void 0)
@@ -5019,6 +5049,12 @@ function fieldToPayload(field, opts) {
5019
5049
  parentFieldName: field.lookupParentFieldName ?? null
5020
5050
  };
5021
5051
  }
5052
+ if (field.optionSource === "LOOKUP" && field.autoPopulateFields !== void 0 && field.autoPopulateFields !== null) {
5053
+ payload.autoPopulateFields = {
5054
+ enabled: field.autoPopulateFields.enabled,
5055
+ fields: Array.isArray(field.autoPopulateFields.fields) ? field.autoPopulateFields.fields : []
5056
+ };
5057
+ }
5022
5058
  if (field.isd !== void 0)
5023
5059
  payload.isd = field.isd;
5024
5060
  if (field.imageUrl !== void 0)
@@ -5068,8 +5104,32 @@ function fieldToPayload(field, opts) {
5068
5104
  if (field.type === "formula") {
5069
5105
  payload.fieldType = "FORMULA";
5070
5106
  payload.type = "formula";
5071
- if (field.formulaConfig !== void 0)
5072
- payload.formulaConfig = field.formulaConfig;
5107
+ payload.readOnly = true;
5108
+ payload.floatingPrecision = field.formulaConfig?.decimalPlaces ?? 2;
5109
+ if (field.formulaConfig?.mode === "single") {
5110
+ payload.formulaType = "SIMPLE";
5111
+ payload.formulaExpression = field.formulaConfig.single?.expression ?? "";
5112
+ payload.switchField = null;
5113
+ payload.switchCases = null;
5114
+ payload.switchDefaultFormula = null;
5115
+ } else if (field.formulaConfig?.mode === "multiple") {
5116
+ const multi = field.formulaConfig.multiple;
5117
+ payload.formulaType = "SWITCH";
5118
+ payload.formulaExpression = "";
5119
+ payload.switchField = multi?.compareField ?? null;
5120
+ payload.switchCases = (multi?.conditions ?? []).map((c) => ({
5121
+ matchValue: c.value,
5122
+ formulaExpression: c.expression ?? ""
5123
+ }));
5124
+ payload.switchDefaultFormula = multi?.fallbackExpression ?? "";
5125
+ payload.switchCases = payload.switchCases;
5126
+ } else {
5127
+ payload.formulaType = "SIMPLE";
5128
+ payload.formulaExpression = "";
5129
+ payload.switchField = null;
5130
+ payload.switchCases = null;
5131
+ payload.switchDefaultFormula = null;
5132
+ }
5073
5133
  }
5074
5134
  if (field.type === "repeater") {
5075
5135
  payload.fieldType = "REPEATER";
@@ -10507,9 +10567,647 @@ var SectionList = class {
10507
10567
  }
10508
10568
  };
10509
10569
 
10570
+ // src/utils/formulaTokenParser.ts
10571
+ var FUNCTION_NAMES = /* @__PURE__ */ new Set(["ROUND", "ABS", "MIN", "MAX", "FLOOR", "CEIL", "SQRT", "POW"]);
10572
+ var OPERATOR_CHARS = /* @__PURE__ */ new Set(["+", "-", "*", "/"]);
10573
+ function parseExpressionToTokens(expression, fieldMap, mode) {
10574
+ if (!expression)
10575
+ return [];
10576
+ const tokens = [];
10577
+ let i = 0;
10578
+ const len = expression.length;
10579
+ while (i < len) {
10580
+ const ch = expression[i];
10581
+ if (/\s/.test(ch)) {
10582
+ let ws = "";
10583
+ while (i < len && /\s/.test(expression[i]))
10584
+ ws += expression[i++];
10585
+ tokens.push({ type: "space", value: ws, rawValue: ws });
10586
+ continue;
10587
+ }
10588
+ if (mode === "bracket" && ch === "{") {
10589
+ i++;
10590
+ let ref = "";
10591
+ while (i < len && expression[i] !== "}")
10592
+ ref += expression[i++];
10593
+ if (i < len && expression[i] === "}")
10594
+ i++;
10595
+ const trimmedRef = ref.trim();
10596
+ const field = fieldMap.get(trimmedRef);
10597
+ tokens.push({
10598
+ type: "field",
10599
+ value: field ? field.label || field.fieldName || trimmedRef : trimmedRef,
10600
+ rawValue: `{${trimmedRef}}`,
10601
+ fieldRef: trimmedRef
10602
+ });
10603
+ continue;
10604
+ }
10605
+ if (OPERATOR_CHARS.has(ch)) {
10606
+ tokens.push({ type: "operator", value: ch, rawValue: ch });
10607
+ i++;
10608
+ continue;
10609
+ }
10610
+ if (ch === "%") {
10611
+ tokens.push({ type: "percent", value: "%", rawValue: "%" });
10612
+ i++;
10613
+ continue;
10614
+ }
10615
+ if (ch === "(" || ch === ")") {
10616
+ tokens.push({ type: "paren", value: ch, rawValue: ch });
10617
+ i++;
10618
+ continue;
10619
+ }
10620
+ if (ch === ",") {
10621
+ tokens.push({ type: "comma", value: ",", rawValue: "," });
10622
+ i++;
10623
+ continue;
10624
+ }
10625
+ if (/[0-9]/.test(ch) || ch === "." && i + 1 < len && /[0-9]/.test(expression[i + 1])) {
10626
+ let num = "";
10627
+ while (i < len && /[0-9.]/.test(expression[i]))
10628
+ num += expression[i++];
10629
+ tokens.push({ type: "number", value: num, rawValue: num });
10630
+ continue;
10631
+ }
10632
+ if (/[a-zA-Z_]/.test(ch)) {
10633
+ let ident = "";
10634
+ while (i < len && /[a-zA-Z0-9_]/.test(expression[i]))
10635
+ ident += expression[i++];
10636
+ if (FUNCTION_NAMES.has(ident.toUpperCase())) {
10637
+ tokens.push({ type: "function", value: ident, rawValue: ident });
10638
+ continue;
10639
+ }
10640
+ if (mode === "plain") {
10641
+ const field = fieldMap.get(ident);
10642
+ tokens.push({
10643
+ type: "field",
10644
+ value: field ? field.label || field.fieldName || ident : ident,
10645
+ rawValue: ident,
10646
+ fieldRef: ident
10647
+ });
10648
+ continue;
10649
+ }
10650
+ tokens.push({ type: "unknown", value: ident, rawValue: ident });
10651
+ continue;
10652
+ }
10653
+ tokens.push({ type: "unknown", value: ch, rawValue: ch });
10654
+ i++;
10655
+ }
10656
+ return tokens;
10657
+ }
10658
+ function buildFieldMap(fields) {
10659
+ const map = /* @__PURE__ */ new Map();
10660
+ for (const f of fields) {
10661
+ if (f.fieldName)
10662
+ map.set(f.fieldName, f);
10663
+ if (f.id && f.id !== f.fieldName)
10664
+ map.set(f.id, f);
10665
+ }
10666
+ return map;
10667
+ }
10668
+ function getFieldRef(field) {
10669
+ return field.fieldName && field.fieldName !== field.id ? field.fieldName : field.id;
10670
+ }
10671
+
10672
+ // src/builder/FormulaEditorWidget.ts
10673
+ var _stylesInjected = false;
10674
+ var STYLE_ID = "formula-editor-widget-styles-v2";
10675
+ function injectStyles() {
10676
+ if (_stylesInjected && document.getElementById(STYLE_ID))
10677
+ return;
10678
+ document.getElementById(STYLE_ID)?.remove();
10679
+ document.getElementById("formula-editor-widget-styles")?.remove();
10680
+ _stylesInjected = true;
10681
+ const style = document.createElement("style");
10682
+ style.id = STYLE_ID;
10683
+ style.textContent = `
10684
+ /* \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
10685
+ FormulaEditorWidget v2
10686
+ \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 */
10687
+
10688
+ /* \u2500\u2500 Container \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
10689
+ .few-container {
10690
+ position: relative;
10691
+ width: 100%;
10692
+ }
10693
+
10694
+ /* \u2500\u2500 Editor \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
10695
+ /*
10696
+ * NOTE: color uses !important so it is never muted by
10697
+ * Angular ViewEncapsulation, host-element rules, or global resets.
10698
+ * This ensures operators/numbers always render at full text contrast.
10699
+ */
10700
+ .few-editor {
10701
+ display: block;
10702
+ width: 100%;
10703
+ min-height: 38px;
10704
+ padding: 6px 32px 6px 12px; /* right padding reserves space for clear btn */
10705
+ border: 1px solid #e2e8f0;
10706
+ border-radius: 6px;
10707
+ font-family: ui-monospace, 'Cascadia Code', 'Source Code Pro', Menlo, monospace;
10708
+ font-size: 13px;
10709
+ font-weight: 700 !important; /* \u2190 operators/numbers always bold */
10710
+ line-height: 1.7;
10711
+ color: rgb(213, 12, 65) !important; /* operator/text color \u2014 chips override with their own !important */
10712
+ caret-color: #635bff;
10713
+ background: transparent;
10714
+ outline: none;
10715
+ word-break: break-word;
10716
+ white-space: pre-wrap;
10717
+ cursor: text;
10718
+ transition: border-color 0.15s, box-shadow 0.15s;
10719
+ box-sizing: border-box;
10720
+ }
10721
+
10722
+ .few-editor:focus {
10723
+ border-color: #635bff;
10724
+ box-shadow: 0 0 0 3px rgba(99, 91, 255, 0.15);
10725
+ }
10726
+
10727
+ .few-editor.few-has-error {
10728
+ border-color: #ef4444;
10729
+ box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.12);
10730
+ }
10731
+
10732
+ /* \u2500\u2500 Placeholder \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
10733
+ .few-placeholder {
10734
+ position: absolute;
10735
+ top: 0;
10736
+ left: 0;
10737
+ right: 32px; /* don't overlap clear button */
10738
+ bottom: 0;
10739
+ padding: 6px 0 6px 12px;
10740
+ font-family: ui-monospace, 'Cascadia Code', 'Source Code Pro', Menlo, monospace;
10741
+ font-size: 13px;
10742
+ line-height: 1.7;
10743
+ color: #94a3b8;
10744
+ pointer-events: none;
10745
+ user-select: none;
10746
+ white-space: nowrap;
10747
+ overflow: hidden;
10748
+ text-overflow: ellipsis;
10749
+ }
10750
+
10751
+ /* \u2500\u2500 Field chip \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
10752
+ .few-chip {
10753
+ display: inline-flex;
10754
+ align-items: center;
10755
+ padding: 0px 7px 1px 6px;
10756
+ margin: 0 2px;
10757
+ font-size: 11.5px;
10758
+ font-weight: 600 !important; /* explicit !important so the editor's 700 !important doesn't cascade in */
10759
+ font-family: ui-sans-serif, system-ui, sans-serif;
10760
+ letter-spacing: 0.01em;
10761
+ border-radius: 4px;
10762
+ background: rgba(99, 91, 255, 0.10);
10763
+ color: #635bff !important; /* chip label always purple \u2014 not inherited */
10764
+ border: 1px solid rgba(99, 91, 255, 0.28);
10765
+ cursor: default;
10766
+ user-select: none;
10767
+ vertical-align: middle;
10768
+ line-height: 1.6;
10769
+ white-space: nowrap;
10770
+ transition: background 0.1s;
10771
+ }
10772
+
10773
+ .few-chip:hover {
10774
+ background: rgba(99, 91, 255, 0.17);
10775
+ }
10776
+
10777
+ .few-chip-unknown {
10778
+ background: rgba(239, 68, 68, 0.08);
10779
+ color: #dc2626 !important;
10780
+ border-color: rgba(239, 68, 68, 0.25);
10781
+ }
10782
+
10783
+ /* \u2500\u2500 Clear (\xD7) button \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
10784
+ .few-clear-btn {
10785
+ position: absolute;
10786
+ top: 50%;
10787
+ right: 7px;
10788
+ transform: translateY(-50%);
10789
+ display: flex;
10790
+ align-items: center;
10791
+ justify-content: center;
10792
+ width: 18px;
10793
+ height: 18px;
10794
+ padding: 0;
10795
+ border: none;
10796
+ border-radius: 50%;
10797
+ background: #94a3b8;
10798
+ color: #ffffff;
10799
+ font-size: 12px;
10800
+ line-height: 1;
10801
+ font-weight: 700;
10802
+ cursor: pointer;
10803
+ opacity: 0;
10804
+ pointer-events: none;
10805
+ transition: opacity 0.15s, background 0.15s;
10806
+ z-index: 2;
10807
+ }
10808
+
10809
+ .few-clear-btn:hover {
10810
+ background: #475569;
10811
+ }
10812
+
10813
+ /* Shown only when the editor has content */
10814
+ .few-container.few-has-content .few-clear-btn {
10815
+ opacity: 1;
10816
+ pointer-events: auto;
10817
+ }
10818
+
10819
+ /* \u2500\u2500 Dark mode (media query) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
10820
+ @media (prefers-color-scheme: dark) {
10821
+ .few-editor {
10822
+ border-color: #334155;
10823
+ /* operator color is set via inline !important \u2014 not overridden here */
10824
+ }
10825
+ .few-editor:focus {
10826
+ border-color: #635bff;
10827
+ box-shadow: 0 0 0 3px rgba(99, 91, 255, 0.25);
10828
+ }
10829
+ .few-placeholder { color: #64748b; }
10830
+ .few-chip {
10831
+ background: rgba(99, 91, 255, 0.18);
10832
+ color: #a5b4fc !important;
10833
+ border-color: rgba(99, 91, 255, 0.38);
10834
+ }
10835
+ .few-chip-unknown {
10836
+ background: rgba(239, 68, 68, 0.15);
10837
+ color: #f87171 !important;
10838
+ border-color: rgba(239, 68, 68, 0.35);
10839
+ }
10840
+ .few-clear-btn {
10841
+ background: #64748b;
10842
+ color: #f1f5f9;
10843
+ }
10844
+ .few-clear-btn:hover { background: #475569; }
10845
+ }
10846
+
10847
+ /* \u2500\u2500 Dark mode (Tailwind class strategy) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
10848
+ .dark .few-editor {
10849
+ border-color: #334155;
10850
+ /* operator color is set via inline !important \u2014 not overridden here */
10851
+ }
10852
+ .dark .few-editor:focus {
10853
+ border-color: #635bff;
10854
+ box-shadow: 0 0 0 3px rgba(99, 91, 255, 0.25);
10855
+ }
10856
+ .dark .few-placeholder { color: #64748b; }
10857
+ .dark .few-chip {
10858
+ background: rgba(99, 91, 255, 0.18);
10859
+ color: #a5b4fc !important;
10860
+ border-color: rgba(99, 91, 255, 0.38);
10861
+ }
10862
+ .dark .few-chip-unknown {
10863
+ background: rgba(239, 68, 68, 0.15);
10864
+ color: #f87171 !important;
10865
+ border-color: rgba(239, 68, 68, 0.35);
10866
+ }
10867
+ .dark .few-clear-btn {
10868
+ background: #64748b;
10869
+ color: #f1f5f9;
10870
+ }
10871
+ .dark .few-clear-btn:hover { background: #475569; }
10872
+ `;
10873
+ document.head.appendChild(style);
10874
+ }
10875
+ var FormulaEditorWidget = class {
10876
+ constructor(options) {
10877
+ __publicField(this, "_container");
10878
+ __publicField(this, "_editor");
10879
+ __publicField(this, "_placeholderEl");
10880
+ __publicField(this, "_clearBtn");
10881
+ __publicField(this, "_options");
10882
+ __publicField(this, "_fieldMap");
10883
+ /**
10884
+ * The canonical internal expression — SINGLE SOURCE OF TRUTH.
10885
+ * The DOM is always derived from this; never the reverse.
10886
+ * Updated by _onEditorInput() (user edits) and setValue() (programmatic).
10887
+ */
10888
+ __publicField(this, "_internalValue", "");
10889
+ injectStyles();
10890
+ this._options = { ...options };
10891
+ this._fieldMap = buildFieldMap(options.availableFields);
10892
+ this._container = document.createElement("div");
10893
+ this._container.className = "few-container";
10894
+ this._placeholderEl = document.createElement("div");
10895
+ this._placeholderEl.className = "few-placeholder";
10896
+ this._placeholderEl.textContent = options.placeholder ?? "Enter formula\u2026";
10897
+ this._editor = document.createElement("div");
10898
+ this._editor.className = "few-editor";
10899
+ this._editor.contentEditable = "true";
10900
+ this._editor.spellcheck = false;
10901
+ this._editor.setAttribute("autocomplete", "off");
10902
+ this._editor.setAttribute("autocorrect", "off");
10903
+ this._editor.setAttribute("autocapitalize", "off");
10904
+ this._editor.setAttribute("data-formula-editor", "true");
10905
+ this._editor.style.setProperty("color", "rgb(213, 12, 65)", "important");
10906
+ this._editor.style.setProperty("font-weight", "700", "important");
10907
+ this._editor.style.setProperty("caret-color", "#635bff", "important");
10908
+ this._clearBtn = document.createElement("button");
10909
+ this._clearBtn.type = "button";
10910
+ this._clearBtn.className = "few-clear-btn";
10911
+ this._clearBtn.title = "Clear formula";
10912
+ this._clearBtn.textContent = "\xD7";
10913
+ this._clearBtn.addEventListener("mousedown", (e) => {
10914
+ e.preventDefault();
10915
+ this.clear();
10916
+ });
10917
+ this._container.appendChild(this._placeholderEl);
10918
+ this._container.appendChild(this._editor);
10919
+ this._container.appendChild(this._clearBtn);
10920
+ this._attachEvents();
10921
+ if (options.initialValue) {
10922
+ this.setValue(options.initialValue);
10923
+ } else {
10924
+ this._syncUI();
10925
+ }
10926
+ }
10927
+ // ─── Internal event wiring ───────────────────────────────────────────────
10928
+ _attachEvents() {
10929
+ this._editor.addEventListener("input", () => this._onEditorInput());
10930
+ this._editor.addEventListener("keydown", (e) => {
10931
+ if (e.key === "Enter") {
10932
+ e.preventDefault();
10933
+ return;
10934
+ }
10935
+ if (e.key === "Backspace")
10936
+ this._handleBackspaceOnChip(e);
10937
+ });
10938
+ this._editor.addEventListener("paste", (e) => {
10939
+ e.preventDefault();
10940
+ const text = e.clipboardData?.getData("text/plain") ?? "";
10941
+ if (text) {
10942
+ this._insertTextAtCaret(text);
10943
+ this._onEditorInput();
10944
+ }
10945
+ });
10946
+ this._editor.addEventListener("dragover", (e) => e.preventDefault());
10947
+ this._editor.addEventListener("drop", (e) => e.preventDefault());
10948
+ }
10949
+ // ─── Core edit cycle ─────────────────────────────────────────────────────
10950
+ _onEditorInput() {
10951
+ this._normaliseDom();
10952
+ const newValue = this._readInternalExpression();
10953
+ const changed = newValue !== this._internalValue;
10954
+ this._internalValue = newValue;
10955
+ this._syncUI();
10956
+ if (changed) {
10957
+ this._options.onChange?.(newValue);
10958
+ }
10959
+ }
10960
+ /**
10961
+ * Remove browser-inserted artefacts (<br>, stray <div>/<span>) that some
10962
+ * browsers inject into a contenteditable. Chips (data-field-ref) are kept.
10963
+ */
10964
+ _normaliseDom() {
10965
+ for (const node of Array.from(this._editor.childNodes)) {
10966
+ if (node.nodeType !== Node.ELEMENT_NODE)
10967
+ continue;
10968
+ const el = node;
10969
+ if (el.hasAttribute("data-field-ref"))
10970
+ continue;
10971
+ if (el.tagName === "BR") {
10972
+ el.remove();
10973
+ continue;
10974
+ }
10975
+ const textNode = document.createTextNode(el.textContent ?? "");
10976
+ this._editor.replaceChild(textNode, el);
10977
+ }
10978
+ }
10979
+ /**
10980
+ * Backspace immediately before a chip should delete the whole chip, not
10981
+ * step into its non-editable content.
10982
+ */
10983
+ _handleBackspaceOnChip(e) {
10984
+ const sel = window.getSelection();
10985
+ if (!sel || sel.rangeCount === 0)
10986
+ return;
10987
+ const range = sel.getRangeAt(0);
10988
+ if (!range.collapsed)
10989
+ return;
10990
+ let prev = null;
10991
+ if (range.startContainer === this._editor) {
10992
+ if (range.startOffset > 0)
10993
+ prev = this._editor.childNodes[range.startOffset - 1];
10994
+ } else if (range.startContainer.nodeType === Node.TEXT_NODE) {
10995
+ if (range.startOffset === 0)
10996
+ prev = range.startContainer.previousSibling;
10997
+ }
10998
+ if (prev && prev.hasAttribute?.("data-field-ref")) {
10999
+ e.preventDefault();
11000
+ prev.remove();
11001
+ this._onEditorInput();
11002
+ }
11003
+ }
11004
+ // ─── Serialisation: DOM → internal expression ────────────────────────────
11005
+ _readInternalExpression() {
11006
+ let result = "";
11007
+ for (const node of Array.from(this._editor.childNodes)) {
11008
+ if (node.nodeType === Node.TEXT_NODE) {
11009
+ result += node.textContent ?? "";
11010
+ } else if (node.nodeType === Node.ELEMENT_NODE) {
11011
+ const el = node;
11012
+ const ref = el.getAttribute("data-field-ref");
11013
+ if (ref !== null) {
11014
+ result += this._options.mode === "bracket" ? `{${ref}}` : ref;
11015
+ } else {
11016
+ result += el.textContent ?? "";
11017
+ }
11018
+ }
11019
+ }
11020
+ return result;
11021
+ }
11022
+ // ─── Deserialization: internal expression → DOM ──────────────────────────
11023
+ /**
11024
+ * Load an internal expression string.
11025
+ *
11026
+ * This is the ONLY path that populates the editor with chips.
11027
+ * It does NOT fire onChange (programmatic set ≠ user edit).
11028
+ *
11029
+ * Design invariant: the DOM is ALWAYS fully rebuilt from _internalValue,
11030
+ * never from user-visible text. This guarantees correct state on re-render
11031
+ * after field switches, undo/redo, or hot-reload.
11032
+ */
11033
+ setValue(internalExpression) {
11034
+ this._internalValue = internalExpression ?? "";
11035
+ this._editor.innerHTML = "";
11036
+ if (this._internalValue.trim()) {
11037
+ const tokens = parseExpressionToTokens(
11038
+ this._internalValue,
11039
+ this._fieldMap,
11040
+ this._options.mode
11041
+ );
11042
+ for (const token of tokens) {
11043
+ if (token.type === "field" && token.fieldRef !== void 0) {
11044
+ const field = this._fieldMap.get(token.fieldRef) ?? {
11045
+ id: token.fieldRef,
11046
+ fieldName: token.fieldRef,
11047
+ label: token.fieldRef
11048
+ // shows raw ref — still readable
11049
+ };
11050
+ const isUnknown = !this._fieldMap.has(token.fieldRef);
11051
+ this._editor.appendChild(this._createChipEl(field, isUnknown));
11052
+ } else {
11053
+ this._editor.appendChild(document.createTextNode(token.value));
11054
+ }
11055
+ }
11056
+ }
11057
+ this._syncUI();
11058
+ }
11059
+ /** Return the current stored expression. Always reflects live DOM state. */
11060
+ getValue() {
11061
+ return this._readInternalExpression();
11062
+ }
11063
+ // ─── Public mutation ─────────────────────────────────────────────────────
11064
+ /**
11065
+ * Insert a field chip at the caret (or append if editor lacks focus).
11066
+ * Handles spacing automatically so chips never run into adjacent text.
11067
+ */
11068
+ insertField(field) {
11069
+ this._editor.focus();
11070
+ const chip = this._createChipEl(field, false);
11071
+ const sel = window.getSelection();
11072
+ const inEditor = sel && sel.rangeCount > 0 && this._editor.contains(sel.getRangeAt(0).commonAncestorContainer);
11073
+ if (inEditor) {
11074
+ const range = sel.getRangeAt(0);
11075
+ range.deleteContents();
11076
+ if (this._needsSpaceBefore(range)) {
11077
+ range.insertNode(document.createTextNode(" "));
11078
+ range.collapse(false);
11079
+ }
11080
+ range.insertNode(chip);
11081
+ const spaceAfter = document.createTextNode(" ");
11082
+ range.setStartAfter(chip);
11083
+ range.setEndAfter(chip);
11084
+ range.insertNode(spaceAfter);
11085
+ range.setStartAfter(spaceAfter);
11086
+ range.setEndAfter(spaceAfter);
11087
+ sel.removeAllRanges();
11088
+ sel.addRange(range);
11089
+ } else {
11090
+ this._appendAtEnd(chip);
11091
+ }
11092
+ this._onEditorInput();
11093
+ }
11094
+ /**
11095
+ * Insert plain text at the caret (operators, function names, parens).
11096
+ * Operators rendered by this path are text nodes and always inherit
11097
+ * .few-editor's high-contrast color.
11098
+ */
11099
+ insertText(text) {
11100
+ this._editor.focus();
11101
+ this._insertTextAtCaret(text);
11102
+ this._onEditorInput();
11103
+ }
11104
+ /**
11105
+ * Clear the entire formula expression.
11106
+ * Fires onChange('') so the store is updated immediately.
11107
+ */
11108
+ clear() {
11109
+ this._editor.innerHTML = "";
11110
+ this._internalValue = "";
11111
+ this._syncUI();
11112
+ this._options.onChange?.("");
11113
+ }
11114
+ // ─── Visual state ────────────────────────────────────────────────────────
11115
+ /** Toggle error (red border) state. */
11116
+ setError(hasError) {
11117
+ this._editor.classList.toggle("few-has-error", hasError);
11118
+ }
11119
+ /**
11120
+ * Refresh the field map with a new field list (e.g. schema changed).
11121
+ * Re-renders from _internalValue so stale labels / unknowns are resolved.
11122
+ */
11123
+ updateFields(fields) {
11124
+ this._options.availableFields = fields;
11125
+ this._fieldMap = buildFieldMap(fields);
11126
+ this.setValue(this._internalValue);
11127
+ }
11128
+ /** Return the root element to mount. */
11129
+ getElement() {
11130
+ return this._container;
11131
+ }
11132
+ /** Focus the editable area. */
11133
+ focus() {
11134
+ this._editor.focus();
11135
+ }
11136
+ /** Remove from DOM. */
11137
+ destroy() {
11138
+ this._container.remove();
11139
+ }
11140
+ // ─── Private helpers ─────────────────────────────────────────────────────
11141
+ _insertTextAtCaret(text) {
11142
+ const sel = window.getSelection();
11143
+ const inEditor = sel && sel.rangeCount > 0 && this._editor.contains(sel.getRangeAt(0).commonAncestorContainer);
11144
+ if (inEditor) {
11145
+ const range = sel.getRangeAt(0);
11146
+ range.deleteContents();
11147
+ const tn = document.createTextNode(text);
11148
+ range.insertNode(tn);
11149
+ range.setStartAfter(tn);
11150
+ range.setEndAfter(tn);
11151
+ sel.removeAllRanges();
11152
+ sel.addRange(range);
11153
+ } else {
11154
+ this._editor.appendChild(document.createTextNode(text));
11155
+ }
11156
+ }
11157
+ _appendAtEnd(chip) {
11158
+ const last = this._editor.lastChild;
11159
+ if (last) {
11160
+ if (last.nodeType === Node.TEXT_NODE) {
11161
+ const txt = last.textContent ?? "";
11162
+ if (txt.length > 0 && !/\s$/.test(txt)) {
11163
+ this._editor.appendChild(document.createTextNode(" "));
11164
+ }
11165
+ } else if (last.hasAttribute?.("data-field-ref")) {
11166
+ this._editor.appendChild(document.createTextNode(" "));
11167
+ }
11168
+ }
11169
+ this._editor.appendChild(chip);
11170
+ this._editor.appendChild(document.createTextNode(" "));
11171
+ }
11172
+ _needsSpaceBefore(range) {
11173
+ const { startContainer, startOffset } = range;
11174
+ if (startContainer.nodeType === Node.TEXT_NODE) {
11175
+ const text = startContainer.textContent ?? "";
11176
+ const ch = text[startOffset - 1];
11177
+ return startOffset > 0 && ch !== void 0 && !/[\s(]/.test(ch);
11178
+ }
11179
+ if (startContainer === this._editor && startOffset > 0) {
11180
+ const prev = this._editor.childNodes[startOffset - 1];
11181
+ return prev?.hasAttribute?.("data-field-ref") ?? false;
11182
+ }
11183
+ return false;
11184
+ }
11185
+ _createChipEl(field, isUnknown) {
11186
+ const chip = document.createElement("span");
11187
+ chip.contentEditable = "false";
11188
+ chip.setAttribute("data-field-ref", getFieldRef(field));
11189
+ chip.textContent = field.label || field.fieldName || field.id;
11190
+ chip.className = isUnknown ? "few-chip few-chip-unknown" : "few-chip";
11191
+ chip.title = this._options.mode === "bracket" ? `{${getFieldRef(field)}}` : getFieldRef(field);
11192
+ return chip;
11193
+ }
11194
+ /**
11195
+ * Synchronise all purely-visual state:
11196
+ * - placeholder visibility
11197
+ * - clear-button visibility (few-has-content on container)
11198
+ */
11199
+ _syncUI() {
11200
+ const hasContent = this._internalValue.trim().length > 0 || !!this._editor.querySelector("[data-field-ref]");
11201
+ this._placeholderEl.style.display = hasContent ? "none" : "block";
11202
+ this._container.classList.toggle("few-has-content", hasContent);
11203
+ this._editor.style.setProperty("color", "rgb(213, 12, 65)", "important");
11204
+ this._editor.style.setProperty("font-weight", "700", "important");
11205
+ }
11206
+ };
11207
+
10510
11208
  // src/builder/FormBuilder.ts
10511
11209
  var advancedCssPanelState = /* @__PURE__ */ new Map();
10512
- var lastFocusedExprTextarea = null;
11210
+ var lastFocusedFormulaWidget = null;
10513
11211
  var LABEL_DEBOUNCE_MS = 300;
10514
11212
  var labelUpdateTimeouts = /* @__PURE__ */ new Map();
10515
11213
  var FormBuilder = class {
@@ -10988,8 +11686,15 @@ var FormBuilder = class {
10988
11686
  if (field.type !== "formula" || !field.formulaConfig)
10989
11687
  continue;
10990
11688
  const fcfg = field.formulaConfig;
10991
- const fAvailable = getFieldsForFormula(schema, field.id);
10992
- const fNames = fAvailable.map((f) => f.fieldName);
11689
+ const allOtherFields = schema.sections.flatMap((s) => s.fields).filter((f) => f.id !== field.id);
11690
+ const validRefSet = /* @__PURE__ */ new Set();
11691
+ allOtherFields.forEach((f) => {
11692
+ if (f.fieldName)
11693
+ validRefSet.add(f.fieldName);
11694
+ if (f.id)
11695
+ validRefSet.add(f.id);
11696
+ });
11697
+ const validRefs = Array.from(validRefSet);
10993
11698
  const exprs = [];
10994
11699
  if (fcfg.mode === "single") {
10995
11700
  if (fcfg.single?.expression)
@@ -11003,7 +11708,7 @@ var FormBuilder = class {
11003
11708
  exprs.push(fcfg.multiple.fallbackExpression);
11004
11709
  }
11005
11710
  for (const expr of exprs) {
11006
- const result = validateFormulaExpression(expr, fNames);
11711
+ const result = validateFormulaExpression(expr, validRefs);
11007
11712
  if (!result.valid) {
11008
11713
  alert(`Formula error in "${field.label}": ${result.error}`);
11009
11714
  return;
@@ -11761,38 +12466,64 @@ var FormBuilder = class {
11761
12466
  const numericFields = getNumericFieldsForFormula(schema, selectedField.id);
11762
12467
  const availableIds = numericFields.map((f) => f.id);
11763
12468
  const availableNames = numericFields.map((f) => f.fieldName);
12469
+ const allFieldInfosForWidget = schema.sections.flatMap((s) => s.fields).filter((f) => f.id !== selectedField.id).map((f) => ({
12470
+ id: f.id,
12471
+ fieldName: f.fieldName ?? f.id,
12472
+ label: f.label || f.fieldName || f.id
12473
+ }));
12474
+ const numFieldInfos = numericFields.map((f) => ({
12475
+ id: f.id,
12476
+ fieldName: f.fieldName,
12477
+ label: f.label || f.fieldName || f.id
12478
+ }));
11764
12479
  const formulaGroup = createElement("div", { className: "mb-3" });
11765
- formulaGroup.appendChild(createElement("label", { className: "block text-sm font-normal text-gray-700 dark:text-gray-300 mb-1", text: "Formula" }));
11766
- const formulaInput = createElement("input", {
11767
- type: "text",
11768
- className: "w-full px-3 py-2 border border-gray-200 dark:border-gray-700 rounded-md bg-transparent font-mono text-sm",
11769
- value: selectedField.formula || "",
12480
+ formulaGroup.appendChild(createElement("label", {
12481
+ className: "block text-sm font-normal text-gray-700 dark:text-gray-300 mb-1",
12482
+ text: "Formula"
12483
+ }));
12484
+ const numFormulaWidget = new FormulaEditorWidget({
12485
+ mode: "plain",
12486
+ availableFields: allFieldInfosForWidget,
12487
+ // Always read from the store snapshot — guarantees correct reload
12488
+ // after field switch and re-render.
12489
+ initialValue: selectedField.formula || "",
11770
12490
  placeholder: "e.g. quantity * price",
11771
- "data-focus-id": `field-formula-${selectedField.id}`,
11772
- oninput: (e) => {
11773
- const formula = e.target.value.trim();
12491
+ onChange: (formula) => {
11774
12492
  const deps = parseFormulaDependencies(formula);
11775
- const validation = validateFormula(formula, availableIds, availableNames, selectedField.id);
11776
- const hasCircular = deps.length > 0 && detectCircularDependency(schema, selectedField.id, formula, deps);
11777
- const errEl = formulaGroup.querySelector(".formula-error");
11778
- if (errEl) {
11779
- if (validation.valid && !hasCircular) {
11780
- errEl.textContent = "";
11781
- errEl.classList.add("hidden");
11782
- } else {
11783
- errEl.textContent = !validation.valid ? validation.error : "Circular dependency detected";
11784
- errEl.classList.remove("hidden");
11785
- }
12493
+ const isValid2 = (() => {
12494
+ if (!formula.trim())
12495
+ return true;
12496
+ const v = validateFormula(formula, availableIds, availableNames, selectedField.id);
12497
+ if (!v.valid)
12498
+ return false;
12499
+ return !(deps.length > 0 && detectCircularDependency(schema, selectedField.id, formula, deps));
12500
+ })();
12501
+ numFormulaWidget.setError(!isValid2 && formula.trim().length > 0);
12502
+ if (!isValid2 && formula.trim()) {
12503
+ const v = validateFormula(formula, availableIds, availableNames, selectedField.id);
12504
+ formulaError.textContent = !v.valid ? v.error : "Circular dependency detected";
12505
+ formulaError.classList.remove("hidden");
12506
+ } else {
12507
+ formulaError.textContent = "";
12508
+ formulaError.classList.add("hidden");
11786
12509
  }
11787
12510
  formStore.getState().updateField(selectedField.id, { formula, dependencies: deps });
11788
12511
  }
11789
12512
  });
11790
- formulaGroup.appendChild(formulaInput);
11791
- const formulaError = createElement("div", { className: "text-xs text-red-600 dark:text-red-400 mt-1 formula-error hidden" });
12513
+ numFormulaWidget.getElement().querySelector("[data-formula-editor]")?.addEventListener("focus", () => {
12514
+ lastFocusedFormulaWidget = numFormulaWidget;
12515
+ });
12516
+ formulaGroup.appendChild(numFormulaWidget.getElement());
12517
+ const formulaError = createElement("div", {
12518
+ className: "text-xs text-red-600 dark:text-red-400 mt-1 formula-error hidden"
12519
+ });
11792
12520
  formulaGroup.appendChild(formulaError);
11793
12521
  body.appendChild(formulaGroup);
11794
12522
  const insertGroup = createElement("div", { className: "mb-3" });
11795
- insertGroup.appendChild(createElement("label", { className: "block text-sm font-normal text-gray-700 dark:text-gray-300 mb-1", text: "Insert Field" }));
12523
+ insertGroup.appendChild(createElement("label", {
12524
+ className: "block text-sm font-normal text-gray-700 dark:text-gray-300 mb-1",
12525
+ text: "Insert Field"
12526
+ }));
11796
12527
  const insertSelect = createElement("select", {
11797
12528
  className: "w-full px-3 py-2 border border-gray-200 dark:border-gray-700 rounded-md bg-transparent",
11798
12529
  onchange: (e) => {
@@ -11800,34 +12531,58 @@ var FormBuilder = class {
11800
12531
  const ref = sel.value;
11801
12532
  if (!ref)
11802
12533
  return;
11803
- const current = selectedField.formula || "";
11804
- const insert = current ? ` ${ref} ` : ref;
11805
- const newFormula = current + insert;
11806
- formStore.getState().updateField(selectedField.id, {
11807
- formula: newFormula,
11808
- dependencies: parseFormulaDependencies(newFormula)
11809
- });
11810
- formulaInput.value = newFormula;
12534
+ const field = numFieldInfos.find((f) => f.fieldName === ref || f.id === ref);
12535
+ if (field) {
12536
+ numFormulaWidget.insertField(field);
12537
+ lastFocusedFormulaWidget = numFormulaWidget;
12538
+ }
11811
12539
  sel.value = "";
11812
- this.render();
11813
12540
  }
11814
12541
  });
11815
- insertSelect.appendChild(createElement("option", { value: "", text: "Select field to insert...", selected: true }));
11816
- numericFields.forEach((f) => {
12542
+ insertSelect.appendChild(createElement("option", { value: "", text: "Select field to insert\u2026", selected: true }));
12543
+ numFieldInfos.forEach((f) => {
11817
12544
  const ref = f.fieldName !== f.id ? f.fieldName : f.id;
11818
- insertSelect.appendChild(createElement("option", { value: ref, text: `${f.label} (${ref})` }));
12545
+ insertSelect.appendChild(createElement("option", { value: ref, text: f.label }));
11819
12546
  });
11820
12547
  insertGroup.appendChild(insertSelect);
11821
12548
  body.appendChild(insertGroup);
11822
- const hintEl = createElement("p", {
11823
- className: "text-xs text-gray-500 dark:text-gray-400 mb-2",
11824
- text: "Use +, -, *, / and parentheses. Reference fields by their name or ID."
12549
+ body.appendChild(createElement("label", {
12550
+ className: "block text-sm font-normal text-gray-700 dark:text-gray-300 mb-1",
12551
+ text: "Operators"
12552
+ }));
12553
+ const numMathWrap = createElement("div", { className: "flex flex-wrap gap-1 mb-3" });
12554
+ [
12555
+ { text: "+", insert: " + " },
12556
+ { text: "-", insert: " - " },
12557
+ { text: "*", insert: " * " },
12558
+ { text: "/", insert: " / " },
12559
+ { text: "%", insert: " % " },
12560
+ { text: "(", insert: "(" },
12561
+ { text: ")", insert: ")" }
12562
+ ].forEach((op) => {
12563
+ numMathWrap.appendChild(createElement("button", {
12564
+ type: "button",
12565
+ className: "px-2 py-0.5 text-xs bg-gray-100 dark:bg-gray-800 text-gray-700 dark:text-gray-300 border border-gray-200 dark:border-gray-700 rounded font-mono hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors",
12566
+ text: op.text,
12567
+ onclick: () => {
12568
+ (lastFocusedFormulaWidget ?? numFormulaWidget).insertText(op.insert);
12569
+ }
12570
+ }));
11825
12571
  });
11826
- body.appendChild(hintEl);
12572
+ body.appendChild(numMathWrap);
12573
+ body.appendChild(createElement("p", {
12574
+ className: "text-xs text-gray-500 dark:text-gray-400 mb-2",
12575
+ text: "Fields shown as labels \u2014 stored as field references internally."
12576
+ }));
11827
12577
  }
11828
12578
  }
11829
12579
  if (selectedField.type === "formula") {
11830
12580
  const schema = formStore.getState().schema;
12581
+ const allSchemaFieldInfos = schema.sections.flatMap((s) => s.fields).filter((f) => f.id !== selectedField.id).map((f) => ({
12582
+ id: f.id,
12583
+ fieldName: f.fieldName ?? f.id,
12584
+ label: f.label || f.fieldName || f.id
12585
+ }));
11831
12586
  const availableFields = getFieldsForFormula(schema, selectedField.id);
11832
12587
  const cfg = selectedField.formulaConfig ?? {
11833
12588
  mode: "single",
@@ -11880,35 +12635,43 @@ var FormBuilder = class {
11880
12635
  modeRow.appendChild(multipleBtn);
11881
12636
  modeGroup.appendChild(modeRow);
11882
12637
  body.appendChild(modeGroup);
12638
+ const fwFieldInfos = allSchemaFieldInfos;
12639
+ lastFocusedFormulaWidget = null;
12640
+ const registerFormulaWidget = (widget) => {
12641
+ widget.getElement().querySelector("[data-formula-editor]")?.addEventListener("focus", () => {
12642
+ lastFocusedFormulaWidget = widget;
12643
+ });
12644
+ };
11883
12645
  if (cfg.mode === "single") {
11884
12646
  const exprGroup = createElement("div", { className: "mb-3" });
11885
12647
  exprGroup.appendChild(createElement("label", {
11886
12648
  className: "block text-sm font-normal text-gray-700 dark:text-gray-300 mb-1",
11887
12649
  text: "Expression"
11888
12650
  }));
11889
- const exprTextarea = createElement("textarea", {
11890
- className: "w-full px-3 py-2 border border-gray-200 dark:border-gray-700 rounded-md bg-transparent font-mono text-sm resize-none",
11891
- placeholder: "e.g. {fieldA} + {fieldB} * 1.18",
11892
- rows: "3"
11893
- });
11894
- exprTextarea.value = cfg.single?.expression ?? "";
11895
- exprTextarea.addEventListener("focus", () => {
11896
- lastFocusedExprTextarea = exprTextarea;
11897
- });
11898
12651
  const exprError = createElement("div", { className: "text-xs text-red-500 mt-1 hidden" });
11899
- exprTextarea.addEventListener("input", () => {
11900
- const expr = exprTextarea.value;
11901
- const result = validateFormulaExpression(expr, availableFields.map((f) => f.fieldName));
11902
- if (result.valid || !expr.trim()) {
11903
- exprError.classList.add("hidden");
11904
- } else {
11905
- exprError.textContent = result.error;
11906
- exprError.classList.remove("hidden");
12652
+ const singleWidget = new FormulaEditorWidget({
12653
+ mode: "bracket",
12654
+ // Use ALL schema fields for the fieldMap so any ref resolves to its label
12655
+ availableFields: fwFieldInfos,
12656
+ // Source of truth: always read from persisted cfg, never from a
12657
+ // closure variable. This guarantees correct reload after field switch.
12658
+ initialValue: cfg.single?.expression ?? "",
12659
+ placeholder: "e.g. {fieldA} + {fieldB} * 1.18",
12660
+ onChange: (expr) => {
12661
+ const result = validateFormulaExpression(expr, fwFieldInfos.map((f) => f.fieldName));
12662
+ const isValid2 = result.valid || !expr.trim();
12663
+ singleWidget.setError(!isValid2);
12664
+ exprError.textContent = isValid2 ? "" : result.error;
12665
+ exprError.classList.toggle("hidden", isValid2);
12666
+ const freshCfg = formStore.getState().schema.sections.flatMap((s) => s.fields).find((f) => f.id === selectedField.id)?.formulaConfig ?? cfg;
12667
+ formStore.getState().updateField(selectedField.id, {
12668
+ formulaConfig: { ...freshCfg, single: { expression: expr } }
12669
+ });
11907
12670
  }
11908
- const freshCfg = formStore.getState().schema.sections.flatMap((s) => s.fields).find((f) => f.id === selectedField.id)?.formulaConfig ?? cfg;
11909
- formStore.getState().updateField(selectedField.id, { formulaConfig: { ...freshCfg, single: { expression: expr } } });
11910
12671
  });
11911
- exprGroup.appendChild(exprTextarea);
12672
+ registerFormulaWidget(singleWidget);
12673
+ lastFocusedFormulaWidget = singleWidget;
12674
+ exprGroup.appendChild(singleWidget.getElement());
11912
12675
  exprGroup.appendChild(exprError);
11913
12676
  body.appendChild(exprGroup);
11914
12677
  } else {
@@ -11935,16 +12698,15 @@ var FormBuilder = class {
11935
12698
  });
11936
12699
  compareGroup.appendChild(compareSelect);
11937
12700
  body.appendChild(compareGroup);
11938
- const conditionsLabel = createElement("label", {
12701
+ body.appendChild(createElement("label", {
11939
12702
  className: "block text-sm font-normal text-gray-700 dark:text-gray-300 mb-2",
11940
12703
  text: "Conditions"
11941
- });
11942
- body.appendChild(conditionsLabel);
12704
+ }));
11943
12705
  const conditions = cfg.multiple?.conditions ?? [];
11944
12706
  conditions.forEach((cond, idx) => {
11945
12707
  const row = createElement("div", { className: "mb-2 p-2 rounded-md border border-gray-100 dark:border-gray-800 space-y-1" });
11946
- const whenLabel = createElement("div", { className: "text-xs text-gray-500 dark:text-gray-400", text: "When equals" });
11947
- const valueInput = createElement("input", {
12708
+ row.appendChild(createElement("div", { className: "text-xs text-gray-500 dark:text-gray-400", text: "When equals" }));
12709
+ row.appendChild(createElement("input", {
11948
12710
  type: "text",
11949
12711
  className: "w-full px-2 py-1 text-sm border border-gray-200 dark:border-gray-700 rounded-md bg-transparent",
11950
12712
  value: cond.value,
@@ -11956,25 +12718,26 @@ var FormBuilder = class {
11956
12718
  newConds[idx] = { ...newConds[idx], value: v };
11957
12719
  patchMultiple({ conditions: newConds });
11958
12720
  }
11959
- });
11960
- const exprLabel = createElement("div", { className: "text-xs text-gray-500 dark:text-gray-400", text: "\u2192 Expression" });
11961
- const condTextarea = createElement("textarea", {
11962
- className: "w-full px-2 py-1 text-sm border border-gray-200 dark:border-gray-700 rounded-md bg-transparent font-mono resize-none",
12721
+ }));
12722
+ row.appendChild(createElement("div", { className: "text-xs text-gray-500 dark:text-gray-400", text: "\u2192 Expression" }));
12723
+ const condWidget = new FormulaEditorWidget({
12724
+ mode: "bracket",
12725
+ availableFields: fwFieldInfos,
12726
+ // all-schema fieldMap
12727
+ initialValue: cond.expression ?? "",
11963
12728
  placeholder: "e.g. {fieldA} + {fieldB}",
11964
- rows: "2"
11965
- });
11966
- condTextarea.value = cond.expression ?? "";
11967
- condTextarea.addEventListener("focus", () => {
11968
- lastFocusedExprTextarea = condTextarea;
11969
- });
11970
- condTextarea.addEventListener("input", () => {
11971
- const expr = condTextarea.value;
11972
- const freshCfg = formStore.getState().schema.sections.flatMap((s) => s.fields).find((f) => f.id === selectedField.id)?.formulaConfig ?? cfg;
11973
- const newConds = [...freshCfg.multiple?.conditions ?? []];
11974
- newConds[idx] = { ...newConds[idx], expression: expr };
11975
- patchMultiple({ conditions: newConds });
12729
+ onChange: (expr) => {
12730
+ const freshCfg = formStore.getState().schema.sections.flatMap((s) => s.fields).find((f) => f.id === selectedField.id)?.formulaConfig ?? cfg;
12731
+ const newConds = [...freshCfg.multiple?.conditions ?? []];
12732
+ newConds[idx] = { ...newConds[idx], expression: expr };
12733
+ patchMultiple({ conditions: newConds });
12734
+ }
11976
12735
  });
11977
- const removeBtn = createElement("button", {
12736
+ registerFormulaWidget(condWidget);
12737
+ if (!lastFocusedFormulaWidget)
12738
+ lastFocusedFormulaWidget = condWidget;
12739
+ row.appendChild(condWidget.getElement());
12740
+ row.appendChild(createElement("button", {
11978
12741
  type: "button",
11979
12742
  className: "text-xs text-red-500 hover:text-red-700 dark:text-red-400",
11980
12743
  text: "\u2212 Remove",
@@ -11985,12 +12748,7 @@ var FormBuilder = class {
11985
12748
  patchMultiple({ conditions: newConds });
11986
12749
  this.render();
11987
12750
  }
11988
- });
11989
- row.appendChild(whenLabel);
11990
- row.appendChild(valueInput);
11991
- row.appendChild(exprLabel);
11992
- row.appendChild(condTextarea);
11993
- row.appendChild(removeBtn);
12751
+ }));
11994
12752
  body.appendChild(row);
11995
12753
  });
11996
12754
  body.appendChild(createElement("button", {
@@ -12009,22 +12767,22 @@ var FormBuilder = class {
12009
12767
  className: "block text-sm font-normal text-gray-700 dark:text-gray-300 mb-1",
12010
12768
  text: "Fallback Expression"
12011
12769
  }));
12012
- const fallbackTextarea = createElement("textarea", {
12013
- className: "w-full px-3 py-2 border border-gray-200 dark:border-gray-700 rounded-md bg-transparent font-mono text-sm resize-none",
12770
+ const fallbackWidget = new FormulaEditorWidget({
12771
+ mode: "bracket",
12772
+ availableFields: fwFieldInfos,
12773
+ initialValue: cfg.multiple?.fallbackExpression ?? "",
12014
12774
  placeholder: "e.g. 0",
12015
- rows: "2"
12016
- });
12017
- fallbackTextarea.value = cfg.multiple?.fallbackExpression ?? "";
12018
- fallbackTextarea.addEventListener("focus", () => {
12019
- lastFocusedExprTextarea = fallbackTextarea;
12020
- });
12021
- fallbackTextarea.addEventListener("input", () => {
12022
- patchMultiple({ fallbackExpression: fallbackTextarea.value });
12775
+ onChange: (expr) => {
12776
+ patchMultiple({ fallbackExpression: expr });
12777
+ }
12023
12778
  });
12024
- fallbackGroup.appendChild(fallbackTextarea);
12779
+ registerFormulaWidget(fallbackWidget);
12780
+ if (!lastFocusedFormulaWidget)
12781
+ lastFocusedFormulaWidget = fallbackWidget;
12782
+ fallbackGroup.appendChild(fallbackWidget.getElement());
12025
12783
  body.appendChild(fallbackGroup);
12026
12784
  }
12027
- if (availableFields.length > 0) {
12785
+ if (fwFieldInfos.length > 0) {
12028
12786
  const insertGroup = createElement("div", { className: "mb-3" });
12029
12787
  insertGroup.appendChild(createElement("label", {
12030
12788
  className: "block text-sm font-normal text-gray-700 dark:text-gray-300 mb-1",
@@ -12037,27 +12795,20 @@ var FormBuilder = class {
12037
12795
  const ref = sel.value;
12038
12796
  if (!ref)
12039
12797
  return;
12040
- const ta = lastFocusedExprTextarea;
12041
- if (ta) {
12042
- const start = ta.selectionStart ?? ta.value.length;
12043
- const end = ta.selectionEnd ?? ta.value.length;
12044
- const before = ta.value.slice(0, start);
12045
- const after = ta.value.slice(end);
12046
- const pad = before.length > 0 && !/\s$/.test(before) ? " " : "";
12047
- ta.value = before + pad + ref + after;
12048
- const newPos = before.length + pad.length + ref.length;
12049
- ta.selectionStart = ta.selectionEnd = newPos;
12050
- ta.dispatchEvent(new Event("input", { bubbles: true }));
12051
- ta.focus();
12798
+ const field = fwFieldInfos.find((f) => f.fieldName === ref || f.id === ref);
12799
+ const target = lastFocusedFormulaWidget;
12800
+ if (field && target) {
12801
+ target.insertField(field);
12052
12802
  }
12053
12803
  sel.value = "";
12054
12804
  }
12055
12805
  });
12056
12806
  insertSelect.appendChild(createElement("option", { value: "", text: "Select field to insert\u2026", selected: true }));
12057
- availableFields.forEach((f) => {
12807
+ fwFieldInfos.forEach((f) => {
12058
12808
  insertSelect.appendChild(createElement("option", {
12059
- value: `{${f.fieldName}}`,
12060
- text: `${f.label} ({${f.fieldName}})`
12809
+ value: f.fieldName !== f.id ? f.fieldName : f.id,
12810
+ text: f.label
12811
+ // show only human-readable label
12061
12812
  }));
12062
12813
  });
12063
12814
  insertGroup.appendChild(insertSelect);
@@ -12088,16 +12839,7 @@ var FormBuilder = class {
12088
12839
  className: "px-2 py-0.5 text-xs bg-gray-100 dark:bg-gray-800 text-gray-700 dark:text-gray-300 border border-gray-200 dark:border-gray-700 rounded font-mono hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors",
12089
12840
  text: op.text,
12090
12841
  onclick: () => {
12091
- const ta = lastFocusedExprTextarea;
12092
- if (!ta)
12093
- return;
12094
- const start = ta.selectionStart ?? ta.value.length;
12095
- const end = ta.selectionEnd ?? ta.value.length;
12096
- ta.value = ta.value.slice(0, start) + op.insert + ta.value.slice(end);
12097
- const newPos = start + op.insert.length;
12098
- ta.selectionStart = ta.selectionEnd = newPos;
12099
- ta.dispatchEvent(new Event("input", { bubbles: true }));
12100
- ta.focus();
12842
+ lastFocusedFormulaWidget?.insertText(op.insert);
12101
12843
  }
12102
12844
  }));
12103
12845
  });
@@ -12834,6 +13576,84 @@ var FormBuilder = class {
12834
13576
  });
12835
13577
  parentFieldGroup.appendChild(parentFieldSelect);
12836
13578
  body.appendChild(parentFieldGroup);
13579
+ const autoPopHeader = createElement("h3", {
13580
+ className: "text-xs font-semibold text-gray-500 uppercase tracking-wider mb-3 mt-6",
13581
+ text: "Auto Populate Fields"
13582
+ });
13583
+ body.appendChild(autoPopHeader);
13584
+ const autoPopEnabled = selectedField.autoPopulateFields?.enabled === true;
13585
+ body.appendChild(this.createCheckboxField(
13586
+ "Enable automation for selected records",
13587
+ autoPopEnabled,
13588
+ (checked) => {
13589
+ const current = formStore.getState().schema.sections.flatMap((s) => s.fields).find((f) => f.id === selectedField.id);
13590
+ formStore.getState().updateField(selectedField.id, {
13591
+ autoPopulateFields: {
13592
+ enabled: checked,
13593
+ fields: current?.autoPopulateFields?.fields ?? []
13594
+ }
13595
+ });
13596
+ this.render();
13597
+ },
13598
+ `auto-populate-enabled-${selectedField.id}`
13599
+ ));
13600
+ {
13601
+ const autoPopFieldsGroup = createElement("div", { className: "mb-4 mt-2" });
13602
+ autoPopFieldsGroup.appendChild(createElement("label", {
13603
+ className: "block text-sm font-normal text-gray-700 dark:text-gray-300 mb-1",
13604
+ text: "Fields to auto-populate"
13605
+ }));
13606
+ const lookupFieldOptionsMapForAP = formStore.getState().lookupFieldOptionsMap;
13607
+ const availableAutoPopFields = selectedField.lookupSource ? lookupFieldOptionsMapForAP[selectedField.lookupSource] || [] : [];
13608
+ const selectedAutoPopFields = selectedField.autoPopulateFields?.fields ?? [];
13609
+ const isAutoPopDisabled = !autoPopEnabled || !selectedField.lookupSource;
13610
+ if (availableAutoPopFields.length === 0) {
13611
+ const emptyNote = createElement("p", {
13612
+ className: "text-xs text-gray-400 dark:text-gray-500 mt-1",
13613
+ text: selectedField.lookupSource ? "No fields available for this lookup source." : "Select a Lookup Source first."
13614
+ });
13615
+ autoPopFieldsGroup.appendChild(emptyNote);
13616
+ } else {
13617
+ const fieldList = createElement("div", {
13618
+ className: `border border-gray-200 dark:border-gray-700 rounded-md divide-y divide-gray-100 dark:divide-gray-700 overflow-y-auto max-h-40 ${isAutoPopDisabled ? "opacity-50 pointer-events-none" : ""}`
13619
+ });
13620
+ availableAutoPopFields.forEach((fieldKey) => {
13621
+ const isChecked = selectedAutoPopFields.includes(fieldKey);
13622
+ const row = createElement("label", {
13623
+ className: "flex items-center gap-2 px-3 py-2 cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 text-sm text-gray-700 dark:text-gray-300"
13624
+ });
13625
+ const cb = createElement("input", {
13626
+ type: "checkbox",
13627
+ className: "w-4 h-4 accent-blue-600",
13628
+ checked: isChecked,
13629
+ disabled: isAutoPopDisabled,
13630
+ onchange: (e) => {
13631
+ const target = e.target;
13632
+ const latestField = formStore.getState().schema.sections.flatMap((s) => s.fields).find((f) => f.id === selectedField.id);
13633
+ const latestSelected = latestField?.autoPopulateFields?.fields ?? [];
13634
+ const updatedFields = target.checked ? [.../* @__PURE__ */ new Set([...latestSelected, fieldKey])] : latestSelected.filter((k) => k !== fieldKey);
13635
+ formStore.getState().updateField(selectedField.id, {
13636
+ autoPopulateFields: {
13637
+ enabled: latestField?.autoPopulateFields?.enabled ?? true,
13638
+ fields: updatedFields
13639
+ }
13640
+ });
13641
+ }
13642
+ });
13643
+ row.appendChild(cb);
13644
+ row.appendChild(createElement("span", { text: fieldKey }));
13645
+ fieldList.appendChild(row);
13646
+ });
13647
+ autoPopFieldsGroup.appendChild(fieldList);
13648
+ if (selectedAutoPopFields.length > 0) {
13649
+ autoPopFieldsGroup.appendChild(createElement("p", {
13650
+ className: "text-xs text-gray-400 dark:text-gray-500 mt-1",
13651
+ text: `${selectedAutoPopFields.length} field(s) selected`
13652
+ }));
13653
+ }
13654
+ }
13655
+ body.appendChild(autoPopFieldsGroup);
13656
+ }
12837
13657
  body.appendChild(this.createCheckboxField(
12838
13658
  "Visibility",
12839
13659
  selectedField.visible !== false,