// ARS Page Router Component // Provides routing functionality for web applications with URL integration // // usage: // //
Dashboard page content
//
Bots page content
//
Settings page content
//
// // Remote call methods: // - showPage(pageId): Shows the page with the specified ID // - hidePage(pageId): Hides the page with the specified ID // - showAllPages(): Shows all pages // - hideAllPages(): Hides all pages // - getCurrentPage(): Returns the ID of the currently visible page // - getPageInfo(): Returns information about the current page and available pages // - navigateToRoute(route): Navigate to a specific route // - getCurrentRoute(): Get the current route import WebComponentBase from "../web-component-base/web-component-base.js"; class ArsPage extends WebComponentBase { [key: string]: any; constructor() { super(); this._currentPage = null; this._pages = new Map(); this._defaultPage = null; this._routes = {}; this._routeToPageMap = new Map(); this._pageToRouteMap = new Map(); this._currentRoute = null; this._routingMode = "browser"; this._popstateHandler = this._handlePopState.bind(this); } static get observedAttributes() { return ["default-page", "routes", "base-path", "routing-mode"]; } static defaultAttributeValue(name: string) { if (name === "routes") { return "{}"; } if (name === "routing-mode") { return "browser"; } return null; } static parseAttributeValue(name: string, value: string) { if (name === "routes") { try { return JSON.parse(value); } catch (e) { console.error("Failed to parse routes attribute:", e); return {}; } } return WebComponentBase.parseAttributeValue(name, value); } allAttributesChangedCallback(attributes: Record) { if (attributes.routes) { this._routes = attributes.routes; this._buildRouteMaps(); } if (attributes["default-page"]) { this._defaultPage = attributes["default-page"]; } this._basePath = attributes["base-path"] || ""; this._routingMode = attributes["routing-mode"] || "browser"; } connectedCallback() { super.connectedCallback(); if (this._usesBrowserRouting()) { this._getBrowserWindow()?.addEventListener("popstate", this._popstateHandler); } this._initializePages(); this._buildRouteMaps(); // Browser routing uses the current URL; internal routing stays local to the component. const currentPath = this._usesBrowserRouting() ? this._getBrowserWindow()?.location.pathname || null : this._currentRoute; const pageId = currentPath ? this._getPageIdFromRoute(currentPath) : null; if (pageId && this._pages.has(pageId)) { this.showPage(pageId); } else if (this._defaultPage) { this.showPage(this._defaultPage); } else if (this._pages.size > 0) { const firstPageId = this._pages.keys().next().value; this.showPage(firstPageId); } } disconnectedCallback() { super.disconnectedCallback(); if (this._usesBrowserRouting()) { this._getBrowserWindow()?.removeEventListener("popstate", this._popstateHandler); } this._pages.clear(); } // Resolves the window object from the current ownerDocument. _getBrowserWindow() { return this.ownerDocument?.defaultView || null; } // Internal mode skips global history/location integration so the component can be embedded safely. _usesBrowserRouting() { return this._routingMode !== "internal"; } // Private methods _initializePages() { this._pages.clear(); const pageElements = Array.from(this.children).filter((child) => child.id); pageElements.forEach((element: Element) => { this._pages.set(element.id, element); (element as HTMLElement).style.display = "none"; }); } _buildRouteMaps() { this._routeToPageMap.clear(); this._pageToRouteMap.clear(); const processRoutes = (routes: Record, parentKey: string | null = null) => { Object.entries(routes).forEach(([key, value]) => { if (typeof value === "string") { // The key is the pageId (e.g., "demo-ars-calendar") // The value is the route (e.g., "/demos/ars-calendar") this._routeToPageMap.set(value, key); // Maps route -> pageId this._pageToRouteMap.set(key, value); // Maps pageId -> route } else if (typeof value === "object" && value !== null) { const parentPageId = parentKey || key; if (!this._routeToPageMap.has(`/${parentPageId}`)) { this._routeToPageMap.set(`/${parentPageId}`, parentPageId); this._pageToRouteMap.set(parentPageId, `/${parentPageId}`); } Object.entries(value as Record).forEach(([nestedKey, nestedValue]) => { if (typeof nestedValue === "string") { this._routeToPageMap.set(nestedValue, parentPageId); } else if (typeof nestedValue === "object" && nestedValue !== null) { processRoutes({ [nestedKey]: nestedValue }, parentPageId); } }); } }); }; processRoutes(this._routes); } _getPageIdFromRoute(route: string) { const rel = route.startsWith(this._basePath) ? route.slice(this._basePath.length) : route; // Try exact match first if (this._routeToPageMap.has(rel)) { const pageId = this._routeToPageMap.get(rel); return pageId; } // Try partial matches for nested routes for (const [routePath, pageId] of this._routeToPageMap) { if (rel.startsWith(routePath)) { return pageId; } } return null; } _getRouteFromPageId(pageId: string) { // Get the direct route mapping const directRoute = this._pageToRouteMap.get(pageId); if (directRoute) { return this._basePath + directRoute; } // If no direct route, return null return null; } _isNestedRouteForPage(path: string, pageId: string) { const directRoute = this._getRouteFromPageId(pageId); if (!directRoute) return false; // Check if the path is a nested route for this page // For example, if pageId is 'configs' and directRoute is '/configs', // then '/configs/system' would be a nested route const directRoutePath = directRoute.replace(this._basePath, ''); return path.startsWith(directRoutePath) && path !== directRoutePath; } _updateBrowserUrl(route: string | null) { if (!route) { return; } if (!this._usesBrowserRouting()) { this._currentRoute = route; return; } const hostWindow = this._getBrowserWindow(); const abs = this._basePath + route; if (hostWindow && abs && abs !== hostWindow.location.pathname) { hostWindow.history.pushState({ pageId: this._currentPage }, "", abs); this._currentRoute = abs; } } _handlePopState(_event: PopStateEvent) { if (!this._usesBrowserRouting()) { return; } const currentPath = this._getBrowserWindow()?.location.pathname || ""; const pageId = this._getPageIdFromRoute(currentPath); if (pageId && this._pages.has(pageId)) { // Update the current route first so the event has the correct route this._currentRoute = currentPath; this._showPage(pageId, false); // Don't update URL since we're responding to URL change } } _showPage(pageId: string, updateUrl: boolean = true) { if (!this._pages.has(pageId)) { console.error(`ARS Page: Page with ID '${pageId}' not found`); return false; } const previousPage = this._currentPage; if (previousPage && this._pages.has(previousPage)) { this._pages.get(previousPage).style.display = "none"; } const pageElement = this._pages.get(pageId); pageElement.style.display = ""; this._currentPage = pageId; // Update browser URL if requested if (updateUrl) { // For nested routes, preserve the original route if it's more specific const currentPath = this._usesBrowserRouting() ? this._getBrowserWindow()?.location.pathname || "" : (this._currentRoute || ""); const directRoute = this._getRouteFromPageId(pageId); // Check if current path is a nested route for this page const isNestedRoute = this._isNestedRouteForPage(currentPath, pageId); if (isNestedRoute) { // Preserve the nested route this._updateBrowserUrl(currentPath); } else { // Use the direct route this._updateBrowserUrl(directRoute); } } this.dispatchEvent( new CustomEvent("ars-page:page-changed", { detail: { previousPage, currentPage: pageId, pageElement: pageElement, route: this._currentRoute, }, bubbles: true, composed: true, }), ); return true; } _hidePage(pageId: string) { if (!this._pages.has(pageId)) { console.error(`ARS Page: Page with ID '${pageId}' not found`); return false; } const pageElement = this._pages.get(pageId); pageElement.style.display = "none"; if (this._currentPage === pageId) { this._currentPage = null; } return true; } // Public methods (called by ars-page-controller via _callRemote) showPage(pageId: string) { const success = this._showPage(pageId); return { success, pageId, currentPage: this._currentPage, route: this._currentRoute, }; } hidePage(pageId: string) { const success = this._hidePage(pageId); return { success, pageId, currentPage: this._currentPage }; } showAllPages() { this._pages.forEach((element: Element) => { (element as HTMLElement).style.display = ""; }); return { success: true, pagesShown: this._pages.size }; } hideAllPages() { this._pages.forEach((element: Element) => { (element as HTMLElement).style.display = "none"; }); this._currentPage = null; return { success: true, pagesHidden: this._pages.size }; } getCurrentPage() { return { currentPage: this._currentPage, availablePages: Array.from(this._pages.keys()), currentRoute: this._currentRoute, }; } getPageInfo() { return { currentPage: this._currentPage, availablePages: Array.from(this._pages.keys()), totalPages: this._pages.size, defaultPage: this._defaultPage, currentRoute: this._currentRoute, routes: this._routes, }; } // New methods for route-based navigation navigateToRoute(route: string) { const pageId = this._getPageIdFromRoute(route); if (pageId) { // Update the current route first so the event has the correct route this._currentRoute = route; // Show the page without updating URL (we'll do it manually) const success = this._showPage(pageId, false); if (success) { // Internal mode keeps route state local; browser mode updates history. this._updateBrowserUrl(route); return { success: true, pageId, currentPage: this._currentPage, route: this._currentRoute, }; } return { success: false, pageId, currentPage: this._currentPage, route: this._currentRoute, }; } console.error(`ARS Page: No page found for route '${route}'`); return { success: false, route, error: "Route not found" }; } getCurrentRoute() { return { currentRoute: this._currentRoute, currentPage: this._currentPage, availableRoutes: Array.from(this._routeToPageMap.keys()), }; } } // Register the custom element export { ArsPage, ArsPage as default };