@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.
@@ -0,0 +1,213 @@
1
+
2
+ import {
3
+ LCMComponent,
4
+ LCMComponentConfig,
5
+ LCMComponentEvent,
6
+ } from './LCMComponent';
7
+ import { EventBus, LifecycleEventTypes, LifecycleEventType } from './EventBus';
8
+ import { ComponentLifecycleEvent } from './events';
9
+
10
+ /**
11
+ * LifecycleController - Orchestrates breadth-first component initialization
12
+ *
13
+ * This controller implements a breadth-first traversal of the component tree
14
+ * followed by phase-wise initialization with synchronization barriers.
15
+ *
16
+ * Process:
17
+ * 1. Discovery Phase: Traverse component tree to find all components
18
+ * 2. DOM Binding Phase: All components bind to DOM simultaneously
19
+ * 3. Dependency Injection Phase: All components receive dependencies
20
+ * 4. Activation Phase: All components complete initialization
21
+ *
22
+ * Each phase acts as a synchronization barrier - no component can proceed
23
+ * to the next phase until ALL components have completed the current phase.
24
+ */
25
+ export class LifecycleController {
26
+ private allComponents: LCMComponent[];
27
+ private componentsByLevel: LCMComponent[][];
28
+ private config: Required<LCMComponentConfig>;
29
+ public static DefaultConfig: LCMComponentConfig = {
30
+ enableDebugLogging: true,
31
+ phaseTimeoutMs: 15000, // Increased timeout for component loading
32
+ continueOnError: false // Fail fast for debugging
33
+ }
34
+
35
+ constructor(protected eventBus: EventBus, config: LCMComponentConfig = {}) {
36
+ this.config = {
37
+ phaseTimeoutMs: config.phaseTimeoutMs ?? 10000,
38
+ continueOnError: config.continueOnError ?? false,
39
+ validateDependencies: false,
40
+ enableDebugLogging: config.enableDebugLogging ?? false
41
+ };
42
+ this.allComponents = [];
43
+ this.componentsByLevel = [];
44
+ }
45
+
46
+ /**
47
+ * Initialize component tree starting from root component
48
+ *
49
+ * @param rootComponent The root component to start initialization from
50
+ * @param rootName Optional name for the root component (for debugging)
51
+ * @returns Promise that resolves when all components are fully initialized
52
+ */
53
+ public async initializeFromRoot(rootComponent: LCMComponent): Promise<void> {
54
+ // Phase 1 - Performance post-load setup where each parent will have the change
55
+ // to set dependencies on its children (either to its siblings or to itself).
56
+ await this.performLocalInit(rootComponent);
57
+
58
+ // Phase 2: Dependency Injection - All components receive dependencies
59
+ await this.injectDependencies();
60
+
61
+ await this.activate();
62
+ }
63
+
64
+ /**
65
+ * Phase 0: Discover all components via breadth-first traversal
66
+ */
67
+ private async performLocalInit(rootComponent: LCMComponent): Promise<void> {
68
+ const visited = new Set<LCMComponent>();
69
+ let queue = [rootComponent] as LCMComponent[]
70
+ this.allComponents = [rootComponent]
71
+
72
+ if (this.componentsByLevel.length > 0) {
73
+ throw new Error("Already done")
74
+ }
75
+ for (let level = 0; queue.length > 0; level ++) {
76
+ const newqueue = [] as LCMComponent[];
77
+ const promises = new Array<Promise<LCMComponent[]>>()
78
+ this.componentsByLevel.push(queue)
79
+ for (let i = 0;i < queue.length;i++) {
80
+ const component = queue[i]
81
+
82
+ // Enforce tree structure: each component should have exactly one parent
83
+ if (visited.has(component)) {
84
+ throw new Error(
85
+ `Component hierarchy integrity violation: Component '${component.constructor.name}' ` +
86
+ `discovered as child of multiple parents at level ${level}. ` +
87
+ `Component hierarchies must form a tree structure where each component has exactly one parent.`
88
+ );
89
+ }
90
+
91
+ this.emitEvent(LifecycleEventTypes.LOCAL_INIT_STARTED, component, level)
92
+ const result = component.performLocalInit()
93
+ promises.push(Promise.resolve(result).then((children: LCMComponent[]) => {
94
+ for (const ch of children) {
95
+ this.allComponents.push(ch)
96
+ newqueue.push(ch)
97
+ }
98
+ return children
99
+ }))
100
+
101
+ // now wait for all the promises to finish
102
+ await Promise.all(promises)
103
+
104
+ // TODO - Handle errors
105
+ this.emitEvent(LifecycleEventTypes.LOCAL_INIT_FINISHED, component, level)
106
+ }
107
+ queue = newqueue
108
+ }
109
+ }
110
+
111
+ /**
112
+ * Phase 1: Dependency injection.
113
+ * Here all our components are already discovered so we can call directly
114
+ */
115
+ protected injectDependencies(): void {
116
+ // This is guaranteed to be in order of levels
117
+ for (const comp of this.allComponents) {
118
+ comp.setupDependencies()
119
+ }
120
+ }
121
+
122
+ /**
123
+ * Phase 2: Activate all components
124
+ *
125
+ * Here all our components are already discovered and their dependencies setup
126
+ * so we can call directly
127
+ */
128
+ protected async activate(): Promise<void> {
129
+ // This is guaranteed to be in order of levels
130
+ const visited = new Set<LCMComponent>();
131
+ for (let level = 0;level < this.componentsByLevel.length; level++) {
132
+ const promises = new Array<Promise<void>>()
133
+ const levelComps = this.componentsByLevel[level]
134
+ for (const comp of levelComps) {
135
+ if (visited.has(comp)) {
136
+ throw new Error("Comp already visited")
137
+ }
138
+ visited.add(comp)
139
+ this.emitEvent(LifecycleEventTypes.ACTIVATION_STARTED , comp, 0)
140
+ const result = comp.activate()
141
+ promises.push(Promise.resolve(result).then(() => {
142
+ this.emitEvent(LifecycleEventTypes.ACTIVATION_FINISHED, comp, 0)
143
+ }))
144
+ }
145
+ await Promise.all(promises)
146
+ }
147
+ }
148
+
149
+ /**
150
+ * Deactivate all components in reverse order. This is usually called when a page quits or a component is
151
+ * deactivated
152
+ */
153
+ public async deactivateAll(): Promise<void> {
154
+ // destruction should leaf to the root so we are removing things from bottom to top
155
+ for (let level = this.componentsByLevel.length - 1;level >= 0; level--) {
156
+ const promises = new Array<Promise<void>>()
157
+ const levelComps = this.componentsByLevel[level]
158
+ for (const comp of levelComps) {
159
+ this.emitEvent(LifecycleEventTypes.DEACTIVATION_STARTED , comp, level)
160
+ const result = comp.deactivate()
161
+ promises.push(Promise.resolve(result).then(() => {
162
+ this.emitEvent(LifecycleEventTypes.DEACTIVATION_FINISHED, comp, level)
163
+ }))
164
+ }
165
+ await Promise.all(promises)
166
+ }
167
+ }
168
+
169
+ /**
170
+ * Emit lifecycle event to registered callbacks
171
+ */
172
+ private emitEvent(
173
+ event: LifecycleEventType,
174
+ component: LCMComponent,
175
+ level: number,
176
+ error: Error | null = null,
177
+ ): void {
178
+ this.eventBus.emit<ComponentLifecycleEvent>(
179
+ event,
180
+ { error: error, success: error == null},
181
+ this,
182
+ component
183
+ );
184
+ }
185
+
186
+ /**
187
+ * Log message if debug logging is enabled
188
+ */
189
+ private log(message: string, ...args: any[]): void {
190
+ if (this.config.enableDebugLogging) {
191
+ console.log(`[LifecycleController] ${message}`, ...args);
192
+ }
193
+ }
194
+
195
+ /**
196
+ * Log error message
197
+ */
198
+ private logError(message: string, error?: any): void {
199
+ console.error(`[LifecycleController] ${message}`, error);
200
+ }
201
+
202
+ /**
203
+ * Get current status of all components
204
+ */
205
+ /*
206
+ public getComponentStatus(): Array<{ name: string; phase: ComponentPhase }> {
207
+ return Array.from(this.allComponents).map(component => ({
208
+ name: this.getComponentName(component),
209
+ phase: this.componentPhases.get(component) || 'created'
210
+ }));
211
+ }
212
+ */
213
+ }
@@ -0,0 +1,236 @@
1
+ import { BaseComponent } from './Component';
2
+ import { LCMComponent } from './LCMComponent';
3
+ import { EventBus } from './EventBus';
4
+
5
+ /**
6
+ * MobileBottomDrawer - Reusable bottom drawer component for mobile layouts
7
+ *
8
+ * Features:
9
+ * - Slides up from bottom covering 60-70% of viewport
10
+ * - Backdrop overlay that dims the content behind
11
+ * - Auto-closes when backdrop is tapped
12
+ * - Swipe down to close gesture
13
+ * - Smooth slide-up/down animations
14
+ * - Holds any panel content
15
+ */
16
+ export class MobileBottomDrawer extends BaseComponent implements LCMComponent {
17
+ private backdropElement: HTMLElement;
18
+ private drawerElement: HTMLElement;
19
+ private contentElement: HTMLElement;
20
+ private closeButton: HTMLElement | null;
21
+ private isOpen: boolean = false;
22
+ private onCloseCallback?: () => void;
23
+
24
+ // Swipe gesture tracking
25
+ private touchStartY: number = 0;
26
+ private touchCurrentY: number = 0;
27
+ private isDragging: boolean = false;
28
+
29
+ /**
30
+ * Create a MobileBottomDrawer
31
+ * @param rootElement - The root container element for the drawer
32
+ * @param eventBus - Event bus for component communication
33
+ * @param debugMode - Enable debug logging
34
+ */
35
+ constructor(rootElement: HTMLElement, eventBus: EventBus, debugMode: boolean = false) {
36
+ super('mobile-bottom-drawer', rootElement, eventBus, debugMode);
37
+ }
38
+
39
+ /**
40
+ * Phase 1: Initialize DOM and discover child components
41
+ */
42
+ async performLocalInit(): Promise<LCMComponent[]> {
43
+ // Find drawer elements
44
+ this.backdropElement = this.rootElement.querySelector('.drawer-backdrop') as HTMLElement;
45
+ this.drawerElement = this.rootElement.querySelector('.drawer-container') as HTMLElement;
46
+ this.contentElement = this.rootElement.querySelector('.drawer-content') as HTMLElement;
47
+ this.closeButton = this.rootElement.querySelector('.drawer-close-btn') as HTMLElement;
48
+
49
+ if (!this.backdropElement || !this.drawerElement || !this.contentElement) {
50
+ throw new Error('MobileBottomDrawer: Required drawer elements not found');
51
+ }
52
+
53
+ // Bind event listeners
54
+ this.bindEvents();
55
+
56
+ return [];
57
+ }
58
+
59
+ /**
60
+ * Bind event listeners for drawer interactions
61
+ */
62
+ private bindEvents(): void {
63
+ // Close drawer on backdrop click
64
+ this.backdropElement.addEventListener('click', (e) => {
65
+ if (e.target === this.backdropElement) {
66
+ this.close();
67
+ }
68
+ });
69
+
70
+ // Close drawer on close button click
71
+ if (this.closeButton) {
72
+ this.closeButton.addEventListener('click', () => {
73
+ this.close();
74
+ });
75
+ }
76
+
77
+ // Close drawer on Escape key
78
+ document.addEventListener('keydown', (e) => {
79
+ if (e.key === 'Escape' && this.isOpen) {
80
+ this.close();
81
+ }
82
+ });
83
+
84
+ // Swipe-to-close gesture on drawer element
85
+ this.drawerElement.addEventListener('touchstart', (e) => this.handleTouchStart(e), { passive: true });
86
+ this.drawerElement.addEventListener('touchmove', (e) => this.handleTouchMove(e), { passive: false });
87
+ this.drawerElement.addEventListener('touchend', (e) => this.handleTouchEnd(e), { passive: true });
88
+ }
89
+
90
+ /**
91
+ * Handle touch start for swipe gesture
92
+ */
93
+ private handleTouchStart(e: TouchEvent): void {
94
+ // Only track if touching the drawer itself (not scrollable content)
95
+ const contentScrollTop = this.contentElement.scrollTop;
96
+
97
+ // If content is scrolled down, let normal scrolling happen
98
+ if (contentScrollTop > 0) {
99
+ return;
100
+ }
101
+
102
+ this.touchStartY = e.touches[0].clientY;
103
+ this.touchCurrentY = this.touchStartY;
104
+ this.isDragging = true;
105
+ }
106
+
107
+ /**
108
+ * Handle touch move for swipe gesture
109
+ */
110
+ private handleTouchMove(e: TouchEvent): void {
111
+ if (!this.isDragging) return;
112
+
113
+ this.touchCurrentY = e.touches[0].clientY;
114
+ const deltaY = this.touchCurrentY - this.touchStartY;
115
+
116
+ // Only allow downward swipes
117
+ if (deltaY > 0) {
118
+ // Prevent default scrolling when swiping down
119
+ e.preventDefault();
120
+
121
+ // Apply drag transform with resistance
122
+ const dragAmount = Math.min(deltaY, 300); // Cap at 300px
123
+ this.drawerElement.style.transform = `translateY(${dragAmount}px)`;
124
+
125
+ // Reduce backdrop opacity based on drag amount
126
+ const opacity = Math.max(0, 0.5 - (dragAmount / 600));
127
+ this.backdropElement.style.background = `rgba(0, 0, 0, ${opacity})`;
128
+ }
129
+ }
130
+
131
+ /**
132
+ * Handle touch end for swipe gesture
133
+ */
134
+ private handleTouchEnd(e: TouchEvent): void {
135
+ if (!this.isDragging) return;
136
+
137
+ const deltaY = this.touchCurrentY - this.touchStartY;
138
+ const velocity = deltaY / 300; // Rough velocity calculation
139
+
140
+ // Close if swiped down more than 100px or with high velocity
141
+ if (deltaY > 100 || velocity > 0.3) {
142
+ this.close();
143
+ } else {
144
+ // Snap back to open position
145
+ this.drawerElement.style.transform = '';
146
+ this.backdropElement.style.background = '';
147
+ }
148
+
149
+ this.isDragging = false;
150
+ }
151
+
152
+ /**
153
+ * Open the drawer with slide-up animation
154
+ */
155
+ public open(): void {
156
+ if (this.isOpen) return;
157
+
158
+ this.isOpen = true;
159
+
160
+ // Reset any drag transform
161
+ this.drawerElement.style.transform = '';
162
+ this.backdropElement.style.background = '';
163
+
164
+ // Add open class - CSS handles all animations
165
+ this.rootElement.classList.add('open');
166
+
167
+ // Emit event
168
+ this.eventBus.emit('drawer-opened', { drawerId: this.componentId }, null, this);
169
+ }
170
+
171
+ /**
172
+ * Close the drawer with slide-down animation
173
+ */
174
+ public close(): void {
175
+ if (!this.isOpen) return;
176
+
177
+ this.isOpen = false;
178
+
179
+ // Reset any drag transform
180
+ this.drawerElement.style.transform = '';
181
+ this.backdropElement.style.background = '';
182
+
183
+ // Remove open class - CSS handles animation
184
+ this.rootElement.classList.remove('open');
185
+
186
+ // Emit event
187
+ this.eventBus.emit('drawer-closed', { drawerId: this.componentId }, null, this);
188
+
189
+ // Call close callback if provided
190
+ if (this.onCloseCallback) {
191
+ this.onCloseCallback();
192
+ }
193
+ }
194
+
195
+ /**
196
+ * Toggle drawer open/closed
197
+ */
198
+ public toggle(): void {
199
+ if (this.isOpen) {
200
+ this.close();
201
+ } else {
202
+ this.open();
203
+ }
204
+ }
205
+
206
+ /**
207
+ * Check if drawer is currently open
208
+ */
209
+ public getIsOpen(): boolean {
210
+ return this.isOpen;
211
+ }
212
+
213
+ /**
214
+ * Set the content element for the drawer
215
+ * @param element - The element to insert into the drawer content area
216
+ */
217
+ public setContent(element: HTMLElement): void {
218
+ this.contentElement.innerHTML = '';
219
+ this.contentElement.appendChild(element);
220
+ }
221
+
222
+ /**
223
+ * Set a callback to be called when drawer closes
224
+ * @param callback - Function to call on close
225
+ */
226
+ public setOnClose(callback: () => void): void {
227
+ this.onCloseCallback = callback;
228
+ }
229
+
230
+ /**
231
+ * Get the content container element
232
+ */
233
+ public getContentContainer(): HTMLElement {
234
+ return this.contentElement;
235
+ }
236
+ }
package/src/Modal.ts ADDED
@@ -0,0 +1,233 @@
1
+ import { TemplateLoader } from './TemplateLoader';
2
+
3
+ /**
4
+ * Modal manager for the application
5
+ * Handles showing and hiding modals with different content
6
+ */
7
+ export class Modal {
8
+ private static instance: Modal | null = null;
9
+
10
+ // Modal DOM elements
11
+ private modalContainer: HTMLDivElement
12
+ private modalBackdrop: HTMLElement | null;
13
+ private modalPanel: HTMLElement | null;
14
+ private modalContent: HTMLElement | null;
15
+ private closeButton: HTMLElement | null;
16
+
17
+ private templateLoader: TemplateLoader;
18
+
19
+ // Current modal data - including optional callbacks
20
+ private currentTemplateId: string | null = null;
21
+ private currentData: any = null;
22
+ private onSubmitCallback: ((modalData: any) => void) | null = null; // Store the callback
23
+ private onApplyCallback: ((modalData: any) => void) | null = null; // Store the Apply callback
24
+
25
+
26
+ /**
27
+ * Private constructor for singleton pattern
28
+ */
29
+ private constructor() {
30
+ // Get modal elements
31
+ this.modalContainer = document.getElementById('modal-container') as HTMLDivElement;
32
+ this.modalBackdrop = document.getElementById('modal-backdrop');
33
+ this.modalPanel = document.getElementById('modal-panel');
34
+ this.modalContent = document.getElementById('modal-content');
35
+ this.closeButton = document.getElementById('modal-close');
36
+
37
+ this.templateLoader = new TemplateLoader();
38
+
39
+ this.bindEvents();
40
+ }
41
+
42
+ /**
43
+ * Get the Modal instance (singleton)
44
+ */
45
+ public static getInstance(): Modal {
46
+ if (!Modal.instance) {
47
+ Modal.instance = new Modal();
48
+ }
49
+ return Modal.instance;
50
+ }
51
+
52
+ /**
53
+ * Bind event listeners for modal interactions
54
+ */
55
+ private bindEvents(): void {
56
+ // Close button click
57
+ if (this.closeButton) {
58
+ this.closeButton.addEventListener('click', () => this.hide());
59
+ }
60
+
61
+ // Click on backdrop to close
62
+ if (this.modalBackdrop) {
63
+ this.modalBackdrop.addEventListener('click', (e) => {
64
+ // Only close if clicking directly on the backdrop
65
+ if (e.target === this.modalBackdrop) {
66
+ this.hide();
67
+ }
68
+ });
69
+ }
70
+
71
+ // Listen for Escape key
72
+ document.addEventListener('keydown', (e) => {
73
+ if (e.key === 'Escape' && this.isVisible()) {
74
+ this.hide();
75
+ }
76
+ });
77
+
78
+ // --- Event Delegation for Modal Actions ---
79
+ if (this.modalPanel) { // Listen on a persistent parent element
80
+ this.modalPanel.addEventListener('click', (e: MouseEvent) => {
81
+ const target = e.target as HTMLElement;
82
+
83
+ // Handle generic close/cancel buttons
84
+ const closeButton = target.closest('button[id$="-cancel"], button[id$="-close"]');
85
+ if (closeButton) {
86
+ this.hide();
87
+ return; // Stop further processing
88
+ }
89
+
90
+ // Handle specific actions like submit
91
+ const actionButton = target.closest('button[data-modal-action]');
92
+ if (actionButton) {
93
+ const action = actionButton.getAttribute('data-modal-action');
94
+
95
+ if (action === 'submit' && this.onSubmitCallback) {
96
+ this.onSubmitCallback(this.currentData); // Call the stored callback
97
+ } else if (action === 'apply' && this.onApplyCallback) {
98
+ this.onApplyCallback(this.currentData); // Call the Apply callback
99
+ this.hide(); // Typically hide after applying
100
+ }
101
+ // Add more actions (e.g., 'apply', 'revise') later if needed
102
+ }
103
+ });
104
+ }
105
+ }
106
+
107
+ /**
108
+ * Check if the modal is currently visible
109
+ */
110
+ public isVisible(): boolean {
111
+ return this.modalContainer ? !this.modalContainer.classList.contains('hidden') : false;
112
+ }
113
+
114
+ /**
115
+ * Show a modal with content from the specified template ID.
116
+ * Uses TemplateLoader to get the content element.
117
+ * @param templateId ID used in `data-template-id` attribute in TemplateRegistry.html
118
+ * @param data Optional data to pass to the modal. Can include callbacks like `onSubmit`.
119
+ * @returns The root HTMLElement of the loaded content, or null if failed.
120
+ */
121
+ public show(templateId: string, data: any = null): HTMLElement | null {
122
+ if (!this.modalContainer || !this.modalContent) {
123
+ console.error("Modal container or content area not found.");
124
+ return null;
125
+ }
126
+
127
+ // Use TemplateLoader to get the content element
128
+ // Load content directly into the modal content area
129
+ const success = this.templateLoader.loadInto(templateId, this.modalContent);
130
+ if (!success) {
131
+ // Error message is already placed into modalContent by loadInto on failure
132
+ // Ensure modal is still visible
133
+ this.modalContainer.classList.remove('hidden');
134
+ setTimeout(() => this.modalContainer.classList.add('modal-active'), 10);
135
+ return null; // Indicate failure
136
+ }
137
+
138
+ // Store current modal info
139
+ this.currentTemplateId = templateId;
140
+ this.currentData = data || {}; // Ensure data is an object
141
+ this.onSubmitCallback = data?.onSubmit || null; // Store the submit callback
142
+ this.onApplyCallback = data?.onApply || null; // Store the apply callback
143
+
144
+ // Set data attributes for non-function data
145
+ if (data) {
146
+ Object.entries(data).forEach(([key, value]) => {
147
+ if (key !== 'onSubmit' && (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean')) {
148
+ if (this.modalContent) this.modalContent.dataset[key] = String(value);
149
+ }
150
+ });
151
+ }
152
+
153
+ // Show modal container
154
+ this.modalContainer.classList.remove('hidden');
155
+
156
+ // Trigger animations if needed (add active class after a tick)
157
+ setTimeout(() => {
158
+ this.modalContainer.classList.add('modal-active');
159
+ }, 10); // Small delay ensures transition applies correctly
160
+
161
+ const firstElement = this.modalContent//?.firstElementChild as HTMLElement | null;
162
+ return firstElement;
163
+ }
164
+
165
+ /**
166
+ * Hide the modal
167
+ */
168
+ public hide(): Promise<void> {
169
+ return new Promise((resolve) => {
170
+ if (!this.modalContainer) return;
171
+
172
+ // Remove active class first (for animations)
173
+ this.modalContainer.classList.remove('modal-active');
174
+
175
+ // Hide after a short delay
176
+ setTimeout(() => {
177
+ this.modalContainer.classList.add('hidden');
178
+
179
+ // Clear current modal info
180
+ this.currentTemplateId = null;
181
+ this.currentData = null;
182
+ this.onSubmitCallback = null; // Clear callback
183
+ this.onApplyCallback = null; // Clear apply callback
184
+ if(this.modalContent) this.modalContent.innerHTML = ''; // Clear content
185
+ resolve();
186
+ }, 200); // Match typical transition duration
187
+ });
188
+ }
189
+
190
+ /**
191
+ * Get the current modal content element
192
+ */
193
+ public getContentElement(): HTMLElement | null {
194
+ return this.modalContent;
195
+ }
196
+
197
+ /**
198
+ * Get the current template ID
199
+ */
200
+ public getCurrentTemplate(): string | null {
201
+ return this.currentTemplateId;
202
+ }
203
+
204
+ /**
205
+ * Get the current modal data
206
+ */
207
+ public getCurrentData(): any {
208
+ return this.currentData;
209
+ }
210
+
211
+ /**
212
+ * Update modal data (excluding callbacks for now)
213
+ */
214
+ public updateData(newData: any): void {
215
+ this.currentData = { ...this.currentData, ...newData };
216
+
217
+ // Update data attributes for non-function data
218
+ if (this.modalContent && newData) {
219
+ Object.entries(newData).forEach(([key, value]) => { // Exclude callbacks when setting data attributes
220
+ if (key !== 'onSubmit' && (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean')) {
221
+ if (this.modalContent) this.modalContent.dataset[key] = String(value);
222
+ }
223
+ });
224
+ }
225
+ }
226
+
227
+ /**
228
+ * Initialize the modal component
229
+ */
230
+ public static init(): Modal {
231
+ return Modal.getInstance();
232
+ }
233
+ }