@wildwinter/expr-editor 0.3.0 → 0.5.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
@@ -43,6 +43,18 @@ Notable options beyond the basics:
43
43
  `document.body`). Pass a container inside a focus-trapping dialog (Radix,
44
44
  etc.) so opening a pill's popover counts as "inside" that layer and does not
45
45
  dismiss the dialog; the popover is then positioned relative to that container.
46
+ - `requireNonEmpty` — block a delete that would empty the whole expression
47
+ (single-value fields that must always hold a term). Conditions leave it off
48
+ (an empty condition is the valid "always" state).
49
+ - `valueEnumValues` + `valueField` — flat single-value editing for an
50
+ outcome-style value cell: `valueEnumValues` offers a target's enum values on
51
+ the root literal, and `valueField` makes an empty value render one editable
52
+ "set a value…" pill instead of the condition clause-menu empty state.
53
+ - `flagValue: { target }` — flat compact editor for a `set_flags(@target, …)`
54
+ value whose target is implied by context: renders only the flag-delta pills
55
+ plus an "+ flag" chip, hiding the function name and target arg, and seeds the
56
+ call on the first flag add. Pair with `requireNonEmpty` so the last flag
57
+ can't be removed.
46
58
  - `setText(on)` — the host's raw-text toggle (`</>`); unparseable input falls
47
59
  back to raw text automatically.
48
60
 
package/dist/index.cjs CHANGED
@@ -490,11 +490,14 @@ function numberEditor(ctx, path, value, anchor) {
490
490
  function stringEditor(ctx, path, node, anchor) {
491
491
  const peer = findEnumPeer(ctx.getAst(), path);
492
492
  const enumEntry = peer ? lookup(ctx.catalogue, peer.scope, peer.name) : null;
493
+ const rootValueEnums = path.length === 0 && ctx.valueEnumValues?.length ? ctx.valueEnumValues : null;
494
+ const enumValues = enumEntry?.enumValues?.length ? enumEntry.enumValues : rootValueEnums;
495
+ const enumHeadLabel = enumEntry ? `${displayName(enumEntry, ctx.defaultScope)} value` : "Value";
493
496
  ctx.openPopover(anchor, (close) => {
494
- if (enumEntry?.enumValues?.length) {
497
+ if (enumValues?.length) {
495
498
  const wrap = el("div", "exed-menu");
496
- wrap.append(el("div", "exed-menu-head", [`${displayName(enumEntry, ctx.defaultScope)} value`]));
497
- for (const v of enumEntry.enumValues) {
499
+ wrap.append(el("div", "exed-menu-head", [enumHeadLabel]));
500
+ for (const v of enumValues) {
498
501
  wrap.append(button(`exed-opt${v === node.value ? " sel" : ""}`, v, () => {
499
502
  replace(ctx, path, strLit(v));
500
503
  close();
@@ -538,7 +541,9 @@ function flagEditor(ctx, path, node, anchor) {
538
541
  const call = getNodeAt(ctx.getAst(), callPath);
539
542
  const flagsVar = call?.kind === "call" ? call.args[0] : void 0;
540
543
  const entry = flagsVar?.kind === "scopedvar" ? lookup(ctx.catalogue, flagsVar.scope, flagsVar.name) : null;
544
+ const flagCount = call?.kind === "call" ? call.args.filter((a) => a.kind === "flagdelta").length : 0;
541
545
  const used = call?.kind === "call" ? call.args.filter((a, i) => a.kind === "flagdelta" && i !== path[path.length - 1]).map((a) => a.name) : [];
546
+ const canRemove = !(ctx.requireNonEmpty && flagCount <= 1);
542
547
  ctx.openPopover(anchor, (close) => {
543
548
  const wrap = el("div", "exed-menu");
544
549
  wrap.append(el("div", "exed-menu-head", ["Flag set?"]));
@@ -563,9 +568,16 @@ function flagEditor(ctx, path, node, anchor) {
563
568
  close();
564
569
  } }));
565
570
  }
571
+ if (canRemove) {
572
+ wrap.append(button("exed-opt danger", "Remove flag", () => {
573
+ ctx.apply(deleteAt(ctx.getAst(), path));
574
+ close();
575
+ }));
576
+ }
566
577
  return wrap;
567
578
  });
568
579
  }
580
+ var canDelete = (ctx, path) => !(ctx.requireNonEmpty && path.length === 0);
569
581
  function callEditor(ctx, path, node, anchor) {
570
582
  ctx.openPopover(anchor, (close) => {
571
583
  const wrap = el("div", "exed-menu");
@@ -577,10 +589,12 @@ function callEditor(ctx, path, node, anchor) {
577
589
  close();
578
590
  }));
579
591
  }
580
- wrap.append(button("exed-opt danger", "Delete", () => {
581
- ctx.apply(deleteAt(ctx.getAst(), path));
582
- close();
583
- }));
592
+ if (canDelete(ctx, path)) {
593
+ wrap.append(button("exed-opt danger", "Delete", () => {
594
+ ctx.apply(deleteAt(ctx.getAst(), path));
595
+ close();
596
+ }));
597
+ }
584
598
  return wrap;
585
599
  });
586
600
  }
@@ -591,10 +605,10 @@ function variableEditor(ctx, path, node, anchor) {
591
605
  replace(ctx, path, scopedVar(entry.scope, entry.name));
592
606
  close();
593
607
  },
594
- footer: button("exed-opt danger", "Delete", () => {
608
+ ...canDelete(ctx, path) ? { footer: button("exed-opt danger", "Delete", () => {
595
609
  ctx.apply(deleteAt(ctx.getAst(), path));
596
610
  close();
597
- })
611
+ }) } : {}
598
612
  }));
599
613
  }
600
614
  function propertyPicker(ctx, opts) {
@@ -1059,6 +1073,8 @@ function mountExpressionEditor(host, opts) {
1059
1073
  requestFocus: (p) => {
1060
1074
  pendingFocus = p;
1061
1075
  },
1076
+ ...opts.requireNonEmpty ? { requireNonEmpty: true } : {},
1077
+ ...opts.valueEnumValues ? { valueEnumValues: opts.valueEnumValues } : {},
1062
1078
  ...opts.pickNode ? { pickNode: opts.pickNode } : {},
1063
1079
  ...opts.nodeLabel ? { nodeLabel: opts.nodeLabel } : {}
1064
1080
  };
@@ -1067,10 +1083,14 @@ function mountExpressionEditor(host, opts) {
1067
1083
  closePopover();
1068
1084
  host.replaceChildren();
1069
1085
  const v = validateSource(src, opts.schema, opts.dialect);
1070
- if (raw || v.unparseable) {
1086
+ const flagCompact = !!opts.flagValue && (opts.mode ?? "tree") === "flat";
1087
+ const astIsFlagCall = v.ast?.kind === "call" && (v.ast.name === "set_flags" || v.ast.name === "check_flags");
1088
+ if (raw || v.unparseable || flagCompact && !!v.ast && !astIsFlagCall) {
1071
1089
  host.append(rawArea());
1090
+ } else if (flagCompact) {
1091
+ host.append(flagValueBody(astIsFlagCall ? v.ast : null));
1072
1092
  } else if (!src.trim() || !v.ast) {
1073
- host.append(emptyState());
1093
+ host.append(opts.valueField ? valueEmptyState() : emptyState());
1074
1094
  } else {
1075
1095
  const ctx = buildCtx(v.ast);
1076
1096
  const body = el("div", "exed-body");
@@ -1114,6 +1134,40 @@ function mountExpressionEditor(host, opts) {
1114
1134
  }));
1115
1135
  return wrap;
1116
1136
  }
1137
+ function valueEmptyState() {
1138
+ const ctx = buildCtx(strLit(""));
1139
+ return el("div", "exed-flat", [renderNode(strLit(""), [], ctx)]);
1140
+ }
1141
+ function parseRef(ref) {
1142
+ const bare = ref.replace(/^@/, "");
1143
+ const dot = bare.indexOf(".");
1144
+ return dot >= 0 ? scopedVar(bare.slice(0, dot), bare.slice(dot + 1)) : scopedVar(defaultScope, bare);
1145
+ }
1146
+ function flagValueBody(call) {
1147
+ const wrap = el("div", "exed-flat exed-flagvalue");
1148
+ if (call) {
1149
+ const ctx = buildCtx(call);
1150
+ call.args.forEach((arg, i) => {
1151
+ if (arg.kind === "flagdelta") wrap.append(renderNode(arg, ["args", i], ctx));
1152
+ });
1153
+ wrap.append(addFlagChip(call));
1154
+ } else {
1155
+ wrap.append(addFlagChip(null));
1156
+ }
1157
+ return wrap;
1158
+ }
1159
+ function addFlagChip(call) {
1160
+ return button("exed-add exed-addflag", "+ flag", () => {
1161
+ if (call) {
1162
+ pendingFocus = ["args", call.args.length];
1163
+ emit(toSrc({ ...call, args: [...call.args, flagDelta("+", "")] }));
1164
+ } else {
1165
+ const seeded = callNode("set_flags", [parseRef(opts.flagValue.target), flagDelta("+", "")]);
1166
+ pendingFocus = ["args", 1];
1167
+ emit(toSrc(seeded));
1168
+ }
1169
+ }, "add a flag");
1170
+ }
1117
1171
  function addTermControl(ctx) {
1118
1172
  const boolean = opts.addTerm === "boolean";
1119
1173
  const ops = boolean ? ["and", "or"] : ARITHMETIC_OPS;