@panyam/tsappkit 0.0.3
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 +98 -0
- package/dist/index.d.mts +866 -0
- package/dist/index.d.ts +866 -0
- package/dist/index.js +1736 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +1718 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +43 -0
- package/src/BasePage.ts +270 -0
- package/src/Component.ts +178 -0
- package/src/DOMUtils.ts +72 -0
- package/src/EventBus.ts +272 -0
- package/src/KeyboardShortcutManager.ts +435 -0
- package/src/LCMComponent.ts +140 -0
- package/src/LifecycleController.ts +213 -0
- package/src/MobileBottomDrawer.ts +236 -0
- package/src/Modal.ts +233 -0
- package/src/SplashScreen.ts +111 -0
- package/src/TemplateLoader.ts +101 -0
- package/src/ThemeManager.ts +100 -0
- package/src/ToastManager.ts +161 -0
- package/src/events.ts +5 -0
- package/src/index.ts +39 -0
package/src/Component.ts
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { EventBus, EventSubscriber, ComponentEventTypes } from './EventBus';
|
|
2
|
+
import { LCMComponent } from './LCMComponent';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Base interface for all UI components
|
|
6
|
+
* Enforces separation of concerns and standard lifecycle
|
|
7
|
+
*/
|
|
8
|
+
export interface Component {
|
|
9
|
+
/**
|
|
10
|
+
* Unique identifier for this component instance
|
|
11
|
+
*/
|
|
12
|
+
readonly componentId: string;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Root DOM element that this component owns and manages
|
|
16
|
+
*/
|
|
17
|
+
readonly rootElement: HTMLElement;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Handle dynamic content updates (e.g., from HTMX or server responses)
|
|
21
|
+
* @param newHTML - New HTML content to replace current content
|
|
22
|
+
*/
|
|
23
|
+
contentUpdated(newHTML: string): void;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Clean up the component and release resources
|
|
27
|
+
* Should unsubscribe from events, clean up DOM, and release memory
|
|
28
|
+
*/
|
|
29
|
+
// destroy(): void;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Abstract base class implementing common component functionality
|
|
34
|
+
* Provides standard lifecycle management and event bus integration
|
|
35
|
+
*
|
|
36
|
+
* All components auto-initialize in constructor AND implement LCMComponent
|
|
37
|
+
* for coordination with other components when needed.
|
|
38
|
+
*/
|
|
39
|
+
export abstract class BaseComponent implements Component, LCMComponent, EventSubscriber {
|
|
40
|
+
protected _eventBus: EventBus;
|
|
41
|
+
|
|
42
|
+
constructor(public readonly componentId: string, public readonly rootElement: HTMLElement, eventBus: EventBus | null = null, public readonly debugMode: boolean = false) {
|
|
43
|
+
// Mark as component in DOM for debugging
|
|
44
|
+
this._eventBus = eventBus || new EventBus();
|
|
45
|
+
this.rootElement.setAttribute('data-component', this.componentId);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
public get eventBus(): EventBus {
|
|
49
|
+
return this._eventBus
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
public contentUpdated(newHTML: string): void {
|
|
53
|
+
this.log('Content updated, re-binding to DOM');
|
|
54
|
+
|
|
55
|
+
// Update the DOM
|
|
56
|
+
this.rootElement.innerHTML = newHTML;
|
|
57
|
+
|
|
58
|
+
// Note: In pure LCMComponent approach, re-binding should be handled
|
|
59
|
+
// by the component's LCMComponent lifecycle methods if needed
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Subscribe to an event using the new EventSubscriber pattern
|
|
64
|
+
*/
|
|
65
|
+
protected addSubscription(eventType: string, target: any = null): void {
|
|
66
|
+
this.eventBus.addSubscription(eventType, target, this);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Unsubscribe from an event using the new EventSubscriber pattern
|
|
71
|
+
*/
|
|
72
|
+
protected removeSubscription(eventType: string, target: any = null): void {
|
|
73
|
+
this.eventBus.removeSubscription(eventType, target, this);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Emit an event from this component
|
|
78
|
+
*/
|
|
79
|
+
protected emit<T = any>(eventType: string, data: T, target: any, emitter: any = null): void {
|
|
80
|
+
this.eventBus.emit(eventType, data, target, emitter || this);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Default implementation of EventSubscriber interface
|
|
85
|
+
* Components can override this to handle events
|
|
86
|
+
*/
|
|
87
|
+
public handleBusEvent(eventType: string, data: any, target: any, emitter: any): void {
|
|
88
|
+
// Default: no event handling
|
|
89
|
+
// Subclasses should override this method to handle specific events
|
|
90
|
+
if (this.debugMode) {
|
|
91
|
+
console.log(`[${this.componentId}] Received unhandled event: ${eventType}`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Find elements within this component's root element only
|
|
97
|
+
* Enforces separation of concerns - no cross-component DOM access
|
|
98
|
+
*/
|
|
99
|
+
protected findElement<T extends HTMLElement = HTMLElement>(selector: string): T | null {
|
|
100
|
+
return this.rootElement.querySelector<T>(selector);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Find multiple elements within this component's root element only
|
|
105
|
+
*/
|
|
106
|
+
protected findElements<T extends HTMLElement = HTMLElement>(selector: string): T[] {
|
|
107
|
+
return Array.from(this.rootElement.querySelectorAll<T>(selector));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Log messages with component identification
|
|
112
|
+
*/
|
|
113
|
+
protected log(message: string, data: any = null): void {
|
|
114
|
+
if (this.debugMode) {
|
|
115
|
+
console.log(`[${this.componentId}] ${message}`, data);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Default lifecycle method: discover and return child components
|
|
121
|
+
* Override this if your component creates child components that need lifecycle management
|
|
122
|
+
*/
|
|
123
|
+
public performLocalInit(): Promise<LCMComponent[]> | LCMComponent[] {
|
|
124
|
+
// Default: no child components
|
|
125
|
+
return [];
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Default lifecycle method: inject dependencies
|
|
130
|
+
* Override this if your component needs dependencies from other components
|
|
131
|
+
*/
|
|
132
|
+
public setupDependencies(): void | Promise<void> {
|
|
133
|
+
// Default: no dependencies needed
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Default lifecycle method: activate component for coordination
|
|
138
|
+
* Override this if your component needs to coordinate with other components after initialization
|
|
139
|
+
*/
|
|
140
|
+
public activate(): void | Promise<void> {
|
|
141
|
+
// Default: no coordination needed - component is already auto-initialized
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Default lifecycle method: deactivate component
|
|
146
|
+
* Override this if your component needs cleanup during lifecycle management
|
|
147
|
+
*/
|
|
148
|
+
public deactivate(): void | Promise<void> {
|
|
149
|
+
// Remove component marker from DOM
|
|
150
|
+
this.rootElement?.removeAttribute('data-component');
|
|
151
|
+
|
|
152
|
+
this.log('Component deactivated successfully');
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
set innerHTML(innerHTML: string) {
|
|
156
|
+
const [newHTML, allow] = this.shouldUpdateHtml(innerHTML)
|
|
157
|
+
if (allow) {
|
|
158
|
+
this.rootElement.innerHTML = newHTML
|
|
159
|
+
this.htmlUpdated(newHTML)
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Called BEFORE the HTML is about to be updated. This is an opportunity
|
|
164
|
+
// for subclasses to fix/tweak html being updated and also ignore an update
|
|
165
|
+
// just in case. Default behavior is to return the html as is
|
|
166
|
+
shouldUpdateHtml(html: string): [string, boolean] {
|
|
167
|
+
return [html, true]
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Called after the HTML for the component has been updated.
|
|
171
|
+
// This is an opportunity to rebind any event listeners etc
|
|
172
|
+
htmlUpdated(html: string) {
|
|
173
|
+
// Do nothing
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// LCMComponent implementation with default empty methods
|
|
177
|
+
// Components can override these when they need coordination with other components
|
|
178
|
+
}
|
package/src/DOMUtils.ts
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Common DOM utility functions for consistent behavior across components
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Checks if the user is currently typing in an input field, textarea, or other editable element.
|
|
7
|
+
* This is used to prevent keyboard shortcuts from interfering with user input.
|
|
8
|
+
*
|
|
9
|
+
* @param element - The target element from a keyboard event
|
|
10
|
+
* @returns true if the user is in an input context, false otherwise
|
|
11
|
+
*/
|
|
12
|
+
export function isInInputContext(element: HTMLElement | null): boolean {
|
|
13
|
+
if (!element) return false;
|
|
14
|
+
|
|
15
|
+
const tagName = element.tagName.toLowerCase();
|
|
16
|
+
|
|
17
|
+
// Direct input elements
|
|
18
|
+
if (tagName === 'input' || tagName === 'textarea' || tagName === 'select') {
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Contenteditable elements
|
|
23
|
+
if (element.contentEditable === 'true' || element.isContentEditable) {
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Check if element is inside input-related containers
|
|
28
|
+
if (element.closest('input') !== null ||
|
|
29
|
+
element.closest('textarea') !== null ||
|
|
30
|
+
element.closest('select') !== null ||
|
|
31
|
+
element.closest('[contenteditable="true"]') !== null ||
|
|
32
|
+
element.closest('[contenteditable=""]') !== null) {
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Check if element is inside a modal (modals often contain forms)
|
|
37
|
+
if (element.closest('.modal') !== null) {
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Check for specific input field IDs that might be problematic
|
|
42
|
+
const inputFieldIds = ['map-title-input'];
|
|
43
|
+
if (inputFieldIds.includes(element.id) ||
|
|
44
|
+
inputFieldIds.some(id => element.closest(`#${id}`) !== null)) {
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Checks if modifier keys are pressed (Ctrl, Alt, Cmd, Shift).
|
|
53
|
+
* This is commonly used to filter keyboard shortcuts.
|
|
54
|
+
*
|
|
55
|
+
* @param event - The keyboard event
|
|
56
|
+
* @returns true if any modifier keys are pressed, false otherwise
|
|
57
|
+
*/
|
|
58
|
+
export function hasModifierKeys(event: KeyboardEvent): boolean {
|
|
59
|
+
return event.ctrlKey || event.altKey || event.metaKey || event.shiftKey;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Combined check for whether keyboard shortcuts should be ignored.
|
|
64
|
+
* This checks both modifier keys and input context.
|
|
65
|
+
*
|
|
66
|
+
* @param event - The keyboard event
|
|
67
|
+
* @returns true if shortcuts should be ignored, false if they can be processed
|
|
68
|
+
*/
|
|
69
|
+
export function shouldIgnoreShortcut(event: KeyboardEvent): boolean {
|
|
70
|
+
const target = event.target as HTMLElement;
|
|
71
|
+
return hasModifierKeys(event) || isInInputContext(target);
|
|
72
|
+
}
|
package/src/EventBus.ts
ADDED
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Simple event system for component communication
|
|
3
|
+
* Provides error isolation and idempotent subscriptions
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Simple event handler function type
|
|
8
|
+
*/
|
|
9
|
+
export type EventHandler = (data?: any) => void;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Interface for components that want to receive events via the EventBus
|
|
13
|
+
*/
|
|
14
|
+
export interface EventSubscriber {
|
|
15
|
+
/**
|
|
16
|
+
* Handle incoming events from the EventBus
|
|
17
|
+
* @param eventType - The type of event being handled
|
|
18
|
+
* @param data - The event data payload
|
|
19
|
+
* @param subject - The subject/subject entity (what the event is about)
|
|
20
|
+
* @param emitter - The entity that emitted the event
|
|
21
|
+
*/
|
|
22
|
+
handleBusEvent(eventType: string, data: any, subject: any, emitter: any): void;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Internal wrapper to adapt a simple function handler to EventSubscriber interface
|
|
27
|
+
*/
|
|
28
|
+
class FunctionSubscriber implements EventSubscriber {
|
|
29
|
+
constructor(public readonly handler: EventHandler) {}
|
|
30
|
+
|
|
31
|
+
handleBusEvent(eventType: string, data: any, subject: any, emitter: any): void {
|
|
32
|
+
this.handler(data);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Synchronous event bus for component communication
|
|
38
|
+
* Features:
|
|
39
|
+
* - Type-safe event names and payloads
|
|
40
|
+
* - Error isolation (one handler failure doesn't stop others)
|
|
41
|
+
* - Source exclusion (events not sent back to source)
|
|
42
|
+
* - Debug logging for troubleshooting
|
|
43
|
+
*/
|
|
44
|
+
export class EventBus {
|
|
45
|
+
private subscribers: Map<string, Set<EventSubscriber>> = new Map();
|
|
46
|
+
private functionSubscribers: Map<string, Map<EventHandler, FunctionSubscriber>> = new Map();
|
|
47
|
+
private onceHandlers: Map<string, Set<FunctionSubscriber>> = new Map();
|
|
48
|
+
private debugMode: boolean = false;
|
|
49
|
+
|
|
50
|
+
constructor(debugMode: boolean = false) {
|
|
51
|
+
this.debugMode = debugMode;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ============== Simple Function-Based API ==============
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Subscribe to an event with a simple handler function
|
|
58
|
+
*/
|
|
59
|
+
public on(eventType: string, handler: EventHandler): void {
|
|
60
|
+
if (!this.functionSubscribers.has(eventType)) {
|
|
61
|
+
this.functionSubscribers.set(eventType, new Map());
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const handlersMap = this.functionSubscribers.get(eventType)!;
|
|
65
|
+
if (handlersMap.has(handler)) {
|
|
66
|
+
if (this.debugMode) {
|
|
67
|
+
console.log(`[EventBus] Handler already registered for '${eventType}'`);
|
|
68
|
+
}
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const subscriber = new FunctionSubscriber(handler);
|
|
73
|
+
handlersMap.set(handler, subscriber);
|
|
74
|
+
this.addSubscription(eventType, null, subscriber);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Unsubscribe a handler function from an event
|
|
79
|
+
*/
|
|
80
|
+
public off(eventType: string, handler: EventHandler): void {
|
|
81
|
+
const handlersMap = this.functionSubscribers.get(eventType);
|
|
82
|
+
if (!handlersMap) return;
|
|
83
|
+
|
|
84
|
+
const subscriber = handlersMap.get(handler);
|
|
85
|
+
if (subscriber) {
|
|
86
|
+
handlersMap.delete(handler);
|
|
87
|
+
this.removeSubscription(eventType, null, subscriber);
|
|
88
|
+
|
|
89
|
+
if (handlersMap.size === 0) {
|
|
90
|
+
this.functionSubscribers.delete(eventType);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Subscribe to an event for one-time execution
|
|
97
|
+
*/
|
|
98
|
+
public once(eventType: string, handler: EventHandler): void {
|
|
99
|
+
if (!this.onceHandlers.has(eventType)) {
|
|
100
|
+
this.onceHandlers.set(eventType, new Set());
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const subscriber = new FunctionSubscriber(handler);
|
|
104
|
+
this.onceHandlers.get(eventType)!.add(subscriber);
|
|
105
|
+
|
|
106
|
+
if (this.debugMode) {
|
|
107
|
+
console.log(`[EventBus] Added once handler for '${eventType}'`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ============== EventSubscriber-Based API ==============
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Add a subscription using the EventSubscriber pattern
|
|
115
|
+
* Provides automatic idempotency - same subscriber object won't be added twice
|
|
116
|
+
*/
|
|
117
|
+
public addSubscription(eventType: string, subject: any, subscriber: EventSubscriber): void {
|
|
118
|
+
if (!this.subscribers.has(eventType)) {
|
|
119
|
+
this.subscribers.set(eventType, new Set());
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const subscribers = this.subscribers.get(eventType)!;
|
|
123
|
+
const wasAdded = !subscribers.has(subscriber);
|
|
124
|
+
|
|
125
|
+
if (wasAdded) {
|
|
126
|
+
subscribers.add(subscriber);
|
|
127
|
+
if (this.debugMode) {
|
|
128
|
+
console.log(`[EventBus] Added subscription to '${eventType}' for ${subscriber.constructor.name}`);
|
|
129
|
+
}
|
|
130
|
+
} else if (this.debugMode) {
|
|
131
|
+
console.log(`[EventBus] Subscription already exists for '${eventType}' and ${subscriber.constructor.name}`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Remove a subscription using the EventSubscriber pattern
|
|
137
|
+
*/
|
|
138
|
+
public removeSubscription(eventType: string, subject: any, subscriber: EventSubscriber): void {
|
|
139
|
+
const subscribers = this.subscribers.get(eventType);
|
|
140
|
+
|
|
141
|
+
if (subscribers) {
|
|
142
|
+
const wasRemoved = subscribers.delete(subscriber);
|
|
143
|
+
|
|
144
|
+
if (this.debugMode && wasRemoved) {
|
|
145
|
+
console.log(`[EventBus] Removed subscription from '${eventType}' for ${subscriber.constructor.name}`);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Clean up empty subscription sets
|
|
149
|
+
if (subscribers.size === 0) {
|
|
150
|
+
this.subscribers.delete(eventType);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Emit an event to all subscribers
|
|
157
|
+
* @param eventType - The event type to emit
|
|
158
|
+
* @param data - The event data payload
|
|
159
|
+
* @param subject - The subject/subject entity that this event relates to (optional for simple API)
|
|
160
|
+
* @param emitter - The entity that emitted the event (optional for simple API)
|
|
161
|
+
*/
|
|
162
|
+
public emit<T = any>(eventType: string, data?: T, subject?: any, emitter?: any): void {
|
|
163
|
+
const subscribers = this.subscribers.get(eventType);
|
|
164
|
+
const onceHandlers = this.onceHandlers.get(eventType);
|
|
165
|
+
|
|
166
|
+
const subscriberCount = (subscribers?.size || 0) + (onceHandlers?.size || 0);
|
|
167
|
+
if (subscriberCount === 0) {
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (this.debugMode) {
|
|
172
|
+
console.log(`[EventBus] Emitting '${eventType}' to ${subscriberCount} subscribers`);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
let successCount = 0;
|
|
176
|
+
let errorCount = 0;
|
|
177
|
+
|
|
178
|
+
// Call regular EventSubscriber handlers with error isolation
|
|
179
|
+
if (subscribers) {
|
|
180
|
+
subscribers.forEach(subscriber => {
|
|
181
|
+
try {
|
|
182
|
+
subscriber.handleBusEvent(eventType, data, subject, emitter);
|
|
183
|
+
successCount++;
|
|
184
|
+
} catch (error) {
|
|
185
|
+
errorCount++;
|
|
186
|
+
console.error(
|
|
187
|
+
`[EventBus] Error in EventSubscriber handler for '${eventType}' ` +
|
|
188
|
+
`in subscriber '${subscriber.constructor.name}':`,
|
|
189
|
+
error
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Call one-time handlers and then remove them
|
|
196
|
+
if (onceHandlers && onceHandlers.size > 0) {
|
|
197
|
+
onceHandlers.forEach(subscriber => {
|
|
198
|
+
try {
|
|
199
|
+
subscriber.handleBusEvent(eventType, data, subject, emitter);
|
|
200
|
+
successCount++;
|
|
201
|
+
} catch (error) {
|
|
202
|
+
errorCount++;
|
|
203
|
+
console.error(
|
|
204
|
+
`[EventBus] Error in once handler for '${eventType}':`,
|
|
205
|
+
error
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
// Clear once handlers after execution
|
|
210
|
+
this.onceHandlers.delete(eventType);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (this.debugMode) {
|
|
214
|
+
console.log(
|
|
215
|
+
`[EventBus] Event '${eventType}' completed: ` +
|
|
216
|
+
`${successCount} success, ${errorCount} errors`
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Get all event types that have subscribers
|
|
223
|
+
*/
|
|
224
|
+
public getEventTypes(): string[] {
|
|
225
|
+
return Array.from(this.subscribers.keys());
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Get subscriber count for an event type
|
|
230
|
+
*/
|
|
231
|
+
public getSubscriberCount(eventType: string): number {
|
|
232
|
+
return this.subscribers.get(eventType)?.size || 0;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Clear all subscriptions (useful for cleanup)
|
|
237
|
+
*/
|
|
238
|
+
public clear(): void {
|
|
239
|
+
this.subscribers.clear();
|
|
240
|
+
this.functionSubscribers.clear();
|
|
241
|
+
this.onceHandlers.clear();
|
|
242
|
+
if (this.debugMode) {
|
|
243
|
+
console.log('[EventBus] All subscriptions cleared');
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Enable or disable debug logging
|
|
249
|
+
*/
|
|
250
|
+
public setDebugMode(enabled: boolean): void {
|
|
251
|
+
this.debugMode = enabled;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export const ComponentEventTypes= {
|
|
256
|
+
COMPONENT_INITIALIZED: 'component-initialized',
|
|
257
|
+
COMPONENT_HYDRATED: 'component-hydrated',
|
|
258
|
+
COMPONENT_ERROR: 'component-error'
|
|
259
|
+
} as const;
|
|
260
|
+
|
|
261
|
+
export const LifecycleEventTypes = {
|
|
262
|
+
LOCAL_INIT_STARTED: "lifecycle-local-init-started",
|
|
263
|
+
LOCAL_INIT_FINISHED: "lifecycle-local-init-finished",
|
|
264
|
+
DEPENDENCIES_INJECTED: "lifecycle-dependencies-injected",
|
|
265
|
+
ACTIVATION_STARTED: "lifecycle-activation-started",
|
|
266
|
+
ACTIVATION_FINISHED: "lifecycle-activation-finished",
|
|
267
|
+
DEACTIVATION_STARTED: "lifecycle-deactivation-started",
|
|
268
|
+
DEACTIVATION_FINISHED: "lifecycle-deactivation-finished",
|
|
269
|
+
} as const;
|
|
270
|
+
|
|
271
|
+
export type LifecycleEventType = typeof LifecycleEventTypes[keyof typeof LifecycleEventTypes];
|
|
272
|
+
export type ComponentEventType = typeof ComponentEventTypes[keyof typeof ComponentEventTypes];
|