@mintjamsinc/ichigojs 0.1.65 → 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.
package/dist/ichigo.cjs CHANGED
@@ -6865,11 +6865,16 @@
6865
6865
  const ast = parse(source, { ecmaVersion: "latest" });
6866
6866
  const dependencies = new Set();
6867
6867
  const declaredVariables = new Set();
6868
+ const thisAliases = new Set();
6868
6869
  // First, collect all declared variables (const, let, var, function params, etc.)
6869
6870
  ancestor(ast, {
6870
6871
  VariableDeclarator(node) {
6871
6872
  if (node.id.type === 'Identifier') {
6872
6873
  declaredVariables.add(node.id.name);
6874
+ // Detect aliases assigned from `this`, e.g. `let vm = this;`
6875
+ if (node.init && node.init.type === 'ThisExpression') {
6876
+ thisAliases.add(node.id.name);
6877
+ }
6873
6878
  }
6874
6879
  },
6875
6880
  FunctionDeclaration(node) {
@@ -6921,9 +6926,14 @@
6921
6926
  }
6922
6927
  },
6923
6928
  MemberExpression(node) {
6924
- // Handle 'this.propertyName' patterns
6925
- if (node.object.type === 'ThisExpression' && node.property.type === 'Identifier') {
6926
- dependencies.add(node.property.name);
6929
+ // Handle 'this.propertyName' patterns and aliases like 'vm.x' when 'vm = this'
6930
+ if (node.property.type === 'Identifier') {
6931
+ if (node.object.type === 'ThisExpression') {
6932
+ dependencies.add(node.property.name);
6933
+ }
6934
+ else if (node.object.type === 'Identifier' && thisAliases.has(node.object.name)) {
6935
+ dependencies.add(node.property.name);
6936
+ }
6927
6937
  }
6928
6938
  }
6929
6939
  });
@@ -7017,7 +7027,7 @@
7017
7027
  return expression;
7018
7028
  }
7019
7029
  try {
7020
- // Build a map of positions to replace: { start: number, end: number, name: string }[]
7030
+ // Build a map of positions to replace: { start: number, end: number, name: string, asThisAlias?: boolean }[]
7021
7031
  const replacements = [];
7022
7032
  // In script mode we must not wrap in parens (that would make multi-statement input invalid).
7023
7033
  // Offsets from the parser therefore refer directly to the original expression, so no shift.
@@ -7026,13 +7036,17 @@
7026
7036
  const offsetShift = asScript ? 0 : 1;
7027
7037
  const parsedAst = parse(source, { ecmaVersion: 'latest' });
7028
7038
  // Track identifiers that are locally declared within the handler body (let/const/var, function
7029
- // params) so we don't rewrite them to `this.xxx`. Only relevant in script mode, where the user
7030
- // can write declarations; in expression mode there are no declarations to track.
7039
+ // params) so we don't rewrite them to `this.xxx`. Additionally detect `this` aliases such as
7040
+ // `let vm = this;` so that `vm.x` can be rewritten to `this.x` even though `vm` is locally declared.
7031
7041
  const locallyDeclared = new Set();
7042
+ const thisAliases = new Set();
7032
7043
  if (asScript) {
7033
7044
  full(parsedAst, (node) => {
7034
7045
  if (node.type === 'VariableDeclarator' && node.id?.type === 'Identifier') {
7035
7046
  locallyDeclared.add(node.id.name);
7047
+ if (node.init && node.init.type === 'ThisExpression') {
7048
+ thisAliases.add(node.id.name);
7049
+ }
7036
7050
  }
7037
7051
  else if (node.type === 'FunctionDeclaration' && node.id?.type === 'Identifier') {
7038
7052
  locallyDeclared.add(node.id.name);
@@ -7049,20 +7063,34 @@
7049
7063
  if (!bindingIdentifiers.has(node.name)) {
7050
7064
  return;
7051
7065
  }
7052
- // Skip identifiers that were declared locally in the handler body
7066
+ // Locally declared identifiers must not be rewritten to `this.xxx`. The one
7067
+ // exception is a `this`-alias (e.g. `let vm = this;`): when such an alias is
7068
+ // used as the OBJECT of a MemberExpression (`vm.x`), we replace just the
7069
+ // object with `this` so that `vm.x` becomes `this.x`. In every other position
7070
+ // (the declaration LHS, a bare reference such as `console.log(vm)`, an
7071
+ // argument, etc.) the alias must be left untouched — rewriting it to
7072
+ // `this.vm` would either produce a syntax error (`let this.vm = this;`) or
7073
+ // change the meaning at runtime.
7074
+ // walk.fullAncestor includes the visited node itself as the last entry of
7075
+ // `ancestors`, so the actual parent node is at length - 2.
7076
+ const parentNode = ancestors.length >= 2 ? ancestors[ancestors.length - 2] : undefined;
7077
+ const isAliasAsMemberObject = parentNode?.type === 'MemberExpression' && parentNode.object === node;
7053
7078
  if (locallyDeclared.has(node.name)) {
7079
+ if (!thisAliases.has(node.name) || !isAliasAsMemberObject) {
7080
+ return;
7081
+ }
7082
+ replacements.push({
7083
+ start: node.start - offsetShift,
7084
+ end: node.end - offsetShift,
7085
+ name: node.name,
7086
+ asThisAlias: true
7087
+ });
7054
7088
  return;
7055
7089
  }
7056
- // Check if this identifier is a property of a MemberExpression
7057
- // (e.g., in 'obj.prop', we should skip 'prop')
7058
- if (ancestors.length >= 1) {
7059
- const parent = ancestors[ancestors.length - 1];
7060
- if (parent.type === 'MemberExpression') {
7061
- // Skip if this identifier is the property (not the object) of a non-computed member access
7062
- if (!parent.computed && parent.property === node) {
7063
- return;
7064
- }
7065
- }
7090
+ // Skip identifiers that are the property (not the object) of a non-computed
7091
+ // member access (e.g., the `prop` in `obj.prop`).
7092
+ if (parentNode?.type === 'MemberExpression' && !parentNode.computed && parentNode.property === node) {
7093
+ return;
7066
7094
  }
7067
7095
  // Add to replacements list (adjust for the wrapping parentheses in expression mode)
7068
7096
  replacements.push({
@@ -7076,9 +7104,8 @@
7076
7104
  // Apply replacements
7077
7105
  let result = expression;
7078
7106
  for (const replacement of replacements) {
7079
- result = result.substring(0, replacement.start) +
7080
- `this.${replacement.name}` +
7081
- result.substring(replacement.end);
7107
+ const insertion = replacement.asThisAlias ? 'this' : `this.${replacement.name}`;
7108
+ result = result.substring(0, replacement.start) + insertion + result.substring(replacement.end);
7082
7109
  }
7083
7110
  return result;
7084
7111
  }
@@ -11170,12 +11197,23 @@
11170
11197
  #collectDependentIdentifiers() {
11171
11198
  const ids = new Set(this.#evaluator?.dependentIdentifiers ?? []);
11172
11199
  const element = this.#vNode.node;
11173
- if (element instanceof HTMLInputElement && element.type === 'checkbox') {
11200
+ if (element instanceof HTMLInputElement) {
11174
11201
  const manager = this.#vNode.directiveManager;
11175
- for (const attrName of ['value', 'true-value', 'false-value']) {
11176
- const bindDirective = manager?.findBindDirective(attrName);
11177
- if (bindDirective) {
11178
- bindDirective.dependentIdentifiers.forEach(id => ids.add(id));
11202
+ // For checkboxes and radios, v-model's rendered state depends on
11203
+ // the element's typed `:value` binding (if present). Checkboxes
11204
+ // additionally depend on `:true-value` / `:false-value` bindings.
11205
+ if (element.type === 'checkbox' || element.type === 'radio') {
11206
+ const valueBind = manager?.findBindDirective('value');
11207
+ if (valueBind) {
11208
+ valueBind.dependentIdentifiers.forEach(id => ids.add(id));
11209
+ }
11210
+ }
11211
+ if (element.type === 'checkbox') {
11212
+ for (const attrName of ['true-value', 'false-value']) {
11213
+ const bindDirective = manager?.findBindDirective(attrName);
11214
+ if (bindDirective) {
11215
+ bindDirective.dependentIdentifiers.forEach(id => ids.add(id));
11216
+ }
11179
11217
  }
11180
11218
  }
11181
11219
  }
@@ -11262,11 +11300,13 @@
11262
11300
  this.#renderCheckbox(element, value);
11263
11301
  }
11264
11302
  else if (element.type === 'radio') {
11265
- // Prefer the original typed value stored by VBindDirective (:value binding)
11266
- // to avoid type coercion issues (e.g., boolean false vs string "false").
11267
- const radioValue = element._value !== undefined
11268
- ? element._value
11269
- : element.value;
11303
+ // Prefer the typed value from a sibling :value binding when present,
11304
+ // falling back to any stored `_value` or the raw string `value`.
11305
+ const manager = this.#vNode.directiveManager;
11306
+ const bindDirective = manager?.findBindDirective('value');
11307
+ const radioValue = bindDirective !== undefined
11308
+ ? bindDirective.evaluate()
11309
+ : (element._value !== undefined ? element._value : element.value);
11270
11310
  element.checked = radioValue === value;
11271
11311
  }
11272
11312
  else {
@@ -11295,11 +11335,13 @@
11295
11335
  newValue = this.#computeCheckboxNewValue(target);
11296
11336
  }
11297
11337
  else if (target.type === 'radio') {
11298
- // Prefer the original typed value stored by VBindDirective (:value binding)
11299
- // to preserve the type on write-back (e.g., boolean false, number 0).
11300
- newValue = target._value !== undefined
11301
- ? target._value
11302
- : target.value;
11338
+ // Prefer the typed value from a sibling :value binding when present,
11339
+ // falling back to any stored `_value` or the raw string `value`.
11340
+ const manager = this.#vNode.directiveManager;
11341
+ const bindDirective = manager?.findBindDirective('value');
11342
+ newValue = bindDirective !== undefined
11343
+ ? bindDirective.evaluate()
11344
+ : (target._value !== undefined ? target._value : target.value);
11303
11345
  }
11304
11346
  else {
11305
11347
  newValue = target.value;
@@ -11495,6 +11537,15 @@
11495
11537
  * Mouse button modifiers (MouseEvent): `.left`, `.middle`, `.right`.
11496
11538
  * System modifiers (KeyboardEvent and MouseEvent): `.shift`, `.ctrl`, `.alt`, `.meta`, plus `.exact` to require that no other system modifiers are held.
11497
11539
  *
11540
+ * Listen target and filter modifiers (two orthogonal axes):
11541
+ * - Listen target (where the listener is attached): `.window`, `.document`. When omitted the listener
11542
+ * is attached to the bound element. This is useful for global / cross-component events, e.g.
11543
+ * `@webtop-message.document="onMessage"`, and the listener is removed automatically on unmount.
11544
+ * - Filter (whether the handler runs): `.self` fires only when `event.target` is the bound element;
11545
+ * `.outside` fires only when `event.target` is outside the bound element (e.g. click-outside to
11546
+ * close a popup). `.outside` implies listening on `document` (capture phase) even without `.document`,
11547
+ * and `.self` / `.outside` are mutually exclusive.
11548
+ *
11498
11549
  * Additionally, this directive supports lifecycle hooks:
11499
11550
  * @mount="onMount" - Called before the VNode is mounted to the DOM element
11500
11551
  * @mounted="onMounted" - Called after the VNode is mounted to the DOM element
@@ -11533,6 +11584,26 @@
11533
11584
  * The event listener function for DOM events.
11534
11585
  */
11535
11586
  #listener;
11587
+ /**
11588
+ * The resolved target the listener is attached to (element, document, or window).
11589
+ * Stored so destroy() removes the listener from the same target it was added to.
11590
+ */
11591
+ #resolvedTarget;
11592
+ /**
11593
+ * The resolved capture flag. Shared by attach and destroy so they stay in sync,
11594
+ * since `.outside` forces capture phase regardless of the `.capture` modifier.
11595
+ */
11596
+ #useCapture = false;
11597
+ /**
11598
+ * Whether the listener has actually been attached. `.outside` defers attachment by a
11599
+ * microtask, so destroy() must not attempt removal before it is attached.
11600
+ */
11601
+ #attached = false;
11602
+ /**
11603
+ * Whether the directive has been destroyed. Guards the deferred `.outside` attachment
11604
+ * from attaching after the node was already unmounted.
11605
+ */
11606
+ #destroyed = false;
11536
11607
  /**
11537
11608
  * Map of lifecycle hook names to their handler functions.
11538
11609
  */
@@ -11556,6 +11627,12 @@
11556
11627
  this.#eventName = parts[0];
11557
11628
  parts.slice(1).forEach(mod => this.#modifiers.add(mod));
11558
11629
  }
11630
+ // `.self` and `.outside` are mutually exclusive filters; together they can never fire.
11631
+ if (this.#modifiers.has('self') && this.#modifiers.has('outside')) {
11632
+ context.vNode.vApplication.logManager
11633
+ .getLogger('VOnDirective')
11634
+ .warn(`The '.self' and '.outside' modifiers on '${attrName}' are mutually exclusive; the handler will never fire.`);
11635
+ }
11559
11636
  // Parse the expression to extract identifiers and create the handler wrapper.
11560
11637
  // Event handlers are parsed in script mode so that users can write multi-statement bodies
11561
11638
  // (e.g. "a=1; b=2"), declarations, and control-flow constructs — matching Vue semantics.
@@ -11665,11 +11742,10 @@
11665
11742
  * @inheritdoc
11666
11743
  */
11667
11744
  destroy() {
11668
- // Remove the event listener when the directive is destroyed
11669
- if (this.#eventName && this.#listener) {
11670
- const element = this.#vNode.node;
11671
- const useCapture = this.#modifiers.has('capture');
11672
- element.removeEventListener(this.#eventName, this.#listener, useCapture);
11745
+ this.#destroyed = true;
11746
+ // Remove the event listener from the same target/phase it was attached to.
11747
+ if (this.#eventName && this.#listener && this.#resolvedTarget && this.#attached) {
11748
+ this.#resolvedTarget.removeEventListener(this.#eventName, this.#listener, this.#useCapture);
11673
11749
  }
11674
11750
  }
11675
11751
  /**
@@ -11685,8 +11761,21 @@
11685
11761
  }
11686
11762
  const element = this.#vNode.node;
11687
11763
  const eventName = this.#eventName;
11688
- const useCapture = this.#modifiers.has('capture');
11689
11764
  const isOnce = this.#modifiers.has('once');
11765
+ const isOutside = this.#modifiers.has('outside');
11766
+ // Resolve the listen target (orthogonal to filters): `.window` / `.document` attach
11767
+ // the listener globally; `.outside` also requires a global listener to detect events
11768
+ // originating outside the element, so it implies `document`.
11769
+ this.#resolvedTarget = this.#modifiers.has('window')
11770
+ ? window
11771
+ : (this.#modifiers.has('document') || isOutside)
11772
+ ? document
11773
+ : element;
11774
+ // `.outside` listens in capture phase so it is not suppressed by a descendant's
11775
+ // stopPropagation(); otherwise the capture flag follows the `.capture` modifier.
11776
+ this.#useCapture = this.#modifiers.has('capture') || isOutside;
11777
+ const useCapture = this.#useCapture;
11778
+ const target = this.#resolvedTarget;
11690
11779
  // System modifier keys (held during the event) shared by KeyboardEvent and MouseEvent.
11691
11780
  const systemModifiers = ['shift', 'ctrl', 'alt', 'meta'];
11692
11781
  // Create the event listener function
@@ -11772,6 +11861,14 @@
11772
11861
  if (this.#modifiers.has('self') && event.target !== element) {
11773
11862
  return;
11774
11863
  }
11864
+ // `.outside`: only fire when the event originates outside the bound element.
11865
+ // A non-Node target (e.g. window) is treated as outside.
11866
+ if (isOutside) {
11867
+ const eventTarget = event.target;
11868
+ if (eventTarget instanceof Node && element.contains(eventTarget)) {
11869
+ return;
11870
+ }
11871
+ }
11775
11872
  // Call the pre-generated handler wrapper (if exists)
11776
11873
  if (this.#handlerWrapper) {
11777
11874
  this.#handlerWrapper(event);
@@ -11780,11 +11877,26 @@
11780
11877
  // No need to manually call scheduleUpdate() here
11781
11878
  // If 'once' modifier is used, remove the listener after first execution
11782
11879
  if (isOnce && this.#listener) {
11783
- element.removeEventListener(eventName, this.#listener, useCapture);
11880
+ target.removeEventListener(eventName, this.#listener, useCapture);
11881
+ this.#attached = false;
11784
11882
  }
11785
11883
  };
11786
- // Attach the event listener
11787
- element.addEventListener(eventName, this.#listener, useCapture);
11884
+ if (isOutside) {
11885
+ // Defer attachment by one microtask so the listener does not catch the same
11886
+ // interaction that mounted this element (e.g. the click that opened a popup,
11887
+ // which would otherwise immediately close it). Skip if already destroyed.
11888
+ queueMicrotask(() => {
11889
+ if (this.#destroyed || !this.#listener) {
11890
+ return;
11891
+ }
11892
+ target.addEventListener(eventName, this.#listener, useCapture);
11893
+ this.#attached = true;
11894
+ });
11895
+ }
11896
+ else {
11897
+ target.addEventListener(eventName, this.#listener, useCapture);
11898
+ this.#attached = true;
11899
+ }
11788
11900
  }
11789
11901
  /**
11790
11902
  * Checks if the event name is a lifecycle hook.
@@ -13568,6 +13680,7 @@
13568
13680
  // Inject utility methods into bindings
13569
13681
  this.#bindings.set('$nextTick', (callback) => this.#nextTick(callback));
13570
13682
  this.#bindings.set('$markRaw', (obj) => ReactiveProxy.markRaw(obj));
13683
+ this.#bindings.set('$emit', (name, detail, options) => this.#emit(name, detail, options));
13571
13684
  // Add methods
13572
13685
  if (this.#options.methods) {
13573
13686
  for (const [key, method] of Object.entries(this.#options.methods)) {
@@ -13813,6 +13926,38 @@
13813
13926
  compute(key);
13814
13927
  }
13815
13928
  }
13929
+ /**
13930
+ * Dispatches a CustomEvent, providing the framework-level `$emit` available in expressions
13931
+ * and methods. By default the event is dispatched on the application root element with
13932
+ * `bubbles: true`, so a parent component can listen for it via `v-on` / `@` on the component
13933
+ * tag (the root is rendered inside the host custom element, so the event bubbles out of it).
13934
+ *
13935
+ * The dispatch target can be overridden via `options.target` (e.g. `document` / `window`) to
13936
+ * use a global event bus, interoperating with native `addEventListener` listeners.
13937
+ *
13938
+ * @param name The event name (e.g. "selected"). Listened to as `@selected` on the parent side.
13939
+ * @param detail The payload exposed as `event.detail`.
13940
+ * @param options Dispatch options (bubbles, cancelable, composed, target).
13941
+ * @returns The result of dispatchEvent: false if a listener called preventDefault(), otherwise true.
13942
+ */
13943
+ #emit(name, detail, options) {
13944
+ // Documentation/validation only: warn when emitting an event not declared in `emits`.
13945
+ if (this.#options.emits && !this.#options.emits.includes(name)) {
13946
+ this.#logger.warn(`Event '${name}' is emitted but not declared in the 'emits' option.`);
13947
+ }
13948
+ const target = options?.target ?? this.#vNode?.node;
13949
+ if (!target) {
13950
+ this.#logger.warn(`$emit('${name}') was called before the application was mounted; the event was not dispatched.`);
13951
+ return false;
13952
+ }
13953
+ const event = new CustomEvent(name, {
13954
+ detail,
13955
+ bubbles: options?.bubbles ?? true,
13956
+ cancelable: options?.cancelable ?? true,
13957
+ composed: options?.composed ?? false,
13958
+ });
13959
+ return target.dispatchEvent(event);
13960
+ }
13816
13961
  /**
13817
13962
  * Executes a callback after the next DOM update.
13818
13963
  * @param callback The callback to execute.
@@ -14105,7 +14250,7 @@
14105
14250
  * @param options Component options including template selector and optional props.
14106
14251
  */
14107
14252
  function defineComponent(tagName, options) {
14108
- const { props = [], template, data, computed, methods, watch, logLevel } = options;
14253
+ const { props = [], template, data, computed, methods, watch, emits, logLevel } = options;
14109
14254
  // Build a subclass of IchigoElement specific to this component
14110
14255
  class ComponentElement extends IchigoElement {
14111
14256
  static _template = template;
@@ -14127,6 +14272,7 @@
14127
14272
  computed,
14128
14273
  methods,
14129
14274
  watch,
14275
+ emits,
14130
14276
  logLevel,
14131
14277
  };
14132
14278
  }