@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 CHANGED
@@ -55,6 +55,14 @@ Notable options beyond the basics:
55
55
  the flag-delta pills plus an "+ flag" chip (which picks sign + name from
56
56
  `flags`), hiding the function name and target arg, and seeds the call on the
57
57
  first flag add. Pair with `requireNonEmpty` so the last flag can't be removed.
58
+ - `propertyActions(ref)` — host actions shown when a property pill is
59
+ right-clicked (e.g. "Go to definition"); the host resolves what each does.
60
+ - `rawPlaceholder` — placeholder for the raw-text textarea (value cells pass a
61
+ value-flavoured hint).
62
+
63
+ Pills carry `aria-haspopup`/labels and the tree controls carry aria-labels for
64
+ assistive tech. A comparison of a numeric dialect function against a number
65
+ (e.g. `turns_since_tag("x") > 3`) deletes as a unit.
58
66
  - `setText(on)` — the host's raw-text toggle (`</>`); unparseable input falls
59
67
  back to raw text automatically.
60
68
 
package/dist/index.cjs CHANGED
@@ -305,10 +305,11 @@ function el(tag, cls, children) {
305
305
  }
306
306
  return node;
307
307
  }
308
- function button(cls, label, onClick, title) {
308
+ function button(cls, label, onClick, title, aria) {
309
309
  const b = el("button", cls, [label]);
310
310
  b.type = "button";
311
311
  if (title) b.title = title;
312
+ if (aria) b.setAttribute("aria-label", aria);
312
313
  b.addEventListener("click", onClick);
313
314
  return b;
314
315
  }
@@ -389,10 +390,25 @@ function textField(opts) {
389
390
  var FLAG_FNS = /* @__PURE__ */ new Set(["check_flags", "set_flags"]);
390
391
  var TAG_ARG0_FNS = /* @__PURE__ */ new Set(["seen", "patter_seen", "visits", "patter_visits"]);
391
392
  function pill(kind, label, onClick, opts = {}) {
392
- const b = button(`exed-pill exed-pill-${kind}${opts.issue ? " exed-pill-err" : ""}`, label, () => onClick(b), opts.issue ?? opts.title);
393
+ const b = button(`exed-pill exed-pill-${kind}${opts.issue ? " exed-pill-err" : ""}`, label, () => onClick(b), opts.issue ?? opts.title, opts.aria);
394
+ b.setAttribute("aria-haspopup", "dialog");
393
395
  if (opts.path) b.dataset["exedPath"] = opts.path.join("/");
394
396
  return b;
395
397
  }
398
+ var OP_ARIA = {
399
+ "==": "equals",
400
+ "!=": "not equal to",
401
+ ">": "greater than",
402
+ ">=": "at least",
403
+ "<": "less than",
404
+ "<=": "at most",
405
+ "and": "and",
406
+ "or": "or",
407
+ "+": "plus",
408
+ "-": "minus",
409
+ "*": "times",
410
+ "/": "divided by"
411
+ };
396
412
  var issueText = (ctx, path) => {
397
413
  const list = issuesAt(ctx.byPath, path);
398
414
  return list.length ? list.map((i) => i.message).join("; ") : void 0;
@@ -420,7 +436,9 @@ function renderNode(node, path, ctx, parentOp, side) {
420
436
  case "scopedvar": {
421
437
  const entry = lookup(ctx.catalogue, node.scope, node.name);
422
438
  const label = displayName({ scope: node.scope, name: node.name }, ctx.defaultScope);
423
- return pill("var", label, (b) => variableEditor(ctx, path, node, b), { issue: issue ?? (entry ? void 0 : `Unknown property ${label}`) });
439
+ const varPill = pill("var", label, (b) => variableEditor(ctx, path, node, b), { issue: issue ?? (entry ? void 0 : `Unknown property ${label}`) });
440
+ attachPropertyMenu(ctx, node, varPill);
441
+ return varPill;
424
442
  }
425
443
  case "flagdelta":
426
444
  return pill(node.sign === "+" ? "flagpos" : "flagneg", `${node.sign}${node.name || "flag"}`, (b) => flagEditor(ctx, path, node, b), { issue, path });
@@ -437,7 +455,7 @@ function renderNode(node, path, ctx, parentOp, side) {
437
455
  }
438
456
  case "unary": {
439
457
  const row = el("span", "exed-unary");
440
- row.append(pill("op", UNARY_LABEL[node.op], () => ctx.apply(deleteOrUnwrapNot(ctx, path, node)), { title: "remove" }));
458
+ 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)` }));
441
459
  row.append(renderNode(node.operand, [...path, "operand"], ctx));
442
460
  return row;
443
461
  }
@@ -449,7 +467,7 @@ function renderNode(node, path, ctx, parentOp, side) {
449
467
  const swap = opSwapGroup(node.op);
450
468
  const opPill = pill("op", BINARY_LABEL[node.op], (b) => {
451
469
  if (swap) operatorEditor(ctx, path, node.op, swap, b);
452
- }, { title: swap ? "swap operator" : void 0 });
470
+ }, { title: swap ? "swap operator" : void 0, aria: OP_ARIA[node.op] ?? node.op });
453
471
  if (!swap) opPill.classList.add("exed-op-structural");
454
472
  row.append(opPill);
455
473
  row.append(renderNode(node.right, [...path, "right"], ctx, node.op, "right"));
@@ -458,10 +476,47 @@ function renderNode(node, path, ctx, parentOp, side) {
458
476
  }
459
477
  }
460
478
  }
479
+ function signToggle(current, onPick) {
480
+ const row = el("div", "exed-sign");
481
+ ["+", "-"].forEach((s) => {
482
+ const cls = `exed-signbtn${s === current ? s === "+" ? " sel-pos" : " sel-neg" : ""}`;
483
+ row.append(button(cls, s === "+" ? "+ set" : "\u2212 unset", () => onPick(s)));
484
+ });
485
+ return row;
486
+ }
461
487
  var deleteOrUnwrapNot = (ctx, path, node) => (
462
488
  // clicking the NOT pill strips it (keeps the operand)
463
489
  setNodeAt(ctx.getAst(), path, node.operand)
464
490
  );
491
+ function attachPropertyMenu(ctx, node, pillEl) {
492
+ if (!ctx.propertyActions) return;
493
+ const actions = ctx.propertyActions({ scope: node.scope, name: node.name });
494
+ if (!actions.length) return;
495
+ pillEl.addEventListener("contextmenu", (e) => {
496
+ e.preventDefault();
497
+ ctx.openPopover(pillEl, (close) => {
498
+ const wrap = el("div", "exed-menu");
499
+ for (const a of actions) wrap.append(button("exed-opt", a.label, () => {
500
+ a.run();
501
+ close();
502
+ }));
503
+ return wrap;
504
+ });
505
+ });
506
+ }
507
+ function isCompoundComparison(node, ctx) {
508
+ if (node.kind !== "binary" || !isComparisonOp(node.op)) return false;
509
+ const numCall = (n) => n.kind === "call" && ctx.dialect.functions[n.name]?.returnType === "number";
510
+ const isNum = (n) => n.kind === "number";
511
+ return numCall(node.left) && isNum(node.right) || numCall(node.right) && isNum(node.left);
512
+ }
513
+ function effectiveDeletePath(ctx, path) {
514
+ const last = path[path.length - 1];
515
+ if (last !== "left" && last !== "right") return path;
516
+ const parentPath = path.slice(0, -1);
517
+ const parent = getNodeAt(ctx.getAst(), parentPath);
518
+ return parent && isCompoundComparison(parent, ctx) ? parentPath : path;
519
+ }
465
520
  function boolEditor(ctx, path, value, anchor) {
466
521
  ctx.openPopover(anchor, (close) => {
467
522
  const wrap = el("div", "exed-menu");
@@ -547,14 +602,10 @@ function flagEditor(ctx, path, node, anchor) {
547
602
  ctx.openPopover(anchor, (close) => {
548
603
  const wrap = el("div", "exed-menu");
549
604
  wrap.append(el("div", "exed-menu-head", ["Flag set?"]));
550
- const signRow = el("div", "exed-field-row");
551
- for (const s of ["+", "-"]) {
552
- signRow.append(button(`exed-opt${s === node.sign ? " sel" : ""}`, s === "+" ? "+ set" : "\u2212 unset", () => {
553
- replace(ctx, path, flagDelta(s, node.name));
554
- close();
555
- }));
556
- }
557
- wrap.append(signRow);
605
+ wrap.append(signToggle(node.sign, (s) => {
606
+ replace(ctx, path, flagDelta(s, node.name));
607
+ close();
608
+ }));
558
609
  const names = (entry?.enumValues ?? []).filter((n) => !used.includes(n));
559
610
  if (names.length) {
560
611
  wrap.append(el("div", "exed-menu-head", ["Flag"]));
@@ -577,7 +628,7 @@ function flagEditor(ctx, path, node, anchor) {
577
628
  return wrap;
578
629
  });
579
630
  }
580
- var canDelete = (ctx, path) => !(ctx.requireNonEmpty && path.length === 0);
631
+ var canDelete = (ctx, path) => !(ctx.requireNonEmpty && effectiveDeletePath(ctx, path).length === 0);
581
632
  function callEditor(ctx, path, node, anchor) {
582
633
  ctx.openPopover(anchor, (close) => {
583
634
  const wrap = el("div", "exed-menu");
@@ -591,7 +642,7 @@ function callEditor(ctx, path, node, anchor) {
591
642
  }
592
643
  if (canDelete(ctx, path)) {
593
644
  wrap.append(button("exed-opt danger", "Delete", () => {
594
- ctx.apply(deleteAt(ctx.getAst(), path));
645
+ ctx.apply(deleteAt(ctx.getAst(), effectiveDeletePath(ctx, path)));
595
646
  close();
596
647
  }));
597
648
  }
@@ -606,7 +657,7 @@ function variableEditor(ctx, path, node, anchor) {
606
657
  close();
607
658
  },
608
659
  ...canDelete(ctx, path) ? { footer: button("exed-opt danger", "Delete", () => {
609
- ctx.apply(deleteAt(ctx.getAst(), path));
660
+ ctx.apply(deleteAt(ctx.getAst(), effectiveDeletePath(ctx, path)));
610
661
  close();
611
662
  }) } : {}
612
663
  }));
@@ -876,15 +927,17 @@ function renderRow(ctx, row, env) {
876
927
  if (row.kind === "container") return renderContainer(ctx, row, env);
877
928
  return renderLeaf(ctx, row, env);
878
929
  }
879
- function notToggle(ctx, path) {
880
- return button("exed-rowbtn", "NOT", () => ctx.apply(toggleContainerNot(ctx.getAst(), path)), "toggle NOT");
930
+ function notToggle(ctx, path, pressed) {
931
+ const b = button("exed-rowbtn", "NOT", () => ctx.apply(toggleContainerNot(ctx.getAst(), path)), "toggle NOT", pressed ? "remove NOT" : "apply NOT");
932
+ b.setAttribute("aria-pressed", String(pressed));
933
+ return b;
881
934
  }
882
935
  function rowActions(ctx, row, env) {
883
936
  const acts = el("div", "exed-rowacts");
884
- acts.append(notToggle(ctx, row.path));
937
+ acts.append(notToggle(ctx, row.path, row.negated));
885
938
  if (env.chainPath && env.count != null && env.index != null) {
886
- if (env.index > 0) acts.append(button("exed-rowbtn", "\u2191", () => ctx.apply(moveChildInContainer(ctx.getAst(), env.chainPath, env.index, env.index - 1)), "move up"));
887
- 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"));
939
+ 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"));
940
+ 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"));
888
941
  }
889
942
  acts.append(button("exed-rowbtn danger", "\u2715", () => {
890
943
  if (env.root) {
@@ -893,7 +946,7 @@ function rowActions(ctx, row, env) {
893
946
  }
894
947
  const target = redirectDeleteForPlaceholderSibling(ctx.getAst(), row.path);
895
948
  ctx.apply(deleteAt(ctx.getAst(), target));
896
- }, "delete"));
949
+ }, "delete", "delete this condition"));
897
950
  return acts;
898
951
  }
899
952
  function renderLeaf(ctx, row, env) {
@@ -921,8 +974,8 @@ function renderContainer(ctx, row, env) {
921
974
  head.append(el("span", "exed-grouplabel", [label]));
922
975
  }
923
976
  const headActs = el("div", "exed-rowacts");
924
- headActs.append(notToggle(ctx, row.path));
925
- if (!env.root) headActs.append(button("exed-rowbtn danger", "\u2715", () => ctx.apply(deleteAt(ctx.getAst(), row.path)), "delete group"));
977
+ headActs.append(notToggle(ctx, row.path, row.negated));
978
+ if (!env.root) headActs.append(button("exed-rowbtn danger", "\u2715", () => ctx.apply(deleteAt(ctx.getAst(), row.path)), "delete group", "delete this group"));
926
979
  head.append(headActs);
927
980
  box.append(head);
928
981
  const body = el("div", "exed-groupbody");
@@ -1075,6 +1128,7 @@ function mountExpressionEditor(host, opts) {
1075
1128
  },
1076
1129
  ...opts.requireNonEmpty ? { requireNonEmpty: true } : {},
1077
1130
  ...opts.valueEnumValues ? { valueEnumValues: opts.valueEnumValues } : {},
1131
+ ...opts.propertyActions ? { propertyActions: opts.propertyActions } : {},
1078
1132
  ...opts.pickNode ? { pickNode: opts.pickNode } : {},
1079
1133
  ...opts.nodeLabel ? { nodeLabel: opts.nodeLabel } : {}
1080
1134
  };
@@ -1111,7 +1165,7 @@ function mountExpressionEditor(host, opts) {
1111
1165
  function rawArea() {
1112
1166
  const ta = el("textarea", "exed-raw");
1113
1167
  ta.value = src;
1114
- ta.placeholder = "@gold > 0 and @met_anna";
1168
+ ta.placeholder = opts.rawPlaceholder ?? "@gold > 0 and @met_anna";
1115
1169
  ta.rows = 2;
1116
1170
  ta.addEventListener("input", () => {
1117
1171
  src = ta.value;
@@ -1164,21 +1218,15 @@ function mountExpressionEditor(host, opts) {
1164
1218
  let sign = "+";
1165
1219
  const wrap = el("div", "exed-menu");
1166
1220
  wrap.append(el("div", "exed-menu-head", ["Add flag"]));
1167
- const signRow = el("div", "exed-field-row");
1221
+ const signHost = el("div");
1168
1222
  const drawSign = () => {
1169
- signRow.replaceChildren(
1170
- button(`exed-opt${sign === "+" ? " sel" : ""}`, "+ set", () => {
1171
- sign = "+";
1172
- drawSign();
1173
- }),
1174
- button(`exed-opt${sign === "-" ? " sel" : ""}`, "\u2212 unset", () => {
1175
- sign = "-";
1176
- drawSign();
1177
- })
1178
- );
1223
+ signHost.replaceChildren(signToggle(sign, (s) => {
1224
+ sign = s;
1225
+ drawSign();
1226
+ }));
1179
1227
  };
1180
1228
  drawSign();
1181
- wrap.append(signRow);
1229
+ wrap.append(signHost);
1182
1230
  const commit = (name) => {
1183
1231
  const delta = flagDelta(sign, name);
1184
1232
  emit(toSrc(call ? { ...call, args: [...call.args, delta] } : callNode("set_flags", [parseRef(opts.flagValue.target), delta])));