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.js
CHANGED
|
@@ -4099,7 +4099,8 @@ var FIELD_TYPES = [
|
|
|
4099
4099
|
{ type: "repeater", label: "Repeater", icon: "Copy" },
|
|
4100
4100
|
{ type: "file", label: "File Upload", icon: "Upload" },
|
|
4101
4101
|
{ type: "image", label: "Image", icon: "Image" },
|
|
4102
|
-
{ type: "name_generator", label: "Name Generator", icon: "Hash" }
|
|
4102
|
+
{ type: "name_generator", label: "Name Generator", icon: "Hash" },
|
|
4103
|
+
{ type: "formula", label: "Formula", icon: "Calculator" }
|
|
4103
4104
|
];
|
|
4104
4105
|
var DEFAULT_FIELD_CONFIG = {
|
|
4105
4106
|
text: { label: "Text Input", placeholder: "Enter text...", width: "100%", enabled: true, visible: true },
|
|
@@ -4156,6 +4157,19 @@ var DEFAULT_FIELD_CONFIG = {
|
|
|
4156
4157
|
nameGeneratorFormat: "TEXT_ID",
|
|
4157
4158
|
nameGeneratorText: "",
|
|
4158
4159
|
nameGeneratorIdPadding: 4
|
|
4160
|
+
},
|
|
4161
|
+
formula: {
|
|
4162
|
+
label: "Formula Field",
|
|
4163
|
+
placeholder: "\u2014",
|
|
4164
|
+
width: "50%",
|
|
4165
|
+
enabled: true,
|
|
4166
|
+
visible: true,
|
|
4167
|
+
formulaConfig: {
|
|
4168
|
+
mode: "single",
|
|
4169
|
+
single: { expression: "" },
|
|
4170
|
+
multiple: { compareField: "", conditions: [], fallbackExpression: "" },
|
|
4171
|
+
decimalPlaces: 2
|
|
4172
|
+
}
|
|
4159
4173
|
}
|
|
4160
4174
|
};
|
|
4161
4175
|
var VALIDATION_TYPE_PRESETS = {
|
|
@@ -4505,6 +4519,8 @@ function normalizeFieldType(type) {
|
|
|
4505
4519
|
return "datetime";
|
|
4506
4520
|
if (str === "NAME_GENERATOR" || normalized === "namegenerator")
|
|
4507
4521
|
return "name_generator";
|
|
4522
|
+
if (str === "FORMULA" || normalized === "formula")
|
|
4523
|
+
return "formula";
|
|
4508
4524
|
return str.toLowerCase();
|
|
4509
4525
|
}
|
|
4510
4526
|
function transformField(field) {
|
|
@@ -4733,6 +4749,31 @@ function transformField(field) {
|
|
|
4733
4749
|
transformed.repeatIncrementEnabled = field.repeatIncrementEnabled;
|
|
4734
4750
|
if (field.dateConstraints !== void 0)
|
|
4735
4751
|
transformed.dateConstraints = field.dateConstraints;
|
|
4752
|
+
if (field.formulaConfig !== void 0) {
|
|
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
|
+
}
|
|
4736
4777
|
if (field.nameGeneratorFormat !== void 0)
|
|
4737
4778
|
transformed.nameGeneratorFormat = field.nameGeneratorFormat;
|
|
4738
4779
|
if (field.nameGeneratorText !== void 0)
|
|
@@ -5047,6 +5088,36 @@ function fieldToPayload(field, opts) {
|
|
|
5047
5088
|
payload.showWhenValueOffFields = field.showWhenValueOffFields;
|
|
5048
5089
|
}
|
|
5049
5090
|
}
|
|
5091
|
+
if (field.type === "formula") {
|
|
5092
|
+
payload.fieldType = "FORMULA";
|
|
5093
|
+
payload.type = "formula";
|
|
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
|
+
}
|
|
5120
|
+
}
|
|
5050
5121
|
if (field.type === "repeater") {
|
|
5051
5122
|
payload.fieldType = "REPEATER";
|
|
5052
5123
|
payload.type = "number";
|
|
@@ -5994,7 +6065,8 @@ var ICONS = {
|
|
|
5994
6065
|
"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" />',
|
|
5995
6066
|
"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" />',
|
|
5996
6067
|
"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" />',
|
|
5997
|
-
"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" />'
|
|
6068
|
+
"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" />',
|
|
6069
|
+
"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" />'
|
|
5998
6070
|
};
|
|
5999
6071
|
function getIcon(name, size = 20) {
|
|
6000
6072
|
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" />`;
|
|
@@ -6228,6 +6300,311 @@ function getNumericFieldsForFormula(schema, excludeFieldId) {
|
|
|
6228
6300
|
}
|
|
6229
6301
|
return result;
|
|
6230
6302
|
}
|
|
6303
|
+
var _tokenCache = /* @__PURE__ */ new Map();
|
|
6304
|
+
function _tokenize(expr) {
|
|
6305
|
+
if (_tokenCache.has(expr))
|
|
6306
|
+
return _tokenCache.get(expr);
|
|
6307
|
+
const tokens = [];
|
|
6308
|
+
let i = 0;
|
|
6309
|
+
while (i < expr.length) {
|
|
6310
|
+
const c = expr[i];
|
|
6311
|
+
if (/\s/.test(c)) {
|
|
6312
|
+
i++;
|
|
6313
|
+
continue;
|
|
6314
|
+
}
|
|
6315
|
+
if (/[+\-*/(),]/.test(c)) {
|
|
6316
|
+
tokens.push(c);
|
|
6317
|
+
i++;
|
|
6318
|
+
continue;
|
|
6319
|
+
}
|
|
6320
|
+
if (/[0-9.]/.test(c)) {
|
|
6321
|
+
let num = "";
|
|
6322
|
+
while (i < expr.length && /[0-9.]/.test(expr[i])) {
|
|
6323
|
+
num += expr[i++];
|
|
6324
|
+
}
|
|
6325
|
+
tokens.push(num);
|
|
6326
|
+
continue;
|
|
6327
|
+
}
|
|
6328
|
+
if (/[a-zA-Z_]/.test(c)) {
|
|
6329
|
+
let ident = "";
|
|
6330
|
+
while (i < expr.length && /[a-zA-Z0-9_]/.test(expr[i])) {
|
|
6331
|
+
ident += expr[i++];
|
|
6332
|
+
}
|
|
6333
|
+
tokens.push(ident);
|
|
6334
|
+
continue;
|
|
6335
|
+
}
|
|
6336
|
+
i++;
|
|
6337
|
+
}
|
|
6338
|
+
_tokenCache.set(expr, tokens);
|
|
6339
|
+
return tokens;
|
|
6340
|
+
}
|
|
6341
|
+
function _evalResolved(expr) {
|
|
6342
|
+
const tokens = _tokenize(expr);
|
|
6343
|
+
let pos = 0;
|
|
6344
|
+
const parseExpr = () => {
|
|
6345
|
+
let left = parseTerm();
|
|
6346
|
+
while (pos < tokens.length) {
|
|
6347
|
+
if (tokens[pos] === "+") {
|
|
6348
|
+
pos++;
|
|
6349
|
+
left += parseTerm();
|
|
6350
|
+
} else if (tokens[pos] === "-") {
|
|
6351
|
+
pos++;
|
|
6352
|
+
left -= parseTerm();
|
|
6353
|
+
} else
|
|
6354
|
+
break;
|
|
6355
|
+
}
|
|
6356
|
+
return left;
|
|
6357
|
+
};
|
|
6358
|
+
const parseTerm = () => {
|
|
6359
|
+
let left = parseFactor();
|
|
6360
|
+
while (pos < tokens.length) {
|
|
6361
|
+
if (tokens[pos] === "*") {
|
|
6362
|
+
pos++;
|
|
6363
|
+
left *= parseFactor();
|
|
6364
|
+
} else if (tokens[pos] === "/") {
|
|
6365
|
+
pos++;
|
|
6366
|
+
const r = parseFactor();
|
|
6367
|
+
if (r === 0)
|
|
6368
|
+
return NaN;
|
|
6369
|
+
left /= r;
|
|
6370
|
+
} else
|
|
6371
|
+
break;
|
|
6372
|
+
}
|
|
6373
|
+
return left;
|
|
6374
|
+
};
|
|
6375
|
+
const parseArgs = () => {
|
|
6376
|
+
const args = [];
|
|
6377
|
+
if (pos < tokens.length && tokens[pos] !== ")") {
|
|
6378
|
+
args.push(parseExpr());
|
|
6379
|
+
while (pos < tokens.length && tokens[pos] === ",") {
|
|
6380
|
+
pos++;
|
|
6381
|
+
args.push(parseExpr());
|
|
6382
|
+
}
|
|
6383
|
+
}
|
|
6384
|
+
return args;
|
|
6385
|
+
};
|
|
6386
|
+
const parseFactor = () => {
|
|
6387
|
+
if (pos >= tokens.length)
|
|
6388
|
+
return NaN;
|
|
6389
|
+
const t = tokens[pos];
|
|
6390
|
+
if (t === "(") {
|
|
6391
|
+
pos++;
|
|
6392
|
+
const v = parseExpr();
|
|
6393
|
+
if (tokens[pos] === ")")
|
|
6394
|
+
pos++;
|
|
6395
|
+
return v;
|
|
6396
|
+
}
|
|
6397
|
+
if (t === "-") {
|
|
6398
|
+
pos++;
|
|
6399
|
+
return -parseFactor();
|
|
6400
|
+
}
|
|
6401
|
+
if (t === "+") {
|
|
6402
|
+
pos++;
|
|
6403
|
+
return parseFactor();
|
|
6404
|
+
}
|
|
6405
|
+
const n = parseFloat(t);
|
|
6406
|
+
if (!isNaN(n)) {
|
|
6407
|
+
pos++;
|
|
6408
|
+
return n;
|
|
6409
|
+
}
|
|
6410
|
+
if (/^[a-zA-Z_]/.test(t) && pos + 1 < tokens.length && tokens[pos + 1] === "(") {
|
|
6411
|
+
const fn = t.toUpperCase();
|
|
6412
|
+
pos += 2;
|
|
6413
|
+
const args = parseArgs();
|
|
6414
|
+
if (pos < tokens.length && tokens[pos] === ")")
|
|
6415
|
+
pos++;
|
|
6416
|
+
switch (fn) {
|
|
6417
|
+
case "ROUND":
|
|
6418
|
+
return args.length >= 2 ? Math.round(args[0] * Math.pow(10, args[1])) / Math.pow(10, args[1]) : Math.round(args[0] ?? 0);
|
|
6419
|
+
case "ABS":
|
|
6420
|
+
return Math.abs(args[0] ?? 0);
|
|
6421
|
+
case "MIN":
|
|
6422
|
+
return args.length ? Math.min(...args) : NaN;
|
|
6423
|
+
case "MAX":
|
|
6424
|
+
return args.length ? Math.max(...args) : NaN;
|
|
6425
|
+
case "FLOOR":
|
|
6426
|
+
return Math.floor(args[0] ?? 0);
|
|
6427
|
+
case "CEIL":
|
|
6428
|
+
return Math.ceil(args[0] ?? 0);
|
|
6429
|
+
case "SQRT":
|
|
6430
|
+
return Math.sqrt(args[0] ?? 0);
|
|
6431
|
+
case "POW":
|
|
6432
|
+
return Math.pow(args[0] ?? 0, args[1] ?? 2);
|
|
6433
|
+
default:
|
|
6434
|
+
return NaN;
|
|
6435
|
+
}
|
|
6436
|
+
}
|
|
6437
|
+
pos++;
|
|
6438
|
+
return 0;
|
|
6439
|
+
};
|
|
6440
|
+
try {
|
|
6441
|
+
const result = parseExpr();
|
|
6442
|
+
return isNaN(result) ? NaN : result;
|
|
6443
|
+
} catch {
|
|
6444
|
+
return NaN;
|
|
6445
|
+
}
|
|
6446
|
+
}
|
|
6447
|
+
function extractBracketFields(expression) {
|
|
6448
|
+
if (!expression)
|
|
6449
|
+
return [];
|
|
6450
|
+
const seen = /* @__PURE__ */ new Set();
|
|
6451
|
+
const out = [];
|
|
6452
|
+
for (const m of expression.matchAll(/\{([^}]+)\}/g)) {
|
|
6453
|
+
const name = m[1].trim();
|
|
6454
|
+
if (name && !seen.has(name)) {
|
|
6455
|
+
seen.add(name);
|
|
6456
|
+
out.push(name);
|
|
6457
|
+
}
|
|
6458
|
+
}
|
|
6459
|
+
return out;
|
|
6460
|
+
}
|
|
6461
|
+
function evaluateFormulaExpression(expression, values) {
|
|
6462
|
+
if (!expression?.trim())
|
|
6463
|
+
return NaN;
|
|
6464
|
+
const resolved = expression.replace(/\{([^}]+)\}/g, (_, name) => {
|
|
6465
|
+
const v = values[name.trim()];
|
|
6466
|
+
if (v === void 0 || v === null || v === "")
|
|
6467
|
+
return "0";
|
|
6468
|
+
const n = typeof v === "number" ? v : parseFloat(String(v));
|
|
6469
|
+
return isNaN(n) ? "0" : String(n);
|
|
6470
|
+
});
|
|
6471
|
+
_tokenCache.delete(resolved);
|
|
6472
|
+
return _evalResolved(resolved);
|
|
6473
|
+
}
|
|
6474
|
+
function evaluateFormulaConfig(config, values, compareValue) {
|
|
6475
|
+
if (!config)
|
|
6476
|
+
return { result: NaN, error: "No formula config" };
|
|
6477
|
+
const dp = config.decimalPlaces ?? 2;
|
|
6478
|
+
try {
|
|
6479
|
+
let expression;
|
|
6480
|
+
if (config.mode === "single") {
|
|
6481
|
+
expression = config.single?.expression ?? "";
|
|
6482
|
+
if (!expression.trim())
|
|
6483
|
+
return { result: NaN, error: "No expression defined" };
|
|
6484
|
+
} else {
|
|
6485
|
+
const cmpVal = compareValue ?? "";
|
|
6486
|
+
const matched = config.multiple?.conditions?.find((c) => c.value === cmpVal);
|
|
6487
|
+
expression = matched?.expression ?? config.multiple?.fallbackExpression ?? "";
|
|
6488
|
+
if (!expression.trim())
|
|
6489
|
+
return { result: NaN, error: "No matching condition and no fallback" };
|
|
6490
|
+
}
|
|
6491
|
+
const raw = evaluateFormulaExpression(expression, values);
|
|
6492
|
+
if (isNaN(raw))
|
|
6493
|
+
return { result: NaN, error: "Expression evaluation failed (check syntax or divide-by-zero)" };
|
|
6494
|
+
const result = parseFloat(raw.toFixed(dp));
|
|
6495
|
+
return { result };
|
|
6496
|
+
} catch (e) {
|
|
6497
|
+
return { result: NaN, error: String(e) };
|
|
6498
|
+
}
|
|
6499
|
+
}
|
|
6500
|
+
function validateFormulaExpression(expression, availableFieldNames) {
|
|
6501
|
+
if (!expression?.trim())
|
|
6502
|
+
return { valid: false, error: "Expression cannot be empty" };
|
|
6503
|
+
const refs = extractBracketFields(expression);
|
|
6504
|
+
const known = new Set(availableFieldNames);
|
|
6505
|
+
for (const ref of refs) {
|
|
6506
|
+
if (!known.has(ref)) {
|
|
6507
|
+
return { valid: false, error: `Unknown field reference: "{${ref}}"` };
|
|
6508
|
+
}
|
|
6509
|
+
}
|
|
6510
|
+
let open = 0;
|
|
6511
|
+
for (const c of expression) {
|
|
6512
|
+
if (c === "(")
|
|
6513
|
+
open++;
|
|
6514
|
+
else if (c === ")") {
|
|
6515
|
+
open--;
|
|
6516
|
+
if (open < 0)
|
|
6517
|
+
return { valid: false, error: "Unbalanced parentheses" };
|
|
6518
|
+
}
|
|
6519
|
+
}
|
|
6520
|
+
if (open !== 0)
|
|
6521
|
+
return { valid: false, error: "Unbalanced parentheses" };
|
|
6522
|
+
return { valid: true };
|
|
6523
|
+
}
|
|
6524
|
+
function getFieldsForFormula(schema, excludeFieldId) {
|
|
6525
|
+
const result = [];
|
|
6526
|
+
const NUMERIC_TYPES = /* @__PURE__ */ new Set(["number", "formula"]);
|
|
6527
|
+
for (const section of schema.sections) {
|
|
6528
|
+
for (const field of section.fields) {
|
|
6529
|
+
if (!NUMERIC_TYPES.has(field.type))
|
|
6530
|
+
continue;
|
|
6531
|
+
if (excludeFieldId && field.id === excludeFieldId)
|
|
6532
|
+
continue;
|
|
6533
|
+
const fieldName = field.fieldName ?? field.id;
|
|
6534
|
+
result.push({ id: field.id, fieldName, label: field.label || fieldName });
|
|
6535
|
+
}
|
|
6536
|
+
}
|
|
6537
|
+
return result;
|
|
6538
|
+
}
|
|
6539
|
+
function detectFormulaFieldCircularDependency(schema, formulaFieldId, config) {
|
|
6540
|
+
const resolveField = (ref) => {
|
|
6541
|
+
for (const s of schema.sections) {
|
|
6542
|
+
for (const f of s.fields) {
|
|
6543
|
+
if (f.fieldName === ref || f.id === ref)
|
|
6544
|
+
return f;
|
|
6545
|
+
}
|
|
6546
|
+
}
|
|
6547
|
+
return void 0;
|
|
6548
|
+
};
|
|
6549
|
+
const getDepsFromConfig = (f) => {
|
|
6550
|
+
if (f.type !== "formula" || !f.formulaConfig)
|
|
6551
|
+
return [];
|
|
6552
|
+
const cfg = f.formulaConfig;
|
|
6553
|
+
const exprs = [];
|
|
6554
|
+
if (cfg.mode === "single") {
|
|
6555
|
+
if (cfg.single?.expression)
|
|
6556
|
+
exprs.push(cfg.single.expression);
|
|
6557
|
+
} else {
|
|
6558
|
+
cfg.multiple?.conditions?.forEach((c) => {
|
|
6559
|
+
if (c.expression)
|
|
6560
|
+
exprs.push(c.expression);
|
|
6561
|
+
});
|
|
6562
|
+
if (cfg.multiple?.fallbackExpression)
|
|
6563
|
+
exprs.push(cfg.multiple.fallbackExpression);
|
|
6564
|
+
}
|
|
6565
|
+
return exprs.flatMap((e) => extractBracketFields(e));
|
|
6566
|
+
};
|
|
6567
|
+
const visited = /* @__PURE__ */ new Set();
|
|
6568
|
+
const hasCycle = (fieldId) => {
|
|
6569
|
+
if (visited.has(fieldId))
|
|
6570
|
+
return true;
|
|
6571
|
+
visited.add(fieldId);
|
|
6572
|
+
const field = resolveField(fieldId);
|
|
6573
|
+
if (!field || field.type !== "formula" || !field.formulaConfig) {
|
|
6574
|
+
visited.delete(fieldId);
|
|
6575
|
+
return false;
|
|
6576
|
+
}
|
|
6577
|
+
for (const dep of getDepsFromConfig(field)) {
|
|
6578
|
+
const depField = resolveField(dep);
|
|
6579
|
+
if (!depField)
|
|
6580
|
+
continue;
|
|
6581
|
+
if (depField.id === formulaFieldId) {
|
|
6582
|
+
visited.delete(fieldId);
|
|
6583
|
+
return true;
|
|
6584
|
+
}
|
|
6585
|
+
if (hasCycle(depField.id)) {
|
|
6586
|
+
visited.delete(fieldId);
|
|
6587
|
+
return true;
|
|
6588
|
+
}
|
|
6589
|
+
}
|
|
6590
|
+
visited.delete(fieldId);
|
|
6591
|
+
return false;
|
|
6592
|
+
};
|
|
6593
|
+
const thisField = resolveField(formulaFieldId);
|
|
6594
|
+
if (!thisField || !config)
|
|
6595
|
+
return false;
|
|
6596
|
+
const thisDeps = getDepsFromConfig({ ...thisField, formulaConfig: config });
|
|
6597
|
+
for (const dep of thisDeps) {
|
|
6598
|
+
const depField = resolveField(dep);
|
|
6599
|
+
if (!depField)
|
|
6600
|
+
continue;
|
|
6601
|
+
if (depField.id === formulaFieldId)
|
|
6602
|
+
return true;
|
|
6603
|
+
if (hasCycle(depField.id))
|
|
6604
|
+
return true;
|
|
6605
|
+
}
|
|
6606
|
+
return false;
|
|
6607
|
+
}
|
|
6231
6608
|
|
|
6232
6609
|
// src/core/countryData.ts
|
|
6233
6610
|
var COUNTRY_DATA = [
|
|
@@ -6877,6 +7254,19 @@ var FieldRenderer = class {
|
|
|
6877
7254
|
});
|
|
6878
7255
|
break;
|
|
6879
7256
|
}
|
|
7257
|
+
case "formula": {
|
|
7258
|
+
const formulaDisplay = value !== void 0 && value !== null ? String(value) : "\u2014";
|
|
7259
|
+
input = createElement("input", {
|
|
7260
|
+
type: "text",
|
|
7261
|
+
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",
|
|
7262
|
+
placeholder: "\u2014",
|
|
7263
|
+
value: formulaDisplay,
|
|
7264
|
+
readonly: true,
|
|
7265
|
+
disabled: true,
|
|
7266
|
+
title: "Computed field \u2014 value is calculated from a formula"
|
|
7267
|
+
});
|
|
7268
|
+
break;
|
|
7269
|
+
}
|
|
6880
7270
|
default:
|
|
6881
7271
|
const rules = getValidationRules(field);
|
|
6882
7272
|
const useNumericTextInput = field.type === "text" && isNumericTextField(field);
|
|
@@ -7301,6 +7691,21 @@ function buildFormulaValuesMap(schema, data) {
|
|
|
7301
7691
|
values[field.fieldName] = newVal;
|
|
7302
7692
|
}
|
|
7303
7693
|
}
|
|
7694
|
+
const formulaTypeFields = allFields.filter((f) => f.type === "formula" && f.formulaConfig);
|
|
7695
|
+
for (let pass = 0; pass < Math.max(1, formulaTypeFields.length); pass++) {
|
|
7696
|
+
for (const field of formulaTypeFields) {
|
|
7697
|
+
const modelKey = getModelKey(field);
|
|
7698
|
+
const compareFieldName = field.formulaConfig.multiple?.compareField;
|
|
7699
|
+
const compareValue = compareFieldName ? String(values[compareFieldName] ?? "") : "";
|
|
7700
|
+
const evalResult = evaluateFormulaConfig(field.formulaConfig, values, compareValue);
|
|
7701
|
+
const dp = field.formulaConfig.decimalPlaces ?? 2;
|
|
7702
|
+
const newVal = !evalResult.error && !isNaN(evalResult.result) ? parseFloat(evalResult.result.toFixed(dp)) : void 0;
|
|
7703
|
+
values[modelKey] = newVal;
|
|
7704
|
+
values[field.id] = newVal;
|
|
7705
|
+
if (field.fieldName)
|
|
7706
|
+
values[field.fieldName] = newVal;
|
|
7707
|
+
}
|
|
7708
|
+
}
|
|
7304
7709
|
return values;
|
|
7305
7710
|
}
|
|
7306
7711
|
function computeFormulaValue(field, schema, data) {
|
|
@@ -7324,6 +7729,31 @@ function isFormulaDependency(schema, modelKey, fieldId) {
|
|
|
7324
7729
|
if (fieldId && field.dependencies.includes(fieldId))
|
|
7325
7730
|
return true;
|
|
7326
7731
|
}
|
|
7732
|
+
if (field.type === "formula" && field.formulaConfig) {
|
|
7733
|
+
const cfg = field.formulaConfig;
|
|
7734
|
+
const exprs = [];
|
|
7735
|
+
if (cfg.mode === "single") {
|
|
7736
|
+
if (cfg.single?.expression)
|
|
7737
|
+
exprs.push(cfg.single.expression);
|
|
7738
|
+
} else {
|
|
7739
|
+
cfg.multiple?.conditions?.forEach((c) => {
|
|
7740
|
+
if (c.expression)
|
|
7741
|
+
exprs.push(c.expression);
|
|
7742
|
+
});
|
|
7743
|
+
if (cfg.multiple?.fallbackExpression)
|
|
7744
|
+
exprs.push(cfg.multiple.fallbackExpression);
|
|
7745
|
+
if (cfg.multiple?.compareField) {
|
|
7746
|
+
if (cfg.multiple.compareField === modelKey || cfg.multiple.compareField === fieldId)
|
|
7747
|
+
return true;
|
|
7748
|
+
}
|
|
7749
|
+
}
|
|
7750
|
+
for (const expr of exprs) {
|
|
7751
|
+
for (const ref of extractBracketFields(expr)) {
|
|
7752
|
+
if (ref === modelKey || fieldId && ref === fieldId)
|
|
7753
|
+
return true;
|
|
7754
|
+
}
|
|
7755
|
+
}
|
|
7756
|
+
}
|
|
7327
7757
|
}
|
|
7328
7758
|
}
|
|
7329
7759
|
return false;
|
|
@@ -7392,6 +7822,15 @@ var FormRenderer = class {
|
|
|
7392
7822
|
const computed = computeFormulaValue(field, this.schema, this.data);
|
|
7393
7823
|
fieldValue = computed;
|
|
7394
7824
|
this.data[modelKey] = computed;
|
|
7825
|
+
} else if (field.type === "formula" && field.formulaConfig) {
|
|
7826
|
+
const allValues = buildFormulaValuesMap(this.schema, this.data);
|
|
7827
|
+
const compareFieldName = field.formulaConfig.multiple?.compareField;
|
|
7828
|
+
const compareValue = compareFieldName ? String(allValues[compareFieldName] ?? "") : "";
|
|
7829
|
+
const evalResult = evaluateFormulaConfig(field.formulaConfig, allValues, compareValue);
|
|
7830
|
+
const dp = field.formulaConfig.decimalPlaces ?? 2;
|
|
7831
|
+
const computed = !evalResult.error && !isNaN(evalResult.result) ? parseFloat(evalResult.result.toFixed(dp)) : void 0;
|
|
7832
|
+
fieldValue = computed;
|
|
7833
|
+
this.data[modelKey] = computed;
|
|
7395
7834
|
} else if (field.type === "name_generator") {
|
|
7396
7835
|
fieldValue = this.data[modelKey];
|
|
7397
7836
|
if (!fieldValue) {
|
|
@@ -7403,7 +7842,7 @@ var FormRenderer = class {
|
|
|
7403
7842
|
} else {
|
|
7404
7843
|
fieldValue = this.data[modelKey];
|
|
7405
7844
|
}
|
|
7406
|
-
const isFormulaField = field.type === "number" && field.valueSource === "formula";
|
|
7845
|
+
const isFormulaField = field.type === "number" && field.valueSource === "formula" || field.type === "formula";
|
|
7407
7846
|
const fieldEl = FieldRenderer.render(
|
|
7408
7847
|
field,
|
|
7409
7848
|
fieldValue,
|
|
@@ -10115,8 +10554,647 @@ var SectionList = class {
|
|
|
10115
10554
|
}
|
|
10116
10555
|
};
|
|
10117
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
|
+
|
|
10118
11195
|
// src/builder/FormBuilder.ts
|
|
10119
11196
|
var advancedCssPanelState = /* @__PURE__ */ new Map();
|
|
11197
|
+
var lastFocusedFormulaWidget = null;
|
|
10120
11198
|
var LABEL_DEBOUNCE_MS = 300;
|
|
10121
11199
|
var labelUpdateTimeouts = /* @__PURE__ */ new Map();
|
|
10122
11200
|
var FormBuilder = class {
|
|
@@ -10591,6 +11669,43 @@ var FormBuilder = class {
|
|
|
10591
11669
|
}
|
|
10592
11670
|
}
|
|
10593
11671
|
}
|
|
11672
|
+
for (const field of schema.sections.flatMap((s) => s.fields)) {
|
|
11673
|
+
if (field.type !== "formula" || !field.formulaConfig)
|
|
11674
|
+
continue;
|
|
11675
|
+
const fcfg = field.formulaConfig;
|
|
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);
|
|
11685
|
+
const exprs = [];
|
|
11686
|
+
if (fcfg.mode === "single") {
|
|
11687
|
+
if (fcfg.single?.expression)
|
|
11688
|
+
exprs.push(fcfg.single.expression);
|
|
11689
|
+
} else {
|
|
11690
|
+
fcfg.multiple?.conditions?.forEach((c) => {
|
|
11691
|
+
if (c.expression)
|
|
11692
|
+
exprs.push(c.expression);
|
|
11693
|
+
});
|
|
11694
|
+
if (fcfg.multiple?.fallbackExpression)
|
|
11695
|
+
exprs.push(fcfg.multiple.fallbackExpression);
|
|
11696
|
+
}
|
|
11697
|
+
for (const expr of exprs) {
|
|
11698
|
+
const result = validateFormulaExpression(expr, validRefs);
|
|
11699
|
+
if (!result.valid) {
|
|
11700
|
+
alert(`Formula error in "${field.label}": ${result.error}`);
|
|
11701
|
+
return;
|
|
11702
|
+
}
|
|
11703
|
+
}
|
|
11704
|
+
if (detectFormulaFieldCircularDependency(schema, field.id, fcfg)) {
|
|
11705
|
+
alert(`Circular dependency detected in formula for "${field.label}"`);
|
|
11706
|
+
return;
|
|
11707
|
+
}
|
|
11708
|
+
}
|
|
10594
11709
|
schema.sections.forEach((section) => {
|
|
10595
11710
|
section.fields?.forEach((field) => {
|
|
10596
11711
|
if (field.type === "number" && field.validations) {
|
|
@@ -11338,38 +12453,64 @@ var FormBuilder = class {
|
|
|
11338
12453
|
const numericFields = getNumericFieldsForFormula(schema, selectedField.id);
|
|
11339
12454
|
const availableIds = numericFields.map((f) => f.id);
|
|
11340
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
|
+
}));
|
|
11341
12466
|
const formulaGroup = createElement("div", { className: "mb-3" });
|
|
11342
|
-
formulaGroup.appendChild(createElement("label", {
|
|
11343
|
-
|
|
11344
|
-
|
|
11345
|
-
|
|
11346
|
-
|
|
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 || "",
|
|
11347
12477
|
placeholder: "e.g. quantity * price",
|
|
11348
|
-
|
|
11349
|
-
oninput: (e) => {
|
|
11350
|
-
const formula = e.target.value.trim();
|
|
12478
|
+
onChange: (formula) => {
|
|
11351
12479
|
const deps = parseFormulaDependencies(formula);
|
|
11352
|
-
const
|
|
11353
|
-
|
|
11354
|
-
|
|
11355
|
-
|
|
11356
|
-
if (
|
|
11357
|
-
|
|
11358
|
-
|
|
11359
|
-
|
|
11360
|
-
|
|
11361
|
-
|
|
11362
|
-
|
|
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");
|
|
11363
12496
|
}
|
|
11364
12497
|
formStore.getState().updateField(selectedField.id, { formula, dependencies: deps });
|
|
11365
12498
|
}
|
|
11366
12499
|
});
|
|
11367
|
-
|
|
11368
|
-
|
|
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
|
+
});
|
|
11369
12507
|
formulaGroup.appendChild(formulaError);
|
|
11370
12508
|
body.appendChild(formulaGroup);
|
|
11371
12509
|
const insertGroup = createElement("div", { className: "mb-3" });
|
|
11372
|
-
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
|
+
}));
|
|
11373
12514
|
const insertSelect = createElement("select", {
|
|
11374
12515
|
className: "w-full px-3 py-2 border border-gray-200 dark:border-gray-700 rounded-md bg-transparent",
|
|
11375
12516
|
onchange: (e) => {
|
|
@@ -11377,33 +12518,364 @@ var FormBuilder = class {
|
|
|
11377
12518
|
const ref = sel.value;
|
|
11378
12519
|
if (!ref)
|
|
11379
12520
|
return;
|
|
11380
|
-
const
|
|
11381
|
-
|
|
11382
|
-
|
|
11383
|
-
|
|
11384
|
-
|
|
11385
|
-
dependencies: parseFormulaDependencies(newFormula)
|
|
11386
|
-
});
|
|
11387
|
-
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
|
+
}
|
|
11388
12526
|
sel.value = "";
|
|
11389
|
-
this.render();
|
|
11390
12527
|
}
|
|
11391
12528
|
});
|
|
11392
|
-
insertSelect.appendChild(createElement("option", { value: "", text: "Select field to insert
|
|
11393
|
-
|
|
12529
|
+
insertSelect.appendChild(createElement("option", { value: "", text: "Select field to insert\u2026", selected: true }));
|
|
12530
|
+
numFieldInfos.forEach((f) => {
|
|
11394
12531
|
const ref = f.fieldName !== f.id ? f.fieldName : f.id;
|
|
11395
|
-
insertSelect.appendChild(createElement("option", { value: ref, text:
|
|
12532
|
+
insertSelect.appendChild(createElement("option", { value: ref, text: f.label }));
|
|
11396
12533
|
});
|
|
11397
12534
|
insertGroup.appendChild(insertSelect);
|
|
11398
12535
|
body.appendChild(insertGroup);
|
|
11399
|
-
|
|
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
|
+
}));
|
|
12558
|
+
});
|
|
12559
|
+
body.appendChild(numMathWrap);
|
|
12560
|
+
body.appendChild(createElement("p", {
|
|
11400
12561
|
className: "text-xs text-gray-500 dark:text-gray-400 mb-2",
|
|
11401
|
-
text: "
|
|
12562
|
+
text: "Fields shown as labels \u2014 stored as field references internally."
|
|
12563
|
+
}));
|
|
12564
|
+
}
|
|
12565
|
+
}
|
|
12566
|
+
if (selectedField.type === "formula") {
|
|
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
|
+
}));
|
|
12573
|
+
const availableFields = getFieldsForFormula(schema, selectedField.id);
|
|
12574
|
+
const cfg = selectedField.formulaConfig ?? {
|
|
12575
|
+
mode: "single",
|
|
12576
|
+
single: { expression: "" },
|
|
12577
|
+
multiple: { compareField: "", conditions: [], fallbackExpression: "" },
|
|
12578
|
+
decimalPlaces: 2
|
|
12579
|
+
};
|
|
12580
|
+
const patchFormulaConfig = (patch) => {
|
|
12581
|
+
const fresh = formStore.getState().schema.sections.flatMap((s) => s.fields).find((f) => f.id === selectedField.id);
|
|
12582
|
+
const current = fresh?.formulaConfig ?? cfg;
|
|
12583
|
+
formStore.getState().updateField(selectedField.id, { formulaConfig: { ...current, ...patch } });
|
|
12584
|
+
};
|
|
12585
|
+
const patchMultiple = (patch) => {
|
|
12586
|
+
const fresh = formStore.getState().schema.sections.flatMap((s) => s.fields).find((f) => f.id === selectedField.id);
|
|
12587
|
+
const current = fresh?.formulaConfig ?? cfg;
|
|
12588
|
+
patchFormulaConfig({ multiple: { ...current.multiple, ...patch } });
|
|
12589
|
+
};
|
|
12590
|
+
const formulaHeader = createElement("h3", {
|
|
12591
|
+
className: "text-xs font-semibold text-gray-500 uppercase tracking-wider mb-3 mt-4",
|
|
12592
|
+
text: "Formula Configuration"
|
|
12593
|
+
});
|
|
12594
|
+
body.appendChild(formulaHeader);
|
|
12595
|
+
const modeGroup = createElement("div", { className: "mb-4" });
|
|
12596
|
+
modeGroup.appendChild(createElement("label", {
|
|
12597
|
+
className: "block text-sm font-normal text-gray-700 dark:text-gray-300 mb-2",
|
|
12598
|
+
text: "Mode"
|
|
12599
|
+
}));
|
|
12600
|
+
const modeRow = createElement("div", { className: "flex gap-2" });
|
|
12601
|
+
const activeModeClass = "flex-1 px-3 py-1.5 text-sm rounded-md border bg-[#635bff] text-white border-[#635bff] font-medium";
|
|
12602
|
+
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";
|
|
12603
|
+
const singleBtn = createElement("button", {
|
|
12604
|
+
type: "button",
|
|
12605
|
+
className: cfg.mode === "single" ? activeModeClass : inactiveModeClass,
|
|
12606
|
+
text: "Single Expression",
|
|
12607
|
+
onclick: () => {
|
|
12608
|
+
patchFormulaConfig({ mode: "single" });
|
|
12609
|
+
this.render();
|
|
12610
|
+
}
|
|
12611
|
+
});
|
|
12612
|
+
const multipleBtn = createElement("button", {
|
|
12613
|
+
type: "button",
|
|
12614
|
+
className: cfg.mode === "multiple" ? activeModeClass : inactiveModeClass,
|
|
12615
|
+
text: "Multiple Conditions",
|
|
12616
|
+
onclick: () => {
|
|
12617
|
+
patchFormulaConfig({ mode: "multiple" });
|
|
12618
|
+
this.render();
|
|
12619
|
+
}
|
|
12620
|
+
});
|
|
12621
|
+
modeRow.appendChild(singleBtn);
|
|
12622
|
+
modeRow.appendChild(multipleBtn);
|
|
12623
|
+
modeGroup.appendChild(modeRow);
|
|
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
|
+
};
|
|
12632
|
+
if (cfg.mode === "single") {
|
|
12633
|
+
const exprGroup = createElement("div", { className: "mb-3" });
|
|
12634
|
+
exprGroup.appendChild(createElement("label", {
|
|
12635
|
+
className: "block text-sm font-normal text-gray-700 dark:text-gray-300 mb-1",
|
|
12636
|
+
text: "Expression"
|
|
12637
|
+
}));
|
|
12638
|
+
const exprError = createElement("div", { className: "text-xs text-red-500 mt-1 hidden" });
|
|
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
|
+
});
|
|
12657
|
+
}
|
|
12658
|
+
});
|
|
12659
|
+
registerFormulaWidget(singleWidget);
|
|
12660
|
+
lastFocusedFormulaWidget = singleWidget;
|
|
12661
|
+
exprGroup.appendChild(singleWidget.getElement());
|
|
12662
|
+
exprGroup.appendChild(exprError);
|
|
12663
|
+
body.appendChild(exprGroup);
|
|
12664
|
+
} else {
|
|
12665
|
+
const compareGroup = createElement("div", { className: "mb-3" });
|
|
12666
|
+
compareGroup.appendChild(createElement("label", {
|
|
12667
|
+
className: "block text-sm font-normal text-gray-700 dark:text-gray-300 mb-1",
|
|
12668
|
+
text: "Compare Field"
|
|
12669
|
+
}));
|
|
12670
|
+
const compareSelect = createElement("select", {
|
|
12671
|
+
className: "w-full px-3 py-2 border border-gray-200 dark:border-gray-700 rounded-md bg-transparent",
|
|
12672
|
+
onchange: (e) => {
|
|
12673
|
+
const val = e.target.value;
|
|
12674
|
+
patchMultiple({ compareField: val });
|
|
12675
|
+
}
|
|
12676
|
+
});
|
|
12677
|
+
compareSelect.appendChild(createElement("option", { value: "", text: "Select field to compare\u2026", selected: !cfg.multiple?.compareField }));
|
|
12678
|
+
schema.sections.flatMap((s) => s.fields).filter((f) => f.id !== selectedField.id).forEach((f) => {
|
|
12679
|
+
const fn = f.fieldName ?? f.id;
|
|
12680
|
+
compareSelect.appendChild(createElement("option", {
|
|
12681
|
+
value: fn,
|
|
12682
|
+
text: `${f.label} (${fn})`,
|
|
12683
|
+
selected: cfg.multiple?.compareField === fn
|
|
12684
|
+
}));
|
|
12685
|
+
});
|
|
12686
|
+
compareGroup.appendChild(compareSelect);
|
|
12687
|
+
body.appendChild(compareGroup);
|
|
12688
|
+
body.appendChild(createElement("label", {
|
|
12689
|
+
className: "block text-sm font-normal text-gray-700 dark:text-gray-300 mb-2",
|
|
12690
|
+
text: "Conditions"
|
|
12691
|
+
}));
|
|
12692
|
+
const conditions = cfg.multiple?.conditions ?? [];
|
|
12693
|
+
conditions.forEach((cond, idx) => {
|
|
12694
|
+
const row = createElement("div", { className: "mb-2 p-2 rounded-md border border-gray-100 dark:border-gray-800 space-y-1" });
|
|
12695
|
+
row.appendChild(createElement("div", { className: "text-xs text-gray-500 dark:text-gray-400", text: "When equals" }));
|
|
12696
|
+
row.appendChild(createElement("input", {
|
|
12697
|
+
type: "text",
|
|
12698
|
+
className: "w-full px-2 py-1 text-sm border border-gray-200 dark:border-gray-700 rounded-md bg-transparent",
|
|
12699
|
+
value: cond.value,
|
|
12700
|
+
placeholder: "e.g. BHS 146",
|
|
12701
|
+
oninput: (e) => {
|
|
12702
|
+
const v = e.target.value;
|
|
12703
|
+
const freshCfg = formStore.getState().schema.sections.flatMap((s) => s.fields).find((f) => f.id === selectedField.id)?.formulaConfig ?? cfg;
|
|
12704
|
+
const newConds = [...freshCfg.multiple?.conditions ?? []];
|
|
12705
|
+
newConds[idx] = { ...newConds[idx], value: v };
|
|
12706
|
+
patchMultiple({ conditions: newConds });
|
|
12707
|
+
}
|
|
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 ?? "",
|
|
12715
|
+
placeholder: "e.g. {fieldA} + {fieldB}",
|
|
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
|
+
}
|
|
12722
|
+
});
|
|
12723
|
+
registerFormulaWidget(condWidget);
|
|
12724
|
+
if (!lastFocusedFormulaWidget)
|
|
12725
|
+
lastFocusedFormulaWidget = condWidget;
|
|
12726
|
+
row.appendChild(condWidget.getElement());
|
|
12727
|
+
row.appendChild(createElement("button", {
|
|
12728
|
+
type: "button",
|
|
12729
|
+
className: "text-xs text-red-500 hover:text-red-700 dark:text-red-400",
|
|
12730
|
+
text: "\u2212 Remove",
|
|
12731
|
+
onclick: () => {
|
|
12732
|
+
const freshCfg = formStore.getState().schema.sections.flatMap((s) => s.fields).find((f) => f.id === selectedField.id)?.formulaConfig ?? cfg;
|
|
12733
|
+
const newConds = [...freshCfg.multiple?.conditions ?? []];
|
|
12734
|
+
newConds.splice(idx, 1);
|
|
12735
|
+
patchMultiple({ conditions: newConds });
|
|
12736
|
+
this.render();
|
|
12737
|
+
}
|
|
12738
|
+
}));
|
|
12739
|
+
body.appendChild(row);
|
|
11402
12740
|
});
|
|
11403
|
-
body.appendChild(
|
|
12741
|
+
body.appendChild(createElement("button", {
|
|
12742
|
+
type: "button",
|
|
12743
|
+
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",
|
|
12744
|
+
text: "+ Add Condition",
|
|
12745
|
+
onclick: () => {
|
|
12746
|
+
const freshCfg = formStore.getState().schema.sections.flatMap((s) => s.fields).find((f) => f.id === selectedField.id)?.formulaConfig ?? cfg;
|
|
12747
|
+
const newConds = [...freshCfg.multiple?.conditions ?? [], { value: "", expression: "" }];
|
|
12748
|
+
patchMultiple({ conditions: newConds });
|
|
12749
|
+
this.render();
|
|
12750
|
+
}
|
|
12751
|
+
}));
|
|
12752
|
+
const fallbackGroup = createElement("div", { className: "mb-3" });
|
|
12753
|
+
fallbackGroup.appendChild(createElement("label", {
|
|
12754
|
+
className: "block text-sm font-normal text-gray-700 dark:text-gray-300 mb-1",
|
|
12755
|
+
text: "Fallback Expression"
|
|
12756
|
+
}));
|
|
12757
|
+
const fallbackWidget = new FormulaEditorWidget({
|
|
12758
|
+
mode: "bracket",
|
|
12759
|
+
availableFields: fwFieldInfos,
|
|
12760
|
+
initialValue: cfg.multiple?.fallbackExpression ?? "",
|
|
12761
|
+
placeholder: "e.g. 0",
|
|
12762
|
+
onChange: (expr) => {
|
|
12763
|
+
patchMultiple({ fallbackExpression: expr });
|
|
12764
|
+
}
|
|
12765
|
+
});
|
|
12766
|
+
registerFormulaWidget(fallbackWidget);
|
|
12767
|
+
if (!lastFocusedFormulaWidget)
|
|
12768
|
+
lastFocusedFormulaWidget = fallbackWidget;
|
|
12769
|
+
fallbackGroup.appendChild(fallbackWidget.getElement());
|
|
12770
|
+
body.appendChild(fallbackGroup);
|
|
11404
12771
|
}
|
|
12772
|
+
if (fwFieldInfos.length > 0) {
|
|
12773
|
+
const insertGroup = createElement("div", { className: "mb-3" });
|
|
12774
|
+
insertGroup.appendChild(createElement("label", {
|
|
12775
|
+
className: "block text-sm font-normal text-gray-700 dark:text-gray-300 mb-1",
|
|
12776
|
+
text: "Insert Field Reference"
|
|
12777
|
+
}));
|
|
12778
|
+
const insertSelect = createElement("select", {
|
|
12779
|
+
className: "w-full px-3 py-2 border border-gray-200 dark:border-gray-700 rounded-md bg-transparent",
|
|
12780
|
+
onchange: (e) => {
|
|
12781
|
+
const sel = e.target;
|
|
12782
|
+
const ref = sel.value;
|
|
12783
|
+
if (!ref)
|
|
12784
|
+
return;
|
|
12785
|
+
const field = fwFieldInfos.find((f) => f.fieldName === ref || f.id === ref);
|
|
12786
|
+
const target = lastFocusedFormulaWidget;
|
|
12787
|
+
if (field && target) {
|
|
12788
|
+
target.insertField(field);
|
|
12789
|
+
}
|
|
12790
|
+
sel.value = "";
|
|
12791
|
+
}
|
|
12792
|
+
});
|
|
12793
|
+
insertSelect.appendChild(createElement("option", { value: "", text: "Select field to insert\u2026", selected: true }));
|
|
12794
|
+
fwFieldInfos.forEach((f) => {
|
|
12795
|
+
insertSelect.appendChild(createElement("option", {
|
|
12796
|
+
value: f.fieldName !== f.id ? f.fieldName : f.id,
|
|
12797
|
+
text: f.label
|
|
12798
|
+
// show only human-readable label
|
|
12799
|
+
}));
|
|
12800
|
+
});
|
|
12801
|
+
insertGroup.appendChild(insertSelect);
|
|
12802
|
+
body.appendChild(insertGroup);
|
|
12803
|
+
}
|
|
12804
|
+
body.appendChild(createElement("label", {
|
|
12805
|
+
className: "block text-sm font-normal text-gray-700 dark:text-gray-300 mb-1",
|
|
12806
|
+
text: "Math Helpers"
|
|
12807
|
+
}));
|
|
12808
|
+
const mathWrap = createElement("div", { className: "flex flex-wrap gap-1 mb-3" });
|
|
12809
|
+
const mathOps = [
|
|
12810
|
+
{ text: "+", insert: " + " },
|
|
12811
|
+
{ text: "-", insert: " - " },
|
|
12812
|
+
{ text: "*", insert: " * " },
|
|
12813
|
+
{ text: "/", insert: " / " },
|
|
12814
|
+
{ text: "(", insert: "(" },
|
|
12815
|
+
{ text: ")", insert: ")" },
|
|
12816
|
+
{ text: "ROUND", insert: "ROUND(" },
|
|
12817
|
+
{ text: "ABS", insert: "ABS(" },
|
|
12818
|
+
{ text: "MIN", insert: "MIN(" },
|
|
12819
|
+
{ text: "MAX", insert: "MAX(" },
|
|
12820
|
+
{ text: "FLOOR", insert: "FLOOR(" },
|
|
12821
|
+
{ text: "CEIL", insert: "CEIL(" }
|
|
12822
|
+
];
|
|
12823
|
+
mathOps.forEach((op) => {
|
|
12824
|
+
mathWrap.appendChild(createElement("button", {
|
|
12825
|
+
type: "button",
|
|
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",
|
|
12827
|
+
text: op.text,
|
|
12828
|
+
onclick: () => {
|
|
12829
|
+
lastFocusedFormulaWidget?.insertText(op.insert);
|
|
12830
|
+
}
|
|
12831
|
+
}));
|
|
12832
|
+
});
|
|
12833
|
+
body.appendChild(mathWrap);
|
|
12834
|
+
const dpGroup = createElement("div", { className: "mb-3" });
|
|
12835
|
+
dpGroup.appendChild(createElement("label", {
|
|
12836
|
+
className: "block text-sm font-normal text-gray-700 dark:text-gray-300 mb-1",
|
|
12837
|
+
text: "Decimal Places"
|
|
12838
|
+
}));
|
|
12839
|
+
dpGroup.appendChild(createElement("input", {
|
|
12840
|
+
type: "number",
|
|
12841
|
+
className: "w-full px-3 py-2 border border-gray-200 dark:border-gray-700 rounded-md bg-transparent",
|
|
12842
|
+
value: String(cfg.decimalPlaces ?? 2),
|
|
12843
|
+
min: "0",
|
|
12844
|
+
max: "10",
|
|
12845
|
+
placeholder: "2",
|
|
12846
|
+
oninput: (e) => {
|
|
12847
|
+
const v = e.target.value;
|
|
12848
|
+
patchFormulaConfig({ decimalPlaces: v !== "" ? parseInt(v, 10) : 2 });
|
|
12849
|
+
}
|
|
12850
|
+
}));
|
|
12851
|
+
body.appendChild(dpGroup);
|
|
12852
|
+
const zeroValues = {};
|
|
12853
|
+
availableFields.forEach((f) => {
|
|
12854
|
+
zeroValues[f.fieldName] = 0;
|
|
12855
|
+
zeroValues[f.id] = 0;
|
|
12856
|
+
});
|
|
12857
|
+
let previewText = "\u2014";
|
|
12858
|
+
try {
|
|
12859
|
+
const previewResult = evaluateFormulaConfig(cfg, zeroValues);
|
|
12860
|
+
if (!previewResult.error && !isNaN(previewResult.result)) {
|
|
12861
|
+
previewText = previewResult.result.toFixed(cfg.decimalPlaces ?? 2);
|
|
12862
|
+
} else if (previewResult.error) {
|
|
12863
|
+
previewText = `Error: ${previewResult.error}`;
|
|
12864
|
+
}
|
|
12865
|
+
} catch {
|
|
12866
|
+
}
|
|
12867
|
+
const previewSection = createElement("div", { className: "mb-4" });
|
|
12868
|
+
previewSection.appendChild(createElement("label", {
|
|
12869
|
+
className: "block text-xs font-semibold text-gray-500 uppercase tracking-wider mb-1",
|
|
12870
|
+
text: "Preview (fields = 0)"
|
|
12871
|
+
}));
|
|
12872
|
+
previewSection.appendChild(createElement("div", {
|
|
12873
|
+
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",
|
|
12874
|
+
text: previewText
|
|
12875
|
+
}));
|
|
12876
|
+
body.appendChild(previewSection);
|
|
11405
12877
|
}
|
|
11406
|
-
if (selectedField.type !== "image") {
|
|
12878
|
+
if (selectedField.type !== "image" && selectedField.type !== "formula") {
|
|
11407
12879
|
const placeholderGroup = createElement("div");
|
|
11408
12880
|
placeholderGroup.appendChild(createElement("label", { className: "block text-sm font-normal text-gray-700 dark:text-gray-300 mb-1", text: "Placeholder" }));
|
|
11409
12881
|
placeholderGroup.appendChild(createElement("input", {
|
|
@@ -12885,10 +14357,15 @@ exports.builderToPlatform = builderToPlatform;
|
|
|
12885
14357
|
exports.cleanFormSchema = cleanFormSchema;
|
|
12886
14358
|
exports.convertValidationObjectToArray = convertValidationObjectToArray;
|
|
12887
14359
|
exports.detectCircularDependency = detectCircularDependency;
|
|
14360
|
+
exports.detectFormulaFieldCircularDependency = detectFormulaFieldCircularDependency;
|
|
12888
14361
|
exports.evaluateFormula = evaluateFormula;
|
|
14362
|
+
exports.evaluateFormulaConfig = evaluateFormulaConfig;
|
|
14363
|
+
exports.evaluateFormulaExpression = evaluateFormulaExpression;
|
|
14364
|
+
exports.extractBracketFields = extractBracketFields;
|
|
12889
14365
|
exports.formStore = formStore;
|
|
12890
14366
|
exports.generateName = generateName;
|
|
12891
14367
|
exports.getColSpanFromWidth = getColSpanFromWidth;
|
|
14368
|
+
exports.getFieldsForFormula = getFieldsForFormula;
|
|
12892
14369
|
exports.getNumericFieldsForFormula = getNumericFieldsForFormula;
|
|
12893
14370
|
exports.getValidationConfigForAngular = getValidationConfigForAngular;
|
|
12894
14371
|
exports.initFormBuilder = initFormBuilder;
|
|
@@ -12897,5 +14374,6 @@ exports.parseWidth = parseWidth;
|
|
|
12897
14374
|
exports.platformToBuilder = platformToBuilder;
|
|
12898
14375
|
exports.resetNameGeneratorCounter = resetNameGeneratorCounter;
|
|
12899
14376
|
exports.validateFormula = validateFormula;
|
|
14377
|
+
exports.validateFormulaExpression = validateFormulaExpression;
|
|
12900
14378
|
//# sourceMappingURL=out.js.map
|
|
12901
14379
|
//# sourceMappingURL=index.js.map
|