@wildwinter/expr-editor 0.7.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 +67 -17
- 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 +67 -17
- package/dist/index.js.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"));
|
|
@@ -468,6 +486,35 @@ var deleteOrUnwrapNot = (ctx, path, node) => (
|
|
|
468
486
|
// clicking the NOT pill strips it (keeps the operand)
|
|
469
487
|
setNodeAt(ctx.getAst(), path, node.operand)
|
|
470
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
|
+
}
|
|
471
518
|
function boolEditor(ctx, path, value, anchor) {
|
|
472
519
|
ctx.openPopover(anchor, (close) => {
|
|
473
520
|
const wrap = el("div", "exed-menu");
|
|
@@ -579,7 +626,7 @@ function flagEditor(ctx, path, node, anchor) {
|
|
|
579
626
|
return wrap;
|
|
580
627
|
});
|
|
581
628
|
}
|
|
582
|
-
var canDelete = (ctx, path) => !(ctx.requireNonEmpty && path.length === 0);
|
|
629
|
+
var canDelete = (ctx, path) => !(ctx.requireNonEmpty && effectiveDeletePath(ctx, path).length === 0);
|
|
583
630
|
function callEditor(ctx, path, node, anchor) {
|
|
584
631
|
ctx.openPopover(anchor, (close) => {
|
|
585
632
|
const wrap = el("div", "exed-menu");
|
|
@@ -593,7 +640,7 @@ function callEditor(ctx, path, node, anchor) {
|
|
|
593
640
|
}
|
|
594
641
|
if (canDelete(ctx, path)) {
|
|
595
642
|
wrap.append(button("exed-opt danger", "Delete", () => {
|
|
596
|
-
ctx.apply(deleteAt(ctx.getAst(), path));
|
|
643
|
+
ctx.apply(deleteAt(ctx.getAst(), effectiveDeletePath(ctx, path)));
|
|
597
644
|
close();
|
|
598
645
|
}));
|
|
599
646
|
}
|
|
@@ -608,7 +655,7 @@ function variableEditor(ctx, path, node, anchor) {
|
|
|
608
655
|
close();
|
|
609
656
|
},
|
|
610
657
|
...canDelete(ctx, path) ? { footer: button("exed-opt danger", "Delete", () => {
|
|
611
|
-
ctx.apply(deleteAt(ctx.getAst(), path));
|
|
658
|
+
ctx.apply(deleteAt(ctx.getAst(), effectiveDeletePath(ctx, path)));
|
|
612
659
|
close();
|
|
613
660
|
}) } : {}
|
|
614
661
|
}));
|
|
@@ -878,15 +925,17 @@ function renderRow(ctx, row, env) {
|
|
|
878
925
|
if (row.kind === "container") return renderContainer(ctx, row, env);
|
|
879
926
|
return renderLeaf(ctx, row, env);
|
|
880
927
|
}
|
|
881
|
-
function notToggle(ctx, path) {
|
|
882
|
-
|
|
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;
|
|
883
932
|
}
|
|
884
933
|
function rowActions(ctx, row, env) {
|
|
885
934
|
const acts = el("div", "exed-rowacts");
|
|
886
|
-
acts.append(notToggle(ctx, row.path));
|
|
935
|
+
acts.append(notToggle(ctx, row.path, row.negated));
|
|
887
936
|
if (env.chainPath && env.count != null && env.index != null) {
|
|
888
|
-
if (env.index > 0) acts.append(button("exed-rowbtn", "\u2191", () => ctx.apply(moveChildInContainer(ctx.getAst(), env.chainPath, env.index, env.index - 1)), "move up"));
|
|
889
|
-
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"));
|
|
890
939
|
}
|
|
891
940
|
acts.append(button("exed-rowbtn danger", "\u2715", () => {
|
|
892
941
|
if (env.root) {
|
|
@@ -895,7 +944,7 @@ function rowActions(ctx, row, env) {
|
|
|
895
944
|
}
|
|
896
945
|
const target = redirectDeleteForPlaceholderSibling(ctx.getAst(), row.path);
|
|
897
946
|
ctx.apply(deleteAt(ctx.getAst(), target));
|
|
898
|
-
}, "delete"));
|
|
947
|
+
}, "delete", "delete this condition"));
|
|
899
948
|
return acts;
|
|
900
949
|
}
|
|
901
950
|
function renderLeaf(ctx, row, env) {
|
|
@@ -923,8 +972,8 @@ function renderContainer(ctx, row, env) {
|
|
|
923
972
|
head.append(el("span", "exed-grouplabel", [label]));
|
|
924
973
|
}
|
|
925
974
|
const headActs = el("div", "exed-rowacts");
|
|
926
|
-
headActs.append(notToggle(ctx, row.path));
|
|
927
|
-
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"));
|
|
928
977
|
head.append(headActs);
|
|
929
978
|
box.append(head);
|
|
930
979
|
const body = el("div", "exed-groupbody");
|
|
@@ -1077,6 +1126,7 @@ function mountExpressionEditor(host, opts) {
|
|
|
1077
1126
|
},
|
|
1078
1127
|
...opts.requireNonEmpty ? { requireNonEmpty: true } : {},
|
|
1079
1128
|
...opts.valueEnumValues ? { valueEnumValues: opts.valueEnumValues } : {},
|
|
1129
|
+
...opts.propertyActions ? { propertyActions: opts.propertyActions } : {},
|
|
1080
1130
|
...opts.pickNode ? { pickNode: opts.pickNode } : {},
|
|
1081
1131
|
...opts.nodeLabel ? { nodeLabel: opts.nodeLabel } : {}
|
|
1082
1132
|
};
|
|
@@ -1113,7 +1163,7 @@ function mountExpressionEditor(host, opts) {
|
|
|
1113
1163
|
function rawArea() {
|
|
1114
1164
|
const ta = el("textarea", "exed-raw");
|
|
1115
1165
|
ta.value = src;
|
|
1116
|
-
ta.placeholder = "@gold > 0 and @met_anna";
|
|
1166
|
+
ta.placeholder = opts.rawPlaceholder ?? "@gold > 0 and @met_anna";
|
|
1117
1167
|
ta.rows = 2;
|
|
1118
1168
|
ta.addEventListener("input", () => {
|
|
1119
1169
|
src = ta.value;
|