on-events 0.0.3 → 0.0.4

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
@@ -1,3 +1,14 @@
1
+
2
+ # on-events
3
+
4
+ [![npm version](https://img.shields.io/npm/v/on-events.svg)](https://www.npmjs.com/package/on-events)
5
+ [![npm downloads](https://img.shields.io/npm/dm/on-events.svg)](https://www.npmjs.com/package/on-events)
6
+ [![GitHub stars](https://img.shields.io/github/stars/iWhatty/on-event-js.svg?style=social)](https://github.com/iWhatty/on-event-js)
7
+ [![License](https://img.shields.io/github/license/iWhatty/on-event-js.svg)](https://github.com/iWhatty/on-event-js/blob/main/LICENSE)
8
+
9
+ **A tiny DOM event utility with composable sugar.**
10
+
11
+
1
12
  # on-events
2
13
 
3
14
  **A tiny DOM event utility with composable sugar.**
@@ -7,7 +18,7 @@ Write clean event bindings using fluent chains like `On.click(...)`, `On.capture
7
18
 
8
19
  ## ⚡ Example Usage
9
20
 
10
- One expressive line. Fully composable. No option objects.
21
+ Compose `once`, `capture`, `passive`, and delegation without repetitive option objects.
11
22
 
12
23
  ```js
13
24
  import { On } from 'on-events'
@@ -20,11 +31,11 @@ function handleLinkClick(e) {
20
31
  On.first.delegate.capture.click(document, 'a.nav-link', handleLinkClick)
21
32
  ```
22
33
 
23
- Delegated
24
- Capture phase
25
- Fires once
26
- Clean `this` binding
27
- Returns `stop()` if you need manual control
34
+ * Delegated
35
+ * Capture phase
36
+ * Fires once
37
+ * Clean `this` binding
38
+ * Returns `stop()` if you need manual control
28
39
 
29
40
  ---
30
41
 
@@ -36,10 +47,12 @@ On.first.delegate.capture.click(document, 'a.nav-link', handleLinkClick)
36
47
  * `On.capture.passive.scroll(el, fn)` — fully composable modifiers
37
48
  * `On.delegate.click(el, selector, fn)` — delegated events
38
49
  * `On.hover(el, enter, leave)` — mouseenter/leave pair
39
- * `On.batch(el, { click, ... })` — multi-bind at once
50
+ * `On.batch(el, { click, ... })` — bind multiple events at once
40
51
  * `On.first.batch(...)` — one-time multi-bind
41
52
  * `On.ready(fn)` — run when DOM is ready
42
- * ESM, zero dependencies, < 1KB min+gzip
53
+ * `On.group()` collect related listeners and tear them down together
54
+ * Better TS support for simple binds like `On.input(el, fn)` and `On.change(el, fn)`
55
+ * ESM, zero dependencies, tiny footprint
43
56
 
44
57
  ---
45
58
 
@@ -51,10 +64,6 @@ npm install on-events
51
64
 
52
65
  ---
53
66
 
54
- ## Usage
55
-
56
- ---
57
-
58
67
  ## Fluent & Composable Sugar
59
68
 
60
69
  ```js
@@ -150,7 +159,7 @@ On.first.batch(document, {
150
159
 
151
160
  ---
152
161
 
153
- ## Delegate Batch (Optional Pattern)
162
+ ## Delegate Batch
154
163
 
155
164
  You may pass `[selector, handler]` for delegated batch entries:
156
165
 
@@ -178,9 +187,56 @@ On.ready(handleReady)
178
187
 
179
188
  ---
180
189
 
190
+ ## Grouped Cleanup
191
+
192
+ When a UI module binds listeners across multiple elements, `On.group()` lets you track them under one scoped teardown handle.
193
+
194
+ ```js
195
+ import { On } from 'on-events'
196
+
197
+ const page = On.group()
198
+
199
+ page.click(settingsToggleBtn, () => {
200
+ settingsSection.classList.toggle('collapsed')
201
+ })
202
+
203
+ page.input(searchInput, handleSearch)
204
+ page.delegate.click(document, 'button.save', handleSave)
205
+
206
+ // Later:
207
+ page.stop()
208
+ ```
209
+
210
+ You can also manually add an existing cleanup function:
211
+
212
+ ```js
213
+ const group = On.group()
214
+
215
+ group.add(On.click(button, handleClick))
216
+ group.add(null) // safely ignored
217
+
218
+ group.stop()
219
+ ```
220
+
221
+ ---
222
+
223
+ ## Custom Events
224
+
225
+ For custom or non-standard event names, use `On.event(type)`:
226
+
227
+ ```js
228
+ const stop = On.event('panel:open')(panel, (e) => {
229
+ console.log('opened', e.type)
230
+ })
231
+
232
+ stop()
233
+ ```
234
+
235
+ ---
236
+
181
237
  # API Reference
182
238
 
183
- # Composable Modifiers
239
+ ## Composable Modifiers
184
240
 
185
241
  Modifiers can be chained before the event name.
186
242
 
@@ -239,6 +295,23 @@ Runs `fn` once the DOM is fully loaded (`DOMContentLoaded` or already ready).
239
295
 
240
296
  ---
241
297
 
298
+ ## `On.group()`
299
+
300
+ Creates a scoped cleanup collector for related listeners.
301
+
302
+ ```js
303
+ const group = On.group()
304
+
305
+ group.click(button, onClick)
306
+ group.input(input, onInput)
307
+
308
+ group.stop()
309
+ ```
310
+
311
+ Useful when a page, modal, or UI module binds listeners across multiple elements and wants one teardown call.
312
+
313
+ ---
314
+
242
315
  ## Why not `addEventListener` directly?
243
316
 
244
317
  `addEventListener` is great — this library just removes the repetitive parts when you bind lots of UI events.
@@ -247,6 +320,7 @@ Runs `fn` once the DOM is fully loaded (`DOMContentLoaded` or already ready).
247
320
  * Every bind returns a `stop()` cleanup function
248
321
  * Delegation helper that sets `this` to the matched element
249
322
  * Batch binding to keep setup code tidy
323
+ * Grouped cleanup for lifecycle-based teardown
250
324
  * Composable modifiers instead of option object juggling
251
325
  * Zero deps and tiny footprint
252
326
 
@@ -260,7 +334,7 @@ This library is a thin wrapper around native `addEventListener`.
260
334
  * **`first` / `capture` / `passive`**: uses native listener options.
261
335
  * **Delegation (`On.delegate.*`)**: performs a `closest(selector)` lookup per event. Ideal for reducing listener count, but direct binding is better for extremely hot events like `mousemove`.
262
336
 
263
- Rule of thumb: delegate `click/input/submit`, bind directly for high-frequency events.
337
+ Rule of thumb: delegate `click`, `input`, and `submit`; bind directly for high-frequency events.
264
338
 
265
339
  ---
266
340
 
@@ -273,14 +347,18 @@ Rule of thumb: delegate `click/input/submit`, bind directly for high-frequency e
273
347
 
274
348
  ---
275
349
 
276
- ## Low-level API (on / off)
350
+ ## Low-level API (`on` / `off`)
277
351
 
278
352
  If you prefer a minimal, explicit API without fluent modifiers, you can use the core helpers directly.
279
353
 
280
354
  ### `on(el, event, handler)`
281
355
 
356
+ ### `on(el, event, handler, options)`
357
+
282
358
  ### `on(el, event, selector, handler)`
283
359
 
360
+ ### `on(el, event, selector, handler, options)`
361
+
284
362
  Adds a standard or delegated event listener.
285
363
  Returns a `stop()` function that removes the listener.
286
364
 
@@ -5,87 +5,99 @@ export type Stop = () => void
5
5
  export type Handler<E extends Event = Event> = (this: Element, ev: E) => any
6
6
  export type DirectHandler<E extends Event = Event> = (ev: E) => any
7
7
 
8
+ export type BatchMap = Record<
9
+ string,
10
+ | DirectHandler<any>
11
+ | [string, Handler<any>]
12
+ >
13
+
8
14
  // --- Low-level API ---
9
15
 
10
16
  export function on<E extends Event = Event>(
11
- el: EventTarget,
12
- event: string,
13
- handler: DirectHandler<E>
17
+ el: EventTarget,
18
+ event: string,
19
+ handler: DirectHandler<E>,
20
+ options?: AddEventListenerOptions | boolean
14
21
  ): Stop
15
22
 
16
23
  export function on<E extends Event = Event>(
17
- el: Element | Document,
18
- event: string,
19
- selector: string,
20
- handler: Handler<E>
24
+ el: Element | Document,
25
+ event: string,
26
+ selector: string,
27
+ handler: Handler<E>,
28
+ options?: AddEventListenerOptions | boolean
21
29
  ): Stop
22
30
 
23
31
  export function off<E extends Event = Event>(
24
- el: EventTarget,
25
- event: string,
26
- handler: DirectHandler<E>,
27
- selector?: null
32
+ el: EventTarget,
33
+ event: string,
34
+ handler: DirectHandler<E>,
35
+ selector?: null
28
36
  ): void
29
37
 
30
38
  export function off<E extends Event = Event>(
31
- el: Element | Document,
32
- event: string,
33
- handler: Handler<E>,
34
- selector?: string
39
+ el: Element | Document,
40
+ event: string,
41
+ handler: Handler<E>,
42
+ selector?: string
35
43
  ): void
36
44
 
37
- // --- Fluent Chain API ---
45
+ // --- Fluent Event Binder ---
46
+
47
+ export interface EventBinder<E extends Event = Event> {
48
+ (el: EventTarget, handler: DirectHandler<E>): Stop
49
+ (el: Element | Document, selector: string, handler: Handler<E>): Stop
50
+ }
51
+
52
+ // --- Fluent Chain API core ---
38
53
 
39
54
  export interface OnChain {
40
- // modifiers (composable)
41
- first: OnChain
42
- once: OnChain
43
- capture: OnChain
44
- passive: OnChain
45
- delegate: OnChain
46
-
47
- // utilities
48
- hover(
49
- el: Element,
50
- enter: (ev: MouseEvent) => any,
51
- leave: (ev: MouseEvent) => any
52
- ): Stop
53
-
54
- batch(
55
- el: EventTarget,
56
- map: Record<
57
- string,
58
- | DirectHandler<any>
59
- | [string, Handler<any>]
60
- >
61
- ): Stop
62
-
63
- ready(fn: () => void): void
64
-
65
- // event binder (fluent)
66
- <E extends Event = Event>(
67
- el: EventTarget,
68
- handler: DirectHandler<E>
69
- ): Stop
70
-
71
- <E extends Event = Event>(
72
- el: Element | Document,
73
- selector: string,
74
- handler: Handler<E>
75
- ): Stop
76
-
77
- // dynamic event access (On.click, On.keydown, etc.)
78
- [event: string]:
79
- | OnChain
80
- | (<E extends Event = Event>(
81
- el: EventTarget,
82
- handler: DirectHandler<E>
83
- ) => Stop)
84
- | (<E extends Event = Event>(
85
- el: Element | Document,
86
- selector: string,
87
- handler: Handler<E>
88
- ) => Stop)
55
+ first: OnChain
56
+ once: OnChain
57
+ capture: OnChain
58
+ passive: OnChain
59
+ delegate: OnChain
60
+
61
+ hover(
62
+ el: Element,
63
+ enter: (ev: MouseEvent) => any,
64
+ leave: (ev: MouseEvent) => any
65
+ ): Stop
66
+
67
+ batch(
68
+ el: EventTarget,
69
+ map: BatchMap
70
+ ): Stop
71
+
72
+ ready(fn: () => void): void
73
+
74
+ group(): OnGroup
75
+
76
+ <E extends Event = Event>(
77
+ el: EventTarget,
78
+ handler: DirectHandler<E>
79
+ ): Stop
80
+
81
+ <E extends Event = Event>(
82
+ el: Element | Document,
83
+ selector: string,
84
+ handler: Handler<E>
85
+ ): Stop
86
+
87
+ // fallback for custom / non-standard event names
88
+ event(type: string): OnChain & EventBinder<Event>
89
+ }
90
+
91
+ export type OnEventMap = {
92
+ [K in keyof GlobalEventHandlersEventMap]:
93
+ OnChain & EventBinder<GlobalEventHandlersEventMap[K]>
94
+ }
95
+
96
+ export type OnAPI = OnChain & OnEventMap
97
+
98
+ export type OnGroup = OnAPI & {
99
+ stop(): void
100
+ add<T extends Stop | null | undefined | false>(stop: T): T
89
101
  }
90
102
 
91
- export const On: OnChain
103
+ export const On: OnAPI
@@ -1,2 +1,2 @@
1
1
  // On-Events - A tiny DOM event utility with sugar.
2
- var p=new WeakMap;function d(t,c,i,n,e){typeof i=="function"&&(e=n,n=i,i=null);let u=e||void 0,r=i?o=>{let s=o.target instanceof Element?o.target:o.target?.parentElement;if(!s)return;let f=s.closest(i);f&&t.contains(f)&&n.call(f,o)}:n;return t.addEventListener(c,r,u),p.has(t)||p.set(t,[]),p.get(t).push({type:c,cb:n,selector:i,wrapped:r,capture:!!(u&&u.capture)}),()=>L(t,c,n,i)}function L(t,c,i,n=null){let e=p.get(t);if(e){for(let u=e.length;u-- >0;){let r=e[u];r.type===c&&r.cb===i&&(n?r.selector===n:!r.selector)&&(t.removeEventListener(c,r.wrapped,{capture:r.capture}),e.splice(u,1))}e.length||p.delete(t)}}var m=1,E=2,v=4,h=8,O=t=>{if(!(t&(m|E|v)))return;let c={};return t&m&&(c.once=!0),t&E&&(c.capture=!0),t&v&&(c.passive=!0),c},a=(t=0)=>new Proxy(Object.create(null),{get(c,i){let n=String(i);return n==="first"||n==="once"?a(t|m):n==="capture"?a(t|E):n==="passive"?a(t|v):n==="delegate"?a(t|h):n==="hover"?(e,u,r)=>{let o=O(t),s=d(e,"mouseenter",u,o),f=d(e,"mouseleave",r,o);return()=>{s(),f()}}:n==="batch"?(e,u)=>{let r=[];for(let[o,s]of Object.entries(u)){if(typeof s=="function"){r.push(a(t)[o](e,s));continue}if(Array.isArray(s)){let[f,A]=s;r.push(a(t|h)[o](e,f,A))}}return()=>r.forEach(o=>o())}:n==="ready"?e=>{typeof document>"u"||(document.readyState==="loading"?document.addEventListener("DOMContentLoaded",e,{once:!0}):e())}:(e,...u)=>{let r=O(t);if(t&h){let[s,f]=u;return d(e,n,s,f,r)}let[o]=u;return d(e,n,o,r)}}}),y=a();y.once=y.first;var g=(t,c,...i)=>d(t,c,...i);export{y as On,L as off,g as on};
2
+ var h=new WeakMap;function p(n,u,c,r,t){typeof c=="function"&&(t=r,r=c,c=null);let o=t||void 0,e=c?i=>{let f=i.target instanceof Element?i.target:i.target?.parentElement;if(!f)return;let d=f.closest(c);d&&typeof n.contains=="function"&&n.contains(d)&&r.call(d,i)}:r;return n.addEventListener(u,e,o),h.has(n)||h.set(n,[]),h.get(n).push({type:u,cb:r,selector:c,wrapped:e,capture:!!(o&&o.capture)}),()=>w(n,u,r,c)}function w(n,u,c,r=null){let t=h.get(n);if(t){for(let o=t.length;o-- >0;){let e=t[o];e.type===u&&e.cb===c&&(r?e.selector===r:!e.selector)&&(n.removeEventListener(u,e.wrapped,{capture:e.capture}),t.splice(o,1))}t.length||h.delete(n)}}var m=1,v=2,y=4,l=8,E=n=>{if(!(n&(m|v|y)))return;let u={};return n&m&&(u.once=!0),n&v&&(u.capture=!0),n&y&&(u.passive=!0),u},a=(n=0)=>new Proxy(Object.create(null),{get(u,c){if(Reflect.has(u,c))return Reflect.get(u,c);let r=String(c);return r==="first"||r==="once"?a(n|m):r==="capture"?a(n|v):r==="passive"?a(n|y):r==="delegate"?a(n|l):r==="event"?t=>a(n)[t]:r==="hover"?(t,o,e)=>{let s=E(n),i=p(t,"mouseenter",o,s),f=p(t,"mouseleave",e,s);return()=>{i(),f()}}:r==="batch"?(t,o)=>{let e=[];for(let[s,i]of Object.entries(o)){if(typeof i=="function"){e.push(a(n)[s](t,i));continue}if(Array.isArray(i)){let[f,d]=i;e.push(a(n|l)[s](t,f,d))}}return()=>{for(let s of e)s()}}:r==="ready"?t=>{typeof document>"u"||(document.readyState==="loading"?document.addEventListener("DOMContentLoaded",t,{once:!0}):t())}:(t,...o)=>{let e=E(n);if(n&l){let[i,f]=o;return p(t,r,i,f,e)}let[s]=o;return p(t,r,s,e)}}}),g=a();g.group=function(){let u=new Set,c=t=>(typeof t=="function"&&u.add(t),t),r=()=>{let t=[];for(let o of u)try{o()}catch(e){t.push(e)}if(u.clear(),t.length===1)throw t[0];if(t.length>1)throw new AggregateError(t,"On.group().stop() failed for one or more listeners")};return new Proxy(Object.create(null),{get(t,o){if(o==="add")return c;if(o==="stop")return r;let e=g[o];return e==null?e:o==="event"?s=>{let i=e(s);return(...f)=>c(i(...f))}:typeof e=="function"?(...s)=>c(e(...s)):new Proxy(e,{get(s,i){let f=e[i];return typeof f=="function"?(...d)=>c(f(...d)):f}})}})};g.once=g.first;var O=(n,u,...c)=>p(n,u,...c);export{g as On,w as off,O as on};
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/on-events.js"],
4
- "sourcesContent": ["// on-events.js\r\n// On-Events \u2014 tiny DOM event utility with composable sugar.\r\n\r\n// --- internal base ---\r\nconst listeners = new WeakMap()\r\n\r\n/**\r\n * baseOn(el, type, handler)\r\n * baseOn(el, type, selector, handler)\r\n * baseOn(el, type, handler, options)\r\n * baseOn(el, type, selector, handler, options)\r\n */\r\nfunction baseOn(el, type, selector, cb, options) {\r\n // normalize args\r\n if (typeof selector === 'function') {\r\n options = cb\r\n cb = selector\r\n selector = null\r\n }\r\n\r\n const opts = options || undefined\r\n\r\n const wrapped = selector\r\n ? (e) => {\r\n const target = e.target instanceof Element ? e.target : e.target?.parentElement\r\n if (!target) return\r\n const match = target.closest(selector)\r\n if (match && el.contains(match)) cb.call(match, e)\r\n }\r\n : cb\r\n\r\n el.addEventListener(type, wrapped, opts)\r\n\r\n if (!listeners.has(el)) listeners.set(el, [])\r\n listeners.get(el).push({\r\n type,\r\n cb,\r\n selector,\r\n wrapped,\r\n capture: !!(opts && opts.capture), // critical for proper removal\r\n })\r\n\r\n return () => off(el, type, cb, selector)\r\n}\r\n\r\nfunction off(el, type, cb, selector = null) {\r\n const group = listeners.get(el)\r\n if (!group) return\r\n\r\n for (let i = group.length; i-- > 0;) {\r\n const h = group[i]\r\n const match =\r\n h.type === type &&\r\n h.cb === cb &&\r\n (selector ? h.selector === selector : !h.selector)\r\n\r\n if (match) {\r\n // capture must match for removal\r\n el.removeEventListener(type, h.wrapped, { capture: h.capture })\r\n group.splice(i, 1)\r\n }\r\n }\r\n\r\n if (!group.length) listeners.delete(el)\r\n}\r\n\r\n// --- composable sugar: On.<mods>.<event>(...) ---\r\n// bitflags keep the proxy tiny + fast\r\nconst ONCE = 1\r\nconst CAPTURE = 2\r\nconst PASSIVE = 4\r\nconst DELEGATE = 8\r\n\r\nconst optsFor = (f) => {\r\n // Avoid allocating options when none are set.\r\n // Note: capture removal is handled by storing capture in registry.\r\n if (!(f & (ONCE | CAPTURE | PASSIVE))) return undefined\r\n const o = {}\r\n if (f & ONCE) o.once = true\r\n if (f & CAPTURE) o.capture = true\r\n if (f & PASSIVE) o.passive = true\r\n return o\r\n}\r\n\r\nconst makeOn = (flags = 0) =>\r\n new Proxy(Object.create(null), {\r\n get(_t, prop) {\r\n const k = String(prop)\r\n\r\n // modifier chaining\r\n if (k === 'first' || k === 'once') return makeOn(flags | ONCE)\r\n if (k === 'capture') return makeOn(flags | CAPTURE)\r\n if (k === 'passive') return makeOn(flags | PASSIVE)\r\n if (k === 'delegate') return makeOn(flags | DELEGATE)\r\n\r\n // utilities (kept as properties for backwards-compat ergonomics)\r\n if (k === 'hover') {\r\n return (el, enter, leave) => {\r\n const o = optsFor(flags)\r\n const offIn = baseOn(el, 'mouseenter', enter, o)\r\n const offOut = baseOn(el, 'mouseleave', leave, o)\r\n return () => {\r\n offIn()\r\n offOut()\r\n }\r\n }\r\n }\r\n\r\n if (k === 'batch') {\r\n return (el, map) => {\r\n const stops = []\r\n for (const [event, val] of Object.entries(map)) {\r\n // basic: { click: fn }\r\n if (typeof val === 'function') {\r\n stops.push(makeOn(flags)[event](el, val))\r\n continue\r\n }\r\n // optional delegate batch: { click: ['a', fn] }\r\n if (Array.isArray(val)) {\r\n const [selector, fn] = val\r\n stops.push(makeOn(flags | DELEGATE)[event](el, selector, fn))\r\n }\r\n }\r\n return () => stops.forEach((stop) => stop())\r\n }\r\n }\r\n\r\n if (k === 'ready') {\r\n return (fn) => {\r\n if (typeof document === 'undefined') return\r\n if (document.readyState === 'loading') {\r\n document.addEventListener('DOMContentLoaded', fn, { once: true })\r\n } else {\r\n fn()\r\n }\r\n }\r\n }\r\n\r\n // event binder\r\n return (el, ...args) => {\r\n const o = optsFor(flags)\r\n\r\n if (flags & DELEGATE) {\r\n const [selector, handler] = args\r\n return baseOn(el, k, selector, handler, o)\r\n }\r\n\r\n const [handler] = args\r\n return baseOn(el, k, handler, o)\r\n }\r\n },\r\n })\r\n\r\nconst On = makeOn()\r\n\r\n// --- legacy alias for backward compatibility ---\r\nOn.once = On.first\r\n\r\n// --- classic support ---\r\nconst on = (el, event, ...args) => baseOn(el, event, ...args)\r\n\r\nexport { on, On, off }"],
5
- "mappings": "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",
6
- "names": ["listeners", "baseOn", "el", "type", "selector", "cb", "options", "opts", "wrapped", "e", "target", "match", "off", "group", "i", "h", "ONCE", "CAPTURE", "PASSIVE", "DELEGATE", "optsFor", "f", "o", "makeOn", "flags", "_t", "prop", "k", "enter", "leave", "offIn", "offOut", "map", "stops", "event", "val", "fn", "stop", "args", "handler", "On", "on"]
4
+ "sourcesContent": ["// on-events.js\r\n// On-Events \u2014 tiny DOM event utility with composable sugar.\r\n\r\n// --- internal base ---\r\nconst listeners = new WeakMap()\r\n\r\n/**\r\n * baseOn(el, type, handler)\r\n * baseOn(el, type, selector, handler)\r\n * baseOn(el, type, handler, options)\r\n * baseOn(el, type, selector, handler, options)\r\n */\r\nfunction baseOn(el, type, selector, cb, options) {\r\n // Normalize overloaded signatures so everything below can use one path.\r\n if (typeof selector === 'function') {\r\n options = cb\r\n cb = selector\r\n selector = null\r\n }\r\n\r\n const opts = options || undefined\r\n\r\n const wrapped = selector\r\n ? (e) => {\r\n // Delegation can receive a Text node as the event target.\r\n // Normalize that to an element before calling closest().\r\n const target = e.target instanceof Element ? e.target : e.target?.parentElement\r\n if (!target) return\r\n\r\n const match = target.closest(selector)\r\n if (!match) return\r\n\r\n // Guard delegated matching so odd targets like window do not explode.\r\n if (typeof el.contains === 'function' && el.contains(match)) {\r\n cb.call(match, e)\r\n }\r\n }\r\n : cb\r\n\r\n el.addEventListener(type, wrapped, opts)\r\n\r\n if (!listeners.has(el)) listeners.set(el, [])\r\n const group = listeners.get(el)\r\n\r\n group.push({\r\n type,\r\n cb,\r\n selector,\r\n wrapped,\r\n capture: !!(opts && opts.capture), // capture must match during removal\r\n })\r\n\r\n return () => off(el, type, cb, selector)\r\n}\r\n\r\nfunction off(el, type, cb, selector = null) {\r\n const group = listeners.get(el)\r\n if (!group) return\r\n\r\n for (let i = group.length; i-- > 0;) {\r\n const h = group[i]\r\n const match =\r\n h.type === type &&\r\n h.cb === cb &&\r\n (selector ? h.selector === selector : !h.selector)\r\n\r\n if (match) {\r\n // removeEventListener only cares about capture on teardown.\r\n el.removeEventListener(type, h.wrapped, { capture: h.capture })\r\n group.splice(i, 1)\r\n }\r\n }\r\n\r\n if (!group.length) listeners.delete(el)\r\n}\r\n\r\n// --- composable sugar: On.<mods>.<event>(...) ---\r\n// Bitflags keep the proxy small and cheap.\r\nconst ONCE = 1\r\nconst CAPTURE = 2\r\nconst PASSIVE = 4\r\nconst DELEGATE = 8\r\n\r\nconst optsFor = (f) => {\r\n // Allocate listener options only when needed.\r\n // Capture is stored separately so removeEventListener can match it later.\r\n if (!(f & (ONCE | CAPTURE | PASSIVE))) return undefined\r\n\r\n const o = {}\r\n if (f & ONCE) o.once = true\r\n if (f & CAPTURE) o.capture = true\r\n if (f & PASSIVE) o.passive = true\r\n return o\r\n}\r\n\r\nconst makeOn = (flags = 0) =>\r\n new Proxy(Object.create(null), {\r\n get(target, prop) {\r\n // Respect concrete properties assigned directly onto the proxy target.\r\n // This is what makes late-added members like On.group work instead of\r\n // being mistaken for an event name.\r\n if (Reflect.has(target, prop)) {\r\n return Reflect.get(target, prop)\r\n }\r\n\r\n const k = String(prop)\r\n\r\n // Modifier chaining accumulates flags and returns a fresh proxy.\r\n if (k === 'first' || k === 'once') return makeOn(flags | ONCE)\r\n if (k === 'capture') return makeOn(flags | CAPTURE)\r\n if (k === 'passive') return makeOn(flags | PASSIVE)\r\n if (k === 'delegate') return makeOn(flags | DELEGATE)\r\n\r\n // Custom event helper: On.event('panel:open')(el, fn)\r\n if (k === 'event') {\r\n return (type) => makeOn(flags)[type]\r\n }\r\n\r\n // Utilities live on the same fluent surface for ergonomics.\r\n if (k === 'hover') {\r\n return (el, enter, leave) => {\r\n const o = optsFor(flags)\r\n const offIn = baseOn(el, 'mouseenter', enter, o)\r\n const offOut = baseOn(el, 'mouseleave', leave, o)\r\n return () => {\r\n offIn()\r\n offOut()\r\n }\r\n }\r\n }\r\n\r\n if (k === 'batch') {\r\n return (el, map) => {\r\n const stops = []\r\n\r\n for (const [event, val] of Object.entries(map)) {\r\n // Direct entry: { click: fn }\r\n if (typeof val === 'function') {\r\n stops.push(makeOn(flags)[event](el, val))\r\n continue\r\n }\r\n\r\n // Delegated entry: { click: ['a', fn] }\r\n if (Array.isArray(val)) {\r\n const [selector, fn] = val\r\n stops.push(makeOn(flags | DELEGATE)[event](el, selector, fn))\r\n }\r\n }\r\n\r\n return () => {\r\n for (const stop of stops) stop()\r\n }\r\n }\r\n }\r\n\r\n if (k === 'ready') {\r\n return (fn) => {\r\n if (typeof document === 'undefined') return\r\n\r\n // Run immediately if the DOM is already ready.\r\n if (document.readyState === 'loading') {\r\n document.addEventListener('DOMContentLoaded', fn, { once: true })\r\n } else {\r\n fn()\r\n }\r\n }\r\n }\r\n\r\n // Default path: treat the property name as the event type.\r\n return (el, ...args) => {\r\n const o = optsFor(flags)\r\n\r\n if (flags & DELEGATE) {\r\n const [selector, handler] = args\r\n return baseOn(el, k, selector, handler, o)\r\n }\r\n\r\n const [handler] = args\r\n return baseOn(el, k, handler, o)\r\n }\r\n },\r\n })\r\n\r\nconst On = makeOn()\r\n\r\n// Creates a scoped listener collector.\r\n// Any stop() returned by On.* calls is tracked and can be removed together via group.stop().\r\nOn.group = function group() {\r\n const stops = new Set()\r\n\r\n const track = (stop) => {\r\n // Pass through nullish / false values so callers can safely write:\r\n // group.add(maybeElement ? On.click(...) : null)\r\n if (typeof stop === 'function') stops.add(stop)\r\n return stop\r\n }\r\n\r\n const stopAll = () => {\r\n const errors = []\r\n\r\n // Try every tracked cleanup so one bad stop() does not block the rest.\r\n for (const stop of stops) {\r\n try {\r\n stop()\r\n } catch (err) {\r\n errors.push(err)\r\n }\r\n }\r\n\r\n stops.clear()\r\n\r\n if (errors.length === 1) throw errors[0]\r\n if (errors.length > 1) {\r\n throw new AggregateError(errors, 'On.group().stop() failed for one or more listeners')\r\n }\r\n }\r\n\r\n return new Proxy(Object.create(null), {\r\n get(_target, prop) {\r\n // Hard override these names so they never fall through into event binding.\r\n if (prop === 'add') return track\r\n if (prop === 'stop') return stopAll\r\n\r\n const value = On[prop]\r\n if (value == null) return value\r\n\r\n // event(type) is a factory: it returns a binder, not a stop handle.\r\n // Wrap the binder so the eventual stop() is what gets tracked.\r\n if (prop === 'event') {\r\n return (type) => {\r\n const binder = value(type)\r\n return (...args) => track(binder(...args))\r\n }\r\n }\r\n\r\n // Top-level callables like group.click(...), group.batch(...), group.ready(...)\r\n // are wrapped so any returned stop() is automatically tracked.\r\n if (typeof value === 'function') {\r\n return (...args) => track(value(...args))\r\n }\r\n\r\n // Nested chain objects like group.capture or group.delegate are proxies too.\r\n // Wrap their callable leaves so group.capture.click(...) also tracks cleanup.\r\n return new Proxy(value, {\r\n get(_nestedTarget, nestedProp) {\r\n const nestedValue = value[nestedProp]\r\n\r\n if (typeof nestedValue === 'function') {\r\n return (...args) => track(nestedValue(...args))\r\n }\r\n\r\n return nestedValue\r\n },\r\n })\r\n },\r\n })\r\n}\r\n\r\n// --- legacy alias for backward compatibility ---\r\nOn.once = On.first\r\n\r\n// --- classic support ---\r\nconst on = (el, event, ...args) => baseOn(el, event, ...args)\r\n\r\nexport { on, On, off }\r\n"],
5
+ "mappings": "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",
6
+ "names": ["listeners", "baseOn", "el", "type", "selector", "cb", "options", "opts", "wrapped", "e", "target", "match", "off", "group", "i", "h", "ONCE", "CAPTURE", "PASSIVE", "DELEGATE", "optsFor", "f", "o", "makeOn", "flags", "prop", "k", "enter", "leave", "offIn", "offOut", "map", "stops", "event", "val", "fn", "stop", "args", "handler", "On", "track", "stopAll", "errors", "err", "_target", "value", "binder", "_nestedTarget", "nestedProp", "nestedValue", "on"]
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "on-events",
3
- "version": "0.0.3",
3
+ "version": "0.0.4",
4
4
  "description": "Tiny DOM event utility with composable sugar, delegation, and cleanup.",
5
5
  "type": "module",
6
6
  "sideEffects": false,