@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.
@@ -171,6 +171,8 @@ var StandardDirectiveName;
171
171
  StandardDirectiveName["V_HTML"] = "v-html";
172
172
  /** Text content directive. */
173
173
  StandardDirectiveName["V_TEXT"] = "v-text";
174
+ /** Focus management directive. */
175
+ StandardDirectiveName["V_FOCUS"] = "v-focus";
174
176
  })(StandardDirectiveName || (StandardDirectiveName = {}));
175
177
 
176
178
  // This file was generated. Do not modify manually!
@@ -8963,6 +8965,13 @@ class VNode {
8963
8965
  * VNode is destroyed.
8964
8966
  */
8965
8967
  #userData;
8968
+ /**
8969
+ * When this VNode wraps a <template>-derived DocumentFragment that has been
8970
+ * expanded into the DOM, this range tracks the start/end boundary of the
8971
+ * expanded content. Directives such as v-for and v-if use this to move or
8972
+ * remove the entire fragment atomically.
8973
+ */
8974
+ #fragmentRange;
8966
8975
  /**
8967
8976
  * Creates an instance of the virtual node.
8968
8977
  * @param args The initialization arguments for the virtual node.
@@ -9198,6 +9207,17 @@ class VNode {
9198
9207
  }
9199
9208
  return this.#userData;
9200
9209
  }
9210
+ /**
9211
+ * The DOM range that bounds this VNode's expanded fragment content, if any.
9212
+ * Set by directives that expand a <template> into a DocumentFragment
9213
+ * (currently v-for and v-if).
9214
+ */
9215
+ get fragmentRange() {
9216
+ return this.#fragmentRange;
9217
+ }
9218
+ set fragmentRange(range) {
9219
+ this.#fragmentRange = range;
9220
+ }
9201
9221
  /**
9202
9222
  * The DOM path of this virtual node.
9203
9223
  * This is a string representation of the path from the root to this node,
@@ -9543,6 +9563,102 @@ class VConditionalDirectiveContext {
9543
9563
  }
9544
9564
  }
9545
9565
 
9566
+ // Copyright (c) 2025 MintJams Inc. Licensed under MIT License.
9567
+ /**
9568
+ * Represents a contiguous range of DOM nodes that conceptually belong to a single VNode
9569
+ * whose source is a <template> element (and therefore expands to multiple sibling nodes
9570
+ * when inserted into the DOM).
9571
+ *
9572
+ * The range is bounded by two comment markers (start and end). All nodes between the
9573
+ * markers (inclusive) form the range. Move and remove operations always act on the
9574
+ * entire range atomically via the DOM Range API, which prevents the marker pair from
9575
+ * becoming desynchronized from the content it bounds.
9576
+ *
9577
+ * This abstraction replaces ad-hoc per-marker bookkeeping previously stored in
9578
+ * {@link VNode.userData} by directives such as v-for and v-if.
9579
+ */
9580
+ class VFragmentRange {
9581
+ #start;
9582
+ #end;
9583
+ constructor(start, end) {
9584
+ this.#start = start;
9585
+ this.#end = end;
9586
+ }
9587
+ /**
9588
+ * Inserts content into a parent before the given reference sibling (or as last
9589
+ * child when {@code refSibling} is null), wrapping it with start/end comment
9590
+ * markers. Returns a {@link VFragmentRange} that tracks the inserted region.
9591
+ *
9592
+ * @param parent The parent node to insert into.
9593
+ * @param refSibling The sibling to insert before (null = append).
9594
+ * @param label A short label used for the marker comment text (helps debugging).
9595
+ * @param content The content to insert. Typically a DocumentFragment cloned
9596
+ * from a template's content, but any Node works.
9597
+ */
9598
+ static insert(parent, refSibling, label, content) {
9599
+ const start = document.createComment(`#${label}-start`);
9600
+ const end = document.createComment(`#${label}-end`);
9601
+ parent.insertBefore(start, refSibling);
9602
+ parent.insertBefore(end, refSibling);
9603
+ parent.insertBefore(content, end);
9604
+ return new VFragmentRange(start, end);
9605
+ }
9606
+ /**
9607
+ * The start marker (inclusive boundary).
9608
+ */
9609
+ get firstNode() {
9610
+ return this.#start;
9611
+ }
9612
+ /**
9613
+ * The end marker (inclusive boundary).
9614
+ */
9615
+ get lastNode() {
9616
+ return this.#end;
9617
+ }
9618
+ /**
9619
+ * Move the entire range (start marker through end marker, inclusive) to be
9620
+ * inserted before {@code refSibling} under {@code parent}. If {@code refSibling}
9621
+ * is null the range is appended.
9622
+ *
9623
+ * Implemented via the DOM Range API so that all nodes between the markers move
9624
+ * together as a single contiguous block, regardless of how many or which kinds
9625
+ * of nodes currently sit between them.
9626
+ */
9627
+ moveBefore(parent, refSibling) {
9628
+ // No-op if already in the desired position
9629
+ if (refSibling === this.#start) {
9630
+ return;
9631
+ }
9632
+ const range = this.#asDomRange();
9633
+ const fragment = range.extractContents();
9634
+ parent.insertBefore(fragment, refSibling);
9635
+ range.detach();
9636
+ }
9637
+ /**
9638
+ * Remove the entire range (start marker through end marker, inclusive) from its
9639
+ * current parent. Safe to call even if the markers are already detached.
9640
+ */
9641
+ remove() {
9642
+ if (!this.#start.parentNode) {
9643
+ return;
9644
+ }
9645
+ const range = this.#asDomRange();
9646
+ range.deleteContents();
9647
+ range.detach();
9648
+ }
9649
+ /**
9650
+ * Builds a DOM {@link Range} that spans from immediately before the start marker
9651
+ * to immediately after the end marker, covering both markers and everything
9652
+ * between them.
9653
+ */
9654
+ #asDomRange() {
9655
+ const range = document.createRange();
9656
+ range.setStartBefore(this.#start);
9657
+ range.setEndAfter(this.#end);
9658
+ return range;
9659
+ }
9660
+ }
9661
+
9546
9662
  // Copyright (c) 2025 MintJams Inc. Licensed under MIT License.
9547
9663
  /**
9548
9664
  * Base class for conditional directives such as v-if, v-else-if, and v-else.
@@ -9743,19 +9859,7 @@ class VConditionalDirective {
9743
9859
  const anchorParent = this.#vNode.anchorNode?.parentNode;
9744
9860
  const nextSibling = this.#vNode.anchorNode?.nextSibling ?? null;
9745
9861
  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);
9862
+ vNode.fragmentRange = VFragmentRange.insert(anchorParent, nextSibling, 'vif-fragment', clone);
9759
9863
  this.#renderedVNode = vNode;
9760
9864
  this.#renderedVNode.forceUpdate();
9761
9865
  return;
@@ -9776,19 +9880,11 @@ class VConditionalDirective {
9776
9880
  }
9777
9881
  // Destroy VNode first (calls @unmount hooks while DOM is still accessible)
9778
9882
  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
- }
9883
+ // Then remove from DOM. Handle fragment ranges if present.
9884
+ const range = this.#renderedVNode.fragmentRange;
9885
+ if (range) {
9886
+ range.remove();
9887
+ this.#renderedVNode.fragmentRange = undefined;
9792
9888
  this.#renderedVNode = undefined;
9793
9889
  return;
9794
9890
  }
@@ -10054,6 +10150,12 @@ class VForDirective {
10054
10150
  }
10055
10151
  // Then remove DOM nodes
10056
10152
  for (const vNode of this.#renderedItems.values()) {
10153
+ const range = vNode.fragmentRange;
10154
+ if (range) {
10155
+ range.remove();
10156
+ vNode.fragmentRange = undefined;
10157
+ continue;
10158
+ }
10057
10159
  if (vNode.node.parentNode) {
10058
10160
  vNode.node.parentNode.removeChild(vNode.node);
10059
10161
  }
@@ -10127,22 +10229,12 @@ class VForDirective {
10127
10229
  vNode.destroy();
10128
10230
  }
10129
10231
  }
10130
- // Then remove from DOM. Handle both Element nodes and fragment-marked ranges.
10232
+ // Then remove from DOM. Handle both Element nodes and fragment ranges.
10131
10233
  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
- }
10234
+ const range = vNode.fragmentRange;
10235
+ if (range) {
10236
+ range.remove();
10237
+ vNode.fragmentRange = undefined;
10146
10238
  continue;
10147
10239
  }
10148
10240
  // Fallback: remove the node itself if it's attached
@@ -10182,26 +10274,14 @@ class VForDirective {
10182
10274
  bindings,
10183
10275
  dependentIdentifiers: depIds,
10184
10276
  });
10185
- // If clone is a DocumentFragment, insert it between start/end comment markers
10277
+ // If clone is a DocumentFragment, wrap it in a VFragmentRange so the
10278
+ // entire expanded content moves/removes as one atomic unit.
10186
10279
  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);
10280
+ const range = VFragmentRange.insert(parent, prevNode.nextSibling, 'vfor-fragment', clone);
10281
+ vNode.fragmentRange = range;
10201
10282
  newRenderedItems.set(key, vNode);
10202
10283
  vNode.forceUpdate();
10203
- // Use endMarker as prevNode for subsequent insertions
10204
- prevNode = endMarker;
10284
+ prevNode = range.lastNode;
10205
10285
  continue;
10206
10286
  }
10207
10287
  // Determine what to insert: anchor node (if exists) or the clone itself
@@ -10221,22 +10301,28 @@ class VForDirective {
10221
10301
  newRenderedItems.set(key, vNode);
10222
10302
  // Update bindings
10223
10303
  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);
10304
+ // For fragment-backed iterations, move the entire range atomically.
10305
+ // For single-node iterations, move just the node.
10306
+ const range = vNode.fragmentRange;
10307
+ if (range) {
10308
+ if (prevNode.nextSibling !== range.firstNode) {
10309
+ range.moveBefore(parent, prevNode.nextSibling);
10231
10310
  }
10232
- else {
10233
- parent.appendChild(actualNode);
10311
+ }
10312
+ else {
10313
+ const actualNode = vNode.anchorNode || vNode.node;
10314
+ if (prevNode.nextSibling !== actualNode) {
10315
+ if (prevNode.nextSibling) {
10316
+ parent.insertBefore(actualNode, prevNode.nextSibling);
10317
+ }
10318
+ else {
10319
+ parent.appendChild(actualNode);
10320
+ }
10234
10321
  }
10235
10322
  }
10236
10323
  }
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;
10324
+ // Advance prevNode to this iteration's last DOM node
10325
+ prevNode = vNode.fragmentRange?.lastNode ?? vNode.anchorNode ?? vNode.node;
10240
10326
  }
10241
10327
  // Update rendered items map
10242
10328
  this.#renderedItems = newRenderedItems;
@@ -12295,6 +12381,235 @@ class VTextDirective {
12295
12381
  }
12296
12382
  }
12297
12383
 
12384
+ // Copyright (c) 2025 MintJams Inc. Licensed under MIT License.
12385
+ /**
12386
+ * Directive for managing focus on form elements.
12387
+ *
12388
+ * Usage:
12389
+ * <input v-focus> Focus once on mount.
12390
+ * <input v-focus.select> Focus + select all on mount.
12391
+ * <input v-focus="isOpen"> Focus when expression transitions from falsy to truthy.
12392
+ * <input v-focus.select="isOpen"> Conditional focus + select all.
12393
+ * <input v-focus.cursor-end="isOpen"> Conditional focus + place caret at end.
12394
+ *
12395
+ * Behavior notes:
12396
+ * - Without an expression, the element is focused exactly once after mount.
12397
+ * - With an expression, focus fires only on the falsy -> truthy edge,
12398
+ * so the user is not repeatedly re-focused on every reactive update.
12399
+ * - If the value is already truthy on mount, the element is focused.
12400
+ * - Focus is deferred via requestAnimationFrame so that elements which
12401
+ * become visible just before this directive runs (e.g. inside v-if /
12402
+ * display:none containers) can still receive focus reliably.
12403
+ */
12404
+ class VFocusDirective {
12405
+ /**
12406
+ * The virtual node to which this directive is applied.
12407
+ */
12408
+ #vNode;
12409
+ /**
12410
+ * Optional expression evaluator. When absent, the directive focuses once on mount.
12411
+ */
12412
+ #evaluator;
12413
+ /**
12414
+ * Modifiers extracted from the directive name (e.g. "select", "cursor-end").
12415
+ */
12416
+ #modifiers = new Set();
12417
+ /**
12418
+ * Last evaluated boolean value, used for falsy -> truthy edge detection.
12419
+ */
12420
+ #previousValue = false;
12421
+ /**
12422
+ * @param context The context for parsing the directive.
12423
+ */
12424
+ constructor(context) {
12425
+ this.#vNode = context.vNode;
12426
+ // Extract modifiers from the directive name
12427
+ // e.g., "v-focus.select" -> modifiers = {"select"}
12428
+ const attrName = context.attribute.name;
12429
+ if (attrName.startsWith(StandardDirectiveName.V_FOCUS + '.')) {
12430
+ const parts = attrName.split('.');
12431
+ parts.slice(1).forEach(mod => this.#modifiers.add(mod));
12432
+ }
12433
+ // Parse the expression and create the evaluator (optional)
12434
+ const expression = context.attribute.value;
12435
+ if (expression) {
12436
+ if (!context.vNode.bindings) {
12437
+ throw new Error('VFocusDirective requires bindings when an expression is provided');
12438
+ }
12439
+ this.#evaluator = ExpressionEvaluator.create(expression, context.vNode.bindings, context.vNode.vApplication.functionDependencies);
12440
+ }
12441
+ // Remove the directive attribute from the element
12442
+ this.#vNode.node.removeAttribute(context.attribute.name);
12443
+ }
12444
+ /**
12445
+ * @inheritdoc
12446
+ */
12447
+ get name() {
12448
+ return StandardDirectiveName.V_FOCUS;
12449
+ }
12450
+ /**
12451
+ * @inheritdoc
12452
+ */
12453
+ get vNode() {
12454
+ return this.#vNode;
12455
+ }
12456
+ /**
12457
+ * @inheritdoc
12458
+ */
12459
+ get needsAnchor() {
12460
+ return false;
12461
+ }
12462
+ /**
12463
+ * @inheritdoc
12464
+ */
12465
+ get bindingsPreparer() {
12466
+ return undefined;
12467
+ }
12468
+ /**
12469
+ * @inheritdoc
12470
+ */
12471
+ get domUpdater() {
12472
+ // Without an expression, there is nothing reactive to track.
12473
+ if (!this.#evaluator) {
12474
+ return undefined;
12475
+ }
12476
+ const identifiers = this.#evaluator.dependentIdentifiers;
12477
+ const evaluator = this.#evaluator;
12478
+ const focusElement = () => this.#focus();
12479
+ const updater = {
12480
+ get dependentIdentifiers() {
12481
+ return identifiers;
12482
+ },
12483
+ applyToDOM: () => {
12484
+ const value = evaluator.evaluateAsBoolean();
12485
+ const previous = this.#previousValue;
12486
+ this.#previousValue = value;
12487
+ // Edge: falsy -> truthy
12488
+ if (!previous && value) {
12489
+ focusElement();
12490
+ }
12491
+ }
12492
+ };
12493
+ return updater;
12494
+ }
12495
+ /**
12496
+ * @inheritdoc
12497
+ */
12498
+ get templatize() {
12499
+ return false;
12500
+ }
12501
+ /**
12502
+ * @inheritdoc
12503
+ */
12504
+ get dependentIdentifiers() {
12505
+ return this.#evaluator?.dependentIdentifiers ?? [];
12506
+ }
12507
+ /**
12508
+ * @inheritdoc
12509
+ */
12510
+ get onMount() {
12511
+ return undefined;
12512
+ }
12513
+ /**
12514
+ * @inheritdoc
12515
+ */
12516
+ get onMounted() {
12517
+ return () => {
12518
+ if (!this.#evaluator) {
12519
+ // Unconditional: focus once on mount.
12520
+ this.#focus();
12521
+ return;
12522
+ }
12523
+ // Conditional: seed previous value and focus if already truthy on mount.
12524
+ const value = this.#evaluator.evaluateAsBoolean();
12525
+ this.#previousValue = value;
12526
+ if (value) {
12527
+ this.#focus();
12528
+ }
12529
+ };
12530
+ }
12531
+ /**
12532
+ * @inheritdoc
12533
+ */
12534
+ get onUpdate() {
12535
+ return undefined;
12536
+ }
12537
+ /**
12538
+ * @inheritdoc
12539
+ */
12540
+ get onUpdated() {
12541
+ return undefined;
12542
+ }
12543
+ /**
12544
+ * @inheritdoc
12545
+ */
12546
+ get onUnmount() {
12547
+ return undefined;
12548
+ }
12549
+ /**
12550
+ * @inheritdoc
12551
+ */
12552
+ get onUnmounted() {
12553
+ return undefined;
12554
+ }
12555
+ /**
12556
+ * @inheritdoc
12557
+ */
12558
+ destroy() {
12559
+ // No specific cleanup needed for this directive.
12560
+ }
12561
+ /**
12562
+ * Focuses the element, applying any modifier-driven post-focus behavior.
12563
+ * Deferred via requestAnimationFrame so that elements transitioning out
12564
+ * of display:none (e.g. inside v-if) can receive focus reliably.
12565
+ */
12566
+ #focus() {
12567
+ const element = this.#vNode.node;
12568
+ if (!element || typeof element.focus !== 'function') {
12569
+ return;
12570
+ }
12571
+ const applyModifiers = () => this.#applyModifiers();
12572
+ requestAnimationFrame(() => {
12573
+ // The element may have been unmounted between scheduling and execution.
12574
+ if (!element.isConnected) {
12575
+ return;
12576
+ }
12577
+ element.focus();
12578
+ applyModifiers();
12579
+ });
12580
+ }
12581
+ /**
12582
+ * Applies modifier-driven behavior after focus.
12583
+ */
12584
+ #applyModifiers() {
12585
+ const element = this.#vNode.node;
12586
+ if (this.#modifiers.has('select')) {
12587
+ const inputEl = element;
12588
+ if (typeof inputEl.select === 'function') {
12589
+ try {
12590
+ inputEl.select();
12591
+ }
12592
+ catch {
12593
+ // Some input types (e.g. number) reject select(); ignore.
12594
+ }
12595
+ }
12596
+ return;
12597
+ }
12598
+ if (this.#modifiers.has('cursor-end')) {
12599
+ const inputEl = element;
12600
+ if (typeof inputEl.setSelectionRange === 'function') {
12601
+ const len = (inputEl.value ?? '').length;
12602
+ try {
12603
+ inputEl.setSelectionRange(len, len);
12604
+ }
12605
+ catch {
12606
+ // Some input types (e.g. number) do not support selection ranges; ignore.
12607
+ }
12608
+ }
12609
+ }
12610
+ }
12611
+ }
12612
+
12298
12613
  // Copyright (c) 2025 MintJams Inc. Licensed under MIT License.
12299
12614
  /**
12300
12615
  * The directive parser for standard directives.
@@ -12336,7 +12651,10 @@ class VStandardDirectiveParser {
12336
12651
  // v-html
12337
12652
  context.attribute.name === StandardDirectiveName.V_HTML ||
12338
12653
  // v-text
12339
- context.attribute.name === StandardDirectiveName.V_TEXT) {
12654
+ context.attribute.name === StandardDirectiveName.V_TEXT ||
12655
+ // v-focus, v-focus.<modifier>
12656
+ context.attribute.name === StandardDirectiveName.V_FOCUS ||
12657
+ context.attribute.name.startsWith(StandardDirectiveName.V_FOCUS + ".")) {
12340
12658
  return true;
12341
12659
  }
12342
12660
  return false;
@@ -12400,6 +12718,11 @@ class VStandardDirectiveParser {
12400
12718
  if (context.attribute.name === StandardDirectiveName.V_TEXT) {
12401
12719
  return new VTextDirective(context);
12402
12720
  }
12721
+ // v-focus, v-focus.<modifier>
12722
+ if (context.attribute.name === StandardDirectiveName.V_FOCUS ||
12723
+ context.attribute.name.startsWith(StandardDirectiveName.V_FOCUS + ".")) {
12724
+ return new VFocusDirective(context);
12725
+ }
12403
12726
  throw new Error(`The attribute "${context.attribute.name}" cannot be parsed by ${this.name}.`);
12404
12727
  }
12405
12728
  }