@excom/listenable-element 0.1.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/.rush/temp/chunked-rush-logs/listenable-element.apply-exports.chunks.jsonl +1 -0
- package/.rush/temp/chunked-rush-logs/listenable-element.build_docs.chunks.jsonl +1 -0
- package/.rush/temp/chunked-rush-logs/listenable-element.build_package-metas.chunks.jsonl +1 -0
- package/.rush/temp/operation/apply-exports/all.log +1 -0
- package/.rush/temp/operation/apply-exports/log-chunks.jsonl +1 -0
- package/.rush/temp/operation/apply-exports/state.json +3 -0
- package/.rush/temp/operation/build_docs/all.log +1 -0
- package/.rush/temp/operation/build_docs/log-chunks.jsonl +1 -0
- package/.rush/temp/operation/build_docs/state.json +3 -0
- package/.rush/temp/operation/build_package-metas/all.log +1 -0
- package/.rush/temp/operation/build_package-metas/log-chunks.jsonl +1 -0
- package/.rush/temp/operation/build_package-metas/state.json +3 -0
- package/.rush/temp/shrinkwrap-deps.json +4 -0
- package/config/rig.json +6 -0
- package/index.ts +282 -0
- package/package.json +45 -0
- package/rush-logs/listenable-element.apply-exports.cache.log +1 -0
- package/rush-logs/listenable-element.apply-exports.log +1 -0
- package/rush-logs/listenable-element.build_docs.cache.log +1 -0
- package/rush-logs/listenable-element.build_docs.log +1 -0
- package/rush-logs/listenable-element.build_package-metas.cache.log +1 -0
- package/rush-logs/listenable-element.build_package-metas.log +1 -0
- package/src/index.css +12 -0
- package/support/custom-elements.json +332 -0
- package/support/dist-docs/listenable-element.md +122 -0
- package/support/docs/README.md +82 -0
- package/support/package-meta.json +148 -0
- package/support/tests/listenable-element.matrix.test.ts +300 -0
- package/support/tests/listenable-element.test.ts +455 -0
- package/tsconfig.json +5 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"kind":"O","text":"Invoking: cd \"$RUSH_PROJECT_FOLDER\" && node ../heft-rig/scripts/apply-exports.mjs \n"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"kind":"O","text":"Invoking: node node_modules/@excom/heft-rig/scripts/build-docs.mjs \n"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"kind":"O","text":"Invoking: node node_modules/@excom/heft-rig/scripts/build-package-metas.mjs \n"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Invoking: cd "$RUSH_PROJECT_FOLDER" && node ../heft-rig/scripts/apply-exports.mjs
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"kind":"O","text":"Invoking: cd \"$RUSH_PROJECT_FOLDER\" && node ../heft-rig/scripts/apply-exports.mjs \n"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Invoking: node node_modules/@excom/heft-rig/scripts/build-docs.mjs
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"kind":"O","text":"Invoking: node node_modules/@excom/heft-rig/scripts/build-docs.mjs \n"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Invoking: node node_modules/@excom/heft-rig/scripts/build-package-metas.mjs
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"kind":"O","text":"Invoking: node node_modules/@excom/heft-rig/scripts/build-package-metas.mjs \n"}
|
package/config/rig.json
ADDED
package/index.ts
ADDED
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
import { selectOne } from "../kit-utils/dom";
|
|
2
|
+
import { Neutron, TokenList } from "@excom/neutron";
|
|
3
|
+
import { debounce } from "throttle-debounce";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Composition base that wires event and lifecycle listening. Subclasses implement `actionHandler`. Used by `<spa-a>`, `<event-handler>`, and similar interactive elements.
|
|
7
|
+
*
|
|
8
|
+
* @summary Declarative event / lifecycle listening for Neutron elements.
|
|
9
|
+
*/
|
|
10
|
+
export const ListenableElement = Neutron({
|
|
11
|
+
tag: "noop-tag",
|
|
12
|
+
props: {
|
|
13
|
+
/**
|
|
14
|
+
* @option
|
|
15
|
+
* Listen on another element / `window` / `document` — e.g. Escape to
|
|
16
|
+
* dismiss a dialog from a global `keydown`. Defaults to `:scope`. Used
|
|
17
|
+
* with `listen-for`. Not compatible with `listen-for-lifecycle`. The
|
|
18
|
+
* selector MUST resolve when `host-ref` is set — it will not wait for a
|
|
19
|
+
* match to appear.
|
|
20
|
+
* @values <CSS Selector> | window | document | html | body | head
|
|
21
|
+
*/
|
|
22
|
+
hostRef: String,
|
|
23
|
+
/**
|
|
24
|
+
* @option
|
|
25
|
+
* Space-separated event names to listen for. Defaults to `click` when unset (and no lifecycle list is set).
|
|
26
|
+
* @values <EventName>…
|
|
27
|
+
*/
|
|
28
|
+
listenFor: TokenList,
|
|
29
|
+
/**
|
|
30
|
+
* @option
|
|
31
|
+
* Space-separated element lifecycles to handle.
|
|
32
|
+
* @values connected | disconnected | adopted
|
|
33
|
+
*/
|
|
34
|
+
listenForLifecycle: TokenList,
|
|
35
|
+
/**
|
|
36
|
+
* @option
|
|
37
|
+
* Handle each distinct event name / lifecycle at most once.
|
|
38
|
+
*/
|
|
39
|
+
listenOnce: Boolean,
|
|
40
|
+
// TODO support :scope in the selector below
|
|
41
|
+
/**
|
|
42
|
+
* @option
|
|
43
|
+
* Only handle events whose `event.target` matches this CSS selector. Does not support `:scope` in the selector.
|
|
44
|
+
* @values <CSS Selector>
|
|
45
|
+
*/
|
|
46
|
+
selectorFilter: String,
|
|
47
|
+
/**
|
|
48
|
+
* @option
|
|
49
|
+
* Space-separated key filters (OR). Join modifiers with `+` (AND, any
|
|
50
|
+
* order): `shift+k tab` → Shift+K or Tab. Modifiers: `shift`, `alt`,
|
|
51
|
+
* `ctrl`/`control`, `meta`. Case-insensitive.
|
|
52
|
+
* @values <key|mod+key>…
|
|
53
|
+
*/
|
|
54
|
+
keycodeFilter: TokenList,
|
|
55
|
+
/**
|
|
56
|
+
* @option
|
|
57
|
+
* Only handle when `location.pathname` is one of these values — route-aware behaviors without a separate router element.
|
|
58
|
+
* @values <pathname>…
|
|
59
|
+
*/
|
|
60
|
+
pathnameFilter: TokenList,
|
|
61
|
+
/**
|
|
62
|
+
* @option
|
|
63
|
+
* Call `preventDefault()` on matched events (ignored for lifecycles).
|
|
64
|
+
*/
|
|
65
|
+
preventDefault: Boolean,
|
|
66
|
+
/**
|
|
67
|
+
* @option
|
|
68
|
+
* Call `stopPropagation()` on matched events (ignored for lifecycles).
|
|
69
|
+
*/
|
|
70
|
+
stopPropagation: Boolean,
|
|
71
|
+
/**
|
|
72
|
+
* @option
|
|
73
|
+
* Call `stopImmediatePropagation()` on matched events (ignored for lifecycles).
|
|
74
|
+
*/
|
|
75
|
+
stopImmediatePropagation: Boolean,
|
|
76
|
+
/**
|
|
77
|
+
* @option
|
|
78
|
+
* Vibrate on handle (`navigator.vibrate`). Empty / `0` uses a 20ms pulse.
|
|
79
|
+
* @default 20 (when attribute is present with no value)
|
|
80
|
+
*/
|
|
81
|
+
vibrateMs: Number,
|
|
82
|
+
/**
|
|
83
|
+
* @option
|
|
84
|
+
* Delay handling by this many milliseconds.
|
|
85
|
+
*/
|
|
86
|
+
delayMs: Number,
|
|
87
|
+
/**
|
|
88
|
+
* @option
|
|
89
|
+
* With `delay-ms`, coalesce bursts into one trailing call (debounce).
|
|
90
|
+
*/
|
|
91
|
+
isDebounced: Boolean,
|
|
92
|
+
// Subclass hooks, not attributes (the analyzer skips Function-typed props).
|
|
93
|
+
debouncedActionHandler: Function,
|
|
94
|
+
actionHandler: Function,
|
|
95
|
+
// private state
|
|
96
|
+
_lifecycleTracker: {
|
|
97
|
+
type: Array<"connected" | "disconnected" | "adopted">,
|
|
98
|
+
defaultValue: () => [] as Array<"connected" | "disconnected" | "adopted">,
|
|
99
|
+
},
|
|
100
|
+
_hostElement: {
|
|
101
|
+
// EventTarget (not Element) so `window` / `document` are allowed.
|
|
102
|
+
type: EventTarget,
|
|
103
|
+
store: "weak",
|
|
104
|
+
notify: "prop",
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
})
|
|
108
|
+
.defineMethods({
|
|
109
|
+
handleEvent: (
|
|
110
|
+
element,
|
|
111
|
+
e: Event | "connected" | "disconnected" | "adopted"
|
|
112
|
+
) => {
|
|
113
|
+
const {
|
|
114
|
+
preventDefault,
|
|
115
|
+
stopPropagation,
|
|
116
|
+
stopImmediatePropagation,
|
|
117
|
+
vibrateMs,
|
|
118
|
+
} = element;
|
|
119
|
+
const isLifecycle = typeof e === "string";
|
|
120
|
+
if (
|
|
121
|
+
isLifecycle ||
|
|
122
|
+
(matchesKeycode(element, e) &&
|
|
123
|
+
matchesSelector(element, e) &&
|
|
124
|
+
matchesPathname(element))
|
|
125
|
+
) {
|
|
126
|
+
if (!isLifecycle && preventDefault) {
|
|
127
|
+
e.preventDefault();
|
|
128
|
+
}
|
|
129
|
+
if (!isLifecycle && stopPropagation) {
|
|
130
|
+
e.stopPropagation();
|
|
131
|
+
}
|
|
132
|
+
if (!isLifecycle && stopImmediatePropagation) {
|
|
133
|
+
e.stopImmediatePropagation();
|
|
134
|
+
}
|
|
135
|
+
// Boolean or number. A boolean attr (`vibrate=""`) is 0, so default to 20.
|
|
136
|
+
if (typeof vibrateMs === "number") {
|
|
137
|
+
navigator.vibrate?.(vibrateMs || 20);
|
|
138
|
+
}
|
|
139
|
+
if (typeof element.delayMs === "number") {
|
|
140
|
+
if (element.debouncedActionHandler) {
|
|
141
|
+
element.debouncedActionHandler(e);
|
|
142
|
+
} else {
|
|
143
|
+
setTimeout(() => element.actionHandler?.(e), element.delayMs);
|
|
144
|
+
}
|
|
145
|
+
} else {
|
|
146
|
+
element.actionHandler?.(e);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
},
|
|
150
|
+
shouldLifecycleOnce: (
|
|
151
|
+
{ listenForLifecycle, listenOnce, _lifecycleTracker },
|
|
152
|
+
lc: "connected" | "adopted" | "disconnected"
|
|
153
|
+
) => {
|
|
154
|
+
const willRun =
|
|
155
|
+
listenForLifecycle?.includes(lc) &&
|
|
156
|
+
(!listenOnce || !_lifecycleTracker.includes(lc));
|
|
157
|
+
return [
|
|
158
|
+
willRun && {
|
|
159
|
+
// Mark this lifecycle as already run (`listen-once`)
|
|
160
|
+
_lifecycleTracker: [..._lifecycleTracker, lc],
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
returns: willRun,
|
|
164
|
+
},
|
|
165
|
+
];
|
|
166
|
+
},
|
|
167
|
+
})
|
|
168
|
+
.onConnected(
|
|
169
|
+
({ isMoving, shouldLifecycleOnce }) =>
|
|
170
|
+
!isMoving &&
|
|
171
|
+
shouldLifecycleOnce("connected") && { handleEvent: ["connected"] }
|
|
172
|
+
)
|
|
173
|
+
.onAdopted(
|
|
174
|
+
({ shouldLifecycleOnce }) =>
|
|
175
|
+
shouldLifecycleOnce("adopted") && { handleEvent: ["adopted"] }
|
|
176
|
+
)
|
|
177
|
+
.onDisconnected(
|
|
178
|
+
({ isMoving, shouldLifecycleOnce }) =>
|
|
179
|
+
!isMoving &&
|
|
180
|
+
// Don't dispatch after disconnect; the element is gone.
|
|
181
|
+
shouldLifecycleOnce("disconnected") && { handleEvent: ["disconnected"] }
|
|
182
|
+
)
|
|
183
|
+
.onPropChanged("hostRef", (element) => {
|
|
184
|
+
if (!element.hostRef) return { _hostElement: null };
|
|
185
|
+
const hostElement = selectOne(element.hostRef, {
|
|
186
|
+
scope: element,
|
|
187
|
+
enableRootRefs: true,
|
|
188
|
+
});
|
|
189
|
+
if (!hostElement)
|
|
190
|
+
throw new Error(`Host element not found for ${element.hostRef}`);
|
|
191
|
+
return { _hostElement: hostElement };
|
|
192
|
+
})
|
|
193
|
+
.onPropChanged(
|
|
194
|
+
["_hostElement", "listenFor"],
|
|
195
|
+
({ _hostElement, listenFor, handleEvent, listenOnce }, previous) => {
|
|
196
|
+
// `previous` only has changed props; fall back to current values.
|
|
197
|
+
const removeHost =
|
|
198
|
+
previous && "_hostElement" in previous
|
|
199
|
+
? previous._hostElement
|
|
200
|
+
: _hostElement;
|
|
201
|
+
const removeArgs = removeHost ? { target: removeHost } : {};
|
|
202
|
+
const addArgs = _hostElement ? { target: _hostElement } : {};
|
|
203
|
+
const prevEvents =
|
|
204
|
+
previous && "listenFor" in previous
|
|
205
|
+
? previous.listenFor?.length
|
|
206
|
+
? previous.listenFor
|
|
207
|
+
: ["click"]
|
|
208
|
+
: listenFor?.length
|
|
209
|
+
? listenFor
|
|
210
|
+
: ["click"];
|
|
211
|
+
const nextEvents = listenFor?.length ? listenFor : ["click"];
|
|
212
|
+
return [
|
|
213
|
+
{
|
|
214
|
+
removeListeners: prevEvents.map((k) => [k, handleEvent, removeArgs]),
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
addListeners: nextEvents.map((k) => [
|
|
218
|
+
k,
|
|
219
|
+
handleEvent,
|
|
220
|
+
listenOnce ? { once: true, ...addArgs } : addArgs,
|
|
221
|
+
]),
|
|
222
|
+
},
|
|
223
|
+
];
|
|
224
|
+
}
|
|
225
|
+
)
|
|
226
|
+
.onPropChanged("isDebounced", ({ isDebounced, delayMs, actionHandler }) => {
|
|
227
|
+
return {
|
|
228
|
+
debouncedActionHandler: isDebounced
|
|
229
|
+
? debounce(delayMs || 10, actionHandler)
|
|
230
|
+
: null,
|
|
231
|
+
};
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
const KEY_MODIFIERS: Record<string, (e: KeyboardEvent) => boolean> = {
|
|
235
|
+
shift: (e) => e.shiftKey,
|
|
236
|
+
alt: (e) => e.altKey,
|
|
237
|
+
ctrl: (e) => e.ctrlKey,
|
|
238
|
+
control: (e) => e.ctrlKey,
|
|
239
|
+
meta: (e) => e.metaKey,
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
function modifierKeyName(mod: string): string {
|
|
243
|
+
return mod === "ctrl" || mod === "control" ? "control" : mod;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/** One filter token: `k`, `shift`, or `shift+k` / `k+shift`. */
|
|
247
|
+
function matchesKeycodeToken(token: string, e: KeyboardEvent): boolean {
|
|
248
|
+
const parts = token.toLowerCase().split("+").filter(Boolean);
|
|
249
|
+
if (!parts.length) return false;
|
|
250
|
+
|
|
251
|
+
const mods = parts.filter((p) => p in KEY_MODIFIERS);
|
|
252
|
+
const keys = parts.filter((p) => !(p in KEY_MODIFIERS));
|
|
253
|
+
if (!mods.every((m) => KEY_MODIFIERS[m](e))) return false;
|
|
254
|
+
|
|
255
|
+
const key = e.key?.toLowerCase();
|
|
256
|
+
if (!key) return false;
|
|
257
|
+
if (keys.length) return keys.every((k) => key === k);
|
|
258
|
+
// Modifier-only token (e.g. `shift`): match that modifier keydown.
|
|
259
|
+
return mods.some((m) => key === modifierKeyName(m));
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function matchesKeycode(element: any, e: Event) {
|
|
263
|
+
if (!element.keycodeFilter?.length) return true;
|
|
264
|
+
const ke = e as KeyboardEvent;
|
|
265
|
+
return [...element.keycodeFilter].some((token: string) =>
|
|
266
|
+
matchesKeycodeToken(token, ke)
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function matchesSelector(element: any, e: Event) {
|
|
271
|
+
return (
|
|
272
|
+
!element.selectorFilter ||
|
|
273
|
+
(e.target as HTMLElement).matches(element.selectorFilter)
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function matchesPathname(element: any) {
|
|
278
|
+
return (
|
|
279
|
+
!element.pathnameFilter?.length ||
|
|
280
|
+
element.pathnameFilter.includes(location.pathname)
|
|
281
|
+
);
|
|
282
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@excom/listenable-element",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "ListenableElement base for Neutron elements",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"engines": {
|
|
7
|
+
"node": ">=24.13.0"
|
|
8
|
+
},
|
|
9
|
+
"type": "module",
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"throttle-debounce": "^5.0.2",
|
|
12
|
+
"@excom/neutron": "^0.1.0"
|
|
13
|
+
},
|
|
14
|
+
"peerDependencies": {},
|
|
15
|
+
"devDependencies": {
|
|
16
|
+
"@excom/heft-rig": "^0.1.0"
|
|
17
|
+
},
|
|
18
|
+
"repository": {
|
|
19
|
+
"url": "excom-dev/nucleus",
|
|
20
|
+
"directory": "packages/listenable-element"
|
|
21
|
+
},
|
|
22
|
+
"homepage": "https://github.com/excom-dev/nucleus/tree/main/packages/listenable-element/support/docs/README.md",
|
|
23
|
+
"bugs": "https://github.com/excom-dev/nucleus/issues",
|
|
24
|
+
"keywords": [
|
|
25
|
+
"ListenableElement",
|
|
26
|
+
"listenable-element",
|
|
27
|
+
"neutron",
|
|
28
|
+
"kit-element-base",
|
|
29
|
+
"custom-elements"
|
|
30
|
+
],
|
|
31
|
+
"excom": {
|
|
32
|
+
"packageType": "element-base"
|
|
33
|
+
},
|
|
34
|
+
"scripts": {
|
|
35
|
+
"build": "node node_modules/@excom/heft-rig/scripts/vite-build.mjs",
|
|
36
|
+
"build:watch": "node node_modules/@excom/heft-rig/scripts/vite-build-watch.mjs",
|
|
37
|
+
"format": "node node_modules/@excom/heft-rig/scripts/format.mjs",
|
|
38
|
+
"test": "node node_modules/@excom/heft-rig/scripts/vitest.mjs",
|
|
39
|
+
"coverage": "node node_modules/@excom/heft-rig/scripts/coverage.mjs",
|
|
40
|
+
"dev": "node node_modules/@excom/heft-rig/scripts/vite-dev.mjs",
|
|
41
|
+
"preview": "node node_modules/@excom/heft-rig/scripts/vite-preview.mjs",
|
|
42
|
+
"build:package-metas": "node node_modules/@excom/heft-rig/scripts/build-package-metas.mjs",
|
|
43
|
+
"build:docs": "node node_modules/@excom/heft-rig/scripts/build-docs.mjs"
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Caching has been disabled for this project's "apply-exports" command.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Invoking: cd "$RUSH_PROJECT_FOLDER" && node ../heft-rig/scripts/apply-exports.mjs
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
This project does not define the caching behavior of the "build:docs" command, so caching has been disabled.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Invoking: node node_modules/@excom/heft-rig/scripts/build-docs.mjs
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
This project does not define the caching behavior of the "build:package-metas" command, so caching has been disabled.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Invoking: node node_modules/@excom/heft-rig/scripts/build-package-metas.mjs
|
package/src/index.css
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pointer affordance when listening for click / mouse (or default click).
|
|
3
|
+
* @element noop-tag
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
@define-mixin listenable-element {
|
|
7
|
+
&[listen-for*="click"],
|
|
8
|
+
&[listen-for*="mouse"],
|
|
9
|
+
&:not([listen-for]):not([listen-for-lifecycle]) {
|
|
10
|
+
cursor: pointer;
|
|
11
|
+
}
|
|
12
|
+
}
|