@hypen-space/web 0.2.12 → 0.3.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/dist/src/dom/applicators/effects.js +38 -2
- package/dist/src/dom/applicators/effects.js.map +3 -3
- package/dist/src/dom/applicators/events.js +280 -397
- package/dist/src/dom/applicators/events.js.map +5 -4
- package/dist/src/dom/applicators/font.js +94 -5
- package/dist/src/dom/applicators/font.js.map +3 -3
- package/dist/src/dom/applicators/index.js +590 -425
- package/dist/src/dom/applicators/index.js.map +10 -9
- package/dist/src/dom/applicators/layout.js +33 -5
- package/dist/src/dom/applicators/layout.js.map +3 -3
- package/dist/src/dom/applicators/size.js +81 -16
- package/dist/src/dom/applicators/size.js.map +3 -3
- package/dist/src/dom/components/hypenapp.js +296 -0
- package/dist/src/dom/components/hypenapp.js.map +10 -0
- package/dist/src/dom/components/index.js +263 -1
- package/dist/src/dom/components/index.js.map +5 -4
- package/dist/src/dom/element-data.js +140 -0
- package/dist/src/dom/element-data.js.map +10 -0
- package/dist/src/dom/index.js +857 -430
- package/dist/src/dom/index.js.map +13 -11
- package/dist/src/dom/renderer.js +857 -430
- package/dist/src/dom/renderer.js.map +13 -11
- package/dist/src/hypen.js +857 -430
- package/dist/src/hypen.js.map +13 -11
- package/dist/src/index.js +862 -430
- package/dist/src/index.js.map +15 -12
- package/package.json +3 -3
- package/src/canvas/QUICKSTART.md +2 -4
- package/src/dom/applicators/effects.ts +45 -1
- package/src/dom/applicators/events.ts +348 -537
- package/src/dom/applicators/font.ts +127 -2
- package/src/dom/applicators/index.ts +117 -7
- package/src/dom/applicators/layout.ts +40 -4
- package/src/dom/applicators/size.ts +101 -16
- package/src/dom/components/hypenapp.ts +348 -0
- package/src/dom/components/index.ts +2 -0
- package/src/dom/element-data.ts +234 -0
- package/src/dom/renderer.ts +8 -5
- package/src/index.ts +3 -0
|
@@ -1,14 +1,55 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Event Applicators
|
|
3
|
-
*
|
|
3
|
+
*
|
|
4
4
|
* Handles event applicators like onClick, onPress, etc.
|
|
5
|
+
* Uses a factory pattern to reduce boilerplate and ensure consistency.
|
|
5
6
|
*/
|
|
6
7
|
|
|
7
8
|
import type { ApplicatorHandler } from "./index.js";
|
|
9
|
+
import {
|
|
10
|
+
getElementDisposables,
|
|
11
|
+
disposableListener,
|
|
12
|
+
disposableTimeout,
|
|
13
|
+
type Disposable,
|
|
14
|
+
} from "@hypen/core";
|
|
15
|
+
import {
|
|
16
|
+
type IEngine,
|
|
17
|
+
getEngine,
|
|
18
|
+
getRegisteredEvents,
|
|
19
|
+
registerEvent,
|
|
20
|
+
unregisterEvent,
|
|
21
|
+
getKeyTarget,
|
|
22
|
+
setKeyTarget,
|
|
23
|
+
} from "../element-data.js";
|
|
24
|
+
|
|
25
|
+
// ============================================================================
|
|
26
|
+
// Types
|
|
27
|
+
// ============================================================================
|
|
28
|
+
|
|
29
|
+
interface EventHandlerOptions {
|
|
30
|
+
/** Custom payload extractor for this event type */
|
|
31
|
+
extractPayload?: (event: Event, element: HTMLElement) => Record<string, unknown>;
|
|
32
|
+
/** Throttle events to max one per N milliseconds */
|
|
33
|
+
throttleMs?: number;
|
|
34
|
+
/** Prevent default behavior */
|
|
35
|
+
preventDefault?: boolean;
|
|
36
|
+
/** Use passive listener (for scroll, touch) */
|
|
37
|
+
passive?: boolean;
|
|
38
|
+
/** Key to listen for (keyboard events) */
|
|
39
|
+
key?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
// ============================================================================
|
|
44
|
+
// Utility Functions
|
|
45
|
+
// ============================================================================
|
|
8
46
|
|
|
9
|
-
|
|
47
|
+
/**
|
|
48
|
+
* Convert Map or nested objects to plain objects
|
|
49
|
+
*/
|
|
50
|
+
function toPlainObject(value: unknown): unknown {
|
|
10
51
|
if (value instanceof Map) {
|
|
11
|
-
const obj: Record<string,
|
|
52
|
+
const obj: Record<string, unknown> = {};
|
|
12
53
|
for (const [key, val] of value.entries()) {
|
|
13
54
|
obj[key] = toPlainObject(val);
|
|
14
55
|
}
|
|
@@ -20,7 +61,7 @@ function toPlainObject(value: any): any {
|
|
|
20
61
|
}
|
|
21
62
|
|
|
22
63
|
if (value && typeof value === "object") {
|
|
23
|
-
const obj: Record<string,
|
|
64
|
+
const obj: Record<string, unknown> = {};
|
|
24
65
|
for (const [key, val] of Object.entries(value)) {
|
|
25
66
|
obj[key] = toPlainObject(val);
|
|
26
67
|
}
|
|
@@ -30,7 +71,14 @@ function toPlainObject(value: any): any {
|
|
|
30
71
|
return value;
|
|
31
72
|
}
|
|
32
73
|
|
|
33
|
-
|
|
74
|
+
/**
|
|
75
|
+
* Extract action name and custom payload from an applicator value
|
|
76
|
+
*/
|
|
77
|
+
function extractActionDetails(value: unknown): {
|
|
78
|
+
actionName: string | null;
|
|
79
|
+
payload: Record<string, unknown>;
|
|
80
|
+
} {
|
|
81
|
+
// String format: "@actions.doSomething" or "@doSomething"
|
|
34
82
|
if (typeof value === "string") {
|
|
35
83
|
if (!value.startsWith("@")) {
|
|
36
84
|
return { actionName: null, payload: {} };
|
|
@@ -43,9 +91,10 @@ function extractActionDetails(value: any): { actionName: string | null; payload:
|
|
|
43
91
|
return { actionName, payload: {} };
|
|
44
92
|
}
|
|
45
93
|
|
|
94
|
+
// Object format: { "0": "@actions.doSomething", "customKey": "value" }
|
|
46
95
|
if (value && typeof value === "object") {
|
|
47
|
-
const plain = toPlainObject(value)
|
|
48
|
-
const payload: Record<string,
|
|
96
|
+
const plain = toPlainObject(value) as Record<string, unknown>;
|
|
97
|
+
const payload: Record<string, unknown> = {};
|
|
49
98
|
let actionName: string | null = null;
|
|
50
99
|
|
|
51
100
|
if (plain && typeof plain === "object") {
|
|
@@ -59,7 +108,17 @@ function extractActionDetails(value: any): { actionName: string | null; payload:
|
|
|
59
108
|
|
|
60
109
|
for (const [key, val] of Object.entries(plain)) {
|
|
61
110
|
if (key !== "0") {
|
|
62
|
-
|
|
111
|
+
// If the key is numeric (like "1", "2") and the value is an object,
|
|
112
|
+
// merge the object's keys into the payload directly.
|
|
113
|
+
// This handles: .onClick("@actions.foo", { id: "123" })
|
|
114
|
+
// where the second positional arg becomes "1": { id: "123" }
|
|
115
|
+
if (/^\d+$/.test(key) && val && typeof val === "object" && !Array.isArray(val)) {
|
|
116
|
+
for (const [innerKey, innerVal] of Object.entries(val)) {
|
|
117
|
+
payload[innerKey] = innerVal;
|
|
118
|
+
}
|
|
119
|
+
} else {
|
|
120
|
+
payload[key] = val;
|
|
121
|
+
}
|
|
63
122
|
}
|
|
64
123
|
}
|
|
65
124
|
}
|
|
@@ -70,620 +129,372 @@ function extractActionDetails(value: any): { actionName: string | null; payload:
|
|
|
70
129
|
return { actionName: null, payload: {} };
|
|
71
130
|
}
|
|
72
131
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
132
|
+
/**
|
|
133
|
+
* Extract relevant data from a DOM event
|
|
134
|
+
*/
|
|
135
|
+
function extractEventData(event: Event, element: HTMLElement): Record<string, unknown> {
|
|
136
|
+
const data: Record<string, unknown> = {
|
|
137
|
+
type: event.type,
|
|
138
|
+
timestamp: Date.now(),
|
|
139
|
+
};
|
|
76
140
|
|
|
77
|
-
|
|
141
|
+
// Mouse events
|
|
142
|
+
if (event instanceof MouseEvent) {
|
|
143
|
+
data.clientX = event.clientX;
|
|
144
|
+
data.clientY = event.clientY;
|
|
145
|
+
data.button = event.button;
|
|
146
|
+
}
|
|
78
147
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
148
|
+
// Keyboard events
|
|
149
|
+
if (event instanceof KeyboardEvent) {
|
|
150
|
+
data.key = event.key;
|
|
151
|
+
data.code = event.code;
|
|
152
|
+
data.ctrlKey = event.ctrlKey;
|
|
153
|
+
data.shiftKey = event.shiftKey;
|
|
154
|
+
data.altKey = event.altKey;
|
|
155
|
+
data.metaKey = event.metaKey;
|
|
156
|
+
}
|
|
83
157
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
}
|
|
158
|
+
// Input element values
|
|
159
|
+
if (element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement) {
|
|
160
|
+
data.value = element.value;
|
|
161
|
+
}
|
|
89
162
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
163
|
+
// Select element values
|
|
164
|
+
if (element instanceof HTMLSelectElement) {
|
|
165
|
+
data.value = element.value;
|
|
166
|
+
data.selectedIndex = element.selectedIndex;
|
|
167
|
+
}
|
|
93
168
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
169
|
+
// Form data
|
|
170
|
+
if (event.type === "submit" && element instanceof HTMLFormElement) {
|
|
171
|
+
data.formData = new FormData(element);
|
|
172
|
+
}
|
|
98
173
|
|
|
99
|
-
|
|
174
|
+
return data;
|
|
175
|
+
}
|
|
100
176
|
|
|
101
|
-
// Dispatch action to engine
|
|
102
|
-
const engine = (element as any).__hypenEngine;
|
|
103
|
-
if (engine) {
|
|
104
|
-
engine.dispatchAction(actionName, payload);
|
|
105
|
-
} else {
|
|
106
|
-
console.warn(`[EventApplicator] No engine attached to element for onClick`);
|
|
107
|
-
}
|
|
108
|
-
};
|
|
109
177
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
178
|
+
/**
|
|
179
|
+
* Capitalize first letter of a string
|
|
180
|
+
*/
|
|
181
|
+
function capitalize(str: string): string {
|
|
182
|
+
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
183
|
+
}
|
|
113
184
|
|
|
114
|
-
|
|
115
|
-
|
|
185
|
+
// ============================================================================
|
|
186
|
+
// Event Handler Factory
|
|
187
|
+
// ============================================================================
|
|
116
188
|
|
|
117
|
-
onPress: (element, value) => {
|
|
118
|
-
// For now, treat onPress same as onClick
|
|
119
|
-
// In the future, this could handle touch events differently
|
|
120
|
-
eventHandlers.onClick(element, value);
|
|
121
|
-
},
|
|
122
189
|
|
|
123
|
-
|
|
124
|
-
|
|
190
|
+
/**
|
|
191
|
+
* Create an event handler applicator with common boilerplate
|
|
192
|
+
*/
|
|
193
|
+
function createEventHandler(
|
|
194
|
+
eventType: string,
|
|
195
|
+
options: EventHandlerOptions = {}
|
|
196
|
+
): ApplicatorHandler {
|
|
197
|
+
return (element: HTMLElement, value: unknown) => {
|
|
198
|
+
const { actionName, payload: customPayload } = extractActionDetails(value);
|
|
125
199
|
|
|
126
200
|
if (!actionName) {
|
|
127
|
-
console.warn(
|
|
201
|
+
console.warn(
|
|
202
|
+
`[EventApplicator] ${eventType} requires an action reference starting with @, got:`,
|
|
203
|
+
value
|
|
204
|
+
);
|
|
128
205
|
return;
|
|
129
206
|
}
|
|
130
207
|
|
|
131
|
-
const
|
|
132
|
-
if (existingListener) {
|
|
133
|
-
element.removeEventListener("change", existingListener);
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
const listener = (event: Event) => {
|
|
137
|
-
console.log(`🔥 [EventApplicator] onChange fired, dispatching action: ${actionName}`);
|
|
138
|
-
|
|
139
|
-
const payload = extractEventData(event, element);
|
|
140
|
-
|
|
141
|
-
const engine = (element as any).__hypenEngine;
|
|
142
|
-
if (engine) {
|
|
143
|
-
engine.dispatchAction(actionName, payload);
|
|
144
|
-
} else {
|
|
145
|
-
console.warn(`[EventApplicator] No engine attached to element for onChange`);
|
|
146
|
-
}
|
|
147
|
-
};
|
|
148
|
-
|
|
149
|
-
(element as any).__hypenChangeListener = listener;
|
|
150
|
-
element.addEventListener("change", listener);
|
|
208
|
+
const disposables = getElementDisposables(element);
|
|
151
209
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
if (!actionName) {
|
|
159
|
-
console.warn(`[EventApplicator] onSubmit value must be an action reference starting with @, got:`, value);
|
|
210
|
+
// Track that we've registered this event type
|
|
211
|
+
// The disposable stack handles cleanup automatically
|
|
212
|
+
const eventKey = `${eventType}:${actionName}`;
|
|
213
|
+
if (getRegisteredEvents(element).has(eventKey)) {
|
|
214
|
+
// Already registered - skip to avoid duplicates
|
|
215
|
+
// This can happen during re-renders
|
|
160
216
|
return;
|
|
161
217
|
}
|
|
218
|
+
registerEvent(element, eventKey);
|
|
162
219
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
element.removeEventListener("submit", existingListener);
|
|
166
|
-
}
|
|
220
|
+
// Create throttle state if needed
|
|
221
|
+
let throttleTimer: Disposable | null = null;
|
|
167
222
|
|
|
223
|
+
// Create the event listener
|
|
168
224
|
const listener = (event: Event) => {
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
const payload = extractEventData(event, element);
|
|
174
|
-
|
|
175
|
-
const engine = (element as any).__hypenEngine;
|
|
176
|
-
if (engine) {
|
|
177
|
-
engine.dispatchAction(actionName, payload);
|
|
178
|
-
} else {
|
|
179
|
-
console.warn(`[EventApplicator] No engine attached to element for onSubmit`);
|
|
225
|
+
// Handle throttling
|
|
226
|
+
if (options.throttleMs && throttleTimer) {
|
|
227
|
+
return;
|
|
180
228
|
}
|
|
181
|
-
};
|
|
182
|
-
|
|
183
|
-
(element as any).__hypenSubmitListener = listener;
|
|
184
|
-
element.addEventListener("submit", listener);
|
|
185
229
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
const { actionName } = extractActionDetails(value);
|
|
193
|
-
|
|
194
|
-
if (!actionName) {
|
|
195
|
-
console.warn(`[EventApplicator] onInput value must be an action reference starting with @, got:`, value);
|
|
196
|
-
return;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
const existingListener = (element as any).__hypenInputListener;
|
|
200
|
-
if (existingListener) {
|
|
201
|
-
element.removeEventListener("input", existingListener);
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
const listener = (event: Event) => {
|
|
205
|
-
console.log(`🔥 [EventApplicator] onInput fired, dispatching action: ${actionName}`);
|
|
230
|
+
if (options.throttleMs) {
|
|
231
|
+
throttleTimer = disposableTimeout(() => {
|
|
232
|
+
throttleTimer = null;
|
|
233
|
+
}, options.throttleMs);
|
|
234
|
+
}
|
|
206
235
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
value: target.value,
|
|
212
|
-
input: target.value,
|
|
213
|
-
};
|
|
236
|
+
// Handle preventDefault
|
|
237
|
+
if (options.preventDefault) {
|
|
238
|
+
event.preventDefault();
|
|
239
|
+
}
|
|
214
240
|
|
|
215
|
-
|
|
241
|
+
// Build payload
|
|
242
|
+
const payload =
|
|
243
|
+
Object.keys(customPayload).length > 0
|
|
244
|
+
? { ...customPayload }
|
|
245
|
+
: options.extractPayload
|
|
246
|
+
? options.extractPayload(event, element)
|
|
247
|
+
: extractEventData(event, element);
|
|
216
248
|
|
|
217
|
-
|
|
249
|
+
// Dispatch to engine
|
|
250
|
+
const engine = getEngine(element);
|
|
218
251
|
if (engine) {
|
|
219
252
|
engine.dispatchAction(actionName, payload);
|
|
220
|
-
} else {
|
|
221
|
-
console.warn(`[EventApplicator] No engine attached to element for onInput`);
|
|
222
253
|
}
|
|
223
254
|
};
|
|
224
255
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
// For now, we'll handle the simple case where onKey receives the action directly
|
|
239
|
-
// and triggers on Enter key by default
|
|
240
|
-
const { actionName } = extractActionDetails(value);
|
|
241
|
-
|
|
242
|
-
if (actionName) {
|
|
243
|
-
|
|
244
|
-
const existingListener = (element as any).__hypenKeyListener;
|
|
245
|
-
if (existingListener) {
|
|
246
|
-
element.removeEventListener("keydown", existingListener);
|
|
256
|
+
// Register the listener using disposable pattern
|
|
257
|
+
disposables.add(
|
|
258
|
+
disposableListener(element, eventType, listener, {
|
|
259
|
+
passive: options.passive,
|
|
260
|
+
})
|
|
261
|
+
);
|
|
262
|
+
|
|
263
|
+
// Clean up registered events tracking on dispose
|
|
264
|
+
disposables.addCallback(() => {
|
|
265
|
+
unregisterEvent(element, eventKey);
|
|
266
|
+
if (throttleTimer) {
|
|
267
|
+
throttleTimer.dispose();
|
|
247
268
|
}
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
if (event.key === "Enter") {
|
|
252
|
-
console.log(`🔥 [EventApplicator] onKey fired (Enter), dispatching action: ${actionName}`);
|
|
253
|
-
|
|
254
|
-
event.preventDefault();
|
|
255
|
-
|
|
256
|
-
const target = event.target as HTMLInputElement | HTMLTextAreaElement;
|
|
257
|
-
const payload = {
|
|
258
|
-
type: event.type,
|
|
259
|
-
timestamp: Date.now(),
|
|
260
|
-
key: event.key,
|
|
261
|
-
code: event.code,
|
|
262
|
-
value: target.value,
|
|
263
|
-
input: target.value,
|
|
264
|
-
ctrlKey: event.ctrlKey,
|
|
265
|
-
shiftKey: event.shiftKey,
|
|
266
|
-
altKey: event.altKey,
|
|
267
|
-
metaKey: event.metaKey,
|
|
268
|
-
};
|
|
269
|
-
|
|
270
|
-
const engine = (element as any).__hypenEngine;
|
|
271
|
-
if (engine) {
|
|
272
|
-
engine.dispatchAction(actionName, payload);
|
|
273
|
-
} else {
|
|
274
|
-
console.warn(`[EventApplicator] No engine attached to element for onKey`);
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
};
|
|
278
|
-
|
|
279
|
-
(element as any).__hypenKeyListener = listener;
|
|
280
|
-
element.addEventListener("keydown", listener);
|
|
281
|
-
|
|
282
|
-
console.log(`[EventApplicator] onKey handler attached for action: ${actionName} (triggers on Enter)`);
|
|
283
|
-
} else {
|
|
284
|
-
console.warn(`[EventApplicator] onKey value must be an action reference starting with @, got: ${value}`);
|
|
285
|
-
}
|
|
286
|
-
},
|
|
287
|
-
|
|
288
|
-
// Handle key-specific applicator: onKey.key for specifying which key
|
|
289
|
-
"onKey.key": (element, keyValue) => {
|
|
290
|
-
console.log(`[EventApplicator] onKey.key called with value:`, keyValue);
|
|
291
|
-
// Store the key to check in the listener
|
|
292
|
-
(element as any).__hypenKeyTarget = keyValue;
|
|
293
|
-
},
|
|
294
|
-
|
|
295
|
-
// Handle action for specific key: onKey.action
|
|
296
|
-
"onKey.action": (element, value) => {
|
|
297
|
-
console.log(`[EventApplicator] onKey.action called with value:`, value);
|
|
298
|
-
|
|
299
|
-
const { actionName } = extractActionDetails(value);
|
|
300
|
-
|
|
301
|
-
if (actionName) {
|
|
302
|
-
|
|
303
|
-
const targetKey = (element as any).__hypenKeyTarget || "Enter";
|
|
304
|
-
|
|
305
|
-
const existingListener = (element as any).__hypenKeyListener;
|
|
306
|
-
if (existingListener) {
|
|
307
|
-
element.removeEventListener("keydown", existingListener);
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
const listener = (event: KeyboardEvent) => {
|
|
311
|
-
const keyToMatch = targetKey.toLowerCase() === "return" ? "Enter" : targetKey;
|
|
312
|
-
|
|
313
|
-
if (event.key === keyToMatch) {
|
|
314
|
-
console.log(`🔥 [EventApplicator] onKey fired (${keyToMatch}), dispatching action: ${actionName}`);
|
|
315
|
-
|
|
316
|
-
event.preventDefault();
|
|
317
|
-
|
|
318
|
-
const target = event.target as HTMLInputElement | HTMLTextAreaElement;
|
|
319
|
-
const payload = {
|
|
320
|
-
type: event.type,
|
|
321
|
-
timestamp: Date.now(),
|
|
322
|
-
key: event.key,
|
|
323
|
-
code: event.code,
|
|
324
|
-
value: target.value,
|
|
325
|
-
input: target.value,
|
|
326
|
-
ctrlKey: event.ctrlKey,
|
|
327
|
-
shiftKey: event.shiftKey,
|
|
328
|
-
altKey: event.altKey,
|
|
329
|
-
metaKey: event.metaKey,
|
|
330
|
-
};
|
|
331
|
-
|
|
332
|
-
const engine = (element as any).__hypenEngine;
|
|
333
|
-
if (engine) {
|
|
334
|
-
engine.dispatchAction(actionName, payload);
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
};
|
|
338
|
-
|
|
339
|
-
(element as any).__hypenKeyListener = listener;
|
|
340
|
-
element.addEventListener("keydown", listener);
|
|
341
|
-
|
|
342
|
-
console.log(`[EventApplicator] onKey handler attached for action: ${actionName} on key: ${targetKey}`);
|
|
343
|
-
}
|
|
344
|
-
},
|
|
345
|
-
|
|
346
|
-
onScroll: (element, value) => {
|
|
347
|
-
console.log(`[EventApplicator] onScroll called with value:`, value);
|
|
348
|
-
|
|
349
|
-
const { actionName } = extractActionDetails(value);
|
|
350
|
-
|
|
351
|
-
if (actionName) {
|
|
352
|
-
|
|
353
|
-
// Remove existing scroll listener if any
|
|
354
|
-
const existingListener = (element as any).__hypenScrollListener;
|
|
355
|
-
if (existingListener) {
|
|
356
|
-
element.removeEventListener("scroll", existingListener);
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
// Create new scroll listener with throttling to avoid too many events
|
|
360
|
-
let throttleTimer: number | null = null;
|
|
361
|
-
const listener = (event: Event) => {
|
|
362
|
-
if (throttleTimer) return;
|
|
363
|
-
|
|
364
|
-
throttleTimer = setTimeout(() => {
|
|
365
|
-
throttleTimer = null;
|
|
366
|
-
}, 100) as unknown as number; // Throttle to max 10 events/second
|
|
367
|
-
|
|
368
|
-
const target = event.target as HTMLElement;
|
|
369
|
-
const scrollTop = target.scrollTop;
|
|
370
|
-
const scrollHeight = target.scrollHeight;
|
|
371
|
-
const clientHeight = target.clientHeight;
|
|
372
|
-
const scrollPercentage = (scrollTop / (scrollHeight - clientHeight)) * 100;
|
|
373
|
-
|
|
374
|
-
// Calculate if near bottom (within 100px or 90% scrolled)
|
|
375
|
-
const nearBottom = scrollHeight - scrollTop - clientHeight < 100 || scrollPercentage > 90;
|
|
376
|
-
|
|
377
|
-
console.log(`🔥 [EventApplicator] onScroll fired, scrollTop: ${scrollTop}, nearBottom: ${nearBottom}`);
|
|
378
|
-
|
|
379
|
-
const payload = {
|
|
380
|
-
type: "scroll",
|
|
381
|
-
timestamp: Date.now(),
|
|
382
|
-
scrollTop,
|
|
383
|
-
scrollLeft: target.scrollLeft,
|
|
384
|
-
scrollHeight,
|
|
385
|
-
scrollWidth: target.scrollWidth,
|
|
386
|
-
clientHeight,
|
|
387
|
-
clientWidth: target.clientWidth,
|
|
388
|
-
scrollPercentage: Math.round(scrollPercentage),
|
|
389
|
-
nearBottom,
|
|
390
|
-
atBottom: scrollHeight - scrollTop === clientHeight,
|
|
391
|
-
atTop: scrollTop === 0,
|
|
392
|
-
};
|
|
393
|
-
|
|
394
|
-
const engine = (element as any).__hypenEngine;
|
|
395
|
-
if (engine) {
|
|
396
|
-
engine.dispatchAction(actionName, payload);
|
|
397
|
-
} else {
|
|
398
|
-
console.warn(`[EventApplicator] No engine attached to element for onScroll`);
|
|
399
|
-
}
|
|
400
|
-
};
|
|
401
|
-
|
|
402
|
-
(element as any).__hypenScrollListener = listener;
|
|
403
|
-
element.addEventListener("scroll", listener, { passive: true });
|
|
404
|
-
|
|
405
|
-
console.log(`[EventApplicator] onScroll handler attached for action: ${actionName}`);
|
|
406
|
-
} else {
|
|
407
|
-
console.warn(`[EventApplicator] onScroll value must be an action reference starting with @, got: ${value}`);
|
|
408
|
-
}
|
|
409
|
-
},
|
|
410
|
-
|
|
411
|
-
onLongClick: (element, value) => {
|
|
412
|
-
console.log(`[EventApplicator] onLongClick called with value:`, value);
|
|
269
|
+
});
|
|
270
|
+
};
|
|
271
|
+
}
|
|
413
272
|
|
|
273
|
+
/**
|
|
274
|
+
* Create a keyboard event handler that filters by key
|
|
275
|
+
*/
|
|
276
|
+
function createKeyHandler(defaultKey: string = "Enter"): ApplicatorHandler {
|
|
277
|
+
return (element: HTMLElement, value: unknown) => {
|
|
414
278
|
const { actionName, payload: customPayload } = extractActionDetails(value);
|
|
415
279
|
|
|
416
280
|
if (!actionName) {
|
|
417
|
-
console.warn(
|
|
281
|
+
console.warn(
|
|
282
|
+
`[EventApplicator] onKey requires an action reference starting with @, got:`,
|
|
283
|
+
value
|
|
284
|
+
);
|
|
418
285
|
return;
|
|
419
286
|
}
|
|
420
287
|
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
const
|
|
424
|
-
if (
|
|
425
|
-
|
|
426
|
-
}
|
|
427
|
-
if (existingUpListener) {
|
|
428
|
-
element.removeEventListener("pointerup", existingUpListener);
|
|
429
|
-
element.removeEventListener("pointerleave", existingUpListener);
|
|
288
|
+
const disposables = getElementDisposables(element);
|
|
289
|
+
|
|
290
|
+
const eventKey = `keydown:${actionName}:${defaultKey}`;
|
|
291
|
+
if (getRegisteredEvents(element).has(eventKey)) {
|
|
292
|
+
return;
|
|
430
293
|
}
|
|
294
|
+
registerEvent(element, eventKey);
|
|
431
295
|
|
|
432
|
-
//
|
|
433
|
-
|
|
434
|
-
const
|
|
296
|
+
// Get target key from element data or use default
|
|
297
|
+
const targetKey = getKeyTarget(element) || defaultKey;
|
|
298
|
+
const keyToMatch = targetKey.toLowerCase() === "return" ? "Enter" : targetKey;
|
|
435
299
|
|
|
436
|
-
const
|
|
437
|
-
|
|
438
|
-
|
|
300
|
+
const listener = (event: Event) => {
|
|
301
|
+
const keyEvent = event as KeyboardEvent;
|
|
302
|
+
if (keyEvent.key !== keyToMatch) {
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
event.preventDefault();
|
|
439
307
|
|
|
440
|
-
|
|
308
|
+
const target = event.target as HTMLInputElement | HTMLTextAreaElement;
|
|
309
|
+
const payload =
|
|
310
|
+
Object.keys(customPayload).length > 0
|
|
441
311
|
? { ...customPayload }
|
|
442
312
|
: {
|
|
443
|
-
type:
|
|
313
|
+
type: event.type,
|
|
444
314
|
timestamp: Date.now(),
|
|
445
|
-
|
|
446
|
-
|
|
315
|
+
key: keyEvent.key,
|
|
316
|
+
code: keyEvent.code,
|
|
317
|
+
value: target.value,
|
|
318
|
+
input: target.value,
|
|
319
|
+
ctrlKey: keyEvent.ctrlKey,
|
|
320
|
+
shiftKey: keyEvent.shiftKey,
|
|
321
|
+
altKey: keyEvent.altKey,
|
|
322
|
+
metaKey: keyEvent.metaKey,
|
|
447
323
|
};
|
|
448
324
|
|
|
449
|
-
|
|
450
|
-
if (engine) {
|
|
451
|
-
engine.dispatchAction(actionName, payload);
|
|
452
|
-
} else {
|
|
453
|
-
console.warn(`[EventApplicator] No engine attached to element for onLongClick`);
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
longClickTimer = null;
|
|
457
|
-
}, LONG_CLICK_THRESHOLD);
|
|
458
|
-
};
|
|
459
|
-
|
|
460
|
-
const upListener = () => {
|
|
461
|
-
if (longClickTimer) {
|
|
462
|
-
clearTimeout(longClickTimer);
|
|
463
|
-
longClickTimer = null;
|
|
464
|
-
}
|
|
465
|
-
};
|
|
466
|
-
|
|
467
|
-
(element as any).__hypenLongClickDownListener = downListener;
|
|
468
|
-
(element as any).__hypenLongClickUpListener = upListener;
|
|
469
|
-
element.addEventListener("pointerdown", downListener);
|
|
470
|
-
element.addEventListener("pointerup", upListener);
|
|
471
|
-
element.addEventListener("pointerleave", upListener);
|
|
472
|
-
|
|
473
|
-
console.log(`[EventApplicator] onLongClick handler attached for action: ${actionName}`);
|
|
474
|
-
},
|
|
475
|
-
|
|
476
|
-
onFocus: (element, value) => {
|
|
477
|
-
console.log(`[EventApplicator] onFocus called with value:`, value);
|
|
478
|
-
|
|
479
|
-
const { actionName, payload: customPayload } = extractActionDetails(value);
|
|
480
|
-
|
|
481
|
-
if (!actionName) {
|
|
482
|
-
console.warn(`[EventApplicator] onFocus value must be an action reference, got:`, value);
|
|
483
|
-
return;
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
// Remove existing focus listener if any
|
|
487
|
-
const existingListener = (element as any).__hypenFocusListener;
|
|
488
|
-
if (existingListener) {
|
|
489
|
-
element.removeEventListener("focus", existingListener);
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
const listener = (event: FocusEvent) => {
|
|
493
|
-
console.log(`🔥 [EventApplicator] onFocus fired, dispatching action: ${actionName}`);
|
|
494
|
-
|
|
495
|
-
const target = event.target as HTMLElement;
|
|
496
|
-
const payload = Object.keys(customPayload).length > 0
|
|
497
|
-
? { ...customPayload }
|
|
498
|
-
: {
|
|
499
|
-
type: "focus",
|
|
500
|
-
timestamp: Date.now(),
|
|
501
|
-
value: (target as HTMLInputElement).value ?? undefined,
|
|
502
|
-
};
|
|
503
|
-
|
|
504
|
-
const engine = (element as any).__hypenEngine;
|
|
325
|
+
const engine = getEngine(element);
|
|
505
326
|
if (engine) {
|
|
506
327
|
engine.dispatchAction(actionName, payload);
|
|
507
|
-
} else {
|
|
508
|
-
console.warn(`[EventApplicator] No engine attached to element for onFocus`);
|
|
509
328
|
}
|
|
510
329
|
};
|
|
511
330
|
|
|
512
|
-
(element
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
onBlur: (element, value) => {
|
|
519
|
-
console.log(`[EventApplicator] onBlur called with value:`, value);
|
|
331
|
+
disposables.add(disposableListener(element, "keydown", listener));
|
|
332
|
+
disposables.addCallback(() => {
|
|
333
|
+
unregisterEvent(element, eventKey);
|
|
334
|
+
});
|
|
335
|
+
};
|
|
336
|
+
}
|
|
520
337
|
|
|
338
|
+
/**
|
|
339
|
+
* Create a long-click/long-press handler
|
|
340
|
+
*/
|
|
341
|
+
function createLongClickHandler(thresholdMs: number = 500): ApplicatorHandler {
|
|
342
|
+
return (element: HTMLElement, value: unknown) => {
|
|
521
343
|
const { actionName, payload: customPayload } = extractActionDetails(value);
|
|
522
344
|
|
|
523
345
|
if (!actionName) {
|
|
524
|
-
console.warn(
|
|
346
|
+
console.warn(
|
|
347
|
+
`[EventApplicator] onLongClick requires an action reference starting with @, got:`,
|
|
348
|
+
value
|
|
349
|
+
);
|
|
525
350
|
return;
|
|
526
351
|
}
|
|
527
352
|
|
|
528
|
-
|
|
529
|
-
const existingListener = (element as any).__hypenBlurListener;
|
|
530
|
-
if (existingListener) {
|
|
531
|
-
element.removeEventListener("blur", existingListener);
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
const listener = (event: FocusEvent) => {
|
|
535
|
-
console.log(`🔥 [EventApplicator] onBlur fired, dispatching action: ${actionName}`);
|
|
536
|
-
|
|
537
|
-
const target = event.target as HTMLElement;
|
|
538
|
-
const payload = Object.keys(customPayload).length > 0
|
|
539
|
-
? { ...customPayload }
|
|
540
|
-
: {
|
|
541
|
-
type: "blur",
|
|
542
|
-
timestamp: Date.now(),
|
|
543
|
-
value: (target as HTMLInputElement).value ?? undefined,
|
|
544
|
-
};
|
|
353
|
+
const disposables = getElementDisposables(element);
|
|
545
354
|
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
engine.dispatchAction(actionName, payload);
|
|
549
|
-
} else {
|
|
550
|
-
console.warn(`[EventApplicator] No engine attached to element for onBlur`);
|
|
551
|
-
}
|
|
552
|
-
};
|
|
553
|
-
|
|
554
|
-
(element as any).__hypenBlurListener = listener;
|
|
555
|
-
element.addEventListener("blur", listener);
|
|
556
|
-
|
|
557
|
-
console.log(`[EventApplicator] onBlur handler attached for action: ${actionName}`);
|
|
558
|
-
},
|
|
559
|
-
|
|
560
|
-
onMouseEnter: (element, value) => {
|
|
561
|
-
console.log(`[EventApplicator] onMouseEnter called with value:`, value);
|
|
562
|
-
|
|
563
|
-
const { actionName, payload: customPayload } = extractActionDetails(value);
|
|
564
|
-
|
|
565
|
-
if (!actionName) {
|
|
566
|
-
console.warn(`[EventApplicator] onMouseEnter value must be an action reference, got:`, value);
|
|
355
|
+
const eventKey = `longclick:${actionName}`;
|
|
356
|
+
if (getRegisteredEvents(element).has(eventKey)) {
|
|
567
357
|
return;
|
|
568
358
|
}
|
|
359
|
+
registerEvent(element, eventKey);
|
|
569
360
|
|
|
570
|
-
|
|
571
|
-
const existingListener = (element as any).__hypenMouseEnterListener;
|
|
572
|
-
if (existingListener) {
|
|
573
|
-
element.removeEventListener("mouseenter", existingListener);
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
const listener = (event: MouseEvent) => {
|
|
577
|
-
console.log(`🔥 [EventApplicator] onMouseEnter fired, dispatching action: ${actionName}`);
|
|
578
|
-
|
|
579
|
-
const payload = Object.keys(customPayload).length > 0
|
|
580
|
-
? { ...customPayload }
|
|
581
|
-
: {
|
|
582
|
-
type: "mouseenter",
|
|
583
|
-
timestamp: Date.now(),
|
|
584
|
-
clientX: event.clientX,
|
|
585
|
-
clientY: event.clientY,
|
|
586
|
-
};
|
|
587
|
-
|
|
588
|
-
const engine = (element as any).__hypenEngine;
|
|
589
|
-
if (engine) {
|
|
590
|
-
engine.dispatchAction(actionName, payload);
|
|
591
|
-
} else {
|
|
592
|
-
console.warn(`[EventApplicator] No engine attached to element for onMouseEnter`);
|
|
593
|
-
}
|
|
594
|
-
};
|
|
595
|
-
|
|
596
|
-
(element as any).__hypenMouseEnterListener = listener;
|
|
597
|
-
element.addEventListener("mouseenter", listener);
|
|
361
|
+
let longClickTimer: Disposable | null = null;
|
|
598
362
|
|
|
599
|
-
|
|
600
|
-
|
|
363
|
+
const downListener = (event: Event) => {
|
|
364
|
+
const pointerEvent = event as PointerEvent;
|
|
601
365
|
|
|
602
|
-
|
|
603
|
-
|
|
366
|
+
longClickTimer = disposableTimeout(() => {
|
|
367
|
+
const payload =
|
|
368
|
+
Object.keys(customPayload).length > 0
|
|
369
|
+
? { ...customPayload }
|
|
370
|
+
: {
|
|
371
|
+
type: "longclick",
|
|
372
|
+
timestamp: Date.now(),
|
|
373
|
+
clientX: pointerEvent.clientX,
|
|
374
|
+
clientY: pointerEvent.clientY,
|
|
375
|
+
};
|
|
604
376
|
|
|
605
|
-
|
|
377
|
+
const engine = getEngine(element);
|
|
378
|
+
if (engine) {
|
|
379
|
+
engine.dispatchAction(actionName, payload);
|
|
380
|
+
}
|
|
606
381
|
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
}
|
|
382
|
+
longClickTimer = null;
|
|
383
|
+
}, thresholdMs);
|
|
384
|
+
};
|
|
611
385
|
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
386
|
+
const cancelListener = () => {
|
|
387
|
+
if (longClickTimer) {
|
|
388
|
+
longClickTimer.dispose();
|
|
389
|
+
longClickTimer = null;
|
|
390
|
+
}
|
|
391
|
+
};
|
|
617
392
|
|
|
618
|
-
|
|
619
|
-
|
|
393
|
+
disposables.add(disposableListener(element, "pointerdown", downListener));
|
|
394
|
+
disposables.add(disposableListener(element, "pointerup", cancelListener));
|
|
395
|
+
disposables.add(disposableListener(element, "pointerleave", cancelListener));
|
|
396
|
+
disposables.addCallback(() => {
|
|
397
|
+
unregisterEvent(element, eventKey);
|
|
398
|
+
cancelListener();
|
|
399
|
+
});
|
|
400
|
+
};
|
|
401
|
+
}
|
|
620
402
|
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
type: "mouseleave",
|
|
625
|
-
timestamp: Date.now(),
|
|
626
|
-
clientX: event.clientX,
|
|
627
|
-
clientY: event.clientY,
|
|
628
|
-
};
|
|
403
|
+
// ============================================================================
|
|
404
|
+
// Payload Extractors
|
|
405
|
+
// ============================================================================
|
|
629
406
|
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
407
|
+
const inputPayload = (event: Event, element: HTMLElement): Record<string, unknown> => {
|
|
408
|
+
const target = element as HTMLInputElement | HTMLTextAreaElement;
|
|
409
|
+
return {
|
|
410
|
+
type: event.type,
|
|
411
|
+
timestamp: Date.now(),
|
|
412
|
+
value: target.value,
|
|
413
|
+
input: target.value,
|
|
414
|
+
};
|
|
415
|
+
};
|
|
637
416
|
|
|
638
|
-
|
|
639
|
-
|
|
417
|
+
const scrollPayload = (_event: Event, element: HTMLElement): Record<string, unknown> => {
|
|
418
|
+
const scrollTop = element.scrollTop;
|
|
419
|
+
const scrollHeight = element.scrollHeight;
|
|
420
|
+
const clientHeight = element.clientHeight;
|
|
421
|
+
const scrollPercentage =
|
|
422
|
+
scrollHeight - clientHeight > 0
|
|
423
|
+
? (scrollTop / (scrollHeight - clientHeight)) * 100
|
|
424
|
+
: 0;
|
|
640
425
|
|
|
641
|
-
|
|
642
|
-
|
|
426
|
+
const nearBottom =
|
|
427
|
+
scrollHeight - scrollTop - clientHeight < 100 || scrollPercentage > 90;
|
|
643
428
|
|
|
429
|
+
return {
|
|
430
|
+
type: "scroll",
|
|
431
|
+
timestamp: Date.now(),
|
|
432
|
+
scrollTop,
|
|
433
|
+
scrollLeft: element.scrollLeft,
|
|
434
|
+
scrollHeight,
|
|
435
|
+
scrollWidth: element.scrollWidth,
|
|
436
|
+
clientHeight,
|
|
437
|
+
clientWidth: element.clientWidth,
|
|
438
|
+
scrollPercentage: Math.round(scrollPercentage),
|
|
439
|
+
nearBottom,
|
|
440
|
+
atBottom: scrollHeight - scrollTop === clientHeight,
|
|
441
|
+
atTop: scrollTop === 0,
|
|
442
|
+
};
|
|
644
443
|
};
|
|
645
444
|
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
445
|
+
const focusPayload = (event: Event, element: HTMLElement): Record<string, unknown> => ({
|
|
446
|
+
type: event.type,
|
|
447
|
+
timestamp: Date.now(),
|
|
448
|
+
value: (element as HTMLInputElement).value ?? undefined,
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
const mousePayload = (event: Event, _element: HTMLElement): Record<string, unknown> => {
|
|
452
|
+
const mouseEvent = event as MouseEvent;
|
|
453
|
+
return {
|
|
651
454
|
type: event.type,
|
|
652
455
|
timestamp: Date.now(),
|
|
456
|
+
clientX: mouseEvent.clientX,
|
|
457
|
+
clientY: mouseEvent.clientY,
|
|
653
458
|
};
|
|
459
|
+
};
|
|
654
460
|
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
461
|
+
// ============================================================================
|
|
462
|
+
// Event Handlers Export
|
|
463
|
+
// ============================================================================
|
|
464
|
+
|
|
465
|
+
export const eventHandlers: Record<string, ApplicatorHandler> = {
|
|
466
|
+
// Basic click/press
|
|
467
|
+
onClick: createEventHandler("click"),
|
|
468
|
+
onPress: createEventHandler("click"), // Alias for mobile-style naming
|
|
469
|
+
|
|
470
|
+
// Form events
|
|
471
|
+
onChange: createEventHandler("change"),
|
|
472
|
+
onSubmit: createEventHandler("submit", { preventDefault: true }),
|
|
473
|
+
onInput: createEventHandler("input", { extractPayload: inputPayload }),
|
|
661
474
|
|
|
662
475
|
// Keyboard events
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
data.metaKey = event.metaKey;
|
|
670
|
-
}
|
|
476
|
+
onKey: createKeyHandler("Enter"),
|
|
477
|
+
"onKey.key": (element: HTMLElement, value: unknown) => {
|
|
478
|
+
// Store the target key for the action handler to use
|
|
479
|
+
setKeyTarget(element, String(value));
|
|
480
|
+
},
|
|
481
|
+
"onKey.action": createKeyHandler("Enter"),
|
|
671
482
|
|
|
672
|
-
//
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
483
|
+
// Scroll (throttled)
|
|
484
|
+
onScroll: createEventHandler("scroll", {
|
|
485
|
+
throttleMs: 100,
|
|
486
|
+
passive: true,
|
|
487
|
+
extractPayload: scrollPayload,
|
|
488
|
+
}),
|
|
676
489
|
|
|
677
|
-
//
|
|
678
|
-
|
|
679
|
-
data.value = element.value;
|
|
680
|
-
data.selectedIndex = element.selectedIndex;
|
|
681
|
-
}
|
|
490
|
+
// Long click/press
|
|
491
|
+
onLongClick: createLongClickHandler(500),
|
|
682
492
|
|
|
683
|
-
//
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
}
|
|
493
|
+
// Focus events
|
|
494
|
+
onFocus: createEventHandler("focus", { extractPayload: focusPayload }),
|
|
495
|
+
onBlur: createEventHandler("blur", { extractPayload: focusPayload }),
|
|
687
496
|
|
|
688
|
-
|
|
689
|
-
}
|
|
497
|
+
// Mouse hover events
|
|
498
|
+
onMouseEnter: createEventHandler("mouseenter", { extractPayload: mousePayload }),
|
|
499
|
+
onMouseLeave: createEventHandler("mouseleave", { extractPayload: mousePayload }),
|
|
500
|
+
};
|