diff --git a/src/layout/appconfigurator.ts b/src/layout/appconfigurator.ts
index e347b31..9ba1a87 100644
--- a/src/layout/appconfigurator.ts
+++ b/src/layout/appconfigurator.ts
@@ -11,7 +11,7 @@ import { InputSwitchModule } from 'primeng/inputswitch';
import { RadioButtonModule } from 'primeng/radiobutton';
import { SelectButton } from 'primeng/selectbutton';
import { ToggleSwitchModule } from 'primeng/toggleswitch';
-import { AppConfigService } from '@/src/service/appconfigservice';
+import { LayoutService } from '@/src/service/applayoutservice';
const presets = {
Aura,
@@ -21,6 +21,7 @@ const presets = {
@Component({
selector: 'app-configurator',
standalone: true,
+ imports: [CommonModule, FormsModule, InputSwitchModule, ButtonModule, RadioButtonModule, SelectButton, ToggleSwitchModule],
template: `
@@ -48,7 +49,7 @@ const presets = {
type="button"
[title]="surface.name"
(click)="updateColors($event, 'surface', surface)"
- [ngClass]="{ 'active-color': selectedSurfaceColor() ? selectedSurfaceColor() === surface.name : configService.appState().darkTheme ? surface.name === 'zinc' : surface.name === 'slate' }"
+ [ngClass]="{ 'active-color': selectedSurfaceColor() ? selectedSurfaceColor() === surface.name : layoutService.layoutConfig().darkTheme ? surface.name === 'zinc' : surface.name === 'slate' }"
[style]="{
'background-color': surface.name === 'noir' ? 'var(--text-color)' : surface?.palette['500']
}"
@@ -62,17 +63,9 @@ const presets = {
@@ -80,7 +73,6 @@ const presets = {
host: {
class: 'config-panel hidden'
},
- imports: [CommonModule, FormsModule, InputSwitchModule, ButtonModule, RadioButtonModule, SelectButton, ToggleSwitchModule]
})
export class AppConfigurator {
get ripple() {
@@ -91,20 +83,16 @@ export class AppConfigurator {
this.config.ripple.set(value);
}
- get isRTL() {
- return this.configService.appState().RTL;
- }
-
config: PrimeNG = inject(PrimeNG);
- configService: AppConfigService = inject(AppConfigService);
+ layoutService: LayoutService = inject(LayoutService);
platformId = inject(PLATFORM_ID);
presets = Object.keys(presets);
onRTLChange(value: boolean) {
- this.configService.appState.update((state) => ({ ...state, RTL: value }));
+ this.layoutService.layoutConfig.update((state) => ({ ...state, RTL: value }));
if (!(document as any).startViewTransition) {
this.toggleRTL(value);
return;
@@ -125,8 +113,8 @@ export class AppConfigurator {
ngOnInit() {
if (isPlatformBrowser(this.platformId)) {
- this.onPresetChange(this.configService.appState().preset);
- this.toggleRTL(this.configService.appState().RTL);
+ this.onPresetChange(this.layoutService.layoutConfig().preset);
+
}
}
@@ -270,15 +258,15 @@ export class AppConfigurator {
];
selectedPrimaryColor = computed(() => {
- return this.configService.appState().primary;
+ return this.layoutService.layoutConfig().primary;
});
- selectedSurfaceColor = computed(() => this.configService.appState().surface);
+ selectedSurfaceColor = computed(() => this.layoutService.layoutConfig().surface);
- selectedPreset = computed(() => this.configService.appState().preset);
+ selectedPreset = computed(() => this.layoutService.layoutConfig().preset);
primaryColors = computed(() => {
- const presetPalette = presets[this.configService.appState().preset].primitive;
+ const presetPalette = presets[this.layoutService.layoutConfig().preset].primitive;
const colors = ['emerald', 'green', 'lime', 'orange', 'amber', 'yellow', 'teal', 'cyan', 'sky', 'blue', 'indigo', 'violet', 'purple', 'fuchsia', 'pink', 'rose'];
const palettes = [{ name: 'noir', palette: {} }];
@@ -344,7 +332,7 @@ export class AppConfigurator {
}
};
} else {
- if (this.configService.appState().preset === 'Nora') {
+ if (this.layoutService.layoutConfig().preset === 'Nora') {
return {
semantic: {
primary: color.palette,
@@ -380,7 +368,7 @@ export class AppConfigurator {
}
}
};
- } else if (this.configService.appState().preset === 'Material') {
+ } else if (this.layoutService.layoutConfig().preset === 'Material') {
return {
semantic: {
primary: color.palette,
@@ -458,9 +446,9 @@ export class AppConfigurator {
updateColors(event: any, type: string, color: any) {
if (type === 'primary') {
- this.configService.appState.update((state) => ({ ...state, primary: color.name }));
+ this.layoutService.layoutConfig.update((state) => ({ ...state, primary: color.name }));
} else if (type === 'surface') {
- this.configService.appState.update((state) => ({ ...state, surface: color.name }));
+ this.layoutService.layoutConfig.update((state) => ({ ...state, surface: color.name }));
}
this.applyTheme(type, color);
@@ -476,10 +464,10 @@ export class AppConfigurator {
}
onPresetChange(event: any) {
- this.configService.appState.update((state) => ({ ...state, preset: event }));
+ this.layoutService.layoutConfig.update((state) => ({ ...state, preset: event }));
const preset = presets[event];
const surfacePalette = this.surfaces.find((s) => s.name === this.selectedSurfaceColor())?.palette;
- if (this.configService.appState().preset === 'Material') {
+ if (this.layoutService.layoutConfig().preset === 'Material') {
document.body.classList.add('material');
this.config.ripple.set(true);
} else {
diff --git a/src/layout/applayout.ts b/src/layout/applayout.ts
new file mode 100644
index 0000000..88560e6
--- /dev/null
+++ b/src/layout/applayout.ts
@@ -0,0 +1,132 @@
+import { Component, Renderer2, ViewChild } from '@angular/core';
+import { ToastModule } from 'primeng/toast';
+import { CommonModule } from '@angular/common';
+import { AppTopBar } from '@/src/layout/apptopbar';
+import { AppSidebar } from '@/src/layout/appsidebar';
+import { NavigationEnd, Router, RouterModule } from '@angular/router';
+import { AppConfigurator } from '@/src/layout/appconfigurator';
+import { AppFooter } from '@/src/layout/appfooter';
+import { filter, Subscription } from 'rxjs';
+import { LayoutService } from '@/src/service/applayoutservice';
+
+@Component({
+ selector: 'app-layout',
+ standalone:true,
+ imports: [
+ CommonModule,
+ ToastModule,
+ AppTopBar,
+ AppSidebar,
+ RouterModule,
+ AppFooter,
+ AppConfigurator
+ ],
+ template: `
+ `,
+})
+export class AppLayout {
+ overlayMenuOpenSubscription: Subscription;
+
+ menuOutsideClickListener: any;
+
+ profileMenuOutsideClickListener: any;
+
+ @ViewChild(AppSidebar) appSidebar!: AppSidebar;
+
+ @ViewChild(AppTopBar) appTopBar!: AppTopBar;
+
+ constructor(public layoutService: LayoutService, public renderer: Renderer2, public router: Router) {
+ this.overlayMenuOpenSubscription = this.layoutService.overlayOpen$.subscribe(() => {
+ if (!this.menuOutsideClickListener) {
+ this.menuOutsideClickListener = this.renderer.listen('document', 'click', event => {
+ const isOutsideClicked = !(this.appSidebar.el.nativeElement.isSameNode(event.target) || this.appSidebar.el.nativeElement.contains(event.target)
+ || this.appTopBar.menuButton.nativeElement.isSameNode(event.target) || this.appTopBar.menuButton.nativeElement.contains(event.target));
+
+ if (isOutsideClicked) {
+ this.hideMenu();
+ }
+ });
+ }
+
+ if (!this.profileMenuOutsideClickListener) {
+ this.profileMenuOutsideClickListener = this.renderer.listen('document', 'click', event => {
+ const isOutsideClicked = !(this.appTopBar.menu.nativeElement.isSameNode(event.target) || this.appTopBar.menu.nativeElement.contains(event.target)
+ || this.appTopBar.topbarMenuButton.nativeElement.isSameNode(event.target) || this.appTopBar.topbarMenuButton.nativeElement.contains(event.target));
+
+ if (isOutsideClicked) {
+ }
+ });
+ }
+
+ if (this.layoutService.layoutState().staticMenuMobileActive) {
+ this.blockBodyScroll();
+ }
+ });
+
+ this.router.events.pipe(filter(event => event instanceof NavigationEnd))
+ .subscribe(() => {
+ this.hideMenu();
+ });
+ }
+
+ hideMenu() {
+ this.layoutService.layoutState.update((prev) => ({...prev, overlayMenuActive:false, staticMenuMobileActive: false, menuHoverActive: false}))
+ if (this.menuOutsideClickListener) {
+ this.menuOutsideClickListener();
+ this.menuOutsideClickListener = null;
+ }
+ this.unblockBodyScroll();
+ }
+
+ blockBodyScroll(): void {
+ if (document.body.classList) {
+ document.body.classList.add('blocked-scroll');
+ }
+ else {
+ document.body.className += ' blocked-scroll';
+ }
+ }
+
+ unblockBodyScroll(): void {
+ if (document.body.classList) {
+ document.body.classList.remove('blocked-scroll');
+ }
+ else {
+ document.body.className = document.body.className.replace(new RegExp('(^|\\b)' +
+ 'blocked-scroll'.split(' ').join('|') + '(\\b|$)', 'gi'), ' ');
+ }
+ }
+
+ get containerClass() {
+ return {
+ 'layout-overlay': this.layoutService.layoutConfig().menuMode === 'overlay',
+ 'layout-static': this.layoutService.layoutConfig().menuMode === 'static',
+ 'layout-static-inactive': this.layoutService.layoutState().staticMenuDesktopInactive && this.layoutService.layoutConfig().menuMode === 'static',
+ 'layout-overlay-active': this.layoutService.layoutState().overlayMenuActive,
+ 'layout-mobile-active': this.layoutService.layoutState().staticMenuMobileActive
+ }
+ }
+
+ ngOnDestroy() {
+ if (this.overlayMenuOpenSubscription) {
+ this.overlayMenuOpenSubscription.unsubscribe();
+ }
+
+ if (this.menuOutsideClickListener) {
+ this.menuOutsideClickListener();
+ }
+ }
+}
diff --git a/src/layout/appmenu.ts b/src/layout/appmenu.ts
index 660b04b..2362729 100644
--- a/src/layout/appmenu.ts
+++ b/src/layout/appmenu.ts
@@ -1,9 +1,9 @@
import { Component, inject } from '@angular/core';
-import { AppConfigService } from '@/src/service/appconfigservice';
import { CommonModule } from '@angular/common';
import { AppMenuItem } from '@/src/layout/appmenuitem';
@Component({
+ selector: 'app-menu',
standalone:true,
imports: [
CommonModule,
@@ -21,8 +21,6 @@ export class AppMenu {
model: any[] = [];
- configService = inject(AppConfigService);
-
ngOnInit() {
this.model = [
{
diff --git a/src/layout/appmenuitem.ts b/src/layout/appmenuitem.ts
index f5f3fda..2b83feb 100644
--- a/src/layout/appmenuitem.ts
+++ b/src/layout/appmenuitem.ts
@@ -1,9 +1,8 @@
-import { ChangeDetectorRef, Component, Host, HostBinding, Input } from '@angular/core';
+import { Component, HostBinding, Input } from '@angular/core';
import { NavigationEnd, Router, RouterModule } from '@angular/router';
import { animate, state, style, transition, trigger } from '@angular/animations';
import { Subscription } from 'rxjs';
import { filter } from 'rxjs/operators';
-import { AppConfigService } from '@/src/service/appconfigservice';
import { MenuService } from '@/src/app/layout/app.menu.service';
import { CommonModule } from '@angular/common';
import { RippleModule } from 'primeng/ripple';
@@ -53,7 +52,7 @@ import { MenuItem} from 'primeng/api';
})
export class AppMenuItem {
- @Input() item: MenuItem;
+ @Input() item!: MenuItem;
@Input() index!: number;
@@ -69,7 +68,7 @@ export class AppMenuItem {
key: string = "";
- constructor(public configService: AppConfigService , private cd: ChangeDetectorRef, public router: Router, private menuService: MenuService) {
+ constructor(public router: Router, private menuService: MenuService) {
this.menuSourceSubscription = this.menuService.menuSource$.subscribe(value => {
Promise.resolve(null).then(() => {
if (value.routeEvent) {
diff --git a/src/layout/appsidebar.ts b/src/layout/appsidebar.ts
new file mode 100644
index 0000000..aae5357
--- /dev/null
+++ b/src/layout/appsidebar.ts
@@ -0,0 +1,17 @@
+import { Component, ElementRef } from '@angular/core';
+import { AppMenu } from '@/src/layout/appmenu';
+
+@Component({
+ selector: 'app-sidebar',
+ standalone:true,
+ imports: [
+ AppMenu
+ ],
+ template: `
+ `,
+})
+export class AppSidebar {
+ constructor(public el: ElementRef) {}
+}
diff --git a/src/layout/apptopbar.ts b/src/layout/apptopbar.ts
new file mode 100644
index 0000000..eebf44f
--- /dev/null
+++ b/src/layout/apptopbar.ts
@@ -0,0 +1,118 @@
+import { Component, ElementRef, ViewChild } from '@angular/core';
+import { MenuItem } from 'primeng/api';
+import { RouterModule } from '@angular/router';
+import { CommonModule } from '@angular/common';
+import { StyleClassModule } from 'primeng/styleclass';
+import { AppConfigurator } from '@/src/layout/appconfigurator';
+import { LayoutService } from '@/src/service/applayoutservice';
+
+@Component({
+ selector: 'app-topbar',
+ standalone:true,
+ imports: [
+ RouterModule,
+ CommonModule,
+ StyleClassModule,
+ AppConfigurator
+ ],
+ template: `
+
`,
+})
+export class AppTopBar {
+
+ items!: MenuItem[];
+
+ @ViewChild('menubutton') menuButton!: ElementRef;
+
+ @ViewChild('topbarmenubutton') topbarMenuButton!: ElementRef;
+
+ @ViewChild('topbarmenu') menu!: ElementRef;
+
+ constructor(public layoutService: LayoutService) { }
+
+ toggleDarkMode(){
+ this.layoutService.toggleDarkMode()
+ }
+
+}
diff --git a/src/service/appconfigservice.ts b/src/service/appconfigservice.ts
deleted file mode 100644
index d299a69..0000000
--- a/src/service/appconfigservice.ts
+++ /dev/null
@@ -1,135 +0,0 @@
-import { DOCUMENT, isPlatformBrowser } from '@angular/common';
-import { computed, effect, inject, Injectable, PLATFORM_ID, signal } from '@angular/core';
-
-interface AppState {
- preset?: string;
- primary?: string;
- surface?: string;
- darkTheme?: boolean;
- menuActive?: boolean;
- designerKey?: string;
- RTL?: boolean;
- overlayMenuActive?: boolean;
- menuMode?: string;
- staticMenuDesktopInactive?: boolean;
- staticMenuMobileActive?: boolean;
- profileSidebarVisible?: boolean;
- configSidebarVisible?: boolean;
- menuHoverActive?: boolean;
- activeMenuItem?: boolean;
-}
-
-
-@Injectable({
- providedIn: 'root'
-})
-export class AppConfigService {
- private readonly STORAGE_KEY = 'appConfigState';
-
- appState = signal
(null);
-
- document = inject(DOCUMENT);
-
- platformId = inject(PLATFORM_ID);
-
- theme = computed(() => (this.appState()?.darkTheme ? 'dark' : 'light'));
-
- transitionComplete = signal(false);
-
- private initialized = false;
-
- constructor() {
- effect(() => {
- this.appState.set({ ...this.loadAppState() });
- const state = this.appState();
-
- if (!this.initialized || !state) {
- this.initialized = true;
- return;
- }
-
- this.saveAppState(state);
- this.handleDarkModeTransition(state);
- });
- }
-
- private handleDarkModeTransition(state: AppState): void {
- if (isPlatformBrowser(this.platformId)) {
- if ((document as any).startViewTransition) {
- this.startViewTransition(state);
- } else {
- this.toggleDarkMode(state);
- this.onTransitionEnd();
- }
- }
- }
-
- private startViewTransition(state: AppState): void {
- const transition = (document as any).startViewTransition(() => {
- this.toggleDarkMode(state);
- });
-
- transition.ready.then(() => this.onTransitionEnd());
- }
-
- private toggleDarkMode(state: AppState): void {
- if (state.darkTheme) {
- this.document.documentElement.classList.add('app-dark');
- } else {
- this.document.documentElement.classList.remove('app-dark');
- }
- }
-
- private toggleMenu () {
- const {menuMode, overlayMenuActive, staticMenuDesktopInactive, staticMenuMobileActive} = this.appState();
-
- if (menuMode === 'overlay') {
- this.appState.update((prev) => ({...prev, overlayMenuActive: !overlayMenuActive}));
- }
-
- if (window.innerWidth > 991) {
- this.appState.update((prev) => ({...prev, staticMenuDesktopInactive: !staticMenuDesktopInactive}));
- } else {
- this.appState.update((prev) => ({...prev, staticMenuMobileActive: !staticMenuMobileActive}));
- }
- };
-
- isSidebarActive = computed(() => this.appState().overlayMenuActive || this.appState().staticMenuMobileActive);
-
- isDarkTheme = computed(() => this.appState().darkTheme);
-
- getPrimary = computed(() => this.appState().primary);
-
- getSurface = computed(() => this.appState().surface);
-
- private onTransitionEnd() {
- this.transitionComplete.set(true);
- setTimeout(() => {
- this.transitionComplete.set(false);
- });
- }
-
- private loadAppState(): any {
- if (isPlatformBrowser(this.platformId)) {
- const storedState = localStorage.getItem(this.STORAGE_KEY);
- if (storedState) {
- return JSON.parse(storedState);
- }
- }
- return {
- preset: 'Aura',
- primary: 'noir',
- surface: null,
- darkTheme: false,
- menuActive: false,
- designerKey: 'primeng-designer-theme',
- RTL: false
- };
- }
-
- private saveAppState(state: any): void {
- if (isPlatformBrowser(this.platformId)) {
- localStorage.setItem(this.STORAGE_KEY, JSON.stringify(state));
- }
- }
-}
diff --git a/src/service/applayoutservice.ts b/src/service/applayoutservice.ts
new file mode 100644
index 0000000..64238fc
--- /dev/null
+++ b/src/service/applayoutservice.ts
@@ -0,0 +1,154 @@
+import { Injectable, effect, signal, computed } from '@angular/core';
+import { Subject } from 'rxjs';
+
+export interface layoutConfig {
+ preset?: string,
+ primary?: string;
+ surface?: string;
+ darkTheme?: boolean;
+ menuMode?: string;
+}
+
+interface LayoutState {
+ staticMenuDesktopInactive?: boolean;
+ overlayMenuActive?: boolean;
+ configSidebarVisible?: boolean;
+ staticMenuMobileActive?: boolean;
+ menuHoverActive?: boolean;
+}
+
+@Injectable({
+ providedIn: 'root',
+})
+export class LayoutService {
+ _config: layoutConfig = {
+ preset: 'Aura',
+ primary: 'emerald',
+ surface: null,
+ darkTheme: false,
+ menuMode: 'static'
+ };
+
+ _state: LayoutState = {
+ staticMenuDesktopInactive: false,
+ overlayMenuActive: false,
+ configSidebarVisible: false,
+ staticMenuMobileActive: false,
+ menuHoverActive: false,
+ };
+
+ layoutConfig = signal(this._config);
+
+ layoutState = signal(this._state)
+
+ private configUpdate = new Subject();
+
+ private overlayOpen = new Subject();
+
+ configUpdate$ = this.configUpdate.asObservable();
+
+ overlayOpen$ = this.overlayOpen.asObservable();
+
+ theme = computed(() => this.layoutConfig()?.darkTheme ? 'light' : 'dark');
+
+ isSidebarActive = computed(() => this.layoutState().overlayMenuActive || this.layoutState().staticMenuMobileActive);
+
+ isDarkTheme = computed(() => this.layoutConfig().darkTheme);
+
+ getPrimary = computed(() => this.layoutConfig().primary);
+
+ getSurface = computed(() => this.layoutConfig().surface);
+
+ isOverlay = computed(() => this.layoutConfig().menuMode === 'overlay')
+
+ transitionComplete = signal(false);
+
+ private initialized = false;
+
+ constructor() {
+ effect(() => {
+ const config = this.layoutConfig();
+ if(config) {
+ this.onConfigUpdate();
+ }
+ });
+
+ effect(() => {
+ const config = this.layoutConfig();
+
+ if (!this.initialized || !config) {
+ this.initialized = true;
+ return;
+ }
+
+ this.handleDarkModeTransition(config);
+ })
+ }
+
+ private handleDarkModeTransition(config: layoutConfig): void {
+ if ((document as any).startViewTransition) {
+ this.startViewTransition(config);
+ } else {
+ this.toggleDarkMode(config);
+ this.onTransitionEnd();
+ }
+ }
+
+ private startViewTransition(config: layoutConfig): void {
+ const transition = (document as any).startViewTransition(() => {
+ this.toggleDarkMode(config);
+ });
+
+ transition.ready.then(() => this.onTransitionEnd());
+ }
+
+ toggleDarkMode(config?: layoutConfig): void {
+ const _config = config || this.layoutConfig()
+ if (_config.darkTheme) {
+ document.documentElement.classList.add('app-dark');
+ } else {
+ document.documentElement.classList.remove('app-dark');
+ }
+ }
+
+ private onTransitionEnd() {
+ this.transitionComplete.set(true);
+ setTimeout(() => {
+ this.transitionComplete.set(false);
+ });
+ }
+
+
+ onMenuToggle() {
+ if (this.isOverlay()) {
+ this.layoutState.update((prev) => ({...prev, overlayMenuActive: !this.layoutState().overlayMenuActive}));
+
+ if (this.layoutState().overlayMenuActive) {
+ this.overlayOpen.next(null);
+ }
+ }
+
+ if (this.isDesktop()) {
+ this.layoutState.update((prev) => ({...prev, staticMenuDesktopInactive: !this.layoutState().staticMenuDesktopInactive}));
+ } else {
+ this.layoutState.update((prev) => ({...prev, staticMenuMobileActive: !this.layoutState().staticMenuMobileActive}));
+
+ if (this.layoutState().staticMenuMobileActive) {
+ this.overlayOpen.next(null);
+ }
+ }
+ }
+
+ isDesktop() {
+ return window.innerWidth > 991;
+ }
+
+ isMobile() {
+ return !this.isDesktop();
+ }
+
+ onConfigUpdate() {
+ this._config = { ...this.layoutConfig() };
+ this.configUpdate.next(this.layoutConfig());
+ }
+}