@mintjamsinc/ichigojs 0.1.7 → 0.1.8

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/README.md CHANGED
@@ -16,8 +16,9 @@ A simple and intuitive reactive framework. Lightweight, fast, and user-friendly
16
16
  - 📦 **Lightweight** - Minimal bundle size
17
17
  - 🚀 **High Performance** - Efficient batched updates via microtask queue
18
18
  - 💪 **TypeScript** - Written in TypeScript with full type support
19
- - 🎨 **Directives** - `v-if`, `v-for`, `v-show`, `v-bind`, `v-on`, `v-model`, `v-resize`
19
+ - 🎨 **Directives** - `v-if`, `v-for`, `v-show`, `v-bind`, `v-on`, `v-model`, `v-resize`, `v-intersection`
20
20
  - 📐 **Resize Observer** - Monitor element size changes with `v-resize` directive
21
+ - 👁️ **Intersection Observer** - Detect element visibility with `v-intersection` directive
21
22
 
22
23
  ## Installation
23
24
 
@@ -201,7 +202,55 @@ methods: {
201
202
 
202
203
  **Features:**
203
204
  - Native ResizeObserver API for efficient resize detection
204
- - Automatic cleanup when element is removed
205
+ - Automatic cleanup in destroy phase
206
+ - Access to element, VNode, and userData via `$ctx`
207
+
208
+ #### v-intersection
209
+
210
+ Detect element visibility using IntersectionObserver:
211
+
212
+ ```html
213
+ <div v-intersection="onIntersection" class="observable-box">
214
+ I'm {{ isVisible ? 'VISIBLE' : 'NOT VISIBLE' }}
215
+ </div>
216
+ ```
217
+
218
+ ```javascript
219
+ methods: {
220
+ onIntersection(entries, $ctx) {
221
+ const entry = entries[0];
222
+ this.isVisible = entry.isIntersecting;
223
+ this.intersectionRatio = entry.intersectionRatio;
224
+
225
+ // Access element through $ctx
226
+ console.log('Element:', $ctx.element);
227
+ }
228
+ }
229
+ ```
230
+
231
+ **With custom options:**
232
+
233
+ ```html
234
+ <div v-intersection="onIntersection"
235
+ :options.intersection="{threshold: 0.5, rootMargin: '0px'}">
236
+ Triggers at 50% visibility
237
+ </div>
238
+ ```
239
+
240
+ You can also use `:options` for generic options:
241
+
242
+ ```html
243
+ <div v-intersection="onIntersection"
244
+ :options="{threshold: 0.5}">
245
+ Observable content
246
+ </div>
247
+ ```
248
+
249
+ **Features:**
250
+ - Native IntersectionObserver API for efficient visibility detection
251
+ - Custom threshold and rootMargin options via `:options.intersection` or `:options`
252
+ - Automatic cleanup in destroy phase
253
+ - Perfect for lazy loading, infinite scroll, and animation triggers
205
254
  - Access to element, VNode, and userData via `$ctx`
206
255
 
207
256
  #### Lifecycle Hooks
@@ -226,7 +275,7 @@ Lifecycle hooks allow you to run code at specific stages of an element's lifecyc
226
275
  - `@mounted` - Called after the element is inserted into the DOM
227
276
  - `@update` - Called before the element is updated
228
277
  - `@updated` - Called after the element is updated
229
- - `@unmount` - Called before the element is removed from the DOM
278
+ - `@unmount` - Called before VNode cleanup begins
230
279
  - `@unmounted` - Called after VNode cleanup is complete (element reference still available)
231
280
 
232
281
  **Lifecycle Context (`$ctx`):**
@@ -292,7 +341,7 @@ methods: {
292
341
  };
293
342
 
294
343
  $ctx.userData.set('myResource', resource);
295
- // resource.close() will be called automatically on unmount
344
+ // resource.close() will be called automatically during destroy phase
296
345
  }
297
346
  }
298
347
  ```
@@ -54,6 +54,7 @@ var StandardDirectiveName;
54
54
  StandardDirectiveName["V_BIND"] = "v-bind";
55
55
  StandardDirectiveName["V_MODEL"] = "v-model";
56
56
  StandardDirectiveName["V_RESIZE"] = "v-resize";
57
+ StandardDirectiveName["V_INTERSECTION"] = "v-intersection";
57
58
  })(StandardDirectiveName || (StandardDirectiveName = {}));
58
59
 
59
60
  // This file was generated. Do not modify manually!
@@ -6868,6 +6869,19 @@ class VBindDirective {
6868
6869
  get isKey() {
6869
6870
  return (this.#attributeName === 'key');
6870
6871
  }
6872
+ /**
6873
+ * Indicates if this directive is binding the "options" attribute or any of its sub-properties (e.g., "options.intersection").
6874
+ * The "options" attribute is special and is used for passing options to certain directives like VIntersectionDirective.
6875
+ */
6876
+ get isOptions() {
6877
+ return (this.#attributeName === 'options' || this.#attributeName?.startsWith('options.') === true);
6878
+ }
6879
+ /**
6880
+ * Gets the name of the attribute being bound (e.g., "src", "class", "options", "options.intersection").
6881
+ */
6882
+ get attributeName() {
6883
+ return this.#attributeName;
6884
+ }
6871
6885
  /**
6872
6886
  * Gets the original expression string from the directive.
6873
6887
  */
@@ -6920,8 +6934,8 @@ class VBindDirective {
6920
6934
  * Renders the bound attribute by evaluating the expression and updating the DOM element.
6921
6935
  */
6922
6936
  #render() {
6923
- // If this directive is binding the "key" attribute, do nothing
6924
- if (this.isKey) {
6937
+ // Do nothing for special attributes
6938
+ if (this.isKey || this.isOptions) {
6925
6939
  return;
6926
6940
  }
6927
6941
  const element = this.#vNode.node;
@@ -7365,16 +7379,47 @@ class VBindings {
7365
7379
  }
7366
7380
 
7367
7381
  // Copyright (c) 2025 MintJams Inc. Licensed under MIT License.
7382
+ /**
7383
+ * Manages directives associated with a virtual node (VNode).
7384
+ * This class is responsible for parsing, storing, and managing the lifecycle of directives.
7385
+ * It also provides access to bindings preparers and DOM updaters from the associated directives.
7386
+ */
7368
7387
  class VDirectiveManager {
7369
7388
  /**
7370
7389
  * The virtual node to which this directive handler is associated.
7371
7390
  */
7372
7391
  #vNode;
7392
+ /**
7393
+ * The list of directives associated with the virtual node.
7394
+ * This may be undefined if there are no directives.
7395
+ */
7373
7396
  #directives;
7397
+ /**
7398
+ * The anchor comment node used for certain directives (e.g., v-if, v-for).
7399
+ * This may be undefined if no directive requires an anchor.
7400
+ */
7374
7401
  #anchorNode;
7402
+ /**
7403
+ * The list of bindings preparers from the associated directives.
7404
+ * This may be undefined if no directive provides a bindings preparer.
7405
+ */
7375
7406
  #bindingsPreparers;
7407
+ /**
7408
+ * The list of DOM updaters from the associated directives.
7409
+ * This may be undefined if no directive provides a DOM updater.
7410
+ */
7376
7411
  #domUpdaters;
7412
+ /**
7413
+ * The directive that binds the ":key" or "v-bind:key" attribute, if any.
7414
+ * This directive is special and is used for optimizing rendering of lists.
7415
+ * If no such directive exists, this is undefined.
7416
+ */
7377
7417
  #keyDirective;
7418
+ /**
7419
+ * A cache of VBindDirectives for options specific to certain directives.
7420
+ * The keys are directive names (e.g., 'options', 'options.intersection').
7421
+ */
7422
+ #optionsDirectives = {};
7378
7423
  constructor(vNode) {
7379
7424
  // Directives can only be associated with element nodes
7380
7425
  if (vNode.nodeType !== Node.ELEMENT_NODE) {
@@ -7432,6 +7477,35 @@ class VDirectiveManager {
7432
7477
  get keyDirective() {
7433
7478
  return this.#keyDirective;
7434
7479
  }
7480
+ /**
7481
+ * Gets a record of VBindDirectives for options specific to certain directives.
7482
+ * The keys are directive names (e.g., 'options', 'options.intersection').
7483
+ */
7484
+ get optionsDirectives() {
7485
+ return this.#optionsDirectives;
7486
+ }
7487
+ /**
7488
+ * Gets the VBindDirective for options specific to the given directive name.
7489
+ * Searches in order: `:options.{directive}` -> `:options`
7490
+ *
7491
+ * @param directive The directive name (e.g., 'intersection', 'resize')
7492
+ * @returns The VBindDirective instance or undefined
7493
+ */
7494
+ optionsDirective(directive) {
7495
+ if (!this.#directives || this.#directives.length === 0) {
7496
+ return undefined;
7497
+ }
7498
+ // Search for `:options.{directive}` or `v-bind:options.{directive}` first
7499
+ const specificAttrName = `options.${directive}`;
7500
+ if (this.#optionsDirectives[specificAttrName]) {
7501
+ return this.#optionsDirectives[specificAttrName];
7502
+ }
7503
+ // Fallback: search for `:options` or `v-bind:options`
7504
+ if (this.#optionsDirectives['options']) {
7505
+ return this.#optionsDirectives['options'];
7506
+ }
7507
+ return undefined;
7508
+ }
7435
7509
  /**
7436
7510
  * Cleans up any resources used by the directive handler.
7437
7511
  */
@@ -7491,6 +7565,10 @@ class VDirectiveManager {
7491
7565
  if (directive.name === StandardDirectiveName.V_BIND && directive.isKey) {
7492
7566
  this.#keyDirective = directive;
7493
7567
  }
7568
+ // If this is an options binding directive, cache it
7569
+ if (directive.name === StandardDirectiveName.V_BIND && directive.isOptions) {
7570
+ this.#optionsDirectives[directive.name] = directive;
7571
+ }
7494
7572
  }
7495
7573
  }
7496
7574
  // Sort directives by priority: v-for > v-if > v-else-if > v-else > v-show > others
@@ -8978,6 +9056,209 @@ class VIfDirective extends VConditionalDirective {
8978
9056
  }
8979
9057
  }
8980
9058
 
9059
+ // Copyright (c) 2025 MintJams Inc. Licensed under MIT License.
9060
+ /**
9061
+ * Directive for observing element intersection with viewport or ancestor elements using IntersectionObserver.
9062
+ * The `v-intersection` directive allows you to respond to changes in an element's visibility.
9063
+ *
9064
+ * Example usage:
9065
+ * <div v-intersection="handleIntersection">Observable content</div>
9066
+ * <div v-intersection="handleIntersection" :options.intersection="{threshold: 0.5}">Observable content</div>
9067
+ *
9068
+ * The handler receives IntersectionObserverEntry array as the first argument and $ctx as the second:
9069
+ * handleIntersection(entries, $ctx) {
9070
+ * const entry = entries[0];
9071
+ * if (entry.isIntersecting) {
9072
+ * console.log('Element is visible!');
9073
+ * }
9074
+ * }
9075
+ *
9076
+ * Options can be provided via :options or :options.intersection attribute:
9077
+ * :options="{root: null, threshold: 0.5, rootMargin: '0px'}"
9078
+ * :options.intersection="{root: null, threshold: 0.5, rootMargin: '0px'}"
9079
+ *
9080
+ * This directive is useful for lazy-loading, infinite scrolling, animation triggers,
9081
+ * and other features that depend on element visibility.
9082
+ */
9083
+ class VIntersectionDirective {
9084
+ /**
9085
+ * The virtual node to which this directive is applied.
9086
+ */
9087
+ #vNode;
9088
+ /**
9089
+ * A list of variable and function names used in the directive's expression.
9090
+ */
9091
+ #dependentIdentifiers;
9092
+ /**
9093
+ * The intersection handler wrapper function.
9094
+ */
9095
+ #handlerWrapper;
9096
+ /**
9097
+ * The IntersectionObserver instance.
9098
+ */
9099
+ #intersectionObserver;
9100
+ /**
9101
+ * @param context The context for parsing the directive.
9102
+ */
9103
+ constructor(context) {
9104
+ this.#vNode = context.vNode;
9105
+ // Parse the expression to extract identifiers and create the handler wrapper
9106
+ const expression = context.attribute.value;
9107
+ if (expression) {
9108
+ this.#dependentIdentifiers = ExpressionUtils.extractIdentifiers(expression, context.vNode.vApplication.functionDependencies);
9109
+ this.#handlerWrapper = this.#createIntersectionHandlerWrapper(expression);
9110
+ }
9111
+ // Remove the directive attribute from the element
9112
+ this.#vNode.node.removeAttribute(context.attribute.name);
9113
+ }
9114
+ /**
9115
+ * @inheritdoc
9116
+ */
9117
+ get name() {
9118
+ return StandardDirectiveName.V_INTERSECTION;
9119
+ }
9120
+ /**
9121
+ * @inheritdoc
9122
+ */
9123
+ get vNode() {
9124
+ return this.#vNode;
9125
+ }
9126
+ /**
9127
+ * @inheritdoc
9128
+ */
9129
+ get needsAnchor() {
9130
+ return false;
9131
+ }
9132
+ /**
9133
+ * @inheritdoc
9134
+ */
9135
+ get bindingsPreparer() {
9136
+ return undefined;
9137
+ }
9138
+ /**
9139
+ * @inheritdoc
9140
+ */
9141
+ get domUpdater() {
9142
+ return undefined;
9143
+ }
9144
+ /**
9145
+ * @inheritdoc
9146
+ */
9147
+ get templatize() {
9148
+ return false;
9149
+ }
9150
+ /**
9151
+ * @inheritdoc
9152
+ */
9153
+ get dependentIdentifiers() {
9154
+ return this.#dependentIdentifiers ?? [];
9155
+ }
9156
+ /**
9157
+ * @inheritdoc
9158
+ */
9159
+ get onMount() {
9160
+ return undefined;
9161
+ }
9162
+ /**
9163
+ * @inheritdoc
9164
+ */
9165
+ get onMounted() {
9166
+ if (!this.#handlerWrapper) {
9167
+ return undefined;
9168
+ }
9169
+ const element = this.#vNode.node;
9170
+ const handler = this.#handlerWrapper;
9171
+ return () => {
9172
+ // Get options from :options.intersection or :options directive
9173
+ let optionsDirective = this.#vNode.directiveManager?.optionsDirective('intersection');
9174
+ // Evaluate the options expression
9175
+ let options;
9176
+ if (optionsDirective && optionsDirective.expression) {
9177
+ // Evaluate the options expression
9178
+ const identifiers = optionsDirective.dependentIdentifiers;
9179
+ const values = identifiers.map(id => this.#vNode.bindings?.get(id));
9180
+ const args = identifiers.join(", ");
9181
+ const funcBody = `return (${optionsDirective.expression});`;
9182
+ const func = new Function(args, funcBody);
9183
+ options = func(...values);
9184
+ }
9185
+ // Create IntersectionObserver and start observing
9186
+ this.#intersectionObserver = new IntersectionObserver((entries) => {
9187
+ handler(entries);
9188
+ }, options);
9189
+ this.#intersectionObserver.observe(element);
9190
+ };
9191
+ }
9192
+ /**
9193
+ * @inheritdoc
9194
+ */
9195
+ get onUpdate() {
9196
+ return undefined;
9197
+ }
9198
+ /**
9199
+ * @inheritdoc
9200
+ */
9201
+ get onUpdated() {
9202
+ return undefined;
9203
+ }
9204
+ /**
9205
+ * @inheritdoc
9206
+ */
9207
+ get onUnmount() {
9208
+ return undefined;
9209
+ }
9210
+ /**
9211
+ * @inheritdoc
9212
+ */
9213
+ get onUnmounted() {
9214
+ return undefined;
9215
+ }
9216
+ /**
9217
+ * @inheritdoc
9218
+ */
9219
+ destroy() {
9220
+ // Disconnect the IntersectionObserver when the directive is destroyed
9221
+ if (this.#intersectionObserver) {
9222
+ this.#intersectionObserver.disconnect();
9223
+ this.#intersectionObserver = undefined;
9224
+ }
9225
+ }
9226
+ /**
9227
+ * Creates a wrapper function for intersection handlers.
9228
+ * @param expression The expression string to evaluate.
9229
+ * @returns A function that handles the intersection event.
9230
+ */
9231
+ #createIntersectionHandlerWrapper(expression) {
9232
+ const identifiers = this.#dependentIdentifiers ?? [];
9233
+ const vNode = this.#vNode;
9234
+ // Return a function that handles the intersection event with proper scope
9235
+ return (entries) => {
9236
+ const bindings = vNode.bindings;
9237
+ const $ctx = {
9238
+ element: vNode.node,
9239
+ vnode: vNode,
9240
+ userData: vNode.userData
9241
+ };
9242
+ // If the expression is just a method name, call it with bindings as 'this'
9243
+ const trimmedExpr = expression.trim();
9244
+ if (identifiers.includes(trimmedExpr) && typeof bindings?.get(trimmedExpr) === 'function') {
9245
+ const methodName = trimmedExpr;
9246
+ const originalMethod = bindings?.get(methodName);
9247
+ // Call the method with bindings as 'this' context
9248
+ // Pass entries as first argument and $ctx as second argument
9249
+ return originalMethod(entries, $ctx);
9250
+ }
9251
+ // For inline expressions, evaluate normally
9252
+ // Note: inline expressions receive entries and $ctx as parameters
9253
+ const values = identifiers.map(id => vNode.bindings?.get(id));
9254
+ const args = [...identifiers, 'entries', '$ctx'].join(", ");
9255
+ const funcBody = `return (${expression});`;
9256
+ const func = new Function(args, funcBody);
9257
+ return func.call(bindings?.raw, ...values, entries, $ctx);
9258
+ };
9259
+ }
9260
+ }
9261
+
8981
9262
  // Copyright (c) 2025 MintJams Inc. Licensed under MIT License.
8982
9263
  /**
8983
9264
  * Directive for two-way data binding on form input elements.
@@ -9303,7 +9584,7 @@ class VModelDirective {
9303
9584
  * @mounted="onMounted" - Called after the element is inserted into the DOM
9304
9585
  * @update="onUpdate" - Called before the element is updated
9305
9586
  * @updated="onUpdated" - Called after the element is updated
9306
- * @unmount="onUnmount" - Called before the element is removed from the DOM
9587
+ * @unmount="onUnmount" - Called before VNode cleanup begins
9307
9588
  * @unmounted="onUnmounted" - Called after VNode cleanup is complete (element reference still available)
9308
9589
  *
9309
9590
  * This directive is essential for handling user interactions and lifecycle events in your application.
@@ -10025,7 +10306,9 @@ class VStandardDirectiveParser {
10025
10306
  context.attribute.name === StandardDirectiveName.V_MODEL ||
10026
10307
  context.attribute.name.startsWith(StandardDirectiveName.V_MODEL + ".") ||
10027
10308
  // v-resize
10028
- context.attribute.name === StandardDirectiveName.V_RESIZE) {
10309
+ context.attribute.name === StandardDirectiveName.V_RESIZE ||
10310
+ // v-intersection
10311
+ context.attribute.name === StandardDirectiveName.V_INTERSECTION) {
10029
10312
  return true;
10030
10313
  }
10031
10314
  return false;
@@ -10068,6 +10351,10 @@ class VStandardDirectiveParser {
10068
10351
  if (context.attribute.name === StandardDirectiveName.V_RESIZE) {
10069
10352
  return new VResizeDirective(context);
10070
10353
  }
10354
+ // v-intersection
10355
+ if (context.attribute.name === StandardDirectiveName.V_INTERSECTION) {
10356
+ return new VIntersectionDirective(context);
10357
+ }
10071
10358
  throw new Error(`The attribute "${context.attribute.name}" cannot be parsed by ${this.name}.`);
10072
10359
  }
10073
10360
  }