@hawsen-the-first/interactiv 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +326 -0
- package/dist/animations.css +160 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +26 -0
- package/dist/src/animationBus.d.ts +30 -0
- package/dist/src/animationBus.d.ts.map +1 -0
- package/dist/src/animationBus.js +125 -0
- package/dist/src/appBuilder.d.ts +173 -0
- package/dist/src/appBuilder.d.ts.map +1 -0
- package/dist/src/appBuilder.js +957 -0
- package/dist/src/eventBus.d.ts +100 -0
- package/dist/src/eventBus.d.ts.map +1 -0
- package/dist/src/eventBus.js +326 -0
- package/dist/src/eventManager.d.ts +87 -0
- package/dist/src/eventManager.d.ts.map +1 -0
- package/dist/src/eventManager.js +455 -0
- package/dist/src/garbageCollector.d.ts +68 -0
- package/dist/src/garbageCollector.d.ts.map +1 -0
- package/dist/src/garbageCollector.js +169 -0
- package/dist/src/logger.d.ts +11 -0
- package/dist/src/logger.d.ts.map +1 -0
- package/dist/src/logger.js +15 -0
- package/dist/src/navigationManager.d.ts +105 -0
- package/dist/src/navigationManager.d.ts.map +1 -0
- package/dist/src/navigationManager.js +533 -0
- package/dist/src/screensaverManager.d.ts +66 -0
- package/dist/src/screensaverManager.d.ts.map +1 -0
- package/dist/src/screensaverManager.js +417 -0
- package/dist/src/settingsManager.d.ts +48 -0
- package/dist/src/settingsManager.d.ts.map +1 -0
- package/dist/src/settingsManager.js +317 -0
- package/dist/src/stateManager.d.ts +58 -0
- package/dist/src/stateManager.d.ts.map +1 -0
- package/dist/src/stateManager.js +278 -0
- package/dist/src/types.d.ts +32 -0
- package/dist/src/types.d.ts.map +1 -0
- package/dist/src/types.js +1 -0
- package/dist/utils/generateGuid.d.ts +2 -0
- package/dist/utils/generateGuid.d.ts.map +1 -0
- package/dist/utils/generateGuid.js +19 -0
- package/dist/utils/logger.d.ts +9 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +42 -0
- package/dist/utils/template-helpers.d.ts +32 -0
- package/dist/utils/template-helpers.d.ts.map +1 -0
- package/dist/utils/template-helpers.js +24 -0
- package/package.json +59 -0
|
@@ -0,0 +1,455 @@
|
|
|
1
|
+
import { logger } from "./logger";
|
|
2
|
+
const log = logger;
|
|
3
|
+
export class EventManager {
|
|
4
|
+
shadowRoot;
|
|
5
|
+
abortController;
|
|
6
|
+
eventListeners = [];
|
|
7
|
+
selectorListeners = new Map(); // Track listeners by selector+method
|
|
8
|
+
dragStates = new Map();
|
|
9
|
+
swipeStates = new Map();
|
|
10
|
+
longPressTimers = new Map();
|
|
11
|
+
componentId;
|
|
12
|
+
constructor(shadowRoot, componentId) {
|
|
13
|
+
this.shadowRoot = shadowRoot;
|
|
14
|
+
this.componentId = componentId;
|
|
15
|
+
this.abortController = new AbortController();
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Unified point interaction - handles both click and tap
|
|
19
|
+
*/
|
|
20
|
+
point(selector, callback) {
|
|
21
|
+
log.trace(`EventManager.point() called for selector: ${selector} in component: ${this.componentId}`);
|
|
22
|
+
// Check if we already have listeners for this selector+method combination
|
|
23
|
+
const key = `point:${selector}`;
|
|
24
|
+
const existing = this.selectorListeners.get(key);
|
|
25
|
+
if (existing) {
|
|
26
|
+
const currentElements = Array.from(this.shadowRoot.querySelectorAll(selector));
|
|
27
|
+
const elementsChanged = existing.elements.length !== currentElements.length ||
|
|
28
|
+
existing.elements.some((el, i) => el !== currentElements[i]);
|
|
29
|
+
if (!elementsChanged) {
|
|
30
|
+
log.trace(`Point listeners already exist for selector: ${selector}, elements unchanged, skipping duplicate`);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
log.trace(`DOM elements changed for selector: ${selector}, clearing old listeners and re-attaching`);
|
|
35
|
+
// Elements changed, clear old listeners
|
|
36
|
+
this.clearSelectorListeners(selector, "point");
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
const elements = this.shadowRoot.querySelectorAll(selector);
|
|
40
|
+
log.trace(`Found ${elements.length} elements for selector: ${selector}`);
|
|
41
|
+
// Store the selector listener mapping
|
|
42
|
+
this.selectorListeners.set(key, {
|
|
43
|
+
selector,
|
|
44
|
+
method: "point",
|
|
45
|
+
callback,
|
|
46
|
+
elements: Array.from(elements),
|
|
47
|
+
});
|
|
48
|
+
elements.forEach((element) => {
|
|
49
|
+
// Handle mouse click
|
|
50
|
+
this.addEventListenerWithTracking(element, "click", (e) => {
|
|
51
|
+
const mouseEvent = e;
|
|
52
|
+
mouseEvent.preventDefault();
|
|
53
|
+
const data = this.createPointerEventData(mouseEvent, "mouse");
|
|
54
|
+
callback(data);
|
|
55
|
+
}, undefined, selector, "point");
|
|
56
|
+
// Handle touch tap (short touch)
|
|
57
|
+
let touchStartTime = 0;
|
|
58
|
+
let pixelStartX, pixelStartY;
|
|
59
|
+
this.addEventListenerWithTracking(element, "touchstart", (e) => {
|
|
60
|
+
log.trace("Touch event ", e);
|
|
61
|
+
const touchEvent = e;
|
|
62
|
+
pixelStartX = touchEvent.changedTouches[0]?.clientX;
|
|
63
|
+
pixelStartY = touchEvent.changedTouches[0]?.clientY;
|
|
64
|
+
touchStartTime = Date.now();
|
|
65
|
+
}, { passive: true }, selector, "point");
|
|
66
|
+
this.addEventListenerWithTracking(element, "touchend", (e) => {
|
|
67
|
+
const touchEvent = e;
|
|
68
|
+
const touchDuration = Date.now() - touchStartTime;
|
|
69
|
+
log.trace("Touch event ", e);
|
|
70
|
+
if (touchDuration < 300 &&
|
|
71
|
+
!this.movementThresholdMet(pixelStartX, pixelStartY, touchEvent.changedTouches[0].clientX, touchEvent.changedTouches[0].clientY)) {
|
|
72
|
+
// Short tap
|
|
73
|
+
touchEvent.preventDefault();
|
|
74
|
+
const data = this.createPointerEventData(touchEvent, "touch");
|
|
75
|
+
callback(data);
|
|
76
|
+
}
|
|
77
|
+
}, undefined, selector, "point");
|
|
78
|
+
});
|
|
79
|
+
log.trace(`Point listeners added for selector: ${selector}`, {
|
|
80
|
+
componentId: this.componentId,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Un-point interaction - handles touch and click events on the background to de-select an active element
|
|
85
|
+
*/
|
|
86
|
+
unpoint(selector, callback) {
|
|
87
|
+
const body = document.querySelector("body");
|
|
88
|
+
const key = `unpoint:${selector}`;
|
|
89
|
+
// Store the selector listener mapping
|
|
90
|
+
this.selectorListeners.set(key, {
|
|
91
|
+
selector,
|
|
92
|
+
method: "unpoint",
|
|
93
|
+
callback,
|
|
94
|
+
elements: [body],
|
|
95
|
+
});
|
|
96
|
+
this.addEventListenerWithTracking(body, "click", (e) => {
|
|
97
|
+
const mouseEvent = e;
|
|
98
|
+
mouseEvent.preventDefault();
|
|
99
|
+
const data = this.createPointerEventData(mouseEvent, "mouse");
|
|
100
|
+
callback(data);
|
|
101
|
+
}, { once: true }, selector, "point");
|
|
102
|
+
// Handle touch tap (short touch)
|
|
103
|
+
let touchStartTime = 0;
|
|
104
|
+
this.addEventListenerWithTracking(body, "touchstart", (_) => {
|
|
105
|
+
touchStartTime = Date.now();
|
|
106
|
+
}, { passive: true, once: true }, selector, "point");
|
|
107
|
+
this.addEventListenerWithTracking(body, "touchend", (e) => {
|
|
108
|
+
const touchEvent = e;
|
|
109
|
+
const touchDuration = Date.now() - touchStartTime;
|
|
110
|
+
if (touchDuration < 300) {
|
|
111
|
+
// Short tap
|
|
112
|
+
touchEvent.preventDefault();
|
|
113
|
+
const data = this.createPointerEventData(touchEvent, "touch");
|
|
114
|
+
callback(data);
|
|
115
|
+
}
|
|
116
|
+
}, { once: true }, selector, "point");
|
|
117
|
+
log.trace(`Un-Point listeners added for selector: ${selector}`, {
|
|
118
|
+
componentId: this.componentId,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Drag interaction - handles both mouse drag and touch drag
|
|
123
|
+
*/
|
|
124
|
+
drag(selector, callbacks) {
|
|
125
|
+
const elements = this.shadowRoot.querySelectorAll(selector);
|
|
126
|
+
elements.forEach((element) => {
|
|
127
|
+
// Mouse drag
|
|
128
|
+
this.addEventListener(element, "mousedown", (e) => {
|
|
129
|
+
this.startDrag(element, e, "mouse", callbacks);
|
|
130
|
+
});
|
|
131
|
+
// Touch drag
|
|
132
|
+
this.addEventListener(element, "touchstart", (e) => {
|
|
133
|
+
this.startDrag(element, e, "touch", callbacks);
|
|
134
|
+
}, { passive: false });
|
|
135
|
+
});
|
|
136
|
+
// Global mouse move and up listeners
|
|
137
|
+
this.addEventListener(document, "mousemove", (e) => {
|
|
138
|
+
this.handleDragMove(e, "mouse");
|
|
139
|
+
});
|
|
140
|
+
this.addEventListener(document, "mouseup", (e) => {
|
|
141
|
+
this.handleDragEnd(e, "mouse");
|
|
142
|
+
});
|
|
143
|
+
// Global touch move and end listeners
|
|
144
|
+
this.addEventListener(document, "touchmove", (e) => {
|
|
145
|
+
this.handleDragMove(e, "touch");
|
|
146
|
+
}, { passive: false });
|
|
147
|
+
this.addEventListener(document, "touchend", (e) => {
|
|
148
|
+
this.handleDragEnd(e, "touch");
|
|
149
|
+
});
|
|
150
|
+
log.trace(`Drag listeners added for selector: ${selector}`, {
|
|
151
|
+
componentId: this.componentId,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Hover interaction - mouse enter/leave with touch fallback
|
|
156
|
+
*/
|
|
157
|
+
hover(selector, callbacks) {
|
|
158
|
+
// Check if we already have listeners for this selector+method combination
|
|
159
|
+
const key = `hover:${selector}`;
|
|
160
|
+
if (this.selectorListeners.has(key)) {
|
|
161
|
+
log.trace(`Hover listeners already exist for selector: ${selector}, skipping duplicate`, {
|
|
162
|
+
componentId: this.componentId,
|
|
163
|
+
});
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
const elements = this.shadowRoot.querySelectorAll(selector);
|
|
167
|
+
// Store the selector listener mapping
|
|
168
|
+
this.selectorListeners.set(key, {
|
|
169
|
+
selector,
|
|
170
|
+
method: "hover",
|
|
171
|
+
callback: callbacks,
|
|
172
|
+
elements: Array.from(elements),
|
|
173
|
+
});
|
|
174
|
+
elements.forEach((element) => {
|
|
175
|
+
this.addEventListenerWithTracking(element, "mouseenter", (e) => {
|
|
176
|
+
if (callbacks.enter) {
|
|
177
|
+
const data = this.createPointerEventData(e, "mouse");
|
|
178
|
+
callbacks.enter(data);
|
|
179
|
+
}
|
|
180
|
+
}, undefined, selector, "hover");
|
|
181
|
+
this.addEventListenerWithTracking(element, "mouseleave", (e) => {
|
|
182
|
+
if (callbacks.leave) {
|
|
183
|
+
const data = this.createPointerEventData(e, "mouse");
|
|
184
|
+
callbacks.leave(data);
|
|
185
|
+
}
|
|
186
|
+
}, undefined, selector, "hover");
|
|
187
|
+
// Touch fallback - simulate hover with touch
|
|
188
|
+
this.addEventListenerWithTracking(element, "touchstart", (e) => {
|
|
189
|
+
if (callbacks.enter) {
|
|
190
|
+
const data = this.createPointerEventData(e, "touch");
|
|
191
|
+
callbacks.enter(data);
|
|
192
|
+
}
|
|
193
|
+
}, { passive: true }, selector, "hover");
|
|
194
|
+
});
|
|
195
|
+
log.trace(`Hover listeners added for selector: ${selector}`, {
|
|
196
|
+
componentId: this.componentId,
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Long press interaction - works for both mouse and touch
|
|
201
|
+
*/
|
|
202
|
+
longPress(selector, callback, duration = 500) {
|
|
203
|
+
const elements = this.shadowRoot.querySelectorAll(selector);
|
|
204
|
+
elements.forEach((element) => {
|
|
205
|
+
// Mouse long press
|
|
206
|
+
this.addEventListener(element, "mousedown", (e) => {
|
|
207
|
+
this.startLongPress(element, e, "mouse", callback, duration);
|
|
208
|
+
});
|
|
209
|
+
this.addEventListener(element, "mouseup", () => {
|
|
210
|
+
this.cancelLongPress(element);
|
|
211
|
+
});
|
|
212
|
+
this.addEventListener(element, "mouseleave", () => {
|
|
213
|
+
this.cancelLongPress(element);
|
|
214
|
+
});
|
|
215
|
+
// Touch long press
|
|
216
|
+
this.addEventListener(element, "touchstart", (e) => {
|
|
217
|
+
this.startLongPress(element, e, "touch", callback, duration);
|
|
218
|
+
}, { passive: true });
|
|
219
|
+
this.addEventListener(element, "touchend", () => {
|
|
220
|
+
this.cancelLongPress(element);
|
|
221
|
+
});
|
|
222
|
+
this.addEventListener(element, "touchcancel", () => {
|
|
223
|
+
this.cancelLongPress(element);
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
log.trace(`Long press listeners added for selector: ${selector}`, {
|
|
227
|
+
componentId: this.componentId,
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Swipe gesture detection
|
|
232
|
+
*/
|
|
233
|
+
swipe(selector, callbacks, threshold = 50) {
|
|
234
|
+
const elements = this.shadowRoot.querySelectorAll(selector);
|
|
235
|
+
elements.forEach((element) => {
|
|
236
|
+
// Touch swipe
|
|
237
|
+
this.addEventListener(element, "touchstart", (e) => {
|
|
238
|
+
this.startSwipe(element, e, callbacks);
|
|
239
|
+
}, { passive: true });
|
|
240
|
+
this.addEventListener(element, "touchend", (e) => {
|
|
241
|
+
this.endSwipe(element, e, callbacks, threshold);
|
|
242
|
+
});
|
|
243
|
+
// Mouse swipe (drag-based)
|
|
244
|
+
this.addEventListener(element, "mousedown", (e) => {
|
|
245
|
+
this.startSwipe(element, e, callbacks);
|
|
246
|
+
});
|
|
247
|
+
this.addEventListener(document, "mouseup", (e) => {
|
|
248
|
+
this.endSwipe(element, e, callbacks, threshold);
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
log.trace(`Swipe listeners added for selector: ${selector}`, {
|
|
252
|
+
componentId: this.componentId,
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Add a custom event listener with automatic cleanup
|
|
257
|
+
*/
|
|
258
|
+
addEventListener(element, type, listener, options) {
|
|
259
|
+
const finalOptions = {
|
|
260
|
+
...options,
|
|
261
|
+
signal: this.abortController.signal,
|
|
262
|
+
};
|
|
263
|
+
element.addEventListener(type, listener, finalOptions);
|
|
264
|
+
this.eventListeners.push({
|
|
265
|
+
element: element,
|
|
266
|
+
type,
|
|
267
|
+
listener,
|
|
268
|
+
options: finalOptions,
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Add an event listener with tracking for selector and method
|
|
273
|
+
*/
|
|
274
|
+
addEventListenerWithTracking(element, type, listener, options, selector, method) {
|
|
275
|
+
const finalOptions = {
|
|
276
|
+
...options,
|
|
277
|
+
signal: this.abortController.signal,
|
|
278
|
+
};
|
|
279
|
+
element.addEventListener(type, listener, finalOptions);
|
|
280
|
+
this.eventListeners.push({
|
|
281
|
+
element: element,
|
|
282
|
+
type,
|
|
283
|
+
listener,
|
|
284
|
+
options: finalOptions,
|
|
285
|
+
selector,
|
|
286
|
+
method,
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* Remove a specific event listener
|
|
291
|
+
*/
|
|
292
|
+
removeEventListener(element, type, listener) {
|
|
293
|
+
element.removeEventListener(type, listener);
|
|
294
|
+
const index = this.eventListeners.findIndex((record) => record.element === element && record.type === type && record.listener === listener);
|
|
295
|
+
if (index !== -1) {
|
|
296
|
+
this.eventListeners.splice(index, 1);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Clean up listeners for a specific selector and method
|
|
301
|
+
*/
|
|
302
|
+
clearSelectorListeners(selector, method) {
|
|
303
|
+
const key = `${method}:${selector}`;
|
|
304
|
+
if (this.selectorListeners.has(key)) {
|
|
305
|
+
// Remove from tracking
|
|
306
|
+
this.selectorListeners.delete(key);
|
|
307
|
+
// Remove actual event listeners for this selector+method
|
|
308
|
+
this.eventListeners = this.eventListeners.filter((record) => {
|
|
309
|
+
if (record.selector === selector && record.method === method) {
|
|
310
|
+
// Remove the actual event listener
|
|
311
|
+
record.element.removeEventListener(record.type, record.listener);
|
|
312
|
+
return false; // Remove from array
|
|
313
|
+
}
|
|
314
|
+
return true; // Keep in array
|
|
315
|
+
});
|
|
316
|
+
log.trace(`Cleared listeners for selector: ${selector}, method: ${method}`, { componentId: this.componentId });
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* Clean up all event listeners
|
|
321
|
+
*/
|
|
322
|
+
destroy() {
|
|
323
|
+
this.abortController.abort();
|
|
324
|
+
this.eventListeners.length = 0;
|
|
325
|
+
this.selectorListeners.clear();
|
|
326
|
+
this.dragStates.clear();
|
|
327
|
+
this.swipeStates.clear();
|
|
328
|
+
// Clear any pending long press timers
|
|
329
|
+
this.longPressTimers.forEach((timerId) => clearTimeout(timerId));
|
|
330
|
+
this.longPressTimers.clear();
|
|
331
|
+
log.trace(`EventManager destroyed for component: ${this.componentId}`);
|
|
332
|
+
}
|
|
333
|
+
// Private helper methods
|
|
334
|
+
createPointerEventData(event, type) {
|
|
335
|
+
let x, y;
|
|
336
|
+
if (type === "touch" && "touches" in event) {
|
|
337
|
+
const touch = event.touches[0] || event.changedTouches[0];
|
|
338
|
+
x = touch.clientX;
|
|
339
|
+
y = touch.clientY;
|
|
340
|
+
}
|
|
341
|
+
else if ("clientX" in event) {
|
|
342
|
+
x = event.clientX;
|
|
343
|
+
y = event.clientY;
|
|
344
|
+
}
|
|
345
|
+
else {
|
|
346
|
+
x = 0;
|
|
347
|
+
y = 0;
|
|
348
|
+
}
|
|
349
|
+
return {
|
|
350
|
+
x,
|
|
351
|
+
y,
|
|
352
|
+
target: event.target,
|
|
353
|
+
originalEvent: event,
|
|
354
|
+
type,
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
startDrag(element, event, type, callbacks) {
|
|
358
|
+
const data = this.createPointerEventData(event, type);
|
|
359
|
+
const dragState = {
|
|
360
|
+
isDragging: true,
|
|
361
|
+
startX: data.x,
|
|
362
|
+
startY: data.y,
|
|
363
|
+
element,
|
|
364
|
+
callbacks,
|
|
365
|
+
};
|
|
366
|
+
this.dragStates.set(element, dragState);
|
|
367
|
+
if (callbacks.start) {
|
|
368
|
+
callbacks.start(data);
|
|
369
|
+
}
|
|
370
|
+
event.preventDefault();
|
|
371
|
+
}
|
|
372
|
+
handleDragMove(event, type) {
|
|
373
|
+
this.dragStates.forEach((dragState, _) => {
|
|
374
|
+
if (dragState.isDragging && dragState.callbacks.move) {
|
|
375
|
+
const data = this.createPointerEventData(event, type);
|
|
376
|
+
dragState.callbacks.move(data);
|
|
377
|
+
}
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
handleDragEnd(event, type) {
|
|
381
|
+
this.dragStates.forEach((dragState, element) => {
|
|
382
|
+
if (dragState.isDragging) {
|
|
383
|
+
dragState.isDragging = false;
|
|
384
|
+
if (dragState.callbacks.end) {
|
|
385
|
+
const data = this.createPointerEventData(event, type);
|
|
386
|
+
dragState.callbacks.end(data);
|
|
387
|
+
}
|
|
388
|
+
this.dragStates.delete(element);
|
|
389
|
+
}
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
movementThresholdMet(startX, startY, endX, endY) {
|
|
393
|
+
const movementThreshold = 5; // Default movement threshold in pixels
|
|
394
|
+
const distance = Math.sqrt(Math.pow(endX - startX, 2) + Math.pow(endY - startY, 2));
|
|
395
|
+
return distance > movementThreshold;
|
|
396
|
+
}
|
|
397
|
+
startLongPress(element, event, type, callback, duration) {
|
|
398
|
+
this.cancelLongPress(element); // Cancel any existing timer
|
|
399
|
+
const timerId = window.setTimeout(() => {
|
|
400
|
+
const data = this.createPointerEventData(event, type);
|
|
401
|
+
callback(data);
|
|
402
|
+
this.longPressTimers.delete(element);
|
|
403
|
+
}, duration);
|
|
404
|
+
this.longPressTimers.set(element, timerId);
|
|
405
|
+
}
|
|
406
|
+
cancelLongPress(element) {
|
|
407
|
+
const timerId = this.longPressTimers.get(element);
|
|
408
|
+
if (timerId) {
|
|
409
|
+
clearTimeout(timerId);
|
|
410
|
+
this.longPressTimers.delete(element);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
startSwipe(element, event, callbacks) {
|
|
414
|
+
const data = this.createPointerEventData(event, "touch");
|
|
415
|
+
const swipeState = {
|
|
416
|
+
startX: data.x,
|
|
417
|
+
startY: data.y,
|
|
418
|
+
startTime: Date.now(),
|
|
419
|
+
element,
|
|
420
|
+
callbacks,
|
|
421
|
+
};
|
|
422
|
+
this.swipeStates.set(element, swipeState);
|
|
423
|
+
}
|
|
424
|
+
endSwipe(element, event, callbacks, threshold) {
|
|
425
|
+
const swipeState = this.swipeStates.get(element);
|
|
426
|
+
if (!swipeState)
|
|
427
|
+
return;
|
|
428
|
+
const data = this.createPointerEventData(event, "touch");
|
|
429
|
+
const deltaX = data.x - swipeState.startX;
|
|
430
|
+
const deltaY = data.y - swipeState.startY;
|
|
431
|
+
const deltaTime = Date.now() - swipeState.startTime;
|
|
432
|
+
// Only consider it a swipe if it was fast enough (< 300ms) and moved enough
|
|
433
|
+
if (deltaTime < 300 && (Math.abs(deltaX) > threshold || Math.abs(deltaY) > threshold)) {
|
|
434
|
+
if (Math.abs(deltaX) > Math.abs(deltaY)) {
|
|
435
|
+
// Horizontal swipe
|
|
436
|
+
if (deltaX > 0 && callbacks.right) {
|
|
437
|
+
callbacks.right(data);
|
|
438
|
+
}
|
|
439
|
+
else if (deltaX < 0 && callbacks.left) {
|
|
440
|
+
callbacks.left(data);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
else {
|
|
444
|
+
// Vertical swipe
|
|
445
|
+
if (deltaY > 0 && callbacks.down) {
|
|
446
|
+
callbacks.down(data);
|
|
447
|
+
}
|
|
448
|
+
else if (deltaY < 0 && callbacks.up) {
|
|
449
|
+
callbacks.up(data);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
this.swipeStates.delete(element);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { EventOrchestrator } from "./eventBus";
|
|
2
|
+
import type { AnimationManager } from "./animationBus";
|
|
3
|
+
export interface GarbageCollectionStats {
|
|
4
|
+
timestamp: number;
|
|
5
|
+
animationsCleaned: number;
|
|
6
|
+
expiredEventsCleaned: number;
|
|
7
|
+
orphanedTransitionsCleaned: number;
|
|
8
|
+
totalListeners: number;
|
|
9
|
+
queueSize: number;
|
|
10
|
+
activeAnimations: number;
|
|
11
|
+
activeTransitions: number;
|
|
12
|
+
}
|
|
13
|
+
export declare class GarbageCollector {
|
|
14
|
+
private orchestrator;
|
|
15
|
+
private animationManager?;
|
|
16
|
+
private cleanupInterval;
|
|
17
|
+
private stats;
|
|
18
|
+
private readonly MAX_STATS_HISTORY;
|
|
19
|
+
constructor(orchestrator: EventOrchestrator, animationManager?: AnimationManager);
|
|
20
|
+
/**
|
|
21
|
+
* Start automatic garbage collection at specified interval
|
|
22
|
+
* @param intervalMinutes - How often to run cleanup (default: 5 minutes)
|
|
23
|
+
*/
|
|
24
|
+
startAutoCleanup(intervalMinutes?: number): void;
|
|
25
|
+
/**
|
|
26
|
+
* Stop automatic garbage collection
|
|
27
|
+
*/
|
|
28
|
+
stopAutoCleanup(): void;
|
|
29
|
+
/**
|
|
30
|
+
* Manually trigger a garbage collection run
|
|
31
|
+
*/
|
|
32
|
+
runCleanup(): GarbageCollectionStats;
|
|
33
|
+
/**
|
|
34
|
+
* Get statistics from previous cleanup runs
|
|
35
|
+
*/
|
|
36
|
+
getStats(): GarbageCollectionStats[];
|
|
37
|
+
/**
|
|
38
|
+
* Get the most recent cleanup stats
|
|
39
|
+
*/
|
|
40
|
+
getLatestStats(): GarbageCollectionStats | null;
|
|
41
|
+
/**
|
|
42
|
+
* Get a summary report of memory usage trends
|
|
43
|
+
*/
|
|
44
|
+
getMemoryReport(): {
|
|
45
|
+
averageAnimations: number;
|
|
46
|
+
averageListeners: number;
|
|
47
|
+
averageQueueSize: number;
|
|
48
|
+
averageTransitions: number;
|
|
49
|
+
totalCleanups: number;
|
|
50
|
+
totalItemsCleaned: number;
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
* Clear all stats history
|
|
54
|
+
*/
|
|
55
|
+
clearStats(): void;
|
|
56
|
+
/**
|
|
57
|
+
* Destroy the garbage collector and stop auto cleanup
|
|
58
|
+
*/
|
|
59
|
+
destroy(): void;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Create and configure a garbage collector instance
|
|
63
|
+
*
|
|
64
|
+
* Note: NavigationManager is automatically retrieved via singleton pattern
|
|
65
|
+
* when needed during cleanup operations.
|
|
66
|
+
*/
|
|
67
|
+
export declare function createGarbageCollector(orchestrator: EventOrchestrator, animationManager?: AnimationManager, autoStart?: boolean, intervalMinutes?: number): GarbageCollector;
|
|
68
|
+
//# sourceMappingURL=garbageCollector.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"garbageCollector.d.ts","sourceRoot":"","sources":["../../src/garbageCollector.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AACpD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAKvD,MAAM,WAAW,sBAAsB;IACrC,SAAS,EAAE,MAAM,CAAC;IAClB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,oBAAoB,EAAE,MAAM,CAAC;IAC7B,0BAA0B,EAAE,MAAM,CAAC;IACnC,cAAc,EAAE,MAAM,CAAC;IACvB,SAAS,EAAE,MAAM,CAAC;IAClB,gBAAgB,EAAE,MAAM,CAAC;IACzB,iBAAiB,EAAE,MAAM,CAAC;CAC3B;AAED,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,YAAY,CAAoB;IACxC,OAAO,CAAC,gBAAgB,CAAC,CAAmB;IAC5C,OAAO,CAAC,eAAe,CAAuB;IAC9C,OAAO,CAAC,KAAK,CAAgC;IAC7C,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAM;gBAGtC,YAAY,EAAE,iBAAiB,EAC/B,gBAAgB,CAAC,EAAE,gBAAgB;IAMrC;;;OAGG;IACI,gBAAgB,CAAC,eAAe,GAAE,MAAU,GAAG,IAAI;IAiB1D;;OAEG;IACI,eAAe,IAAI,IAAI;IAQ9B;;OAEG;IACI,UAAU,IAAI,sBAAsB;IAqE3C;;OAEG;IACI,QAAQ,IAAI,sBAAsB,EAAE;IAI3C;;OAEG;IACI,cAAc,IAAI,sBAAsB,GAAG,IAAI;IAItD;;OAEG;IACI,eAAe,IAAI;QACxB,iBAAiB,EAAE,MAAM,CAAC;QAC1B,gBAAgB,EAAE,MAAM,CAAC;QACzB,gBAAgB,EAAE,MAAM,CAAC;QACzB,kBAAkB,EAAE,MAAM,CAAC;QAC3B,aAAa,EAAE,MAAM,CAAC;QACtB,iBAAiB,EAAE,MAAM,CAAC;KAC3B;IAiCD;;OAEG;IACI,UAAU,IAAI,IAAI;IAIzB;;OAEG;IACI,OAAO,IAAI,IAAI;CAKvB;AAED;;;;;GAKG;AACH,wBAAgB,sBAAsB,CACpC,YAAY,EAAE,iBAAiB,EAC/B,gBAAgB,CAAC,EAAE,gBAAgB,EACnC,SAAS,GAAE,OAAc,EACzB,eAAe,GAAE,MAAU,GAC1B,gBAAgB,CAQlB"}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { logger } from "./logger";
|
|
2
|
+
import { NavigationManager } from "./navigationManager";
|
|
3
|
+
const log = logger;
|
|
4
|
+
export class GarbageCollector {
|
|
5
|
+
orchestrator;
|
|
6
|
+
animationManager;
|
|
7
|
+
cleanupInterval = null;
|
|
8
|
+
stats = [];
|
|
9
|
+
MAX_STATS_HISTORY = 50; // Keep last 50 cleanup runs
|
|
10
|
+
constructor(orchestrator, animationManager) {
|
|
11
|
+
this.orchestrator = orchestrator;
|
|
12
|
+
this.animationManager = animationManager;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Start automatic garbage collection at specified interval
|
|
16
|
+
* @param intervalMinutes - How often to run cleanup (default: 5 minutes)
|
|
17
|
+
*/
|
|
18
|
+
startAutoCleanup(intervalMinutes = 5) {
|
|
19
|
+
if (this.cleanupInterval !== null) {
|
|
20
|
+
log.warn("Auto cleanup already running");
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
const intervalMs = intervalMinutes * 60 * 1000;
|
|
24
|
+
log.trace(`Starting automatic garbage collection every ${intervalMinutes} minute(s)`);
|
|
25
|
+
this.cleanupInterval = window.setInterval(() => {
|
|
26
|
+
this.runCleanup();
|
|
27
|
+
}, intervalMs);
|
|
28
|
+
// Run initial cleanup
|
|
29
|
+
this.runCleanup();
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Stop automatic garbage collection
|
|
33
|
+
*/
|
|
34
|
+
stopAutoCleanup() {
|
|
35
|
+
if (this.cleanupInterval !== null) {
|
|
36
|
+
clearInterval(this.cleanupInterval);
|
|
37
|
+
this.cleanupInterval = null;
|
|
38
|
+
log.trace("Automatic garbage collection stopped");
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Manually trigger a garbage collection run
|
|
43
|
+
*/
|
|
44
|
+
runCleanup() {
|
|
45
|
+
const startTime = Date.now();
|
|
46
|
+
log.trace("Running garbage collection...");
|
|
47
|
+
const stats = {
|
|
48
|
+
timestamp: startTime,
|
|
49
|
+
animationsCleaned: 0,
|
|
50
|
+
expiredEventsCleaned: 0,
|
|
51
|
+
orphanedTransitionsCleaned: 0,
|
|
52
|
+
totalListeners: 0,
|
|
53
|
+
queueSize: 0,
|
|
54
|
+
activeAnimations: 0,
|
|
55
|
+
activeTransitions: 0,
|
|
56
|
+
};
|
|
57
|
+
// Clean up stale animations
|
|
58
|
+
if (this.animationManager) {
|
|
59
|
+
const beforeAnimations = this.animationManager.getActiveAnimationCount();
|
|
60
|
+
this.animationManager.cleanupStaleAnimations();
|
|
61
|
+
const afterAnimations = this.animationManager.getActiveAnimationCount();
|
|
62
|
+
stats.animationsCleaned = beforeAnimations - afterAnimations;
|
|
63
|
+
stats.activeAnimations = afterAnimations;
|
|
64
|
+
}
|
|
65
|
+
// Clean up expired events from queue
|
|
66
|
+
const expiredEvents = this.orchestrator.cleanupExpiredEvents();
|
|
67
|
+
stats.expiredEventsCleaned = expiredEvents;
|
|
68
|
+
stats.queueSize = this.orchestrator.getQueueSize();
|
|
69
|
+
stats.totalListeners = this.orchestrator.getTotalListenerCount();
|
|
70
|
+
// Clean up orphaned navigation transitions - get singleton instance
|
|
71
|
+
const navigationManager = NavigationManager.getInstance();
|
|
72
|
+
if (navigationManager) {
|
|
73
|
+
const beforeTransitions = navigationManager.getActiveTransitionCount();
|
|
74
|
+
navigationManager.cleanupOrphanedTransitions();
|
|
75
|
+
const afterTransitions = navigationManager.getActiveTransitionCount();
|
|
76
|
+
stats.orphanedTransitionsCleaned = beforeTransitions - afterTransitions;
|
|
77
|
+
stats.activeTransitions = afterTransitions;
|
|
78
|
+
}
|
|
79
|
+
const duration = Date.now() - startTime;
|
|
80
|
+
// Log summary
|
|
81
|
+
const totalCleaned = stats.animationsCleaned + stats.expiredEventsCleaned + stats.orphanedTransitionsCleaned;
|
|
82
|
+
if (totalCleaned > 0) {
|
|
83
|
+
log.trace(`Garbage collection complete in ${duration}ms. Cleaned: ${stats.animationsCleaned} animations, ` +
|
|
84
|
+
`${stats.expiredEventsCleaned} expired events, ${stats.orphanedTransitionsCleaned} transitions. ` +
|
|
85
|
+
`Active: ${stats.activeAnimations} animations, ${stats.activeTransitions} transitions, ` +
|
|
86
|
+
`${stats.totalListeners} listeners, queue size: ${stats.queueSize}`);
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
log.trace(`Garbage collection complete in ${duration}ms. Nothing to clean. ` +
|
|
90
|
+
`Active: ${stats.activeAnimations} animations, ${stats.activeTransitions} transitions, ` +
|
|
91
|
+
`${stats.totalListeners} listeners, queue size: ${stats.queueSize}`);
|
|
92
|
+
}
|
|
93
|
+
// Store stats
|
|
94
|
+
this.stats.push(stats);
|
|
95
|
+
if (this.stats.length > this.MAX_STATS_HISTORY) {
|
|
96
|
+
this.stats.shift(); // Remove oldest
|
|
97
|
+
}
|
|
98
|
+
return stats;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Get statistics from previous cleanup runs
|
|
102
|
+
*/
|
|
103
|
+
getStats() {
|
|
104
|
+
return [...this.stats];
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Get the most recent cleanup stats
|
|
108
|
+
*/
|
|
109
|
+
getLatestStats() {
|
|
110
|
+
return this.stats.length > 0 ? this.stats[this.stats.length - 1] : null;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Get a summary report of memory usage trends
|
|
114
|
+
*/
|
|
115
|
+
getMemoryReport() {
|
|
116
|
+
if (this.stats.length === 0) {
|
|
117
|
+
return {
|
|
118
|
+
averageAnimations: 0,
|
|
119
|
+
averageListeners: 0,
|
|
120
|
+
averageQueueSize: 0,
|
|
121
|
+
averageTransitions: 0,
|
|
122
|
+
totalCleanups: 0,
|
|
123
|
+
totalItemsCleaned: 0,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
const sum = this.stats.reduce((acc, stat) => ({
|
|
127
|
+
animations: acc.animations + stat.activeAnimations,
|
|
128
|
+
listeners: acc.listeners + stat.totalListeners,
|
|
129
|
+
queueSize: acc.queueSize + stat.queueSize,
|
|
130
|
+
transitions: acc.transitions + stat.activeTransitions,
|
|
131
|
+
cleaned: acc.cleaned + stat.animationsCleaned + stat.expiredEventsCleaned + stat.orphanedTransitionsCleaned,
|
|
132
|
+
}), { animations: 0, listeners: 0, queueSize: 0, transitions: 0, cleaned: 0 });
|
|
133
|
+
return {
|
|
134
|
+
averageAnimations: sum.animations / this.stats.length,
|
|
135
|
+
averageListeners: sum.listeners / this.stats.length,
|
|
136
|
+
averageQueueSize: sum.queueSize / this.stats.length,
|
|
137
|
+
averageTransitions: sum.transitions / this.stats.length,
|
|
138
|
+
totalCleanups: this.stats.length,
|
|
139
|
+
totalItemsCleaned: sum.cleaned,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Clear all stats history
|
|
144
|
+
*/
|
|
145
|
+
clearStats() {
|
|
146
|
+
this.stats = [];
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Destroy the garbage collector and stop auto cleanup
|
|
150
|
+
*/
|
|
151
|
+
destroy() {
|
|
152
|
+
this.stopAutoCleanup();
|
|
153
|
+
this.clearStats();
|
|
154
|
+
log.trace("GarbageCollector destroyed");
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Create and configure a garbage collector instance
|
|
159
|
+
*
|
|
160
|
+
* Note: NavigationManager is automatically retrieved via singleton pattern
|
|
161
|
+
* when needed during cleanup operations.
|
|
162
|
+
*/
|
|
163
|
+
export function createGarbageCollector(orchestrator, animationManager, autoStart = true, intervalMinutes = 5) {
|
|
164
|
+
const gc = new GarbageCollector(orchestrator, animationManager);
|
|
165
|
+
if (autoStart) {
|
|
166
|
+
gc.startAutoCleanup(intervalMinutes);
|
|
167
|
+
}
|
|
168
|
+
return gc;
|
|
169
|
+
}
|