@wildwinter/expr-editor 0.3.0 → 0.5.0
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/README.md +12 -0
- package/dist/index.cjs +65 -11
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +27 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.js +65 -11
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.cts
CHANGED
|
@@ -224,6 +224,14 @@ interface EditCtx {
|
|
|
224
224
|
* next render — used by insert-then-refine templates so the author lands
|
|
225
225
|
* straight in the first unfilled slot instead of chasing the error ring. */
|
|
226
226
|
requestFocus?(path: AstPath): void;
|
|
227
|
+
/** When true, a delete that would empty the whole expression is blocked (the
|
|
228
|
+
* Delete affordance is withheld). Used by single-value fields that must
|
|
229
|
+
* always hold at least one term; conditions leave it false (empty = always). */
|
|
230
|
+
requireNonEmpty?: boolean;
|
|
231
|
+
/** Enum values offered when editing the single root literal of a value field
|
|
232
|
+
* (an enum-typed outcome target). Lets the string editor show an enum picker
|
|
233
|
+
* even without a comparison peer. No effect on non-root literals. */
|
|
234
|
+
valueEnumValues?: string[];
|
|
227
235
|
}
|
|
228
236
|
|
|
229
237
|
interface ExpressionEditorOptions {
|
|
@@ -258,6 +266,25 @@ interface ExpressionEditorOptions {
|
|
|
258
266
|
* container inside a focus-trapping dialog (Radix, etc.) so opening a pill's
|
|
259
267
|
* popover does not dismiss the surrounding dialog. */
|
|
260
268
|
popoverContainer?: HTMLElement;
|
|
269
|
+
/** Block a delete that would empty the whole expression (single-value fields
|
|
270
|
+
* that must always hold a term). Conditions leave this off (empty = always). */
|
|
271
|
+
requireNonEmpty?: boolean;
|
|
272
|
+
/** Enum values for the single root literal of a value field (an enum-typed
|
|
273
|
+
* target): the literal editor then offers them as a picker. Flat mode only. */
|
|
274
|
+
valueEnumValues?: string[];
|
|
275
|
+
/** Flat single-value field: an empty value renders one editable "set a value…"
|
|
276
|
+
* placeholder pill (a root string literal) rather than the condition
|
|
277
|
+
* clause-menu empty state. Pairs with mode:"flat". */
|
|
278
|
+
valueField?: boolean;
|
|
279
|
+
/** Flat compact editor for a `set_flags(@target, +a, -b)` value whose target is
|
|
280
|
+
* implied by context (an outcome row's target column). Renders only the
|
|
281
|
+
* flag-delta pills plus an "+ flag" chip, hiding the function name and target
|
|
282
|
+
* arg. `target` is the name-form ref of the flags property (e.g. "@quests");
|
|
283
|
+
* it seeds the call on the first flag add. Pairs with mode:"flat", and set
|
|
284
|
+
* requireNonEmpty so the last flag can't be removed. */
|
|
285
|
+
flagValue?: {
|
|
286
|
+
target: string;
|
|
287
|
+
};
|
|
261
288
|
/** Emitted on every edit (name-form; "" when cleared). */
|
|
262
289
|
onChange: (src: string) => void;
|
|
263
290
|
/** Notified when the author starts (true) / stops (false) editing inside a popover
|
package/dist/index.d.ts
CHANGED
|
@@ -224,6 +224,14 @@ interface EditCtx {
|
|
|
224
224
|
* next render — used by insert-then-refine templates so the author lands
|
|
225
225
|
* straight in the first unfilled slot instead of chasing the error ring. */
|
|
226
226
|
requestFocus?(path: AstPath): void;
|
|
227
|
+
/** When true, a delete that would empty the whole expression is blocked (the
|
|
228
|
+
* Delete affordance is withheld). Used by single-value fields that must
|
|
229
|
+
* always hold at least one term; conditions leave it false (empty = always). */
|
|
230
|
+
requireNonEmpty?: boolean;
|
|
231
|
+
/** Enum values offered when editing the single root literal of a value field
|
|
232
|
+
* (an enum-typed outcome target). Lets the string editor show an enum picker
|
|
233
|
+
* even without a comparison peer. No effect on non-root literals. */
|
|
234
|
+
valueEnumValues?: string[];
|
|
227
235
|
}
|
|
228
236
|
|
|
229
237
|
interface ExpressionEditorOptions {
|
|
@@ -258,6 +266,25 @@ interface ExpressionEditorOptions {
|
|
|
258
266
|
* container inside a focus-trapping dialog (Radix, etc.) so opening a pill's
|
|
259
267
|
* popover does not dismiss the surrounding dialog. */
|
|
260
268
|
popoverContainer?: HTMLElement;
|
|
269
|
+
/** Block a delete that would empty the whole expression (single-value fields
|
|
270
|
+
* that must always hold a term). Conditions leave this off (empty = always). */
|
|
271
|
+
requireNonEmpty?: boolean;
|
|
272
|
+
/** Enum values for the single root literal of a value field (an enum-typed
|
|
273
|
+
* target): the literal editor then offers them as a picker. Flat mode only. */
|
|
274
|
+
valueEnumValues?: string[];
|
|
275
|
+
/** Flat single-value field: an empty value renders one editable "set a value…"
|
|
276
|
+
* placeholder pill (a root string literal) rather than the condition
|
|
277
|
+
* clause-menu empty state. Pairs with mode:"flat". */
|
|
278
|
+
valueField?: boolean;
|
|
279
|
+
/** Flat compact editor for a `set_flags(@target, +a, -b)` value whose target is
|
|
280
|
+
* implied by context (an outcome row's target column). Renders only the
|
|
281
|
+
* flag-delta pills plus an "+ flag" chip, hiding the function name and target
|
|
282
|
+
* arg. `target` is the name-form ref of the flags property (e.g. "@quests");
|
|
283
|
+
* it seeds the call on the first flag add. Pairs with mode:"flat", and set
|
|
284
|
+
* requireNonEmpty so the last flag can't be removed. */
|
|
285
|
+
flagValue?: {
|
|
286
|
+
target: string;
|
|
287
|
+
};
|
|
261
288
|
/** Emitted on every edit (name-form; "" when cleared). */
|
|
262
289
|
onChange: (src: string) => void;
|
|
263
290
|
/** Notified when the author starts (true) / stops (false) editing inside a popover
|
package/dist/index.js
CHANGED
|
@@ -488,11 +488,14 @@ function numberEditor(ctx, path, value, anchor) {
|
|
|
488
488
|
function stringEditor(ctx, path, node, anchor) {
|
|
489
489
|
const peer = findEnumPeer(ctx.getAst(), path);
|
|
490
490
|
const enumEntry = peer ? lookup(ctx.catalogue, peer.scope, peer.name) : null;
|
|
491
|
+
const rootValueEnums = path.length === 0 && ctx.valueEnumValues?.length ? ctx.valueEnumValues : null;
|
|
492
|
+
const enumValues = enumEntry?.enumValues?.length ? enumEntry.enumValues : rootValueEnums;
|
|
493
|
+
const enumHeadLabel = enumEntry ? `${displayName(enumEntry, ctx.defaultScope)} value` : "Value";
|
|
491
494
|
ctx.openPopover(anchor, (close) => {
|
|
492
|
-
if (
|
|
495
|
+
if (enumValues?.length) {
|
|
493
496
|
const wrap = el("div", "exed-menu");
|
|
494
|
-
wrap.append(el("div", "exed-menu-head", [
|
|
495
|
-
for (const v of
|
|
497
|
+
wrap.append(el("div", "exed-menu-head", [enumHeadLabel]));
|
|
498
|
+
for (const v of enumValues) {
|
|
496
499
|
wrap.append(button(`exed-opt${v === node.value ? " sel" : ""}`, v, () => {
|
|
497
500
|
replace(ctx, path, strLit(v));
|
|
498
501
|
close();
|
|
@@ -536,7 +539,9 @@ function flagEditor(ctx, path, node, anchor) {
|
|
|
536
539
|
const call = getNodeAt(ctx.getAst(), callPath);
|
|
537
540
|
const flagsVar = call?.kind === "call" ? call.args[0] : void 0;
|
|
538
541
|
const entry = flagsVar?.kind === "scopedvar" ? lookup(ctx.catalogue, flagsVar.scope, flagsVar.name) : null;
|
|
542
|
+
const flagCount = call?.kind === "call" ? call.args.filter((a) => a.kind === "flagdelta").length : 0;
|
|
539
543
|
const used = call?.kind === "call" ? call.args.filter((a, i) => a.kind === "flagdelta" && i !== path[path.length - 1]).map((a) => a.name) : [];
|
|
544
|
+
const canRemove = !(ctx.requireNonEmpty && flagCount <= 1);
|
|
540
545
|
ctx.openPopover(anchor, (close) => {
|
|
541
546
|
const wrap = el("div", "exed-menu");
|
|
542
547
|
wrap.append(el("div", "exed-menu-head", ["Flag set?"]));
|
|
@@ -561,9 +566,16 @@ function flagEditor(ctx, path, node, anchor) {
|
|
|
561
566
|
close();
|
|
562
567
|
} }));
|
|
563
568
|
}
|
|
569
|
+
if (canRemove) {
|
|
570
|
+
wrap.append(button("exed-opt danger", "Remove flag", () => {
|
|
571
|
+
ctx.apply(deleteAt(ctx.getAst(), path));
|
|
572
|
+
close();
|
|
573
|
+
}));
|
|
574
|
+
}
|
|
564
575
|
return wrap;
|
|
565
576
|
});
|
|
566
577
|
}
|
|
578
|
+
var canDelete = (ctx, path) => !(ctx.requireNonEmpty && path.length === 0);
|
|
567
579
|
function callEditor(ctx, path, node, anchor) {
|
|
568
580
|
ctx.openPopover(anchor, (close) => {
|
|
569
581
|
const wrap = el("div", "exed-menu");
|
|
@@ -575,10 +587,12 @@ function callEditor(ctx, path, node, anchor) {
|
|
|
575
587
|
close();
|
|
576
588
|
}));
|
|
577
589
|
}
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
590
|
+
if (canDelete(ctx, path)) {
|
|
591
|
+
wrap.append(button("exed-opt danger", "Delete", () => {
|
|
592
|
+
ctx.apply(deleteAt(ctx.getAst(), path));
|
|
593
|
+
close();
|
|
594
|
+
}));
|
|
595
|
+
}
|
|
582
596
|
return wrap;
|
|
583
597
|
});
|
|
584
598
|
}
|
|
@@ -589,10 +603,10 @@ function variableEditor(ctx, path, node, anchor) {
|
|
|
589
603
|
replace(ctx, path, scopedVar(entry.scope, entry.name));
|
|
590
604
|
close();
|
|
591
605
|
},
|
|
592
|
-
footer: button("exed-opt danger", "Delete", () => {
|
|
606
|
+
...canDelete(ctx, path) ? { footer: button("exed-opt danger", "Delete", () => {
|
|
593
607
|
ctx.apply(deleteAt(ctx.getAst(), path));
|
|
594
608
|
close();
|
|
595
|
-
})
|
|
609
|
+
}) } : {}
|
|
596
610
|
}));
|
|
597
611
|
}
|
|
598
612
|
function propertyPicker(ctx, opts) {
|
|
@@ -1057,6 +1071,8 @@ function mountExpressionEditor(host, opts) {
|
|
|
1057
1071
|
requestFocus: (p) => {
|
|
1058
1072
|
pendingFocus = p;
|
|
1059
1073
|
},
|
|
1074
|
+
...opts.requireNonEmpty ? { requireNonEmpty: true } : {},
|
|
1075
|
+
...opts.valueEnumValues ? { valueEnumValues: opts.valueEnumValues } : {},
|
|
1060
1076
|
...opts.pickNode ? { pickNode: opts.pickNode } : {},
|
|
1061
1077
|
...opts.nodeLabel ? { nodeLabel: opts.nodeLabel } : {}
|
|
1062
1078
|
};
|
|
@@ -1065,10 +1081,14 @@ function mountExpressionEditor(host, opts) {
|
|
|
1065
1081
|
closePopover();
|
|
1066
1082
|
host.replaceChildren();
|
|
1067
1083
|
const v = validateSource(src, opts.schema, opts.dialect);
|
|
1068
|
-
|
|
1084
|
+
const flagCompact = !!opts.flagValue && (opts.mode ?? "tree") === "flat";
|
|
1085
|
+
const astIsFlagCall = v.ast?.kind === "call" && (v.ast.name === "set_flags" || v.ast.name === "check_flags");
|
|
1086
|
+
if (raw || v.unparseable || flagCompact && !!v.ast && !astIsFlagCall) {
|
|
1069
1087
|
host.append(rawArea());
|
|
1088
|
+
} else if (flagCompact) {
|
|
1089
|
+
host.append(flagValueBody(astIsFlagCall ? v.ast : null));
|
|
1070
1090
|
} else if (!src.trim() || !v.ast) {
|
|
1071
|
-
host.append(emptyState());
|
|
1091
|
+
host.append(opts.valueField ? valueEmptyState() : emptyState());
|
|
1072
1092
|
} else {
|
|
1073
1093
|
const ctx = buildCtx(v.ast);
|
|
1074
1094
|
const body = el("div", "exed-body");
|
|
@@ -1112,6 +1132,40 @@ function mountExpressionEditor(host, opts) {
|
|
|
1112
1132
|
}));
|
|
1113
1133
|
return wrap;
|
|
1114
1134
|
}
|
|
1135
|
+
function valueEmptyState() {
|
|
1136
|
+
const ctx = buildCtx(strLit(""));
|
|
1137
|
+
return el("div", "exed-flat", [renderNode(strLit(""), [], ctx)]);
|
|
1138
|
+
}
|
|
1139
|
+
function parseRef(ref) {
|
|
1140
|
+
const bare = ref.replace(/^@/, "");
|
|
1141
|
+
const dot = bare.indexOf(".");
|
|
1142
|
+
return dot >= 0 ? scopedVar(bare.slice(0, dot), bare.slice(dot + 1)) : scopedVar(defaultScope, bare);
|
|
1143
|
+
}
|
|
1144
|
+
function flagValueBody(call) {
|
|
1145
|
+
const wrap = el("div", "exed-flat exed-flagvalue");
|
|
1146
|
+
if (call) {
|
|
1147
|
+
const ctx = buildCtx(call);
|
|
1148
|
+
call.args.forEach((arg, i) => {
|
|
1149
|
+
if (arg.kind === "flagdelta") wrap.append(renderNode(arg, ["args", i], ctx));
|
|
1150
|
+
});
|
|
1151
|
+
wrap.append(addFlagChip(call));
|
|
1152
|
+
} else {
|
|
1153
|
+
wrap.append(addFlagChip(null));
|
|
1154
|
+
}
|
|
1155
|
+
return wrap;
|
|
1156
|
+
}
|
|
1157
|
+
function addFlagChip(call) {
|
|
1158
|
+
return button("exed-add exed-addflag", "+ flag", () => {
|
|
1159
|
+
if (call) {
|
|
1160
|
+
pendingFocus = ["args", call.args.length];
|
|
1161
|
+
emit(toSrc({ ...call, args: [...call.args, flagDelta("+", "")] }));
|
|
1162
|
+
} else {
|
|
1163
|
+
const seeded = callNode("set_flags", [parseRef(opts.flagValue.target), flagDelta("+", "")]);
|
|
1164
|
+
pendingFocus = ["args", 1];
|
|
1165
|
+
emit(toSrc(seeded));
|
|
1166
|
+
}
|
|
1167
|
+
}, "add a flag");
|
|
1168
|
+
}
|
|
1115
1169
|
function addTermControl(ctx) {
|
|
1116
1170
|
const boolean = opts.addTerm === "boolean";
|
|
1117
1171
|
const ops = boolean ? ["and", "or"] : ARITHMETIC_OPS;
|