From 339007fbf674a45bdb904884cb624a4b6e5acd57 Mon Sep 17 00:00:00 2001
From: Eric Kwoka <43540491+ekwoka@users.noreply.github.com>
Date: Sun, 30 Jun 2024 09:26:50 +0400
Subject: [PATCH 1/3] :twisted_rightwards_arrows: Syncs on code to Alpine
---
packages/alpinets/src/utils/on.ts | 88 +++++++++++++++++++------------
size.json | 8 +--
2 files changed, 57 insertions(+), 39 deletions(-)
diff --git a/packages/alpinets/src/utils/on.ts b/packages/alpinets/src/utils/on.ts
index 91dba8e..7647fa3 100644
--- a/packages/alpinets/src/utils/on.ts
+++ b/packages/alpinets/src/utils/on.ts
@@ -1,4 +1,4 @@
-import { ElementWithXAttributes } from '../types';
+import type { ElementWithXAttributes } from '../types';
import { debounce } from './debounce';
import { camelCase, dotSyntax, kebabCase } from './stringTransformers';
import { throttle } from './throttle';
@@ -34,20 +34,46 @@ export const on = (
if (modifiers.includes('capture')) options.capture = true;
if (modifiers.includes('window')) listenerTarget = window;
if (modifiers.includes('document')) listenerTarget = document;
+
+ if (modifiers.includes('debounce')) {
+ const nextModifier =
+ modifiers[modifiers.indexOf('debounce') + 1] || 'invalid-wait';
+ const wait = isNumeric(nextModifier.split('ms')[0])
+ ? Number(nextModifier.split('ms')[0])
+ : 250;
+
+ handler = debounce(handler, wait);
+ }
+
+ if (modifiers.includes('throttle')) {
+ const nextModifier =
+ modifiers[modifiers.indexOf('throttle') + 1] || 'invalid-limit';
+ const limit = isNumeric(nextModifier.split('ms')[0])
+ ? Number(nextModifier.split('ms')[0])
+ : 250;
+
+ handler = throttle(handler, limit);
+ }
+
if (modifiers.includes('prevent'))
handler = wrapHandler(handler, (next, e) => {
e.preventDefault();
next(e);
});
+
if (modifiers.includes('stop'))
handler = wrapHandler(handler, (next, e) => {
e.stopPropagation();
next(e);
});
- if (modifiers.includes('self'))
+
+ if (modifiers.includes('once')) {
handler = wrapHandler(handler, (next, e) => {
- e.target === el && next(e);
+ next(e);
+
+ listenerTarget.removeEventListener(event, handler, options);
});
+ }
if (modifiers.includes('away') || modifiers.includes('outside')) {
listenerTarget = document;
@@ -67,43 +93,20 @@ export const on = (
});
}
- if (modifiers.includes('once')) {
+ if (modifiers.includes('self'))
handler = wrapHandler(handler, (next, e) => {
- next(e);
-
- listenerTarget.removeEventListener(event, handler, options);
+ e.target === el && next(e);
});
- }
// Handle :keydown and :keyup listeners.
- handler = wrapHandler(handler, (next, e) => {
- if (isKeyEvent(event)) {
+ if (isKeyEvent(event) || isClickEvent(event)) {
+ handler = wrapHandler(handler, (next, e) => {
if (isListeningForASpecificKeyThatHasntBeenPressed(e, modifiers)) {
return;
}
- }
-
- next(e);
- });
-
- if (modifiers.includes('debounce')) {
- const nextModifier =
- modifiers[modifiers.indexOf('debounce') + 1] || 'invalid-wait';
- const wait = isNumeric(nextModifier.split('ms')[0])
- ? Number(nextModifier.split('ms')[0])
- : 250;
- handler = debounce(handler, wait);
- }
-
- if (modifiers.includes('throttle')) {
- const nextModifier =
- modifiers[modifiers.indexOf('throttle') + 1] || 'invalid-limit';
- const limit = isNumeric(nextModifier.split('ms')[0])
- ? Number(nextModifier.split('ms')[0])
- : 250;
-
- handler = throttle(handler, limit);
+ next(e);
+ });
}
listenerTarget.addEventListener(event, handler, options);
@@ -121,15 +124,28 @@ export const isNumeric = (subject: unknown): subject is number =>
const isKeyEvent = (event: string): event is 'keydown' | 'keyup' =>
['keydown', 'keyup'].includes(event);
+const isClickEvent = (event: string) => {
+ return ['contextmenu', 'click', 'mouse'].some((i) => event.includes(i));
+};
+
const isListeningForASpecificKeyThatHasntBeenPressed = (
e: Event,
modifiers: string[],
) => {
let keyModifiers = modifiers.filter(
(mod) =>
- !['window', 'document', 'prevent', 'stop', 'once', 'capture'].includes(
- mod,
- ),
+ ![
+ 'window',
+ 'document',
+ 'prevent',
+ 'stop',
+ 'once',
+ 'capture',
+ 'self',
+ 'away',
+ 'outside',
+ 'passive',
+ ].includes(mod),
);
if (keyModifiers.includes('debounce')) {
@@ -189,6 +205,8 @@ const isListeningForASpecificKeyThatHasntBeenPressed = (
if (
activelyPressedKeyModifiers.length === selectedSystemKeyModifiers.length
) {
+ // AND the event is a click. It's a pass.
+ if (isClickEvent(e.type)) return false;
// AND the remaining key is pressed as well. It's a press.
if (keyToModifiers((e as KeyboardEvent).key).includes(keyModifiers[0]))
return false;
diff --git a/size.json b/size.json
index 41c0557..1638539 100644
--- a/size.json
+++ b/size.json
@@ -1,12 +1,12 @@
{
"alpinets": {
"minified": {
- "pretty": "39.8 kB",
- "raw": 39829
+ "pretty": "39.9 kB",
+ "raw": 39949
},
"brotli": {
- "pretty": "13.5 kB",
- "raw": 13534
+ "pretty": "13.6 kB",
+ "raw": 13573
}
},
"anchor": {
From cf2874e1576a54563fdb95b7c5c1c0dc63a90c55 Mon Sep 17 00:00:00 2001
From: Eric Kwoka <43540491+ekwoka@users.noreply.github.com>
Date: Sun, 30 Jun 2024 14:05:16 +0400
Subject: [PATCH 2/3] :white_check_mark: Updates mouse key tests
---
.../alpinets/tests/directives/x-on.test.ts | 34 ++++++++++++++++++-
test-utils/render.ts | 13 ++++---
2 files changed, 41 insertions(+), 6 deletions(-)
diff --git a/packages/alpinets/tests/directives/x-on.test.ts b/packages/alpinets/tests/directives/x-on.test.ts
index 4a18095..63f82a7 100644
--- a/packages/alpinets/tests/directives/x-on.test.ts
+++ b/packages/alpinets/tests/directives/x-on.test.ts
@@ -257,7 +257,9 @@ describe('x-on modifiers', () => {
expect($('[x-text]').textContent).toBe('0');
await click('button');
expect($('[x-text]').textContent).toBe('0');
- await sleep(500);
+ await sleep(200);
+ expect($('[x-text]').textContent).toBe('0');
+ await sleep(100);
expect($('[x-text]').textContent).toBe('1');
});
it('can be throttled', async () => {
@@ -476,6 +478,36 @@ describe('@click modifiers', () => {
await click('div');
expect($('span').style.display).toBe('none');
});
+ it('allows system key modifiers', async () => {
+ const { $, click } = await render(
+ undefined,
+ `
+
+ >
+
+
+
+ `,
+ );
+ expect($('[x-text]').textContent).toBe('0');
+ await click('button', { ctrlKey: true, bubbles: true });
+ expect($('[x-text]').textContent).toBe('1');
+ await click('button', { shiftKey: true, bubbles: true });
+ expect($('[x-text]').textContent).toBe('10');
+ await click('button', { metaKey: true, bubbles: true });
+ expect($('[x-text]').textContent).toBe('110100');
+ await click('button', { altKey: true, bubbles: true });
+ expect($('[x-text]').textContent).toBe('1000');
+ await click('button', {
+ ctrlKey: true,
+ shiftKey: true,
+ metaKey: true,
+ altKey: true,
+ bubbles: true,
+ });
+ expect($('[x-text]').textContent).toBe('111111');
+ });
});
describe('@window / @document modifiers', () => {
it('listens on the window', async () => {
diff --git a/test-utils/render.ts b/test-utils/render.ts
index 35d786a..4d1b509 100644
--- a/test-utils/render.ts
+++ b/test-utils/render.ts
@@ -9,6 +9,7 @@ import {
IKeyboardEventInit,
InputEvent,
KeyboardEvent,
+ MouseEvent,
Window,
} from 'happy-dom';
@@ -46,10 +47,12 @@ export const render = async (
$: window.document.querySelector.bind(window.document),
$$: window.document.querySelectorAll.bind(window.document),
happyDOM: window.happyDOM,
- click: async (selector: string) => {
- (
- window.document.querySelector(selector) as unknown as HTMLElement
- ).click();
+ click: async (selector: string, options?: MouseEventInit) => {
+ const target = window.document.querySelector(
+ selector,
+ ) as unknown as HTMLElement;
+ if (!options) target.click();
+ else target.dispatchEvent(new MouseEvent('click', options));
await window.happyDOM.whenAsyncComplete();
},
type: async (
@@ -126,7 +129,7 @@ type RenderReturn = {
$: typeof window.document.querySelector;
$$: typeof window.document.querySelectorAll;
happyDOM: Window['happyDOM'];
- click: (selector: string) => Promise;
+ click: (selector: string, options?: MouseEventInit) => Promise;
type: (
selector: string,
value: string,
From 74e5be019b7f885fc837f4ecbf1dd6972f490952 Mon Sep 17 00:00:00 2001
From: Eric Kwoka <43540491+ekwoka@users.noreply.github.com>
Date: Sun, 30 Jun 2024 20:03:38 +0400
Subject: [PATCH 3/3] :construction: Attempts Rewriting on logic
---
.eslintrc.json | 2 +-
packages/alpinets/src/utils/on-old.ts | 247 ++++++++++++++++++
packages/alpinets/src/utils/on.ts | 224 ++++++++++------
.../alpinets/tests/directives/x-on.bench.ts | 87 ++++++
size.json | 8 +-
5 files changed, 479 insertions(+), 89 deletions(-)
create mode 100644 packages/alpinets/src/utils/on-old.ts
create mode 100644 packages/alpinets/tests/directives/x-on.bench.ts
diff --git a/.eslintrc.json b/.eslintrc.json
index ace91c2..a048fed 100644
--- a/.eslintrc.json
+++ b/.eslintrc.json
@@ -25,7 +25,7 @@
},
"overrides": [
{
- "files": ["*.test.*", "*.spec.*"],
+ "files": ["*.test.*", "*.spec.*", "*.bench.*"],
"rules": {
"@typescript-eslint/no-explicit-any": "off"
}
diff --git a/packages/alpinets/src/utils/on-old.ts b/packages/alpinets/src/utils/on-old.ts
new file mode 100644
index 0000000..7647fa3
--- /dev/null
+++ b/packages/alpinets/src/utils/on-old.ts
@@ -0,0 +1,247 @@
+import type { ElementWithXAttributes } from '../types';
+import { debounce } from './debounce';
+import { camelCase, dotSyntax, kebabCase } from './stringTransformers';
+import { throttle } from './throttle';
+
+export const on = (
+ el: ElementWithXAttributes,
+ event: string,
+ modifiers: string[],
+ callback: EventHandler,
+) => {
+ let listenerTarget: ElementWithXAttributes | Window | Document = el;
+
+ let handler: EventHandler = (e: Event) => callback(e);
+
+ const options = {
+ passive: false,
+ capture: false,
+ };
+
+ // This little helper allows us to add functionality to the listener's
+ // handler more flexibly in a "middleware" style.
+ const wrapHandler =
+ (
+ callback: EventHandler,
+ wrapper: (next: EventHandler, event: Event) => void,
+ ): EventHandler =>
+ (e) =>
+ wrapper(callback, e);
+
+ if (modifiers.includes('dot')) event = dotSyntax(event);
+ if (modifiers.includes('camel')) event = camelCase(event);
+ if (modifiers.includes('passive')) options.passive = true;
+ if (modifiers.includes('capture')) options.capture = true;
+ if (modifiers.includes('window')) listenerTarget = window;
+ if (modifiers.includes('document')) listenerTarget = document;
+
+ if (modifiers.includes('debounce')) {
+ const nextModifier =
+ modifiers[modifiers.indexOf('debounce') + 1] || 'invalid-wait';
+ const wait = isNumeric(nextModifier.split('ms')[0])
+ ? Number(nextModifier.split('ms')[0])
+ : 250;
+
+ handler = debounce(handler, wait);
+ }
+
+ if (modifiers.includes('throttle')) {
+ const nextModifier =
+ modifiers[modifiers.indexOf('throttle') + 1] || 'invalid-limit';
+ const limit = isNumeric(nextModifier.split('ms')[0])
+ ? Number(nextModifier.split('ms')[0])
+ : 250;
+
+ handler = throttle(handler, limit);
+ }
+
+ if (modifiers.includes('prevent'))
+ handler = wrapHandler(handler, (next, e) => {
+ e.preventDefault();
+ next(e);
+ });
+
+ if (modifiers.includes('stop'))
+ handler = wrapHandler(handler, (next, e) => {
+ e.stopPropagation();
+ next(e);
+ });
+
+ if (modifiers.includes('once')) {
+ handler = wrapHandler(handler, (next, e) => {
+ next(e);
+
+ listenerTarget.removeEventListener(event, handler, options);
+ });
+ }
+
+ if (modifiers.includes('away') || modifiers.includes('outside')) {
+ listenerTarget = document;
+
+ handler = wrapHandler(handler, (next, e) => {
+ if (el.contains(e.target as Node)) return;
+
+ if ((e.target as Node).isConnected === false) return;
+
+ if (el.offsetWidth < 1 && el.offsetHeight < 1) return;
+
+ // Additional check for special implementations like x-collapse
+ // where the element doesn't have display: none
+ if (el._x_isShown === false) return;
+
+ next(e);
+ });
+ }
+
+ if (modifiers.includes('self'))
+ handler = wrapHandler(handler, (next, e) => {
+ e.target === el && next(e);
+ });
+
+ // Handle :keydown and :keyup listeners.
+ if (isKeyEvent(event) || isClickEvent(event)) {
+ handler = wrapHandler(handler, (next, e) => {
+ if (isListeningForASpecificKeyThatHasntBeenPressed(e, modifiers)) {
+ return;
+ }
+
+ next(e);
+ });
+ }
+
+ listenerTarget.addEventListener(event, handler, options);
+
+ return () => {
+ listenerTarget.removeEventListener(event, handler, options);
+ };
+};
+
+type EventHandler = (event: Event) => void;
+
+export const isNumeric = (subject: unknown): subject is number =>
+ !Array.isArray(subject) && !isNaN(Number(subject));
+
+const isKeyEvent = (event: string): event is 'keydown' | 'keyup' =>
+ ['keydown', 'keyup'].includes(event);
+
+const isClickEvent = (event: string) => {
+ return ['contextmenu', 'click', 'mouse'].some((i) => event.includes(i));
+};
+
+const isListeningForASpecificKeyThatHasntBeenPressed = (
+ e: Event,
+ modifiers: string[],
+) => {
+ let keyModifiers = modifiers.filter(
+ (mod) =>
+ ![
+ 'window',
+ 'document',
+ 'prevent',
+ 'stop',
+ 'once',
+ 'capture',
+ 'self',
+ 'away',
+ 'outside',
+ 'passive',
+ ].includes(mod),
+ );
+
+ if (keyModifiers.includes('debounce')) {
+ const debounceIndex = keyModifiers.indexOf('debounce');
+ keyModifiers.splice(
+ debounceIndex,
+ isNumeric(
+ (keyModifiers[debounceIndex + 1] || 'invalid-wait').split('ms')[0],
+ )
+ ? 2
+ : 1,
+ );
+ }
+ if (keyModifiers.includes('throttle')) {
+ const throttleIndex = keyModifiers.indexOf('throttle');
+ keyModifiers.splice(
+ throttleIndex,
+ isNumeric(
+ (keyModifiers[throttleIndex + 1] || 'invalid-wait').split('ms')[0],
+ )
+ ? 2
+ : 1,
+ );
+ }
+
+ // If no modifier is specified, we'll call it a press.
+ if (keyModifiers.length === 0) return false;
+
+ // If one is passed, AND it matches the key pressed, we'll call it a press.
+ if (
+ keyModifiers.length === 1 &&
+ keyToModifiers((e as KeyboardEvent).key).includes(keyModifiers[0])
+ )
+ return false;
+
+ // The user is listening for key combinations.
+ const systemKeyModifiers = ['ctrl', 'shift', 'alt', 'meta', 'cmd', 'super'];
+ const selectedSystemKeyModifiers = systemKeyModifiers.filter((modifier) =>
+ keyModifiers.includes(modifier),
+ );
+
+ keyModifiers = keyModifiers.filter(
+ (i) => !selectedSystemKeyModifiers.includes(i),
+ );
+
+ if (selectedSystemKeyModifiers.length > 0) {
+ const activelyPressedKeyModifiers = selectedSystemKeyModifiers.filter(
+ (modifier) => {
+ // Alias "cmd" and "super" to "meta"
+ if (modifier === 'cmd' || modifier === 'super') modifier = 'meta';
+
+ return e[`${modifier}Key`];
+ },
+ );
+
+ // If all the modifiers selected are pressed, ...
+ if (
+ activelyPressedKeyModifiers.length === selectedSystemKeyModifiers.length
+ ) {
+ // AND the event is a click. It's a pass.
+ if (isClickEvent(e.type)) return false;
+ // AND the remaining key is pressed as well. It's a press.
+ if (keyToModifiers((e as KeyboardEvent).key).includes(keyModifiers[0]))
+ return false;
+ }
+ }
+
+ // We'll call it NOT a valid keypress.
+ return true;
+};
+
+const keyToModifiers = (key: string): string[] => {
+ if (!key) return [];
+
+ key = kebabCase(key);
+
+ const modifierToKeyMap = {
+ ctrl: 'control',
+ slash: '/',
+ space: ' ',
+ spacebar: ' ',
+ cmd: 'meta',
+ esc: 'escape',
+ up: 'arrow-up',
+ down: 'arrow-down',
+ left: 'arrow-left',
+ right: 'arrow-right',
+ period: '.',
+ equal: '=',
+ minus: '-',
+ underscore: '_',
+ };
+
+ modifierToKeyMap[key] = key;
+
+ return Object.entries(modifierToKeyMap)
+ .map(([modifier, keytype]) => (keytype === key ? modifier : false))
+ .filter((mod: string | false): mod is string => Boolean(mod));
+};
diff --git a/packages/alpinets/src/utils/on.ts b/packages/alpinets/src/utils/on.ts
index 7647fa3..200d158 100644
--- a/packages/alpinets/src/utils/on.ts
+++ b/packages/alpinets/src/utils/on.ts
@@ -3,117 +3,156 @@ import { debounce } from './debounce';
import { camelCase, dotSyntax, kebabCase } from './stringTransformers';
import { throttle } from './throttle';
+const callWith =
+ unknown>(ev: Event) =>
+ (fn: T) =>
+ fn(ev);
export const on = (
el: ElementWithXAttributes,
event: string,
modifiers: string[],
callback: EventHandler,
) => {
- let listenerTarget: ElementWithXAttributes | Window | Document = el;
+ const listener: ListenerInfo = {
+ event,
+ target: el,
+ filters: [],
+ handler(e) {
+ const caller = callWith(e);
+ if (listener.filters.every(caller)) {
+ listener.cleanups.forEach(caller);
+ callback(e);
+ }
+ },
+ modifiers,
+ cleanups: [],
+ options: {
+ passive: false,
+ capture: false,
+ },
+ };
+
+ for (const mod of modifiers) modifierOptions[mod]?.(listener);
- let handler: EventHandler = (e: Event) => callback(e);
+ // Handle :keydown and :keyup listeners.
+ if (isKeyEvent(event) || isClickEvent(event)) {
+ keyedEvent(listener);
+ }
- const options = {
- passive: false,
- capture: false,
+ listener.target.addEventListener(
+ listener.event,
+ listener.handler,
+ listener.options,
+ );
+
+ return () => {
+ listener.target.removeEventListener(
+ listener.event,
+ listener.handler,
+ listener.options,
+ );
};
+};
- // This little helper allows us to add functionality to the listener's
- // handler more flexibly in a "middleware" style.
- const wrapHandler =
- (
- callback: EventHandler,
- wrapper: (next: EventHandler, event: Event) => void,
- ): EventHandler =>
- (e) =>
- wrapper(callback, e);
-
- if (modifiers.includes('dot')) event = dotSyntax(event);
- if (modifiers.includes('camel')) event = camelCase(event);
- if (modifiers.includes('passive')) options.passive = true;
- if (modifiers.includes('capture')) options.capture = true;
- if (modifiers.includes('window')) listenerTarget = window;
- if (modifiers.includes('document')) listenerTarget = document;
-
- if (modifiers.includes('debounce')) {
- const nextModifier =
- modifiers[modifiers.indexOf('debounce') + 1] || 'invalid-wait';
- const wait = isNumeric(nextModifier.split('ms')[0])
- ? Number(nextModifier.split('ms')[0])
- : 250;
-
- handler = debounce(handler, wait);
- }
+type ListenerInfo = {
+ event: string;
+ target: ElementWithXAttributes | Window | Document;
+ filters: ((e: Event) => boolean)[];
+ handler(e: Event): void;
+ modifiers: string[];
+ cleanups: ((e: Event) => void)[];
+ options: {
+ passive: boolean;
+ capture: boolean;
+ };
+};
- if (modifiers.includes('throttle')) {
- const nextModifier =
- modifiers[modifiers.indexOf('throttle') + 1] || 'invalid-limit';
- const limit = isNumeric(nextModifier.split('ms')[0])
- ? Number(nextModifier.split('ms')[0])
- : 250;
+const dot = (listener: ListenerInfo) => {
+ listener.event = dotSyntax(listener.event);
+ return listener;
+};
- handler = throttle(handler, limit);
- }
+const camel = (listener: ListenerInfo) => {
+ listener.event = camelCase(listener.event);
+ return listener;
+};
- if (modifiers.includes('prevent'))
- handler = wrapHandler(handler, (next, e) => {
- e.preventDefault();
- next(e);
- });
+const passive = (listener: ListenerInfo) => {
+ listener.options.passive = true;
+ return listener;
+};
- if (modifiers.includes('stop'))
- handler = wrapHandler(handler, (next, e) => {
- e.stopPropagation();
- next(e);
- });
+const capture = (listener: ListenerInfo) => {
+ listener.options.capture = true;
+ return listener;
+};
- if (modifiers.includes('once')) {
- handler = wrapHandler(handler, (next, e) => {
- next(e);
+const hasWindow = (listener: ListenerInfo) =>
+ (listener.target = (listener.target as Element).ownerDocument?.defaultView);
+const hasDocument = (listener: ListenerInfo) =>
+ (listener.target = (listener.target as Element).ownerDocument);
- listenerTarget.removeEventListener(event, handler, options);
- });
- }
+const debounceListener = (listener: ListenerInfo, wait: number = 250) => {
+ listener.handler = debounce(listener.handler, wait);
+ return listener;
+};
- if (modifiers.includes('away') || modifiers.includes('outside')) {
- listenerTarget = document;
+const throttleListener = (listener: ListenerInfo, limit: number = 250) => {
+ listener.handler = throttle(listener.handler, limit);
+ return listener;
+};
- handler = wrapHandler(handler, (next, e) => {
- if (el.contains(e.target as Node)) return;
+const self = (listener: ListenerInfo) => {
+ listener.filters.push(isSelf);
+ return listener;
+};
- if ((e.target as Node).isConnected === false) return;
+const isSelf = (e: Event) => e.target === e.currentTarget;
- if (el.offsetWidth < 1 && el.offsetHeight < 1) return;
+const outside = (listener: ListenerInfo) => {
+ listener.filters.push(isAwayOutside.bind(null, listener.target));
+ hasDocument(listener);
+ return listener;
+};
- // Additional check for special implementations like x-collapse
- // where the element doesn't have display: none
- if (el._x_isShown === false) return;
+const once = (listener: ListenerInfo) => {
+ listener.cleanups.push(() => {
+ listener.target.removeEventListener(
+ listener.event,
+ listener.handler,
+ listener.options,
+ );
+ });
+ return listener;
+};
- next(e);
- });
- }
+const isAwayOutside = (el: ElementWithXAttributes, e: Event) => {
+ if (el.contains(e.target as Node)) return false;
- if (modifiers.includes('self'))
- handler = wrapHandler(handler, (next, e) => {
- e.target === el && next(e);
- });
+ if ((e.target as Node).isConnected === false) return false;
- // Handle :keydown and :keyup listeners.
- if (isKeyEvent(event) || isClickEvent(event)) {
- handler = wrapHandler(handler, (next, e) => {
- if (isListeningForASpecificKeyThatHasntBeenPressed(e, modifiers)) {
- return;
- }
+ if (el.offsetWidth < 1 && el.offsetHeight < 1) return false;
- next(e);
- });
- }
+ // Additional check for special implementations like x-collapse
+ // where the element doesn't have display: none
+ if (el._x_isShown === false) return false;
+
+ return true;
+};
- listenerTarget.addEventListener(event, handler, options);
+const hasPrevent = (listener: ListenerInfo) => listener.cleanups.push(prevent);
- return () => {
- listenerTarget.removeEventListener(event, handler, options);
- };
+const hasStop = (listener: ListenerInfo) => listener.cleanups.push(stop);
+
+const stop = (e: Event) => e.stopPropagation();
+
+const prevent = (e: Event) => e.preventDefault();
+
+const keyedEvent = (listener: ListenerInfo) => {
+ listener.filters.push(
+ (e) =>
+ !isListeningForASpecificKeyThatHasntBeenPressed(e, listener.modifiers),
+ );
};
type EventHandler = (event: Event) => void;
@@ -245,3 +284,20 @@ const keyToModifiers = (key: string): string[] => {
.map(([modifier, keytype]) => (keytype === key ? modifier : false))
.filter((mod: string | false): mod is string => Boolean(mod));
};
+
+const modifierOptions = {
+ dot,
+ camel,
+ passive,
+ capture,
+ window: hasWindow,
+ document: hasDocument,
+ debounce: debounceListener,
+ throttle: throttleListener,
+ self,
+ away: outside,
+ outside,
+ once,
+ prevent: hasPrevent,
+ stop: hasStop,
+};
diff --git a/packages/alpinets/tests/directives/x-on.bench.ts b/packages/alpinets/tests/directives/x-on.bench.ts
new file mode 100644
index 0000000..32fc5d1
--- /dev/null
+++ b/packages/alpinets/tests/directives/x-on.bench.ts
@@ -0,0 +1,87 @@
+import { on } from '../../src/utils/on';
+import { on as oldOn } from '../../src/utils/on-old';
+import { bench } from 'vitest';
+
+describe('x-on Handler Creation', () => {
+ bench(
+ 'New',
+ () => {
+ const el = {
+ addEventListener() {},
+ };
+ on(
+ el as any,
+ 'click',
+ ['prevent', 'stop', 'self', 'dot', 'camel', 'once'],
+ (_e) => {},
+ );
+ },
+ { iterations: 1, time: 160 },
+ );
+ bench(
+ 'Old',
+ () => {
+ const el = {
+ addEventListener() {},
+ };
+ oldOn(
+ el as any,
+ 'click',
+ ['prevent', 'stop', 'self', 'dot', 'camel', 'once'],
+ (_e) => {},
+ );
+ },
+ { iterations: 1, time: 160 },
+ );
+});
+describe('x-on Handler Execution', () => {
+ const event = {
+ type: 'click',
+ target: 'hello',
+ currentTarget: 'hello',
+ stopPropagation() {},
+ preventDefault() {},
+ };
+ const oldel = {
+ handler: null as (e: typeof event) => void | null,
+ addEventListener(event: string, handler: (e: typeof event) => void) {
+ this.handler = handler;
+ },
+ removeEventListener() {},
+ };
+ oldOn(
+ oldel as any,
+ 'click',
+ ['prevent', 'stop', 'self', 'dot', 'camel', 'once'],
+ (_e) => {},
+ );
+
+ const newel = {
+ handler: null as (e: typeof event) => void | null,
+ addEventListener(event: string, handler: (e: typeof event) => void) {
+ this.handler = handler;
+ },
+ removeEventListener() {},
+ };
+ on(
+ newel as any,
+ 'click',
+ ['prevent', 'stop', 'self', 'dot', 'camel', 'once'],
+ (_e) => {},
+ );
+
+ bench(
+ 'Old',
+ () => {
+ oldel.handler!(event);
+ },
+ { iterations: 1, time: 160 },
+ );
+ bench(
+ 'New',
+ () => {
+ newel.handler!(event);
+ },
+ { iterations: 1, time: 160 },
+ );
+});
diff --git a/size.json b/size.json
index 1638539..21c4239 100644
--- a/size.json
+++ b/size.json
@@ -1,12 +1,12 @@
{
"alpinets": {
"minified": {
- "pretty": "39.9 kB",
- "raw": 39949
+ "pretty": "40.1 kB",
+ "raw": 40114
},
"brotli": {
- "pretty": "13.6 kB",
- "raw": 13573
+ "pretty": "13.7 kB",
+ "raw": 13711
}
},
"anchor": {