@mintjamsinc/ichigojs 0.1.66 → 0.1.67

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.
@@ -11531,6 +11531,15 @@ class VModelDirective {
11531
11531
  * Mouse button modifiers (MouseEvent): `.left`, `.middle`, `.right`.
11532
11532
  * System modifiers (KeyboardEvent and MouseEvent): `.shift`, `.ctrl`, `.alt`, `.meta`, plus `.exact` to require that no other system modifiers are held.
11533
11533
  *
11534
+ * Listen target and filter modifiers (two orthogonal axes):
11535
+ * - Listen target (where the listener is attached): `.window`, `.document`. When omitted the listener
11536
+ * is attached to the bound element. This is useful for global / cross-component events, e.g.
11537
+ * `@webtop-message.document="onMessage"`, and the listener is removed automatically on unmount.
11538
+ * - Filter (whether the handler runs): `.self` fires only when `event.target` is the bound element;
11539
+ * `.outside` fires only when `event.target` is outside the bound element (e.g. click-outside to
11540
+ * close a popup). `.outside` implies listening on `document` (capture phase) even without `.document`,
11541
+ * and `.self` / `.outside` are mutually exclusive.
11542
+ *
11534
11543
  * Additionally, this directive supports lifecycle hooks:
11535
11544
  * @mount="onMount" - Called before the VNode is mounted to the DOM element
11536
11545
  * @mounted="onMounted" - Called after the VNode is mounted to the DOM element
@@ -11569,6 +11578,26 @@ class VOnDirective {
11569
11578
  * The event listener function for DOM events.
11570
11579
  */
11571
11580
  #listener;
11581
+ /**
11582
+ * The resolved target the listener is attached to (element, document, or window).
11583
+ * Stored so destroy() removes the listener from the same target it was added to.
11584
+ */
11585
+ #resolvedTarget;
11586
+ /**
11587
+ * The resolved capture flag. Shared by attach and destroy so they stay in sync,
11588
+ * since `.outside` forces capture phase regardless of the `.capture` modifier.
11589
+ */
11590
+ #useCapture = false;
11591
+ /**
11592
+ * Whether the listener has actually been attached. `.outside` defers attachment by a
11593
+ * microtask, so destroy() must not attempt removal before it is attached.
11594
+ */
11595
+ #attached = false;
11596
+ /**
11597
+ * Whether the directive has been destroyed. Guards the deferred `.outside` attachment
11598
+ * from attaching after the node was already unmounted.
11599
+ */
11600
+ #destroyed = false;
11572
11601
  /**
11573
11602
  * Map of lifecycle hook names to their handler functions.
11574
11603
  */
@@ -11592,6 +11621,12 @@ class VOnDirective {
11592
11621
  this.#eventName = parts[0];
11593
11622
  parts.slice(1).forEach(mod => this.#modifiers.add(mod));
11594
11623
  }
11624
+ // `.self` and `.outside` are mutually exclusive filters; together they can never fire.
11625
+ if (this.#modifiers.has('self') && this.#modifiers.has('outside')) {
11626
+ context.vNode.vApplication.logManager
11627
+ .getLogger('VOnDirective')
11628
+ .warn(`The '.self' and '.outside' modifiers on '${attrName}' are mutually exclusive; the handler will never fire.`);
11629
+ }
11595
11630
  // Parse the expression to extract identifiers and create the handler wrapper.
11596
11631
  // Event handlers are parsed in script mode so that users can write multi-statement bodies
11597
11632
  // (e.g. "a=1; b=2"), declarations, and control-flow constructs — matching Vue semantics.
@@ -11701,11 +11736,10 @@ class VOnDirective {
11701
11736
  * @inheritdoc
11702
11737
  */
11703
11738
  destroy() {
11704
- // Remove the event listener when the directive is destroyed
11705
- if (this.#eventName && this.#listener) {
11706
- const element = this.#vNode.node;
11707
- const useCapture = this.#modifiers.has('capture');
11708
- element.removeEventListener(this.#eventName, this.#listener, useCapture);
11739
+ this.#destroyed = true;
11740
+ // Remove the event listener from the same target/phase it was attached to.
11741
+ if (this.#eventName && this.#listener && this.#resolvedTarget && this.#attached) {
11742
+ this.#resolvedTarget.removeEventListener(this.#eventName, this.#listener, this.#useCapture);
11709
11743
  }
11710
11744
  }
11711
11745
  /**
@@ -11721,8 +11755,21 @@ class VOnDirective {
11721
11755
  }
11722
11756
  const element = this.#vNode.node;
11723
11757
  const eventName = this.#eventName;
11724
- const useCapture = this.#modifiers.has('capture');
11725
11758
  const isOnce = this.#modifiers.has('once');
11759
+ const isOutside = this.#modifiers.has('outside');
11760
+ // Resolve the listen target (orthogonal to filters): `.window` / `.document` attach
11761
+ // the listener globally; `.outside` also requires a global listener to detect events
11762
+ // originating outside the element, so it implies `document`.
11763
+ this.#resolvedTarget = this.#modifiers.has('window')
11764
+ ? window
11765
+ : (this.#modifiers.has('document') || isOutside)
11766
+ ? document
11767
+ : element;
11768
+ // `.outside` listens in capture phase so it is not suppressed by a descendant's
11769
+ // stopPropagation(); otherwise the capture flag follows the `.capture` modifier.
11770
+ this.#useCapture = this.#modifiers.has('capture') || isOutside;
11771
+ const useCapture = this.#useCapture;
11772
+ const target = this.#resolvedTarget;
11726
11773
  // System modifier keys (held during the event) shared by KeyboardEvent and MouseEvent.
11727
11774
  const systemModifiers = ['shift', 'ctrl', 'alt', 'meta'];
11728
11775
  // Create the event listener function
@@ -11808,6 +11855,14 @@ class VOnDirective {
11808
11855
  if (this.#modifiers.has('self') && event.target !== element) {
11809
11856
  return;
11810
11857
  }
11858
+ // `.outside`: only fire when the event originates outside the bound element.
11859
+ // A non-Node target (e.g. window) is treated as outside.
11860
+ if (isOutside) {
11861
+ const eventTarget = event.target;
11862
+ if (eventTarget instanceof Node && element.contains(eventTarget)) {
11863
+ return;
11864
+ }
11865
+ }
11811
11866
  // Call the pre-generated handler wrapper (if exists)
11812
11867
  if (this.#handlerWrapper) {
11813
11868
  this.#handlerWrapper(event);
@@ -11816,11 +11871,26 @@ class VOnDirective {
11816
11871
  // No need to manually call scheduleUpdate() here
11817
11872
  // If 'once' modifier is used, remove the listener after first execution
11818
11873
  if (isOnce && this.#listener) {
11819
- element.removeEventListener(eventName, this.#listener, useCapture);
11874
+ target.removeEventListener(eventName, this.#listener, useCapture);
11875
+ this.#attached = false;
11820
11876
  }
11821
11877
  };
11822
- // Attach the event listener
11823
- element.addEventListener(eventName, this.#listener, useCapture);
11878
+ if (isOutside) {
11879
+ // Defer attachment by one microtask so the listener does not catch the same
11880
+ // interaction that mounted this element (e.g. the click that opened a popup,
11881
+ // which would otherwise immediately close it). Skip if already destroyed.
11882
+ queueMicrotask(() => {
11883
+ if (this.#destroyed || !this.#listener) {
11884
+ return;
11885
+ }
11886
+ target.addEventListener(eventName, this.#listener, useCapture);
11887
+ this.#attached = true;
11888
+ });
11889
+ }
11890
+ else {
11891
+ target.addEventListener(eventName, this.#listener, useCapture);
11892
+ this.#attached = true;
11893
+ }
11824
11894
  }
11825
11895
  /**
11826
11896
  * Checks if the event name is a lifecycle hook.
@@ -13604,6 +13674,7 @@ class VApplication {
13604
13674
  // Inject utility methods into bindings
13605
13675
  this.#bindings.set('$nextTick', (callback) => this.#nextTick(callback));
13606
13676
  this.#bindings.set('$markRaw', (obj) => ReactiveProxy.markRaw(obj));
13677
+ this.#bindings.set('$emit', (name, detail, options) => this.#emit(name, detail, options));
13607
13678
  // Add methods
13608
13679
  if (this.#options.methods) {
13609
13680
  for (const [key, method] of Object.entries(this.#options.methods)) {
@@ -13849,6 +13920,38 @@ class VApplication {
13849
13920
  compute(key);
13850
13921
  }
13851
13922
  }
13923
+ /**
13924
+ * Dispatches a CustomEvent, providing the framework-level `$emit` available in expressions
13925
+ * and methods. By default the event is dispatched on the application root element with
13926
+ * `bubbles: true`, so a parent component can listen for it via `v-on` / `@` on the component
13927
+ * tag (the root is rendered inside the host custom element, so the event bubbles out of it).
13928
+ *
13929
+ * The dispatch target can be overridden via `options.target` (e.g. `document` / `window`) to
13930
+ * use a global event bus, interoperating with native `addEventListener` listeners.
13931
+ *
13932
+ * @param name The event name (e.g. "selected"). Listened to as `@selected` on the parent side.
13933
+ * @param detail The payload exposed as `event.detail`.
13934
+ * @param options Dispatch options (bubbles, cancelable, composed, target).
13935
+ * @returns The result of dispatchEvent: false if a listener called preventDefault(), otherwise true.
13936
+ */
13937
+ #emit(name, detail, options) {
13938
+ // Documentation/validation only: warn when emitting an event not declared in `emits`.
13939
+ if (this.#options.emits && !this.#options.emits.includes(name)) {
13940
+ this.#logger.warn(`Event '${name}' is emitted but not declared in the 'emits' option.`);
13941
+ }
13942
+ const target = options?.target ?? this.#vNode?.node;
13943
+ if (!target) {
13944
+ this.#logger.warn(`$emit('${name}') was called before the application was mounted; the event was not dispatched.`);
13945
+ return false;
13946
+ }
13947
+ const event = new CustomEvent(name, {
13948
+ detail,
13949
+ bubbles: options?.bubbles ?? true,
13950
+ cancelable: options?.cancelable ?? true,
13951
+ composed: options?.composed ?? false,
13952
+ });
13953
+ return target.dispatchEvent(event);
13954
+ }
13852
13955
  /**
13853
13956
  * Executes a callback after the next DOM update.
13854
13957
  * @param callback The callback to execute.
@@ -14141,7 +14244,7 @@ class IchigoElement extends HTMLElement {
14141
14244
  * @param options Component options including template selector and optional props.
14142
14245
  */
14143
14246
  function defineComponent(tagName, options) {
14144
- const { props = [], template, data, computed, methods, watch, logLevel } = options;
14247
+ const { props = [], template, data, computed, methods, watch, emits, logLevel } = options;
14145
14248
  // Build a subclass of IchigoElement specific to this component
14146
14249
  class ComponentElement extends IchigoElement {
14147
14250
  static _template = template;
@@ -14163,6 +14266,7 @@ function defineComponent(tagName, options) {
14163
14266
  computed,
14164
14267
  methods,
14165
14268
  watch,
14269
+ emits,
14166
14270
  logLevel,
14167
14271
  };
14168
14272
  }