@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/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
- return pill("var", label, (b) => variableEditor(ctx, path, node, b), { issue: issue ?? (entry ? void 0 : `Unknown property ${label}`) });
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
- return button("exed-rowbtn", "NOT", () => ctx.apply(toggleContainerNot(ctx.getAst(), path)), "toggle NOT");
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;