elementdrawing 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 +21 -0
- package/dist/elementdrawing.min.js +3 -0
- package/dist/elementdrawing.min.js.LICENSE.txt +8 -0
- package/dist/elementdrawing.min.js.map +1 -0
- package/dist/index.html +1 -0
- package/package.json +127 -0
- package/src/core/bridge.h +855 -0
- package/src/core/diff.c +900 -0
- package/src/core/element.c +1078 -0
- package/src/core/event.c +813 -0
- package/src/core/fiber.c +1027 -0
- package/src/core/hooks.c +919 -0
- package/src/core/renderer.c +963 -0
- package/src/core/scheduler.c +702 -0
- package/src/core/state.c +803 -0
- package/src/css/animations.css +779 -0
- package/src/css/base.css +615 -0
- package/src/css/components.css +1311 -0
- package/src/css/tailwind.css +370 -0
- package/src/css/themes.css +517 -0
- package/src/css/utilities.css +475 -0
- package/src/index.js +746 -0
- package/src/js/animation.js +655 -0
- package/src/js/dom.js +665 -0
- package/src/js/events.js +585 -0
- package/src/js/http.js +446 -0
- package/src/js/index.js +26 -0
- package/src/js/router.js +483 -0
- package/src/js/store.js +539 -0
- package/src/js/utils.js +593 -0
- package/src/js/validator.js +529 -0
- package/src/jsx/components/Accordion.jsx +210 -0
- package/src/jsx/components/Alert.jsx +169 -0
- package/src/jsx/components/Avatar.jsx +214 -0
- package/src/jsx/components/Badge.jsx +136 -0
- package/src/jsx/components/Breadcrumb.jsx +200 -0
- package/src/jsx/components/Button.jsx +188 -0
- package/src/jsx/components/Card.jsx +192 -0
- package/src/jsx/components/Carousel.jsx +278 -0
- package/src/jsx/components/Checkbox.jsx +215 -0
- package/src/jsx/components/Dialog.jsx +242 -0
- package/src/jsx/components/Drawer.jsx +190 -0
- package/src/jsx/components/Dropdown.jsx +268 -0
- package/src/jsx/components/Form.jsx +274 -0
- package/src/jsx/components/Input.jsx +285 -0
- package/src/jsx/components/Menu.jsx +276 -0
- package/src/jsx/components/Modal.jsx +274 -0
- package/src/jsx/components/Navbar.jsx +292 -0
- package/src/jsx/components/Pagination.jsx +268 -0
- package/src/jsx/components/Progress.jsx +252 -0
- package/src/jsx/components/Radio.jsx +208 -0
- package/src/jsx/components/Select.jsx +397 -0
- package/src/jsx/components/Sidebar.jsx +250 -0
- package/src/jsx/components/Slider.jsx +310 -0
- package/src/jsx/components/Spinner.jsx +198 -0
- package/src/jsx/components/Switch.jsx +201 -0
- package/src/jsx/components/Table.jsx +332 -0
- package/src/jsx/components/Tabs.jsx +227 -0
- package/src/jsx/components/Textarea.jsx +212 -0
- package/src/jsx/components/Toast.jsx +270 -0
- package/src/jsx/components/Tooltip.jsx +178 -0
- package/src/jsx/components/Typography.jsx +299 -0
- package/src/jsx/components/index.jsx +70 -0
- package/src/jsx/core/element.js +3 -0
- package/src/jsx/hooks/index.js +356 -0
- package/src/jsx/hooks/useCallback.js +472 -0
- package/src/jsx/hooks/useContext.js +586 -0
- package/src/jsx/hooks/useEffect.js +704 -0
- package/src/jsx/hooks/useLayoutEffect.js +508 -0
- package/src/jsx/hooks/useMemo.js +689 -0
- package/src/jsx/hooks/useReducer.js +729 -0
- package/src/jsx/hooks/useRef.js +542 -0
- package/src/jsx/hooks/useState.js +854 -0
- package/src/jsx/runtime/commit.js +903 -0
- package/src/jsx/runtime/createElement.js +860 -0
- package/src/jsx/runtime/index.js +356 -0
- package/src/jsx/runtime/reconcile.js +687 -0
- package/src/jsx/runtime/render.js +914 -0
package/src/js/events.js
ADDED
|
@@ -0,0 +1,585 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Event System Utilities
|
|
3
|
+
* ElementDrawing Framework - Event binding, creation, normalization,
|
|
4
|
+
* keyboard/mouse/touch/scroll/resize handling, and custom event emitter.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
'use strict';
|
|
8
|
+
|
|
9
|
+
// ─── Event Binding ────────────────────────────────────────────────────────────
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Bind an event handler to an element.
|
|
13
|
+
* @param {HTMLElement|Window|Document} el
|
|
14
|
+
* @param {string} event - Event type
|
|
15
|
+
* @param {Function} handler
|
|
16
|
+
* @param {Object|boolean} [options=false]
|
|
17
|
+
* @returns {Function} Unbind function
|
|
18
|
+
*/
|
|
19
|
+
function on(el, event, handler, options) {
|
|
20
|
+
el.addEventListener(event, handler, options || false);
|
|
21
|
+
return function unbind() {
|
|
22
|
+
el.removeEventListener(event, handler, options || false);
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Remove an event handler from an element.
|
|
28
|
+
* @param {HTMLElement|Window|Document} el
|
|
29
|
+
* @param {string} event
|
|
30
|
+
* @param {Function} handler
|
|
31
|
+
* @param {Object|boolean} [options=false]
|
|
32
|
+
*/
|
|
33
|
+
function off(el, event, handler, options) {
|
|
34
|
+
el.removeEventListener(event, handler, options || false);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Bind an event handler that fires only once.
|
|
39
|
+
* @param {HTMLElement|Window|Document} el
|
|
40
|
+
* @param {string} event
|
|
41
|
+
* @param {Function} handler
|
|
42
|
+
* @param {Object|boolean} [options=false]
|
|
43
|
+
* @returns {Function} Unbind function
|
|
44
|
+
*/
|
|
45
|
+
function once(el, event, handler, options) {
|
|
46
|
+
function onceHandler() {
|
|
47
|
+
off(el, event, onceHandler, options);
|
|
48
|
+
return handler.apply(this, arguments);
|
|
49
|
+
}
|
|
50
|
+
on(el, event, onceHandler, options);
|
|
51
|
+
return function unbind() {
|
|
52
|
+
off(el, event, onceHandler, options);
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Delegate an event to matching child elements.
|
|
58
|
+
* @param {HTMLElement} parent - Parent element to listen on
|
|
59
|
+
* @param {string} event - Event type
|
|
60
|
+
* @param {string} selector - CSS selector for child elements
|
|
61
|
+
* @param {Function} handler - Event handler
|
|
62
|
+
* @returns {Function} Unbind function
|
|
63
|
+
*/
|
|
64
|
+
function delegate(parent, event, selector, handler) {
|
|
65
|
+
function delegateHandler(e) {
|
|
66
|
+
const target = e.target.closest(selector);
|
|
67
|
+
if (target && parent.contains(target)) {
|
|
68
|
+
handler.call(target, e, target);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
on(parent, event, delegateHandler);
|
|
72
|
+
return function unbind() {
|
|
73
|
+
off(parent, event, delegateHandler);
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ─── Event Creation ───────────────────────────────────────────────────────────
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Create a custom event.
|
|
81
|
+
* @param {string} type - Event type
|
|
82
|
+
* @param {Object} [detail] - Custom data
|
|
83
|
+
* @param {Object} [options] - Event options
|
|
84
|
+
* @returns {CustomEvent}
|
|
85
|
+
*/
|
|
86
|
+
function createEvent(type, detail, options) {
|
|
87
|
+
options = options || {};
|
|
88
|
+
return new CustomEvent(type, {
|
|
89
|
+
bubbles: options.bubbles !== undefined ? options.bubbles : true,
|
|
90
|
+
cancelable: options.cancelable !== undefined ? options.cancelable : true,
|
|
91
|
+
detail: detail,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Dispatch an event on an element.
|
|
97
|
+
* @param {HTMLElement} el
|
|
98
|
+
* @param {Event|string} event
|
|
99
|
+
* @param {Object} [detail]
|
|
100
|
+
* @returns {boolean} False if preventDefault was called
|
|
101
|
+
*/
|
|
102
|
+
function dispatchEvent(el, event, detail) {
|
|
103
|
+
if (typeof event === 'string') {
|
|
104
|
+
event = createEvent(event, detail);
|
|
105
|
+
}
|
|
106
|
+
return el.dispatchEvent(event);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Fire a native-like event on an element.
|
|
111
|
+
* @param {HTMLElement} el
|
|
112
|
+
* @param {string} type - e.g. 'click', 'focus', 'blur'
|
|
113
|
+
* @param {Object} [options]
|
|
114
|
+
* @returns {boolean}
|
|
115
|
+
*/
|
|
116
|
+
function fireEvent(el, type, options) {
|
|
117
|
+
const event = new Event(type, {
|
|
118
|
+
bubbles: (options && options.bubbles) !== undefined ? options.bubbles : true,
|
|
119
|
+
cancelable: (options && options.cancelable) !== undefined ? options.cancelable : true,
|
|
120
|
+
});
|
|
121
|
+
Object.assign(event, options || {});
|
|
122
|
+
return el.dispatchEvent(event);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ─── Event Normalization ──────────────────────────────────────────────────────
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Get the event target.
|
|
129
|
+
* @param {Event} e
|
|
130
|
+
* @returns {HTMLElement}
|
|
131
|
+
*/
|
|
132
|
+
function getTarget(e) {
|
|
133
|
+
return e.target || e.srcElement;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Get the related target (for mouseenter/mouseleave, focus/blur).
|
|
138
|
+
* @param {Event} e
|
|
139
|
+
* @returns {HTMLElement|null}
|
|
140
|
+
*/
|
|
141
|
+
function getRelatedTarget(e) {
|
|
142
|
+
return e.relatedTarget || null;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Get the offset coordinates of a mouse event relative to the target.
|
|
147
|
+
* @param {MouseEvent} e
|
|
148
|
+
* @returns {{ offsetX: number, offsetY: number }}
|
|
149
|
+
*/
|
|
150
|
+
function getOffset(e) {
|
|
151
|
+
return { offsetX: e.offsetX, offsetY: e.offsetY };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Prevent default behavior.
|
|
156
|
+
* @param {Event} e
|
|
157
|
+
*/
|
|
158
|
+
function preventDefault(e) {
|
|
159
|
+
if (e.preventDefault) e.preventDefault();
|
|
160
|
+
e.returnValue = false;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Stop event propagation.
|
|
165
|
+
* @param {Event} e
|
|
166
|
+
*/
|
|
167
|
+
function stopPropagation(e) {
|
|
168
|
+
if (e.stopPropagation) e.stopPropagation();
|
|
169
|
+
e.cancelBubble = true;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Stop immediate propagation.
|
|
174
|
+
* @param {Event} e
|
|
175
|
+
*/
|
|
176
|
+
function stopImmediatePropagation(e) {
|
|
177
|
+
if (e.stopImmediatePropagation) e.stopImmediatePropagation();
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// ─── Keyboard Events ──────────────────────────────────────────────────────────
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Check if the key is Enter.
|
|
184
|
+
* @param {KeyboardEvent} e
|
|
185
|
+
* @returns {boolean}
|
|
186
|
+
*/
|
|
187
|
+
function isEnter(e) {
|
|
188
|
+
return e.key === 'Enter' || e.keyCode === 13;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Check if the key is Escape.
|
|
193
|
+
* @param {KeyboardEvent} e
|
|
194
|
+
* @returns {boolean}
|
|
195
|
+
*/
|
|
196
|
+
function isEscape(e) {
|
|
197
|
+
return e.key === 'Escape' || e.keyCode === 27;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Check if the key is Tab.
|
|
202
|
+
* @param {KeyboardEvent} e
|
|
203
|
+
* @returns {boolean}
|
|
204
|
+
*/
|
|
205
|
+
function isTab(e) {
|
|
206
|
+
return e.key === 'Tab' || e.keyCode === 9;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Check if the key is an arrow key.
|
|
211
|
+
* @param {KeyboardEvent} e
|
|
212
|
+
* @returns {boolean}
|
|
213
|
+
*/
|
|
214
|
+
function isArrow(e) {
|
|
215
|
+
return e.key.startsWith('Arrow') || (e.keyCode >= 37 && e.keyCode <= 40);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Get the key code from a keyboard event.
|
|
220
|
+
* @param {KeyboardEvent} e
|
|
221
|
+
* @returns {number}
|
|
222
|
+
*/
|
|
223
|
+
function getKeyCode(e) {
|
|
224
|
+
return e.keyCode || e.which || 0;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Check if a modifier key is pressed.
|
|
229
|
+
* @param {KeyboardEvent} e
|
|
230
|
+
* @param {string} modifier - 'ctrl' | 'shift' | 'alt' | 'meta'
|
|
231
|
+
* @returns {boolean}
|
|
232
|
+
*/
|
|
233
|
+
function isModifier(e, modifier) {
|
|
234
|
+
switch (modifier) {
|
|
235
|
+
case 'ctrl': return e.ctrlKey;
|
|
236
|
+
case 'shift': return e.shiftKey;
|
|
237
|
+
case 'alt': return e.altKey;
|
|
238
|
+
case 'meta': return e.metaKey;
|
|
239
|
+
default: return false;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// ─── Mouse Events ─────────────────────────────────────────────────────────────
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Check if the mouse button is a left click.
|
|
247
|
+
* @param {MouseEvent} e
|
|
248
|
+
* @returns {boolean}
|
|
249
|
+
*/
|
|
250
|
+
function isLeftClick(e) {
|
|
251
|
+
return e.button === 0;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Check if the mouse button is a right click.
|
|
256
|
+
* @param {MouseEvent} e
|
|
257
|
+
* @returns {boolean}
|
|
258
|
+
*/
|
|
259
|
+
function isRightClick(e) {
|
|
260
|
+
return e.button === 2;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Get the mouse button number.
|
|
265
|
+
* @param {MouseEvent} e
|
|
266
|
+
* @returns {number}
|
|
267
|
+
*/
|
|
268
|
+
function getButton(e) {
|
|
269
|
+
return e.button;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Get the mouse coordinates (page and client).
|
|
274
|
+
* @param {MouseEvent} e
|
|
275
|
+
* @returns {{ pageX: number, pageY: number, clientX: number, clientY: number }}
|
|
276
|
+
*/
|
|
277
|
+
function getCoordinates(e) {
|
|
278
|
+
return {
|
|
279
|
+
pageX: e.pageX,
|
|
280
|
+
pageY: e.pageY,
|
|
281
|
+
clientX: e.clientX,
|
|
282
|
+
clientY: e.clientY,
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// ─── Touch Events ─────────────────────────────────────────────────────────────
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Get a specific touch from a touch event.
|
|
290
|
+
* @param {TouchEvent} e
|
|
291
|
+
* @param {number} [index=0]
|
|
292
|
+
* @returns {Touch|null}
|
|
293
|
+
*/
|
|
294
|
+
function getTouch(e, index) {
|
|
295
|
+
index = index || 0;
|
|
296
|
+
return e.touches && e.touches[index] ? e.touches[index] : null;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Get the distance between two touch points.
|
|
301
|
+
* @param {TouchEvent} e
|
|
302
|
+
* @returns {number}
|
|
303
|
+
*/
|
|
304
|
+
function getTouchDistance(e) {
|
|
305
|
+
if (!e.touches || e.touches.length < 2) return 0;
|
|
306
|
+
const dx = e.touches[0].clientX - e.touches[1].clientX;
|
|
307
|
+
const dy = e.touches[0].clientY - e.touches[1].clientY;
|
|
308
|
+
return Math.sqrt(dx * dx + dy * dy);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Check if a touch event is a pinch gesture.
|
|
313
|
+
* @param {TouchEvent} e
|
|
314
|
+
* @returns {boolean}
|
|
315
|
+
*/
|
|
316
|
+
function isPinch(e) {
|
|
317
|
+
return e.touches && e.touches.length >= 2;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Check if a touch event is a swipe.
|
|
322
|
+
* @param {Object} startTouch - Starting touch coordinates
|
|
323
|
+
* @param {Object} endTouch - Ending touch coordinates
|
|
324
|
+
* @param {number} [threshold=50] - Minimum distance for a swipe
|
|
325
|
+
* @returns {{ isSwipe: boolean, direction: string, distance: number }}
|
|
326
|
+
*/
|
|
327
|
+
function isSwipe(startTouch, endTouch, threshold) {
|
|
328
|
+
threshold = threshold || 50;
|
|
329
|
+
const dx = endTouch.clientX - startTouch.clientX;
|
|
330
|
+
const dy = endTouch.clientY - startTouch.clientY;
|
|
331
|
+
const absDx = Math.abs(dx);
|
|
332
|
+
const absDy = Math.abs(dy);
|
|
333
|
+
const distance = Math.sqrt(dx * dx + dy * dy);
|
|
334
|
+
|
|
335
|
+
if (distance < threshold) {
|
|
336
|
+
return { isSwipe: false, direction: '', distance };
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
let direction;
|
|
340
|
+
if (absDx > absDy) {
|
|
341
|
+
direction = dx > 0 ? 'right' : 'left';
|
|
342
|
+
} else {
|
|
343
|
+
direction = dy > 0 ? 'down' : 'up';
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
return { isSwipe: true, direction, distance };
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// ─── Scroll Events ────────────────────────────────────────────────────────────
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Bind a scroll handler with optional throttling.
|
|
353
|
+
* @param {HTMLElement|Window} el
|
|
354
|
+
* @param {Function} handler
|
|
355
|
+
* @param {number} [throttleMs=100]
|
|
356
|
+
* @returns {Function} Unbind function
|
|
357
|
+
*/
|
|
358
|
+
function onScroll(el, handler, throttleMs) {
|
|
359
|
+
throttleMs = throttleMs || 100;
|
|
360
|
+
let lastCall = 0;
|
|
361
|
+
let ticking = false;
|
|
362
|
+
|
|
363
|
+
function scrollHandler(e) {
|
|
364
|
+
const now = Date.now();
|
|
365
|
+
if (now - lastCall < throttleMs) {
|
|
366
|
+
if (!ticking) {
|
|
367
|
+
ticking = true;
|
|
368
|
+
requestAnimationFrame(() => {
|
|
369
|
+
handler(e);
|
|
370
|
+
ticking = false;
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
lastCall = now;
|
|
376
|
+
handler(e);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
return on(el, 'scroll', scrollHandler, { passive: true });
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Bind a handler that fires when scrolling ends.
|
|
384
|
+
* @param {HTMLElement|Window} el
|
|
385
|
+
* @param {Function} handler
|
|
386
|
+
* @param {number} [delay=150]
|
|
387
|
+
* @returns {Function} Unbind function
|
|
388
|
+
*/
|
|
389
|
+
function onScrollEnd(el, handler, delay) {
|
|
390
|
+
delay = delay || 150;
|
|
391
|
+
let timeoutId;
|
|
392
|
+
|
|
393
|
+
function scrollHandler() {
|
|
394
|
+
clearTimeout(timeoutId);
|
|
395
|
+
timeoutId = setTimeout(() => handler(), delay);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
const unbind = on(el, 'scroll', scrollHandler, { passive: true });
|
|
399
|
+
return function unbindScrollEnd() {
|
|
400
|
+
clearTimeout(timeoutId);
|
|
401
|
+
unbind();
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Get scroll direction from previous and current scroll positions.
|
|
407
|
+
* @param {number} prevScrollTop
|
|
408
|
+
* @param {number} currentScrollTop
|
|
409
|
+
* @returns {'up'|'down'|'none'}
|
|
410
|
+
*/
|
|
411
|
+
function getScrollDirection(prevScrollTop, currentScrollTop) {
|
|
412
|
+
const diff = currentScrollTop - prevScrollTop;
|
|
413
|
+
if (diff > 0) return 'down';
|
|
414
|
+
if (diff < 0) return 'up';
|
|
415
|
+
return 'none';
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// ─── Resize Events ────────────────────────────────────────────────────────────
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Bind a resize handler with debouncing.
|
|
422
|
+
* @param {Function} handler
|
|
423
|
+
* @param {number} [debounceMs=150]
|
|
424
|
+
* @returns {Function} Unbind function
|
|
425
|
+
*/
|
|
426
|
+
function onResize(handler, debounceMs) {
|
|
427
|
+
debounceMs = debounceMs || 150;
|
|
428
|
+
let timeoutId;
|
|
429
|
+
|
|
430
|
+
function resizeHandler() {
|
|
431
|
+
clearTimeout(timeoutId);
|
|
432
|
+
timeoutId = setTimeout(() => {
|
|
433
|
+
handler({
|
|
434
|
+
width: window.innerWidth,
|
|
435
|
+
height: window.innerHeight,
|
|
436
|
+
});
|
|
437
|
+
}, debounceMs);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
return on(window, 'resize', resizeHandler);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* Create a debounced resize observer for a specific element.
|
|
445
|
+
* @param {HTMLElement} el
|
|
446
|
+
* @param {Function} handler
|
|
447
|
+
* @returns {{ disconnect: Function }} Observer handle
|
|
448
|
+
*/
|
|
449
|
+
function debounceResize(el, handler) {
|
|
450
|
+
if (typeof ResizeObserver === 'undefined') {
|
|
451
|
+
return { disconnect: function () {} };
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
let timeoutId;
|
|
455
|
+
const observer = new ResizeObserver((entries) => {
|
|
456
|
+
clearTimeout(timeoutId);
|
|
457
|
+
timeoutId = setTimeout(() => handler(entries), 100);
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
observer.observe(el);
|
|
461
|
+
return {
|
|
462
|
+
disconnect: function () {
|
|
463
|
+
clearTimeout(timeoutId);
|
|
464
|
+
observer.disconnect();
|
|
465
|
+
},
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// ─── Custom Event Emitter ─────────────────────────────────────────────────────
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* Create a custom event emitter.
|
|
473
|
+
* @returns {Object} Emitter with on, off, emit, once methods
|
|
474
|
+
*/
|
|
475
|
+
function emitter() {
|
|
476
|
+
const listeners = {};
|
|
477
|
+
|
|
478
|
+
return {
|
|
479
|
+
/**
|
|
480
|
+
* Subscribe to an event.
|
|
481
|
+
* @param {string} event
|
|
482
|
+
* @param {Function} handler
|
|
483
|
+
* @returns {Function} Unsubscribe function
|
|
484
|
+
*/
|
|
485
|
+
on: function (event, handler) {
|
|
486
|
+
if (!listeners[event]) listeners[event] = [];
|
|
487
|
+
listeners[event].push(handler);
|
|
488
|
+
return function () {
|
|
489
|
+
const idx = listeners[event].indexOf(handler);
|
|
490
|
+
if (idx !== -1) listeners[event].splice(idx, 1);
|
|
491
|
+
};
|
|
492
|
+
},
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* Unsubscribe from an event.
|
|
496
|
+
* @param {string} event
|
|
497
|
+
* @param {Function} [handler] - If omitted, removes all handlers for the event
|
|
498
|
+
*/
|
|
499
|
+
off: function (event, handler) {
|
|
500
|
+
if (!listeners[event]) return;
|
|
501
|
+
if (!handler) {
|
|
502
|
+
delete listeners[event];
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
const idx = listeners[event].indexOf(handler);
|
|
506
|
+
if (idx !== -1) listeners[event].splice(idx, 1);
|
|
507
|
+
},
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* Emit an event.
|
|
511
|
+
* @param {string} event
|
|
512
|
+
* @param {...*} args
|
|
513
|
+
* @returns {boolean} True if any handler was called
|
|
514
|
+
*/
|
|
515
|
+
emit: function (event) {
|
|
516
|
+
const args = Array.prototype.slice.call(arguments, 1);
|
|
517
|
+
if (!listeners[event]) return false;
|
|
518
|
+
|
|
519
|
+
listeners[event].slice().forEach((handler) => {
|
|
520
|
+
try {
|
|
521
|
+
handler.apply(null, args);
|
|
522
|
+
} catch (error) {
|
|
523
|
+
console.error('[emitter] Handler error for "' + event + '":', error);
|
|
524
|
+
}
|
|
525
|
+
});
|
|
526
|
+
return true;
|
|
527
|
+
},
|
|
528
|
+
|
|
529
|
+
/**
|
|
530
|
+
* Subscribe to an event once.
|
|
531
|
+
* @param {string} event
|
|
532
|
+
* @param {Function} handler
|
|
533
|
+
* @returns {Function} Unsubscribe function
|
|
534
|
+
*/
|
|
535
|
+
once: function (event, handler) {
|
|
536
|
+
function onceHandler() {
|
|
537
|
+
emitter.off(event, onceHandler);
|
|
538
|
+
return handler.apply(null, arguments);
|
|
539
|
+
}
|
|
540
|
+
return emitter.on(event, onceHandler);
|
|
541
|
+
},
|
|
542
|
+
|
|
543
|
+
/**
|
|
544
|
+
* Get all listener counts.
|
|
545
|
+
* @returns {Object}
|
|
546
|
+
*/
|
|
547
|
+
listenerCounts: function () {
|
|
548
|
+
const counts = {};
|
|
549
|
+
Object.keys(listeners).forEach((event) => {
|
|
550
|
+
counts[event] = listeners[event].length;
|
|
551
|
+
});
|
|
552
|
+
return counts;
|
|
553
|
+
},
|
|
554
|
+
|
|
555
|
+
/**
|
|
556
|
+
* Remove all listeners.
|
|
557
|
+
*/
|
|
558
|
+
removeAllListeners: function () {
|
|
559
|
+
Object.keys(listeners).forEach((key) => delete listeners[key]);
|
|
560
|
+
},
|
|
561
|
+
};
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// ─── Exports ──────────────────────────────────────────────────────────────────
|
|
565
|
+
|
|
566
|
+
module.exports = {
|
|
567
|
+
// Event binding
|
|
568
|
+
on, off, once, delegate,
|
|
569
|
+
// Event creation
|
|
570
|
+
createEvent, dispatchEvent, fireEvent,
|
|
571
|
+
// Event normalization
|
|
572
|
+
getTarget, getRelatedTarget, getOffset, preventDefault, stopPropagation, stopImmediatePropagation,
|
|
573
|
+
// Keyboard events
|
|
574
|
+
isEnter, isEscape, isTab, isArrow, getKeyCode, isModifier,
|
|
575
|
+
// Mouse events
|
|
576
|
+
isLeftClick, isRightClick, getButton, getCoordinates,
|
|
577
|
+
// Touch events
|
|
578
|
+
getTouch, getTouchDistance, isPinch, isSwipe,
|
|
579
|
+
// Scroll events
|
|
580
|
+
onScroll, onScrollEnd, getScrollDirection,
|
|
581
|
+
// Resize events
|
|
582
|
+
onResize, debounceResize,
|
|
583
|
+
// Custom events
|
|
584
|
+
emitter,
|
|
585
|
+
};
|