@wildwinter/expr-editor 0.6.0 → 0.8.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 +8 -0
- package/dist/index.cjs +85 -37
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +22 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.js +85 -37
- package/dist/index.js.map +1 -1
- package/dist/styles.css +26 -0
- package/dist/styles.css.map +1 -1
- package/package.json +1 -1
package/dist/index.d.cts
CHANGED
|
@@ -232,6 +232,16 @@ interface EditCtx {
|
|
|
232
232
|
* (an enum-typed outcome target). Lets the string editor show an enum picker
|
|
233
233
|
* even without a comparison peer. No effect on non-root literals. */
|
|
234
234
|
valueEnumValues?: string[];
|
|
235
|
+
/** Host actions for a property pill (e.g. "Go to definition"). When set,
|
|
236
|
+
* right-clicking a property pill opens a menu of these actions. The host
|
|
237
|
+
* resolves what each does (navigation etc.); empty array = no menu. */
|
|
238
|
+
propertyActions?(ref: {
|
|
239
|
+
scope: string;
|
|
240
|
+
name: string;
|
|
241
|
+
}): Array<{
|
|
242
|
+
label: string;
|
|
243
|
+
run: () => void;
|
|
244
|
+
}>;
|
|
235
245
|
}
|
|
236
246
|
|
|
237
247
|
interface ExpressionEditorOptions {
|
|
@@ -286,6 +296,18 @@ interface ExpressionEditorOptions {
|
|
|
286
296
|
target: string;
|
|
287
297
|
flags: string[];
|
|
288
298
|
};
|
|
299
|
+
/** Placeholder for the raw-text textarea (defaults to a generic example).
|
|
300
|
+
* Value cells pass a value-flavoured hint (e.g. "autumn or @count + 1"). */
|
|
301
|
+
rawPlaceholder?: string;
|
|
302
|
+
/** Host actions for a property pill, shown on right-click (e.g. go to
|
|
303
|
+
* definition). Receives the property ref; returns the menu items. */
|
|
304
|
+
propertyActions?(ref: {
|
|
305
|
+
scope: string;
|
|
306
|
+
name: string;
|
|
307
|
+
}): Array<{
|
|
308
|
+
label: string;
|
|
309
|
+
run: () => void;
|
|
310
|
+
}>;
|
|
289
311
|
/** Emitted on every edit (name-form; "" when cleared). */
|
|
290
312
|
onChange: (src: string) => void;
|
|
291
313
|
/** Notified when the author starts (true) / stops (false) editing inside a popover
|
package/dist/index.d.ts
CHANGED
|
@@ -232,6 +232,16 @@ interface EditCtx {
|
|
|
232
232
|
* (an enum-typed outcome target). Lets the string editor show an enum picker
|
|
233
233
|
* even without a comparison peer. No effect on non-root literals. */
|
|
234
234
|
valueEnumValues?: string[];
|
|
235
|
+
/** Host actions for a property pill (e.g. "Go to definition"). When set,
|
|
236
|
+
* right-clicking a property pill opens a menu of these actions. The host
|
|
237
|
+
* resolves what each does (navigation etc.); empty array = no menu. */
|
|
238
|
+
propertyActions?(ref: {
|
|
239
|
+
scope: string;
|
|
240
|
+
name: string;
|
|
241
|
+
}): Array<{
|
|
242
|
+
label: string;
|
|
243
|
+
run: () => void;
|
|
244
|
+
}>;
|
|
235
245
|
}
|
|
236
246
|
|
|
237
247
|
interface ExpressionEditorOptions {
|
|
@@ -286,6 +296,18 @@ interface ExpressionEditorOptions {
|
|
|
286
296
|
target: string;
|
|
287
297
|
flags: string[];
|
|
288
298
|
};
|
|
299
|
+
/** Placeholder for the raw-text textarea (defaults to a generic example).
|
|
300
|
+
* Value cells pass a value-flavoured hint (e.g. "autumn or @count + 1"). */
|
|
301
|
+
rawPlaceholder?: string;
|
|
302
|
+
/** Host actions for a property pill, shown on right-click (e.g. go to
|
|
303
|
+
* definition). Receives the property ref; returns the menu items. */
|
|
304
|
+
propertyActions?(ref: {
|
|
305
|
+
scope: string;
|
|
306
|
+
name: string;
|
|
307
|
+
}): Array<{
|
|
308
|
+
label: string;
|
|
309
|
+
run: () => void;
|
|
310
|
+
}>;
|
|
289
311
|
/** Emitted on every edit (name-form; "" when cleared). */
|
|
290
312
|
onChange: (src: string) => void;
|
|
291
313
|
/** Notified when the author starts (true) / stops (false) editing inside a popover
|
package/dist/index.js
CHANGED
|
@@ -303,10 +303,11 @@ function el(tag, cls, children) {
|
|
|
303
303
|
}
|
|
304
304
|
return node;
|
|
305
305
|
}
|
|
306
|
-
function button(cls, label, onClick, title) {
|
|
306
|
+
function button(cls, label, onClick, title, aria) {
|
|
307
307
|
const b = el("button", cls, [label]);
|
|
308
308
|
b.type = "button";
|
|
309
309
|
if (title) b.title = title;
|
|
310
|
+
if (aria) b.setAttribute("aria-label", aria);
|
|
310
311
|
b.addEventListener("click", onClick);
|
|
311
312
|
return b;
|
|
312
313
|
}
|
|
@@ -387,10 +388,25 @@ function textField(opts) {
|
|
|
387
388
|
var FLAG_FNS = /* @__PURE__ */ new Set(["check_flags", "set_flags"]);
|
|
388
389
|
var TAG_ARG0_FNS = /* @__PURE__ */ new Set(["seen", "patter_seen", "visits", "patter_visits"]);
|
|
389
390
|
function pill(kind, label, onClick, opts = {}) {
|
|
390
|
-
const b = button(`exed-pill exed-pill-${kind}${opts.issue ? " exed-pill-err" : ""}`, label, () => onClick(b), opts.issue ?? opts.title);
|
|
391
|
+
const b = button(`exed-pill exed-pill-${kind}${opts.issue ? " exed-pill-err" : ""}`, label, () => onClick(b), opts.issue ?? opts.title, opts.aria);
|
|
392
|
+
b.setAttribute("aria-haspopup", "dialog");
|
|
391
393
|
if (opts.path) b.dataset["exedPath"] = opts.path.join("/");
|
|
392
394
|
return b;
|
|
393
395
|
}
|
|
396
|
+
var OP_ARIA = {
|
|
397
|
+
"==": "equals",
|
|
398
|
+
"!=": "not equal to",
|
|
399
|
+
">": "greater than",
|
|
400
|
+
">=": "at least",
|
|
401
|
+
"<": "less than",
|
|
402
|
+
"<=": "at most",
|
|
403
|
+
"and": "and",
|
|
404
|
+
"or": "or",
|
|
405
|
+
"+": "plus",
|
|
406
|
+
"-": "minus",
|
|
407
|
+
"*": "times",
|
|
408
|
+
"/": "divided by"
|
|
409
|
+
};
|
|
394
410
|
var issueText = (ctx, path) => {
|
|
395
411
|
const list = issuesAt(ctx.byPath, path);
|
|
396
412
|
return list.length ? list.map((i) => i.message).join("; ") : void 0;
|
|
@@ -418,7 +434,9 @@ function renderNode(node, path, ctx, parentOp, side) {
|
|
|
418
434
|
case "scopedvar": {
|
|
419
435
|
const entry = lookup(ctx.catalogue, node.scope, node.name);
|
|
420
436
|
const label = displayName({ scope: node.scope, name: node.name }, ctx.defaultScope);
|
|
421
|
-
|
|
437
|
+
const varPill = pill("var", label, (b) => variableEditor(ctx, path, node, b), { issue: issue ?? (entry ? void 0 : `Unknown property ${label}`) });
|
|
438
|
+
attachPropertyMenu(ctx, node, varPill);
|
|
439
|
+
return varPill;
|
|
422
440
|
}
|
|
423
441
|
case "flagdelta":
|
|
424
442
|
return pill(node.sign === "+" ? "flagpos" : "flagneg", `${node.sign}${node.name || "flag"}`, (b) => flagEditor(ctx, path, node, b), { issue, path });
|
|
@@ -435,7 +453,7 @@ function renderNode(node, path, ctx, parentOp, side) {
|
|
|
435
453
|
}
|
|
436
454
|
case "unary": {
|
|
437
455
|
const row = el("span", "exed-unary");
|
|
438
|
-
row.append(pill("op", UNARY_LABEL[node.op], () => ctx.apply(deleteOrUnwrapNot(ctx, path, node)), { title: "remove" }));
|
|
456
|
+
row.append(pill("op", UNARY_LABEL[node.op], () => ctx.apply(deleteOrUnwrapNot(ctx, path, node)), { title: "remove", aria: `${OP_ARIA[node.op] ?? node.op} (click to remove)` }));
|
|
439
457
|
row.append(renderNode(node.operand, [...path, "operand"], ctx));
|
|
440
458
|
return row;
|
|
441
459
|
}
|
|
@@ -447,7 +465,7 @@ function renderNode(node, path, ctx, parentOp, side) {
|
|
|
447
465
|
const swap = opSwapGroup(node.op);
|
|
448
466
|
const opPill = pill("op", BINARY_LABEL[node.op], (b) => {
|
|
449
467
|
if (swap) operatorEditor(ctx, path, node.op, swap, b);
|
|
450
|
-
}, { title: swap ? "swap operator" : void 0 });
|
|
468
|
+
}, { title: swap ? "swap operator" : void 0, aria: OP_ARIA[node.op] ?? node.op });
|
|
451
469
|
if (!swap) opPill.classList.add("exed-op-structural");
|
|
452
470
|
row.append(opPill);
|
|
453
471
|
row.append(renderNode(node.right, [...path, "right"], ctx, node.op, "right"));
|
|
@@ -456,10 +474,47 @@ function renderNode(node, path, ctx, parentOp, side) {
|
|
|
456
474
|
}
|
|
457
475
|
}
|
|
458
476
|
}
|
|
477
|
+
function signToggle(current, onPick) {
|
|
478
|
+
const row = el("div", "exed-sign");
|
|
479
|
+
["+", "-"].forEach((s) => {
|
|
480
|
+
const cls = `exed-signbtn${s === current ? s === "+" ? " sel-pos" : " sel-neg" : ""}`;
|
|
481
|
+
row.append(button(cls, s === "+" ? "+ set" : "\u2212 unset", () => onPick(s)));
|
|
482
|
+
});
|
|
483
|
+
return row;
|
|
484
|
+
}
|
|
459
485
|
var deleteOrUnwrapNot = (ctx, path, node) => (
|
|
460
486
|
// clicking the NOT pill strips it (keeps the operand)
|
|
461
487
|
setNodeAt(ctx.getAst(), path, node.operand)
|
|
462
488
|
);
|
|
489
|
+
function attachPropertyMenu(ctx, node, pillEl) {
|
|
490
|
+
if (!ctx.propertyActions) return;
|
|
491
|
+
const actions = ctx.propertyActions({ scope: node.scope, name: node.name });
|
|
492
|
+
if (!actions.length) return;
|
|
493
|
+
pillEl.addEventListener("contextmenu", (e) => {
|
|
494
|
+
e.preventDefault();
|
|
495
|
+
ctx.openPopover(pillEl, (close) => {
|
|
496
|
+
const wrap = el("div", "exed-menu");
|
|
497
|
+
for (const a of actions) wrap.append(button("exed-opt", a.label, () => {
|
|
498
|
+
a.run();
|
|
499
|
+
close();
|
|
500
|
+
}));
|
|
501
|
+
return wrap;
|
|
502
|
+
});
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
function isCompoundComparison(node, ctx) {
|
|
506
|
+
if (node.kind !== "binary" || !isComparisonOp(node.op)) return false;
|
|
507
|
+
const numCall = (n) => n.kind === "call" && ctx.dialect.functions[n.name]?.returnType === "number";
|
|
508
|
+
const isNum = (n) => n.kind === "number";
|
|
509
|
+
return numCall(node.left) && isNum(node.right) || numCall(node.right) && isNum(node.left);
|
|
510
|
+
}
|
|
511
|
+
function effectiveDeletePath(ctx, path) {
|
|
512
|
+
const last = path[path.length - 1];
|
|
513
|
+
if (last !== "left" && last !== "right") return path;
|
|
514
|
+
const parentPath = path.slice(0, -1);
|
|
515
|
+
const parent = getNodeAt(ctx.getAst(), parentPath);
|
|
516
|
+
return parent && isCompoundComparison(parent, ctx) ? parentPath : path;
|
|
517
|
+
}
|
|
463
518
|
function boolEditor(ctx, path, value, anchor) {
|
|
464
519
|
ctx.openPopover(anchor, (close) => {
|
|
465
520
|
const wrap = el("div", "exed-menu");
|
|
@@ -545,14 +600,10 @@ function flagEditor(ctx, path, node, anchor) {
|
|
|
545
600
|
ctx.openPopover(anchor, (close) => {
|
|
546
601
|
const wrap = el("div", "exed-menu");
|
|
547
602
|
wrap.append(el("div", "exed-menu-head", ["Flag set?"]));
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
close();
|
|
553
|
-
}));
|
|
554
|
-
}
|
|
555
|
-
wrap.append(signRow);
|
|
603
|
+
wrap.append(signToggle(node.sign, (s) => {
|
|
604
|
+
replace(ctx, path, flagDelta(s, node.name));
|
|
605
|
+
close();
|
|
606
|
+
}));
|
|
556
607
|
const names = (entry?.enumValues ?? []).filter((n) => !used.includes(n));
|
|
557
608
|
if (names.length) {
|
|
558
609
|
wrap.append(el("div", "exed-menu-head", ["Flag"]));
|
|
@@ -575,7 +626,7 @@ function flagEditor(ctx, path, node, anchor) {
|
|
|
575
626
|
return wrap;
|
|
576
627
|
});
|
|
577
628
|
}
|
|
578
|
-
var canDelete = (ctx, path) => !(ctx.requireNonEmpty && path.length === 0);
|
|
629
|
+
var canDelete = (ctx, path) => !(ctx.requireNonEmpty && effectiveDeletePath(ctx, path).length === 0);
|
|
579
630
|
function callEditor(ctx, path, node, anchor) {
|
|
580
631
|
ctx.openPopover(anchor, (close) => {
|
|
581
632
|
const wrap = el("div", "exed-menu");
|
|
@@ -589,7 +640,7 @@ function callEditor(ctx, path, node, anchor) {
|
|
|
589
640
|
}
|
|
590
641
|
if (canDelete(ctx, path)) {
|
|
591
642
|
wrap.append(button("exed-opt danger", "Delete", () => {
|
|
592
|
-
ctx.apply(deleteAt(ctx.getAst(), path));
|
|
643
|
+
ctx.apply(deleteAt(ctx.getAst(), effectiveDeletePath(ctx, path)));
|
|
593
644
|
close();
|
|
594
645
|
}));
|
|
595
646
|
}
|
|
@@ -604,7 +655,7 @@ function variableEditor(ctx, path, node, anchor) {
|
|
|
604
655
|
close();
|
|
605
656
|
},
|
|
606
657
|
...canDelete(ctx, path) ? { footer: button("exed-opt danger", "Delete", () => {
|
|
607
|
-
ctx.apply(deleteAt(ctx.getAst(), path));
|
|
658
|
+
ctx.apply(deleteAt(ctx.getAst(), effectiveDeletePath(ctx, path)));
|
|
608
659
|
close();
|
|
609
660
|
}) } : {}
|
|
610
661
|
}));
|
|
@@ -874,15 +925,17 @@ function renderRow(ctx, row, env) {
|
|
|
874
925
|
if (row.kind === "container") return renderContainer(ctx, row, env);
|
|
875
926
|
return renderLeaf(ctx, row, env);
|
|
876
927
|
}
|
|
877
|
-
function notToggle(ctx, path) {
|
|
878
|
-
|
|
928
|
+
function notToggle(ctx, path, pressed) {
|
|
929
|
+
const b = button("exed-rowbtn", "NOT", () => ctx.apply(toggleContainerNot(ctx.getAst(), path)), "toggle NOT", pressed ? "remove NOT" : "apply NOT");
|
|
930
|
+
b.setAttribute("aria-pressed", String(pressed));
|
|
931
|
+
return b;
|
|
879
932
|
}
|
|
880
933
|
function rowActions(ctx, row, env) {
|
|
881
934
|
const acts = el("div", "exed-rowacts");
|
|
882
|
-
acts.append(notToggle(ctx, row.path));
|
|
935
|
+
acts.append(notToggle(ctx, row.path, row.negated));
|
|
883
936
|
if (env.chainPath && env.count != null && env.index != null) {
|
|
884
|
-
if (env.index > 0) acts.append(button("exed-rowbtn", "\u2191", () => ctx.apply(moveChildInContainer(ctx.getAst(), env.chainPath, env.index, env.index - 1)), "move up"));
|
|
885
|
-
if (env.index < env.count - 1) acts.append(button("exed-rowbtn", "\u2193", () => ctx.apply(moveChildInContainer(ctx.getAst(), env.chainPath, env.index, env.index + 1)), "move down"));
|
|
937
|
+
if (env.index > 0) acts.append(button("exed-rowbtn", "\u2191", () => ctx.apply(moveChildInContainer(ctx.getAst(), env.chainPath, env.index, env.index - 1)), "move up", "move up"));
|
|
938
|
+
if (env.index < env.count - 1) acts.append(button("exed-rowbtn", "\u2193", () => ctx.apply(moveChildInContainer(ctx.getAst(), env.chainPath, env.index, env.index + 1)), "move down", "move down"));
|
|
886
939
|
}
|
|
887
940
|
acts.append(button("exed-rowbtn danger", "\u2715", () => {
|
|
888
941
|
if (env.root) {
|
|
@@ -891,7 +944,7 @@ function rowActions(ctx, row, env) {
|
|
|
891
944
|
}
|
|
892
945
|
const target = redirectDeleteForPlaceholderSibling(ctx.getAst(), row.path);
|
|
893
946
|
ctx.apply(deleteAt(ctx.getAst(), target));
|
|
894
|
-
}, "delete"));
|
|
947
|
+
}, "delete", "delete this condition"));
|
|
895
948
|
return acts;
|
|
896
949
|
}
|
|
897
950
|
function renderLeaf(ctx, row, env) {
|
|
@@ -919,8 +972,8 @@ function renderContainer(ctx, row, env) {
|
|
|
919
972
|
head.append(el("span", "exed-grouplabel", [label]));
|
|
920
973
|
}
|
|
921
974
|
const headActs = el("div", "exed-rowacts");
|
|
922
|
-
headActs.append(notToggle(ctx, row.path));
|
|
923
|
-
if (!env.root) headActs.append(button("exed-rowbtn danger", "\u2715", () => ctx.apply(deleteAt(ctx.getAst(), row.path)), "delete group"));
|
|
975
|
+
headActs.append(notToggle(ctx, row.path, row.negated));
|
|
976
|
+
if (!env.root) headActs.append(button("exed-rowbtn danger", "\u2715", () => ctx.apply(deleteAt(ctx.getAst(), row.path)), "delete group", "delete this group"));
|
|
924
977
|
head.append(headActs);
|
|
925
978
|
box.append(head);
|
|
926
979
|
const body = el("div", "exed-groupbody");
|
|
@@ -1073,6 +1126,7 @@ function mountExpressionEditor(host, opts) {
|
|
|
1073
1126
|
},
|
|
1074
1127
|
...opts.requireNonEmpty ? { requireNonEmpty: true } : {},
|
|
1075
1128
|
...opts.valueEnumValues ? { valueEnumValues: opts.valueEnumValues } : {},
|
|
1129
|
+
...opts.propertyActions ? { propertyActions: opts.propertyActions } : {},
|
|
1076
1130
|
...opts.pickNode ? { pickNode: opts.pickNode } : {},
|
|
1077
1131
|
...opts.nodeLabel ? { nodeLabel: opts.nodeLabel } : {}
|
|
1078
1132
|
};
|
|
@@ -1109,7 +1163,7 @@ function mountExpressionEditor(host, opts) {
|
|
|
1109
1163
|
function rawArea() {
|
|
1110
1164
|
const ta = el("textarea", "exed-raw");
|
|
1111
1165
|
ta.value = src;
|
|
1112
|
-
ta.placeholder = "@gold > 0 and @met_anna";
|
|
1166
|
+
ta.placeholder = opts.rawPlaceholder ?? "@gold > 0 and @met_anna";
|
|
1113
1167
|
ta.rows = 2;
|
|
1114
1168
|
ta.addEventListener("input", () => {
|
|
1115
1169
|
src = ta.value;
|
|
@@ -1162,21 +1216,15 @@ function mountExpressionEditor(host, opts) {
|
|
|
1162
1216
|
let sign = "+";
|
|
1163
1217
|
const wrap = el("div", "exed-menu");
|
|
1164
1218
|
wrap.append(el("div", "exed-menu-head", ["Add flag"]));
|
|
1165
|
-
const
|
|
1219
|
+
const signHost = el("div");
|
|
1166
1220
|
const drawSign = () => {
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
}),
|
|
1172
|
-
button(`exed-opt${sign === "-" ? " sel" : ""}`, "\u2212 unset", () => {
|
|
1173
|
-
sign = "-";
|
|
1174
|
-
drawSign();
|
|
1175
|
-
})
|
|
1176
|
-
);
|
|
1221
|
+
signHost.replaceChildren(signToggle(sign, (s) => {
|
|
1222
|
+
sign = s;
|
|
1223
|
+
drawSign();
|
|
1224
|
+
}));
|
|
1177
1225
|
};
|
|
1178
1226
|
drawSign();
|
|
1179
|
-
wrap.append(
|
|
1227
|
+
wrap.append(signHost);
|
|
1180
1228
|
const commit = (name) => {
|
|
1181
1229
|
const delta = flagDelta(sign, name);
|
|
1182
1230
|
emit(toSrc(call ? { ...call, args: [...call.args, delta] } : callNode("set_flags", [parseRef(opts.flagValue.target), delta])));
|