@mintjamsinc/ichigojs 0.1.58 → 0.1.60

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
@@ -177,6 +177,8 @@
177
177
  StandardDirectiveName["V_HTML"] = "v-html";
178
178
  /** Text content directive. */
179
179
  StandardDirectiveName["V_TEXT"] = "v-text";
180
+ /** Focus management directive. */
181
+ StandardDirectiveName["V_FOCUS"] = "v-focus";
180
182
  })(StandardDirectiveName || (StandardDirectiveName = {}));
181
183
 
182
184
  // This file was generated. Do not modify manually!
@@ -8969,6 +8971,13 @@
8969
8971
  * VNode is destroyed.
8970
8972
  */
8971
8973
  #userData;
8974
+ /**
8975
+ * When this VNode wraps a <template>-derived DocumentFragment that has been
8976
+ * expanded into the DOM, this range tracks the start/end boundary of the
8977
+ * expanded content. Directives such as v-for and v-if use this to move or
8978
+ * remove the entire fragment atomically.
8979
+ */
8980
+ #fragmentRange;
8972
8981
  /**
8973
8982
  * Creates an instance of the virtual node.
8974
8983
  * @param args The initialization arguments for the virtual node.
@@ -9204,6 +9213,17 @@
9204
9213
  }
9205
9214
  return this.#userData;
9206
9215
  }
9216
+ /**
9217
+ * The DOM range that bounds this VNode's expanded fragment content, if any.
9218
+ * Set by directives that expand a <template> into a DocumentFragment
9219
+ * (currently v-for and v-if).
9220
+ */
9221
+ get fragmentRange() {
9222
+ return this.#fragmentRange;
9223
+ }
9224
+ set fragmentRange(range) {
9225
+ this.#fragmentRange = range;
9226
+ }
9207
9227
  /**
9208
9228
  * The DOM path of this virtual node.
9209
9229
  * This is a string representation of the path from the root to this node,
@@ -9549,6 +9569,102 @@
9549
9569
  }
9550
9570
  }
9551
9571
 
9572
+ // Copyright (c) 2025 MintJams Inc. Licensed under MIT License.
9573
+ /**
9574
+ * Represents a contiguous range of DOM nodes that conceptually belong to a single VNode
9575
+ * whose source is a <template> element (and therefore expands to multiple sibling nodes
9576
+ * when inserted into the DOM).
9577
+ *
9578
+ * The range is bounded by two comment markers (start and end). All nodes between the
9579
+ * markers (inclusive) form the range. Move and remove operations always act on the
9580
+ * entire range atomically via the DOM Range API, which prevents the marker pair from
9581
+ * becoming desynchronized from the content it bounds.
9582
+ *
9583
+ * This abstraction replaces ad-hoc per-marker bookkeeping previously stored in
9584
+ * {@link VNode.userData} by directives such as v-for and v-if.
9585
+ */
9586
+ class VFragmentRange {
9587
+ #start;
9588
+ #end;
9589
+ constructor(start, end) {
9590
+ this.#start = start;
9591
+ this.#end = end;
9592
+ }
9593
+ /**
9594
+ * Inserts content into a parent before the given reference sibling (or as last
9595
+ * child when {@code refSibling} is null), wrapping it with start/end comment
9596
+ * markers. Returns a {@link VFragmentRange} that tracks the inserted region.
9597
+ *
9598
+ * @param parent The parent node to insert into.
9599
+ * @param refSibling The sibling to insert before (null = append).
9600
+ * @param label A short label used for the marker comment text (helps debugging).
9601
+ * @param content The content to insert. Typically a DocumentFragment cloned
9602
+ * from a template's content, but any Node works.
9603
+ */
9604
+ static insert(parent, refSibling, label, content) {
9605
+ const start = document.createComment(`#${label}-start`);
9606
+ const end = document.createComment(`#${label}-end`);
9607
+ parent.insertBefore(start, refSibling);
9608
+ parent.insertBefore(end, refSibling);
9609
+ parent.insertBefore(content, end);
9610
+ return new VFragmentRange(start, end);
9611
+ }
9612
+ /**
9613
+ * The start marker (inclusive boundary).
9614
+ */
9615
+ get firstNode() {
9616
+ return this.#start;
9617
+ }
9618
+ /**
9619
+ * The end marker (inclusive boundary).
9620
+ */
9621
+ get lastNode() {
9622
+ return this.#end;
9623
+ }
9624
+ /**
9625
+ * Move the entire range (start marker through end marker, inclusive) to be
9626
+ * inserted before {@code refSibling} under {@code parent}. If {@code refSibling}
9627
+ * is null the range is appended.
9628
+ *
9629
+ * Implemented via the DOM Range API so that all nodes between the markers move
9630
+ * together as a single contiguous block, regardless of how many or which kinds
9631
+ * of nodes currently sit between them.
9632
+ */
9633
+ moveBefore(parent, refSibling) {
9634
+ // No-op if already in the desired position
9635
+ if (refSibling === this.#start) {
9636
+ return;
9637
+ }
9638
+ const range = this.#asDomRange();
9639
+ const fragment = range.extractContents();
9640
+ parent.insertBefore(fragment, refSibling);
9641
+ range.detach();
9642
+ }
9643
+ /**
9644
+ * Remove the entire range (start marker through end marker, inclusive) from its
9645
+ * current parent. Safe to call even if the markers are already detached.
9646
+ */
9647
+ remove() {
9648
+ if (!this.#start.parentNode) {
9649
+ return;
9650
+ }
9651
+ const range = this.#asDomRange();
9652
+ range.deleteContents();
9653
+ range.detach();
9654
+ }
9655
+ /**
9656
+ * Builds a DOM {@link Range} that spans from immediately before the start marker
9657
+ * to immediately after the end marker, covering both markers and everything
9658
+ * between them.
9659
+ */
9660
+ #asDomRange() {
9661
+ const range = document.createRange();
9662
+ range.setStartBefore(this.#start);
9663
+ range.setEndAfter(this.#end);
9664
+ return range;
9665
+ }
9666
+ }
9667
+
9552
9668
  // Copyright (c) 2025 MintJams Inc. Licensed under MIT License.
9553
9669
  /**
9554
9670
  * Base class for conditional directives such as v-if, v-else-if, and v-else.
@@ -9749,19 +9865,7 @@
9749
9865
  const anchorParent = this.#vNode.anchorNode?.parentNode;
9750
9866
  const nextSibling = this.#vNode.anchorNode?.nextSibling ?? null;
9751
9867
  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);
9868
+ vNode.fragmentRange = VFragmentRange.insert(anchorParent, nextSibling, 'vif-fragment', clone);
9765
9869
  this.#renderedVNode = vNode;
9766
9870
  this.#renderedVNode.forceUpdate();
9767
9871
  return;
@@ -9782,19 +9886,11 @@
9782
9886
  }
9783
9887
  // Destroy VNode first (calls @unmount hooks while DOM is still accessible)
9784
9888
  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
- }
9889
+ // Then remove from DOM. Handle fragment ranges if present.
9890
+ const range = this.#renderedVNode.fragmentRange;
9891
+ if (range) {
9892
+ range.remove();
9893
+ this.#renderedVNode.fragmentRange = undefined;
9798
9894
  this.#renderedVNode = undefined;
9799
9895
  return;
9800
9896
  }
@@ -10060,6 +10156,12 @@
10060
10156
  }
10061
10157
  // Then remove DOM nodes
10062
10158
  for (const vNode of this.#renderedItems.values()) {
10159
+ const range = vNode.fragmentRange;
10160
+ if (range) {
10161
+ range.remove();
10162
+ vNode.fragmentRange = undefined;
10163
+ continue;
10164
+ }
10063
10165
  if (vNode.node.parentNode) {
10064
10166
  vNode.node.parentNode.removeChild(vNode.node);
10065
10167
  }
@@ -10133,22 +10235,12 @@
10133
10235
  vNode.destroy();
10134
10236
  }
10135
10237
  }
10136
- // Then remove from DOM. Handle both Element nodes and fragment-marked ranges.
10238
+ // Then remove from DOM. Handle both Element nodes and fragment ranges.
10137
10239
  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
- }
10240
+ const range = vNode.fragmentRange;
10241
+ if (range) {
10242
+ range.remove();
10243
+ vNode.fragmentRange = undefined;
10152
10244
  continue;
10153
10245
  }
10154
10246
  // Fallback: remove the node itself if it's attached
@@ -10188,26 +10280,14 @@
10188
10280
  bindings,
10189
10281
  dependentIdentifiers: depIds,
10190
10282
  });
10191
- // If clone is a DocumentFragment, insert it between start/end comment markers
10283
+ // If clone is a DocumentFragment, wrap it in a VFragmentRange so the
10284
+ // entire expanded content moves/removes as one atomic unit.
10192
10285
  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);
10286
+ const range = VFragmentRange.insert(parent, prevNode.nextSibling, 'vfor-fragment', clone);
10287
+ vNode.fragmentRange = range;
10207
10288
  newRenderedItems.set(key, vNode);
10208
10289
  vNode.forceUpdate();
10209
- // Use endMarker as prevNode for subsequent insertions
10210
- prevNode = endMarker;
10290
+ prevNode = range.lastNode;
10211
10291
  continue;
10212
10292
  }
10213
10293
  // Determine what to insert: anchor node (if exists) or the clone itself
@@ -10227,22 +10307,28 @@
10227
10307
  newRenderedItems.set(key, vNode);
10228
10308
  // Update bindings
10229
10309
  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);
10310
+ // For fragment-backed iterations, move the entire range atomically.
10311
+ // For single-node iterations, move just the node.
10312
+ const range = vNode.fragmentRange;
10313
+ if (range) {
10314
+ if (prevNode.nextSibling !== range.firstNode) {
10315
+ range.moveBefore(parent, prevNode.nextSibling);
10237
10316
  }
10238
- else {
10239
- parent.appendChild(actualNode);
10317
+ }
10318
+ else {
10319
+ const actualNode = vNode.anchorNode || vNode.node;
10320
+ if (prevNode.nextSibling !== actualNode) {
10321
+ if (prevNode.nextSibling) {
10322
+ parent.insertBefore(actualNode, prevNode.nextSibling);
10323
+ }
10324
+ else {
10325
+ parent.appendChild(actualNode);
10326
+ }
10240
10327
  }
10241
10328
  }
10242
10329
  }
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;
10330
+ // Advance prevNode to this iteration's last DOM node
10331
+ prevNode = vNode.fragmentRange?.lastNode ?? vNode.anchorNode ?? vNode.node;
10246
10332
  }
10247
10333
  // Update rendered items map
10248
10334
  this.#renderedItems = newRenderedItems;
@@ -12301,6 +12387,235 @@
12301
12387
  }
12302
12388
  }
12303
12389
 
12390
+ // Copyright (c) 2025 MintJams Inc. Licensed under MIT License.
12391
+ /**
12392
+ * Directive for managing focus on form elements.
12393
+ *
12394
+ * Usage:
12395
+ * <input v-focus> Focus once on mount.
12396
+ * <input v-focus.select> Focus + select all on mount.
12397
+ * <input v-focus="isOpen"> Focus when expression transitions from falsy to truthy.
12398
+ * <input v-focus.select="isOpen"> Conditional focus + select all.
12399
+ * <input v-focus.cursor-end="isOpen"> Conditional focus + place caret at end.
12400
+ *
12401
+ * Behavior notes:
12402
+ * - Without an expression, the element is focused exactly once after mount.
12403
+ * - With an expression, focus fires only on the falsy -> truthy edge,
12404
+ * so the user is not repeatedly re-focused on every reactive update.
12405
+ * - If the value is already truthy on mount, the element is focused.
12406
+ * - Focus is deferred via requestAnimationFrame so that elements which
12407
+ * become visible just before this directive runs (e.g. inside v-if /
12408
+ * display:none containers) can still receive focus reliably.
12409
+ */
12410
+ class VFocusDirective {
12411
+ /**
12412
+ * The virtual node to which this directive is applied.
12413
+ */
12414
+ #vNode;
12415
+ /**
12416
+ * Optional expression evaluator. When absent, the directive focuses once on mount.
12417
+ */
12418
+ #evaluator;
12419
+ /**
12420
+ * Modifiers extracted from the directive name (e.g. "select", "cursor-end").
12421
+ */
12422
+ #modifiers = new Set();
12423
+ /**
12424
+ * Last evaluated boolean value, used for falsy -> truthy edge detection.
12425
+ */
12426
+ #previousValue = false;
12427
+ /**
12428
+ * @param context The context for parsing the directive.
12429
+ */
12430
+ constructor(context) {
12431
+ this.#vNode = context.vNode;
12432
+ // Extract modifiers from the directive name
12433
+ // e.g., "v-focus.select" -> modifiers = {"select"}
12434
+ const attrName = context.attribute.name;
12435
+ if (attrName.startsWith(StandardDirectiveName.V_FOCUS + '.')) {
12436
+ const parts = attrName.split('.');
12437
+ parts.slice(1).forEach(mod => this.#modifiers.add(mod));
12438
+ }
12439
+ // Parse the expression and create the evaluator (optional)
12440
+ const expression = context.attribute.value;
12441
+ if (expression) {
12442
+ if (!context.vNode.bindings) {
12443
+ throw new Error('VFocusDirective requires bindings when an expression is provided');
12444
+ }
12445
+ this.#evaluator = ExpressionEvaluator.create(expression, context.vNode.bindings, context.vNode.vApplication.functionDependencies);
12446
+ }
12447
+ // Remove the directive attribute from the element
12448
+ this.#vNode.node.removeAttribute(context.attribute.name);
12449
+ }
12450
+ /**
12451
+ * @inheritdoc
12452
+ */
12453
+ get name() {
12454
+ return StandardDirectiveName.V_FOCUS;
12455
+ }
12456
+ /**
12457
+ * @inheritdoc
12458
+ */
12459
+ get vNode() {
12460
+ return this.#vNode;
12461
+ }
12462
+ /**
12463
+ * @inheritdoc
12464
+ */
12465
+ get needsAnchor() {
12466
+ return false;
12467
+ }
12468
+ /**
12469
+ * @inheritdoc
12470
+ */
12471
+ get bindingsPreparer() {
12472
+ return undefined;
12473
+ }
12474
+ /**
12475
+ * @inheritdoc
12476
+ */
12477
+ get domUpdater() {
12478
+ // Without an expression, there is nothing reactive to track.
12479
+ if (!this.#evaluator) {
12480
+ return undefined;
12481
+ }
12482
+ const identifiers = this.#evaluator.dependentIdentifiers;
12483
+ const evaluator = this.#evaluator;
12484
+ const focusElement = () => this.#focus();
12485
+ const updater = {
12486
+ get dependentIdentifiers() {
12487
+ return identifiers;
12488
+ },
12489
+ applyToDOM: () => {
12490
+ const value = evaluator.evaluateAsBoolean();
12491
+ const previous = this.#previousValue;
12492
+ this.#previousValue = value;
12493
+ // Edge: falsy -> truthy
12494
+ if (!previous && value) {
12495
+ focusElement();
12496
+ }
12497
+ }
12498
+ };
12499
+ return updater;
12500
+ }
12501
+ /**
12502
+ * @inheritdoc
12503
+ */
12504
+ get templatize() {
12505
+ return false;
12506
+ }
12507
+ /**
12508
+ * @inheritdoc
12509
+ */
12510
+ get dependentIdentifiers() {
12511
+ return this.#evaluator?.dependentIdentifiers ?? [];
12512
+ }
12513
+ /**
12514
+ * @inheritdoc
12515
+ */
12516
+ get onMount() {
12517
+ return undefined;
12518
+ }
12519
+ /**
12520
+ * @inheritdoc
12521
+ */
12522
+ get onMounted() {
12523
+ return () => {
12524
+ if (!this.#evaluator) {
12525
+ // Unconditional: focus once on mount.
12526
+ this.#focus();
12527
+ return;
12528
+ }
12529
+ // Conditional: seed previous value and focus if already truthy on mount.
12530
+ const value = this.#evaluator.evaluateAsBoolean();
12531
+ this.#previousValue = value;
12532
+ if (value) {
12533
+ this.#focus();
12534
+ }
12535
+ };
12536
+ }
12537
+ /**
12538
+ * @inheritdoc
12539
+ */
12540
+ get onUpdate() {
12541
+ return undefined;
12542
+ }
12543
+ /**
12544
+ * @inheritdoc
12545
+ */
12546
+ get onUpdated() {
12547
+ return undefined;
12548
+ }
12549
+ /**
12550
+ * @inheritdoc
12551
+ */
12552
+ get onUnmount() {
12553
+ return undefined;
12554
+ }
12555
+ /**
12556
+ * @inheritdoc
12557
+ */
12558
+ get onUnmounted() {
12559
+ return undefined;
12560
+ }
12561
+ /**
12562
+ * @inheritdoc
12563
+ */
12564
+ destroy() {
12565
+ // No specific cleanup needed for this directive.
12566
+ }
12567
+ /**
12568
+ * Focuses the element, applying any modifier-driven post-focus behavior.
12569
+ * Deferred via requestAnimationFrame so that elements transitioning out
12570
+ * of display:none (e.g. inside v-if) can receive focus reliably.
12571
+ */
12572
+ #focus() {
12573
+ const element = this.#vNode.node;
12574
+ if (!element || typeof element.focus !== 'function') {
12575
+ return;
12576
+ }
12577
+ const applyModifiers = () => this.#applyModifiers();
12578
+ requestAnimationFrame(() => {
12579
+ // The element may have been unmounted between scheduling and execution.
12580
+ if (!element.isConnected) {
12581
+ return;
12582
+ }
12583
+ element.focus();
12584
+ applyModifiers();
12585
+ });
12586
+ }
12587
+ /**
12588
+ * Applies modifier-driven behavior after focus.
12589
+ */
12590
+ #applyModifiers() {
12591
+ const element = this.#vNode.node;
12592
+ if (this.#modifiers.has('select')) {
12593
+ const inputEl = element;
12594
+ if (typeof inputEl.select === 'function') {
12595
+ try {
12596
+ inputEl.select();
12597
+ }
12598
+ catch {
12599
+ // Some input types (e.g. number) reject select(); ignore.
12600
+ }
12601
+ }
12602
+ return;
12603
+ }
12604
+ if (this.#modifiers.has('cursor-end')) {
12605
+ const inputEl = element;
12606
+ if (typeof inputEl.setSelectionRange === 'function') {
12607
+ const len = (inputEl.value ?? '').length;
12608
+ try {
12609
+ inputEl.setSelectionRange(len, len);
12610
+ }
12611
+ catch {
12612
+ // Some input types (e.g. number) do not support selection ranges; ignore.
12613
+ }
12614
+ }
12615
+ }
12616
+ }
12617
+ }
12618
+
12304
12619
  // Copyright (c) 2025 MintJams Inc. Licensed under MIT License.
12305
12620
  /**
12306
12621
  * The directive parser for standard directives.
@@ -12342,7 +12657,10 @@
12342
12657
  // v-html
12343
12658
  context.attribute.name === StandardDirectiveName.V_HTML ||
12344
12659
  // v-text
12345
- context.attribute.name === StandardDirectiveName.V_TEXT) {
12660
+ context.attribute.name === StandardDirectiveName.V_TEXT ||
12661
+ // v-focus, v-focus.<modifier>
12662
+ context.attribute.name === StandardDirectiveName.V_FOCUS ||
12663
+ context.attribute.name.startsWith(StandardDirectiveName.V_FOCUS + ".")) {
12346
12664
  return true;
12347
12665
  }
12348
12666
  return false;
@@ -12406,6 +12724,11 @@
12406
12724
  if (context.attribute.name === StandardDirectiveName.V_TEXT) {
12407
12725
  return new VTextDirective(context);
12408
12726
  }
12727
+ // v-focus, v-focus.<modifier>
12728
+ if (context.attribute.name === StandardDirectiveName.V_FOCUS ||
12729
+ context.attribute.name.startsWith(StandardDirectiveName.V_FOCUS + ".")) {
12730
+ return new VFocusDirective(context);
12731
+ }
12409
12732
  throw new Error(`The attribute "${context.attribute.name}" cannot be parsed by ${this.name}.`);
12410
12733
  }
12411
12734
  }