@mintjamsinc/ichigojs 0.1.82 → 0.1.84

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/ichigo.cjs CHANGED
@@ -10136,9 +10136,9 @@
10136
10136
  */
10137
10137
  #fragmentRange;
10138
10138
  /**
10139
- * Set by {@link destroy}. A destroyed clone stays in its parent's child
10140
- * list (v-if / v-for render new clones under the same parent), so walks
10141
- * over the children must skip it.
10139
+ * Set by {@link destroy}. A destroyed node also leaves its parent's child
10140
+ * list, but a walk that took its copy of the list before the node was
10141
+ * destroyed may still reach it, and must skip it.
10142
10142
  */
10143
10143
  #destroyed = false;
10144
10144
  /**
@@ -10308,7 +10308,8 @@
10308
10308
  // walk runs once the subtree is in the document, which the outlet
10309
10309
  // cannot know when it renders inside a detached v-if / v-for clone.
10310
10310
  this.#directiveManager?.directives?.forEach(d => d.expandComponents?.());
10311
- this.#childVNodes?.forEach(child => child.expandComponents());
10311
+ // A copy: expanding can render and destroy clones under this node.
10312
+ this.#childVNodes?.slice().forEach(child => child.expandComponents());
10312
10313
  }
10313
10314
  /**
10314
10315
  * The application instance associated with this virtual node.
@@ -10620,8 +10621,9 @@
10620
10621
  updater.applyToDOM();
10621
10622
  }
10622
10623
  }
10623
- // Recursively update child virtual nodes
10624
- this.#childVNodes?.forEach(childVNode => {
10624
+ // Recursively update child virtual nodes. A copy: an update can
10625
+ // destroy a v-if / v-for clone, which removes it from this list.
10626
+ this.#childVNodes?.slice().forEach(childVNode => {
10625
10627
  childVNode.forceUpdate();
10626
10628
  });
10627
10629
  // If no directive requires template preservation, call onUpdated for directives that do not templatize
@@ -10633,7 +10635,7 @@
10633
10635
  }
10634
10636
  else if (this.#nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
10635
10637
  // For document fragments, recursively force update child VNodes
10636
- this.#childVNodes?.forEach(childVNode => {
10638
+ this.#childVNodes?.slice().forEach(childVNode => {
10637
10639
  childVNode.forceUpdate();
10638
10640
  });
10639
10641
  }
@@ -10715,6 +10717,7 @@
10715
10717
  * 4. Unregister dependencies
10716
10718
  * 5. Clean up directive manager
10717
10719
  * 6. Call onUnmounted lifecycle hooks
10720
+ * 7. Leave the parent's child list
10718
10721
  */
10719
10722
  destroy() {
10720
10723
  this.#destroyed = true;
@@ -10741,9 +10744,9 @@
10741
10744
  }
10742
10745
  this.#userData.clear();
10743
10746
  }
10744
- // Recursively destroy child nodes
10747
+ // Recursively destroy child nodes (a copy: each one leaves this list)
10745
10748
  if (this.#childVNodes) {
10746
- for (const childVNode of this.#childVNodes) {
10749
+ for (const childVNode of this.#childVNodes.slice()) {
10747
10750
  try {
10748
10751
  childVNode.destroy();
10749
10752
  }
@@ -10771,6 +10774,15 @@
10771
10774
  d.onUnmounted?.();
10772
10775
  });
10773
10776
  }
10777
+ // Leave the parent's child list. v-if and v-for render every new clone
10778
+ // under the same parent VNode, so a destroyed clone left in the list
10779
+ // would keep its whole subtree — VNodes, DOM nodes, bindings — alive
10780
+ // for as long as the parent, growing with each toggle or row change.
10781
+ const siblings = this.#parentVNode ? this.#parentVNode.#childVNodes : undefined;
10782
+ const index = siblings ? siblings.indexOf(this) : -1;
10783
+ if (index !== -1) {
10784
+ siblings.splice(index, 1);
10785
+ }
10774
10786
  }
10775
10787
  }
10776
10788
 
@@ -11307,6 +11319,25 @@
11307
11319
  * Previous iterations to detect changes
11308
11320
  */
11309
11321
  #previousIterations = [];
11322
+ /**
11323
+ * The source array watched for writes made directly to it, and the function
11324
+ * that stops watching it.
11325
+ *
11326
+ * A write such as `items[1] = x` is reported as a change of "items[1]", which
11327
+ * does not match this directive's dependency on "items", and the row's loop
11328
+ * variable holds the value it was rendered with. The directive therefore
11329
+ * watches the array itself and refreshes the rows that were written.
11330
+ */
11331
+ #watchedSource;
11332
+ #unwatchSource;
11333
+ /**
11334
+ * Positions written since the last refresh, and whether a write (such as one
11335
+ * to `length`) needs the whole list rendered again.
11336
+ */
11337
+ #writtenIndexes = new Set();
11338
+ #writtenWhole = false;
11339
+ #refreshScheduled = false;
11340
+ #destroyed = false;
11310
11341
  /**
11311
11342
  * @param context The context for parsing the directive.
11312
11343
  */
@@ -11423,6 +11454,10 @@
11423
11454
  * @inheritdoc
11424
11455
  */
11425
11456
  destroy() {
11457
+ this.#destroyed = true;
11458
+ this.#unwatchSource?.();
11459
+ this.#unwatchSource = undefined;
11460
+ this.#watchedSource = undefined;
11426
11461
  // Clean up all rendered items
11427
11462
  // First destroy all VNodes (calls @unmount hooks), then remove from DOM
11428
11463
  for (const { vNode } of this.#renderedItems) {
@@ -11454,6 +11489,10 @@
11454
11489
  }
11455
11490
  // Evaluate the source expression to get the data
11456
11491
  const sourceData = this.#sourceEvaluator.evaluate();
11492
+ // Every row is rendered from the source below, including those written in place
11493
+ this.#watchSource(sourceData);
11494
+ this.#writtenIndexes.clear();
11495
+ this.#writtenWhole = false;
11457
11496
  // Get iterations from the source data
11458
11497
  let iterations = this.#getIterations(sourceData);
11459
11498
  // If we don't have a key evaluator yet, try to create it
@@ -11847,6 +11886,89 @@
11847
11886
  }
11848
11887
  vNode.update();
11849
11888
  }
11889
+ /**
11890
+ * Watches the source for writes made directly to its elements. Only a
11891
+ * reactive array can be written that way; anything else is not watched.
11892
+ */
11893
+ #watchSource(source) {
11894
+ if (source === this.#watchedSource) {
11895
+ return;
11896
+ }
11897
+ this.#unwatchSource?.();
11898
+ this.#unwatchSource = undefined;
11899
+ this.#watchedSource = source;
11900
+ const path = Array.isArray(source) ? ReactiveProxy.getPath(source) : undefined;
11901
+ if (!path) {
11902
+ return;
11903
+ }
11904
+ this.#unwatchSource = ReactiveProxy.subscribe(source, (changedPath) => {
11905
+ this.#onSourceWritten(path, changedPath);
11906
+ });
11907
+ }
11908
+ /**
11909
+ * Notes a write made directly to the source array. A change below an
11910
+ * element (e.g. "items[1].name") is followed by the row's own bindings, and
11911
+ * a change of the whole array (e.g. by splice) by the usual dependency on it.
11912
+ */
11913
+ #onSourceWritten(path, changedPath) {
11914
+ if (!changedPath || !changedPath.startsWith(path + '[') || !changedPath.endsWith(']')) {
11915
+ return;
11916
+ }
11917
+ const property = changedPath.substring(path.length + 1, changedPath.length - 1);
11918
+ if (property.includes(']')) {
11919
+ return;
11920
+ }
11921
+ if (/^\d+$/.test(property)) {
11922
+ this.#writtenIndexes.add(Number(property));
11923
+ }
11924
+ else {
11925
+ // e.g. `items.length = 0`
11926
+ this.#writtenWhole = true;
11927
+ }
11928
+ // Writes made together are refreshed together, after the application's update
11929
+ if (!this.#refreshScheduled) {
11930
+ this.#refreshScheduled = true;
11931
+ queueMicrotask(() => {
11932
+ this.#refreshScheduled = false;
11933
+ this.#refreshWritten();
11934
+ });
11935
+ }
11936
+ }
11937
+ /**
11938
+ * Shows the elements written since the last refresh. A row whose position or
11939
+ * key no longer holds is not patched; the whole list is rendered again.
11940
+ */
11941
+ #refreshWritten() {
11942
+ if (this.#destroyed || (!this.#writtenWhole && this.#writtenIndexes.size === 0)) {
11943
+ return;
11944
+ }
11945
+ const indexes = [...this.#writtenIndexes];
11946
+ const whole = this.#writtenWhole;
11947
+ this.#writtenIndexes.clear();
11948
+ this.#writtenWhole = false;
11949
+ const source = this.#sourceEvaluator?.evaluate();
11950
+ if (whole || source !== this.#watchedSource || !Array.isArray(source) || source.length !== this.#renderedItems.length) {
11951
+ this.#render();
11952
+ return;
11953
+ }
11954
+ for (const index of indexes) {
11955
+ const rendered = this.#renderedItems[index];
11956
+ let context = { key: index, item: source[index], index };
11957
+ if (this.#evaluateKey && (this.#itemName || this.#itemDestructure)) {
11958
+ const itemBindings = new VBindings({
11959
+ parent: this.#vNode.bindings
11960
+ });
11961
+ this.#setItemBindings(itemBindings, context);
11962
+ context = { ...context, key: this.#evaluateKey(itemBindings) };
11963
+ }
11964
+ if (!rendered || context.key !== rendered.key) {
11965
+ this.#render();
11966
+ return;
11967
+ }
11968
+ this.#updateItemBindings(rendered.vNode, context);
11969
+ this.#previousIterations[index] = context;
11970
+ }
11971
+ }
11850
11972
  /**
11851
11973
  * Get iterations from various data types
11852
11974
  * Supports: Arrays, Objects, Sets, Maps, Iterables, Numbers, Strings
@@ -11856,7 +11978,8 @@
11856
11978
  return [];
11857
11979
  // Array
11858
11980
  if (Array.isArray(data)) {
11859
- return data.map((item, index) => ({
11981
+ // Array.from rather than map: a hole (e.g. after `items[5] = x` past the end) is a row of undefined
11982
+ return Array.from(data, (item, index) => ({
11860
11983
  item,
11861
11984
  index,
11862
11985
  key: index