@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.
@@ -8963,6 +8963,13 @@ class VNode {
8963
8963
  * VNode is destroyed.
8964
8964
  */
8965
8965
  #userData;
8966
+ /**
8967
+ * When this VNode wraps a <template>-derived DocumentFragment that has been
8968
+ * expanded into the DOM, this range tracks the start/end boundary of the
8969
+ * expanded content. Directives such as v-for and v-if use this to move or
8970
+ * remove the entire fragment atomically.
8971
+ */
8972
+ #fragmentRange;
8966
8973
  /**
8967
8974
  * Creates an instance of the virtual node.
8968
8975
  * @param args The initialization arguments for the virtual node.
@@ -9198,6 +9205,17 @@ class VNode {
9198
9205
  }
9199
9206
  return this.#userData;
9200
9207
  }
9208
+ /**
9209
+ * The DOM range that bounds this VNode's expanded fragment content, if any.
9210
+ * Set by directives that expand a <template> into a DocumentFragment
9211
+ * (currently v-for and v-if).
9212
+ */
9213
+ get fragmentRange() {
9214
+ return this.#fragmentRange;
9215
+ }
9216
+ set fragmentRange(range) {
9217
+ this.#fragmentRange = range;
9218
+ }
9201
9219
  /**
9202
9220
  * The DOM path of this virtual node.
9203
9221
  * This is a string representation of the path from the root to this node,
@@ -9543,6 +9561,102 @@ class VConditionalDirectiveContext {
9543
9561
  }
9544
9562
  }
9545
9563
 
9564
+ // Copyright (c) 2025 MintJams Inc. Licensed under MIT License.
9565
+ /**
9566
+ * Represents a contiguous range of DOM nodes that conceptually belong to a single VNode
9567
+ * whose source is a <template> element (and therefore expands to multiple sibling nodes
9568
+ * when inserted into the DOM).
9569
+ *
9570
+ * The range is bounded by two comment markers (start and end). All nodes between the
9571
+ * markers (inclusive) form the range. Move and remove operations always act on the
9572
+ * entire range atomically via the DOM Range API, which prevents the marker pair from
9573
+ * becoming desynchronized from the content it bounds.
9574
+ *
9575
+ * This abstraction replaces ad-hoc per-marker bookkeeping previously stored in
9576
+ * {@link VNode.userData} by directives such as v-for and v-if.
9577
+ */
9578
+ class VFragmentRange {
9579
+ #start;
9580
+ #end;
9581
+ constructor(start, end) {
9582
+ this.#start = start;
9583
+ this.#end = end;
9584
+ }
9585
+ /**
9586
+ * Inserts content into a parent before the given reference sibling (or as last
9587
+ * child when {@code refSibling} is null), wrapping it with start/end comment
9588
+ * markers. Returns a {@link VFragmentRange} that tracks the inserted region.
9589
+ *
9590
+ * @param parent The parent node to insert into.
9591
+ * @param refSibling The sibling to insert before (null = append).
9592
+ * @param label A short label used for the marker comment text (helps debugging).
9593
+ * @param content The content to insert. Typically a DocumentFragment cloned
9594
+ * from a template's content, but any Node works.
9595
+ */
9596
+ static insert(parent, refSibling, label, content) {
9597
+ const start = document.createComment(`#${label}-start`);
9598
+ const end = document.createComment(`#${label}-end`);
9599
+ parent.insertBefore(start, refSibling);
9600
+ parent.insertBefore(end, refSibling);
9601
+ parent.insertBefore(content, end);
9602
+ return new VFragmentRange(start, end);
9603
+ }
9604
+ /**
9605
+ * The start marker (inclusive boundary).
9606
+ */
9607
+ get firstNode() {
9608
+ return this.#start;
9609
+ }
9610
+ /**
9611
+ * The end marker (inclusive boundary).
9612
+ */
9613
+ get lastNode() {
9614
+ return this.#end;
9615
+ }
9616
+ /**
9617
+ * Move the entire range (start marker through end marker, inclusive) to be
9618
+ * inserted before {@code refSibling} under {@code parent}. If {@code refSibling}
9619
+ * is null the range is appended.
9620
+ *
9621
+ * Implemented via the DOM Range API so that all nodes between the markers move
9622
+ * together as a single contiguous block, regardless of how many or which kinds
9623
+ * of nodes currently sit between them.
9624
+ */
9625
+ moveBefore(parent, refSibling) {
9626
+ // No-op if already in the desired position
9627
+ if (refSibling === this.#start) {
9628
+ return;
9629
+ }
9630
+ const range = this.#asDomRange();
9631
+ const fragment = range.extractContents();
9632
+ parent.insertBefore(fragment, refSibling);
9633
+ range.detach();
9634
+ }
9635
+ /**
9636
+ * Remove the entire range (start marker through end marker, inclusive) from its
9637
+ * current parent. Safe to call even if the markers are already detached.
9638
+ */
9639
+ remove() {
9640
+ if (!this.#start.parentNode) {
9641
+ return;
9642
+ }
9643
+ const range = this.#asDomRange();
9644
+ range.deleteContents();
9645
+ range.detach();
9646
+ }
9647
+ /**
9648
+ * Builds a DOM {@link Range} that spans from immediately before the start marker
9649
+ * to immediately after the end marker, covering both markers and everything
9650
+ * between them.
9651
+ */
9652
+ #asDomRange() {
9653
+ const range = document.createRange();
9654
+ range.setStartBefore(this.#start);
9655
+ range.setEndAfter(this.#end);
9656
+ return range;
9657
+ }
9658
+ }
9659
+
9546
9660
  // Copyright (c) 2025 MintJams Inc. Licensed under MIT License.
9547
9661
  /**
9548
9662
  * Base class for conditional directives such as v-if, v-else-if, and v-else.
@@ -9743,19 +9857,7 @@ class VConditionalDirective {
9743
9857
  const anchorParent = this.#vNode.anchorNode?.parentNode;
9744
9858
  const nextSibling = this.#vNode.anchorNode?.nextSibling ?? null;
9745
9859
  if (clone.nodeType === Node.DOCUMENT_FRAGMENT_NODE && anchorParent) {
9746
- const startMarker = document.createComment('#vif-fragment-start');
9747
- const endMarker = document.createComment('#vif-fragment-end');
9748
- if (nextSibling) {
9749
- anchorParent.insertBefore(startMarker, nextSibling);
9750
- }
9751
- else {
9752
- anchorParent.appendChild(startMarker);
9753
- }
9754
- anchorParent.insertBefore(endMarker, startMarker.nextSibling);
9755
- anchorParent.insertBefore(clone, endMarker);
9756
- // Store markers for later removal
9757
- vNode.userData.set('vif_fragment_start', startMarker);
9758
- vNode.userData.set('vif_fragment_end', endMarker);
9860
+ vNode.fragmentRange = VFragmentRange.insert(anchorParent, nextSibling, 'vif-fragment', clone);
9759
9861
  this.#renderedVNode = vNode;
9760
9862
  this.#renderedVNode.forceUpdate();
9761
9863
  return;
@@ -9776,19 +9878,11 @@ class VConditionalDirective {
9776
9878
  }
9777
9879
  // Destroy VNode first (calls @unmount hooks while DOM is still accessible)
9778
9880
  this.#renderedVNode.destroy();
9779
- // Then remove from DOM. Handle fragment markers if present
9780
- const startMarker = this.#renderedVNode.userData.get?.('vif_fragment_start');
9781
- const endMarker = this.#renderedVNode.userData.get?.('vif_fragment_end');
9782
- if (startMarker && endMarker && startMarker.parentNode === endMarker.parentNode && startMarker.parentNode) {
9783
- const parentNode = startMarker.parentNode;
9784
- let node = startMarker;
9785
- while (node) {
9786
- const next = node.nextSibling;
9787
- parentNode.removeChild(node);
9788
- if (node === endMarker)
9789
- break;
9790
- node = next;
9791
- }
9881
+ // Then remove from DOM. Handle fragment ranges if present.
9882
+ const range = this.#renderedVNode.fragmentRange;
9883
+ if (range) {
9884
+ range.remove();
9885
+ this.#renderedVNode.fragmentRange = undefined;
9792
9886
  this.#renderedVNode = undefined;
9793
9887
  return;
9794
9888
  }
@@ -10054,6 +10148,12 @@ class VForDirective {
10054
10148
  }
10055
10149
  // Then remove DOM nodes
10056
10150
  for (const vNode of this.#renderedItems.values()) {
10151
+ const range = vNode.fragmentRange;
10152
+ if (range) {
10153
+ range.remove();
10154
+ vNode.fragmentRange = undefined;
10155
+ continue;
10156
+ }
10057
10157
  if (vNode.node.parentNode) {
10058
10158
  vNode.node.parentNode.removeChild(vNode.node);
10059
10159
  }
@@ -10127,22 +10227,12 @@ class VForDirective {
10127
10227
  vNode.destroy();
10128
10228
  }
10129
10229
  }
10130
- // Then remove from DOM. Handle both Element nodes and fragment-marked ranges.
10230
+ // Then remove from DOM. Handle both Element nodes and fragment ranges.
10131
10231
  for (const vNode of nodesToRemove) {
10132
- // If this VNode stored fragment markers, remove the range between them
10133
- const startMarker = vNode.userData.get?.('vfor_fragment_start');
10134
- const endMarker = vNode.userData.get?.('vfor_fragment_end');
10135
- if (startMarker && endMarker && startMarker.parentNode === endMarker.parentNode && startMarker.parentNode) {
10136
- const parentNode = startMarker.parentNode;
10137
- let node = startMarker;
10138
- // Remove nodes from startMarker up to and including endMarker
10139
- while (node) {
10140
- const next = node.nextSibling;
10141
- parentNode.removeChild(node);
10142
- if (node === endMarker)
10143
- break;
10144
- node = next;
10145
- }
10232
+ const range = vNode.fragmentRange;
10233
+ if (range) {
10234
+ range.remove();
10235
+ vNode.fragmentRange = undefined;
10146
10236
  continue;
10147
10237
  }
10148
10238
  // Fallback: remove the node itself if it's attached
@@ -10182,26 +10272,14 @@ class VForDirective {
10182
10272
  bindings,
10183
10273
  dependentIdentifiers: depIds,
10184
10274
  });
10185
- // If clone is a DocumentFragment, insert it between start/end comment markers
10275
+ // If clone is a DocumentFragment, wrap it in a VFragmentRange so the
10276
+ // entire expanded content moves/removes as one atomic unit.
10186
10277
  if (clone.nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
10187
- const startMarker = document.createComment('#vfor-fragment-start');
10188
- const endMarker = document.createComment('#vfor-fragment-end');
10189
- // Insert start and end markers and the fragment's children between them
10190
- if (prevNode.nextSibling) {
10191
- parent.insertBefore(startMarker, prevNode.nextSibling);
10192
- }
10193
- else {
10194
- parent.appendChild(startMarker);
10195
- }
10196
- parent.insertBefore(endMarker, startMarker.nextSibling);
10197
- parent.insertBefore(clone, endMarker);
10198
- // Store markers on the VNode for later removal/movement
10199
- vNode.userData.set('vfor_fragment_start', startMarker);
10200
- vNode.userData.set('vfor_fragment_end', endMarker);
10278
+ const range = VFragmentRange.insert(parent, prevNode.nextSibling, 'vfor-fragment', clone);
10279
+ vNode.fragmentRange = range;
10201
10280
  newRenderedItems.set(key, vNode);
10202
10281
  vNode.forceUpdate();
10203
- // Use endMarker as prevNode for subsequent insertions
10204
- prevNode = endMarker;
10282
+ prevNode = range.lastNode;
10205
10283
  continue;
10206
10284
  }
10207
10285
  // Determine what to insert: anchor node (if exists) or the clone itself
@@ -10221,22 +10299,28 @@ class VForDirective {
10221
10299
  newRenderedItems.set(key, vNode);
10222
10300
  // Update bindings
10223
10301
  this.#updateItemBindings(vNode, context);
10224
- // Determine the actual node in DOM: prefer fragment end marker, then anchor node, then vNode.node
10225
- const fragmentEnd = vNode.userData.get?.('vfor_fragment_end');
10226
- const actualNode = fragmentEnd || vNode.anchorNode || vNode.node;
10227
- // Move to correct position if needed
10228
- if (prevNode.nextSibling !== actualNode) {
10229
- if (prevNode.nextSibling) {
10230
- parent.insertBefore(actualNode, prevNode.nextSibling);
10302
+ // For fragment-backed iterations, move the entire range atomically.
10303
+ // For single-node iterations, move just the node.
10304
+ const range = vNode.fragmentRange;
10305
+ if (range) {
10306
+ if (prevNode.nextSibling !== range.firstNode) {
10307
+ range.moveBefore(parent, prevNode.nextSibling);
10231
10308
  }
10232
- else {
10233
- parent.appendChild(actualNode);
10309
+ }
10310
+ else {
10311
+ const actualNode = vNode.anchorNode || vNode.node;
10312
+ if (prevNode.nextSibling !== actualNode) {
10313
+ if (prevNode.nextSibling) {
10314
+ parent.insertBefore(actualNode, prevNode.nextSibling);
10315
+ }
10316
+ else {
10317
+ parent.appendChild(actualNode);
10318
+ }
10234
10319
  }
10235
10320
  }
10236
10321
  }
10237
- // Use fragment end marker > anchor node > vNode.node as prevNode
10238
- const fragmentEndForPrev = vNode.userData.get?.('vfor_fragment_end');
10239
- prevNode = fragmentEndForPrev || vNode.anchorNode || vNode.node;
10322
+ // Advance prevNode to this iteration's last DOM node
10323
+ prevNode = vNode.fragmentRange?.lastNode ?? vNode.anchorNode ?? vNode.node;
10240
10324
  }
10241
10325
  // Update rendered items map
10242
10326
  this.#renderedItems = newRenderedItems;