dom-primitive-bindings 0.0.0-stage → 1.0.0

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/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 KeshavSoft
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
package/README.md CHANGED
@@ -1,3 +1,126 @@
1
- # Temporary Holding Version
1
+ # DOM Primitive Bindings
2
+
3
+ > Modern, lightweight, delegated event engine for browser DOMs. Attach exactly one listener per event type to any container; let bubbling handle matching, runners, and extraction with zero re-binding overhead.
4
+
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
6
+ [![Tests](https://img.shields.io/badge/tests-passing-brightgreen.svg)]()
7
+ [![Single Source of Truth](https://img.shields.io/badge/Version-v1-blue.svg)]()
8
+
9
+ ---
10
+
11
+ ## 🎯 The Core Concept
12
+
13
+ Traditional client-side code attaches listeners to every button, link, and input. When tables are refreshed, cards are added, or modals render dynamic markup, those listeners break or leak memory unless manually re-attached.
14
+
15
+ **DOM Primitive Bindings** inverts the architecture:
16
+ - Attach **one listener** per event type to the parent container (`table`, `form`, `card-grid`).
17
+ - As events bubble up, `closest(selector)` identifies target elements.
18
+ - The associated `runner` executes and forwards data cleanly to `onExtract`.
19
+ - Dynamically created DOM elements are supported immediately with **zero re-binding**.
20
+
21
+ ---
22
+
23
+ ## 🏗️ Staged Architecture (`src/v1/`)
24
+
25
+ The repository strictly enforces a staged pipeline where every directory contains an `index.js` and all files export a single function as `default`:
26
+
27
+ ```text
28
+ src/
29
+ └── v1/
30
+ ├── guards/ # Validates container and bindings array
31
+ │ ├── container/ # Validates and resolves 1st input (container / ID)
32
+ │ ├── bindings/ # Validates 2nd input (bindings array)
33
+ │ └── index.js
34
+ ├── prepare/ # Groups bindings by event type into Map
35
+ │ ├── groupBindings.js
36
+ │ └── index.js
37
+ ├── listeners/ # Attaches single listener per event type to container
38
+ │ ├── attachListener.js
39
+ │ └── index.js
40
+ ├── dispatch/ # Handles event bubbling, runner execution & extraction
41
+ │ ├── getStartInput.js
42
+ │ ├── getClientData.js
43
+ │ ├── executeBinding.js
44
+ │ └── index.js
45
+ ├── register/ # Exposes module onto window.ks.domPrimitiveBindings
46
+ │ ├── meta.js
47
+ │ └── index.js
48
+ └── index.js # Orchestrator exporting bindEvents
49
+ ```
50
+
51
+ ### Key Design Disciplines
52
+ 1. **Zero Comments:** The folder hierarchy and descriptive file names convey the full narrative without inline code comments.
53
+ 2. **Folder Discipline:** Every folder contains an `index.js` public gateway.
54
+ 3. **Single Function Default Export:** No default export objects; every file exports a single function.
55
+
56
+ ---
57
+
58
+ ## 🚀 Interactive Browser Samples (`samples/v1/`)
59
+
60
+ Start the Vite development server to view and interact with real browser DOM events:
61
+
62
+ ```bash
63
+ npm install
64
+ npm run dev
65
+ ```
66
+
67
+ Open in Chrome or Edge:
68
+ - **`samples/v1/tableWithSearch/`** — Delegated table row clicks and live Enter-key search input.
69
+ - **`samples/v1/cardGrid/`** — Dynamic card insertion into DOM with instant delegated action buttons.
70
+ - **`samples/v1/formEvents/`** — Live form field inputs, dropdown selection changes, and submit/clear buttons.
71
+
72
+ ---
73
+
74
+ ## 📦 Usage
75
+
76
+ ### Browser (ES Module / Script)
77
+
78
+ ```html
79
+ <script type="module" src="./docs/dist/v1/min.js"></script>
80
+ <script type="module">
81
+ const bindings = [
82
+ {
83
+ event: "click",
84
+ selector: ".btn-cart",
85
+ runner: (target) => ({ productId: target.dataset.id }),
86
+ onExtract: (data) => console.log("Added to cart:", data)
87
+ }
88
+ ];
89
+
90
+ window.ks.domPrimitiveBindings.bindEvents("products-container", bindings);
91
+ </script>
92
+ ```
93
+
94
+ ### Module Import
95
+
96
+ ```javascript
97
+ import { bindEvents } from "dom-primitive-bindings";
98
+
99
+ bindEvents(containerElement, [
100
+ {
101
+ event: "click",
102
+ selector: ".action-btn",
103
+ runner: (btn) => btn.dataset.action,
104
+ onExtract: (action) => console.log("Action:", action)
105
+ }
106
+ ]);
107
+ ```
108
+
109
+ ---
110
+
111
+ ## 🧪 Testing & Build
112
+
113
+ ```bash
114
+ # Run unit tests
115
+ npm test
116
+
117
+ # Build production bundle to docs/dist/v1/min.js
118
+ npm run build
119
+ ```
120
+
121
+ ---
122
+
123
+ ## 📄 License
124
+
125
+ MIT © KeshavSoft
2
126
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
@@ -0,0 +1,63 @@
1
+ const u = {
2
+ name: "dom-primitive-bindings",
3
+ version: "1.0.0"
4
+ }, c = (t, n = u) => {
5
+ if (!(!t || typeof globalThis > "u"))
6
+ return globalThis.ks ?? (globalThis.ks = {}), globalThis.ks.domPrimitiveBindings = {
7
+ meta: n,
8
+ bindEvents: t
9
+ }, globalThis.ks.domPrimitiveBindings;
10
+ }, l = (t) => typeof t == "string" ? document.getElementById(t) || document.querySelector(t) : t && typeof t == "object" && "addEventListener" in t ? t : null, a = (t) => {
11
+ if (!t) return null;
12
+ const n = l(t);
13
+ return n || null;
14
+ }, f = (t) => Array.isArray(t) ? t.filter((n) => n && typeof n == "object" && typeof n.event == "string" && typeof n.selector == "string") : [], g = (t) => {
15
+ const n = f(t);
16
+ return n.length === 0 ? null : n;
17
+ }, p = (t, n) => {
18
+ const e = a(t);
19
+ if (!e) return null;
20
+ const r = g(n);
21
+ return r ? { container: e, bindings: r } : null;
22
+ }, d = (t) => {
23
+ const n = /* @__PURE__ */ new Map();
24
+ for (const e of t) {
25
+ const r = n.get(e.event) ?? [];
26
+ r.push(e), n.set(e.event, r);
27
+ }
28
+ return n;
29
+ }, y = (t) => d(t), m = (t, n, e) => {
30
+ var r;
31
+ return t.root === "event" || (r = n == null ? void 0 : n.type) != null && r.startsWith("key") ? n : e;
32
+ }, h = (t, n, e) => ({
33
+ ...t.clientData,
34
+ event: n,
35
+ targetElement: e
36
+ }), v = ({ binding: t, event: n, matchElement: e }) => {
37
+ const r = m(t, n, e), i = h(t, n, e), o = t.runner, s = typeof o == "function" ? o(r, t.instructions, i) : r;
38
+ return s != null && typeof t.onExtract == "function" && t.onExtract(s, n, e), s;
39
+ }, B = ({ bindings: t, event: n, container: e }) => {
40
+ var r, i;
41
+ for (const o of t) {
42
+ const s = (i = (r = n.target) == null ? void 0 : r.closest) == null ? void 0 : i.call(r, o.selector);
43
+ !s || !e.contains(s) || v({ binding: o, event: n, matchElement: s });
44
+ }
45
+ }, E = (t, n, e) => {
46
+ t.addEventListener(n, (r) => {
47
+ B({ bindings: e, event: r, container: t });
48
+ });
49
+ }, k = (t, n) => {
50
+ for (const [e, r] of n.entries())
51
+ E(t, e, r);
52
+ return t;
53
+ }, T = (t, n = []) => {
54
+ const e = p(t, n);
55
+ if (!e) return null;
56
+ const { container: r, bindings: i } = e, o = y(i);
57
+ return k(r, o), r;
58
+ };
59
+ c(T);
60
+ export {
61
+ T as bindEvents,
62
+ T as default
63
+ };
@@ -0,0 +1,63 @@
1
+ const u = {
2
+ name: "dom-primitive-bindings",
3
+ version: "1.0.0"
4
+ }, c = (t, n = u) => {
5
+ if (!(!t || typeof globalThis > "u"))
6
+ return globalThis.ks ?? (globalThis.ks = {}), globalThis.ks.domPrimitiveBindings = {
7
+ meta: n,
8
+ bindEvents: t
9
+ }, globalThis.ks.domPrimitiveBindings;
10
+ }, l = (t) => typeof t == "string" ? document.getElementById(t) || document.querySelector(t) : t && typeof t == "object" && "addEventListener" in t ? t : null, a = (t) => {
11
+ if (!t) return null;
12
+ const n = l(t);
13
+ return n || null;
14
+ }, f = (t) => Array.isArray(t) ? t.filter((n) => n && typeof n == "object" && typeof n.event == "string" && typeof n.selector == "string") : [], g = (t) => {
15
+ const n = f(t);
16
+ return n.length === 0 ? null : n;
17
+ }, p = (t, n) => {
18
+ const e = a(t);
19
+ if (!e) return null;
20
+ const r = g(n);
21
+ return r ? { container: e, bindings: r } : null;
22
+ }, d = (t) => {
23
+ const n = /* @__PURE__ */ new Map();
24
+ for (const e of t) {
25
+ const r = n.get(e.event) ?? [];
26
+ r.push(e), n.set(e.event, r);
27
+ }
28
+ return n;
29
+ }, y = (t) => d(t), m = (t, n, e) => {
30
+ var r;
31
+ return t.root === "event" || (r = n == null ? void 0 : n.type) != null && r.startsWith("key") ? n : e;
32
+ }, h = (t, n, e) => ({
33
+ ...t.clientData,
34
+ event: n,
35
+ targetElement: e
36
+ }), v = ({ binding: t, event: n, matchElement: e }) => {
37
+ const r = m(t, n, e), i = h(t, n, e), o = t.runner, s = typeof o == "function" ? o(r, t.instructions, i) : r;
38
+ return s != null && typeof t.onExtract == "function" && t.onExtract(s, n, e), s;
39
+ }, B = ({ bindings: t, event: n, container: e }) => {
40
+ var r, i;
41
+ for (const o of t) {
42
+ const s = (i = (r = n.target) == null ? void 0 : r.closest) == null ? void 0 : i.call(r, o.selector);
43
+ !s || !e.contains(s) || v({ binding: o, event: n, matchElement: s });
44
+ }
45
+ }, E = (t, n, e) => {
46
+ t.addEventListener(n, (r) => {
47
+ B({ bindings: e, event: r, container: t });
48
+ });
49
+ }, k = (t, n) => {
50
+ for (const [e, r] of n.entries())
51
+ E(t, e, r);
52
+ return t;
53
+ }, T = (t, n = []) => {
54
+ const e = p(t, n);
55
+ if (!e) return null;
56
+ const { container: r, bindings: i } = e, o = y(i);
57
+ return k(r, o), r;
58
+ };
59
+ c(T);
60
+ export {
61
+ T as bindEvents,
62
+ T as default
63
+ };
@@ -0,0 +1,185 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <title>DOM Primitive Bindings (v1) — Architecture & Story</title>
8
+ <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
9
+ <link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css" rel="stylesheet">
10
+ <style>
11
+ body {
12
+ background-color: #f8fafc;
13
+ color: #334155;
14
+ font-family: system-ui, -apple-system, sans-serif;
15
+ line-height: 1.6;
16
+ }
17
+ .hero-banner {
18
+ background: linear-gradient(135deg, #065f46 0%, #047857 100%);
19
+ color: #ffffff;
20
+ padding: 56px 0;
21
+ margin-bottom: 40px;
22
+ }
23
+ .feature-card {
24
+ background: #ffffff;
25
+ border: 1px solid #e2e8f0;
26
+ border-radius: 12px;
27
+ padding: 24px;
28
+ height: 100%;
29
+ }
30
+ pre {
31
+ background-color: #0f172a;
32
+ color: #f1f5f9;
33
+ padding: 16px;
34
+ border-radius: 8px;
35
+ font-size: 0.9rem;
36
+ overflow-x: auto;
37
+ }
38
+ code:not(pre code) {
39
+ background-color: #e2e8f0;
40
+ color: #0f172a;
41
+ padding: 2px 6px;
42
+ border-radius: 4px;
43
+ font-size: 0.88em;
44
+ }
45
+ </style>
46
+ </head>
47
+
48
+ <body>
49
+
50
+ <header class="hero-banner shadow-sm">
51
+ <div class="container">
52
+ <div class="d-flex align-items-center justify-content-between flex-wrap gap-3">
53
+ <div>
54
+ <span class="badge bg-warning text-dark mb-2">v1 · Delegated Event Engine</span>
55
+ <h1 class="display-6 fw-bold mb-2">DOM Primitive Bindings</h1>
56
+ <p class="lead mb-0 text-white opacity-75">
57
+ Single-listener container delegation with zero overhead. Modern, declarative, and dynamic.
58
+ </p>
59
+ </div>
60
+ <div class="d-flex gap-2">
61
+ <a href="../samples/v1/index.html" class="btn btn-outline-light btn-sm">
62
+ <i class="bi bi-play-circle me-1"></i> Interactive Samples
63
+ </a>
64
+ <a href="../index.html" class="btn btn-light btn-sm text-dark">
65
+ <i class="bi bi-house me-1"></i> Home
66
+ </a>
67
+ </div>
68
+ </div>
69
+ </div>
70
+ </header>
71
+
72
+ <main class="container pb-5" style="max-width: 1000px;">
73
+
74
+ <!-- Section 1: The Core Mental Model -->
75
+ <section class="card shadow-sm border-0 p-4 mb-4">
76
+ <h2 class="h5 fw-bold text-dark mb-3">
77
+ <i class="bi bi-diagram-3-fill text-success me-2"></i> The Event Delegation Story
78
+ </h2>
79
+ <p>
80
+ Traditional web apps attach <code>addEventListener</code> to individual buttons, inputs, and rows. When rows are re-rendered, paginated, or filtered, listeners are either lost or accidentally duplicated, creating memory leaks and stale event handlers.
81
+ </p>
82
+ <p class="mb-0">
83
+ <strong>DOM Primitive Bindings</strong> inverts this pattern. Exactly <strong>one listener per event type</strong> is attached to the parent container (e.g. <code>#table-container</code> or <code>#form-container</code>). When an event occurs, it bubbles to the container, checks for matched selectors via <code>closest(selector)</code>, and dispatches to the registered runner. New DOM elements added at runtime work immediately with zero re-binding!
84
+ </p>
85
+ </section>
86
+
87
+ <!-- Section 2: Staged Architecture -->
88
+ <section class="card shadow-sm border-0 p-4 mb-4">
89
+ <h2 class="h5 fw-bold text-dark mb-3">
90
+ <i class="bi bi-layers-fill text-primary me-2"></i> Staged Architecture in <code>src/v1/</code>
91
+ </h2>
92
+
93
+ <div class="row g-3">
94
+ <div class="col-md-6">
95
+ <div class="feature-card">
96
+ <h6 class="fw-bold text-success mb-2">
97
+ <i class="bi bi-shield-check me-1"></i> 1. guards/
98
+ </h6>
99
+ <p class="small text-muted mb-0">
100
+ Validates inputs before any DOM mutation occurs. Separated into <code>container/</code> (validates first input) and <code>bindings/</code> (validates second input array).
101
+ </p>
102
+ </div>
103
+ </div>
104
+
105
+ <div class="col-md-6">
106
+ <div class="feature-card">
107
+ <h6 class="fw-bold text-info mb-2">
108
+ <i class="bi bi-collection me-1"></i> 2. prepare/
109
+ </h6>
110
+ <p class="small text-muted mb-0">
111
+ Groups bindings by event type into a <code>Map&lt;eventType, bindings[]&gt;</code>, ensuring only a single listener is registered per event type.
112
+ </p>
113
+ </div>
114
+ </div>
115
+
116
+ <div class="col-md-6">
117
+ <div class="feature-card">
118
+ <h6 class="fw-bold text-primary mb-2">
119
+ <i class="bi bi-earbuds me-1"></i> 3. listeners/
120
+ </h6>
121
+ <p class="small text-muted mb-0">
122
+ Attaches container event listeners that intercept bubbling browser events and pass them down into the dispatch pipeline.
123
+ </p>
124
+ </div>
125
+ </div>
126
+
127
+ <div class="col-md-6">
128
+ <div class="feature-card">
129
+ <h6 class="fw-bold text-warning mb-2">
130
+ <i class="bi bi-lightning-charge me-1"></i> 4. dispatch/
131
+ </h6>
132
+ <p class="small text-muted mb-0">
133
+ Finds the matching target via <code>closest(selector)</code>, resolves start inputs, executes the runner, and emits extracted data to <code>onExtract</code>.
134
+ </p>
135
+ </div>
136
+ </div>
137
+ </div>
138
+ </section>
139
+
140
+ <!-- Section 3: Browser API & Window.ks -->
141
+ <section class="card shadow-sm border-0 p-4 mb-4">
142
+ <h2 class="h5 fw-bold text-dark mb-2">
143
+ <i class="bi bi-globe2 text-info me-2"></i> Browser Usage via <code>window.ks</code>
144
+ </h2>
145
+ <p class="small text-muted mb-3">
146
+ Include the bundled distribution or ES module, then call <code>bindEvents</code>:
147
+ </p>
148
+
149
+ <pre class="mb-3"><code>&lt;script type="module" src="/docs/dist/v1/min.js"&gt;&lt;/script&gt;
150
+ &lt;script type="module"&gt;
151
+ const bindings = [
152
+ {
153
+ event: "click",
154
+ selector: ".btn-delete",
155
+ runner: (target) => ({ id: target.dataset.id }),
156
+ onExtract: (data) => console.log("Deleted:", data)
157
+ }
158
+ ];
159
+
160
+ window.ks.domPrimitiveBindings.bindEvents("container-id", bindings);
161
+ &lt;/script&gt;</code></pre>
162
+ </section>
163
+
164
+ <!-- Section 4: Design Rules & Zero Comments -->
165
+ <section class="card shadow-sm border-0 p-4">
166
+ <h2 class="h5 fw-bold text-dark mb-3">
167
+ <i class="bi bi-check2-circle text-success me-2"></i> Code Conventions
168
+ </h2>
169
+ <ul class="list-unstyled mb-0 small text-muted">
170
+ <li class="mb-2"><i class="bi bi-check-lg text-success me-2"></i><strong>Every folder has an <code>index.js</code></strong> file acting as its public gateway.</li>
171
+ <li class="mb-2"><i class="bi bi-check-lg text-success me-2"></i><strong>Single-function default export</strong> per file (no default export objects).</li>
172
+ <li class="mb-2"><i class="bi bi-check-lg text-success me-2"></i><strong>Zero comments in code:</strong> Folders and file names tell the story directly.</li>
173
+ <li><i class="bi bi-check-lg text-success me-2"></i><strong>Independent sibling package:</strong> Focused purely on event delegation without mixing extraction concerns.</li>
174
+ </ul>
175
+ </section>
176
+
177
+ </main>
178
+
179
+ <footer class="text-center text-muted py-4 border-top small">
180
+ DOM Primitive Bindings · Single Source of Truth: <code>v1</code> · MIT License
181
+ </footer>
182
+
183
+ </body>
184
+
185
+ </html>
package/package.json CHANGED
@@ -1,6 +1,36 @@
1
1
  {
2
2
  "name": "dom-primitive-bindings",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "1.0.0",
4
+ "description": "Declarative, instruction-driven DOM delegated event bindings",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "author": "KeshavSoft",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/keshavsoft/dom-primitive-bindings.git"
11
+ },
12
+ "bugs": {
13
+ "url": "https://github.com/keshavsoft/dom-primitive-bindings/issues"
14
+ },
15
+ "homepage": "https://github.com/keshavsoft/dom-primitive-bindings#readme",
16
+ "main": "./src/index.js",
17
+ "exports": {
18
+ ".": "./src/index.js"
19
+ },
20
+ "files": [
21
+ "src/index.js",
22
+ "src/v1",
23
+ "docs",
24
+ "README.md",
25
+ "LICENSE"
26
+ ],
27
+ "scripts": {
28
+ "dev": "vite",
29
+ "build": "vite build",
30
+ "preview": "vite preview",
31
+ "test": "node --test"
32
+ },
33
+ "devDependencies": {
34
+ "vite": "^6.4.1"
35
+ }
36
+ }
package/src/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export * from "./v1/index.js";
2
+ export { default } from "./v1/index.js";
3
+
@@ -0,0 +1,21 @@
1
+ import getStartInput from "./getStartInput.js";
2
+ import getClientData from "./getClientData.js";
3
+
4
+ const executeBinding = ({ binding, event, matchElement }) => {
5
+ const startInput = getStartInput(binding, event, matchElement);
6
+ const clientData = getClientData(binding, event, matchElement);
7
+ const runner = binding.runner;
8
+
9
+ const result = typeof runner === "function"
10
+ ? runner(startInput, binding.instructions, clientData)
11
+ : startInput;
12
+
13
+ if (result !== null && result !== undefined && typeof binding.onExtract === "function") {
14
+ binding.onExtract(result, event, matchElement);
15
+ }
16
+
17
+ return result;
18
+ };
19
+
20
+ export default executeBinding;
21
+
@@ -0,0 +1,8 @@
1
+ const getClientData = (binding, event, matchElement) => ({
2
+ ...binding.clientData,
3
+ event,
4
+ targetElement: matchElement
5
+ });
6
+
7
+ export default getClientData;
8
+
@@ -0,0 +1,10 @@
1
+ const getStartInput = (binding, event, matchElement) => {
2
+ if (binding.root === "event" || event?.type?.startsWith("key")) {
3
+ return event;
4
+ }
5
+
6
+ return matchElement;
7
+ };
8
+
9
+ export default getStartInput;
10
+
@@ -0,0 +1,15 @@
1
+ import executeBinding from "./executeBinding.js";
2
+
3
+ const dispatchEvent = ({ bindings, event, container }) => {
4
+ for (const binding of bindings) {
5
+ const matchElement = event.target?.closest?.(binding.selector);
6
+ if (!matchElement || !container.contains(matchElement)) {
7
+ continue;
8
+ }
9
+
10
+ executeBinding({ binding, event, matchElement });
11
+ }
12
+ };
13
+
14
+ export default dispatchEvent;
15
+
@@ -0,0 +1,11 @@
1
+ import validateArray from "./validateArray.js";
2
+
3
+ const checkBindings = (bindings) => {
4
+ const valid = validateArray(bindings);
5
+ if (valid.length === 0) return null;
6
+
7
+ return valid;
8
+ };
9
+
10
+ export default checkBindings;
11
+
@@ -0,0 +1,10 @@
1
+ const validateArray = (bindings) => {
2
+ if (!Array.isArray(bindings)) return [];
3
+
4
+ return bindings.filter((binding) => (
5
+ binding && typeof binding === "object" && typeof binding.event === "string" && typeof binding.selector === "string"
6
+ ));
7
+ };
8
+
9
+ export default validateArray;
10
+
@@ -0,0 +1,13 @@
1
+ import resolveElement from "./resolveElement.js";
2
+
3
+ const checkContainer = (containerOrId) => {
4
+ if (!containerOrId) return null;
5
+
6
+ const element = resolveElement(containerOrId);
7
+ if (!element) return null;
8
+
9
+ return element;
10
+ };
11
+
12
+ export default checkContainer;
13
+
@@ -0,0 +1,14 @@
1
+ const resolveElement = (containerOrId) => {
2
+ if (typeof containerOrId === "string") {
3
+ return document.getElementById(containerOrId) || document.querySelector(containerOrId);
4
+ }
5
+
6
+ if (containerOrId && typeof containerOrId === "object" && "addEventListener" in containerOrId) {
7
+ return containerOrId;
8
+ }
9
+
10
+ return null;
11
+ };
12
+
13
+ export default resolveElement;
14
+
@@ -0,0 +1,15 @@
1
+ import checkContainer from "./container/index.js";
2
+ import checkBindings from "./bindings/index.js";
3
+
4
+ const runGuards = (containerOrId, bindings) => {
5
+ const container = checkContainer(containerOrId);
6
+ if (!container) return null;
7
+
8
+ const validBindings = checkBindings(bindings);
9
+ if (!validBindings) return null;
10
+
11
+ return { container, bindings: validBindings };
12
+ };
13
+
14
+ export default runGuards;
15
+
@@ -0,0 +1,22 @@
1
+ import registerGlobal from "./register/index.js";
2
+ import runGuards from "./guards/index.js";
3
+ import prepareBindings from "./prepare/index.js";
4
+ import attachListeners from "./listeners/index.js";
5
+
6
+ const bindEvents = (containerOrId, bindings = []) => {
7
+ const guarded = runGuards(containerOrId, bindings);
8
+ if (!guarded) return null;
9
+
10
+ const { container, bindings: validBindings } = guarded;
11
+ const eventGroups = prepareBindings(validBindings);
12
+
13
+ attachListeners(container, eventGroups);
14
+
15
+ return container;
16
+ };
17
+
18
+ export { bindEvents };
19
+ export default bindEvents;
20
+
21
+ registerGlobal(bindEvents);
22
+
@@ -0,0 +1,10 @@
1
+ import dispatchEvent from "../dispatch/index.js";
2
+
3
+ const attachListener = (container, eventType, bindings) => {
4
+ container.addEventListener(eventType, (event) => {
5
+ dispatchEvent({ bindings, event, container });
6
+ });
7
+ };
8
+
9
+ export default attachListener;
10
+
@@ -0,0 +1,12 @@
1
+ import attachListener from "./attachListener.js";
2
+
3
+ const attachListeners = (container, eventGroups) => {
4
+ for (const [eventType, bindings] of eventGroups.entries()) {
5
+ attachListener(container, eventType, bindings);
6
+ }
7
+
8
+ return container;
9
+ };
10
+
11
+ export default attachListeners;
12
+
@@ -0,0 +1,14 @@
1
+ const groupBindings = (bindings) => {
2
+ const groups = new Map();
3
+
4
+ for (const binding of bindings) {
5
+ const list = groups.get(binding.event) ?? [];
6
+ list.push(binding);
7
+ groups.set(binding.event, list);
8
+ }
9
+
10
+ return groups;
11
+ };
12
+
13
+ export default groupBindings;
14
+
@@ -0,0 +1,8 @@
1
+ import groupBindings from "./groupBindings.js";
2
+
3
+ const prepareBindings = (bindings) => {
4
+ return groupBindings(bindings);
5
+ };
6
+
7
+ export default prepareBindings;
8
+
@@ -0,0 +1,17 @@
1
+ import meta from "./meta.js";
2
+
3
+ const registerGlobal = (bindEvents, metadata = meta) => {
4
+ if (!bindEvents || typeof globalThis === "undefined") return;
5
+
6
+ globalThis.ks ??= {};
7
+
8
+ globalThis.ks.domPrimitiveBindings = {
9
+ meta: metadata,
10
+ bindEvents
11
+ };
12
+
13
+ return globalThis.ks.domPrimitiveBindings;
14
+ };
15
+
16
+ export default registerGlobal;
17
+
@@ -0,0 +1,7 @@
1
+ export const meta = {
2
+ name: "dom-primitive-bindings",
3
+ version: "1.0.0"
4
+ };
5
+
6
+ export default meta;
7
+