@mintjamsinc/ichigojs 0.1.58 → 0.1.59

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
@@ -8969,6 +8969,13 @@
8969
8969
  * VNode is destroyed.
8970
8970
  */
8971
8971
  #userData;
8972
+ /**
8973
+ * When this VNode wraps a <template>-derived DocumentFragment that has been
8974
+ * expanded into the DOM, this range tracks the start/end boundary of the
8975
+ * expanded content. Directives such as v-for and v-if use this to move or
8976
+ * remove the entire fragment atomically.
8977
+ */
8978
+ #fragmentRange;
8972
8979
  /**
8973
8980
  * Creates an instance of the virtual node.
8974
8981
  * @param args The initialization arguments for the virtual node.
@@ -9204,6 +9211,17 @@
9204
9211
  }
9205
9212
  return this.#userData;
9206
9213
  }
9214
+ /**
9215
+ * The DOM range that bounds this VNode's expanded fragment content, if any.
9216
+ * Set by directives that expand a <template> into a DocumentFragment
9217
+ * (currently v-for and v-if).
9218
+ */
9219
+ get fragmentRange() {
9220
+ return this.#fragmentRange;
9221
+ }
9222
+ set fragmentRange(range) {
9223
+ this.#fragmentRange = range;
9224
+ }
9207
9225
  /**
9208
9226
  * The DOM path of this virtual node.
9209
9227
  * This is a string representation of the path from the root to this node,
@@ -9549,6 +9567,102 @@
9549
9567
  }
9550
9568
  }
9551
9569
 
9570
+ // Copyright (c) 2025 MintJams Inc. Licensed under MIT License.
9571
+ /**
9572
+ * Represents a contiguous range of DOM nodes that conceptually belong to a single VNode
9573
+ * whose source is a <template> element (and therefore expands to multiple sibling nodes
9574
+ * when inserted into the DOM).
9575
+ *
9576
+ * The range is bounded by two comment markers (start and end). All nodes between the
9577
+ * markers (inclusive) form the range. Move and remove operations always act on the
9578
+ * entire range atomically via the DOM Range API, which prevents the marker pair from
9579
+ * becoming desynchronized from the content it bounds.
9580
+ *
9581
+ * This abstraction replaces ad-hoc per-marker bookkeeping previously stored in
9582
+ * {@link VNode.userData} by directives such as v-for and v-if.
9583
+ */
9584
+ class VFragmentRange {
9585
+ #start;
9586
+ #end;
9587
+ constructor(start, end) {
9588
+ this.#start = start;
9589
+ this.#end = end;
9590
+ }
9591
+ /**
9592
+ * Inserts content into a parent before the given reference sibling (or as last
9593
+ * child when {@code refSibling} is null), wrapping it with start/end comment
9594
+ * markers. Returns a {@link VFragmentRange} that tracks the inserted region.
9595
+ *
9596
+ * @param parent The parent node to insert into.
9597
+ * @param refSibling The sibling to insert before (null = append).
9598
+ * @param label A short label used for the marker comment text (helps debugging).
9599
+ * @param content The content to insert. Typically a DocumentFragment cloned
9600
+ * from a template's content, but any Node works.
9601
+ */
9602
+ static insert(parent, refSibling, label, content) {
9603
+ const start = document.createComment(`#${label}-start`);
9604
+ const end = document.createComment(`#${label}-end`);
9605
+ parent.insertBefore(start, refSibling);
9606
+ parent.insertBefore(end, refSibling);
9607
+ parent.insertBefore(content, end);
9608
+ return new VFragmentRange(start, end);
9609
+ }
9610
+ /**
9611
+ * The start marker (inclusive boundary).
9612
+ */
9613
+ get firstNode() {
9614
+ return this.#start;
9615
+ }
9616
+ /**
9617
+ * The end marker (inclusive boundary).
9618
+ */
9619
+ get lastNode() {
9620
+ return this.#end;
9621
+ }
9622
+ /**
9623
+ * Move the entire range (start marker through end marker, inclusive) to be
9624
+ * inserted before {@code refSibling} under {@code parent}. If {@code refSibling}
9625
+ * is null the range is appended.
9626
+ *
9627
+ * Implemented via the DOM Range API so that all nodes between the markers move
9628
+ * together as a single contiguous block, regardless of how many or which kinds
9629
+ * of nodes currently sit between them.
9630
+ */
9631
+ moveBefore(parent, refSibling) {
9632
+ // No-op if already in the desired position
9633
+ if (refSibling === this.#start) {
9634
+ return;
9635
+ }
9636
+ const range = this.#asDomRange();
9637
+ const fragment = range.extractContents();
9638
+ parent.insertBefore(fragment, refSibling);
9639
+ range.detach();
9640
+ }
9641
+ /**
9642
+ * Remove the entire range (start marker through end marker, inclusive) from its
9643
+ * current parent. Safe to call even if the markers are already detached.
9644
+ */
9645
+ remove() {
9646
+ if (!this.#start.parentNode) {
9647
+ return;
9648
+ }
9649
+ const range = this.#asDomRange();
9650
+ range.deleteContents();
9651
+ range.detach();
9652
+ }
9653
+ /**
9654
+ * Builds a DOM {@link Range} that spans from immediately before the start marker
9655
+ * to immediately after the end marker, covering both markers and everything
9656
+ * between them.
9657
+ */
9658
+ #asDomRange() {
9659
+ const range = document.createRange();
9660
+ range.setStartBefore(this.#start);
9661
+ range.setEndAfter(this.#end);
9662
+ return range;
9663
+ }
9664
+ }
9665
+
9552
9666
  // Copyright (c) 2025 MintJams Inc. Licensed under MIT License.
9553
9667
  /**
9554
9668
  * Base class for conditional directives such as v-if, v-else-if, and v-else.
@@ -9749,19 +9863,7 @@
9749
9863
  const anchorParent = this.#vNode.anchorNode?.parentNode;
9750
9864
  const nextSibling = this.#vNode.anchorNode?.nextSibling ?? null;
9751
9865
  if (clone.nodeType === Node.DOCUMENT_FRAGMENT_NODE && anchorParent) {
9752
- const startMarker = document.createComment('#vif-fragment-start');
9753
- const endMarker = document.createComment('#vif-fragment-end');
9754
- if (nextSibling) {
9755
- anchorParent.insertBefore(startMarker, nextSibling);
9756
- }
9757
- else {
9758
- anchorParent.appendChild(startMarker);
9759
- }
9760
- anchorParent.insertBefore(endMarker, startMarker.nextSibling);
9761
- anchorParent.insertBefore(clone, endMarker);
9762
- // Store markers for later removal
9763
- vNode.userData.set('vif_fragment_start', startMarker);
9764
- vNode.userData.set('vif_fragment_end', endMarker);
9866
+ vNode.fragmentRange = VFragmentRange.insert(anchorParent, nextSibling, 'vif-fragment', clone);
9765
9867
  this.#renderedVNode = vNode;
9766
9868
  this.#renderedVNode.forceUpdate();
9767
9869
  return;
@@ -9782,19 +9884,11 @@
9782
9884
  }
9783
9885
  // Destroy VNode first (calls @unmount hooks while DOM is still accessible)
9784
9886
  this.#renderedVNode.destroy();
9785
- // Then remove from DOM. Handle fragment markers if present
9786
- const startMarker = this.#renderedVNode.userData.get?.('vif_fragment_start');
9787
- const endMarker = this.#renderedVNode.userData.get?.('vif_fragment_end');
9788
- if (startMarker && endMarker && startMarker.parentNode === endMarker.parentNode && startMarker.parentNode) {
9789
- const parentNode = startMarker.parentNode;
9790
- let node = startMarker;
9791
- while (node) {
9792
- const next = node.nextSibling;
9793
- parentNode.removeChild(node);
9794
- if (node === endMarker)
9795
- break;
9796
- node = next;
9797
- }
9887
+ // Then remove from DOM. Handle fragment ranges if present.
9888
+ const range = this.#renderedVNode.fragmentRange;
9889
+ if (range) {
9890
+ range.remove();
9891
+ this.#renderedVNode.fragmentRange = undefined;
9798
9892
  this.#renderedVNode = undefined;
9799
9893
  return;
9800
9894
  }
@@ -10060,6 +10154,12 @@
10060
10154
  }
10061
10155
  // Then remove DOM nodes
10062
10156
  for (const vNode of this.#renderedItems.values()) {
10157
+ const range = vNode.fragmentRange;
10158
+ if (range) {
10159
+ range.remove();
10160
+ vNode.fragmentRange = undefined;
10161
+ continue;
10162
+ }
10063
10163
  if (vNode.node.parentNode) {
10064
10164
  vNode.node.parentNode.removeChild(vNode.node);
10065
10165
  }
@@ -10133,22 +10233,12 @@
10133
10233
  vNode.destroy();
10134
10234
  }
10135
10235
  }
10136
- // Then remove from DOM. Handle both Element nodes and fragment-marked ranges.
10236
+ // Then remove from DOM. Handle both Element nodes and fragment ranges.
10137
10237
  for (const vNode of nodesToRemove) {
10138
- // If this VNode stored fragment markers, remove the range between them
10139
- const startMarker = vNode.userData.get?.('vfor_fragment_start');
10140
- const endMarker = vNode.userData.get?.('vfor_fragment_end');
10141
- if (startMarker && endMarker && startMarker.parentNode === endMarker.parentNode && startMarker.parentNode) {
10142
- const parentNode = startMarker.parentNode;
10143
- let node = startMarker;
10144
- // Remove nodes from startMarker up to and including endMarker
10145
- while (node) {
10146
- const next = node.nextSibling;
10147
- parentNode.removeChild(node);
10148
- if (node === endMarker)
10149
- break;
10150
- node = next;
10151
- }
10238
+ const range = vNode.fragmentRange;
10239
+ if (range) {
10240
+ range.remove();
10241
+ vNode.fragmentRange = undefined;
10152
10242
  continue;
10153
10243
  }
10154
10244
  // Fallback: remove the node itself if it's attached
@@ -10188,26 +10278,14 @@
10188
10278
  bindings,
10189
10279
  dependentIdentifiers: depIds,
10190
10280
  });
10191
- // If clone is a DocumentFragment, insert it between start/end comment markers
10281
+ // If clone is a DocumentFragment, wrap it in a VFragmentRange so the
10282
+ // entire expanded content moves/removes as one atomic unit.
10192
10283
  if (clone.nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
10193
- const startMarker = document.createComment('#vfor-fragment-start');
10194
- const endMarker = document.createComment('#vfor-fragment-end');
10195
- // Insert start and end markers and the fragment's children between them
10196
- if (prevNode.nextSibling) {
10197
- parent.insertBefore(startMarker, prevNode.nextSibling);
10198
- }
10199
- else {
10200
- parent.appendChild(startMarker);
10201
- }
10202
- parent.insertBefore(endMarker, startMarker.nextSibling);
10203
- parent.insertBefore(clone, endMarker);
10204
- // Store markers on the VNode for later removal/movement
10205
- vNode.userData.set('vfor_fragment_start', startMarker);
10206
- vNode.userData.set('vfor_fragment_end', endMarker);
10284
+ const range = VFragmentRange.insert(parent, prevNode.nextSibling, 'vfor-fragment', clone);
10285
+ vNode.fragmentRange = range;
10207
10286
  newRenderedItems.set(key, vNode);
10208
10287
  vNode.forceUpdate();
10209
- // Use endMarker as prevNode for subsequent insertions
10210
- prevNode = endMarker;
10288
+ prevNode = range.lastNode;
10211
10289
  continue;
10212
10290
  }
10213
10291
  // Determine what to insert: anchor node (if exists) or the clone itself
@@ -10227,22 +10305,28 @@
10227
10305
  newRenderedItems.set(key, vNode);
10228
10306
  // Update bindings
10229
10307
  this.#updateItemBindings(vNode, context);
10230
- // Determine the actual node in DOM: prefer fragment end marker, then anchor node, then vNode.node
10231
- const fragmentEnd = vNode.userData.get?.('vfor_fragment_end');
10232
- const actualNode = fragmentEnd || vNode.anchorNode || vNode.node;
10233
- // Move to correct position if needed
10234
- if (prevNode.nextSibling !== actualNode) {
10235
- if (prevNode.nextSibling) {
10236
- parent.insertBefore(actualNode, prevNode.nextSibling);
10308
+ // For fragment-backed iterations, move the entire range atomically.
10309
+ // For single-node iterations, move just the node.
10310
+ const range = vNode.fragmentRange;
10311
+ if (range) {
10312
+ if (prevNode.nextSibling !== range.firstNode) {
10313
+ range.moveBefore(parent, prevNode.nextSibling);
10237
10314
  }
10238
- else {
10239
- parent.appendChild(actualNode);
10315
+ }
10316
+ else {
10317
+ const actualNode = vNode.anchorNode || vNode.node;
10318
+ if (prevNode.nextSibling !== actualNode) {
10319
+ if (prevNode.nextSibling) {
10320
+ parent.insertBefore(actualNode, prevNode.nextSibling);
10321
+ }
10322
+ else {
10323
+ parent.appendChild(actualNode);
10324
+ }
10240
10325
  }
10241
10326
  }
10242
10327
  }
10243
- // Use fragment end marker > anchor node > vNode.node as prevNode
10244
- const fragmentEndForPrev = vNode.userData.get?.('vfor_fragment_end');
10245
- prevNode = fragmentEndForPrev || vNode.anchorNode || vNode.node;
10328
+ // Advance prevNode to this iteration's last DOM node
10329
+ prevNode = vNode.fragmentRange?.lastNode ?? vNode.anchorNode ?? vNode.node;
10246
10330
  }
10247
10331
  // Update rendered items map
10248
10332
  this.#renderedItems = newRenderedItems;