form-builder-pro 1.4.1 → 1.4.2
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.css +45 -3
- package/dist/index.js +859 -130
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +859 -130
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
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)
|
|
@@ -5068,8 +5091,32 @@ function fieldToPayload(field, opts) {
|
|
|
5068
5091
|
if (field.type === "formula") {
|
|
5069
5092
|
payload.fieldType = "FORMULA";
|
|
5070
5093
|
payload.type = "formula";
|
|
5071
|
-
|
|
5072
|
-
|
|
5094
|
+
payload.readOnly = true;
|
|
5095
|
+
payload.floatingPrecision = field.formulaConfig?.decimalPlaces ?? 2;
|
|
5096
|
+
if (field.formulaConfig?.mode === "single") {
|
|
5097
|
+
payload.formulaType = "SIMPLE";
|
|
5098
|
+
payload.formulaExpression = field.formulaConfig.single?.expression ?? "";
|
|
5099
|
+
payload.switchField = null;
|
|
5100
|
+
payload.switchCases = null;
|
|
5101
|
+
payload.switchDefaultFormula = null;
|
|
5102
|
+
} else if (field.formulaConfig?.mode === "multiple") {
|
|
5103
|
+
const multi = field.formulaConfig.multiple;
|
|
5104
|
+
payload.formulaType = "SWITCH";
|
|
5105
|
+
payload.formulaExpression = "";
|
|
5106
|
+
payload.switchField = multi?.compareField ?? null;
|
|
5107
|
+
payload.switchCases = (multi?.conditions ?? []).map((c) => ({
|
|
5108
|
+
matchValue: c.value,
|
|
5109
|
+
formulaExpression: c.expression ?? ""
|
|
5110
|
+
}));
|
|
5111
|
+
payload.switchDefaultFormula = multi?.fallbackExpression ?? "";
|
|
5112
|
+
payload.switchCases = payload.switchCases;
|
|
5113
|
+
} else {
|
|
5114
|
+
payload.formulaType = "SIMPLE";
|
|
5115
|
+
payload.formulaExpression = "";
|
|
5116
|
+
payload.switchField = null;
|
|
5117
|
+
payload.switchCases = null;
|
|
5118
|
+
payload.switchDefaultFormula = null;
|
|
5119
|
+
}
|
|
5073
5120
|
}
|
|
5074
5121
|
if (field.type === "repeater") {
|
|
5075
5122
|
payload.fieldType = "REPEATER";
|
|
@@ -10507,9 +10554,647 @@ var SectionList = class {
|
|
|
10507
10554
|
}
|
|
10508
10555
|
};
|
|
10509
10556
|
|
|
10557
|
+
// src/utils/formulaTokenParser.ts
|
|
10558
|
+
var FUNCTION_NAMES = /* @__PURE__ */ new Set(["ROUND", "ABS", "MIN", "MAX", "FLOOR", "CEIL", "SQRT", "POW"]);
|
|
10559
|
+
var OPERATOR_CHARS = /* @__PURE__ */ new Set(["+", "-", "*", "/"]);
|
|
10560
|
+
function parseExpressionToTokens(expression, fieldMap, mode) {
|
|
10561
|
+
if (!expression)
|
|
10562
|
+
return [];
|
|
10563
|
+
const tokens = [];
|
|
10564
|
+
let i = 0;
|
|
10565
|
+
const len = expression.length;
|
|
10566
|
+
while (i < len) {
|
|
10567
|
+
const ch = expression[i];
|
|
10568
|
+
if (/\s/.test(ch)) {
|
|
10569
|
+
let ws = "";
|
|
10570
|
+
while (i < len && /\s/.test(expression[i]))
|
|
10571
|
+
ws += expression[i++];
|
|
10572
|
+
tokens.push({ type: "space", value: ws, rawValue: ws });
|
|
10573
|
+
continue;
|
|
10574
|
+
}
|
|
10575
|
+
if (mode === "bracket" && ch === "{") {
|
|
10576
|
+
i++;
|
|
10577
|
+
let ref = "";
|
|
10578
|
+
while (i < len && expression[i] !== "}")
|
|
10579
|
+
ref += expression[i++];
|
|
10580
|
+
if (i < len && expression[i] === "}")
|
|
10581
|
+
i++;
|
|
10582
|
+
const trimmedRef = ref.trim();
|
|
10583
|
+
const field = fieldMap.get(trimmedRef);
|
|
10584
|
+
tokens.push({
|
|
10585
|
+
type: "field",
|
|
10586
|
+
value: field ? field.label || field.fieldName || trimmedRef : trimmedRef,
|
|
10587
|
+
rawValue: `{${trimmedRef}}`,
|
|
10588
|
+
fieldRef: trimmedRef
|
|
10589
|
+
});
|
|
10590
|
+
continue;
|
|
10591
|
+
}
|
|
10592
|
+
if (OPERATOR_CHARS.has(ch)) {
|
|
10593
|
+
tokens.push({ type: "operator", value: ch, rawValue: ch });
|
|
10594
|
+
i++;
|
|
10595
|
+
continue;
|
|
10596
|
+
}
|
|
10597
|
+
if (ch === "%") {
|
|
10598
|
+
tokens.push({ type: "percent", value: "%", rawValue: "%" });
|
|
10599
|
+
i++;
|
|
10600
|
+
continue;
|
|
10601
|
+
}
|
|
10602
|
+
if (ch === "(" || ch === ")") {
|
|
10603
|
+
tokens.push({ type: "paren", value: ch, rawValue: ch });
|
|
10604
|
+
i++;
|
|
10605
|
+
continue;
|
|
10606
|
+
}
|
|
10607
|
+
if (ch === ",") {
|
|
10608
|
+
tokens.push({ type: "comma", value: ",", rawValue: "," });
|
|
10609
|
+
i++;
|
|
10610
|
+
continue;
|
|
10611
|
+
}
|
|
10612
|
+
if (/[0-9]/.test(ch) || ch === "." && i + 1 < len && /[0-9]/.test(expression[i + 1])) {
|
|
10613
|
+
let num = "";
|
|
10614
|
+
while (i < len && /[0-9.]/.test(expression[i]))
|
|
10615
|
+
num += expression[i++];
|
|
10616
|
+
tokens.push({ type: "number", value: num, rawValue: num });
|
|
10617
|
+
continue;
|
|
10618
|
+
}
|
|
10619
|
+
if (/[a-zA-Z_]/.test(ch)) {
|
|
10620
|
+
let ident = "";
|
|
10621
|
+
while (i < len && /[a-zA-Z0-9_]/.test(expression[i]))
|
|
10622
|
+
ident += expression[i++];
|
|
10623
|
+
if (FUNCTION_NAMES.has(ident.toUpperCase())) {
|
|
10624
|
+
tokens.push({ type: "function", value: ident, rawValue: ident });
|
|
10625
|
+
continue;
|
|
10626
|
+
}
|
|
10627
|
+
if (mode === "plain") {
|
|
10628
|
+
const field = fieldMap.get(ident);
|
|
10629
|
+
tokens.push({
|
|
10630
|
+
type: "field",
|
|
10631
|
+
value: field ? field.label || field.fieldName || ident : ident,
|
|
10632
|
+
rawValue: ident,
|
|
10633
|
+
fieldRef: ident
|
|
10634
|
+
});
|
|
10635
|
+
continue;
|
|
10636
|
+
}
|
|
10637
|
+
tokens.push({ type: "unknown", value: ident, rawValue: ident });
|
|
10638
|
+
continue;
|
|
10639
|
+
}
|
|
10640
|
+
tokens.push({ type: "unknown", value: ch, rawValue: ch });
|
|
10641
|
+
i++;
|
|
10642
|
+
}
|
|
10643
|
+
return tokens;
|
|
10644
|
+
}
|
|
10645
|
+
function buildFieldMap(fields) {
|
|
10646
|
+
const map = /* @__PURE__ */ new Map();
|
|
10647
|
+
for (const f of fields) {
|
|
10648
|
+
if (f.fieldName)
|
|
10649
|
+
map.set(f.fieldName, f);
|
|
10650
|
+
if (f.id && f.id !== f.fieldName)
|
|
10651
|
+
map.set(f.id, f);
|
|
10652
|
+
}
|
|
10653
|
+
return map;
|
|
10654
|
+
}
|
|
10655
|
+
function getFieldRef(field) {
|
|
10656
|
+
return field.fieldName && field.fieldName !== field.id ? field.fieldName : field.id;
|
|
10657
|
+
}
|
|
10658
|
+
|
|
10659
|
+
// src/builder/FormulaEditorWidget.ts
|
|
10660
|
+
var _stylesInjected = false;
|
|
10661
|
+
var STYLE_ID = "formula-editor-widget-styles-v2";
|
|
10662
|
+
function injectStyles() {
|
|
10663
|
+
if (_stylesInjected && document.getElementById(STYLE_ID))
|
|
10664
|
+
return;
|
|
10665
|
+
document.getElementById(STYLE_ID)?.remove();
|
|
10666
|
+
document.getElementById("formula-editor-widget-styles")?.remove();
|
|
10667
|
+
_stylesInjected = true;
|
|
10668
|
+
const style = document.createElement("style");
|
|
10669
|
+
style.id = STYLE_ID;
|
|
10670
|
+
style.textContent = `
|
|
10671
|
+
/* \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
|
|
10672
|
+
FormulaEditorWidget v2
|
|
10673
|
+
\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 */
|
|
10674
|
+
|
|
10675
|
+
/* \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 */
|
|
10676
|
+
.few-container {
|
|
10677
|
+
position: relative;
|
|
10678
|
+
width: 100%;
|
|
10679
|
+
}
|
|
10680
|
+
|
|
10681
|
+
/* \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 */
|
|
10682
|
+
/*
|
|
10683
|
+
* NOTE: color uses !important so it is never muted by
|
|
10684
|
+
* Angular ViewEncapsulation, host-element rules, or global resets.
|
|
10685
|
+
* This ensures operators/numbers always render at full text contrast.
|
|
10686
|
+
*/
|
|
10687
|
+
.few-editor {
|
|
10688
|
+
display: block;
|
|
10689
|
+
width: 100%;
|
|
10690
|
+
min-height: 38px;
|
|
10691
|
+
padding: 6px 32px 6px 12px; /* right padding reserves space for clear btn */
|
|
10692
|
+
border: 1px solid #e2e8f0;
|
|
10693
|
+
border-radius: 6px;
|
|
10694
|
+
font-family: ui-monospace, 'Cascadia Code', 'Source Code Pro', Menlo, monospace;
|
|
10695
|
+
font-size: 13px;
|
|
10696
|
+
font-weight: 700 !important; /* \u2190 operators/numbers always bold */
|
|
10697
|
+
line-height: 1.7;
|
|
10698
|
+
color: rgb(213, 12, 65) !important; /* operator/text color \u2014 chips override with their own !important */
|
|
10699
|
+
caret-color: #635bff;
|
|
10700
|
+
background: transparent;
|
|
10701
|
+
outline: none;
|
|
10702
|
+
word-break: break-word;
|
|
10703
|
+
white-space: pre-wrap;
|
|
10704
|
+
cursor: text;
|
|
10705
|
+
transition: border-color 0.15s, box-shadow 0.15s;
|
|
10706
|
+
box-sizing: border-box;
|
|
10707
|
+
}
|
|
10708
|
+
|
|
10709
|
+
.few-editor:focus {
|
|
10710
|
+
border-color: #635bff;
|
|
10711
|
+
box-shadow: 0 0 0 3px rgba(99, 91, 255, 0.15);
|
|
10712
|
+
}
|
|
10713
|
+
|
|
10714
|
+
.few-editor.few-has-error {
|
|
10715
|
+
border-color: #ef4444;
|
|
10716
|
+
box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.12);
|
|
10717
|
+
}
|
|
10718
|
+
|
|
10719
|
+
/* \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 */
|
|
10720
|
+
.few-placeholder {
|
|
10721
|
+
position: absolute;
|
|
10722
|
+
top: 0;
|
|
10723
|
+
left: 0;
|
|
10724
|
+
right: 32px; /* don't overlap clear button */
|
|
10725
|
+
bottom: 0;
|
|
10726
|
+
padding: 6px 0 6px 12px;
|
|
10727
|
+
font-family: ui-monospace, 'Cascadia Code', 'Source Code Pro', Menlo, monospace;
|
|
10728
|
+
font-size: 13px;
|
|
10729
|
+
line-height: 1.7;
|
|
10730
|
+
color: #94a3b8;
|
|
10731
|
+
pointer-events: none;
|
|
10732
|
+
user-select: none;
|
|
10733
|
+
white-space: nowrap;
|
|
10734
|
+
overflow: hidden;
|
|
10735
|
+
text-overflow: ellipsis;
|
|
10736
|
+
}
|
|
10737
|
+
|
|
10738
|
+
/* \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 */
|
|
10739
|
+
.few-chip {
|
|
10740
|
+
display: inline-flex;
|
|
10741
|
+
align-items: center;
|
|
10742
|
+
padding: 0px 7px 1px 6px;
|
|
10743
|
+
margin: 0 2px;
|
|
10744
|
+
font-size: 11.5px;
|
|
10745
|
+
font-weight: 600 !important; /* explicit !important so the editor's 700 !important doesn't cascade in */
|
|
10746
|
+
font-family: ui-sans-serif, system-ui, sans-serif;
|
|
10747
|
+
letter-spacing: 0.01em;
|
|
10748
|
+
border-radius: 4px;
|
|
10749
|
+
background: rgba(99, 91, 255, 0.10);
|
|
10750
|
+
color: #635bff !important; /* chip label always purple \u2014 not inherited */
|
|
10751
|
+
border: 1px solid rgba(99, 91, 255, 0.28);
|
|
10752
|
+
cursor: default;
|
|
10753
|
+
user-select: none;
|
|
10754
|
+
vertical-align: middle;
|
|
10755
|
+
line-height: 1.6;
|
|
10756
|
+
white-space: nowrap;
|
|
10757
|
+
transition: background 0.1s;
|
|
10758
|
+
}
|
|
10759
|
+
|
|
10760
|
+
.few-chip:hover {
|
|
10761
|
+
background: rgba(99, 91, 255, 0.17);
|
|
10762
|
+
}
|
|
10763
|
+
|
|
10764
|
+
.few-chip-unknown {
|
|
10765
|
+
background: rgba(239, 68, 68, 0.08);
|
|
10766
|
+
color: #dc2626 !important;
|
|
10767
|
+
border-color: rgba(239, 68, 68, 0.25);
|
|
10768
|
+
}
|
|
10769
|
+
|
|
10770
|
+
/* \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 */
|
|
10771
|
+
.few-clear-btn {
|
|
10772
|
+
position: absolute;
|
|
10773
|
+
top: 50%;
|
|
10774
|
+
right: 7px;
|
|
10775
|
+
transform: translateY(-50%);
|
|
10776
|
+
display: flex;
|
|
10777
|
+
align-items: center;
|
|
10778
|
+
justify-content: center;
|
|
10779
|
+
width: 18px;
|
|
10780
|
+
height: 18px;
|
|
10781
|
+
padding: 0;
|
|
10782
|
+
border: none;
|
|
10783
|
+
border-radius: 50%;
|
|
10784
|
+
background: #94a3b8;
|
|
10785
|
+
color: #ffffff;
|
|
10786
|
+
font-size: 12px;
|
|
10787
|
+
line-height: 1;
|
|
10788
|
+
font-weight: 700;
|
|
10789
|
+
cursor: pointer;
|
|
10790
|
+
opacity: 0;
|
|
10791
|
+
pointer-events: none;
|
|
10792
|
+
transition: opacity 0.15s, background 0.15s;
|
|
10793
|
+
z-index: 2;
|
|
10794
|
+
}
|
|
10795
|
+
|
|
10796
|
+
.few-clear-btn:hover {
|
|
10797
|
+
background: #475569;
|
|
10798
|
+
}
|
|
10799
|
+
|
|
10800
|
+
/* Shown only when the editor has content */
|
|
10801
|
+
.few-container.few-has-content .few-clear-btn {
|
|
10802
|
+
opacity: 1;
|
|
10803
|
+
pointer-events: auto;
|
|
10804
|
+
}
|
|
10805
|
+
|
|
10806
|
+
/* \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 */
|
|
10807
|
+
@media (prefers-color-scheme: dark) {
|
|
10808
|
+
.few-editor {
|
|
10809
|
+
border-color: #334155;
|
|
10810
|
+
/* operator color is set via inline !important \u2014 not overridden here */
|
|
10811
|
+
}
|
|
10812
|
+
.few-editor:focus {
|
|
10813
|
+
border-color: #635bff;
|
|
10814
|
+
box-shadow: 0 0 0 3px rgba(99, 91, 255, 0.25);
|
|
10815
|
+
}
|
|
10816
|
+
.few-placeholder { color: #64748b; }
|
|
10817
|
+
.few-chip {
|
|
10818
|
+
background: rgba(99, 91, 255, 0.18);
|
|
10819
|
+
color: #a5b4fc !important;
|
|
10820
|
+
border-color: rgba(99, 91, 255, 0.38);
|
|
10821
|
+
}
|
|
10822
|
+
.few-chip-unknown {
|
|
10823
|
+
background: rgba(239, 68, 68, 0.15);
|
|
10824
|
+
color: #f87171 !important;
|
|
10825
|
+
border-color: rgba(239, 68, 68, 0.35);
|
|
10826
|
+
}
|
|
10827
|
+
.few-clear-btn {
|
|
10828
|
+
background: #64748b;
|
|
10829
|
+
color: #f1f5f9;
|
|
10830
|
+
}
|
|
10831
|
+
.few-clear-btn:hover { background: #475569; }
|
|
10832
|
+
}
|
|
10833
|
+
|
|
10834
|
+
/* \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 */
|
|
10835
|
+
.dark .few-editor {
|
|
10836
|
+
border-color: #334155;
|
|
10837
|
+
/* operator color is set via inline !important \u2014 not overridden here */
|
|
10838
|
+
}
|
|
10839
|
+
.dark .few-editor:focus {
|
|
10840
|
+
border-color: #635bff;
|
|
10841
|
+
box-shadow: 0 0 0 3px rgba(99, 91, 255, 0.25);
|
|
10842
|
+
}
|
|
10843
|
+
.dark .few-placeholder { color: #64748b; }
|
|
10844
|
+
.dark .few-chip {
|
|
10845
|
+
background: rgba(99, 91, 255, 0.18);
|
|
10846
|
+
color: #a5b4fc !important;
|
|
10847
|
+
border-color: rgba(99, 91, 255, 0.38);
|
|
10848
|
+
}
|
|
10849
|
+
.dark .few-chip-unknown {
|
|
10850
|
+
background: rgba(239, 68, 68, 0.15);
|
|
10851
|
+
color: #f87171 !important;
|
|
10852
|
+
border-color: rgba(239, 68, 68, 0.35);
|
|
10853
|
+
}
|
|
10854
|
+
.dark .few-clear-btn {
|
|
10855
|
+
background: #64748b;
|
|
10856
|
+
color: #f1f5f9;
|
|
10857
|
+
}
|
|
10858
|
+
.dark .few-clear-btn:hover { background: #475569; }
|
|
10859
|
+
`;
|
|
10860
|
+
document.head.appendChild(style);
|
|
10861
|
+
}
|
|
10862
|
+
var FormulaEditorWidget = class {
|
|
10863
|
+
constructor(options) {
|
|
10864
|
+
__publicField(this, "_container");
|
|
10865
|
+
__publicField(this, "_editor");
|
|
10866
|
+
__publicField(this, "_placeholderEl");
|
|
10867
|
+
__publicField(this, "_clearBtn");
|
|
10868
|
+
__publicField(this, "_options");
|
|
10869
|
+
__publicField(this, "_fieldMap");
|
|
10870
|
+
/**
|
|
10871
|
+
* The canonical internal expression — SINGLE SOURCE OF TRUTH.
|
|
10872
|
+
* The DOM is always derived from this; never the reverse.
|
|
10873
|
+
* Updated by _onEditorInput() (user edits) and setValue() (programmatic).
|
|
10874
|
+
*/
|
|
10875
|
+
__publicField(this, "_internalValue", "");
|
|
10876
|
+
injectStyles();
|
|
10877
|
+
this._options = { ...options };
|
|
10878
|
+
this._fieldMap = buildFieldMap(options.availableFields);
|
|
10879
|
+
this._container = document.createElement("div");
|
|
10880
|
+
this._container.className = "few-container";
|
|
10881
|
+
this._placeholderEl = document.createElement("div");
|
|
10882
|
+
this._placeholderEl.className = "few-placeholder";
|
|
10883
|
+
this._placeholderEl.textContent = options.placeholder ?? "Enter formula\u2026";
|
|
10884
|
+
this._editor = document.createElement("div");
|
|
10885
|
+
this._editor.className = "few-editor";
|
|
10886
|
+
this._editor.contentEditable = "true";
|
|
10887
|
+
this._editor.spellcheck = false;
|
|
10888
|
+
this._editor.setAttribute("autocomplete", "off");
|
|
10889
|
+
this._editor.setAttribute("autocorrect", "off");
|
|
10890
|
+
this._editor.setAttribute("autocapitalize", "off");
|
|
10891
|
+
this._editor.setAttribute("data-formula-editor", "true");
|
|
10892
|
+
this._editor.style.setProperty("color", "rgb(213, 12, 65)", "important");
|
|
10893
|
+
this._editor.style.setProperty("font-weight", "700", "important");
|
|
10894
|
+
this._editor.style.setProperty("caret-color", "#635bff", "important");
|
|
10895
|
+
this._clearBtn = document.createElement("button");
|
|
10896
|
+
this._clearBtn.type = "button";
|
|
10897
|
+
this._clearBtn.className = "few-clear-btn";
|
|
10898
|
+
this._clearBtn.title = "Clear formula";
|
|
10899
|
+
this._clearBtn.textContent = "\xD7";
|
|
10900
|
+
this._clearBtn.addEventListener("mousedown", (e) => {
|
|
10901
|
+
e.preventDefault();
|
|
10902
|
+
this.clear();
|
|
10903
|
+
});
|
|
10904
|
+
this._container.appendChild(this._placeholderEl);
|
|
10905
|
+
this._container.appendChild(this._editor);
|
|
10906
|
+
this._container.appendChild(this._clearBtn);
|
|
10907
|
+
this._attachEvents();
|
|
10908
|
+
if (options.initialValue) {
|
|
10909
|
+
this.setValue(options.initialValue);
|
|
10910
|
+
} else {
|
|
10911
|
+
this._syncUI();
|
|
10912
|
+
}
|
|
10913
|
+
}
|
|
10914
|
+
// ─── Internal event wiring ───────────────────────────────────────────────
|
|
10915
|
+
_attachEvents() {
|
|
10916
|
+
this._editor.addEventListener("input", () => this._onEditorInput());
|
|
10917
|
+
this._editor.addEventListener("keydown", (e) => {
|
|
10918
|
+
if (e.key === "Enter") {
|
|
10919
|
+
e.preventDefault();
|
|
10920
|
+
return;
|
|
10921
|
+
}
|
|
10922
|
+
if (e.key === "Backspace")
|
|
10923
|
+
this._handleBackspaceOnChip(e);
|
|
10924
|
+
});
|
|
10925
|
+
this._editor.addEventListener("paste", (e) => {
|
|
10926
|
+
e.preventDefault();
|
|
10927
|
+
const text = e.clipboardData?.getData("text/plain") ?? "";
|
|
10928
|
+
if (text) {
|
|
10929
|
+
this._insertTextAtCaret(text);
|
|
10930
|
+
this._onEditorInput();
|
|
10931
|
+
}
|
|
10932
|
+
});
|
|
10933
|
+
this._editor.addEventListener("dragover", (e) => e.preventDefault());
|
|
10934
|
+
this._editor.addEventListener("drop", (e) => e.preventDefault());
|
|
10935
|
+
}
|
|
10936
|
+
// ─── Core edit cycle ─────────────────────────────────────────────────────
|
|
10937
|
+
_onEditorInput() {
|
|
10938
|
+
this._normaliseDom();
|
|
10939
|
+
const newValue = this._readInternalExpression();
|
|
10940
|
+
const changed = newValue !== this._internalValue;
|
|
10941
|
+
this._internalValue = newValue;
|
|
10942
|
+
this._syncUI();
|
|
10943
|
+
if (changed) {
|
|
10944
|
+
this._options.onChange?.(newValue);
|
|
10945
|
+
}
|
|
10946
|
+
}
|
|
10947
|
+
/**
|
|
10948
|
+
* Remove browser-inserted artefacts (<br>, stray <div>/<span>) that some
|
|
10949
|
+
* browsers inject into a contenteditable. Chips (data-field-ref) are kept.
|
|
10950
|
+
*/
|
|
10951
|
+
_normaliseDom() {
|
|
10952
|
+
for (const node of Array.from(this._editor.childNodes)) {
|
|
10953
|
+
if (node.nodeType !== Node.ELEMENT_NODE)
|
|
10954
|
+
continue;
|
|
10955
|
+
const el = node;
|
|
10956
|
+
if (el.hasAttribute("data-field-ref"))
|
|
10957
|
+
continue;
|
|
10958
|
+
if (el.tagName === "BR") {
|
|
10959
|
+
el.remove();
|
|
10960
|
+
continue;
|
|
10961
|
+
}
|
|
10962
|
+
const textNode = document.createTextNode(el.textContent ?? "");
|
|
10963
|
+
this._editor.replaceChild(textNode, el);
|
|
10964
|
+
}
|
|
10965
|
+
}
|
|
10966
|
+
/**
|
|
10967
|
+
* Backspace immediately before a chip should delete the whole chip, not
|
|
10968
|
+
* step into its non-editable content.
|
|
10969
|
+
*/
|
|
10970
|
+
_handleBackspaceOnChip(e) {
|
|
10971
|
+
const sel = window.getSelection();
|
|
10972
|
+
if (!sel || sel.rangeCount === 0)
|
|
10973
|
+
return;
|
|
10974
|
+
const range = sel.getRangeAt(0);
|
|
10975
|
+
if (!range.collapsed)
|
|
10976
|
+
return;
|
|
10977
|
+
let prev = null;
|
|
10978
|
+
if (range.startContainer === this._editor) {
|
|
10979
|
+
if (range.startOffset > 0)
|
|
10980
|
+
prev = this._editor.childNodes[range.startOffset - 1];
|
|
10981
|
+
} else if (range.startContainer.nodeType === Node.TEXT_NODE) {
|
|
10982
|
+
if (range.startOffset === 0)
|
|
10983
|
+
prev = range.startContainer.previousSibling;
|
|
10984
|
+
}
|
|
10985
|
+
if (prev && prev.hasAttribute?.("data-field-ref")) {
|
|
10986
|
+
e.preventDefault();
|
|
10987
|
+
prev.remove();
|
|
10988
|
+
this._onEditorInput();
|
|
10989
|
+
}
|
|
10990
|
+
}
|
|
10991
|
+
// ─── Serialisation: DOM → internal expression ────────────────────────────
|
|
10992
|
+
_readInternalExpression() {
|
|
10993
|
+
let result = "";
|
|
10994
|
+
for (const node of Array.from(this._editor.childNodes)) {
|
|
10995
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
10996
|
+
result += node.textContent ?? "";
|
|
10997
|
+
} else if (node.nodeType === Node.ELEMENT_NODE) {
|
|
10998
|
+
const el = node;
|
|
10999
|
+
const ref = el.getAttribute("data-field-ref");
|
|
11000
|
+
if (ref !== null) {
|
|
11001
|
+
result += this._options.mode === "bracket" ? `{${ref}}` : ref;
|
|
11002
|
+
} else {
|
|
11003
|
+
result += el.textContent ?? "";
|
|
11004
|
+
}
|
|
11005
|
+
}
|
|
11006
|
+
}
|
|
11007
|
+
return result;
|
|
11008
|
+
}
|
|
11009
|
+
// ─── Deserialization: internal expression → DOM ──────────────────────────
|
|
11010
|
+
/**
|
|
11011
|
+
* Load an internal expression string.
|
|
11012
|
+
*
|
|
11013
|
+
* This is the ONLY path that populates the editor with chips.
|
|
11014
|
+
* It does NOT fire onChange (programmatic set ≠ user edit).
|
|
11015
|
+
*
|
|
11016
|
+
* Design invariant: the DOM is ALWAYS fully rebuilt from _internalValue,
|
|
11017
|
+
* never from user-visible text. This guarantees correct state on re-render
|
|
11018
|
+
* after field switches, undo/redo, or hot-reload.
|
|
11019
|
+
*/
|
|
11020
|
+
setValue(internalExpression) {
|
|
11021
|
+
this._internalValue = internalExpression ?? "";
|
|
11022
|
+
this._editor.innerHTML = "";
|
|
11023
|
+
if (this._internalValue.trim()) {
|
|
11024
|
+
const tokens = parseExpressionToTokens(
|
|
11025
|
+
this._internalValue,
|
|
11026
|
+
this._fieldMap,
|
|
11027
|
+
this._options.mode
|
|
11028
|
+
);
|
|
11029
|
+
for (const token of tokens) {
|
|
11030
|
+
if (token.type === "field" && token.fieldRef !== void 0) {
|
|
11031
|
+
const field = this._fieldMap.get(token.fieldRef) ?? {
|
|
11032
|
+
id: token.fieldRef,
|
|
11033
|
+
fieldName: token.fieldRef,
|
|
11034
|
+
label: token.fieldRef
|
|
11035
|
+
// shows raw ref — still readable
|
|
11036
|
+
};
|
|
11037
|
+
const isUnknown = !this._fieldMap.has(token.fieldRef);
|
|
11038
|
+
this._editor.appendChild(this._createChipEl(field, isUnknown));
|
|
11039
|
+
} else {
|
|
11040
|
+
this._editor.appendChild(document.createTextNode(token.value));
|
|
11041
|
+
}
|
|
11042
|
+
}
|
|
11043
|
+
}
|
|
11044
|
+
this._syncUI();
|
|
11045
|
+
}
|
|
11046
|
+
/** Return the current stored expression. Always reflects live DOM state. */
|
|
11047
|
+
getValue() {
|
|
11048
|
+
return this._readInternalExpression();
|
|
11049
|
+
}
|
|
11050
|
+
// ─── Public mutation ─────────────────────────────────────────────────────
|
|
11051
|
+
/**
|
|
11052
|
+
* Insert a field chip at the caret (or append if editor lacks focus).
|
|
11053
|
+
* Handles spacing automatically so chips never run into adjacent text.
|
|
11054
|
+
*/
|
|
11055
|
+
insertField(field) {
|
|
11056
|
+
this._editor.focus();
|
|
11057
|
+
const chip = this._createChipEl(field, false);
|
|
11058
|
+
const sel = window.getSelection();
|
|
11059
|
+
const inEditor = sel && sel.rangeCount > 0 && this._editor.contains(sel.getRangeAt(0).commonAncestorContainer);
|
|
11060
|
+
if (inEditor) {
|
|
11061
|
+
const range = sel.getRangeAt(0);
|
|
11062
|
+
range.deleteContents();
|
|
11063
|
+
if (this._needsSpaceBefore(range)) {
|
|
11064
|
+
range.insertNode(document.createTextNode(" "));
|
|
11065
|
+
range.collapse(false);
|
|
11066
|
+
}
|
|
11067
|
+
range.insertNode(chip);
|
|
11068
|
+
const spaceAfter = document.createTextNode(" ");
|
|
11069
|
+
range.setStartAfter(chip);
|
|
11070
|
+
range.setEndAfter(chip);
|
|
11071
|
+
range.insertNode(spaceAfter);
|
|
11072
|
+
range.setStartAfter(spaceAfter);
|
|
11073
|
+
range.setEndAfter(spaceAfter);
|
|
11074
|
+
sel.removeAllRanges();
|
|
11075
|
+
sel.addRange(range);
|
|
11076
|
+
} else {
|
|
11077
|
+
this._appendAtEnd(chip);
|
|
11078
|
+
}
|
|
11079
|
+
this._onEditorInput();
|
|
11080
|
+
}
|
|
11081
|
+
/**
|
|
11082
|
+
* Insert plain text at the caret (operators, function names, parens).
|
|
11083
|
+
* Operators rendered by this path are text nodes and always inherit
|
|
11084
|
+
* .few-editor's high-contrast color.
|
|
11085
|
+
*/
|
|
11086
|
+
insertText(text) {
|
|
11087
|
+
this._editor.focus();
|
|
11088
|
+
this._insertTextAtCaret(text);
|
|
11089
|
+
this._onEditorInput();
|
|
11090
|
+
}
|
|
11091
|
+
/**
|
|
11092
|
+
* Clear the entire formula expression.
|
|
11093
|
+
* Fires onChange('') so the store is updated immediately.
|
|
11094
|
+
*/
|
|
11095
|
+
clear() {
|
|
11096
|
+
this._editor.innerHTML = "";
|
|
11097
|
+
this._internalValue = "";
|
|
11098
|
+
this._syncUI();
|
|
11099
|
+
this._options.onChange?.("");
|
|
11100
|
+
}
|
|
11101
|
+
// ─── Visual state ────────────────────────────────────────────────────────
|
|
11102
|
+
/** Toggle error (red border) state. */
|
|
11103
|
+
setError(hasError) {
|
|
11104
|
+
this._editor.classList.toggle("few-has-error", hasError);
|
|
11105
|
+
}
|
|
11106
|
+
/**
|
|
11107
|
+
* Refresh the field map with a new field list (e.g. schema changed).
|
|
11108
|
+
* Re-renders from _internalValue so stale labels / unknowns are resolved.
|
|
11109
|
+
*/
|
|
11110
|
+
updateFields(fields) {
|
|
11111
|
+
this._options.availableFields = fields;
|
|
11112
|
+
this._fieldMap = buildFieldMap(fields);
|
|
11113
|
+
this.setValue(this._internalValue);
|
|
11114
|
+
}
|
|
11115
|
+
/** Return the root element to mount. */
|
|
11116
|
+
getElement() {
|
|
11117
|
+
return this._container;
|
|
11118
|
+
}
|
|
11119
|
+
/** Focus the editable area. */
|
|
11120
|
+
focus() {
|
|
11121
|
+
this._editor.focus();
|
|
11122
|
+
}
|
|
11123
|
+
/** Remove from DOM. */
|
|
11124
|
+
destroy() {
|
|
11125
|
+
this._container.remove();
|
|
11126
|
+
}
|
|
11127
|
+
// ─── Private helpers ─────────────────────────────────────────────────────
|
|
11128
|
+
_insertTextAtCaret(text) {
|
|
11129
|
+
const sel = window.getSelection();
|
|
11130
|
+
const inEditor = sel && sel.rangeCount > 0 && this._editor.contains(sel.getRangeAt(0).commonAncestorContainer);
|
|
11131
|
+
if (inEditor) {
|
|
11132
|
+
const range = sel.getRangeAt(0);
|
|
11133
|
+
range.deleteContents();
|
|
11134
|
+
const tn = document.createTextNode(text);
|
|
11135
|
+
range.insertNode(tn);
|
|
11136
|
+
range.setStartAfter(tn);
|
|
11137
|
+
range.setEndAfter(tn);
|
|
11138
|
+
sel.removeAllRanges();
|
|
11139
|
+
sel.addRange(range);
|
|
11140
|
+
} else {
|
|
11141
|
+
this._editor.appendChild(document.createTextNode(text));
|
|
11142
|
+
}
|
|
11143
|
+
}
|
|
11144
|
+
_appendAtEnd(chip) {
|
|
11145
|
+
const last = this._editor.lastChild;
|
|
11146
|
+
if (last) {
|
|
11147
|
+
if (last.nodeType === Node.TEXT_NODE) {
|
|
11148
|
+
const txt = last.textContent ?? "";
|
|
11149
|
+
if (txt.length > 0 && !/\s$/.test(txt)) {
|
|
11150
|
+
this._editor.appendChild(document.createTextNode(" "));
|
|
11151
|
+
}
|
|
11152
|
+
} else if (last.hasAttribute?.("data-field-ref")) {
|
|
11153
|
+
this._editor.appendChild(document.createTextNode(" "));
|
|
11154
|
+
}
|
|
11155
|
+
}
|
|
11156
|
+
this._editor.appendChild(chip);
|
|
11157
|
+
this._editor.appendChild(document.createTextNode(" "));
|
|
11158
|
+
}
|
|
11159
|
+
_needsSpaceBefore(range) {
|
|
11160
|
+
const { startContainer, startOffset } = range;
|
|
11161
|
+
if (startContainer.nodeType === Node.TEXT_NODE) {
|
|
11162
|
+
const text = startContainer.textContent ?? "";
|
|
11163
|
+
const ch = text[startOffset - 1];
|
|
11164
|
+
return startOffset > 0 && ch !== void 0 && !/[\s(]/.test(ch);
|
|
11165
|
+
}
|
|
11166
|
+
if (startContainer === this._editor && startOffset > 0) {
|
|
11167
|
+
const prev = this._editor.childNodes[startOffset - 1];
|
|
11168
|
+
return prev?.hasAttribute?.("data-field-ref") ?? false;
|
|
11169
|
+
}
|
|
11170
|
+
return false;
|
|
11171
|
+
}
|
|
11172
|
+
_createChipEl(field, isUnknown) {
|
|
11173
|
+
const chip = document.createElement("span");
|
|
11174
|
+
chip.contentEditable = "false";
|
|
11175
|
+
chip.setAttribute("data-field-ref", getFieldRef(field));
|
|
11176
|
+
chip.textContent = field.label || field.fieldName || field.id;
|
|
11177
|
+
chip.className = isUnknown ? "few-chip few-chip-unknown" : "few-chip";
|
|
11178
|
+
chip.title = this._options.mode === "bracket" ? `{${getFieldRef(field)}}` : getFieldRef(field);
|
|
11179
|
+
return chip;
|
|
11180
|
+
}
|
|
11181
|
+
/**
|
|
11182
|
+
* Synchronise all purely-visual state:
|
|
11183
|
+
* - placeholder visibility
|
|
11184
|
+
* - clear-button visibility (few-has-content on container)
|
|
11185
|
+
*/
|
|
11186
|
+
_syncUI() {
|
|
11187
|
+
const hasContent = this._internalValue.trim().length > 0 || !!this._editor.querySelector("[data-field-ref]");
|
|
11188
|
+
this._placeholderEl.style.display = hasContent ? "none" : "block";
|
|
11189
|
+
this._container.classList.toggle("few-has-content", hasContent);
|
|
11190
|
+
this._editor.style.setProperty("color", "rgb(213, 12, 65)", "important");
|
|
11191
|
+
this._editor.style.setProperty("font-weight", "700", "important");
|
|
11192
|
+
}
|
|
11193
|
+
};
|
|
11194
|
+
|
|
10510
11195
|
// src/builder/FormBuilder.ts
|
|
10511
11196
|
var advancedCssPanelState = /* @__PURE__ */ new Map();
|
|
10512
|
-
var
|
|
11197
|
+
var lastFocusedFormulaWidget = null;
|
|
10513
11198
|
var LABEL_DEBOUNCE_MS = 300;
|
|
10514
11199
|
var labelUpdateTimeouts = /* @__PURE__ */ new Map();
|
|
10515
11200
|
var FormBuilder = class {
|
|
@@ -10988,8 +11673,15 @@ var FormBuilder = class {
|
|
|
10988
11673
|
if (field.type !== "formula" || !field.formulaConfig)
|
|
10989
11674
|
continue;
|
|
10990
11675
|
const fcfg = field.formulaConfig;
|
|
10991
|
-
const
|
|
10992
|
-
const
|
|
11676
|
+
const allOtherFields = schema.sections.flatMap((s) => s.fields).filter((f) => f.id !== field.id);
|
|
11677
|
+
const validRefSet = /* @__PURE__ */ new Set();
|
|
11678
|
+
allOtherFields.forEach((f) => {
|
|
11679
|
+
if (f.fieldName)
|
|
11680
|
+
validRefSet.add(f.fieldName);
|
|
11681
|
+
if (f.id)
|
|
11682
|
+
validRefSet.add(f.id);
|
|
11683
|
+
});
|
|
11684
|
+
const validRefs = Array.from(validRefSet);
|
|
10993
11685
|
const exprs = [];
|
|
10994
11686
|
if (fcfg.mode === "single") {
|
|
10995
11687
|
if (fcfg.single?.expression)
|
|
@@ -11003,7 +11695,7 @@ var FormBuilder = class {
|
|
|
11003
11695
|
exprs.push(fcfg.multiple.fallbackExpression);
|
|
11004
11696
|
}
|
|
11005
11697
|
for (const expr of exprs) {
|
|
11006
|
-
const result = validateFormulaExpression(expr,
|
|
11698
|
+
const result = validateFormulaExpression(expr, validRefs);
|
|
11007
11699
|
if (!result.valid) {
|
|
11008
11700
|
alert(`Formula error in "${field.label}": ${result.error}`);
|
|
11009
11701
|
return;
|
|
@@ -11761,38 +12453,64 @@ var FormBuilder = class {
|
|
|
11761
12453
|
const numericFields = getNumericFieldsForFormula(schema, selectedField.id);
|
|
11762
12454
|
const availableIds = numericFields.map((f) => f.id);
|
|
11763
12455
|
const availableNames = numericFields.map((f) => f.fieldName);
|
|
12456
|
+
const allFieldInfosForWidget = schema.sections.flatMap((s) => s.fields).filter((f) => f.id !== selectedField.id).map((f) => ({
|
|
12457
|
+
id: f.id,
|
|
12458
|
+
fieldName: f.fieldName ?? f.id,
|
|
12459
|
+
label: f.label || f.fieldName || f.id
|
|
12460
|
+
}));
|
|
12461
|
+
const numFieldInfos = numericFields.map((f) => ({
|
|
12462
|
+
id: f.id,
|
|
12463
|
+
fieldName: f.fieldName,
|
|
12464
|
+
label: f.label || f.fieldName || f.id
|
|
12465
|
+
}));
|
|
11764
12466
|
const formulaGroup = createElement("div", { className: "mb-3" });
|
|
11765
|
-
formulaGroup.appendChild(createElement("label", {
|
|
11766
|
-
|
|
11767
|
-
|
|
11768
|
-
|
|
11769
|
-
|
|
12467
|
+
formulaGroup.appendChild(createElement("label", {
|
|
12468
|
+
className: "block text-sm font-normal text-gray-700 dark:text-gray-300 mb-1",
|
|
12469
|
+
text: "Formula"
|
|
12470
|
+
}));
|
|
12471
|
+
const numFormulaWidget = new FormulaEditorWidget({
|
|
12472
|
+
mode: "plain",
|
|
12473
|
+
availableFields: allFieldInfosForWidget,
|
|
12474
|
+
// Always read from the store snapshot — guarantees correct reload
|
|
12475
|
+
// after field switch and re-render.
|
|
12476
|
+
initialValue: selectedField.formula || "",
|
|
11770
12477
|
placeholder: "e.g. quantity * price",
|
|
11771
|
-
|
|
11772
|
-
oninput: (e) => {
|
|
11773
|
-
const formula = e.target.value.trim();
|
|
12478
|
+
onChange: (formula) => {
|
|
11774
12479
|
const deps = parseFormulaDependencies(formula);
|
|
11775
|
-
const
|
|
11776
|
-
|
|
11777
|
-
|
|
11778
|
-
|
|
11779
|
-
if (
|
|
11780
|
-
|
|
11781
|
-
|
|
11782
|
-
|
|
11783
|
-
|
|
11784
|
-
|
|
11785
|
-
|
|
12480
|
+
const isValid2 = (() => {
|
|
12481
|
+
if (!formula.trim())
|
|
12482
|
+
return true;
|
|
12483
|
+
const v = validateFormula(formula, availableIds, availableNames, selectedField.id);
|
|
12484
|
+
if (!v.valid)
|
|
12485
|
+
return false;
|
|
12486
|
+
return !(deps.length > 0 && detectCircularDependency(schema, selectedField.id, formula, deps));
|
|
12487
|
+
})();
|
|
12488
|
+
numFormulaWidget.setError(!isValid2 && formula.trim().length > 0);
|
|
12489
|
+
if (!isValid2 && formula.trim()) {
|
|
12490
|
+
const v = validateFormula(formula, availableIds, availableNames, selectedField.id);
|
|
12491
|
+
formulaError.textContent = !v.valid ? v.error : "Circular dependency detected";
|
|
12492
|
+
formulaError.classList.remove("hidden");
|
|
12493
|
+
} else {
|
|
12494
|
+
formulaError.textContent = "";
|
|
12495
|
+
formulaError.classList.add("hidden");
|
|
11786
12496
|
}
|
|
11787
12497
|
formStore.getState().updateField(selectedField.id, { formula, dependencies: deps });
|
|
11788
12498
|
}
|
|
11789
12499
|
});
|
|
11790
|
-
|
|
11791
|
-
|
|
12500
|
+
numFormulaWidget.getElement().querySelector("[data-formula-editor]")?.addEventListener("focus", () => {
|
|
12501
|
+
lastFocusedFormulaWidget = numFormulaWidget;
|
|
12502
|
+
});
|
|
12503
|
+
formulaGroup.appendChild(numFormulaWidget.getElement());
|
|
12504
|
+
const formulaError = createElement("div", {
|
|
12505
|
+
className: "text-xs text-red-600 dark:text-red-400 mt-1 formula-error hidden"
|
|
12506
|
+
});
|
|
11792
12507
|
formulaGroup.appendChild(formulaError);
|
|
11793
12508
|
body.appendChild(formulaGroup);
|
|
11794
12509
|
const insertGroup = createElement("div", { className: "mb-3" });
|
|
11795
|
-
insertGroup.appendChild(createElement("label", {
|
|
12510
|
+
insertGroup.appendChild(createElement("label", {
|
|
12511
|
+
className: "block text-sm font-normal text-gray-700 dark:text-gray-300 mb-1",
|
|
12512
|
+
text: "Insert Field"
|
|
12513
|
+
}));
|
|
11796
12514
|
const insertSelect = createElement("select", {
|
|
11797
12515
|
className: "w-full px-3 py-2 border border-gray-200 dark:border-gray-700 rounded-md bg-transparent",
|
|
11798
12516
|
onchange: (e) => {
|
|
@@ -11800,34 +12518,58 @@ var FormBuilder = class {
|
|
|
11800
12518
|
const ref = sel.value;
|
|
11801
12519
|
if (!ref)
|
|
11802
12520
|
return;
|
|
11803
|
-
const
|
|
11804
|
-
|
|
11805
|
-
|
|
11806
|
-
|
|
11807
|
-
|
|
11808
|
-
dependencies: parseFormulaDependencies(newFormula)
|
|
11809
|
-
});
|
|
11810
|
-
formulaInput.value = newFormula;
|
|
12521
|
+
const field = numFieldInfos.find((f) => f.fieldName === ref || f.id === ref);
|
|
12522
|
+
if (field) {
|
|
12523
|
+
numFormulaWidget.insertField(field);
|
|
12524
|
+
lastFocusedFormulaWidget = numFormulaWidget;
|
|
12525
|
+
}
|
|
11811
12526
|
sel.value = "";
|
|
11812
|
-
this.render();
|
|
11813
12527
|
}
|
|
11814
12528
|
});
|
|
11815
|
-
insertSelect.appendChild(createElement("option", { value: "", text: "Select field to insert
|
|
11816
|
-
|
|
12529
|
+
insertSelect.appendChild(createElement("option", { value: "", text: "Select field to insert\u2026", selected: true }));
|
|
12530
|
+
numFieldInfos.forEach((f) => {
|
|
11817
12531
|
const ref = f.fieldName !== f.id ? f.fieldName : f.id;
|
|
11818
|
-
insertSelect.appendChild(createElement("option", { value: ref, text:
|
|
12532
|
+
insertSelect.appendChild(createElement("option", { value: ref, text: f.label }));
|
|
11819
12533
|
});
|
|
11820
12534
|
insertGroup.appendChild(insertSelect);
|
|
11821
12535
|
body.appendChild(insertGroup);
|
|
11822
|
-
|
|
11823
|
-
className: "text-
|
|
11824
|
-
text: "
|
|
12536
|
+
body.appendChild(createElement("label", {
|
|
12537
|
+
className: "block text-sm font-normal text-gray-700 dark:text-gray-300 mb-1",
|
|
12538
|
+
text: "Operators"
|
|
12539
|
+
}));
|
|
12540
|
+
const numMathWrap = createElement("div", { className: "flex flex-wrap gap-1 mb-3" });
|
|
12541
|
+
[
|
|
12542
|
+
{ text: "+", insert: " + " },
|
|
12543
|
+
{ text: "-", insert: " - " },
|
|
12544
|
+
{ text: "*", insert: " * " },
|
|
12545
|
+
{ text: "/", insert: " / " },
|
|
12546
|
+
{ text: "%", insert: " % " },
|
|
12547
|
+
{ text: "(", insert: "(" },
|
|
12548
|
+
{ text: ")", insert: ")" }
|
|
12549
|
+
].forEach((op) => {
|
|
12550
|
+
numMathWrap.appendChild(createElement("button", {
|
|
12551
|
+
type: "button",
|
|
12552
|
+
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",
|
|
12553
|
+
text: op.text,
|
|
12554
|
+
onclick: () => {
|
|
12555
|
+
(lastFocusedFormulaWidget ?? numFormulaWidget).insertText(op.insert);
|
|
12556
|
+
}
|
|
12557
|
+
}));
|
|
11825
12558
|
});
|
|
11826
|
-
body.appendChild(
|
|
12559
|
+
body.appendChild(numMathWrap);
|
|
12560
|
+
body.appendChild(createElement("p", {
|
|
12561
|
+
className: "text-xs text-gray-500 dark:text-gray-400 mb-2",
|
|
12562
|
+
text: "Fields shown as labels \u2014 stored as field references internally."
|
|
12563
|
+
}));
|
|
11827
12564
|
}
|
|
11828
12565
|
}
|
|
11829
12566
|
if (selectedField.type === "formula") {
|
|
11830
12567
|
const schema = formStore.getState().schema;
|
|
12568
|
+
const allSchemaFieldInfos = schema.sections.flatMap((s) => s.fields).filter((f) => f.id !== selectedField.id).map((f) => ({
|
|
12569
|
+
id: f.id,
|
|
12570
|
+
fieldName: f.fieldName ?? f.id,
|
|
12571
|
+
label: f.label || f.fieldName || f.id
|
|
12572
|
+
}));
|
|
11831
12573
|
const availableFields = getFieldsForFormula(schema, selectedField.id);
|
|
11832
12574
|
const cfg = selectedField.formulaConfig ?? {
|
|
11833
12575
|
mode: "single",
|
|
@@ -11880,35 +12622,43 @@ var FormBuilder = class {
|
|
|
11880
12622
|
modeRow.appendChild(multipleBtn);
|
|
11881
12623
|
modeGroup.appendChild(modeRow);
|
|
11882
12624
|
body.appendChild(modeGroup);
|
|
12625
|
+
const fwFieldInfos = allSchemaFieldInfos;
|
|
12626
|
+
lastFocusedFormulaWidget = null;
|
|
12627
|
+
const registerFormulaWidget = (widget) => {
|
|
12628
|
+
widget.getElement().querySelector("[data-formula-editor]")?.addEventListener("focus", () => {
|
|
12629
|
+
lastFocusedFormulaWidget = widget;
|
|
12630
|
+
});
|
|
12631
|
+
};
|
|
11883
12632
|
if (cfg.mode === "single") {
|
|
11884
12633
|
const exprGroup = createElement("div", { className: "mb-3" });
|
|
11885
12634
|
exprGroup.appendChild(createElement("label", {
|
|
11886
12635
|
className: "block text-sm font-normal text-gray-700 dark:text-gray-300 mb-1",
|
|
11887
12636
|
text: "Expression"
|
|
11888
12637
|
}));
|
|
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
12638
|
const exprError = createElement("div", { className: "text-xs text-red-500 mt-1 hidden" });
|
|
11899
|
-
|
|
11900
|
-
|
|
11901
|
-
|
|
11902
|
-
|
|
11903
|
-
|
|
11904
|
-
|
|
11905
|
-
|
|
11906
|
-
|
|
12639
|
+
const singleWidget = new FormulaEditorWidget({
|
|
12640
|
+
mode: "bracket",
|
|
12641
|
+
// Use ALL schema fields for the fieldMap so any ref resolves to its label
|
|
12642
|
+
availableFields: fwFieldInfos,
|
|
12643
|
+
// Source of truth: always read from persisted cfg, never from a
|
|
12644
|
+
// closure variable. This guarantees correct reload after field switch.
|
|
12645
|
+
initialValue: cfg.single?.expression ?? "",
|
|
12646
|
+
placeholder: "e.g. {fieldA} + {fieldB} * 1.18",
|
|
12647
|
+
onChange: (expr) => {
|
|
12648
|
+
const result = validateFormulaExpression(expr, fwFieldInfos.map((f) => f.fieldName));
|
|
12649
|
+
const isValid2 = result.valid || !expr.trim();
|
|
12650
|
+
singleWidget.setError(!isValid2);
|
|
12651
|
+
exprError.textContent = isValid2 ? "" : result.error;
|
|
12652
|
+
exprError.classList.toggle("hidden", isValid2);
|
|
12653
|
+
const freshCfg = formStore.getState().schema.sections.flatMap((s) => s.fields).find((f) => f.id === selectedField.id)?.formulaConfig ?? cfg;
|
|
12654
|
+
formStore.getState().updateField(selectedField.id, {
|
|
12655
|
+
formulaConfig: { ...freshCfg, single: { expression: expr } }
|
|
12656
|
+
});
|
|
11907
12657
|
}
|
|
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
12658
|
});
|
|
11911
|
-
|
|
12659
|
+
registerFormulaWidget(singleWidget);
|
|
12660
|
+
lastFocusedFormulaWidget = singleWidget;
|
|
12661
|
+
exprGroup.appendChild(singleWidget.getElement());
|
|
11912
12662
|
exprGroup.appendChild(exprError);
|
|
11913
12663
|
body.appendChild(exprGroup);
|
|
11914
12664
|
} else {
|
|
@@ -11935,16 +12685,15 @@ var FormBuilder = class {
|
|
|
11935
12685
|
});
|
|
11936
12686
|
compareGroup.appendChild(compareSelect);
|
|
11937
12687
|
body.appendChild(compareGroup);
|
|
11938
|
-
|
|
12688
|
+
body.appendChild(createElement("label", {
|
|
11939
12689
|
className: "block text-sm font-normal text-gray-700 dark:text-gray-300 mb-2",
|
|
11940
12690
|
text: "Conditions"
|
|
11941
|
-
});
|
|
11942
|
-
body.appendChild(conditionsLabel);
|
|
12691
|
+
}));
|
|
11943
12692
|
const conditions = cfg.multiple?.conditions ?? [];
|
|
11944
12693
|
conditions.forEach((cond, idx) => {
|
|
11945
12694
|
const row = createElement("div", { className: "mb-2 p-2 rounded-md border border-gray-100 dark:border-gray-800 space-y-1" });
|
|
11946
|
-
|
|
11947
|
-
|
|
12695
|
+
row.appendChild(createElement("div", { className: "text-xs text-gray-500 dark:text-gray-400", text: "When equals" }));
|
|
12696
|
+
row.appendChild(createElement("input", {
|
|
11948
12697
|
type: "text",
|
|
11949
12698
|
className: "w-full px-2 py-1 text-sm border border-gray-200 dark:border-gray-700 rounded-md bg-transparent",
|
|
11950
12699
|
value: cond.value,
|
|
@@ -11956,25 +12705,26 @@ var FormBuilder = class {
|
|
|
11956
12705
|
newConds[idx] = { ...newConds[idx], value: v };
|
|
11957
12706
|
patchMultiple({ conditions: newConds });
|
|
11958
12707
|
}
|
|
11959
|
-
});
|
|
11960
|
-
|
|
11961
|
-
const
|
|
11962
|
-
|
|
12708
|
+
}));
|
|
12709
|
+
row.appendChild(createElement("div", { className: "text-xs text-gray-500 dark:text-gray-400", text: "\u2192 Expression" }));
|
|
12710
|
+
const condWidget = new FormulaEditorWidget({
|
|
12711
|
+
mode: "bracket",
|
|
12712
|
+
availableFields: fwFieldInfos,
|
|
12713
|
+
// all-schema fieldMap
|
|
12714
|
+
initialValue: cond.expression ?? "",
|
|
11963
12715
|
placeholder: "e.g. {fieldA} + {fieldB}",
|
|
11964
|
-
|
|
11965
|
-
|
|
11966
|
-
|
|
11967
|
-
|
|
11968
|
-
|
|
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 });
|
|
12716
|
+
onChange: (expr) => {
|
|
12717
|
+
const freshCfg = formStore.getState().schema.sections.flatMap((s) => s.fields).find((f) => f.id === selectedField.id)?.formulaConfig ?? cfg;
|
|
12718
|
+
const newConds = [...freshCfg.multiple?.conditions ?? []];
|
|
12719
|
+
newConds[idx] = { ...newConds[idx], expression: expr };
|
|
12720
|
+
patchMultiple({ conditions: newConds });
|
|
12721
|
+
}
|
|
11976
12722
|
});
|
|
11977
|
-
|
|
12723
|
+
registerFormulaWidget(condWidget);
|
|
12724
|
+
if (!lastFocusedFormulaWidget)
|
|
12725
|
+
lastFocusedFormulaWidget = condWidget;
|
|
12726
|
+
row.appendChild(condWidget.getElement());
|
|
12727
|
+
row.appendChild(createElement("button", {
|
|
11978
12728
|
type: "button",
|
|
11979
12729
|
className: "text-xs text-red-500 hover:text-red-700 dark:text-red-400",
|
|
11980
12730
|
text: "\u2212 Remove",
|
|
@@ -11985,12 +12735,7 @@ var FormBuilder = class {
|
|
|
11985
12735
|
patchMultiple({ conditions: newConds });
|
|
11986
12736
|
this.render();
|
|
11987
12737
|
}
|
|
11988
|
-
});
|
|
11989
|
-
row.appendChild(whenLabel);
|
|
11990
|
-
row.appendChild(valueInput);
|
|
11991
|
-
row.appendChild(exprLabel);
|
|
11992
|
-
row.appendChild(condTextarea);
|
|
11993
|
-
row.appendChild(removeBtn);
|
|
12738
|
+
}));
|
|
11994
12739
|
body.appendChild(row);
|
|
11995
12740
|
});
|
|
11996
12741
|
body.appendChild(createElement("button", {
|
|
@@ -12009,22 +12754,22 @@ var FormBuilder = class {
|
|
|
12009
12754
|
className: "block text-sm font-normal text-gray-700 dark:text-gray-300 mb-1",
|
|
12010
12755
|
text: "Fallback Expression"
|
|
12011
12756
|
}));
|
|
12012
|
-
const
|
|
12013
|
-
|
|
12757
|
+
const fallbackWidget = new FormulaEditorWidget({
|
|
12758
|
+
mode: "bracket",
|
|
12759
|
+
availableFields: fwFieldInfos,
|
|
12760
|
+
initialValue: cfg.multiple?.fallbackExpression ?? "",
|
|
12014
12761
|
placeholder: "e.g. 0",
|
|
12015
|
-
|
|
12016
|
-
|
|
12017
|
-
|
|
12018
|
-
fallbackTextarea.addEventListener("focus", () => {
|
|
12019
|
-
lastFocusedExprTextarea = fallbackTextarea;
|
|
12020
|
-
});
|
|
12021
|
-
fallbackTextarea.addEventListener("input", () => {
|
|
12022
|
-
patchMultiple({ fallbackExpression: fallbackTextarea.value });
|
|
12762
|
+
onChange: (expr) => {
|
|
12763
|
+
patchMultiple({ fallbackExpression: expr });
|
|
12764
|
+
}
|
|
12023
12765
|
});
|
|
12024
|
-
|
|
12766
|
+
registerFormulaWidget(fallbackWidget);
|
|
12767
|
+
if (!lastFocusedFormulaWidget)
|
|
12768
|
+
lastFocusedFormulaWidget = fallbackWidget;
|
|
12769
|
+
fallbackGroup.appendChild(fallbackWidget.getElement());
|
|
12025
12770
|
body.appendChild(fallbackGroup);
|
|
12026
12771
|
}
|
|
12027
|
-
if (
|
|
12772
|
+
if (fwFieldInfos.length > 0) {
|
|
12028
12773
|
const insertGroup = createElement("div", { className: "mb-3" });
|
|
12029
12774
|
insertGroup.appendChild(createElement("label", {
|
|
12030
12775
|
className: "block text-sm font-normal text-gray-700 dark:text-gray-300 mb-1",
|
|
@@ -12037,27 +12782,20 @@ var FormBuilder = class {
|
|
|
12037
12782
|
const ref = sel.value;
|
|
12038
12783
|
if (!ref)
|
|
12039
12784
|
return;
|
|
12040
|
-
const
|
|
12041
|
-
|
|
12042
|
-
|
|
12043
|
-
|
|
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();
|
|
12785
|
+
const field = fwFieldInfos.find((f) => f.fieldName === ref || f.id === ref);
|
|
12786
|
+
const target = lastFocusedFormulaWidget;
|
|
12787
|
+
if (field && target) {
|
|
12788
|
+
target.insertField(field);
|
|
12052
12789
|
}
|
|
12053
12790
|
sel.value = "";
|
|
12054
12791
|
}
|
|
12055
12792
|
});
|
|
12056
12793
|
insertSelect.appendChild(createElement("option", { value: "", text: "Select field to insert\u2026", selected: true }));
|
|
12057
|
-
|
|
12794
|
+
fwFieldInfos.forEach((f) => {
|
|
12058
12795
|
insertSelect.appendChild(createElement("option", {
|
|
12059
|
-
value:
|
|
12060
|
-
text:
|
|
12796
|
+
value: f.fieldName !== f.id ? f.fieldName : f.id,
|
|
12797
|
+
text: f.label
|
|
12798
|
+
// show only human-readable label
|
|
12061
12799
|
}));
|
|
12062
12800
|
});
|
|
12063
12801
|
insertGroup.appendChild(insertSelect);
|
|
@@ -12088,16 +12826,7 @@ var FormBuilder = class {
|
|
|
12088
12826
|
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
12827
|
text: op.text,
|
|
12090
12828
|
onclick: () => {
|
|
12091
|
-
|
|
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();
|
|
12829
|
+
lastFocusedFormulaWidget?.insertText(op.insert);
|
|
12101
12830
|
}
|
|
12102
12831
|
}));
|
|
12103
12832
|
});
|