form-builder-pro 1.4.0 → 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 +64 -0
- package/dist/index.d.mts +75 -2
- package/dist/index.d.ts +75 -2
- package/dist/index.js +1519 -41
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1514 -42
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -4097,7 +4097,8 @@ var FIELD_TYPES = [
|
|
|
4097
4097
|
{ type: "repeater", label: "Repeater", icon: "Copy" },
|
|
4098
4098
|
{ type: "file", label: "File Upload", icon: "Upload" },
|
|
4099
4099
|
{ type: "image", label: "Image", icon: "Image" },
|
|
4100
|
-
{ type: "name_generator", label: "Name Generator", icon: "Hash" }
|
|
4100
|
+
{ type: "name_generator", label: "Name Generator", icon: "Hash" },
|
|
4101
|
+
{ type: "formula", label: "Formula", icon: "Calculator" }
|
|
4101
4102
|
];
|
|
4102
4103
|
var DEFAULT_FIELD_CONFIG = {
|
|
4103
4104
|
text: { label: "Text Input", placeholder: "Enter text...", width: "100%", enabled: true, visible: true },
|
|
@@ -4154,6 +4155,19 @@ var DEFAULT_FIELD_CONFIG = {
|
|
|
4154
4155
|
nameGeneratorFormat: "TEXT_ID",
|
|
4155
4156
|
nameGeneratorText: "",
|
|
4156
4157
|
nameGeneratorIdPadding: 4
|
|
4158
|
+
},
|
|
4159
|
+
formula: {
|
|
4160
|
+
label: "Formula Field",
|
|
4161
|
+
placeholder: "\u2014",
|
|
4162
|
+
width: "50%",
|
|
4163
|
+
enabled: true,
|
|
4164
|
+
visible: true,
|
|
4165
|
+
formulaConfig: {
|
|
4166
|
+
mode: "single",
|
|
4167
|
+
single: { expression: "" },
|
|
4168
|
+
multiple: { compareField: "", conditions: [], fallbackExpression: "" },
|
|
4169
|
+
decimalPlaces: 2
|
|
4170
|
+
}
|
|
4157
4171
|
}
|
|
4158
4172
|
};
|
|
4159
4173
|
var VALIDATION_TYPE_PRESETS = {
|
|
@@ -4503,6 +4517,8 @@ function normalizeFieldType(type) {
|
|
|
4503
4517
|
return "datetime";
|
|
4504
4518
|
if (str === "NAME_GENERATOR" || normalized === "namegenerator")
|
|
4505
4519
|
return "name_generator";
|
|
4520
|
+
if (str === "FORMULA" || normalized === "formula")
|
|
4521
|
+
return "formula";
|
|
4506
4522
|
return str.toLowerCase();
|
|
4507
4523
|
}
|
|
4508
4524
|
function transformField(field) {
|
|
@@ -4731,6 +4747,31 @@ function transformField(field) {
|
|
|
4731
4747
|
transformed.repeatIncrementEnabled = field.repeatIncrementEnabled;
|
|
4732
4748
|
if (field.dateConstraints !== void 0)
|
|
4733
4749
|
transformed.dateConstraints = field.dateConstraints;
|
|
4750
|
+
if (field.formulaConfig !== void 0) {
|
|
4751
|
+
transformed.formulaConfig = field.formulaConfig;
|
|
4752
|
+
} else if (field.type === "formula" && field.formulaType) {
|
|
4753
|
+
const dp = field.floatingPrecision ?? 2;
|
|
4754
|
+
if (field.formulaType === "SIMPLE") {
|
|
4755
|
+
transformed.formulaConfig = {
|
|
4756
|
+
mode: "single",
|
|
4757
|
+
single: { expression: field.formulaExpression ?? "" },
|
|
4758
|
+
multiple: { compareField: "", conditions: [], fallbackExpression: "" },
|
|
4759
|
+
decimalPlaces: dp
|
|
4760
|
+
};
|
|
4761
|
+
} else if (field.formulaType === "SWITCH") {
|
|
4762
|
+
const cases = Array.isArray(field.switchCases) ? field.switchCases.map((c) => ({ value: c.matchValue ?? c.value ?? "", expression: c.formulaExpression ?? c.expression ?? "" })) : [];
|
|
4763
|
+
transformed.formulaConfig = {
|
|
4764
|
+
mode: "multiple",
|
|
4765
|
+
single: { expression: "" },
|
|
4766
|
+
multiple: {
|
|
4767
|
+
compareField: field.switchField ?? "",
|
|
4768
|
+
conditions: cases,
|
|
4769
|
+
fallbackExpression: field.switchDefaultFormula ?? ""
|
|
4770
|
+
},
|
|
4771
|
+
decimalPlaces: dp
|
|
4772
|
+
};
|
|
4773
|
+
}
|
|
4774
|
+
}
|
|
4734
4775
|
if (field.nameGeneratorFormat !== void 0)
|
|
4735
4776
|
transformed.nameGeneratorFormat = field.nameGeneratorFormat;
|
|
4736
4777
|
if (field.nameGeneratorText !== void 0)
|
|
@@ -5045,6 +5086,36 @@ function fieldToPayload(field, opts) {
|
|
|
5045
5086
|
payload.showWhenValueOffFields = field.showWhenValueOffFields;
|
|
5046
5087
|
}
|
|
5047
5088
|
}
|
|
5089
|
+
if (field.type === "formula") {
|
|
5090
|
+
payload.fieldType = "FORMULA";
|
|
5091
|
+
payload.type = "formula";
|
|
5092
|
+
payload.readOnly = true;
|
|
5093
|
+
payload.floatingPrecision = field.formulaConfig?.decimalPlaces ?? 2;
|
|
5094
|
+
if (field.formulaConfig?.mode === "single") {
|
|
5095
|
+
payload.formulaType = "SIMPLE";
|
|
5096
|
+
payload.formulaExpression = field.formulaConfig.single?.expression ?? "";
|
|
5097
|
+
payload.switchField = null;
|
|
5098
|
+
payload.switchCases = null;
|
|
5099
|
+
payload.switchDefaultFormula = null;
|
|
5100
|
+
} else if (field.formulaConfig?.mode === "multiple") {
|
|
5101
|
+
const multi = field.formulaConfig.multiple;
|
|
5102
|
+
payload.formulaType = "SWITCH";
|
|
5103
|
+
payload.formulaExpression = "";
|
|
5104
|
+
payload.switchField = multi?.compareField ?? null;
|
|
5105
|
+
payload.switchCases = (multi?.conditions ?? []).map((c) => ({
|
|
5106
|
+
matchValue: c.value,
|
|
5107
|
+
formulaExpression: c.expression ?? ""
|
|
5108
|
+
}));
|
|
5109
|
+
payload.switchDefaultFormula = multi?.fallbackExpression ?? "";
|
|
5110
|
+
payload.switchCases = payload.switchCases;
|
|
5111
|
+
} else {
|
|
5112
|
+
payload.formulaType = "SIMPLE";
|
|
5113
|
+
payload.formulaExpression = "";
|
|
5114
|
+
payload.switchField = null;
|
|
5115
|
+
payload.switchCases = null;
|
|
5116
|
+
payload.switchDefaultFormula = null;
|
|
5117
|
+
}
|
|
5118
|
+
}
|
|
5048
5119
|
if (field.type === "repeater") {
|
|
5049
5120
|
payload.fieldType = "REPEATER";
|
|
5050
5121
|
payload.type = "number";
|
|
@@ -5992,7 +6063,8 @@ var ICONS = {
|
|
|
5992
6063
|
"MapPin": '<path stroke-linecap="round" stroke-linejoin="round" d="M15 10.5a3 3 0 11-6 0 3 3 0 016 0z" /><path stroke-linecap="round" stroke-linejoin="round" d="M19.5 10.5c0 7.142-7.5 11.25-7.5 11.25S4.5 17.642 4.5 10.5a7.5 7.5 0 1115 0z" />',
|
|
5993
6064
|
"Briefcase": '<path stroke-linecap="round" stroke-linejoin="round" d="M20.25 14.15v4.25c0 1.094-.787 2.036-1.872 2.18-2.087.277-4.216.42-6.378.42s-4.291-.143-6.378-.42c-1.085-.144-1.872-1.086-1.872-2.18v-4.25m16.5 0a2.18 2.18 0 00.75-1.661V8.706c0-1.081-.768-2.015-1.837-2.175a48.114 48.114 0 00-3.413-.387m4.5 8.006c-.194.165-.42.295-.67.38-1.053.33-2.177.58-3.345.726-.26.032-.52.05-.778.066m-13.882-6.626h.008v.008h-.008v-.008zm1.096-3.837a48.116 48.116 0 00-3.413.387c-1.069.16-1.837 1.094-1.837 2.175v4.784c0 .493.196.958.536 1.344m0 0a17.8 17.8 0 013.344.726c.25.085.476.215.67.38m13.784-5.32c-.34-.386-.536-.851-.536-1.344v-4.784c0-1.081-.768-2.015-1.837-2.175a48.041 48.041 0 01-3.413-.387m-4.5 8.006c.194.165.42.295.67.38 1.053.33 2.177.58 3.345.726.26.032.52.05.778.066m0-7.384V5.626a2.25 2.25 0 00-2.25-2.25h-4.5a2.25 2.25 0 00-2.25 2.25v2.247M16.5 6h-9" />',
|
|
5994
6065
|
"Eye": '<path stroke-linecap="round" stroke-linejoin="round" d="M2.036 12.322a1.012 1.012 0 010-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178z" /><path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />',
|
|
5995
|
-
"Cog": '<path stroke-linecap="round" stroke-linejoin="round" d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.324.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 011.37.49l1.296 2.247a1.125 1.125 0 01-.26 1.431l-1.003.827c-.293.24-.438.613-.431.992a6.759 6.759 0 010 .255c-.007.378.138.75.43.99l1.005.828c.424.35.534.954.26 1.43l-1.298 2.247a1.125 1.125 0 01-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.57 6.57 0 01-.22.128c-.331.183-.581.495-.644.869l-.213 1.28c-.09.543-.56.941-1.11.941h-2.594c-.55 0-1.02-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 01-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 01-1.369-.49l-1.297-2.247a1.125 1.125 0 01.26-1.431l1.004-.827c.292-.24.437-.613.43-.992a6.932 6.932 0 010-.255c.007-.378-.138-.75-.43-.99l-1.004-.828a1.125 1.125 0 01-.26-1.43l1.297-2.247a1.125 1.125 0 011.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.087.22-.128.332-.183.582-.495.644-.869l.214-1.281z" /><path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />'
|
|
6066
|
+
"Cog": '<path stroke-linecap="round" stroke-linejoin="round" d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.324.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 011.37.49l1.296 2.247a1.125 1.125 0 01-.26 1.431l-1.003.827c-.293.24-.438.613-.431.992a6.759 6.759 0 010 .255c-.007.378.138.75.43.99l1.005.828c.424.35.534.954.26 1.43l-1.298 2.247a1.125 1.125 0 01-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.57 6.57 0 01-.22.128c-.331.183-.581.495-.644.869l-.213 1.28c-.09.543-.56.941-1.11.941h-2.594c-.55 0-1.02-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 01-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 01-1.369-.49l-1.297-2.247a1.125 1.125 0 01.26-1.431l1.004-.827c.292-.24.437-.613.43-.992a6.932 6.932 0 010-.255c.007-.378-.138-.75-.43-.99l-1.004-.828a1.125 1.125 0 01-.26-1.43l1.297-2.247a1.125 1.125 0 011.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.087.22-.128.332-.183.582-.495.644-.869l.214-1.281z" /><path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />',
|
|
6067
|
+
"Calculator": '<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 15.75V18m-7.5-6.75h.008v.008H8.25v-.008zm0 2.25h.008v.008H8.25V13.5zm0 2.25h.008v.008H8.25v-.008zm0 2.25h.008v.008H8.25V18zm2.498-6.75h.007v.008h-.007v-.008zm0 2.25h.007v.008h-.007V13.5zm0 2.25h.007v.008h-.007v-.008zm0 2.25h.007v.008h-.007V18zm2.504-6.75h.008v.008h-.008v-.008zm0 2.25h.008v.008h-.008V13.5zm0 2.25h.008v.008h-.008v-.008zm0 2.25h.008v.008h-.008V18zm2.498-6.75h.008v.008h-.008v-.008zm0 2.25h.008v.008h-.008V13.5zM8.25 6h7.5v2.25h-7.5V6zM12 2.25c-1.892 0-3.758.11-5.593.322C5.307 2.7 4.5 3.65 4.5 4.757V19.5a2.25 2.25 0 002.25 2.25h10.5a2.25 2.25 0 002.25-2.25V4.757c0-1.108-.806-2.057-1.907-2.185A48.507 48.507 0 0012 2.25z" />'
|
|
5996
6068
|
};
|
|
5997
6069
|
function getIcon(name, size = 20) {
|
|
5998
6070
|
const svgString = ICONS[name] || `<path stroke-linecap="round" stroke-linejoin="round" d="M9.879 7.519c1.171-1.025 3.071-1.025 4.242 0 1.172 1.025 1.172 2.687 0 3.712-.203.179-.43.326-.67.442-.745.361-1.45.999-1.45 1.827v.75M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9 5.25h.008v.008H12v-.008z" />`;
|
|
@@ -6226,6 +6298,311 @@ function getNumericFieldsForFormula(schema, excludeFieldId) {
|
|
|
6226
6298
|
}
|
|
6227
6299
|
return result;
|
|
6228
6300
|
}
|
|
6301
|
+
var _tokenCache = /* @__PURE__ */ new Map();
|
|
6302
|
+
function _tokenize(expr) {
|
|
6303
|
+
if (_tokenCache.has(expr))
|
|
6304
|
+
return _tokenCache.get(expr);
|
|
6305
|
+
const tokens = [];
|
|
6306
|
+
let i = 0;
|
|
6307
|
+
while (i < expr.length) {
|
|
6308
|
+
const c = expr[i];
|
|
6309
|
+
if (/\s/.test(c)) {
|
|
6310
|
+
i++;
|
|
6311
|
+
continue;
|
|
6312
|
+
}
|
|
6313
|
+
if (/[+\-*/(),]/.test(c)) {
|
|
6314
|
+
tokens.push(c);
|
|
6315
|
+
i++;
|
|
6316
|
+
continue;
|
|
6317
|
+
}
|
|
6318
|
+
if (/[0-9.]/.test(c)) {
|
|
6319
|
+
let num = "";
|
|
6320
|
+
while (i < expr.length && /[0-9.]/.test(expr[i])) {
|
|
6321
|
+
num += expr[i++];
|
|
6322
|
+
}
|
|
6323
|
+
tokens.push(num);
|
|
6324
|
+
continue;
|
|
6325
|
+
}
|
|
6326
|
+
if (/[a-zA-Z_]/.test(c)) {
|
|
6327
|
+
let ident = "";
|
|
6328
|
+
while (i < expr.length && /[a-zA-Z0-9_]/.test(expr[i])) {
|
|
6329
|
+
ident += expr[i++];
|
|
6330
|
+
}
|
|
6331
|
+
tokens.push(ident);
|
|
6332
|
+
continue;
|
|
6333
|
+
}
|
|
6334
|
+
i++;
|
|
6335
|
+
}
|
|
6336
|
+
_tokenCache.set(expr, tokens);
|
|
6337
|
+
return tokens;
|
|
6338
|
+
}
|
|
6339
|
+
function _evalResolved(expr) {
|
|
6340
|
+
const tokens = _tokenize(expr);
|
|
6341
|
+
let pos = 0;
|
|
6342
|
+
const parseExpr = () => {
|
|
6343
|
+
let left = parseTerm();
|
|
6344
|
+
while (pos < tokens.length) {
|
|
6345
|
+
if (tokens[pos] === "+") {
|
|
6346
|
+
pos++;
|
|
6347
|
+
left += parseTerm();
|
|
6348
|
+
} else if (tokens[pos] === "-") {
|
|
6349
|
+
pos++;
|
|
6350
|
+
left -= parseTerm();
|
|
6351
|
+
} else
|
|
6352
|
+
break;
|
|
6353
|
+
}
|
|
6354
|
+
return left;
|
|
6355
|
+
};
|
|
6356
|
+
const parseTerm = () => {
|
|
6357
|
+
let left = parseFactor();
|
|
6358
|
+
while (pos < tokens.length) {
|
|
6359
|
+
if (tokens[pos] === "*") {
|
|
6360
|
+
pos++;
|
|
6361
|
+
left *= parseFactor();
|
|
6362
|
+
} else if (tokens[pos] === "/") {
|
|
6363
|
+
pos++;
|
|
6364
|
+
const r = parseFactor();
|
|
6365
|
+
if (r === 0)
|
|
6366
|
+
return NaN;
|
|
6367
|
+
left /= r;
|
|
6368
|
+
} else
|
|
6369
|
+
break;
|
|
6370
|
+
}
|
|
6371
|
+
return left;
|
|
6372
|
+
};
|
|
6373
|
+
const parseArgs = () => {
|
|
6374
|
+
const args = [];
|
|
6375
|
+
if (pos < tokens.length && tokens[pos] !== ")") {
|
|
6376
|
+
args.push(parseExpr());
|
|
6377
|
+
while (pos < tokens.length && tokens[pos] === ",") {
|
|
6378
|
+
pos++;
|
|
6379
|
+
args.push(parseExpr());
|
|
6380
|
+
}
|
|
6381
|
+
}
|
|
6382
|
+
return args;
|
|
6383
|
+
};
|
|
6384
|
+
const parseFactor = () => {
|
|
6385
|
+
if (pos >= tokens.length)
|
|
6386
|
+
return NaN;
|
|
6387
|
+
const t = tokens[pos];
|
|
6388
|
+
if (t === "(") {
|
|
6389
|
+
pos++;
|
|
6390
|
+
const v = parseExpr();
|
|
6391
|
+
if (tokens[pos] === ")")
|
|
6392
|
+
pos++;
|
|
6393
|
+
return v;
|
|
6394
|
+
}
|
|
6395
|
+
if (t === "-") {
|
|
6396
|
+
pos++;
|
|
6397
|
+
return -parseFactor();
|
|
6398
|
+
}
|
|
6399
|
+
if (t === "+") {
|
|
6400
|
+
pos++;
|
|
6401
|
+
return parseFactor();
|
|
6402
|
+
}
|
|
6403
|
+
const n = parseFloat(t);
|
|
6404
|
+
if (!isNaN(n)) {
|
|
6405
|
+
pos++;
|
|
6406
|
+
return n;
|
|
6407
|
+
}
|
|
6408
|
+
if (/^[a-zA-Z_]/.test(t) && pos + 1 < tokens.length && tokens[pos + 1] === "(") {
|
|
6409
|
+
const fn = t.toUpperCase();
|
|
6410
|
+
pos += 2;
|
|
6411
|
+
const args = parseArgs();
|
|
6412
|
+
if (pos < tokens.length && tokens[pos] === ")")
|
|
6413
|
+
pos++;
|
|
6414
|
+
switch (fn) {
|
|
6415
|
+
case "ROUND":
|
|
6416
|
+
return args.length >= 2 ? Math.round(args[0] * Math.pow(10, args[1])) / Math.pow(10, args[1]) : Math.round(args[0] ?? 0);
|
|
6417
|
+
case "ABS":
|
|
6418
|
+
return Math.abs(args[0] ?? 0);
|
|
6419
|
+
case "MIN":
|
|
6420
|
+
return args.length ? Math.min(...args) : NaN;
|
|
6421
|
+
case "MAX":
|
|
6422
|
+
return args.length ? Math.max(...args) : NaN;
|
|
6423
|
+
case "FLOOR":
|
|
6424
|
+
return Math.floor(args[0] ?? 0);
|
|
6425
|
+
case "CEIL":
|
|
6426
|
+
return Math.ceil(args[0] ?? 0);
|
|
6427
|
+
case "SQRT":
|
|
6428
|
+
return Math.sqrt(args[0] ?? 0);
|
|
6429
|
+
case "POW":
|
|
6430
|
+
return Math.pow(args[0] ?? 0, args[1] ?? 2);
|
|
6431
|
+
default:
|
|
6432
|
+
return NaN;
|
|
6433
|
+
}
|
|
6434
|
+
}
|
|
6435
|
+
pos++;
|
|
6436
|
+
return 0;
|
|
6437
|
+
};
|
|
6438
|
+
try {
|
|
6439
|
+
const result = parseExpr();
|
|
6440
|
+
return isNaN(result) ? NaN : result;
|
|
6441
|
+
} catch {
|
|
6442
|
+
return NaN;
|
|
6443
|
+
}
|
|
6444
|
+
}
|
|
6445
|
+
function extractBracketFields(expression) {
|
|
6446
|
+
if (!expression)
|
|
6447
|
+
return [];
|
|
6448
|
+
const seen = /* @__PURE__ */ new Set();
|
|
6449
|
+
const out = [];
|
|
6450
|
+
for (const m of expression.matchAll(/\{([^}]+)\}/g)) {
|
|
6451
|
+
const name = m[1].trim();
|
|
6452
|
+
if (name && !seen.has(name)) {
|
|
6453
|
+
seen.add(name);
|
|
6454
|
+
out.push(name);
|
|
6455
|
+
}
|
|
6456
|
+
}
|
|
6457
|
+
return out;
|
|
6458
|
+
}
|
|
6459
|
+
function evaluateFormulaExpression(expression, values) {
|
|
6460
|
+
if (!expression?.trim())
|
|
6461
|
+
return NaN;
|
|
6462
|
+
const resolved = expression.replace(/\{([^}]+)\}/g, (_, name) => {
|
|
6463
|
+
const v = values[name.trim()];
|
|
6464
|
+
if (v === void 0 || v === null || v === "")
|
|
6465
|
+
return "0";
|
|
6466
|
+
const n = typeof v === "number" ? v : parseFloat(String(v));
|
|
6467
|
+
return isNaN(n) ? "0" : String(n);
|
|
6468
|
+
});
|
|
6469
|
+
_tokenCache.delete(resolved);
|
|
6470
|
+
return _evalResolved(resolved);
|
|
6471
|
+
}
|
|
6472
|
+
function evaluateFormulaConfig(config, values, compareValue) {
|
|
6473
|
+
if (!config)
|
|
6474
|
+
return { result: NaN, error: "No formula config" };
|
|
6475
|
+
const dp = config.decimalPlaces ?? 2;
|
|
6476
|
+
try {
|
|
6477
|
+
let expression;
|
|
6478
|
+
if (config.mode === "single") {
|
|
6479
|
+
expression = config.single?.expression ?? "";
|
|
6480
|
+
if (!expression.trim())
|
|
6481
|
+
return { result: NaN, error: "No expression defined" };
|
|
6482
|
+
} else {
|
|
6483
|
+
const cmpVal = compareValue ?? "";
|
|
6484
|
+
const matched = config.multiple?.conditions?.find((c) => c.value === cmpVal);
|
|
6485
|
+
expression = matched?.expression ?? config.multiple?.fallbackExpression ?? "";
|
|
6486
|
+
if (!expression.trim())
|
|
6487
|
+
return { result: NaN, error: "No matching condition and no fallback" };
|
|
6488
|
+
}
|
|
6489
|
+
const raw = evaluateFormulaExpression(expression, values);
|
|
6490
|
+
if (isNaN(raw))
|
|
6491
|
+
return { result: NaN, error: "Expression evaluation failed (check syntax or divide-by-zero)" };
|
|
6492
|
+
const result = parseFloat(raw.toFixed(dp));
|
|
6493
|
+
return { result };
|
|
6494
|
+
} catch (e) {
|
|
6495
|
+
return { result: NaN, error: String(e) };
|
|
6496
|
+
}
|
|
6497
|
+
}
|
|
6498
|
+
function validateFormulaExpression(expression, availableFieldNames) {
|
|
6499
|
+
if (!expression?.trim())
|
|
6500
|
+
return { valid: false, error: "Expression cannot be empty" };
|
|
6501
|
+
const refs = extractBracketFields(expression);
|
|
6502
|
+
const known = new Set(availableFieldNames);
|
|
6503
|
+
for (const ref of refs) {
|
|
6504
|
+
if (!known.has(ref)) {
|
|
6505
|
+
return { valid: false, error: `Unknown field reference: "{${ref}}"` };
|
|
6506
|
+
}
|
|
6507
|
+
}
|
|
6508
|
+
let open = 0;
|
|
6509
|
+
for (const c of expression) {
|
|
6510
|
+
if (c === "(")
|
|
6511
|
+
open++;
|
|
6512
|
+
else if (c === ")") {
|
|
6513
|
+
open--;
|
|
6514
|
+
if (open < 0)
|
|
6515
|
+
return { valid: false, error: "Unbalanced parentheses" };
|
|
6516
|
+
}
|
|
6517
|
+
}
|
|
6518
|
+
if (open !== 0)
|
|
6519
|
+
return { valid: false, error: "Unbalanced parentheses" };
|
|
6520
|
+
return { valid: true };
|
|
6521
|
+
}
|
|
6522
|
+
function getFieldsForFormula(schema, excludeFieldId) {
|
|
6523
|
+
const result = [];
|
|
6524
|
+
const NUMERIC_TYPES = /* @__PURE__ */ new Set(["number", "formula"]);
|
|
6525
|
+
for (const section of schema.sections) {
|
|
6526
|
+
for (const field of section.fields) {
|
|
6527
|
+
if (!NUMERIC_TYPES.has(field.type))
|
|
6528
|
+
continue;
|
|
6529
|
+
if (excludeFieldId && field.id === excludeFieldId)
|
|
6530
|
+
continue;
|
|
6531
|
+
const fieldName = field.fieldName ?? field.id;
|
|
6532
|
+
result.push({ id: field.id, fieldName, label: field.label || fieldName });
|
|
6533
|
+
}
|
|
6534
|
+
}
|
|
6535
|
+
return result;
|
|
6536
|
+
}
|
|
6537
|
+
function detectFormulaFieldCircularDependency(schema, formulaFieldId, config) {
|
|
6538
|
+
const resolveField = (ref) => {
|
|
6539
|
+
for (const s of schema.sections) {
|
|
6540
|
+
for (const f of s.fields) {
|
|
6541
|
+
if (f.fieldName === ref || f.id === ref)
|
|
6542
|
+
return f;
|
|
6543
|
+
}
|
|
6544
|
+
}
|
|
6545
|
+
return void 0;
|
|
6546
|
+
};
|
|
6547
|
+
const getDepsFromConfig = (f) => {
|
|
6548
|
+
if (f.type !== "formula" || !f.formulaConfig)
|
|
6549
|
+
return [];
|
|
6550
|
+
const cfg = f.formulaConfig;
|
|
6551
|
+
const exprs = [];
|
|
6552
|
+
if (cfg.mode === "single") {
|
|
6553
|
+
if (cfg.single?.expression)
|
|
6554
|
+
exprs.push(cfg.single.expression);
|
|
6555
|
+
} else {
|
|
6556
|
+
cfg.multiple?.conditions?.forEach((c) => {
|
|
6557
|
+
if (c.expression)
|
|
6558
|
+
exprs.push(c.expression);
|
|
6559
|
+
});
|
|
6560
|
+
if (cfg.multiple?.fallbackExpression)
|
|
6561
|
+
exprs.push(cfg.multiple.fallbackExpression);
|
|
6562
|
+
}
|
|
6563
|
+
return exprs.flatMap((e) => extractBracketFields(e));
|
|
6564
|
+
};
|
|
6565
|
+
const visited = /* @__PURE__ */ new Set();
|
|
6566
|
+
const hasCycle = (fieldId) => {
|
|
6567
|
+
if (visited.has(fieldId))
|
|
6568
|
+
return true;
|
|
6569
|
+
visited.add(fieldId);
|
|
6570
|
+
const field = resolveField(fieldId);
|
|
6571
|
+
if (!field || field.type !== "formula" || !field.formulaConfig) {
|
|
6572
|
+
visited.delete(fieldId);
|
|
6573
|
+
return false;
|
|
6574
|
+
}
|
|
6575
|
+
for (const dep of getDepsFromConfig(field)) {
|
|
6576
|
+
const depField = resolveField(dep);
|
|
6577
|
+
if (!depField)
|
|
6578
|
+
continue;
|
|
6579
|
+
if (depField.id === formulaFieldId) {
|
|
6580
|
+
visited.delete(fieldId);
|
|
6581
|
+
return true;
|
|
6582
|
+
}
|
|
6583
|
+
if (hasCycle(depField.id)) {
|
|
6584
|
+
visited.delete(fieldId);
|
|
6585
|
+
return true;
|
|
6586
|
+
}
|
|
6587
|
+
}
|
|
6588
|
+
visited.delete(fieldId);
|
|
6589
|
+
return false;
|
|
6590
|
+
};
|
|
6591
|
+
const thisField = resolveField(formulaFieldId);
|
|
6592
|
+
if (!thisField || !config)
|
|
6593
|
+
return false;
|
|
6594
|
+
const thisDeps = getDepsFromConfig({ ...thisField, formulaConfig: config });
|
|
6595
|
+
for (const dep of thisDeps) {
|
|
6596
|
+
const depField = resolveField(dep);
|
|
6597
|
+
if (!depField)
|
|
6598
|
+
continue;
|
|
6599
|
+
if (depField.id === formulaFieldId)
|
|
6600
|
+
return true;
|
|
6601
|
+
if (hasCycle(depField.id))
|
|
6602
|
+
return true;
|
|
6603
|
+
}
|
|
6604
|
+
return false;
|
|
6605
|
+
}
|
|
6229
6606
|
|
|
6230
6607
|
// src/core/countryData.ts
|
|
6231
6608
|
var COUNTRY_DATA = [
|
|
@@ -6875,6 +7252,19 @@ var FieldRenderer = class {
|
|
|
6875
7252
|
});
|
|
6876
7253
|
break;
|
|
6877
7254
|
}
|
|
7255
|
+
case "formula": {
|
|
7256
|
+
const formulaDisplay = value !== void 0 && value !== null ? String(value) : "\u2014";
|
|
7257
|
+
input = createElement("input", {
|
|
7258
|
+
type: "text",
|
|
7259
|
+
className: "flex min-h-touch w-full rounded-md border border-input bg-gray-50 dark:bg-gray-800 px-3 py-2 text-sm sm:text-base font-mono ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
|
7260
|
+
placeholder: "\u2014",
|
|
7261
|
+
value: formulaDisplay,
|
|
7262
|
+
readonly: true,
|
|
7263
|
+
disabled: true,
|
|
7264
|
+
title: "Computed field \u2014 value is calculated from a formula"
|
|
7265
|
+
});
|
|
7266
|
+
break;
|
|
7267
|
+
}
|
|
6878
7268
|
default:
|
|
6879
7269
|
const rules = getValidationRules(field);
|
|
6880
7270
|
const useNumericTextInput = field.type === "text" && isNumericTextField(field);
|
|
@@ -7299,6 +7689,21 @@ function buildFormulaValuesMap(schema, data) {
|
|
|
7299
7689
|
values[field.fieldName] = newVal;
|
|
7300
7690
|
}
|
|
7301
7691
|
}
|
|
7692
|
+
const formulaTypeFields = allFields.filter((f) => f.type === "formula" && f.formulaConfig);
|
|
7693
|
+
for (let pass = 0; pass < Math.max(1, formulaTypeFields.length); pass++) {
|
|
7694
|
+
for (const field of formulaTypeFields) {
|
|
7695
|
+
const modelKey = getModelKey(field);
|
|
7696
|
+
const compareFieldName = field.formulaConfig.multiple?.compareField;
|
|
7697
|
+
const compareValue = compareFieldName ? String(values[compareFieldName] ?? "") : "";
|
|
7698
|
+
const evalResult = evaluateFormulaConfig(field.formulaConfig, values, compareValue);
|
|
7699
|
+
const dp = field.formulaConfig.decimalPlaces ?? 2;
|
|
7700
|
+
const newVal = !evalResult.error && !isNaN(evalResult.result) ? parseFloat(evalResult.result.toFixed(dp)) : void 0;
|
|
7701
|
+
values[modelKey] = newVal;
|
|
7702
|
+
values[field.id] = newVal;
|
|
7703
|
+
if (field.fieldName)
|
|
7704
|
+
values[field.fieldName] = newVal;
|
|
7705
|
+
}
|
|
7706
|
+
}
|
|
7302
7707
|
return values;
|
|
7303
7708
|
}
|
|
7304
7709
|
function computeFormulaValue(field, schema, data) {
|
|
@@ -7322,6 +7727,31 @@ function isFormulaDependency(schema, modelKey, fieldId) {
|
|
|
7322
7727
|
if (fieldId && field.dependencies.includes(fieldId))
|
|
7323
7728
|
return true;
|
|
7324
7729
|
}
|
|
7730
|
+
if (field.type === "formula" && field.formulaConfig) {
|
|
7731
|
+
const cfg = field.formulaConfig;
|
|
7732
|
+
const exprs = [];
|
|
7733
|
+
if (cfg.mode === "single") {
|
|
7734
|
+
if (cfg.single?.expression)
|
|
7735
|
+
exprs.push(cfg.single.expression);
|
|
7736
|
+
} else {
|
|
7737
|
+
cfg.multiple?.conditions?.forEach((c) => {
|
|
7738
|
+
if (c.expression)
|
|
7739
|
+
exprs.push(c.expression);
|
|
7740
|
+
});
|
|
7741
|
+
if (cfg.multiple?.fallbackExpression)
|
|
7742
|
+
exprs.push(cfg.multiple.fallbackExpression);
|
|
7743
|
+
if (cfg.multiple?.compareField) {
|
|
7744
|
+
if (cfg.multiple.compareField === modelKey || cfg.multiple.compareField === fieldId)
|
|
7745
|
+
return true;
|
|
7746
|
+
}
|
|
7747
|
+
}
|
|
7748
|
+
for (const expr of exprs) {
|
|
7749
|
+
for (const ref of extractBracketFields(expr)) {
|
|
7750
|
+
if (ref === modelKey || fieldId && ref === fieldId)
|
|
7751
|
+
return true;
|
|
7752
|
+
}
|
|
7753
|
+
}
|
|
7754
|
+
}
|
|
7325
7755
|
}
|
|
7326
7756
|
}
|
|
7327
7757
|
return false;
|
|
@@ -7390,6 +7820,15 @@ var FormRenderer = class {
|
|
|
7390
7820
|
const computed = computeFormulaValue(field, this.schema, this.data);
|
|
7391
7821
|
fieldValue = computed;
|
|
7392
7822
|
this.data[modelKey] = computed;
|
|
7823
|
+
} else if (field.type === "formula" && field.formulaConfig) {
|
|
7824
|
+
const allValues = buildFormulaValuesMap(this.schema, this.data);
|
|
7825
|
+
const compareFieldName = field.formulaConfig.multiple?.compareField;
|
|
7826
|
+
const compareValue = compareFieldName ? String(allValues[compareFieldName] ?? "") : "";
|
|
7827
|
+
const evalResult = evaluateFormulaConfig(field.formulaConfig, allValues, compareValue);
|
|
7828
|
+
const dp = field.formulaConfig.decimalPlaces ?? 2;
|
|
7829
|
+
const computed = !evalResult.error && !isNaN(evalResult.result) ? parseFloat(evalResult.result.toFixed(dp)) : void 0;
|
|
7830
|
+
fieldValue = computed;
|
|
7831
|
+
this.data[modelKey] = computed;
|
|
7393
7832
|
} else if (field.type === "name_generator") {
|
|
7394
7833
|
fieldValue = this.data[modelKey];
|
|
7395
7834
|
if (!fieldValue) {
|
|
@@ -7401,7 +7840,7 @@ var FormRenderer = class {
|
|
|
7401
7840
|
} else {
|
|
7402
7841
|
fieldValue = this.data[modelKey];
|
|
7403
7842
|
}
|
|
7404
|
-
const isFormulaField = field.type === "number" && field.valueSource === "formula";
|
|
7843
|
+
const isFormulaField = field.type === "number" && field.valueSource === "formula" || field.type === "formula";
|
|
7405
7844
|
const fieldEl = FieldRenderer.render(
|
|
7406
7845
|
field,
|
|
7407
7846
|
fieldValue,
|
|
@@ -10113,8 +10552,647 @@ var SectionList = class {
|
|
|
10113
10552
|
}
|
|
10114
10553
|
};
|
|
10115
10554
|
|
|
10555
|
+
// src/utils/formulaTokenParser.ts
|
|
10556
|
+
var FUNCTION_NAMES = /* @__PURE__ */ new Set(["ROUND", "ABS", "MIN", "MAX", "FLOOR", "CEIL", "SQRT", "POW"]);
|
|
10557
|
+
var OPERATOR_CHARS = /* @__PURE__ */ new Set(["+", "-", "*", "/"]);
|
|
10558
|
+
function parseExpressionToTokens(expression, fieldMap, mode) {
|
|
10559
|
+
if (!expression)
|
|
10560
|
+
return [];
|
|
10561
|
+
const tokens = [];
|
|
10562
|
+
let i = 0;
|
|
10563
|
+
const len = expression.length;
|
|
10564
|
+
while (i < len) {
|
|
10565
|
+
const ch = expression[i];
|
|
10566
|
+
if (/\s/.test(ch)) {
|
|
10567
|
+
let ws = "";
|
|
10568
|
+
while (i < len && /\s/.test(expression[i]))
|
|
10569
|
+
ws += expression[i++];
|
|
10570
|
+
tokens.push({ type: "space", value: ws, rawValue: ws });
|
|
10571
|
+
continue;
|
|
10572
|
+
}
|
|
10573
|
+
if (mode === "bracket" && ch === "{") {
|
|
10574
|
+
i++;
|
|
10575
|
+
let ref = "";
|
|
10576
|
+
while (i < len && expression[i] !== "}")
|
|
10577
|
+
ref += expression[i++];
|
|
10578
|
+
if (i < len && expression[i] === "}")
|
|
10579
|
+
i++;
|
|
10580
|
+
const trimmedRef = ref.trim();
|
|
10581
|
+
const field = fieldMap.get(trimmedRef);
|
|
10582
|
+
tokens.push({
|
|
10583
|
+
type: "field",
|
|
10584
|
+
value: field ? field.label || field.fieldName || trimmedRef : trimmedRef,
|
|
10585
|
+
rawValue: `{${trimmedRef}}`,
|
|
10586
|
+
fieldRef: trimmedRef
|
|
10587
|
+
});
|
|
10588
|
+
continue;
|
|
10589
|
+
}
|
|
10590
|
+
if (OPERATOR_CHARS.has(ch)) {
|
|
10591
|
+
tokens.push({ type: "operator", value: ch, rawValue: ch });
|
|
10592
|
+
i++;
|
|
10593
|
+
continue;
|
|
10594
|
+
}
|
|
10595
|
+
if (ch === "%") {
|
|
10596
|
+
tokens.push({ type: "percent", value: "%", rawValue: "%" });
|
|
10597
|
+
i++;
|
|
10598
|
+
continue;
|
|
10599
|
+
}
|
|
10600
|
+
if (ch === "(" || ch === ")") {
|
|
10601
|
+
tokens.push({ type: "paren", value: ch, rawValue: ch });
|
|
10602
|
+
i++;
|
|
10603
|
+
continue;
|
|
10604
|
+
}
|
|
10605
|
+
if (ch === ",") {
|
|
10606
|
+
tokens.push({ type: "comma", value: ",", rawValue: "," });
|
|
10607
|
+
i++;
|
|
10608
|
+
continue;
|
|
10609
|
+
}
|
|
10610
|
+
if (/[0-9]/.test(ch) || ch === "." && i + 1 < len && /[0-9]/.test(expression[i + 1])) {
|
|
10611
|
+
let num = "";
|
|
10612
|
+
while (i < len && /[0-9.]/.test(expression[i]))
|
|
10613
|
+
num += expression[i++];
|
|
10614
|
+
tokens.push({ type: "number", value: num, rawValue: num });
|
|
10615
|
+
continue;
|
|
10616
|
+
}
|
|
10617
|
+
if (/[a-zA-Z_]/.test(ch)) {
|
|
10618
|
+
let ident = "";
|
|
10619
|
+
while (i < len && /[a-zA-Z0-9_]/.test(expression[i]))
|
|
10620
|
+
ident += expression[i++];
|
|
10621
|
+
if (FUNCTION_NAMES.has(ident.toUpperCase())) {
|
|
10622
|
+
tokens.push({ type: "function", value: ident, rawValue: ident });
|
|
10623
|
+
continue;
|
|
10624
|
+
}
|
|
10625
|
+
if (mode === "plain") {
|
|
10626
|
+
const field = fieldMap.get(ident);
|
|
10627
|
+
tokens.push({
|
|
10628
|
+
type: "field",
|
|
10629
|
+
value: field ? field.label || field.fieldName || ident : ident,
|
|
10630
|
+
rawValue: ident,
|
|
10631
|
+
fieldRef: ident
|
|
10632
|
+
});
|
|
10633
|
+
continue;
|
|
10634
|
+
}
|
|
10635
|
+
tokens.push({ type: "unknown", value: ident, rawValue: ident });
|
|
10636
|
+
continue;
|
|
10637
|
+
}
|
|
10638
|
+
tokens.push({ type: "unknown", value: ch, rawValue: ch });
|
|
10639
|
+
i++;
|
|
10640
|
+
}
|
|
10641
|
+
return tokens;
|
|
10642
|
+
}
|
|
10643
|
+
function buildFieldMap(fields) {
|
|
10644
|
+
const map = /* @__PURE__ */ new Map();
|
|
10645
|
+
for (const f of fields) {
|
|
10646
|
+
if (f.fieldName)
|
|
10647
|
+
map.set(f.fieldName, f);
|
|
10648
|
+
if (f.id && f.id !== f.fieldName)
|
|
10649
|
+
map.set(f.id, f);
|
|
10650
|
+
}
|
|
10651
|
+
return map;
|
|
10652
|
+
}
|
|
10653
|
+
function getFieldRef(field) {
|
|
10654
|
+
return field.fieldName && field.fieldName !== field.id ? field.fieldName : field.id;
|
|
10655
|
+
}
|
|
10656
|
+
|
|
10657
|
+
// src/builder/FormulaEditorWidget.ts
|
|
10658
|
+
var _stylesInjected = false;
|
|
10659
|
+
var STYLE_ID = "formula-editor-widget-styles-v2";
|
|
10660
|
+
function injectStyles() {
|
|
10661
|
+
if (_stylesInjected && document.getElementById(STYLE_ID))
|
|
10662
|
+
return;
|
|
10663
|
+
document.getElementById(STYLE_ID)?.remove();
|
|
10664
|
+
document.getElementById("formula-editor-widget-styles")?.remove();
|
|
10665
|
+
_stylesInjected = true;
|
|
10666
|
+
const style = document.createElement("style");
|
|
10667
|
+
style.id = STYLE_ID;
|
|
10668
|
+
style.textContent = `
|
|
10669
|
+
/* \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
|
|
10670
|
+
FormulaEditorWidget v2
|
|
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
|
+
|
|
10673
|
+
/* \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 */
|
|
10674
|
+
.few-container {
|
|
10675
|
+
position: relative;
|
|
10676
|
+
width: 100%;
|
|
10677
|
+
}
|
|
10678
|
+
|
|
10679
|
+
/* \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 */
|
|
10680
|
+
/*
|
|
10681
|
+
* NOTE: color uses !important so it is never muted by
|
|
10682
|
+
* Angular ViewEncapsulation, host-element rules, or global resets.
|
|
10683
|
+
* This ensures operators/numbers always render at full text contrast.
|
|
10684
|
+
*/
|
|
10685
|
+
.few-editor {
|
|
10686
|
+
display: block;
|
|
10687
|
+
width: 100%;
|
|
10688
|
+
min-height: 38px;
|
|
10689
|
+
padding: 6px 32px 6px 12px; /* right padding reserves space for clear btn */
|
|
10690
|
+
border: 1px solid #e2e8f0;
|
|
10691
|
+
border-radius: 6px;
|
|
10692
|
+
font-family: ui-monospace, 'Cascadia Code', 'Source Code Pro', Menlo, monospace;
|
|
10693
|
+
font-size: 13px;
|
|
10694
|
+
font-weight: 700 !important; /* \u2190 operators/numbers always bold */
|
|
10695
|
+
line-height: 1.7;
|
|
10696
|
+
color: rgb(213, 12, 65) !important; /* operator/text color \u2014 chips override with their own !important */
|
|
10697
|
+
caret-color: #635bff;
|
|
10698
|
+
background: transparent;
|
|
10699
|
+
outline: none;
|
|
10700
|
+
word-break: break-word;
|
|
10701
|
+
white-space: pre-wrap;
|
|
10702
|
+
cursor: text;
|
|
10703
|
+
transition: border-color 0.15s, box-shadow 0.15s;
|
|
10704
|
+
box-sizing: border-box;
|
|
10705
|
+
}
|
|
10706
|
+
|
|
10707
|
+
.few-editor:focus {
|
|
10708
|
+
border-color: #635bff;
|
|
10709
|
+
box-shadow: 0 0 0 3px rgba(99, 91, 255, 0.15);
|
|
10710
|
+
}
|
|
10711
|
+
|
|
10712
|
+
.few-editor.few-has-error {
|
|
10713
|
+
border-color: #ef4444;
|
|
10714
|
+
box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.12);
|
|
10715
|
+
}
|
|
10716
|
+
|
|
10717
|
+
/* \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 */
|
|
10718
|
+
.few-placeholder {
|
|
10719
|
+
position: absolute;
|
|
10720
|
+
top: 0;
|
|
10721
|
+
left: 0;
|
|
10722
|
+
right: 32px; /* don't overlap clear button */
|
|
10723
|
+
bottom: 0;
|
|
10724
|
+
padding: 6px 0 6px 12px;
|
|
10725
|
+
font-family: ui-monospace, 'Cascadia Code', 'Source Code Pro', Menlo, monospace;
|
|
10726
|
+
font-size: 13px;
|
|
10727
|
+
line-height: 1.7;
|
|
10728
|
+
color: #94a3b8;
|
|
10729
|
+
pointer-events: none;
|
|
10730
|
+
user-select: none;
|
|
10731
|
+
white-space: nowrap;
|
|
10732
|
+
overflow: hidden;
|
|
10733
|
+
text-overflow: ellipsis;
|
|
10734
|
+
}
|
|
10735
|
+
|
|
10736
|
+
/* \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 */
|
|
10737
|
+
.few-chip {
|
|
10738
|
+
display: inline-flex;
|
|
10739
|
+
align-items: center;
|
|
10740
|
+
padding: 0px 7px 1px 6px;
|
|
10741
|
+
margin: 0 2px;
|
|
10742
|
+
font-size: 11.5px;
|
|
10743
|
+
font-weight: 600 !important; /* explicit !important so the editor's 700 !important doesn't cascade in */
|
|
10744
|
+
font-family: ui-sans-serif, system-ui, sans-serif;
|
|
10745
|
+
letter-spacing: 0.01em;
|
|
10746
|
+
border-radius: 4px;
|
|
10747
|
+
background: rgba(99, 91, 255, 0.10);
|
|
10748
|
+
color: #635bff !important; /* chip label always purple \u2014 not inherited */
|
|
10749
|
+
border: 1px solid rgba(99, 91, 255, 0.28);
|
|
10750
|
+
cursor: default;
|
|
10751
|
+
user-select: none;
|
|
10752
|
+
vertical-align: middle;
|
|
10753
|
+
line-height: 1.6;
|
|
10754
|
+
white-space: nowrap;
|
|
10755
|
+
transition: background 0.1s;
|
|
10756
|
+
}
|
|
10757
|
+
|
|
10758
|
+
.few-chip:hover {
|
|
10759
|
+
background: rgba(99, 91, 255, 0.17);
|
|
10760
|
+
}
|
|
10761
|
+
|
|
10762
|
+
.few-chip-unknown {
|
|
10763
|
+
background: rgba(239, 68, 68, 0.08);
|
|
10764
|
+
color: #dc2626 !important;
|
|
10765
|
+
border-color: rgba(239, 68, 68, 0.25);
|
|
10766
|
+
}
|
|
10767
|
+
|
|
10768
|
+
/* \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 */
|
|
10769
|
+
.few-clear-btn {
|
|
10770
|
+
position: absolute;
|
|
10771
|
+
top: 50%;
|
|
10772
|
+
right: 7px;
|
|
10773
|
+
transform: translateY(-50%);
|
|
10774
|
+
display: flex;
|
|
10775
|
+
align-items: center;
|
|
10776
|
+
justify-content: center;
|
|
10777
|
+
width: 18px;
|
|
10778
|
+
height: 18px;
|
|
10779
|
+
padding: 0;
|
|
10780
|
+
border: none;
|
|
10781
|
+
border-radius: 50%;
|
|
10782
|
+
background: #94a3b8;
|
|
10783
|
+
color: #ffffff;
|
|
10784
|
+
font-size: 12px;
|
|
10785
|
+
line-height: 1;
|
|
10786
|
+
font-weight: 700;
|
|
10787
|
+
cursor: pointer;
|
|
10788
|
+
opacity: 0;
|
|
10789
|
+
pointer-events: none;
|
|
10790
|
+
transition: opacity 0.15s, background 0.15s;
|
|
10791
|
+
z-index: 2;
|
|
10792
|
+
}
|
|
10793
|
+
|
|
10794
|
+
.few-clear-btn:hover {
|
|
10795
|
+
background: #475569;
|
|
10796
|
+
}
|
|
10797
|
+
|
|
10798
|
+
/* Shown only when the editor has content */
|
|
10799
|
+
.few-container.few-has-content .few-clear-btn {
|
|
10800
|
+
opacity: 1;
|
|
10801
|
+
pointer-events: auto;
|
|
10802
|
+
}
|
|
10803
|
+
|
|
10804
|
+
/* \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 */
|
|
10805
|
+
@media (prefers-color-scheme: dark) {
|
|
10806
|
+
.few-editor {
|
|
10807
|
+
border-color: #334155;
|
|
10808
|
+
/* operator color is set via inline !important \u2014 not overridden here */
|
|
10809
|
+
}
|
|
10810
|
+
.few-editor:focus {
|
|
10811
|
+
border-color: #635bff;
|
|
10812
|
+
box-shadow: 0 0 0 3px rgba(99, 91, 255, 0.25);
|
|
10813
|
+
}
|
|
10814
|
+
.few-placeholder { color: #64748b; }
|
|
10815
|
+
.few-chip {
|
|
10816
|
+
background: rgba(99, 91, 255, 0.18);
|
|
10817
|
+
color: #a5b4fc !important;
|
|
10818
|
+
border-color: rgba(99, 91, 255, 0.38);
|
|
10819
|
+
}
|
|
10820
|
+
.few-chip-unknown {
|
|
10821
|
+
background: rgba(239, 68, 68, 0.15);
|
|
10822
|
+
color: #f87171 !important;
|
|
10823
|
+
border-color: rgba(239, 68, 68, 0.35);
|
|
10824
|
+
}
|
|
10825
|
+
.few-clear-btn {
|
|
10826
|
+
background: #64748b;
|
|
10827
|
+
color: #f1f5f9;
|
|
10828
|
+
}
|
|
10829
|
+
.few-clear-btn:hover { background: #475569; }
|
|
10830
|
+
}
|
|
10831
|
+
|
|
10832
|
+
/* \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 */
|
|
10833
|
+
.dark .few-editor {
|
|
10834
|
+
border-color: #334155;
|
|
10835
|
+
/* operator color is set via inline !important \u2014 not overridden here */
|
|
10836
|
+
}
|
|
10837
|
+
.dark .few-editor:focus {
|
|
10838
|
+
border-color: #635bff;
|
|
10839
|
+
box-shadow: 0 0 0 3px rgba(99, 91, 255, 0.25);
|
|
10840
|
+
}
|
|
10841
|
+
.dark .few-placeholder { color: #64748b; }
|
|
10842
|
+
.dark .few-chip {
|
|
10843
|
+
background: rgba(99, 91, 255, 0.18);
|
|
10844
|
+
color: #a5b4fc !important;
|
|
10845
|
+
border-color: rgba(99, 91, 255, 0.38);
|
|
10846
|
+
}
|
|
10847
|
+
.dark .few-chip-unknown {
|
|
10848
|
+
background: rgba(239, 68, 68, 0.15);
|
|
10849
|
+
color: #f87171 !important;
|
|
10850
|
+
border-color: rgba(239, 68, 68, 0.35);
|
|
10851
|
+
}
|
|
10852
|
+
.dark .few-clear-btn {
|
|
10853
|
+
background: #64748b;
|
|
10854
|
+
color: #f1f5f9;
|
|
10855
|
+
}
|
|
10856
|
+
.dark .few-clear-btn:hover { background: #475569; }
|
|
10857
|
+
`;
|
|
10858
|
+
document.head.appendChild(style);
|
|
10859
|
+
}
|
|
10860
|
+
var FormulaEditorWidget = class {
|
|
10861
|
+
constructor(options) {
|
|
10862
|
+
__publicField(this, "_container");
|
|
10863
|
+
__publicField(this, "_editor");
|
|
10864
|
+
__publicField(this, "_placeholderEl");
|
|
10865
|
+
__publicField(this, "_clearBtn");
|
|
10866
|
+
__publicField(this, "_options");
|
|
10867
|
+
__publicField(this, "_fieldMap");
|
|
10868
|
+
/**
|
|
10869
|
+
* The canonical internal expression — SINGLE SOURCE OF TRUTH.
|
|
10870
|
+
* The DOM is always derived from this; never the reverse.
|
|
10871
|
+
* Updated by _onEditorInput() (user edits) and setValue() (programmatic).
|
|
10872
|
+
*/
|
|
10873
|
+
__publicField(this, "_internalValue", "");
|
|
10874
|
+
injectStyles();
|
|
10875
|
+
this._options = { ...options };
|
|
10876
|
+
this._fieldMap = buildFieldMap(options.availableFields);
|
|
10877
|
+
this._container = document.createElement("div");
|
|
10878
|
+
this._container.className = "few-container";
|
|
10879
|
+
this._placeholderEl = document.createElement("div");
|
|
10880
|
+
this._placeholderEl.className = "few-placeholder";
|
|
10881
|
+
this._placeholderEl.textContent = options.placeholder ?? "Enter formula\u2026";
|
|
10882
|
+
this._editor = document.createElement("div");
|
|
10883
|
+
this._editor.className = "few-editor";
|
|
10884
|
+
this._editor.contentEditable = "true";
|
|
10885
|
+
this._editor.spellcheck = false;
|
|
10886
|
+
this._editor.setAttribute("autocomplete", "off");
|
|
10887
|
+
this._editor.setAttribute("autocorrect", "off");
|
|
10888
|
+
this._editor.setAttribute("autocapitalize", "off");
|
|
10889
|
+
this._editor.setAttribute("data-formula-editor", "true");
|
|
10890
|
+
this._editor.style.setProperty("color", "rgb(213, 12, 65)", "important");
|
|
10891
|
+
this._editor.style.setProperty("font-weight", "700", "important");
|
|
10892
|
+
this._editor.style.setProperty("caret-color", "#635bff", "important");
|
|
10893
|
+
this._clearBtn = document.createElement("button");
|
|
10894
|
+
this._clearBtn.type = "button";
|
|
10895
|
+
this._clearBtn.className = "few-clear-btn";
|
|
10896
|
+
this._clearBtn.title = "Clear formula";
|
|
10897
|
+
this._clearBtn.textContent = "\xD7";
|
|
10898
|
+
this._clearBtn.addEventListener("mousedown", (e) => {
|
|
10899
|
+
e.preventDefault();
|
|
10900
|
+
this.clear();
|
|
10901
|
+
});
|
|
10902
|
+
this._container.appendChild(this._placeholderEl);
|
|
10903
|
+
this._container.appendChild(this._editor);
|
|
10904
|
+
this._container.appendChild(this._clearBtn);
|
|
10905
|
+
this._attachEvents();
|
|
10906
|
+
if (options.initialValue) {
|
|
10907
|
+
this.setValue(options.initialValue);
|
|
10908
|
+
} else {
|
|
10909
|
+
this._syncUI();
|
|
10910
|
+
}
|
|
10911
|
+
}
|
|
10912
|
+
// ─── Internal event wiring ───────────────────────────────────────────────
|
|
10913
|
+
_attachEvents() {
|
|
10914
|
+
this._editor.addEventListener("input", () => this._onEditorInput());
|
|
10915
|
+
this._editor.addEventListener("keydown", (e) => {
|
|
10916
|
+
if (e.key === "Enter") {
|
|
10917
|
+
e.preventDefault();
|
|
10918
|
+
return;
|
|
10919
|
+
}
|
|
10920
|
+
if (e.key === "Backspace")
|
|
10921
|
+
this._handleBackspaceOnChip(e);
|
|
10922
|
+
});
|
|
10923
|
+
this._editor.addEventListener("paste", (e) => {
|
|
10924
|
+
e.preventDefault();
|
|
10925
|
+
const text = e.clipboardData?.getData("text/plain") ?? "";
|
|
10926
|
+
if (text) {
|
|
10927
|
+
this._insertTextAtCaret(text);
|
|
10928
|
+
this._onEditorInput();
|
|
10929
|
+
}
|
|
10930
|
+
});
|
|
10931
|
+
this._editor.addEventListener("dragover", (e) => e.preventDefault());
|
|
10932
|
+
this._editor.addEventListener("drop", (e) => e.preventDefault());
|
|
10933
|
+
}
|
|
10934
|
+
// ─── Core edit cycle ─────────────────────────────────────────────────────
|
|
10935
|
+
_onEditorInput() {
|
|
10936
|
+
this._normaliseDom();
|
|
10937
|
+
const newValue = this._readInternalExpression();
|
|
10938
|
+
const changed = newValue !== this._internalValue;
|
|
10939
|
+
this._internalValue = newValue;
|
|
10940
|
+
this._syncUI();
|
|
10941
|
+
if (changed) {
|
|
10942
|
+
this._options.onChange?.(newValue);
|
|
10943
|
+
}
|
|
10944
|
+
}
|
|
10945
|
+
/**
|
|
10946
|
+
* Remove browser-inserted artefacts (<br>, stray <div>/<span>) that some
|
|
10947
|
+
* browsers inject into a contenteditable. Chips (data-field-ref) are kept.
|
|
10948
|
+
*/
|
|
10949
|
+
_normaliseDom() {
|
|
10950
|
+
for (const node of Array.from(this._editor.childNodes)) {
|
|
10951
|
+
if (node.nodeType !== Node.ELEMENT_NODE)
|
|
10952
|
+
continue;
|
|
10953
|
+
const el = node;
|
|
10954
|
+
if (el.hasAttribute("data-field-ref"))
|
|
10955
|
+
continue;
|
|
10956
|
+
if (el.tagName === "BR") {
|
|
10957
|
+
el.remove();
|
|
10958
|
+
continue;
|
|
10959
|
+
}
|
|
10960
|
+
const textNode = document.createTextNode(el.textContent ?? "");
|
|
10961
|
+
this._editor.replaceChild(textNode, el);
|
|
10962
|
+
}
|
|
10963
|
+
}
|
|
10964
|
+
/**
|
|
10965
|
+
* Backspace immediately before a chip should delete the whole chip, not
|
|
10966
|
+
* step into its non-editable content.
|
|
10967
|
+
*/
|
|
10968
|
+
_handleBackspaceOnChip(e) {
|
|
10969
|
+
const sel = window.getSelection();
|
|
10970
|
+
if (!sel || sel.rangeCount === 0)
|
|
10971
|
+
return;
|
|
10972
|
+
const range = sel.getRangeAt(0);
|
|
10973
|
+
if (!range.collapsed)
|
|
10974
|
+
return;
|
|
10975
|
+
let prev = null;
|
|
10976
|
+
if (range.startContainer === this._editor) {
|
|
10977
|
+
if (range.startOffset > 0)
|
|
10978
|
+
prev = this._editor.childNodes[range.startOffset - 1];
|
|
10979
|
+
} else if (range.startContainer.nodeType === Node.TEXT_NODE) {
|
|
10980
|
+
if (range.startOffset === 0)
|
|
10981
|
+
prev = range.startContainer.previousSibling;
|
|
10982
|
+
}
|
|
10983
|
+
if (prev && prev.hasAttribute?.("data-field-ref")) {
|
|
10984
|
+
e.preventDefault();
|
|
10985
|
+
prev.remove();
|
|
10986
|
+
this._onEditorInput();
|
|
10987
|
+
}
|
|
10988
|
+
}
|
|
10989
|
+
// ─── Serialisation: DOM → internal expression ────────────────────────────
|
|
10990
|
+
_readInternalExpression() {
|
|
10991
|
+
let result = "";
|
|
10992
|
+
for (const node of Array.from(this._editor.childNodes)) {
|
|
10993
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
10994
|
+
result += node.textContent ?? "";
|
|
10995
|
+
} else if (node.nodeType === Node.ELEMENT_NODE) {
|
|
10996
|
+
const el = node;
|
|
10997
|
+
const ref = el.getAttribute("data-field-ref");
|
|
10998
|
+
if (ref !== null) {
|
|
10999
|
+
result += this._options.mode === "bracket" ? `{${ref}}` : ref;
|
|
11000
|
+
} else {
|
|
11001
|
+
result += el.textContent ?? "";
|
|
11002
|
+
}
|
|
11003
|
+
}
|
|
11004
|
+
}
|
|
11005
|
+
return result;
|
|
11006
|
+
}
|
|
11007
|
+
// ─── Deserialization: internal expression → DOM ──────────────────────────
|
|
11008
|
+
/**
|
|
11009
|
+
* Load an internal expression string.
|
|
11010
|
+
*
|
|
11011
|
+
* This is the ONLY path that populates the editor with chips.
|
|
11012
|
+
* It does NOT fire onChange (programmatic set ≠ user edit).
|
|
11013
|
+
*
|
|
11014
|
+
* Design invariant: the DOM is ALWAYS fully rebuilt from _internalValue,
|
|
11015
|
+
* never from user-visible text. This guarantees correct state on re-render
|
|
11016
|
+
* after field switches, undo/redo, or hot-reload.
|
|
11017
|
+
*/
|
|
11018
|
+
setValue(internalExpression) {
|
|
11019
|
+
this._internalValue = internalExpression ?? "";
|
|
11020
|
+
this._editor.innerHTML = "";
|
|
11021
|
+
if (this._internalValue.trim()) {
|
|
11022
|
+
const tokens = parseExpressionToTokens(
|
|
11023
|
+
this._internalValue,
|
|
11024
|
+
this._fieldMap,
|
|
11025
|
+
this._options.mode
|
|
11026
|
+
);
|
|
11027
|
+
for (const token of tokens) {
|
|
11028
|
+
if (token.type === "field" && token.fieldRef !== void 0) {
|
|
11029
|
+
const field = this._fieldMap.get(token.fieldRef) ?? {
|
|
11030
|
+
id: token.fieldRef,
|
|
11031
|
+
fieldName: token.fieldRef,
|
|
11032
|
+
label: token.fieldRef
|
|
11033
|
+
// shows raw ref — still readable
|
|
11034
|
+
};
|
|
11035
|
+
const isUnknown = !this._fieldMap.has(token.fieldRef);
|
|
11036
|
+
this._editor.appendChild(this._createChipEl(field, isUnknown));
|
|
11037
|
+
} else {
|
|
11038
|
+
this._editor.appendChild(document.createTextNode(token.value));
|
|
11039
|
+
}
|
|
11040
|
+
}
|
|
11041
|
+
}
|
|
11042
|
+
this._syncUI();
|
|
11043
|
+
}
|
|
11044
|
+
/** Return the current stored expression. Always reflects live DOM state. */
|
|
11045
|
+
getValue() {
|
|
11046
|
+
return this._readInternalExpression();
|
|
11047
|
+
}
|
|
11048
|
+
// ─── Public mutation ─────────────────────────────────────────────────────
|
|
11049
|
+
/**
|
|
11050
|
+
* Insert a field chip at the caret (or append if editor lacks focus).
|
|
11051
|
+
* Handles spacing automatically so chips never run into adjacent text.
|
|
11052
|
+
*/
|
|
11053
|
+
insertField(field) {
|
|
11054
|
+
this._editor.focus();
|
|
11055
|
+
const chip = this._createChipEl(field, false);
|
|
11056
|
+
const sel = window.getSelection();
|
|
11057
|
+
const inEditor = sel && sel.rangeCount > 0 && this._editor.contains(sel.getRangeAt(0).commonAncestorContainer);
|
|
11058
|
+
if (inEditor) {
|
|
11059
|
+
const range = sel.getRangeAt(0);
|
|
11060
|
+
range.deleteContents();
|
|
11061
|
+
if (this._needsSpaceBefore(range)) {
|
|
11062
|
+
range.insertNode(document.createTextNode(" "));
|
|
11063
|
+
range.collapse(false);
|
|
11064
|
+
}
|
|
11065
|
+
range.insertNode(chip);
|
|
11066
|
+
const spaceAfter = document.createTextNode(" ");
|
|
11067
|
+
range.setStartAfter(chip);
|
|
11068
|
+
range.setEndAfter(chip);
|
|
11069
|
+
range.insertNode(spaceAfter);
|
|
11070
|
+
range.setStartAfter(spaceAfter);
|
|
11071
|
+
range.setEndAfter(spaceAfter);
|
|
11072
|
+
sel.removeAllRanges();
|
|
11073
|
+
sel.addRange(range);
|
|
11074
|
+
} else {
|
|
11075
|
+
this._appendAtEnd(chip);
|
|
11076
|
+
}
|
|
11077
|
+
this._onEditorInput();
|
|
11078
|
+
}
|
|
11079
|
+
/**
|
|
11080
|
+
* Insert plain text at the caret (operators, function names, parens).
|
|
11081
|
+
* Operators rendered by this path are text nodes and always inherit
|
|
11082
|
+
* .few-editor's high-contrast color.
|
|
11083
|
+
*/
|
|
11084
|
+
insertText(text) {
|
|
11085
|
+
this._editor.focus();
|
|
11086
|
+
this._insertTextAtCaret(text);
|
|
11087
|
+
this._onEditorInput();
|
|
11088
|
+
}
|
|
11089
|
+
/**
|
|
11090
|
+
* Clear the entire formula expression.
|
|
11091
|
+
* Fires onChange('') so the store is updated immediately.
|
|
11092
|
+
*/
|
|
11093
|
+
clear() {
|
|
11094
|
+
this._editor.innerHTML = "";
|
|
11095
|
+
this._internalValue = "";
|
|
11096
|
+
this._syncUI();
|
|
11097
|
+
this._options.onChange?.("");
|
|
11098
|
+
}
|
|
11099
|
+
// ─── Visual state ────────────────────────────────────────────────────────
|
|
11100
|
+
/** Toggle error (red border) state. */
|
|
11101
|
+
setError(hasError) {
|
|
11102
|
+
this._editor.classList.toggle("few-has-error", hasError);
|
|
11103
|
+
}
|
|
11104
|
+
/**
|
|
11105
|
+
* Refresh the field map with a new field list (e.g. schema changed).
|
|
11106
|
+
* Re-renders from _internalValue so stale labels / unknowns are resolved.
|
|
11107
|
+
*/
|
|
11108
|
+
updateFields(fields) {
|
|
11109
|
+
this._options.availableFields = fields;
|
|
11110
|
+
this._fieldMap = buildFieldMap(fields);
|
|
11111
|
+
this.setValue(this._internalValue);
|
|
11112
|
+
}
|
|
11113
|
+
/** Return the root element to mount. */
|
|
11114
|
+
getElement() {
|
|
11115
|
+
return this._container;
|
|
11116
|
+
}
|
|
11117
|
+
/** Focus the editable area. */
|
|
11118
|
+
focus() {
|
|
11119
|
+
this._editor.focus();
|
|
11120
|
+
}
|
|
11121
|
+
/** Remove from DOM. */
|
|
11122
|
+
destroy() {
|
|
11123
|
+
this._container.remove();
|
|
11124
|
+
}
|
|
11125
|
+
// ─── Private helpers ─────────────────────────────────────────────────────
|
|
11126
|
+
_insertTextAtCaret(text) {
|
|
11127
|
+
const sel = window.getSelection();
|
|
11128
|
+
const inEditor = sel && sel.rangeCount > 0 && this._editor.contains(sel.getRangeAt(0).commonAncestorContainer);
|
|
11129
|
+
if (inEditor) {
|
|
11130
|
+
const range = sel.getRangeAt(0);
|
|
11131
|
+
range.deleteContents();
|
|
11132
|
+
const tn = document.createTextNode(text);
|
|
11133
|
+
range.insertNode(tn);
|
|
11134
|
+
range.setStartAfter(tn);
|
|
11135
|
+
range.setEndAfter(tn);
|
|
11136
|
+
sel.removeAllRanges();
|
|
11137
|
+
sel.addRange(range);
|
|
11138
|
+
} else {
|
|
11139
|
+
this._editor.appendChild(document.createTextNode(text));
|
|
11140
|
+
}
|
|
11141
|
+
}
|
|
11142
|
+
_appendAtEnd(chip) {
|
|
11143
|
+
const last = this._editor.lastChild;
|
|
11144
|
+
if (last) {
|
|
11145
|
+
if (last.nodeType === Node.TEXT_NODE) {
|
|
11146
|
+
const txt = last.textContent ?? "";
|
|
11147
|
+
if (txt.length > 0 && !/\s$/.test(txt)) {
|
|
11148
|
+
this._editor.appendChild(document.createTextNode(" "));
|
|
11149
|
+
}
|
|
11150
|
+
} else if (last.hasAttribute?.("data-field-ref")) {
|
|
11151
|
+
this._editor.appendChild(document.createTextNode(" "));
|
|
11152
|
+
}
|
|
11153
|
+
}
|
|
11154
|
+
this._editor.appendChild(chip);
|
|
11155
|
+
this._editor.appendChild(document.createTextNode(" "));
|
|
11156
|
+
}
|
|
11157
|
+
_needsSpaceBefore(range) {
|
|
11158
|
+
const { startContainer, startOffset } = range;
|
|
11159
|
+
if (startContainer.nodeType === Node.TEXT_NODE) {
|
|
11160
|
+
const text = startContainer.textContent ?? "";
|
|
11161
|
+
const ch = text[startOffset - 1];
|
|
11162
|
+
return startOffset > 0 && ch !== void 0 && !/[\s(]/.test(ch);
|
|
11163
|
+
}
|
|
11164
|
+
if (startContainer === this._editor && startOffset > 0) {
|
|
11165
|
+
const prev = this._editor.childNodes[startOffset - 1];
|
|
11166
|
+
return prev?.hasAttribute?.("data-field-ref") ?? false;
|
|
11167
|
+
}
|
|
11168
|
+
return false;
|
|
11169
|
+
}
|
|
11170
|
+
_createChipEl(field, isUnknown) {
|
|
11171
|
+
const chip = document.createElement("span");
|
|
11172
|
+
chip.contentEditable = "false";
|
|
11173
|
+
chip.setAttribute("data-field-ref", getFieldRef(field));
|
|
11174
|
+
chip.textContent = field.label || field.fieldName || field.id;
|
|
11175
|
+
chip.className = isUnknown ? "few-chip few-chip-unknown" : "few-chip";
|
|
11176
|
+
chip.title = this._options.mode === "bracket" ? `{${getFieldRef(field)}}` : getFieldRef(field);
|
|
11177
|
+
return chip;
|
|
11178
|
+
}
|
|
11179
|
+
/**
|
|
11180
|
+
* Synchronise all purely-visual state:
|
|
11181
|
+
* - placeholder visibility
|
|
11182
|
+
* - clear-button visibility (few-has-content on container)
|
|
11183
|
+
*/
|
|
11184
|
+
_syncUI() {
|
|
11185
|
+
const hasContent = this._internalValue.trim().length > 0 || !!this._editor.querySelector("[data-field-ref]");
|
|
11186
|
+
this._placeholderEl.style.display = hasContent ? "none" : "block";
|
|
11187
|
+
this._container.classList.toggle("few-has-content", hasContent);
|
|
11188
|
+
this._editor.style.setProperty("color", "rgb(213, 12, 65)", "important");
|
|
11189
|
+
this._editor.style.setProperty("font-weight", "700", "important");
|
|
11190
|
+
}
|
|
11191
|
+
};
|
|
11192
|
+
|
|
10116
11193
|
// src/builder/FormBuilder.ts
|
|
10117
11194
|
var advancedCssPanelState = /* @__PURE__ */ new Map();
|
|
11195
|
+
var lastFocusedFormulaWidget = null;
|
|
10118
11196
|
var LABEL_DEBOUNCE_MS = 300;
|
|
10119
11197
|
var labelUpdateTimeouts = /* @__PURE__ */ new Map();
|
|
10120
11198
|
var FormBuilder = class {
|
|
@@ -10589,6 +11667,43 @@ var FormBuilder = class {
|
|
|
10589
11667
|
}
|
|
10590
11668
|
}
|
|
10591
11669
|
}
|
|
11670
|
+
for (const field of schema.sections.flatMap((s) => s.fields)) {
|
|
11671
|
+
if (field.type !== "formula" || !field.formulaConfig)
|
|
11672
|
+
continue;
|
|
11673
|
+
const fcfg = field.formulaConfig;
|
|
11674
|
+
const allOtherFields = schema.sections.flatMap((s) => s.fields).filter((f) => f.id !== field.id);
|
|
11675
|
+
const validRefSet = /* @__PURE__ */ new Set();
|
|
11676
|
+
allOtherFields.forEach((f) => {
|
|
11677
|
+
if (f.fieldName)
|
|
11678
|
+
validRefSet.add(f.fieldName);
|
|
11679
|
+
if (f.id)
|
|
11680
|
+
validRefSet.add(f.id);
|
|
11681
|
+
});
|
|
11682
|
+
const validRefs = Array.from(validRefSet);
|
|
11683
|
+
const exprs = [];
|
|
11684
|
+
if (fcfg.mode === "single") {
|
|
11685
|
+
if (fcfg.single?.expression)
|
|
11686
|
+
exprs.push(fcfg.single.expression);
|
|
11687
|
+
} else {
|
|
11688
|
+
fcfg.multiple?.conditions?.forEach((c) => {
|
|
11689
|
+
if (c.expression)
|
|
11690
|
+
exprs.push(c.expression);
|
|
11691
|
+
});
|
|
11692
|
+
if (fcfg.multiple?.fallbackExpression)
|
|
11693
|
+
exprs.push(fcfg.multiple.fallbackExpression);
|
|
11694
|
+
}
|
|
11695
|
+
for (const expr of exprs) {
|
|
11696
|
+
const result = validateFormulaExpression(expr, validRefs);
|
|
11697
|
+
if (!result.valid) {
|
|
11698
|
+
alert(`Formula error in "${field.label}": ${result.error}`);
|
|
11699
|
+
return;
|
|
11700
|
+
}
|
|
11701
|
+
}
|
|
11702
|
+
if (detectFormulaFieldCircularDependency(schema, field.id, fcfg)) {
|
|
11703
|
+
alert(`Circular dependency detected in formula for "${field.label}"`);
|
|
11704
|
+
return;
|
|
11705
|
+
}
|
|
11706
|
+
}
|
|
10592
11707
|
schema.sections.forEach((section) => {
|
|
10593
11708
|
section.fields?.forEach((field) => {
|
|
10594
11709
|
if (field.type === "number" && field.validations) {
|
|
@@ -11336,38 +12451,64 @@ var FormBuilder = class {
|
|
|
11336
12451
|
const numericFields = getNumericFieldsForFormula(schema, selectedField.id);
|
|
11337
12452
|
const availableIds = numericFields.map((f) => f.id);
|
|
11338
12453
|
const availableNames = numericFields.map((f) => f.fieldName);
|
|
12454
|
+
const allFieldInfosForWidget = schema.sections.flatMap((s) => s.fields).filter((f) => f.id !== selectedField.id).map((f) => ({
|
|
12455
|
+
id: f.id,
|
|
12456
|
+
fieldName: f.fieldName ?? f.id,
|
|
12457
|
+
label: f.label || f.fieldName || f.id
|
|
12458
|
+
}));
|
|
12459
|
+
const numFieldInfos = numericFields.map((f) => ({
|
|
12460
|
+
id: f.id,
|
|
12461
|
+
fieldName: f.fieldName,
|
|
12462
|
+
label: f.label || f.fieldName || f.id
|
|
12463
|
+
}));
|
|
11339
12464
|
const formulaGroup = createElement("div", { className: "mb-3" });
|
|
11340
|
-
formulaGroup.appendChild(createElement("label", {
|
|
11341
|
-
|
|
11342
|
-
|
|
11343
|
-
|
|
11344
|
-
|
|
12465
|
+
formulaGroup.appendChild(createElement("label", {
|
|
12466
|
+
className: "block text-sm font-normal text-gray-700 dark:text-gray-300 mb-1",
|
|
12467
|
+
text: "Formula"
|
|
12468
|
+
}));
|
|
12469
|
+
const numFormulaWidget = new FormulaEditorWidget({
|
|
12470
|
+
mode: "plain",
|
|
12471
|
+
availableFields: allFieldInfosForWidget,
|
|
12472
|
+
// Always read from the store snapshot — guarantees correct reload
|
|
12473
|
+
// after field switch and re-render.
|
|
12474
|
+
initialValue: selectedField.formula || "",
|
|
11345
12475
|
placeholder: "e.g. quantity * price",
|
|
11346
|
-
|
|
11347
|
-
oninput: (e) => {
|
|
11348
|
-
const formula = e.target.value.trim();
|
|
12476
|
+
onChange: (formula) => {
|
|
11349
12477
|
const deps = parseFormulaDependencies(formula);
|
|
11350
|
-
const
|
|
11351
|
-
|
|
11352
|
-
|
|
11353
|
-
|
|
11354
|
-
if (
|
|
11355
|
-
|
|
11356
|
-
|
|
11357
|
-
|
|
11358
|
-
|
|
11359
|
-
|
|
11360
|
-
|
|
12478
|
+
const isValid2 = (() => {
|
|
12479
|
+
if (!formula.trim())
|
|
12480
|
+
return true;
|
|
12481
|
+
const v = validateFormula(formula, availableIds, availableNames, selectedField.id);
|
|
12482
|
+
if (!v.valid)
|
|
12483
|
+
return false;
|
|
12484
|
+
return !(deps.length > 0 && detectCircularDependency(schema, selectedField.id, formula, deps));
|
|
12485
|
+
})();
|
|
12486
|
+
numFormulaWidget.setError(!isValid2 && formula.trim().length > 0);
|
|
12487
|
+
if (!isValid2 && formula.trim()) {
|
|
12488
|
+
const v = validateFormula(formula, availableIds, availableNames, selectedField.id);
|
|
12489
|
+
formulaError.textContent = !v.valid ? v.error : "Circular dependency detected";
|
|
12490
|
+
formulaError.classList.remove("hidden");
|
|
12491
|
+
} else {
|
|
12492
|
+
formulaError.textContent = "";
|
|
12493
|
+
formulaError.classList.add("hidden");
|
|
11361
12494
|
}
|
|
11362
12495
|
formStore.getState().updateField(selectedField.id, { formula, dependencies: deps });
|
|
11363
12496
|
}
|
|
11364
12497
|
});
|
|
11365
|
-
|
|
11366
|
-
|
|
12498
|
+
numFormulaWidget.getElement().querySelector("[data-formula-editor]")?.addEventListener("focus", () => {
|
|
12499
|
+
lastFocusedFormulaWidget = numFormulaWidget;
|
|
12500
|
+
});
|
|
12501
|
+
formulaGroup.appendChild(numFormulaWidget.getElement());
|
|
12502
|
+
const formulaError = createElement("div", {
|
|
12503
|
+
className: "text-xs text-red-600 dark:text-red-400 mt-1 formula-error hidden"
|
|
12504
|
+
});
|
|
11367
12505
|
formulaGroup.appendChild(formulaError);
|
|
11368
12506
|
body.appendChild(formulaGroup);
|
|
11369
12507
|
const insertGroup = createElement("div", { className: "mb-3" });
|
|
11370
|
-
insertGroup.appendChild(createElement("label", {
|
|
12508
|
+
insertGroup.appendChild(createElement("label", {
|
|
12509
|
+
className: "block text-sm font-normal text-gray-700 dark:text-gray-300 mb-1",
|
|
12510
|
+
text: "Insert Field"
|
|
12511
|
+
}));
|
|
11371
12512
|
const insertSelect = createElement("select", {
|
|
11372
12513
|
className: "w-full px-3 py-2 border border-gray-200 dark:border-gray-700 rounded-md bg-transparent",
|
|
11373
12514
|
onchange: (e) => {
|
|
@@ -11375,33 +12516,364 @@ var FormBuilder = class {
|
|
|
11375
12516
|
const ref = sel.value;
|
|
11376
12517
|
if (!ref)
|
|
11377
12518
|
return;
|
|
11378
|
-
const
|
|
11379
|
-
|
|
11380
|
-
|
|
11381
|
-
|
|
11382
|
-
|
|
11383
|
-
dependencies: parseFormulaDependencies(newFormula)
|
|
11384
|
-
});
|
|
11385
|
-
formulaInput.value = newFormula;
|
|
12519
|
+
const field = numFieldInfos.find((f) => f.fieldName === ref || f.id === ref);
|
|
12520
|
+
if (field) {
|
|
12521
|
+
numFormulaWidget.insertField(field);
|
|
12522
|
+
lastFocusedFormulaWidget = numFormulaWidget;
|
|
12523
|
+
}
|
|
11386
12524
|
sel.value = "";
|
|
11387
|
-
this.render();
|
|
11388
12525
|
}
|
|
11389
12526
|
});
|
|
11390
|
-
insertSelect.appendChild(createElement("option", { value: "", text: "Select field to insert
|
|
11391
|
-
|
|
12527
|
+
insertSelect.appendChild(createElement("option", { value: "", text: "Select field to insert\u2026", selected: true }));
|
|
12528
|
+
numFieldInfos.forEach((f) => {
|
|
11392
12529
|
const ref = f.fieldName !== f.id ? f.fieldName : f.id;
|
|
11393
|
-
insertSelect.appendChild(createElement("option", { value: ref, text:
|
|
12530
|
+
insertSelect.appendChild(createElement("option", { value: ref, text: f.label }));
|
|
11394
12531
|
});
|
|
11395
12532
|
insertGroup.appendChild(insertSelect);
|
|
11396
12533
|
body.appendChild(insertGroup);
|
|
11397
|
-
|
|
12534
|
+
body.appendChild(createElement("label", {
|
|
12535
|
+
className: "block text-sm font-normal text-gray-700 dark:text-gray-300 mb-1",
|
|
12536
|
+
text: "Operators"
|
|
12537
|
+
}));
|
|
12538
|
+
const numMathWrap = createElement("div", { className: "flex flex-wrap gap-1 mb-3" });
|
|
12539
|
+
[
|
|
12540
|
+
{ text: "+", insert: " + " },
|
|
12541
|
+
{ text: "-", insert: " - " },
|
|
12542
|
+
{ text: "*", insert: " * " },
|
|
12543
|
+
{ text: "/", insert: " / " },
|
|
12544
|
+
{ text: "%", insert: " % " },
|
|
12545
|
+
{ text: "(", insert: "(" },
|
|
12546
|
+
{ text: ")", insert: ")" }
|
|
12547
|
+
].forEach((op) => {
|
|
12548
|
+
numMathWrap.appendChild(createElement("button", {
|
|
12549
|
+
type: "button",
|
|
12550
|
+
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",
|
|
12551
|
+
text: op.text,
|
|
12552
|
+
onclick: () => {
|
|
12553
|
+
(lastFocusedFormulaWidget ?? numFormulaWidget).insertText(op.insert);
|
|
12554
|
+
}
|
|
12555
|
+
}));
|
|
12556
|
+
});
|
|
12557
|
+
body.appendChild(numMathWrap);
|
|
12558
|
+
body.appendChild(createElement("p", {
|
|
11398
12559
|
className: "text-xs text-gray-500 dark:text-gray-400 mb-2",
|
|
11399
|
-
text: "
|
|
12560
|
+
text: "Fields shown as labels \u2014 stored as field references internally."
|
|
12561
|
+
}));
|
|
12562
|
+
}
|
|
12563
|
+
}
|
|
12564
|
+
if (selectedField.type === "formula") {
|
|
12565
|
+
const schema = formStore.getState().schema;
|
|
12566
|
+
const allSchemaFieldInfos = schema.sections.flatMap((s) => s.fields).filter((f) => f.id !== selectedField.id).map((f) => ({
|
|
12567
|
+
id: f.id,
|
|
12568
|
+
fieldName: f.fieldName ?? f.id,
|
|
12569
|
+
label: f.label || f.fieldName || f.id
|
|
12570
|
+
}));
|
|
12571
|
+
const availableFields = getFieldsForFormula(schema, selectedField.id);
|
|
12572
|
+
const cfg = selectedField.formulaConfig ?? {
|
|
12573
|
+
mode: "single",
|
|
12574
|
+
single: { expression: "" },
|
|
12575
|
+
multiple: { compareField: "", conditions: [], fallbackExpression: "" },
|
|
12576
|
+
decimalPlaces: 2
|
|
12577
|
+
};
|
|
12578
|
+
const patchFormulaConfig = (patch) => {
|
|
12579
|
+
const fresh = formStore.getState().schema.sections.flatMap((s) => s.fields).find((f) => f.id === selectedField.id);
|
|
12580
|
+
const current = fresh?.formulaConfig ?? cfg;
|
|
12581
|
+
formStore.getState().updateField(selectedField.id, { formulaConfig: { ...current, ...patch } });
|
|
12582
|
+
};
|
|
12583
|
+
const patchMultiple = (patch) => {
|
|
12584
|
+
const fresh = formStore.getState().schema.sections.flatMap((s) => s.fields).find((f) => f.id === selectedField.id);
|
|
12585
|
+
const current = fresh?.formulaConfig ?? cfg;
|
|
12586
|
+
patchFormulaConfig({ multiple: { ...current.multiple, ...patch } });
|
|
12587
|
+
};
|
|
12588
|
+
const formulaHeader = createElement("h3", {
|
|
12589
|
+
className: "text-xs font-semibold text-gray-500 uppercase tracking-wider mb-3 mt-4",
|
|
12590
|
+
text: "Formula Configuration"
|
|
12591
|
+
});
|
|
12592
|
+
body.appendChild(formulaHeader);
|
|
12593
|
+
const modeGroup = createElement("div", { className: "mb-4" });
|
|
12594
|
+
modeGroup.appendChild(createElement("label", {
|
|
12595
|
+
className: "block text-sm font-normal text-gray-700 dark:text-gray-300 mb-2",
|
|
12596
|
+
text: "Mode"
|
|
12597
|
+
}));
|
|
12598
|
+
const modeRow = createElement("div", { className: "flex gap-2" });
|
|
12599
|
+
const activeModeClass = "flex-1 px-3 py-1.5 text-sm rounded-md border bg-[#635bff] text-white border-[#635bff] font-medium";
|
|
12600
|
+
const inactiveModeClass = "flex-1 px-3 py-1.5 text-sm rounded-md border border-gray-300 dark:border-gray-600 text-gray-600 dark:text-gray-400 hover:border-[#635bff] transition-colors";
|
|
12601
|
+
const singleBtn = createElement("button", {
|
|
12602
|
+
type: "button",
|
|
12603
|
+
className: cfg.mode === "single" ? activeModeClass : inactiveModeClass,
|
|
12604
|
+
text: "Single Expression",
|
|
12605
|
+
onclick: () => {
|
|
12606
|
+
patchFormulaConfig({ mode: "single" });
|
|
12607
|
+
this.render();
|
|
12608
|
+
}
|
|
12609
|
+
});
|
|
12610
|
+
const multipleBtn = createElement("button", {
|
|
12611
|
+
type: "button",
|
|
12612
|
+
className: cfg.mode === "multiple" ? activeModeClass : inactiveModeClass,
|
|
12613
|
+
text: "Multiple Conditions",
|
|
12614
|
+
onclick: () => {
|
|
12615
|
+
patchFormulaConfig({ mode: "multiple" });
|
|
12616
|
+
this.render();
|
|
12617
|
+
}
|
|
12618
|
+
});
|
|
12619
|
+
modeRow.appendChild(singleBtn);
|
|
12620
|
+
modeRow.appendChild(multipleBtn);
|
|
12621
|
+
modeGroup.appendChild(modeRow);
|
|
12622
|
+
body.appendChild(modeGroup);
|
|
12623
|
+
const fwFieldInfos = allSchemaFieldInfos;
|
|
12624
|
+
lastFocusedFormulaWidget = null;
|
|
12625
|
+
const registerFormulaWidget = (widget) => {
|
|
12626
|
+
widget.getElement().querySelector("[data-formula-editor]")?.addEventListener("focus", () => {
|
|
12627
|
+
lastFocusedFormulaWidget = widget;
|
|
12628
|
+
});
|
|
12629
|
+
};
|
|
12630
|
+
if (cfg.mode === "single") {
|
|
12631
|
+
const exprGroup = createElement("div", { className: "mb-3" });
|
|
12632
|
+
exprGroup.appendChild(createElement("label", {
|
|
12633
|
+
className: "block text-sm font-normal text-gray-700 dark:text-gray-300 mb-1",
|
|
12634
|
+
text: "Expression"
|
|
12635
|
+
}));
|
|
12636
|
+
const exprError = createElement("div", { className: "text-xs text-red-500 mt-1 hidden" });
|
|
12637
|
+
const singleWidget = new FormulaEditorWidget({
|
|
12638
|
+
mode: "bracket",
|
|
12639
|
+
// Use ALL schema fields for the fieldMap so any ref resolves to its label
|
|
12640
|
+
availableFields: fwFieldInfos,
|
|
12641
|
+
// Source of truth: always read from persisted cfg, never from a
|
|
12642
|
+
// closure variable. This guarantees correct reload after field switch.
|
|
12643
|
+
initialValue: cfg.single?.expression ?? "",
|
|
12644
|
+
placeholder: "e.g. {fieldA} + {fieldB} * 1.18",
|
|
12645
|
+
onChange: (expr) => {
|
|
12646
|
+
const result = validateFormulaExpression(expr, fwFieldInfos.map((f) => f.fieldName));
|
|
12647
|
+
const isValid2 = result.valid || !expr.trim();
|
|
12648
|
+
singleWidget.setError(!isValid2);
|
|
12649
|
+
exprError.textContent = isValid2 ? "" : result.error;
|
|
12650
|
+
exprError.classList.toggle("hidden", isValid2);
|
|
12651
|
+
const freshCfg = formStore.getState().schema.sections.flatMap((s) => s.fields).find((f) => f.id === selectedField.id)?.formulaConfig ?? cfg;
|
|
12652
|
+
formStore.getState().updateField(selectedField.id, {
|
|
12653
|
+
formulaConfig: { ...freshCfg, single: { expression: expr } }
|
|
12654
|
+
});
|
|
12655
|
+
}
|
|
12656
|
+
});
|
|
12657
|
+
registerFormulaWidget(singleWidget);
|
|
12658
|
+
lastFocusedFormulaWidget = singleWidget;
|
|
12659
|
+
exprGroup.appendChild(singleWidget.getElement());
|
|
12660
|
+
exprGroup.appendChild(exprError);
|
|
12661
|
+
body.appendChild(exprGroup);
|
|
12662
|
+
} else {
|
|
12663
|
+
const compareGroup = createElement("div", { className: "mb-3" });
|
|
12664
|
+
compareGroup.appendChild(createElement("label", {
|
|
12665
|
+
className: "block text-sm font-normal text-gray-700 dark:text-gray-300 mb-1",
|
|
12666
|
+
text: "Compare Field"
|
|
12667
|
+
}));
|
|
12668
|
+
const compareSelect = createElement("select", {
|
|
12669
|
+
className: "w-full px-3 py-2 border border-gray-200 dark:border-gray-700 rounded-md bg-transparent",
|
|
12670
|
+
onchange: (e) => {
|
|
12671
|
+
const val = e.target.value;
|
|
12672
|
+
patchMultiple({ compareField: val });
|
|
12673
|
+
}
|
|
12674
|
+
});
|
|
12675
|
+
compareSelect.appendChild(createElement("option", { value: "", text: "Select field to compare\u2026", selected: !cfg.multiple?.compareField }));
|
|
12676
|
+
schema.sections.flatMap((s) => s.fields).filter((f) => f.id !== selectedField.id).forEach((f) => {
|
|
12677
|
+
const fn = f.fieldName ?? f.id;
|
|
12678
|
+
compareSelect.appendChild(createElement("option", {
|
|
12679
|
+
value: fn,
|
|
12680
|
+
text: `${f.label} (${fn})`,
|
|
12681
|
+
selected: cfg.multiple?.compareField === fn
|
|
12682
|
+
}));
|
|
12683
|
+
});
|
|
12684
|
+
compareGroup.appendChild(compareSelect);
|
|
12685
|
+
body.appendChild(compareGroup);
|
|
12686
|
+
body.appendChild(createElement("label", {
|
|
12687
|
+
className: "block text-sm font-normal text-gray-700 dark:text-gray-300 mb-2",
|
|
12688
|
+
text: "Conditions"
|
|
12689
|
+
}));
|
|
12690
|
+
const conditions = cfg.multiple?.conditions ?? [];
|
|
12691
|
+
conditions.forEach((cond, idx) => {
|
|
12692
|
+
const row = createElement("div", { className: "mb-2 p-2 rounded-md border border-gray-100 dark:border-gray-800 space-y-1" });
|
|
12693
|
+
row.appendChild(createElement("div", { className: "text-xs text-gray-500 dark:text-gray-400", text: "When equals" }));
|
|
12694
|
+
row.appendChild(createElement("input", {
|
|
12695
|
+
type: "text",
|
|
12696
|
+
className: "w-full px-2 py-1 text-sm border border-gray-200 dark:border-gray-700 rounded-md bg-transparent",
|
|
12697
|
+
value: cond.value,
|
|
12698
|
+
placeholder: "e.g. BHS 146",
|
|
12699
|
+
oninput: (e) => {
|
|
12700
|
+
const v = e.target.value;
|
|
12701
|
+
const freshCfg = formStore.getState().schema.sections.flatMap((s) => s.fields).find((f) => f.id === selectedField.id)?.formulaConfig ?? cfg;
|
|
12702
|
+
const newConds = [...freshCfg.multiple?.conditions ?? []];
|
|
12703
|
+
newConds[idx] = { ...newConds[idx], value: v };
|
|
12704
|
+
patchMultiple({ conditions: newConds });
|
|
12705
|
+
}
|
|
12706
|
+
}));
|
|
12707
|
+
row.appendChild(createElement("div", { className: "text-xs text-gray-500 dark:text-gray-400", text: "\u2192 Expression" }));
|
|
12708
|
+
const condWidget = new FormulaEditorWidget({
|
|
12709
|
+
mode: "bracket",
|
|
12710
|
+
availableFields: fwFieldInfos,
|
|
12711
|
+
// all-schema fieldMap
|
|
12712
|
+
initialValue: cond.expression ?? "",
|
|
12713
|
+
placeholder: "e.g. {fieldA} + {fieldB}",
|
|
12714
|
+
onChange: (expr) => {
|
|
12715
|
+
const freshCfg = formStore.getState().schema.sections.flatMap((s) => s.fields).find((f) => f.id === selectedField.id)?.formulaConfig ?? cfg;
|
|
12716
|
+
const newConds = [...freshCfg.multiple?.conditions ?? []];
|
|
12717
|
+
newConds[idx] = { ...newConds[idx], expression: expr };
|
|
12718
|
+
patchMultiple({ conditions: newConds });
|
|
12719
|
+
}
|
|
12720
|
+
});
|
|
12721
|
+
registerFormulaWidget(condWidget);
|
|
12722
|
+
if (!lastFocusedFormulaWidget)
|
|
12723
|
+
lastFocusedFormulaWidget = condWidget;
|
|
12724
|
+
row.appendChild(condWidget.getElement());
|
|
12725
|
+
row.appendChild(createElement("button", {
|
|
12726
|
+
type: "button",
|
|
12727
|
+
className: "text-xs text-red-500 hover:text-red-700 dark:text-red-400",
|
|
12728
|
+
text: "\u2212 Remove",
|
|
12729
|
+
onclick: () => {
|
|
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.splice(idx, 1);
|
|
12733
|
+
patchMultiple({ conditions: newConds });
|
|
12734
|
+
this.render();
|
|
12735
|
+
}
|
|
12736
|
+
}));
|
|
12737
|
+
body.appendChild(row);
|
|
11400
12738
|
});
|
|
11401
|
-
body.appendChild(
|
|
12739
|
+
body.appendChild(createElement("button", {
|
|
12740
|
+
type: "button",
|
|
12741
|
+
className: "w-full mb-3 py-1.5 text-sm border border-dashed border-gray-300 dark:border-gray-600 rounded-md text-gray-500 dark:text-gray-400 hover:border-[#635bff] hover:text-[#635bff] transition-colors",
|
|
12742
|
+
text: "+ Add Condition",
|
|
12743
|
+
onclick: () => {
|
|
12744
|
+
const freshCfg = formStore.getState().schema.sections.flatMap((s) => s.fields).find((f) => f.id === selectedField.id)?.formulaConfig ?? cfg;
|
|
12745
|
+
const newConds = [...freshCfg.multiple?.conditions ?? [], { value: "", expression: "" }];
|
|
12746
|
+
patchMultiple({ conditions: newConds });
|
|
12747
|
+
this.render();
|
|
12748
|
+
}
|
|
12749
|
+
}));
|
|
12750
|
+
const fallbackGroup = createElement("div", { className: "mb-3" });
|
|
12751
|
+
fallbackGroup.appendChild(createElement("label", {
|
|
12752
|
+
className: "block text-sm font-normal text-gray-700 dark:text-gray-300 mb-1",
|
|
12753
|
+
text: "Fallback Expression"
|
|
12754
|
+
}));
|
|
12755
|
+
const fallbackWidget = new FormulaEditorWidget({
|
|
12756
|
+
mode: "bracket",
|
|
12757
|
+
availableFields: fwFieldInfos,
|
|
12758
|
+
initialValue: cfg.multiple?.fallbackExpression ?? "",
|
|
12759
|
+
placeholder: "e.g. 0",
|
|
12760
|
+
onChange: (expr) => {
|
|
12761
|
+
patchMultiple({ fallbackExpression: expr });
|
|
12762
|
+
}
|
|
12763
|
+
});
|
|
12764
|
+
registerFormulaWidget(fallbackWidget);
|
|
12765
|
+
if (!lastFocusedFormulaWidget)
|
|
12766
|
+
lastFocusedFormulaWidget = fallbackWidget;
|
|
12767
|
+
fallbackGroup.appendChild(fallbackWidget.getElement());
|
|
12768
|
+
body.appendChild(fallbackGroup);
|
|
11402
12769
|
}
|
|
12770
|
+
if (fwFieldInfos.length > 0) {
|
|
12771
|
+
const insertGroup = createElement("div", { className: "mb-3" });
|
|
12772
|
+
insertGroup.appendChild(createElement("label", {
|
|
12773
|
+
className: "block text-sm font-normal text-gray-700 dark:text-gray-300 mb-1",
|
|
12774
|
+
text: "Insert Field Reference"
|
|
12775
|
+
}));
|
|
12776
|
+
const insertSelect = createElement("select", {
|
|
12777
|
+
className: "w-full px-3 py-2 border border-gray-200 dark:border-gray-700 rounded-md bg-transparent",
|
|
12778
|
+
onchange: (e) => {
|
|
12779
|
+
const sel = e.target;
|
|
12780
|
+
const ref = sel.value;
|
|
12781
|
+
if (!ref)
|
|
12782
|
+
return;
|
|
12783
|
+
const field = fwFieldInfos.find((f) => f.fieldName === ref || f.id === ref);
|
|
12784
|
+
const target = lastFocusedFormulaWidget;
|
|
12785
|
+
if (field && target) {
|
|
12786
|
+
target.insertField(field);
|
|
12787
|
+
}
|
|
12788
|
+
sel.value = "";
|
|
12789
|
+
}
|
|
12790
|
+
});
|
|
12791
|
+
insertSelect.appendChild(createElement("option", { value: "", text: "Select field to insert\u2026", selected: true }));
|
|
12792
|
+
fwFieldInfos.forEach((f) => {
|
|
12793
|
+
insertSelect.appendChild(createElement("option", {
|
|
12794
|
+
value: f.fieldName !== f.id ? f.fieldName : f.id,
|
|
12795
|
+
text: f.label
|
|
12796
|
+
// show only human-readable label
|
|
12797
|
+
}));
|
|
12798
|
+
});
|
|
12799
|
+
insertGroup.appendChild(insertSelect);
|
|
12800
|
+
body.appendChild(insertGroup);
|
|
12801
|
+
}
|
|
12802
|
+
body.appendChild(createElement("label", {
|
|
12803
|
+
className: "block text-sm font-normal text-gray-700 dark:text-gray-300 mb-1",
|
|
12804
|
+
text: "Math Helpers"
|
|
12805
|
+
}));
|
|
12806
|
+
const mathWrap = createElement("div", { className: "flex flex-wrap gap-1 mb-3" });
|
|
12807
|
+
const mathOps = [
|
|
12808
|
+
{ text: "+", insert: " + " },
|
|
12809
|
+
{ text: "-", insert: " - " },
|
|
12810
|
+
{ text: "*", insert: " * " },
|
|
12811
|
+
{ text: "/", insert: " / " },
|
|
12812
|
+
{ text: "(", insert: "(" },
|
|
12813
|
+
{ text: ")", insert: ")" },
|
|
12814
|
+
{ text: "ROUND", insert: "ROUND(" },
|
|
12815
|
+
{ text: "ABS", insert: "ABS(" },
|
|
12816
|
+
{ text: "MIN", insert: "MIN(" },
|
|
12817
|
+
{ text: "MAX", insert: "MAX(" },
|
|
12818
|
+
{ text: "FLOOR", insert: "FLOOR(" },
|
|
12819
|
+
{ text: "CEIL", insert: "CEIL(" }
|
|
12820
|
+
];
|
|
12821
|
+
mathOps.forEach((op) => {
|
|
12822
|
+
mathWrap.appendChild(createElement("button", {
|
|
12823
|
+
type: "button",
|
|
12824
|
+
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",
|
|
12825
|
+
text: op.text,
|
|
12826
|
+
onclick: () => {
|
|
12827
|
+
lastFocusedFormulaWidget?.insertText(op.insert);
|
|
12828
|
+
}
|
|
12829
|
+
}));
|
|
12830
|
+
});
|
|
12831
|
+
body.appendChild(mathWrap);
|
|
12832
|
+
const dpGroup = createElement("div", { className: "mb-3" });
|
|
12833
|
+
dpGroup.appendChild(createElement("label", {
|
|
12834
|
+
className: "block text-sm font-normal text-gray-700 dark:text-gray-300 mb-1",
|
|
12835
|
+
text: "Decimal Places"
|
|
12836
|
+
}));
|
|
12837
|
+
dpGroup.appendChild(createElement("input", {
|
|
12838
|
+
type: "number",
|
|
12839
|
+
className: "w-full px-3 py-2 border border-gray-200 dark:border-gray-700 rounded-md bg-transparent",
|
|
12840
|
+
value: String(cfg.decimalPlaces ?? 2),
|
|
12841
|
+
min: "0",
|
|
12842
|
+
max: "10",
|
|
12843
|
+
placeholder: "2",
|
|
12844
|
+
oninput: (e) => {
|
|
12845
|
+
const v = e.target.value;
|
|
12846
|
+
patchFormulaConfig({ decimalPlaces: v !== "" ? parseInt(v, 10) : 2 });
|
|
12847
|
+
}
|
|
12848
|
+
}));
|
|
12849
|
+
body.appendChild(dpGroup);
|
|
12850
|
+
const zeroValues = {};
|
|
12851
|
+
availableFields.forEach((f) => {
|
|
12852
|
+
zeroValues[f.fieldName] = 0;
|
|
12853
|
+
zeroValues[f.id] = 0;
|
|
12854
|
+
});
|
|
12855
|
+
let previewText = "\u2014";
|
|
12856
|
+
try {
|
|
12857
|
+
const previewResult = evaluateFormulaConfig(cfg, zeroValues);
|
|
12858
|
+
if (!previewResult.error && !isNaN(previewResult.result)) {
|
|
12859
|
+
previewText = previewResult.result.toFixed(cfg.decimalPlaces ?? 2);
|
|
12860
|
+
} else if (previewResult.error) {
|
|
12861
|
+
previewText = `Error: ${previewResult.error}`;
|
|
12862
|
+
}
|
|
12863
|
+
} catch {
|
|
12864
|
+
}
|
|
12865
|
+
const previewSection = createElement("div", { className: "mb-4" });
|
|
12866
|
+
previewSection.appendChild(createElement("label", {
|
|
12867
|
+
className: "block text-xs font-semibold text-gray-500 uppercase tracking-wider mb-1",
|
|
12868
|
+
text: "Preview (fields = 0)"
|
|
12869
|
+
}));
|
|
12870
|
+
previewSection.appendChild(createElement("div", {
|
|
12871
|
+
className: "px-3 py-2 bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-md text-sm font-mono text-gray-700 dark:text-gray-300",
|
|
12872
|
+
text: previewText
|
|
12873
|
+
}));
|
|
12874
|
+
body.appendChild(previewSection);
|
|
11403
12875
|
}
|
|
11404
|
-
if (selectedField.type !== "image") {
|
|
12876
|
+
if (selectedField.type !== "image" && selectedField.type !== "formula") {
|
|
11405
12877
|
const placeholderGroup = createElement("div");
|
|
11406
12878
|
placeholderGroup.appendChild(createElement("label", { className: "block text-sm font-normal text-gray-700 dark:text-gray-300 mb-1", text: "Placeholder" }));
|
|
11407
12879
|
placeholderGroup.appendChild(createElement("input", {
|
|
@@ -12875,6 +14347,6 @@ sortablejs/modular/sortable.esm.js:
|
|
|
12875
14347
|
*)
|
|
12876
14348
|
*/
|
|
12877
14349
|
|
|
12878
|
-
export { FormBuilder, FormRenderer, FormSchemaValidation, LOOKUP_SOURCE_TYPE_OPTIONS, builderToPlatform, cleanFormSchema, convertValidationObjectToArray, detectCircularDependency, evaluateFormula, formStore, generateName, getColSpanFromWidth, getNumericFieldsForFormula, getValidationConfigForAngular, initFormBuilder, parseFormulaDependencies, parseWidth, platformToBuilder, resetNameGeneratorCounter, validateFormula };
|
|
14350
|
+
export { FormBuilder, FormRenderer, FormSchemaValidation, LOOKUP_SOURCE_TYPE_OPTIONS, builderToPlatform, cleanFormSchema, convertValidationObjectToArray, detectCircularDependency, detectFormulaFieldCircularDependency, evaluateFormula, evaluateFormulaConfig, evaluateFormulaExpression, extractBracketFields, formStore, generateName, getColSpanFromWidth, getFieldsForFormula, getNumericFieldsForFormula, getValidationConfigForAngular, initFormBuilder, parseFormulaDependencies, parseWidth, platformToBuilder, resetNameGeneratorCounter, validateFormula, validateFormulaExpression };
|
|
12879
14351
|
//# sourceMappingURL=out.js.map
|
|
12880
14352
|
//# sourceMappingURL=index.mjs.map
|