@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,11 @@
|
|
|
1
|
+
import { Logger } from "../utils/logger";
|
|
2
|
+
declare const logger: Logger;
|
|
3
|
+
/**
|
|
4
|
+
* Configure the internal package logger
|
|
5
|
+
* This allows consuming projects to adjust logging levels without creating their own logger
|
|
6
|
+
* @param debugMode Enable debug mode (overrides minLogLevel)
|
|
7
|
+
* @param minLogLevel Minimum log level: 'none' | 'error' | 'warn' | 'trace'
|
|
8
|
+
*/
|
|
9
|
+
export declare function configureLogger(debugMode?: boolean, minLogLevel?: string): void;
|
|
10
|
+
export { logger };
|
|
11
|
+
//# sourceMappingURL=logger.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../../src/logger.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;AAIzC,QAAA,MAAM,MAAM,QAA6B,CAAC;AAE1C;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,SAAS,GAAE,OAAe,EAAE,WAAW,GAAE,MAAgB,GAAG,IAAI,CAG/F;AAED,OAAO,EAAE,MAAM,EAAE,CAAC"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Logger } from "../utils/logger";
|
|
2
|
+
// Centralized logger instance for internal package use
|
|
3
|
+
// Default configuration: error-only logging for production builds
|
|
4
|
+
const logger = new Logger(false, "error");
|
|
5
|
+
/**
|
|
6
|
+
* Configure the internal package logger
|
|
7
|
+
* This allows consuming projects to adjust logging levels without creating their own logger
|
|
8
|
+
* @param debugMode Enable debug mode (overrides minLogLevel)
|
|
9
|
+
* @param minLogLevel Minimum log level: 'none' | 'error' | 'warn' | 'trace'
|
|
10
|
+
*/
|
|
11
|
+
export function configureLogger(debugMode = false, minLogLevel = "error") {
|
|
12
|
+
logger.debugMode = debugMode;
|
|
13
|
+
logger.minLoglevel = minLogLevel;
|
|
14
|
+
}
|
|
15
|
+
export { logger };
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { EventOrchestrator } from "./eventBus";
|
|
2
|
+
import { Page, View } from "./appBuilder";
|
|
3
|
+
import { type StateSubscription } from "./stateManager";
|
|
4
|
+
export interface TransitionConfig {
|
|
5
|
+
type: "slide" | "fade" | "scale" | "flip" | "snap" | "custom";
|
|
6
|
+
direction?: "left" | "right" | "up" | "down";
|
|
7
|
+
duration?: number;
|
|
8
|
+
easing?: string;
|
|
9
|
+
customCSS?: string;
|
|
10
|
+
}
|
|
11
|
+
export interface NavigationState {
|
|
12
|
+
currentPageId: string | null;
|
|
13
|
+
currentViewId: string | null;
|
|
14
|
+
isTransitioning: boolean;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* NavigationManager - Singleton Service for Application Navigation
|
|
18
|
+
*
|
|
19
|
+
* **IMPORTANT: Only ONE instance of NavigationManager should exist per application.**
|
|
20
|
+
*
|
|
21
|
+
* This class manages page and view navigation using shared global state. Multiple instances
|
|
22
|
+
* would cause critical issues:
|
|
23
|
+
* - Conflicting global state (navigation.currentPageId, navigation.currentViewId, navigation.isTransitioning)
|
|
24
|
+
* - Event bus namespace collisions (all instances register as "navigation-manager")
|
|
25
|
+
* - Ambiguous page/view registration and navigation routing
|
|
26
|
+
*
|
|
27
|
+
* The NavigationManager is automatically instantiated by AppBuilder and should not be
|
|
28
|
+
* manually created elsewhere in the application.
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* // ✅ Correct: Created once by AppBuilder
|
|
32
|
+
* class AppBuilder extends RenderableComponent {
|
|
33
|
+
* private navigationManager: NavigationManager;
|
|
34
|
+
* constructor(orchestrator: EventOrchestrator) {
|
|
35
|
+
* this.navigationManager = new NavigationManager(orchestrator);
|
|
36
|
+
* }
|
|
37
|
+
* }
|
|
38
|
+
*
|
|
39
|
+
* // ❌ Wrong: Creating additional instances
|
|
40
|
+
* const nav1 = new NavigationManager(orchestrator); // First instance - OK
|
|
41
|
+
* const nav2 = new NavigationManager(orchestrator); // ERROR: Will throw!
|
|
42
|
+
*/
|
|
43
|
+
export declare class NavigationManager {
|
|
44
|
+
private static instance;
|
|
45
|
+
private static isDestroyed;
|
|
46
|
+
private eventBus;
|
|
47
|
+
private orchestrator;
|
|
48
|
+
private pages;
|
|
49
|
+
private views;
|
|
50
|
+
private stateSubscriptions;
|
|
51
|
+
private activeTransitionCleanups;
|
|
52
|
+
/**
|
|
53
|
+
* Get the singleton instance of NavigationManager
|
|
54
|
+
* Returns null if no instance has been created yet
|
|
55
|
+
*/
|
|
56
|
+
static getInstance(): NavigationManager | null;
|
|
57
|
+
constructor(orchestrator: EventOrchestrator);
|
|
58
|
+
private initializeGlobalState;
|
|
59
|
+
private setupEventListeners;
|
|
60
|
+
registerPage(page: Page): void;
|
|
61
|
+
registerView(view: View): void;
|
|
62
|
+
private setupPageContainer;
|
|
63
|
+
private setupViewContainer;
|
|
64
|
+
navigateToPage(pageId: string, config?: TransitionConfig, priority?: string): Promise<void>;
|
|
65
|
+
navigateToView(viewId: string, config?: TransitionConfig, priority?: string): Promise<void>;
|
|
66
|
+
private performPageNavigationInternal;
|
|
67
|
+
private performViewNavigationInternal;
|
|
68
|
+
private performPageTransition;
|
|
69
|
+
private performViewTransition;
|
|
70
|
+
private animateOut;
|
|
71
|
+
private animateIn;
|
|
72
|
+
private normalizeTransitionConfig;
|
|
73
|
+
private clearAnimationClasses;
|
|
74
|
+
private showPage;
|
|
75
|
+
private hidePage;
|
|
76
|
+
private showView;
|
|
77
|
+
private hideView;
|
|
78
|
+
private hideAllViews;
|
|
79
|
+
getCurrentPageId(): string | null;
|
|
80
|
+
getCurrentViewId(): string | null;
|
|
81
|
+
isTransitioning(): boolean;
|
|
82
|
+
getRegisteredPages(): string[];
|
|
83
|
+
getRegisteredViews(): string[];
|
|
84
|
+
subscribeToCurrentPage(callback: (pageId: string | null) => void): StateSubscription;
|
|
85
|
+
subscribeToCurrentView(callback: (viewId: string | null) => void): StateSubscription;
|
|
86
|
+
subscribeToTransitionState(callback: (isTransitioning: boolean) => void): StateSubscription;
|
|
87
|
+
/**
|
|
88
|
+
* Clean up any orphaned transition listeners
|
|
89
|
+
* This is a safety net for transitions that didn't complete properly
|
|
90
|
+
*/
|
|
91
|
+
cleanupOrphanedTransitions(): void;
|
|
92
|
+
/**
|
|
93
|
+
* Get the count of active transitions
|
|
94
|
+
*/
|
|
95
|
+
getActiveTransitionCount(): number;
|
|
96
|
+
/**
|
|
97
|
+
* Cleanup method for proper resource management
|
|
98
|
+
*
|
|
99
|
+
* Destroys the NavigationManager instance and clears the singleton reference.
|
|
100
|
+
* After calling destroy(), a new NavigationManager instance can be created if needed
|
|
101
|
+
* (though this is typically only necessary during application teardown/restart).
|
|
102
|
+
*/
|
|
103
|
+
destroy(): void;
|
|
104
|
+
}
|
|
105
|
+
//# sourceMappingURL=navigationManager.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"navigationManager.d.ts","sourceRoot":"","sources":["../../src/navigationManager.ts"],"names":[],"mappings":"AAAA,OAAO,EAAY,iBAAiB,EAAE,MAAM,YAAY,CAAC;AACzD,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,EAAgB,KAAK,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AAKtE,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,OAAO,GAAG,MAAM,GAAG,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,QAAQ,CAAC;IAC9D,SAAS,CAAC,EAAE,MAAM,GAAG,OAAO,GAAG,IAAI,GAAG,MAAM,CAAC;IAC7C,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,eAAe;IAC9B,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,eAAe,EAAE,OAAO,CAAC;CAC1B;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,qBAAa,iBAAiB;IAC5B,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAkC;IACzD,OAAO,CAAC,MAAM,CAAC,WAAW,CAAkB;IAE5C,OAAO,CAAC,QAAQ,CAAW;IAC3B,OAAO,CAAC,YAAY,CAAoB;IACxC,OAAO,CAAC,KAAK,CAAgC;IAC7C,OAAO,CAAC,KAAK,CAAgC;IAC7C,OAAO,CAAC,kBAAkB,CAA2B;IACrD,OAAO,CAAC,wBAAwB,CAA2C;IAE3E;;;OAGG;WACW,WAAW,IAAI,iBAAiB,GAAG,IAAI;gBAIzC,YAAY,EAAE,iBAAiB;IA4B3C,OAAO,CAAC,qBAAqB;IAa7B,OAAO,CAAC,mBAAmB;IAsBpB,YAAY,CAAC,IAAI,EAAE,IAAI,GAAG,IAAI;IAa9B,YAAY,CAAC,IAAI,EAAE,IAAI,GAAG,IAAI;IAQrC,OAAO,CAAC,kBAAkB;IAM1B,OAAO,CAAC,kBAAkB;IAMb,cAAc,CACzB,MAAM,EAAE,MAAM,EACd,MAAM,GAAE,gBAAmC,EAC3C,QAAQ,GAAE,MAAoB,GAC7B,OAAO,CAAC,IAAI,CAAC;IAYH,cAAc,CACzB,MAAM,EAAE,MAAM,EACd,MAAM,GAAE,gBAAmC,EAC3C,QAAQ,GAAE,MAAoB,GAC7B,OAAO,CAAC,IAAI,CAAC;YAYF,6BAA6B;YA6B7B,6BAA6B;YAgC7B,qBAAqB;YAmBrB,qBAAqB;YAqBrB,UAAU;YAuEV,SAAS;IA2FvB,OAAO,CAAC,yBAAyB;IAkBjC,OAAO,CAAC,qBAAqB;IAwB7B,OAAO,CAAC,QAAQ;IAUhB,OAAO,CAAC,QAAQ;IAchB,OAAO,CAAC,QAAQ;IAgBhB,OAAO,CAAC,QAAQ;IAUhB,OAAO,CAAC,YAAY;IAQb,gBAAgB,IAAI,MAAM,GAAG,IAAI;IAIjC,gBAAgB,IAAI,MAAM,GAAG,IAAI;IAIjC,eAAe,IAAI,OAAO;IAI1B,kBAAkB,IAAI,MAAM,EAAE;IAI9B,kBAAkB,IAAI,MAAM,EAAE;IAK9B,sBAAsB,CAAC,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,GAAG,iBAAiB;IAIpF,sBAAsB,CAAC,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,GAAG,iBAAiB;IAIpF,0BAA0B,CAAC,QAAQ,EAAE,CAAC,eAAe,EAAE,OAAO,KAAK,IAAI,GAAG,iBAAiB;IAIlG;;;OAGG;IACI,0BAA0B,IAAI,IAAI;IAezC;;OAEG;IACI,wBAAwB,IAAI,MAAM;IAIzC;;;;;;OAMG;IACI,OAAO,IAAI,IAAI;CAoBvB"}
|
|
@@ -0,0 +1,533 @@
|
|
|
1
|
+
import { stateManager } from "./stateManager";
|
|
2
|
+
import { logger } from "./logger";
|
|
3
|
+
const log = logger;
|
|
4
|
+
/**
|
|
5
|
+
* NavigationManager - Singleton Service for Application Navigation
|
|
6
|
+
*
|
|
7
|
+
* **IMPORTANT: Only ONE instance of NavigationManager should exist per application.**
|
|
8
|
+
*
|
|
9
|
+
* This class manages page and view navigation using shared global state. Multiple instances
|
|
10
|
+
* would cause critical issues:
|
|
11
|
+
* - Conflicting global state (navigation.currentPageId, navigation.currentViewId, navigation.isTransitioning)
|
|
12
|
+
* - Event bus namespace collisions (all instances register as "navigation-manager")
|
|
13
|
+
* - Ambiguous page/view registration and navigation routing
|
|
14
|
+
*
|
|
15
|
+
* The NavigationManager is automatically instantiated by AppBuilder and should not be
|
|
16
|
+
* manually created elsewhere in the application.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* // ✅ Correct: Created once by AppBuilder
|
|
20
|
+
* class AppBuilder extends RenderableComponent {
|
|
21
|
+
* private navigationManager: NavigationManager;
|
|
22
|
+
* constructor(orchestrator: EventOrchestrator) {
|
|
23
|
+
* this.navigationManager = new NavigationManager(orchestrator);
|
|
24
|
+
* }
|
|
25
|
+
* }
|
|
26
|
+
*
|
|
27
|
+
* // ❌ Wrong: Creating additional instances
|
|
28
|
+
* const nav1 = new NavigationManager(orchestrator); // First instance - OK
|
|
29
|
+
* const nav2 = new NavigationManager(orchestrator); // ERROR: Will throw!
|
|
30
|
+
*/
|
|
31
|
+
export class NavigationManager {
|
|
32
|
+
static instance = null;
|
|
33
|
+
static isDestroyed = false;
|
|
34
|
+
eventBus;
|
|
35
|
+
orchestrator;
|
|
36
|
+
pages = new Map();
|
|
37
|
+
views = new Map();
|
|
38
|
+
stateSubscriptions = [];
|
|
39
|
+
activeTransitionCleanups = new Map();
|
|
40
|
+
/**
|
|
41
|
+
* Get the singleton instance of NavigationManager
|
|
42
|
+
* Returns null if no instance has been created yet
|
|
43
|
+
*/
|
|
44
|
+
static getInstance() {
|
|
45
|
+
return NavigationManager.instance;
|
|
46
|
+
}
|
|
47
|
+
constructor(orchestrator) {
|
|
48
|
+
// Enforce singleton pattern - only one instance allowed
|
|
49
|
+
if (NavigationManager.instance !== null) {
|
|
50
|
+
const errorMsg = "NavigationManager instance already exists! Only one NavigationManager should be created per application. " +
|
|
51
|
+
"The NavigationManager is automatically created by AppBuilder and should not be instantiated manually.";
|
|
52
|
+
const error = new Error(errorMsg);
|
|
53
|
+
log.error(errorMsg, error);
|
|
54
|
+
throw error;
|
|
55
|
+
}
|
|
56
|
+
if (NavigationManager.isDestroyed) {
|
|
57
|
+
log.warn("Creating new NavigationManager instance after previous instance was destroyed.");
|
|
58
|
+
NavigationManager.isDestroyed = false;
|
|
59
|
+
}
|
|
60
|
+
// Register this as the singleton instance
|
|
61
|
+
NavigationManager.instance = this;
|
|
62
|
+
log.trace("NavigationManager singleton instance created");
|
|
63
|
+
this.orchestrator = orchestrator;
|
|
64
|
+
this.eventBus = orchestrator.registerEventBus("navigation-manager");
|
|
65
|
+
// Initialize global navigation state
|
|
66
|
+
this.initializeGlobalState();
|
|
67
|
+
this.setupEventListeners();
|
|
68
|
+
}
|
|
69
|
+
initializeGlobalState() {
|
|
70
|
+
// Initialize navigation state in global store if not already present
|
|
71
|
+
if (!stateManager.has("navigation.currentPageId")) {
|
|
72
|
+
stateManager.set("navigation.currentPageId", null);
|
|
73
|
+
}
|
|
74
|
+
if (!stateManager.has("navigation.currentViewId")) {
|
|
75
|
+
stateManager.set("navigation.currentViewId", null);
|
|
76
|
+
}
|
|
77
|
+
if (!stateManager.has("navigation.isTransitioning")) {
|
|
78
|
+
stateManager.set("navigation.isTransitioning", false);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
setupEventListeners() {
|
|
82
|
+
this.eventBus.on("navigate-to-page", (e) => {
|
|
83
|
+
const { pageId, config } = e.detail;
|
|
84
|
+
this.performPageNavigationInternal(pageId, config);
|
|
85
|
+
});
|
|
86
|
+
this.eventBus.on("navigate-to-view", (e) => {
|
|
87
|
+
const { viewId, config } = e.detail;
|
|
88
|
+
this.performViewNavigationInternal(viewId, config);
|
|
89
|
+
});
|
|
90
|
+
this.eventBus.on("register-page", (e) => {
|
|
91
|
+
const { page } = e.detail;
|
|
92
|
+
this.registerPage(page);
|
|
93
|
+
});
|
|
94
|
+
this.eventBus.on("register-view", (e) => {
|
|
95
|
+
const { view } = e.detail;
|
|
96
|
+
this.registerView(view);
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
registerPage(page) {
|
|
100
|
+
this.pages.set(page.componentId, page);
|
|
101
|
+
this.setupPageContainer(page);
|
|
102
|
+
// If this is the first page, make it active
|
|
103
|
+
if (!stateManager.get("navigation.currentPageId")) {
|
|
104
|
+
stateManager.set("navigation.currentPageId", page.componentId);
|
|
105
|
+
this.showPage(page.componentId);
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
this.hidePage(page.componentId);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
registerView(view) {
|
|
112
|
+
this.views.set(view.componentId, view);
|
|
113
|
+
this.setupViewContainer(view);
|
|
114
|
+
// Hide all views by default - they will be shown when navigated to
|
|
115
|
+
this.hideView(view.componentId);
|
|
116
|
+
}
|
|
117
|
+
setupPageContainer(page) {
|
|
118
|
+
const hostElement = page.getHostElement();
|
|
119
|
+
hostElement.classList.add("nav-item", "nav-page");
|
|
120
|
+
hostElement.setAttribute("data-page-id", page.componentId);
|
|
121
|
+
}
|
|
122
|
+
setupViewContainer(view) {
|
|
123
|
+
const hostElement = view.getHostElement();
|
|
124
|
+
hostElement.classList.add("nav-item", "nav-view");
|
|
125
|
+
hostElement.setAttribute("data-view-id", view.componentId);
|
|
126
|
+
}
|
|
127
|
+
async navigateToPage(pageId, config = { type: "snap" }, priority = "immediate") {
|
|
128
|
+
// Use EventOrchestrator queue for navigation requests
|
|
129
|
+
if (stateManager.get("navigation.isTransitioning")) {
|
|
130
|
+
// Queue the navigation through the orchestrator
|
|
131
|
+
this.orchestrator.enqueue("navigate-to-page", "navigation-manager", priority, { pageId, config });
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
// Direct navigation if not transitioning
|
|
135
|
+
this.orchestrator.enqueue("navigate-to-page", "navigation-manager", priority, { pageId, config });
|
|
136
|
+
}
|
|
137
|
+
async navigateToView(viewId, config = { type: "snap" }, priority = "immediate") {
|
|
138
|
+
// Use EventOrchestrator queue for navigation requests
|
|
139
|
+
if (stateManager.get("navigation.isTransitioning")) {
|
|
140
|
+
// Queue the navigation through the orchestrator
|
|
141
|
+
this.orchestrator.enqueue("navigate-to-view", "navigation-manager", priority, { viewId, config });
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
// Direct navigation if not transitioning
|
|
145
|
+
this.orchestrator.enqueue("navigate-to-view", "navigation-manager", priority, { viewId, config });
|
|
146
|
+
}
|
|
147
|
+
async performPageNavigationInternal(pageId, config) {
|
|
148
|
+
if (!this.pages.has(pageId)) {
|
|
149
|
+
throw new Error(`Page with id "${pageId}" not found`);
|
|
150
|
+
}
|
|
151
|
+
if (stateManager.get("navigation.currentPageId") === pageId) {
|
|
152
|
+
return; // Already on this page
|
|
153
|
+
}
|
|
154
|
+
const previousPageId = stateManager.get("navigation.currentPageId");
|
|
155
|
+
stateManager.set("navigation.isTransitioning", true);
|
|
156
|
+
try {
|
|
157
|
+
await this.performPageTransition(pageId, config);
|
|
158
|
+
stateManager.set("navigation.currentPageId", pageId);
|
|
159
|
+
stateManager.set("navigation.currentViewId", null); // Reset view when changing pages
|
|
160
|
+
// Hide all views when navigating to a page to ensure clean state
|
|
161
|
+
this.hideAllViews();
|
|
162
|
+
this.eventBus.emit("page-changed", {
|
|
163
|
+
newPageId: pageId,
|
|
164
|
+
previousPageId,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
finally {
|
|
168
|
+
stateManager.set("navigation.isTransitioning", false);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
async performViewNavigationInternal(viewId, config) {
|
|
172
|
+
if (!this.views.has(viewId)) {
|
|
173
|
+
throw new Error(`View with id "${viewId}" not found`);
|
|
174
|
+
}
|
|
175
|
+
if (stateManager.get("navigation.currentViewId") === viewId) {
|
|
176
|
+
log.trace(`Already on view ${viewId}, emitting re-entry event`);
|
|
177
|
+
//this.eventBus.emit("view-re-entered", { viewId });
|
|
178
|
+
return; // Already on this view
|
|
179
|
+
}
|
|
180
|
+
const previousViewId = stateManager.get("navigation.currentViewId");
|
|
181
|
+
log.trace(`Starting navigation from ${previousViewId} to ${viewId}`);
|
|
182
|
+
stateManager.set("navigation.isTransitioning", true);
|
|
183
|
+
try {
|
|
184
|
+
await this.performViewTransition(viewId, config);
|
|
185
|
+
stateManager.set("navigation.currentViewId", viewId);
|
|
186
|
+
this.eventBus.emit("view-changed", {
|
|
187
|
+
newViewId: viewId,
|
|
188
|
+
previousViewId,
|
|
189
|
+
});
|
|
190
|
+
log.trace(`Navigation to ${viewId} completed successfully`);
|
|
191
|
+
}
|
|
192
|
+
catch (error) {
|
|
193
|
+
log.error(`Navigation to ${viewId} failed:`, error);
|
|
194
|
+
throw error;
|
|
195
|
+
}
|
|
196
|
+
finally {
|
|
197
|
+
stateManager.set("navigation.isTransitioning", false);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
async performPageTransition(targetPageId, config) {
|
|
201
|
+
const currentPageId = stateManager.get("navigation.currentPageId");
|
|
202
|
+
const currentPage = currentPageId ? this.pages.get(currentPageId) : null;
|
|
203
|
+
const targetPage = this.pages.get(targetPageId);
|
|
204
|
+
// Validate and normalize config
|
|
205
|
+
const normalizedConfig = this.normalizeTransitionConfig(config);
|
|
206
|
+
// Show target page first (but invisible)
|
|
207
|
+
this.showPage(targetPageId);
|
|
208
|
+
if (currentPage) {
|
|
209
|
+
await this.animateOut(currentPage.getHostElement(), normalizedConfig);
|
|
210
|
+
this.hidePage(currentPage.componentId);
|
|
211
|
+
}
|
|
212
|
+
await this.animateIn(targetPage.getHostElement(), normalizedConfig);
|
|
213
|
+
}
|
|
214
|
+
async performViewTransition(targetViewId, config) {
|
|
215
|
+
const currentViewId = stateManager.get("navigation.currentViewId");
|
|
216
|
+
const currentView = currentViewId ? this.views.get(currentViewId) : null;
|
|
217
|
+
const targetView = this.views.get(targetViewId);
|
|
218
|
+
// Validate and normalize config
|
|
219
|
+
const normalizedConfig = this.normalizeTransitionConfig(config);
|
|
220
|
+
// Show target view first (before animation)
|
|
221
|
+
this.showView(targetViewId, normalizedConfig.type !== "snap");
|
|
222
|
+
// Animate if needed
|
|
223
|
+
if (currentView) {
|
|
224
|
+
await this.animateOut(currentView.getHostElement(), normalizedConfig);
|
|
225
|
+
// Hide current view after animation completes
|
|
226
|
+
this.hideView(currentViewId);
|
|
227
|
+
}
|
|
228
|
+
await this.animateIn(targetView.getHostElement(), normalizedConfig);
|
|
229
|
+
}
|
|
230
|
+
async animateOut(element, config) {
|
|
231
|
+
// Handle snap navigation - immediate hide with no animation
|
|
232
|
+
if (config.type === "snap") {
|
|
233
|
+
element.style.transition = "none";
|
|
234
|
+
element.style.opacity = "0";
|
|
235
|
+
element.style.visibility = "hidden";
|
|
236
|
+
return Promise.resolve();
|
|
237
|
+
}
|
|
238
|
+
return new Promise((resolve) => {
|
|
239
|
+
const duration = config.duration || 300;
|
|
240
|
+
element.style.transition = `all ${duration}ms ${config.easing || "ease-in-out"}`;
|
|
241
|
+
let transitionendFired = false;
|
|
242
|
+
let fallbackTimerId = null;
|
|
243
|
+
const cleanup = () => {
|
|
244
|
+
if (transitionendFired)
|
|
245
|
+
return; // Prevent double execution
|
|
246
|
+
transitionendFired = true;
|
|
247
|
+
// Clean up both the event listener and timeout
|
|
248
|
+
element.removeEventListener("transitionend", transitionendHandler);
|
|
249
|
+
if (fallbackTimerId !== null) {
|
|
250
|
+
clearTimeout(fallbackTimerId);
|
|
251
|
+
fallbackTimerId = null;
|
|
252
|
+
}
|
|
253
|
+
// Remove from active transitions map
|
|
254
|
+
this.activeTransitionCleanups.delete(element);
|
|
255
|
+
resolve();
|
|
256
|
+
};
|
|
257
|
+
const transitionendHandler = () => {
|
|
258
|
+
cleanup();
|
|
259
|
+
};
|
|
260
|
+
element.addEventListener("transitionend", transitionendHandler, { once: true });
|
|
261
|
+
// Store cleanup function for this element
|
|
262
|
+
this.activeTransitionCleanups.set(element, cleanup);
|
|
263
|
+
// Apply exit animation
|
|
264
|
+
switch (config.type) {
|
|
265
|
+
case "slide":
|
|
266
|
+
element.classList.add(`slide-out-${config.direction || "left"}`);
|
|
267
|
+
break;
|
|
268
|
+
case "fade":
|
|
269
|
+
element.classList.add("fade-out");
|
|
270
|
+
break;
|
|
271
|
+
case "scale":
|
|
272
|
+
element.classList.add("scale-out");
|
|
273
|
+
break;
|
|
274
|
+
case "flip":
|
|
275
|
+
element.classList.add("flip-out");
|
|
276
|
+
break;
|
|
277
|
+
case "custom":
|
|
278
|
+
if (config.customCSS) {
|
|
279
|
+
element.style.cssText += config.customCSS;
|
|
280
|
+
}
|
|
281
|
+
break;
|
|
282
|
+
}
|
|
283
|
+
// Fallback timeout
|
|
284
|
+
fallbackTimerId = window.setTimeout(() => {
|
|
285
|
+
log.trace("Transition fallback timeout triggered for animateOut");
|
|
286
|
+
cleanup();
|
|
287
|
+
}, duration + 50);
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
async animateIn(element, config) {
|
|
291
|
+
// Handle snap navigation - immediate show with no animation
|
|
292
|
+
if (config.type === "snap") {
|
|
293
|
+
element.style.transition = "none";
|
|
294
|
+
element.style.opacity = "1";
|
|
295
|
+
element.style.visibility = "visible";
|
|
296
|
+
element.style.transform = "none";
|
|
297
|
+
this.clearAnimationClasses(element);
|
|
298
|
+
return Promise.resolve();
|
|
299
|
+
}
|
|
300
|
+
return new Promise((resolve) => {
|
|
301
|
+
const duration = config.duration || 300;
|
|
302
|
+
element.style.transition = `all ${duration}ms ${config.easing || "ease-in-out"}`;
|
|
303
|
+
let transitionendFired = false;
|
|
304
|
+
let fallbackTimerId = null;
|
|
305
|
+
const cleanup = () => {
|
|
306
|
+
if (transitionendFired)
|
|
307
|
+
return; // Prevent double execution
|
|
308
|
+
transitionendFired = true;
|
|
309
|
+
// Clean up both the event listener and timeout
|
|
310
|
+
element.removeEventListener("transitionend", transitionendHandler);
|
|
311
|
+
if (fallbackTimerId !== null) {
|
|
312
|
+
clearTimeout(fallbackTimerId);
|
|
313
|
+
fallbackTimerId = null;
|
|
314
|
+
}
|
|
315
|
+
// Remove from active transitions map
|
|
316
|
+
this.activeTransitionCleanups.delete(element);
|
|
317
|
+
this.clearAnimationClasses(element);
|
|
318
|
+
resolve();
|
|
319
|
+
};
|
|
320
|
+
const transitionendHandler = () => {
|
|
321
|
+
cleanup();
|
|
322
|
+
};
|
|
323
|
+
element.addEventListener("transitionend", transitionendHandler, { once: true });
|
|
324
|
+
// Store cleanup function for this element
|
|
325
|
+
this.activeTransitionCleanups.set(element, cleanup);
|
|
326
|
+
// Set initial state
|
|
327
|
+
switch (config.type) {
|
|
328
|
+
case "slide":
|
|
329
|
+
element.classList.add(`slide-in-${config.direction || "right"}`);
|
|
330
|
+
break;
|
|
331
|
+
case "fade":
|
|
332
|
+
element.classList.add("fade-in");
|
|
333
|
+
break;
|
|
334
|
+
case "scale":
|
|
335
|
+
element.classList.add("scale-in");
|
|
336
|
+
break;
|
|
337
|
+
case "flip":
|
|
338
|
+
element.classList.add("flip-in");
|
|
339
|
+
break;
|
|
340
|
+
}
|
|
341
|
+
// Animate to active state
|
|
342
|
+
requestAnimationFrame(() => {
|
|
343
|
+
switch (config.type) {
|
|
344
|
+
case "slide":
|
|
345
|
+
element.classList.remove(`slide-in-${config.direction || "right"}`);
|
|
346
|
+
element.classList.add("slide-active");
|
|
347
|
+
break;
|
|
348
|
+
case "fade":
|
|
349
|
+
element.classList.remove("fade-in");
|
|
350
|
+
element.classList.add("fade-active");
|
|
351
|
+
break;
|
|
352
|
+
case "scale":
|
|
353
|
+
element.classList.remove("scale-in");
|
|
354
|
+
element.classList.add("scale-active");
|
|
355
|
+
break;
|
|
356
|
+
case "flip":
|
|
357
|
+
element.classList.remove("flip-in");
|
|
358
|
+
element.classList.add("flip-active");
|
|
359
|
+
break;
|
|
360
|
+
}
|
|
361
|
+
});
|
|
362
|
+
// Fallback timeout
|
|
363
|
+
fallbackTimerId = window.setTimeout(() => {
|
|
364
|
+
log.trace("Transition fallback timeout triggered for animateIn");
|
|
365
|
+
cleanup();
|
|
366
|
+
}, duration + 50);
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
normalizeTransitionConfig(config) {
|
|
370
|
+
// If config is missing or invalid, fallback to snap
|
|
371
|
+
if (!config || !config.type) {
|
|
372
|
+
log.warn("Invalid transition config, falling back to snap navigation");
|
|
373
|
+
return { type: "snap" };
|
|
374
|
+
}
|
|
375
|
+
// For animated transitions, ensure duration is set
|
|
376
|
+
if (config.type !== "snap" && config.type !== "custom") {
|
|
377
|
+
if (!config.duration || config.duration <= 0) {
|
|
378
|
+
log.warn(`Invalid duration for ${config.type} transition, falling back to snap navigation`);
|
|
379
|
+
return { type: "snap" };
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
return config;
|
|
383
|
+
}
|
|
384
|
+
clearAnimationClasses(element) {
|
|
385
|
+
const animationClasses = [
|
|
386
|
+
"slide-out-left",
|
|
387
|
+
"slide-out-right",
|
|
388
|
+
"slide-out-up",
|
|
389
|
+
"slide-out-down",
|
|
390
|
+
"slide-in-left",
|
|
391
|
+
"slide-in-right",
|
|
392
|
+
"slide-in-up",
|
|
393
|
+
"slide-in-down",
|
|
394
|
+
"slide-active",
|
|
395
|
+
"fade-out",
|
|
396
|
+
"fade-in",
|
|
397
|
+
"fade-active",
|
|
398
|
+
"scale-out",
|
|
399
|
+
"scale-in",
|
|
400
|
+
"scale-active",
|
|
401
|
+
"flip-out",
|
|
402
|
+
"flip-in",
|
|
403
|
+
"flip-active",
|
|
404
|
+
];
|
|
405
|
+
element.classList.remove(...animationClasses);
|
|
406
|
+
}
|
|
407
|
+
showPage(pageId) {
|
|
408
|
+
const page = this.pages.get(pageId);
|
|
409
|
+
if (page) {
|
|
410
|
+
const element = page.getHostElement();
|
|
411
|
+
element.style.display = ""; // Clear the inline display:none
|
|
412
|
+
element.classList.remove("nav-hidden", "out");
|
|
413
|
+
element.classList.add("in");
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
hidePage(pageId) {
|
|
417
|
+
const page = this.pages.get(pageId);
|
|
418
|
+
if (page) {
|
|
419
|
+
const element = page.getHostElement();
|
|
420
|
+
element.classList.add("nav-hidden");
|
|
421
|
+
// Set display none after a short delay to allow animation to complete
|
|
422
|
+
setTimeout(() => {
|
|
423
|
+
if (element.classList.contains("nav-hidden")) {
|
|
424
|
+
element.style.display = "none";
|
|
425
|
+
}
|
|
426
|
+
}, 50);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
showView(viewId, animate = true) {
|
|
430
|
+
const view = this.views.get(viewId);
|
|
431
|
+
if (view) {
|
|
432
|
+
const element = view.getHostElement();
|
|
433
|
+
element.style.display = "block";
|
|
434
|
+
element.classList.remove("nav-hidden");
|
|
435
|
+
element.style.visibility = "visible";
|
|
436
|
+
if (!animate) {
|
|
437
|
+
element.style.opacity = "1";
|
|
438
|
+
element.style.transform = "none";
|
|
439
|
+
}
|
|
440
|
+
// When animating, let CSS animation classes control opacity entirely
|
|
441
|
+
// Don't set inline opacity as it would override CSS classes
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
hideView(viewId) {
|
|
445
|
+
const view = this.views.get(viewId);
|
|
446
|
+
if (view) {
|
|
447
|
+
const element = view.getHostElement();
|
|
448
|
+
element.classList.add("nav-hidden");
|
|
449
|
+
element.style.display = "none";
|
|
450
|
+
element.style.visibility = "hidden";
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
hideAllViews() {
|
|
454
|
+
// Hide all registered views to ensure clean state
|
|
455
|
+
this.views.forEach((_, viewId) => {
|
|
456
|
+
this.hideView(viewId);
|
|
457
|
+
});
|
|
458
|
+
log.trace("All views hidden");
|
|
459
|
+
}
|
|
460
|
+
getCurrentPageId() {
|
|
461
|
+
return stateManager.get("navigation.currentPageId");
|
|
462
|
+
}
|
|
463
|
+
getCurrentViewId() {
|
|
464
|
+
return stateManager.get("navigation.currentViewId");
|
|
465
|
+
}
|
|
466
|
+
isTransitioning() {
|
|
467
|
+
return stateManager.get("navigation.isTransitioning");
|
|
468
|
+
}
|
|
469
|
+
getRegisteredPages() {
|
|
470
|
+
return Array.from(this.pages.keys());
|
|
471
|
+
}
|
|
472
|
+
getRegisteredViews() {
|
|
473
|
+
return Array.from(this.views.keys());
|
|
474
|
+
}
|
|
475
|
+
// Convenience methods for external components to subscribe to navigation state
|
|
476
|
+
subscribeToCurrentPage(callback) {
|
|
477
|
+
return stateManager.subscribe("navigation.currentPageId", callback);
|
|
478
|
+
}
|
|
479
|
+
subscribeToCurrentView(callback) {
|
|
480
|
+
return stateManager.subscribe("navigation.currentViewId", callback);
|
|
481
|
+
}
|
|
482
|
+
subscribeToTransitionState(callback) {
|
|
483
|
+
return stateManager.subscribe("navigation.isTransitioning", callback);
|
|
484
|
+
}
|
|
485
|
+
/**
|
|
486
|
+
* Clean up any orphaned transition listeners
|
|
487
|
+
* This is a safety net for transitions that didn't complete properly
|
|
488
|
+
*/
|
|
489
|
+
cleanupOrphanedTransitions() {
|
|
490
|
+
const orphanedCount = this.activeTransitionCleanups.size;
|
|
491
|
+
if (orphanedCount > 0) {
|
|
492
|
+
log.trace(`Cleaning up ${orphanedCount} orphaned transition(s)`);
|
|
493
|
+
this.activeTransitionCleanups.forEach((cleanup, element) => {
|
|
494
|
+
try {
|
|
495
|
+
cleanup();
|
|
496
|
+
}
|
|
497
|
+
catch (error) {
|
|
498
|
+
log.error(`Error cleaning up orphaned transition for element with ID ${element.id}:`, error);
|
|
499
|
+
}
|
|
500
|
+
});
|
|
501
|
+
this.activeTransitionCleanups.clear();
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
/**
|
|
505
|
+
* Get the count of active transitions
|
|
506
|
+
*/
|
|
507
|
+
getActiveTransitionCount() {
|
|
508
|
+
return this.activeTransitionCleanups.size;
|
|
509
|
+
}
|
|
510
|
+
/**
|
|
511
|
+
* Cleanup method for proper resource management
|
|
512
|
+
*
|
|
513
|
+
* Destroys the NavigationManager instance and clears the singleton reference.
|
|
514
|
+
* After calling destroy(), a new NavigationManager instance can be created if needed
|
|
515
|
+
* (though this is typically only necessary during application teardown/restart).
|
|
516
|
+
*/
|
|
517
|
+
destroy() {
|
|
518
|
+
// Clean up any active transitions
|
|
519
|
+
this.cleanupOrphanedTransitions();
|
|
520
|
+
// Clean up any state subscriptions
|
|
521
|
+
this.stateSubscriptions.forEach((subscription) => {
|
|
522
|
+
subscription.unsubscribe();
|
|
523
|
+
});
|
|
524
|
+
this.stateSubscriptions.length = 0;
|
|
525
|
+
// Clear local maps
|
|
526
|
+
this.pages.clear();
|
|
527
|
+
this.views.clear();
|
|
528
|
+
// Clear the singleton instance reference to allow new instance creation
|
|
529
|
+
NavigationManager.instance = null;
|
|
530
|
+
NavigationManager.isDestroyed = true;
|
|
531
|
+
log.trace("NavigationManager singleton instance destroyed and cleared");
|
|
532
|
+
}
|
|
533
|
+
}
|