@jsenv/navi 0.30.24 → 0.30.25

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.
@@ -28608,7 +28608,11 @@ ${[idKey, ...uniqueKeys].join(", ")}`,
28608
28608
  * (docs/resource.md).
28609
28609
  */
28610
28610
  const upsertAnswer = (...args) => upsertWith(args, true);
28611
- const drop = (...args) => {
28611
+ // Batched like upsertWith: a removal observer may drop rows of another
28612
+ // store (a `.one()` declared with `onDelete: "cascade"`), and an effect
28613
+ // reading both runs once, never between the child leaving and its parents.
28614
+ const drop = (...args) => batch(() => dropNow(...args));
28615
+ const dropNow = (...args) => {
28612
28616
  const removedItemArray = [];
28613
28617
  const triggerRemovedMutations = () => {
28614
28618
  if (removedItemArray.length === 0) {
@@ -28643,6 +28647,7 @@ ${[idKey, ...uniqueKeys].join(", ")}`,
28643
28647
  hasFound = true;
28644
28648
  idToRemoveSet.delete(existingItemId);
28645
28649
  idRemovedArray.push(existingItemId);
28650
+ removedItemArray.push(existingItem);
28646
28651
  } else {
28647
28652
  arrayWithoutDroppedItems.push(existingItem);
28648
28653
  }
@@ -30753,6 +30758,15 @@ const createResource = (
30753
30758
  * The property is reactive: updating the child item anywhere propagates immediately.
30754
30759
  * The child resource exists independently — it is not owned by, nor deleted with, the parent.
30755
30760
  *
30761
+ * The other way round is declared with `onDelete`. Left out, a parent outlives its
30762
+ * child: once the child leaves its store, the property reads an empty object (truthy,
30763
+ * `valueOf()` is `null`, no fields) until the parent is read again — a game whose
30764
+ * organizer is deleted is still a game. `onDelete: "cascade"` is for a parent that is
30765
+ * nothing without its child (an address-book row, the person in it): when the child
30766
+ * leaves its store, the parents pointing at it leave theirs in the same write, and
30767
+ * with them every list holding them. Which one a relation needs is decided in
30768
+ * docs/resource.md, "When the child is deleted".
30769
+ *
30756
30770
  * Use it when the child is a first-class entity with its own store, shared across
30757
30771
  * parents (a user referenced by many games). When the child only exists inside its
30758
30772
  * owner, use `.scopedOne()` instead.
@@ -30772,7 +30786,12 @@ const createResource = (
30772
30786
  *
30773
30787
  * @param {string} propertyName - property holding the child on each parent item
30774
30788
  * @param {Object} childResource - the independent resource created by `resource()`
30775
- * @param {Object} [restCallbacks] - `{ rerunOn, dependencies, GET, PUT, DELETE }`
30789
+ * @param {Object} [restCallbacks] - `{ rerunOn, dependencies, onDelete, GET, PUT, DELETE }`
30790
+ * @param {"cascade"} [restCallbacks.onDelete] - `"cascade"`: a parent leaves its store
30791
+ * when its child leaves the child's (the child's `DELETE`, `DELETE_MANY`, a write
30792
+ * named with verb `DELETE`, a cascade of its own). Not the relation's own `DELETE`,
30793
+ * which unlinks: the property becomes `null` and the parent stays. Throws on a
30794
+ * relation declared on a scoped child.
30776
30795
  * @see docs/resource.md
30777
30796
  */
30778
30797
  stateFacade.one = (
@@ -30781,6 +30800,7 @@ const createResource = (
30781
30800
  {
30782
30801
  rerunOn: oneRerunOn,
30783
30802
  dependencies: oneDependencies,
30803
+ onDelete,
30784
30804
 
30785
30805
  GET,
30786
30806
  PUT,
@@ -30788,16 +30808,54 @@ const createResource = (
30788
30808
  } = {},
30789
30809
  ) => {
30790
30810
  const declarationSite = getDeclarationSite();
30811
+ const relationLabel = `${name}.one("${propertyName}")`;
30791
30812
  if (ownerName) {
30792
30813
  refuseCallbacksNamingScopedItem(
30793
- `${name}.one("${propertyName}")`,
30814
+ relationLabel,
30794
30815
  { GET, PUT, DELETE },
30795
30816
  { name, ownerName, propertyName, declarationSite },
30796
30817
  );
30797
30818
  }
30819
+ if (onDelete !== undefined && onDelete !== "cascade") {
30820
+ throw new TypeError(
30821
+ `${relationLabel}: onDelete must be "cascade" or left out, got ${onDelete}.
30822
+ ${relationLabel} source location: ${declarationSite}`,
30823
+ );
30824
+ }
30825
+ const cascade = onDelete === "cascade";
30826
+ if (cascade && ownerName) {
30827
+ // A scoped item lives in its owner's collection, not in a store this
30828
+ // relation could drop it from.
30829
+ throw new Error(
30830
+ `${relationLabel} cannot declare onDelete: "cascade": a "${name}" item exists only inside its "${ownerName}", and leaves through the "${ownerName}" callbacks.
30831
+ Read the "${ownerName}" again after the "${childResource.name}" is deleted (dependencies).
30832
+ ${relationLabel} source location: ${declarationSite}`,
30833
+ );
30834
+ }
30798
30835
  const childName = `${name}.${propertyName}`;
30799
30836
  const childIdKey = childResource.idKey;
30800
30837
  const childStore = childResource.store;
30838
+ // The id each parent points at, carried by the parent row (copies of a row
30839
+ // keep it, a spread does not): a removal is matched against the parents
30840
+ // the store holds at that moment, under the ids they have then.
30841
+ const childIdSymbol = cascade ? Symbol(`${childName} id`) : undefined;
30842
+ if (cascade) {
30843
+ childStore.observeRemovals((removedChildArray) => {
30844
+ const removedChildIdSet = new Set();
30845
+ for (const removedChild of removedChildArray) {
30846
+ removedChildIdSet.add(removedChild[childIdKey]);
30847
+ }
30848
+ const orphanIdArray = [];
30849
+ for (const parentItem of store.arraySignal.peek()) {
30850
+ if (removedChildIdSet.has(parentItem[childIdSymbol])) {
30851
+ orphanIdArray.push(parentItem[idKey]);
30852
+ }
30853
+ }
30854
+ if (orphanIdArray.length > 0) {
30855
+ store.drop(orphanIdArray);
30856
+ }
30857
+ });
30858
+ }
30801
30859
  addItemSetup((item, answer) => {
30802
30860
  const childItemIdSignal = signal();
30803
30861
  const updateChildItemId = (value, answer) => {
@@ -30852,6 +30910,11 @@ const createResource = (
30852
30910
  get: () => childItemFacadeSignal.value,
30853
30911
  set: updateChildItemId,
30854
30912
  });
30913
+ if (cascade) {
30914
+ Object.defineProperty(item, childIdSymbol, {
30915
+ get: () => childItemIdSignal.peek(),
30916
+ });
30917
+ }
30855
30918
  debug$2(
30856
30919
  `setup ${item}.${propertyName} is one "${childResource.name}" (current value: ${childItemSignal.peek()})`,
30857
30920
  );
@@ -39453,10 +39516,12 @@ const barsOutside = (areaElement) => {
39453
39516
  };
39454
39517
 
39455
39518
  // Room around what a bar's picture holds for what paints past every box — a
39456
- // shadow, the blur of a wall — which no API measures: a filter cuts whatever
39519
+ // shadow, the bar's own filter — which no API measures: a filter cuts whatever
39457
39520
  // falls outside its region.
39458
39521
  const WALL_FILTER_INK_ROOM = 100;
39459
39522
  const SVG_NS = "http://www.w3.org/2000/svg";
39523
+ // A blur() in a computed filter, its radius in px.
39524
+ const BLUR_FUNCTION_REGEXP = /blur\(([^)]*)\)/g;
39460
39525
 
39461
39526
  // The bar's picture dimmed, not a rectangle over it: each wall's colour is
39462
39527
  // flooded atop the bar's own pixels (Porter-Duff source-atop, which no blend
@@ -39484,11 +39549,17 @@ const dimBarPicture = (bar, sources) => {
39484
39549
  const ownFilter = getComputedStyle(bar).filter;
39485
39550
  const chain = ownFilter === "none" ? [] : [ownFilter];
39486
39551
  for (const source of sources) {
39487
- const backdropFilter = getComputedStyle(source)
39488
- .getPropertyValue("--backdrop-filter")
39489
- .trim();
39490
- if (backdropFilter && backdropFilter !== "none") {
39491
- chain.push(backdropFilter);
39552
+ const backdropFilter = readBackdropFilter(source);
39553
+ if (backdropFilter !== "none") {
39554
+ // Blurred alone, the bar would fade to transparent over its edges and
39555
+ // show the live document through them: its blur stays inside its shape.
39556
+ chain.push(
39557
+ backdropFilter.replace(
39558
+ BLUR_FUNCTION_REGEXP,
39559
+ (_, radius) =>
39560
+ `url(#${createShapeBlurFilter(parseFloat(radius), region).id})`,
39561
+ ),
39562
+ );
39492
39563
  }
39493
39564
  chain.push(`url(#${createWallFilter(source, region).id})`);
39494
39565
  }
@@ -39497,7 +39568,59 @@ const dimBarPicture = (bar, sources) => {
39497
39568
  dimmedBars.push(bar);
39498
39569
  };
39499
39570
 
39571
+ // The wall's backdrop-filter as computed, so a blur radius written in em or
39572
+ // rem reads in px, as the wall resolves it where it paints.
39573
+ const readBackdropFilter = (source) => {
39574
+ const sourceStyle = getComputedStyle(source);
39575
+ const backdropFilter = sourceStyle
39576
+ .getPropertyValue("--backdrop-filter")
39577
+ .trim();
39578
+ if (!backdropFilter || backdropFilter === "none") {
39579
+ return "none";
39580
+ }
39581
+ const probe = document.createElement("div");
39582
+ probe.style.fontSize = sourceStyle.fontSize;
39583
+ probe.style.filter = backdropFilter;
39584
+ document.body.append(probe);
39585
+ const filter = getComputedStyle(probe).filter;
39586
+ probe.remove();
39587
+ return filter;
39588
+ };
39589
+
39500
39590
  const createWallFilter = (source, region) => {
39591
+ const filter = createFilter(region);
39592
+ const flood = document.createElementNS(SVG_NS, "feFlood");
39593
+ flood.setAttribute("flood-color", readBackdropColor(source));
39594
+ const composite = document.createElementNS(SVG_NS, "feComposite");
39595
+ composite.setAttribute("in2", "SourceGraphic");
39596
+ composite.setAttribute("operator", "atop");
39597
+ filter.append(flood, composite);
39598
+ return filter;
39599
+ };
39600
+
39601
+ // Colour kept, alpha set to 1.
39602
+ const OPAQUE_MATRIX = "1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 0 1";
39603
+
39604
+ // The wall's blur kept inside the bar. At rest the window is blurred as one
39605
+ // image; a CSS blur() on the bar sees only the bar, so its edges go half
39606
+ // transparent and its colour spreads past its box. Made opaque, each pixel is
39607
+ // the mean of the bar's own pixels around it (feColorMatrix works on
39608
+ // unpremultiplied colours), then cut back to the bar's own alpha.
39609
+ const createShapeBlurFilter = (stdDeviation, region) => {
39610
+ const filter = createFilter(region);
39611
+ const blur = document.createElementNS(SVG_NS, "feGaussianBlur");
39612
+ blur.setAttribute("stdDeviation", stdDeviation);
39613
+ const opaque = document.createElementNS(SVG_NS, "feColorMatrix");
39614
+ opaque.setAttribute("type", "matrix");
39615
+ opaque.setAttribute("values", OPAQUE_MATRIX);
39616
+ const clip = document.createElementNS(SVG_NS, "feComposite");
39617
+ clip.setAttribute("in2", "SourceAlpha");
39618
+ clip.setAttribute("operator", "in");
39619
+ filter.append(blur, opaque, clip);
39620
+ return filter;
39621
+ };
39622
+
39623
+ const createFilter = (region) => {
39501
39624
  if (!wallFilterSvg) {
39502
39625
  wallFilterSvg = document.createElementNS(SVG_NS, "svg");
39503
39626
  wallFilterSvg.setAttribute(WALL_FILTERS_ATTRIBUTE, "");
@@ -39507,18 +39630,13 @@ const createWallFilter = (source, region) => {
39507
39630
  wallFilterCount++;
39508
39631
  const filter = document.createElementNS(SVG_NS, "filter");
39509
39632
  filter.id = `navi-transition-wall-${wallFilterCount}`;
39510
- // linearRGB, the default, floods another colour than the wall's.
39633
+ // linearRGB, the default, floods another colour than the wall's and blurs
39634
+ // in another space than its backdrop-filter.
39511
39635
  filter.setAttribute("color-interpolation-filters", "sRGB");
39512
39636
  filter.setAttribute("x", region.x);
39513
39637
  filter.setAttribute("y", region.y);
39514
39638
  filter.setAttribute("width", region.width);
39515
39639
  filter.setAttribute("height", region.height);
39516
- const flood = document.createElementNS(SVG_NS, "feFlood");
39517
- flood.setAttribute("flood-color", readBackdropColor(source));
39518
- const composite = document.createElementNS(SVG_NS, "feComposite");
39519
- composite.setAttribute("in2", "SourceGraphic");
39520
- composite.setAttribute("operator", "atop");
39521
- filter.append(flood, composite);
39522
39640
  wallFilterSvg.appendChild(filter);
39523
39641
  return filter;
39524
39642
  };