Files
dees-catalog/ts_web/elements/00group-appui/dees-appui/dees-appui.ts

1107 lines
32 KiB
TypeScript

import {
DeesElement,
type TemplateResult,
property,
customElement,
html,
css,
cssManager,
state,
} from '@design.estate/dees-element';
import * as domtools from '@design.estate/dees-domtools';
import * as interfaces from '../../interfaces/index.js';
import type { DeesAppuiBar } from '../dees-appui-appbar/index.js';
import type { DeesAppuiMainmenu } from '../dees-appui-mainmenu/dees-appui-mainmenu.js';
import type { DeesAppuiSecondarymenu } from '../dees-appui-secondarymenu/dees-appui-secondarymenu.js';
import type { DeesAppuiMaincontent } from '../dees-appui-maincontent/dees-appui-maincontent.js';
import type { DeesAppuiActivitylog } from '../dees-appui-activitylog/dees-appui-activitylog.js';
import { demoFunc } from './dees-appui.demo.js';
import { themeDefaultStyles } from '../../00theme.js';
// View registry for managing views
import { ViewRegistry } from './view.registry.js';
// Import child components
import '../dees-appui-appbar/index.js';
import '../dees-appui-mainmenu/dees-appui-mainmenu.js';
import '../dees-appui-secondarymenu/dees-appui-secondarymenu.js';
import '../dees-appui-maincontent/dees-appui-maincontent.js';
import '../dees-appui-activitylog/dees-appui-activitylog.js';
declare global {
interface HTMLElementTagNameMap {
'dees-appui': DeesAppui;
}
}
@customElement('dees-appui')
export class DeesAppui extends DeesElement {
public static demo = demoFunc;
// ==========================================
// REACTIVE OBSERVABLES (RxJS Subjects)
// ==========================================
/** Observable stream of view lifecycle events */
public viewLifecycle$ = new domtools.plugins.smartrx.rxjs.Subject<interfaces.IViewLifecycleEvent>();
/** Observable stream of view change events */
public viewChanged$ = new domtools.plugins.smartrx.rxjs.Subject<interfaces.IViewChangeEvent>();
// ==========================================
// INTERNAL PROPERTIES (Properties for child components)
// ==========================================
// Properties for appbar
@property({ type: Array })
accessor appbarMenuItems: interfaces.IAppBarMenuItem[] = [];
@property({ type: String })
accessor appbarBreadcrumbs: string = '';
@property({ type: String })
accessor appbarBreadcrumbSeparator: string = ' > ';
@property({ type: Boolean })
accessor appbarShowWindowControls: boolean = true;
@property({ type: Object })
accessor appbarUser: interfaces.IAppUser | undefined = undefined;
@property({ type: Array })
accessor appbarProfileMenuItems: interfaces.IAppBarMenuItem[] = [];
@property({ type: Boolean })
accessor appbarShowSearch: boolean = false;
// Properties for mainmenu
@property({ type: String })
accessor mainmenuLogoIcon: string = '';
@property({ type: String })
accessor mainmenuLogoText: string = '';
@property({ type: Array })
accessor mainmenuGroups: interfaces.IMenuGroup[] = [];
@property({ type: Array })
accessor mainmenuBottomTabs: interfaces.IMenuItem[] = [];
@property({ type: Array })
accessor mainmenuTabs: interfaces.IMenuItem[] = [];
@property({ type: Object })
accessor mainmenuSelectedTab: interfaces.IMenuItem | undefined = undefined;
// Properties for secondarymenu
@property({ type: String })
accessor secondarymenuHeading: string = '';
@property({ type: Array })
accessor secondarymenuGroups: interfaces.ISecondaryMenuGroup[] = [];
@property({ type: Object })
accessor secondarymenuSelectedItem: interfaces.ISecondaryMenuItemTab | undefined = undefined;
// Collapse states
@property({ type: Boolean })
accessor mainmenuCollapsed: boolean = false;
@property({ type: Boolean })
accessor secondarymenuCollapsed: boolean = false;
// Visibility states
@property({ type: Boolean })
accessor mainmenuVisible: boolean = true;
@property({ type: Boolean })
accessor secondarymenuVisible: boolean = true;
@property({ type: Boolean })
accessor maincontentTabsVisible: boolean = true;
@property({ type: Boolean })
accessor contentTabsAutoHide: boolean = false;
@property({ type: Number })
accessor contentTabsAutoHideThreshold: number = 0;
// Activity log visibility and count
@state()
accessor activityLogVisible: boolean = false;
@state()
accessor activityLogCount: number = 0;
// Properties for maincontent
@property({ type: Array })
accessor maincontentTabs: interfaces.IMenuItem[] = [];
@property({ type: Object })
accessor maincontentSelectedTab: interfaces.IMenuItem | undefined = undefined;
// References to child components
@state()
accessor appbar: DeesAppuiBar | undefined = undefined;
@state()
accessor mainmenu: DeesAppuiMainmenu | undefined = undefined;
@state()
accessor secondarymenu: DeesAppuiSecondarymenu | undefined = undefined;
@state()
accessor maincontent: DeesAppuiMaincontent | undefined = undefined;
@state()
accessor activitylogElement: DeesAppuiActivitylog | undefined = undefined;
// Current view state
@state()
accessor currentView: interfaces.IViewDefinition | undefined = undefined;
// Internal services
private viewRegistry: ViewRegistry = new ViewRegistry();
private routerCleanup: (() => void) | null = null;
private searchCallback: ((query: string) => void) | null = null;
public static styles = [
themeDefaultStyles,
cssManager.defaultStyles,
css`
/* TODO: Migrate hardcoded values to --dees-* CSS variables */
:host {
position: absolute;
height: 100%;
width: 100%;
background: ${cssManager.bdTheme('#f0f0f0', '#1a1a1a')};
}
.maingrid {
position: absolute;
top: 40px;
height: calc(100% - 40px);
width: 100%;
display: grid;
/* grid-template-columns set dynamically in template */
grid-template-rows: 1fr;
transition: grid-template-columns 0.3s ease;
overflow: hidden;
}
/* Z-index layering for proper stacking */
.maingrid > dees-appui-mainmenu {
position: relative;
z-index: 3;
}
.maingrid > dees-appui-secondarymenu {
position: relative;
z-index: 2;
}
.maingrid > dees-appui-maincontent {
position: relative;
z-index: 1;
}
.maingrid > dees-appui-activitylog {
position: relative;
z-index: 1;
overflow: hidden;
transition: opacity 0.3s ease, transform 0.3s ease;
}
.maingrid > dees-appui-activitylog.hidden {
opacity: 0;
transform: translateX(20px);
pointer-events: none;
}
.maingrid > dees-appui-activitylog.visible {
opacity: 1;
transform: translateX(0);
}
/* View container for dynamically loaded views */
.view-container {
display: contents;
}
.view-container:empty {
display: none;
}
`,
];
public render(): TemplateResult {
return html`
<dees-appui-appbar
.menuItems=${this.appbarMenuItems}
.breadcrumbs=${this.appbarBreadcrumbs}
.breadcrumbSeparator=${this.appbarBreadcrumbSeparator}
.showWindowControls=${this.appbarShowWindowControls}
.user=${this.appbarUser}
.profileMenuItems=${this.appbarProfileMenuItems}
.showSearch=${this.appbarShowSearch}
.showActivityLogToggle=${true}
.activityLogCount=${this.activityLogCount}
.activityLogActive=${this.activityLogVisible}
@menu-select=${(e: CustomEvent) => this.handleAppbarMenuSelect(e)}
@breadcrumb-navigate=${(e: CustomEvent) => this.handleAppbarBreadcrumbNavigate(e)}
@search-click=${() => this.handleAppbarSearchClick()}
@search-query=${(e: CustomEvent) => this.handleAppbarSearchQuery(e)}
@user-menu-open=${() => this.handleAppbarUserMenuOpen()}
@profile-menu-select=${(e: CustomEvent) => this.handleAppbarProfileMenuSelect(e)}
@activity-toggle=${() => this.toggleActivityLog()}
></dees-appui-appbar>
<div class="maingrid" style="grid-template-columns: auto auto 1fr ${this.activityLogVisible ? '280px' : '0px'};">
${this.mainmenuVisible ? html`
<dees-appui-mainmenu
.logoIcon=${this.mainmenuLogoIcon}
.logoText=${this.mainmenuLogoText}
.menuGroups=${this.mainmenuGroups}
.bottomTabs=${this.mainmenuBottomTabs}
.tabs=${this.mainmenuTabs}
.selectedTab=${this.mainmenuSelectedTab}
.collapsed=${this.mainmenuCollapsed}
@tab-select=${(e: CustomEvent) => this.handleMainmenuTabSelect(e)}
@collapse-change=${(e: CustomEvent) => this.handleMainmenuCollapseChange(e)}
></dees-appui-mainmenu>
` : ''}
${this.secondarymenuVisible ? html`
<dees-appui-secondarymenu
.heading=${this.secondarymenuHeading}
.groups=${this.secondarymenuGroups}
.selectedItem=${this.secondarymenuSelectedItem}
.collapsed=${this.secondarymenuCollapsed}
@item-select=${(e: CustomEvent) => this.handleSecondarymenuItemSelect(e)}
@collapse-change=${(e: CustomEvent) => this.handleSecondarymenuCollapseChange(e)}
></dees-appui-secondarymenu>
` : ''}
<dees-appui-maincontent
.tabs=${this.maincontentTabs}
.selectedTab=${this.maincontentSelectedTab}
.showTabs=${this.maincontentTabsVisible}
.tabsAutoHide=${this.contentTabsAutoHide}
.tabsAutoHideThreshold=${this.contentTabsAutoHideThreshold}
@tab-select=${(e: CustomEvent) => this.handleContentTabSelect(e)}
@tab-close=${(e: CustomEvent) => this.handleContentTabClose(e)}
>
<div class="view-container"></div>
<slot name="maincontent"></slot>
</dees-appui-maincontent>
<dees-appui-activitylog
class="${this.activityLogVisible ? 'visible' : 'hidden'}"
></dees-appui-activitylog>
</div>
`;
}
async firstUpdated() {
// Get references to child components
this.appbar = this.shadowRoot!.querySelector('dees-appui-appbar') as DeesAppuiBar;
this.mainmenu = this.shadowRoot!.querySelector('dees-appui-mainmenu') as DeesAppuiMainmenu;
this.secondarymenu = this.shadowRoot!.querySelector('dees-appui-secondarymenu') as DeesAppuiSecondarymenu;
this.maincontent = this.shadowRoot!.querySelector('dees-appui-maincontent') as DeesAppuiMaincontent;
this.activitylogElement = this.shadowRoot!.querySelector('dees-appui-activitylog') as DeesAppuiActivitylog;
// Subscribe to activity log entry changes for badge count
if (this.activitylogElement) {
this.activitylogElement.entries$.subscribe((entries) => {
this.activityLogCount = entries.length;
});
}
// Set appui reference in view registry for lifecycle context
this.viewRegistry.setAppuiRef(this as unknown as interfaces.TDeesAppui);
}
async disconnectedCallback() {
await super.disconnectedCallback();
// Clean up router listener
if (this.routerCleanup) {
this.routerCleanup();
this.routerCleanup = null;
}
// Complete subjects
this.viewLifecycle$.complete();
this.viewChanged$.complete();
}
// ==========================================
// PROGRAMMATIC API: APP BAR
// ==========================================
/**
* Set the app bar menu items (File, Edit, View, etc.)
*/
public setAppBarMenus(menus: interfaces.IAppBarMenuItem[]): void {
this.appbarMenuItems = [...menus];
}
/**
* Update a single app bar menu by name
*/
public updateAppBarMenu(name: string, update: Partial<interfaces.IAppBarMenuItem>): void {
this.appbarMenuItems = this.appbarMenuItems.map(menu => {
// Check if it's not a divider and has a name property
if ('name' in menu && menu.name === name) {
return { ...menu, ...update };
}
return menu;
});
}
/**
* Set the breadcrumbs (string or array)
*/
public setBreadcrumbs(breadcrumbs: string | string[]): void {
if (Array.isArray(breadcrumbs)) {
this.appbarBreadcrumbs = breadcrumbs.join(this.appbarBreadcrumbSeparator);
} else {
this.appbarBreadcrumbs = breadcrumbs;
}
}
/**
* Set the current user
*/
public setUser(user: interfaces.IAppUser | undefined): void {
this.appbarUser = user;
}
/**
* Set the profile dropdown menu items
*/
public setProfileMenuItems(items: interfaces.IAppBarMenuItem[]): void {
this.appbarProfileMenuItems = [...items];
}
/**
* Set search bar visibility
*/
public setSearchVisible(visible: boolean): void {
this.appbarShowSearch = visible;
}
/**
* Set window controls visibility
*/
public setWindowControlsVisible(visible: boolean): void {
this.appbarShowWindowControls = visible;
}
/**
* Register a search callback
*/
public onSearch(callback: (query: string) => void): void {
this.searchCallback = callback;
}
// ==========================================
// PROGRAMMATIC API: MAIN MENU
// ==========================================
/**
* Set the entire main menu configuration
*/
public setMainMenu(config: interfaces.IMainMenuConfig): void {
if (config.logoIcon !== undefined) {
this.mainmenuLogoIcon = config.logoIcon;
}
if (config.logoText !== undefined) {
this.mainmenuLogoText = config.logoText;
}
if (config.groups !== undefined) {
this.mainmenuGroups = [...config.groups];
}
if (config.bottomTabs !== undefined) {
this.mainmenuBottomTabs = [...config.bottomTabs];
}
}
/**
* Update a specific menu group by name
*/
public updateMainMenuGroup(groupName: string, update: Partial<interfaces.IMenuGroup>): void {
this.mainmenuGroups = this.mainmenuGroups.map(group =>
group.name === groupName ? { ...group, ...update } : group
);
}
/**
* Add a menu item to a specific group
*/
public addMainMenuItem(groupName: string, tab: interfaces.IMenuItem): void {
this.mainmenuGroups = this.mainmenuGroups.map(group => {
if (group.name === groupName) {
return {
...group,
items: [...(group.items || []), tab],
};
}
return group;
});
}
/**
* Remove a menu item from a group by key
*/
public removeMainMenuItem(groupName: string, tabKey: string): void {
this.mainmenuGroups = this.mainmenuGroups.map(group => {
if (group.name === groupName) {
return {
...group,
items: (group.items || []).filter(t => t.key !== tabKey),
};
}
return group;
});
}
/**
* Set the selected main menu item by key
*/
public setMainMenuSelection(tabKey: string): void {
for (const group of this.mainmenuGroups) {
const tab = group.items?.find(t => t.key === tabKey);
if (tab) {
this.mainmenuSelectedTab = tab;
return;
}
}
// Check bottom tabs
const bottomTab = this.mainmenuBottomTabs.find(t => t.key === tabKey);
if (bottomTab) {
this.mainmenuSelectedTab = bottomTab;
}
}
/**
* Set main menu collapsed state
*/
public setMainMenuCollapsed(collapsed: boolean): void {
this.mainmenuCollapsed = collapsed;
}
/**
* Set main menu visibility
*/
public setMainMenuVisible(visible: boolean): void {
this.mainmenuVisible = visible;
}
/**
* Set secondary menu collapsed state
*/
public setSecondaryMenuCollapsed(collapsed: boolean): void {
this.secondarymenuCollapsed = collapsed;
}
/**
* Set secondary menu visibility
*/
public setSecondaryMenuVisible(visible: boolean): void {
this.secondarymenuVisible = visible;
}
/**
* Set content tabs visibility
*/
public setContentTabsVisible(visible: boolean): void {
this.maincontentTabsVisible = visible;
}
/**
* Set content tabs auto-hide behavior
* @param enabled - Enable auto-hide feature
* @param threshold - Hide when tabs.length <= threshold (default 0 = hide when no tabs)
*/
public setContentTabsAutoHide(enabled: boolean, threshold: number = 0): void {
this.contentTabsAutoHide = enabled;
this.contentTabsAutoHideThreshold = threshold;
}
/**
* Set a badge on a main menu item
*/
public setMainMenuBadge(tabKey: string, badge: string | number): void {
this.mainmenuGroups = this.mainmenuGroups.map(group => ({
...group,
items: (group.items || []).map(tab =>
tab.key === tabKey ? { ...tab, badge } : tab
),
}));
// Also check bottom tabs
this.mainmenuBottomTabs = this.mainmenuBottomTabs.map(tab =>
tab.key === tabKey ? { ...tab, badge } : tab
);
}
/**
* Clear a badge from a main menu item
*/
public clearMainMenuBadge(tabKey: string): void {
this.mainmenuGroups = this.mainmenuGroups.map(group => ({
...group,
items: (group.items || []).map(tab => {
if (tab.key === tabKey) {
const { badge, ...rest } = tab;
return rest;
}
return tab;
}),
}));
// Also check bottom tabs
this.mainmenuBottomTabs = this.mainmenuBottomTabs.map(tab => {
if (tab.key === tabKey) {
const { badge, ...rest } = tab;
return rest;
}
return tab;
});
}
// ==========================================
// PROGRAMMATIC API: SECONDARY MENU
// ==========================================
/**
* Set the secondary menu configuration
*/
public setSecondaryMenu(config: { heading?: string; groups: interfaces.ISecondaryMenuGroup[] }): void {
if (config.heading !== undefined) {
this.secondarymenuHeading = config.heading;
}
this.secondarymenuGroups = [...config.groups];
}
/**
* Update a specific secondary menu group
*/
public updateSecondaryMenuGroup(groupName: string, update: Partial<interfaces.ISecondaryMenuGroup>): void {
this.secondarymenuGroups = this.secondarymenuGroups.map(group =>
group.name === groupName ? { ...group, ...update } : group
);
}
/**
* Add an item to a secondary menu group
*/
public addSecondaryMenuItem(
groupName: string,
item: interfaces.ISecondaryMenuItem
): void {
this.secondarymenuGroups = this.secondarymenuGroups.map(group => {
if (group.name === groupName) {
return {
...group,
items: [...group.items, item],
};
}
return group;
});
}
/**
* Set the selected secondary menu item by key (for tab items only)
*/
public setSecondaryMenuSelection(itemKey: string): void {
for (const group of this.secondarymenuGroups) {
const item = group.items.find(i => 'key' in i && i.key === itemKey);
if (item && (!('type' in item) || item.type === 'tab' || item.type === undefined)) {
this.secondarymenuSelectedItem = item as interfaces.ISecondaryMenuItemTab;
return;
}
}
}
/**
* Clear the secondary menu
*/
public clearSecondaryMenu(): void {
this.secondarymenuHeading = '';
this.secondarymenuGroups = [];
this.secondarymenuSelectedItem = undefined;
}
// ==========================================
// PROGRAMMATIC API: CONTENT TABS
// ==========================================
/**
* Set the content tabs
*/
public setContentTabs(tabs: interfaces.IMenuItem[]): void {
this.maincontentTabs = [...tabs];
if (tabs.length > 0 && !this.maincontentSelectedTab) {
this.maincontentSelectedTab = tabs[0];
}
}
/**
* Add a content tab
*/
public addContentTab(tab: interfaces.IMenuItem): void {
this.maincontentTabs = [...this.maincontentTabs, tab];
}
/**
* Remove a content tab by key
*/
public removeContentTab(tabKey: string): void {
this.maincontentTabs = this.maincontentTabs.filter(t => t.key !== tabKey);
if (this.maincontentSelectedTab?.key === tabKey) {
this.maincontentSelectedTab = this.maincontentTabs[0];
}
}
/**
* Select a content tab by key
*/
public selectContentTab(tabKey: string): void {
const tab = this.maincontentTabs.find(t => t.key === tabKey);
if (tab) {
this.maincontentSelectedTab = tab;
}
}
/**
* Get the currently selected content tab
*/
public getSelectedContentTab(): interfaces.IMenuItem | undefined {
return this.maincontentSelectedTab;
}
// ==========================================
// PROGRAMMATIC API: ACTIVITY LOG
// ==========================================
/**
* Get the activity log API
*/
public get activityLog(): interfaces.IActivityLogAPI {
if (!this.activitylogElement) {
// Return a deferred API that will work after firstUpdated
return {
add: (entry) => {
this.updateComplete.then(() => this.activitylogElement?.add(entry));
},
addMany: (entries) => {
this.updateComplete.then(() => this.activitylogElement?.addMany(entries));
},
clear: () => {
this.updateComplete.then(() => this.activitylogElement?.clear());
},
getEntries: () => this.activitylogElement?.getEntries() || [],
filter: (criteria) => this.activitylogElement?.filter(criteria) || [],
search: (query) => this.activitylogElement?.search(query) || [],
};
}
return {
add: (entry) => this.activitylogElement!.add(entry),
addMany: (entries) => this.activitylogElement!.addMany(entries),
clear: () => this.activitylogElement!.clear(),
getEntries: () => this.activitylogElement!.getEntries(),
filter: (criteria) => this.activitylogElement!.filter(criteria),
search: (query) => this.activitylogElement!.search(query),
};
}
/**
* Set activity log visibility
*/
public setActivityLogVisible(visible: boolean): void {
this.activityLogVisible = visible;
}
/**
* Toggle activity log visibility
*/
public toggleActivityLog(): void {
this.activityLogVisible = !this.activityLogVisible;
}
/**
* Get activity log visibility state
*/
public getActivityLogVisible(): boolean {
return this.activityLogVisible;
}
// ==========================================
// PROGRAMMATIC API: NAVIGATION
// ==========================================
/**
* Navigate to a view by ID
*/
public async navigateToView(viewId: string, params?: Record<string, string>): Promise<boolean> {
const view = this.viewRegistry.get(viewId);
if (!view) {
console.warn(`Cannot navigate to unknown view: ${viewId}`);
return false;
}
// Check if current view allows navigation
const canLeave = await this.viewRegistry.canLeaveCurrentView();
if (canLeave !== true) {
if (typeof canLeave === 'string') {
// Show confirmation dialog
const confirmed = window.confirm(canLeave);
if (!confirmed) return false;
} else {
return false;
}
}
// Emit loading event
this.viewLifecycle$.next({ type: 'loading', viewId });
try {
await this.loadView(view, params);
// Update URL hash
const route = view.route || viewId;
const newHash = `#${route}`;
if (window.location.hash !== newHash) {
window.history.pushState({ viewId }, '', newHash);
}
return true;
} catch (error) {
this.viewLifecycle$.next({ type: 'loadError', viewId, error });
return false;
}
}
/**
* Get the current view
*/
public getCurrentView(): interfaces.IViewDefinition | undefined {
return this.currentView;
}
/**
* Get access to the view registry (for advanced use)
*/
public getViewRegistry(): ViewRegistry {
return this.viewRegistry;
}
// ==========================================
// UNIFIED CONFIGURATION
// ==========================================
/**
* Configure the app shell with a unified config object
*/
public configure(config: interfaces.IAppConfig): void {
// Register views
if (config.views) {
this.viewRegistry.clear();
this.viewRegistry.registerAll(config.views);
}
// Apply branding
if (config.branding) {
this.mainmenuLogoIcon = config.branding.logoIcon || '';
this.mainmenuLogoText = config.branding.logoText || '';
}
// Apply app bar config
if (config.appBar) {
this.appbarMenuItems = config.appBar.menuItems || [];
this.appbarBreadcrumbs = config.appBar.breadcrumbs || '';
this.appbarBreadcrumbSeparator = config.appBar.breadcrumbSeparator || ' > ';
this.appbarShowWindowControls = config.appBar.showWindowControls ?? true;
this.appbarShowSearch = config.appBar.showSearch ?? false;
this.appbarUser = config.appBar.user;
this.appbarProfileMenuItems = config.appBar.profileMenuItems || [];
}
// Build main menu from view references or direct config
if (config.mainMenu) {
if (config.mainMenu.sections) {
this.mainmenuGroups = this.buildMainMenuFromSections(config);
} else if (config.mainMenu.groups) {
this.mainmenuGroups = config.mainMenu.groups;
}
if (config.mainMenu.logoIcon) {
this.mainmenuLogoIcon = config.mainMenu.logoIcon;
}
if (config.mainMenu.logoText) {
this.mainmenuLogoText = config.mainMenu.logoText;
}
if (config.mainMenu.bottomTabs) {
this.mainmenuBottomTabs = config.mainMenu.bottomTabs;
} else if (config.mainMenu.bottomItems) {
this.mainmenuBottomTabs = this.buildBottomTabsFromItems(config.mainMenu.bottomItems);
}
}
// Setup domtools.router integration
this.setupRouterIntegration(config);
// Bind event callbacks
if (config.onViewChange) {
this.viewChanged$.subscribe((event) => {
config.onViewChange!(event.viewId, event.view);
});
}
if (config.onSearch) {
this.searchCallback = config.onSearch;
}
// Navigate to default view
if (config.defaultView) {
this.navigateToView(config.defaultView);
}
}
// ==========================================
// PRIVATE HELPER METHODS
// ==========================================
private setupRouterIntegration(config: interfaces.IAppConfig): void {
// Handle hash change events
const handleHashChange = () => {
const hash = window.location.hash.slice(1); // Remove #
if (!hash) return;
const match = this.viewRegistry.findByRoute(hash);
if (match) {
this.navigateToView(match.view.id, match.params);
}
};
window.addEventListener('hashchange', handleHashChange);
// Store cleanup function
this.routerCleanup = () => {
window.removeEventListener('hashchange', handleHashChange);
};
// Handle initial route from hash
const currentHash = window.location.hash.slice(1);
if (currentHash) {
const match = this.viewRegistry.findByRoute(currentHash);
if (match) {
// Use setTimeout to allow component to fully initialize
setTimeout(() => this.navigateToView(match.view.id, match.params), 0);
}
}
}
private buildMainMenuFromSections(config: interfaces.IAppConfig): interfaces.IMenuGroup[] {
if (!config.mainMenu?.sections) return [];
return config.mainMenu.sections.map((section) => ({
name: section.name,
items: section.views
.map((viewId) => {
const view = this.viewRegistry.get(viewId);
if (!view) {
console.warn(`View "${viewId}" not found in registry`);
return null;
}
return {
key: view.id,
iconName: view.iconName,
action: () => this.navigateToView(viewId),
badge: view.badge,
} as interfaces.IMenuItem;
})
.filter(Boolean) as interfaces.IMenuItem[],
}));
}
private buildBottomTabsFromItems(items: string[]): interfaces.IMenuItem[] {
return items
.map((viewId) => {
const view = this.viewRegistry.get(viewId);
if (!view) {
console.warn(`View "${viewId}" not found in registry`);
return null;
}
return {
key: view.id,
iconName: view.iconName,
action: () => this.navigateToView(viewId),
} as interfaces.IMenuItem;
})
.filter(Boolean) as interfaces.IMenuItem[];
}
private async loadView(
view: interfaces.IViewDefinition,
params?: Record<string, string>
): Promise<void> {
const previousView = this.currentView;
this.currentView = view;
// Get view container
const viewContainer = this.maincontent?.querySelector('.view-container')
|| this.shadowRoot?.querySelector('.view-container');
if (viewContainer) {
// Activate view with caching and lifecycle hooks
const element = await this.viewRegistry.activateView(
view.id,
viewContainer as HTMLElement,
params
);
if (element) {
// Emit lifecycle event
this.viewLifecycle$.next({
type: 'activated',
viewId: view.id,
element,
params,
});
}
}
// Apply view-specific secondary menu
if (view.secondaryMenu) {
this.secondarymenuGroups = view.secondaryMenu;
this.secondarymenuHeading = view.name;
}
// Apply view-specific content tabs
if (view.contentTabs) {
this.maincontentTabs = view.contentTabs;
}
// Update main menu selection
this.setMainMenuSelection(view.id);
// Emit view change event
const changeEvent: interfaces.IViewChangeEvent = {
viewId: view.id,
view,
previousView,
params,
};
this.viewChanged$.next(changeEvent);
// Also dispatch DOM event for backwards compatibility
this.dispatchEvent(
new CustomEvent('view-change', {
detail: changeEvent,
bubbles: true,
composed: true,
})
);
}
// ==========================================
// EVENT HANDLERS (Internal)
// ==========================================
private handleAppbarMenuSelect(e: CustomEvent) {
this.dispatchEvent(new CustomEvent('appbar-menu-select', {
detail: e.detail,
bubbles: true,
composed: true
}));
}
private handleAppbarBreadcrumbNavigate(e: CustomEvent) {
this.dispatchEvent(new CustomEvent('appbar-breadcrumb-navigate', {
detail: e.detail,
bubbles: true,
composed: true
}));
}
private handleAppbarSearchClick() {
this.dispatchEvent(new CustomEvent('appbar-search-click', {
bubbles: true,
composed: true
}));
}
private handleAppbarSearchQuery(e: CustomEvent) {
if (this.searchCallback) {
this.searchCallback(e.detail.query);
}
this.dispatchEvent(new CustomEvent('search-query', {
detail: e.detail,
bubbles: true,
composed: true
}));
}
private handleAppbarUserMenuOpen() {
this.dispatchEvent(new CustomEvent('appbar-user-menu-open', {
bubbles: true,
composed: true
}));
}
private handleAppbarProfileMenuSelect(e: CustomEvent) {
this.dispatchEvent(new CustomEvent('appbar-profile-menu-select', {
detail: e.detail,
bubbles: true,
composed: true
}));
}
private handleMainmenuTabSelect(e: CustomEvent) {
this.mainmenuSelectedTab = e.detail.tab;
this.dispatchEvent(new CustomEvent('mainmenu-tab-select', {
detail: e.detail,
bubbles: true,
composed: true
}));
}
private handleSecondarymenuItemSelect(e: CustomEvent) {
this.secondarymenuSelectedItem = e.detail.item;
this.dispatchEvent(new CustomEvent('secondarymenu-item-select', {
detail: e.detail,
bubbles: true,
composed: true
}));
}
private handleMainmenuCollapseChange(e: CustomEvent) {
this.mainmenuCollapsed = e.detail.collapsed;
this.dispatchEvent(new CustomEvent('mainmenu-collapse-change', {
detail: e.detail,
bubbles: true,
composed: true
}));
}
private handleSecondarymenuCollapseChange(e: CustomEvent) {
this.secondarymenuCollapsed = e.detail.collapsed;
this.dispatchEvent(new CustomEvent('secondarymenu-collapse-change', {
detail: e.detail,
bubbles: true,
composed: true
}));
}
private handleContentTabSelect(e: CustomEvent) {
this.maincontentSelectedTab = e.detail.tab;
this.dispatchEvent(new CustomEvent('content-tab-select', {
detail: e.detail,
bubbles: true,
composed: true
}));
}
private handleContentTabClose(e: CustomEvent) {
this.dispatchEvent(new CustomEvent('content-tab-close', {
detail: e.detail,
bubbles: true,
composed: true
}));
}
}