@mintjamsinc/ichigojs 0.1.59 → 0.1.61

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!
@@ -8241,6 +8243,13 @@
8241
8243
  * their own alias (e.g. "file" -> "files[0]") without overwriting each other.
8242
8244
  */
8243
8245
  #localAliases = new Map();
8246
+ /**
8247
+ * Setters for writable computed properties. When a key registered here is assigned
8248
+ * through the bindings proxy (e.g. via v-model or `bindings.set`), the setter is invoked
8249
+ * instead of writing directly to the local store. The cached value of the computed property
8250
+ * is updated by the recompute cycle through `setSilent`, which bypasses this routing.
8251
+ */
8252
+ #writableComputeds = new Map();
8244
8253
  /**
8245
8254
  * Creates a new instance of VBindings.
8246
8255
  * @param parent The parent bindings, if any.
@@ -8260,6 +8269,14 @@
8260
8269
  return this.#parent?.raw[key];
8261
8270
  },
8262
8271
  set: (obj, key, value) => {
8272
+ // If this key is a writable computed, route the assignment through its setter.
8273
+ // `setSilent` (used to update the cached value during recompute) sets `suppressOnChange`,
8274
+ // which bypasses this routing so the cached value can be written directly.
8275
+ if (!this.#suppressOnChange && this.#writableComputeds.has(key)) {
8276
+ const setter = this.#writableComputeds.get(key);
8277
+ setter(value);
8278
+ return true;
8279
+ }
8263
8280
  let target = obj;
8264
8281
  if (!Reflect.has(target, key)) {
8265
8282
  for (let parent = this.#parent; parent; parent = parent.#parent) {
@@ -8446,6 +8463,16 @@
8446
8463
  this.#suppressOnChange = false;
8447
8464
  }
8448
8465
  }
8466
+ /**
8467
+ * Registers a setter for a writable computed property. When the given key is assigned
8468
+ * through the bindings proxy, the setter will be invoked instead of writing directly to
8469
+ * the local store.
8470
+ * @param key The computed property name.
8471
+ * @param setter The setter function to invoke on assignment.
8472
+ */
8473
+ registerWritableComputed(key, setter) {
8474
+ this.#writableComputeds.set(key, setter);
8475
+ }
8449
8476
  /**
8450
8477
  * Manually adds an identifier to the set of changed identifiers.
8451
8478
  * This is useful for computed properties that need to mark themselves as changed
@@ -12385,6 +12412,235 @@
12385
12412
  }
12386
12413
  }
12387
12414
 
12415
+ // Copyright (c) 2025 MintJams Inc. Licensed under MIT License.
12416
+ /**
12417
+ * Directive for managing focus on form elements.
12418
+ *
12419
+ * Usage:
12420
+ * <input v-focus> Focus once on mount.
12421
+ * <input v-focus.select> Focus + select all on mount.
12422
+ * <input v-focus="isOpen"> Focus when expression transitions from falsy to truthy.
12423
+ * <input v-focus.select="isOpen"> Conditional focus + select all.
12424
+ * <input v-focus.cursor-end="isOpen"> Conditional focus + place caret at end.
12425
+ *
12426
+ * Behavior notes:
12427
+ * - Without an expression, the element is focused exactly once after mount.
12428
+ * - With an expression, focus fires only on the falsy -> truthy edge,
12429
+ * so the user is not repeatedly re-focused on every reactive update.
12430
+ * - If the value is already truthy on mount, the element is focused.
12431
+ * - Focus is deferred via requestAnimationFrame so that elements which
12432
+ * become visible just before this directive runs (e.g. inside v-if /
12433
+ * display:none containers) can still receive focus reliably.
12434
+ */
12435
+ class VFocusDirective {
12436
+ /**
12437
+ * The virtual node to which this directive is applied.
12438
+ */
12439
+ #vNode;
12440
+ /**
12441
+ * Optional expression evaluator. When absent, the directive focuses once on mount.
12442
+ */
12443
+ #evaluator;
12444
+ /**
12445
+ * Modifiers extracted from the directive name (e.g. "select", "cursor-end").
12446
+ */
12447
+ #modifiers = new Set();
12448
+ /**
12449
+ * Last evaluated boolean value, used for falsy -> truthy edge detection.
12450
+ */
12451
+ #previousValue = false;
12452
+ /**
12453
+ * @param context The context for parsing the directive.
12454
+ */
12455
+ constructor(context) {
12456
+ this.#vNode = context.vNode;
12457
+ // Extract modifiers from the directive name
12458
+ // e.g., "v-focus.select" -> modifiers = {"select"}
12459
+ const attrName = context.attribute.name;
12460
+ if (attrName.startsWith(StandardDirectiveName.V_FOCUS + '.')) {
12461
+ const parts = attrName.split('.');
12462
+ parts.slice(1).forEach(mod => this.#modifiers.add(mod));
12463
+ }
12464
+ // Parse the expression and create the evaluator (optional)
12465
+ const expression = context.attribute.value;
12466
+ if (expression) {
12467
+ if (!context.vNode.bindings) {
12468
+ throw new Error('VFocusDirective requires bindings when an expression is provided');
12469
+ }
12470
+ this.#evaluator = ExpressionEvaluator.create(expression, context.vNode.bindings, context.vNode.vApplication.functionDependencies);
12471
+ }
12472
+ // Remove the directive attribute from the element
12473
+ this.#vNode.node.removeAttribute(context.attribute.name);
12474
+ }
12475
+ /**
12476
+ * @inheritdoc
12477
+ */
12478
+ get name() {
12479
+ return StandardDirectiveName.V_FOCUS;
12480
+ }
12481
+ /**
12482
+ * @inheritdoc
12483
+ */
12484
+ get vNode() {
12485
+ return this.#vNode;
12486
+ }
12487
+ /**
12488
+ * @inheritdoc
12489
+ */
12490
+ get needsAnchor() {
12491
+ return false;
12492
+ }
12493
+ /**
12494
+ * @inheritdoc
12495
+ */
12496
+ get bindingsPreparer() {
12497
+ return undefined;
12498
+ }
12499
+ /**
12500
+ * @inheritdoc
12501
+ */
12502
+ get domUpdater() {
12503
+ // Without an expression, there is nothing reactive to track.
12504
+ if (!this.#evaluator) {
12505
+ return undefined;
12506
+ }
12507
+ const identifiers = this.#evaluator.dependentIdentifiers;
12508
+ const evaluator = this.#evaluator;
12509
+ const focusElement = () => this.#focus();
12510
+ const updater = {
12511
+ get dependentIdentifiers() {
12512
+ return identifiers;
12513
+ },
12514
+ applyToDOM: () => {
12515
+ const value = evaluator.evaluateAsBoolean();
12516
+ const previous = this.#previousValue;
12517
+ this.#previousValue = value;
12518
+ // Edge: falsy -> truthy
12519
+ if (!previous && value) {
12520
+ focusElement();
12521
+ }
12522
+ }
12523
+ };
12524
+ return updater;
12525
+ }
12526
+ /**
12527
+ * @inheritdoc
12528
+ */
12529
+ get templatize() {
12530
+ return false;
12531
+ }
12532
+ /**
12533
+ * @inheritdoc
12534
+ */
12535
+ get dependentIdentifiers() {
12536
+ return this.#evaluator?.dependentIdentifiers ?? [];
12537
+ }
12538
+ /**
12539
+ * @inheritdoc
12540
+ */
12541
+ get onMount() {
12542
+ return undefined;
12543
+ }
12544
+ /**
12545
+ * @inheritdoc
12546
+ */
12547
+ get onMounted() {
12548
+ return () => {
12549
+ if (!this.#evaluator) {
12550
+ // Unconditional: focus once on mount.
12551
+ this.#focus();
12552
+ return;
12553
+ }
12554
+ // Conditional: seed previous value and focus if already truthy on mount.
12555
+ const value = this.#evaluator.evaluateAsBoolean();
12556
+ this.#previousValue = value;
12557
+ if (value) {
12558
+ this.#focus();
12559
+ }
12560
+ };
12561
+ }
12562
+ /**
12563
+ * @inheritdoc
12564
+ */
12565
+ get onUpdate() {
12566
+ return undefined;
12567
+ }
12568
+ /**
12569
+ * @inheritdoc
12570
+ */
12571
+ get onUpdated() {
12572
+ return undefined;
12573
+ }
12574
+ /**
12575
+ * @inheritdoc
12576
+ */
12577
+ get onUnmount() {
12578
+ return undefined;
12579
+ }
12580
+ /**
12581
+ * @inheritdoc
12582
+ */
12583
+ get onUnmounted() {
12584
+ return undefined;
12585
+ }
12586
+ /**
12587
+ * @inheritdoc
12588
+ */
12589
+ destroy() {
12590
+ // No specific cleanup needed for this directive.
12591
+ }
12592
+ /**
12593
+ * Focuses the element, applying any modifier-driven post-focus behavior.
12594
+ * Deferred via requestAnimationFrame so that elements transitioning out
12595
+ * of display:none (e.g. inside v-if) can receive focus reliably.
12596
+ */
12597
+ #focus() {
12598
+ const element = this.#vNode.node;
12599
+ if (!element || typeof element.focus !== 'function') {
12600
+ return;
12601
+ }
12602
+ const applyModifiers = () => this.#applyModifiers();
12603
+ requestAnimationFrame(() => {
12604
+ // The element may have been unmounted between scheduling and execution.
12605
+ if (!element.isConnected) {
12606
+ return;
12607
+ }
12608
+ element.focus();
12609
+ applyModifiers();
12610
+ });
12611
+ }
12612
+ /**
12613
+ * Applies modifier-driven behavior after focus.
12614
+ */
12615
+ #applyModifiers() {
12616
+ const element = this.#vNode.node;
12617
+ if (this.#modifiers.has('select')) {
12618
+ const inputEl = element;
12619
+ if (typeof inputEl.select === 'function') {
12620
+ try {
12621
+ inputEl.select();
12622
+ }
12623
+ catch {
12624
+ // Some input types (e.g. number) reject select(); ignore.
12625
+ }
12626
+ }
12627
+ return;
12628
+ }
12629
+ if (this.#modifiers.has('cursor-end')) {
12630
+ const inputEl = element;
12631
+ if (typeof inputEl.setSelectionRange === 'function') {
12632
+ const len = (inputEl.value ?? '').length;
12633
+ try {
12634
+ inputEl.setSelectionRange(len, len);
12635
+ }
12636
+ catch {
12637
+ // Some input types (e.g. number) do not support selection ranges; ignore.
12638
+ }
12639
+ }
12640
+ }
12641
+ }
12642
+ }
12643
+
12388
12644
  // Copyright (c) 2025 MintJams Inc. Licensed under MIT License.
12389
12645
  /**
12390
12646
  * The directive parser for standard directives.
@@ -12426,7 +12682,10 @@
12426
12682
  // v-html
12427
12683
  context.attribute.name === StandardDirectiveName.V_HTML ||
12428
12684
  // v-text
12429
- context.attribute.name === StandardDirectiveName.V_TEXT) {
12685
+ context.attribute.name === StandardDirectiveName.V_TEXT ||
12686
+ // v-focus, v-focus.<modifier>
12687
+ context.attribute.name === StandardDirectiveName.V_FOCUS ||
12688
+ context.attribute.name.startsWith(StandardDirectiveName.V_FOCUS + ".")) {
12430
12689
  return true;
12431
12690
  }
12432
12691
  return false;
@@ -12490,6 +12749,11 @@
12490
12749
  if (context.attribute.name === StandardDirectiveName.V_TEXT) {
12491
12750
  return new VTextDirective(context);
12492
12751
  }
12752
+ // v-focus, v-focus.<modifier>
12753
+ if (context.attribute.name === StandardDirectiveName.V_FOCUS ||
12754
+ context.attribute.name.startsWith(StandardDirectiveName.V_FOCUS + ".")) {
12755
+ return new VFocusDirective(context);
12756
+ }
12493
12757
  throw new Error(`The attribute "${context.attribute.name}" cannot be parsed by ${this.name}.`);
12494
12758
  }
12495
12759
  }
@@ -12885,8 +13149,15 @@
12885
13149
  this.#logger = this.#logManager.getLogger('VApplication');
12886
13150
  // Analyze function dependencies
12887
13151
  this.#functionDependencies = ExpressionUtils.analyzeFunctionDependencies(options.methods || {});
12888
- // Analyze computed dependencies
12889
- this.#computedDependencies = ExpressionUtils.analyzeFunctionDependencies(options.computed || {});
13152
+ // Analyze computed dependencies based on getter functions only.
13153
+ // Writable computeds (defined as { get, set }) contribute their getter for dependency analysis.
13154
+ const computedGetters = {};
13155
+ if (options.computed) {
13156
+ for (const [key, def] of Object.entries(options.computed)) {
13157
+ computedGetters[key] = VApplication.#getComputedGetter(def);
13158
+ }
13159
+ }
13160
+ this.#computedDependencies = ExpressionUtils.analyzeFunctionDependencies(computedGetters);
12890
13161
  // Initialize watcher manager
12891
13162
  this.#watcher = new VWatcher(this.#logger);
12892
13163
  // Initialize bindings from data, computed, and methods
@@ -13029,6 +13300,26 @@
13029
13300
  }
13030
13301
  }
13031
13302
  }
13303
+ /**
13304
+ * Extracts the getter function from a computed property definition.
13305
+ * Supports both bare function form and { get, set } object form.
13306
+ */
13307
+ static #getComputedGetter(def) {
13308
+ if (typeof def === 'function') {
13309
+ return def;
13310
+ }
13311
+ return def.get;
13312
+ }
13313
+ /**
13314
+ * Extracts the setter function from a computed property definition, if any.
13315
+ * Returns undefined for read-only (function-form) computed properties.
13316
+ */
13317
+ static #getComputedSetter(def) {
13318
+ if (typeof def === 'function') {
13319
+ return undefined;
13320
+ }
13321
+ return def.set;
13322
+ }
13032
13323
  /**
13033
13324
  * Computes dependent identifiers for a given computed property and value.
13034
13325
  * This is used to track dependencies in directives like v-for.
@@ -13091,6 +13382,20 @@
13091
13382
  }
13092
13383
  }
13093
13384
  }
13385
+ // Register setters for writable computed properties so that assignments to them
13386
+ // (e.g. via v-model or direct mutation through bindings.raw) route through the user-provided
13387
+ // setter, which typically writes back to underlying reactive properties.
13388
+ if (this.#options.computed) {
13389
+ for (const [key, def] of Object.entries(this.#options.computed)) {
13390
+ const setter = VApplication.#getComputedSetter(def);
13391
+ if (setter) {
13392
+ const bindings = this.#bindings;
13393
+ this.#bindings.registerWritableComputed(key, (value) => {
13394
+ setter.call(bindings.raw, value);
13395
+ });
13396
+ }
13397
+ }
13398
+ }
13094
13399
  // Add computed properties (initialization mode)
13095
13400
  this.#recomputeProperties(true);
13096
13401
  }
@@ -13249,7 +13554,7 @@
13249
13554
  return;
13250
13555
  }
13251
13556
  // Now compute this property
13252
- const computedFn = this.#options.computed[key];
13557
+ const computedFn = VApplication.#getComputedGetter(this.#options.computed[key]);
13253
13558
  try {
13254
13559
  const oldValue = this.#bindings?.get(key);
13255
13560
  const newValue = computedFn.call(this.#bindings?.raw);