@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/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"));
@@ -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
- const signRow = el("div", "exed-field-row");
549
- for (const s of ["+", "-"]) {
550
- signRow.append(button(`exed-opt${s === node.sign ? " sel" : ""}`, s === "+" ? "+ set" : "\u2212 unset", () => {
551
- replace(ctx, path, flagDelta(s, node.name));
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
- 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;
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 signRow = el("div", "exed-field-row");
1219
+ const signHost = el("div");
1166
1220
  const drawSign = () => {
1167
- signRow.replaceChildren(
1168
- button(`exed-opt${sign === "+" ? " sel" : ""}`, "+ set", () => {
1169
- sign = "+";
1170
- drawSign();
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(signRow);
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])));