@wildwinter/expr-editor 0.2.0 → 0.4.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
@@ -39,6 +39,17 @@ Notable options beyond the basics:
39
39
  host can suppress its own validation display mid-edit.
40
40
  - `messages: false` — hide the editor's internal validation list when the host
41
41
  renders its own.
42
+ - `popoverContainer` — where the popover micro-editors mount (default
43
+ `document.body`). Pass a container inside a focus-trapping dialog (Radix,
44
+ etc.) so opening a pill's popover counts as "inside" that layer and does not
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.
42
53
  - `setText(on)` — the host's raw-text toggle (`</>`); unparseable input falls
43
54
  back to raw text automatically.
44
55
 
package/dist/index.cjs CHANGED
@@ -312,7 +312,8 @@ function button(cls, label, onClick, title) {
312
312
  b.addEventListener("click", onClick);
313
313
  return b;
314
314
  }
315
- function openPopover(anchor, render, onClose) {
315
+ function openPopover(anchor, render, opts = {}) {
316
+ const container = opts.container ?? document.body;
316
317
  const pop = el("div", "exed-pop");
317
318
  let closed = false;
318
319
  const close = () => {
@@ -321,7 +322,7 @@ function openPopover(anchor, render, onClose) {
321
322
  document.removeEventListener("pointerdown", onDown, true);
322
323
  document.removeEventListener("keydown", onKey, true);
323
324
  pop.remove();
324
- onClose?.();
325
+ opts.onClose?.();
325
326
  };
326
327
  const onDown = (e) => {
327
328
  const t = e.target;
@@ -334,15 +335,21 @@ function openPopover(anchor, render, onClose) {
334
335
  }
335
336
  };
336
337
  pop.append(render(close));
337
- document.body.append(pop);
338
+ container.append(pop);
338
339
  const a = anchor.getBoundingClientRect();
339
340
  const ph = pop.getBoundingClientRect();
340
341
  let top = a.bottom + 4;
341
342
  if (top + ph.height > window.innerHeight - 8 && a.top - ph.height - 4 > 8) top = a.top - ph.height - 4;
342
343
  let left = a.left;
343
344
  if (left + ph.width > window.innerWidth - 8) left = Math.max(8, window.innerWidth - 8 - ph.width);
344
- pop.style.top = `${Math.round(top + window.scrollY)}px`;
345
- pop.style.left = `${Math.round(left + window.scrollX)}px`;
345
+ if (container === document.body) {
346
+ pop.style.top = `${Math.round(top + window.scrollY)}px`;
347
+ pop.style.left = `${Math.round(left + window.scrollX)}px`;
348
+ } else {
349
+ const cr = container.getBoundingClientRect();
350
+ pop.style.top = `${Math.round(top - cr.top + container.scrollTop)}px`;
351
+ pop.style.left = `${Math.round(left - cr.left + container.scrollLeft)}px`;
352
+ }
346
353
  setTimeout(() => {
347
354
  document.addEventListener("pointerdown", onDown, true);
348
355
  document.addEventListener("keydown", onKey, true);
@@ -483,11 +490,14 @@ function numberEditor(ctx, path, value, anchor) {
483
490
  function stringEditor(ctx, path, node, anchor) {
484
491
  const peer = findEnumPeer(ctx.getAst(), path);
485
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";
486
496
  ctx.openPopover(anchor, (close) => {
487
- if (enumEntry?.enumValues?.length) {
497
+ if (enumValues?.length) {
488
498
  const wrap = el("div", "exed-menu");
489
- wrap.append(el("div", "exed-menu-head", [`${displayName(enumEntry, ctx.defaultScope)} value`]));
490
- for (const v of enumEntry.enumValues) {
499
+ wrap.append(el("div", "exed-menu-head", [enumHeadLabel]));
500
+ for (const v of enumValues) {
491
501
  wrap.append(button(`exed-opt${v === node.value ? " sel" : ""}`, v, () => {
492
502
  replace(ctx, path, strLit(v));
493
503
  close();
@@ -559,6 +569,7 @@ function flagEditor(ctx, path, node, anchor) {
559
569
  return wrap;
560
570
  });
561
571
  }
572
+ var canDelete = (ctx, path) => !(ctx.requireNonEmpty && path.length === 0);
562
573
  function callEditor(ctx, path, node, anchor) {
563
574
  ctx.openPopover(anchor, (close) => {
564
575
  const wrap = el("div", "exed-menu");
@@ -570,10 +581,12 @@ function callEditor(ctx, path, node, anchor) {
570
581
  close();
571
582
  }));
572
583
  }
573
- wrap.append(button("exed-opt danger", "Delete", () => {
574
- ctx.apply(deleteAt(ctx.getAst(), path));
575
- close();
576
- }));
584
+ if (canDelete(ctx, path)) {
585
+ wrap.append(button("exed-opt danger", "Delete", () => {
586
+ ctx.apply(deleteAt(ctx.getAst(), path));
587
+ close();
588
+ }));
589
+ }
577
590
  return wrap;
578
591
  });
579
592
  }
@@ -584,10 +597,10 @@ function variableEditor(ctx, path, node, anchor) {
584
597
  replace(ctx, path, scopedVar(entry.scope, entry.name));
585
598
  close();
586
599
  },
587
- footer: button("exed-opt danger", "Delete", () => {
600
+ ...canDelete(ctx, path) ? { footer: button("exed-opt danger", "Delete", () => {
588
601
  ctx.apply(deleteAt(ctx.getAst(), path));
589
602
  close();
590
- })
603
+ }) } : {}
591
604
  }));
592
605
  }
593
606
  function propertyPicker(ctx, opts) {
@@ -1039,9 +1052,12 @@ function mountExpressionEditor(host, opts) {
1039
1052
  apply: (next) => emit(toSrc(next)),
1040
1053
  openPopover: (anchor, r) => {
1041
1054
  closePopover();
1042
- const pop = openPopover(anchor, r, () => {
1043
- if (activePopover === pop) activePopover = null;
1044
- setEditing(false);
1055
+ const pop = openPopover(anchor, r, {
1056
+ container: opts.popoverContainer,
1057
+ onClose: () => {
1058
+ if (activePopover === pop) activePopover = null;
1059
+ setEditing(false);
1060
+ }
1045
1061
  });
1046
1062
  activePopover = pop;
1047
1063
  setEditing(true);
@@ -1049,6 +1065,8 @@ function mountExpressionEditor(host, opts) {
1049
1065
  requestFocus: (p) => {
1050
1066
  pendingFocus = p;
1051
1067
  },
1068
+ ...opts.requireNonEmpty ? { requireNonEmpty: true } : {},
1069
+ ...opts.valueEnumValues ? { valueEnumValues: opts.valueEnumValues } : {},
1052
1070
  ...opts.pickNode ? { pickNode: opts.pickNode } : {},
1053
1071
  ...opts.nodeLabel ? { nodeLabel: opts.nodeLabel } : {}
1054
1072
  };
@@ -1060,7 +1078,7 @@ function mountExpressionEditor(host, opts) {
1060
1078
  if (raw || v.unparseable) {
1061
1079
  host.append(rawArea());
1062
1080
  } else if (!src.trim() || !v.ast) {
1063
- host.append(emptyState());
1081
+ host.append(opts.valueField ? valueEmptyState() : emptyState());
1064
1082
  } else {
1065
1083
  const ctx = buildCtx(v.ast);
1066
1084
  const body = el("div", "exed-body");
@@ -1104,6 +1122,10 @@ function mountExpressionEditor(host, opts) {
1104
1122
  }));
1105
1123
  return wrap;
1106
1124
  }
1125
+ function valueEmptyState() {
1126
+ const ctx = buildCtx(strLit(""));
1127
+ return el("div", "exed-flat", [renderNode(strLit(""), [], ctx)]);
1128
+ }
1107
1129
  function addTermControl(ctx) {
1108
1130
  const boolean = opts.addTerm === "boolean";
1109
1131
  const ops = boolean ? ["and", "or"] : ARITHMETIC_OPS;
@@ -1333,7 +1355,7 @@ function mountEffectsEditor(host, opts) {
1333
1355
  close();
1334
1356
  },
1335
1357
  onCancel: close
1336
- }));
1358
+ }), { container: opts.popoverContainer });
1337
1359
  };
1338
1360
  const targetType = (target) => opts.catalogue.find((e) => refOf(e, defaultScope) === target)?.type;
1339
1361
  const valueDisplay = (value, onChange, type) => {
@@ -1348,6 +1370,7 @@ function mountEffectsEditor(host, opts) {
1348
1370
  functions: opts.functions,
1349
1371
  mode: "flat",
1350
1372
  ...addTerm ? { addTerm } : {},
1373
+ ...opts.popoverContainer ? { popoverContainer: opts.popoverContainer } : {},
1351
1374
  text: textMode,
1352
1375
  onChange
1353
1376
  }));
@@ -1387,7 +1410,7 @@ function mountEffectsEditor(host, opts) {
1387
1410
  wrap.append(search, list);
1388
1411
  setTimeout(() => search.focus(), 0);
1389
1412
  return wrap;
1390
- });
1413
+ }, { container: opts.popoverContainer });
1391
1414
  };
1392
1415
  const iconBtn = (glyph, title, onClick, danger = false) => button(`exed-eff-icon${danger ? " danger" : ""}`, glyph, onClick, title);
1393
1416
  function setRow(eff, i) {
@@ -1460,7 +1483,7 @@ function mountEffectsEditor(host, opts) {
1460
1483
  wrap.append(button("exed-btn primary", "Apply", apply));
1461
1484
  setTimeout(() => input.focus(), 0);
1462
1485
  return wrap;
1463
- });
1486
+ }, { container: opts.popoverContainer });
1464
1487
  };
1465
1488
  const rowActions2 = (i) => {
1466
1489
  const acts = el("div", "exed-effect-acts");