UNPKG

@angulogic/ng-sidebar

Version:
924 lines (919 loc) 91.2 kB
import * as i0 from '@angular/core'; import { Injectable, Component, HostBinding, Input, Directive, HostListener, NgModule } from '@angular/core'; import * as i1 from '@angular/router'; import { NavigationEnd } from '@angular/router'; import * as i2 from '@angular/common/http'; import { HttpClientModule } from '@angular/common/http'; import * as i2$1 from '@angular/common'; import { CommonModule, NgStyle } from '@angular/common'; import * as i1$1 from '@angular/platform-browser'; /** * Service responsible for managing sidebar state, configurations, and behaviors. * It handles sidebar initialization, resizing, menu interactions, and theme changes. * * @export * @class NgSidebarService */ class NgSidebarService { /** * Initializes the sidebar service and listens for route changes. * * @param {Router} router - Angular Router for detecting navigation events. * @param {HttpClient} http - Angular HttpClient for loading assets (e.g., SVG icons). */ constructor(router, http) { this.router = router; this.http = http; /** * Tracks whether auto-positioning is enabled. * @default false */ this.autoPositionActive = false; /** * Indicates whether the sidebar is currently being resized. * @default false */ this.isResizing = false; router.events.subscribe(route => { if (route instanceof NavigationEnd) { this.sidebarData.sidebarData.forEach(data => { this.updateActiveState(data.data, route.url); }); } }); } /** * Initializes the sidebar configuration with default values if not provided. * * @param {Partial<SidebarModel> & { sidebarData: SidebarData[] }} data - Sidebar data with optional configurations. * @returns {SidebarModel} - The complete SidebarModel with default values applied. */ initilazeSidebarData(data) { // Banner options data.bannerOptions = data.bannerOptions ? { logo: data.bannerOptions.logo ?? 'assets/icons/angular-logo.png', title: data.bannerOptions.title ?? 'Angulogic', onClick: data.bannerOptions.onClick, } : undefined; // User options data.userOptions = data.userOptions ? { avatar: data.userOptions.avatar ?? 'assets/icons/avatar.svg', name: data.userOptions.name, position: data.userOptions.position ?? 'bottom', onClick: data.userOptions.onClick, } : undefined; // Search options data.searchOptions = data.searchOptions ? { placeholder: data.searchOptions.placeholder, caseSensitive: data.searchOptions.caseSensitive ?? false, strategy: data.searchOptions.strategy ?? 'contains', cssClass: data.searchOptions.cssClass, localCompare: data.searchOptions.localCompare ?? 'en', onSearchStart: data.searchOptions.onSearchStart, onSearchEnd: data.searchOptions.onSearchEnd, } : undefined; // Sidebar data initialization data.sidebarData = data.sidebarData.map(item => ({ title: item.title, cssClass: item.cssClass, visible: item.visible ?? true, data: this.initializeMenuData(item.data), })); // Sidebar options initialization data.options = data.options ? { resize: data.options.resize ?? true, expand: data.options.expand ?? true, favorites: data.options.favorites ?? true, favoritesTitle: data.options.favoritesTitle ?? 'Favorites', search: data.options.search ?? true, cssClass: data.options.cssClass ?? '', viewMode: data.options.viewMode ?? 'toggle', theme: data.options.theme ?? 'light', themePicker: data.options.themePicker ?? true, minWidth: data.options.minWidth ?? 300, maxWidth: data.options.maxWidth ?? 500, width: data.options.width ?? 300, themeText: data.options.themeText ?? { light: 'Light', dark: 'Dark' }, autoPosition: data.options.autoPosition ?? true, toggleCollapseIcon: data.options.toggleCollapseIcon ?? 'assets/icons/collapse.svg', toggleExpandIcon: data.options.toggleExpandIcon ?? 'assets/icons/expand.svg', pinIcon: data.options.pinIcon ?? 'assets/icons/pin.svg', unpinIcon: data.options.unpinIcon ?? 'assets/icons/unpin.svg', closeIcon: data.options.closeIcon ?? 'assets/icons/cancel.svg', pinned: data.options.pinned ?? (data.options.expand && data.options.viewMode === 'hover') ?? false, onThemeChange: data.options.onThemeChange, onResizeStart: data.options.onResizeStart, onResizing: data.options.onResizing, onResizeEnd: data.options.onResizeEnd, onExpand: data.options.onExpand, onCollapse: data.options.onCollapse, onMenuNodeClick: data.options.onMenuNodeClick, } : { resize: true, expand: true, favorites: true, search: true, viewMode: 'toggle', theme: 'light', }; this.sidebarData = data; return data; } /** * Enables automatic positioning of the sidebar. * It observes the sidebar's width and updates the CSS variable `--sidebar-width` dynamically. */ setAutoPosition() { const divElement = document.getElementById('ng-sidebar'); if (!divElement) { return; } if (!document.body.classList.contains('auto-position')) { document.body.classList.add('auto-position'); } const duration = 300; this.observer = new MutationObserver(() => { const width = divElement.offsetWidth; this.updateWidth(divElement, performance.now(), duration); document.documentElement.style.setProperty('--sidebar-width', `${width}px`); }); this.observer.observe(divElement, { attributeFilter: ['style'], }); this.autoPositionActive = true; console.log('Auto position enabled'); } /** * Animates the width update of the sidebar. * * @param {HTMLElement} divElement - The sidebar element whose width is being updated. * @param {number} startTime - The start time of the animation. * @param {number} duration - The duration of the width animation in milliseconds. */ updateWidth(divElement, startTime, duration) { const animateWidth = (timestamp) => { let progress = (timestamp - startTime) / duration; const currentWidth = divElement.offsetWidth; document.documentElement.style.setProperty('--sidebar-width', `${currentWidth}px`); if (progress < 1) { requestAnimationFrame(animateWidth); } }; requestAnimationFrame(animateWidth); } /** * Disables automatic positioning of the sidebar. * Stops observing style changes and resets CSS modifications. */ destroyAutoPosition() { this.observer.disconnect(); if (document.body.classList.contains('auto-position')) { document.body.classList.remove('auto-position'); } this.autoPositionActive = false; console.log('Auto position disabled'); } /** * Handles sidebar resizing using mouse events. * * @param {SidebarModel} sidebarData - The sidebar configuration object. */ resize(sidebarData) { this.isResizing = true; const initialPin = sidebarData.options.pinned; sidebarData.options.pinned = true; const startEvent = { cancel: false, sidebarOptions: sidebarData, }; if (sidebarData.options.onResizeStart) { sidebarData.options.onResizeStart(startEvent); if (startEvent.cancel) { this.isResizing = false; return; } } document.body.classList.add('no-select'); /** * Handles mouse movement during resizing. * * @param {MouseEvent} e - The mouse movement event. */ const mouseMoveListener = (e) => { const resizeEvent = { cancel: false, sidebarOptions: sidebarData, mouseEvent: e, }; if (sidebarData.options.onResizing) { sidebarData.options.onResizing(resizeEvent); if (resizeEvent.cancel) { return; } } if (sidebarData.options.minWidth && e.clientX < sidebarData.options.minWidth) { sidebarData.options.width = sidebarData.options.minWidth; } else if (sidebarData.options.maxWidth && e.clientX > sidebarData.options.maxWidth) { sidebarData.options.width = sidebarData.options.maxWidth; } else { sidebarData.options.width = e.clientX; } }; document.addEventListener('mousemove', mouseMoveListener); /** * Handles mouse release after resizing. * * @param {MouseEvent} e - The mouse up event. */ const mouseUpListener = (e) => { document.body.classList.remove('no-select'); document.removeEventListener('mousemove', mouseMoveListener); document.removeEventListener('mouseup', mouseUpListener); this.isResizing = false; sidebarData.options.pinned = initialPin; const endEvent = { sidebarOptions: sidebarData, mouseEvent: e, }; if (sidebarData.options.onResizeEnd) { sidebarData.options.onResizeEnd(endEvent); } }; document.addEventListener('mouseup', mouseUpListener); } /** * Initializes menu data by assigning default values if not provided. * * @param {MenuData[]} menuData - The menu data array to initialize. * @returns {MenuData[]} - The processed menu data with default values applied. */ initializeMenuData(menuData) { return menuData.map(item => { item['name'] = item['name']; item['icon'] = item['icon']; item['route'] = item['route']; item['visible'] = item['visible'] ?? true; item['disabled'] = item['disabled'] ?? false; item['isExpanded'] = item['isExpanded'] ?? false; item['isFavorited'] = item['isFavorited'] ?? false; item['cssClass'] = item['cssClass']; item['active'] = item['active'] ?? false; item['children'] = item['children'] ? this.initializeMenuData(item['children']) : undefined; item['onClick'] = item['onClick']; return item; }); } /** * Searches for menu items by name within the sidebar data. * * @param {SidebarModel} data - The sidebar configuration containing menu data. * @param {string} searchValue - The search query entered by the user. * @returns {MenuData[]} - An array of matching menu items. */ searchByName(data, searchValue) { const { searchOptions, sidebarData } = data; /** * Compares two strings based on the configured search options. * * @param {string} source - The original menu item name. * @param {string} target - The search query entered by the user. * @returns {boolean} - True if the search criteria match, false otherwise. */ const compareStrings = (source, target) => { if (!searchOptions?.caseSensitive) { source = source.toLocaleLowerCase(searchOptions?.localCompare); target = target.toLocaleLowerCase(searchOptions?.localCompare); } switch (searchOptions?.strategy) { case 'contains': return source.includes(target); case 'startsWith': return source.startsWith(target); case 'endsWith': return source.endsWith(target); case 'equal': return source === target; default: return false; } }; /** * Recursively searches menu items and their children. * * @param {MenuData[]} data - The menu items to search in. * @returns {MenuData[]} - The filtered menu items. */ const searchInMenuData = (data) => { const resultSet = new Set(); data.forEach(item => { if (compareStrings(item.name, searchValue)) { resultSet.add(item); } if (item.children && item.children.length > 0) { const childResults = searchInMenuData(item.children); if (childResults.length > 0) { item.children = childResults; resultSet.add(item); } } }); return Array.from(resultSet); }; return sidebarData.flatMap(sidebarItem => searchInMenuData(sidebarItem.data)); } /** * Updates the active state of menu items based on the current route. * * @param {MenuData[]} menuData - The menu data array to update. * @param {string} currentRoute - The current active route. */ updateActiveState(menuData, currentRoute) { menuData.forEach(item => { if (item.route?.startsWith('/')) { item.active = item.route === currentRoute; } else { item.active = `/${item.route}` === currentRoute; } if (item.children) { this.updateActiveState(item.children, currentRoute); } }); } /** * Toggles the sidebar theme between 'light' and 'dark'. * Updates the `theme` property in `sidebarData.options`. */ changeTheme() { if (this.sidebarData.options.theme === 'light') { this.sidebarData.options.theme = 'dark'; } else { this.sidebarData.options.theme = 'light'; } // Trigger theme change event if provided if (this.sidebarData.options.onThemeChange) { this.sidebarData.options.onThemeChange(this.sidebarData.options.theme); } } /** * Loads an SVG file from the given path. * * @param {string} path - The file path of the SVG to load. * @returns {Promise<string>} - A promise resolving to the SVG content. */ loadSvg(path) { return new Promise((resolve, reject) => { this.http.get(path, { responseType: 'text' }).subscribe({ next: svgContent => resolve(svgContent), error: err => reject(new Error(`Failed to load SVG: ${path}. Error: ${err.message}`)), }); }); } /** * Toggles the sidebar's expanded or collapsed state. * Triggers the appropriate expand/collapse event if provided. */ async toggleSidebar() { let event = { cancel: false, click: true, }; // Trigger collapse event if sidebar is currently expanded if (this.sidebarData.options.expand) { await Promise.resolve(this.sidebarData.options.onCollapse?.(event)); } // Trigger expand event if sidebar is currently collapsed else { await Promise.resolve(this.sidebarData.options.onExpand?.(event)); } // If the event was canceled, do not toggle if (event.cancel) return; // Toggle expand/collapse state this.sidebarData.options.expand = !this.sidebarData.options.expand; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: NgSidebarService, deps: [{ token: i1.Router }, { token: i2.HttpClient }], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: NgSidebarService, providedIn: 'root' }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: NgSidebarService, decorators: [{ type: Injectable, args: [{ providedIn: 'root', }] }], ctorParameters: () => [{ type: i1.Router }, { type: i2.HttpClient }] }); /** * A component that dynamically loads and displays SVG icons or image sources. * If an SVG file is provided, it fetches and sanitizes it to prevent security risks. * Otherwise, it renders an image element with the given source. * * @export * @class AlIconComponent */ class AlIconComponent { /** * Sets the source of the icon. If an SVG file is detected, it is loaded and sanitized. * Otherwise, an image element is created. * * @param {string | undefined} content - The source URL or inline SVG content. */ set icon(content) { if (!content) return; // Load and sanitize SVG content if (content.endsWith('.svg')) { this.sidebarService .loadSvg(content) .then(svg => { this.safeSvg = this.sanitizer.bypassSecurityTrustHtml(svg); }) .catch(error => { console.error('Error loading SVG:', error); this.safeSvg = undefined; }); } else { // Render as an <img> tag this.safeSvg = this.sanitizer.bypassSecurityTrustHtml(`<img src="${content}" />`); } } /** * Creates an instance of `AlIconComponent`. * * @param {DomSanitizer} sanitizer - Angular's sanitizer service to bypass security restrictions. * @param {NgSidebarService} sidebarService - The sidebar service used to fetch SVG content. */ constructor(sanitizer, sidebarService) { this.sanitizer = sanitizer; this.sidebarService = sidebarService; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: AlIconComponent, deps: [{ token: i1$1.DomSanitizer }, { token: NgSidebarService }], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.13", type: AlIconComponent, selector: "al-icon", inputs: { icon: "icon" }, host: { properties: { "innerHTML": "this.safeSvg" }, styleAttribute: "display: flex;", classAttribute: "al-icon" }, ngImport: i0, template: ``, isInline: true }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: AlIconComponent, decorators: [{ type: Component, args: [{ selector: 'al-icon', template: ``, host: { class: 'al-icon', style: 'display: flex;', }, }] }], ctorParameters: () => [{ type: i1$1.DomSanitizer }, { type: NgSidebarService }], propDecorators: { safeSvg: [{ type: HostBinding, args: ['innerHTML'] }], icon: [{ type: Input }] } }); /** * A directive that toggles the sidebar when the attached element is clicked. * * @export * @class TogglerDirective */ class TogglerDirective { /** * Creates an instance of `TogglerDirective`. * * @param {NgSidebarService} sidebarService - The sidebar service that manages sidebar state. */ constructor(sidebarService) { this.sidebarService = sidebarService; } /** * Listens for click events on the host element and toggles the sidebar. */ toggleSidebar() { this.sidebarService.toggleSidebar(); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: TogglerDirective, deps: [{ token: NgSidebarService }], target: i0.ɵɵFactoryTarget.Directive }); } static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "18.2.13", type: TogglerDirective, selector: "[sidebarToggler]", host: { listeners: { "click": "toggleSidebar()" } }, ngImport: i0 }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: TogglerDirective, decorators: [{ type: Directive, args: [{ selector: '[sidebarToggler]', }] }], ctorParameters: () => [{ type: NgSidebarService }], propDecorators: { toggleSidebar: [{ type: HostListener, args: ['click'] }] } }); /** * A component that toggles the theme of the sidebar between 'light' and 'dark'. * * @export * @class ThemeTogglerComponent */ class ThemeTogglerComponent { /** * Creates an instance of `ThemeTogglerComponent`. * * @param {NgSidebarService} sidebarService - The sidebar service used to change the theme. */ constructor(sidebarService) { this.sidebarService = sidebarService; } /** * Toggles the sidebar theme between 'light' and 'dark'. * Calls the `changeTheme()` method from `NgSidebarService`. */ toggleTheme() { this.sidebarService.changeTheme(); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: ThemeTogglerComponent, deps: [{ token: NgSidebarService }], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.13", type: ThemeTogglerComponent, selector: "al-theme-toggler", ngImport: i0, template: "<div class=\"theme-toggler\">\n <svg display=\"none\">\n <symbol id=\"light\" viewBox=\"0 0 24 24\">\n <g stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\">\n <line x1=\"12\" y1=\"17\" x2=\"12\" y2=\"20\" transform=\"rotate(0,12,12)\" />\n <line x1=\"12\" y1=\"17\" x2=\"12\" y2=\"20\" transform=\"rotate(45,12,12)\" />\n <line x1=\"12\" y1=\"17\" x2=\"12\" y2=\"20\" transform=\"rotate(90,12,12)\" />\n <line x1=\"12\" y1=\"17\" x2=\"12\" y2=\"20\" transform=\"rotate(135,12,12)\" />\n <line x1=\"12\" y1=\"17\" x2=\"12\" y2=\"20\" transform=\"rotate(180,12,12)\" />\n <line x1=\"12\" y1=\"17\" x2=\"12\" y2=\"20\" transform=\"rotate(225,12,12)\" />\n <line x1=\"12\" y1=\"17\" x2=\"12\" y2=\"20\" transform=\"rotate(270,12,12)\" />\n <line x1=\"12\" y1=\"17\" x2=\"12\" y2=\"20\" transform=\"rotate(315,12,12)\" />\n </g>\n <circle fill=\"currentColor\" cx=\"12\" cy=\"12\" r=\"5\" />\n </symbol>\n <symbol id=\"dark\" viewBox=\"0 0 24 24\">\n <path\n fill=\"currentColor\"\n d=\"M15.1,14.9c-3-0.5-5.5-3-6-6C8.8,7.1,9.1,5.4,9.9,4c0.4-0.8-0.4-1.7-1.2-1.4C4.6,4,1.8,7.9,2,12.5c0.2,5.1,4.4,9.3,9.5,9.5c4.5,0.2,8.5-2.6,9.9-6.6c0.3-0.8-0.6-1.7-1.4-1.2C18.6,14.9,16.9,15.2,15.1,14.9z\" />\n </symbol>\n </svg>\n <label class=\"switch\">\n <input\n class=\"switch__input\"\n (change)=\"toggleTheme()\"\n type=\"checkbox\"\n role=\"switch\"\n name=\"dark\" />\n <svg class=\"switch__icon\" width=\"24px\" height=\"24px\" aria-hidden=\"true\">\n <use href=\"#light\" />\n </svg>\n <svg class=\"switch__icon\" width=\"24px\" height=\"24px\" aria-hidden=\"true\">\n <use href=\"#dark\" />\n </svg>\n <span class=\"switch__inner\"></span>\n <span class=\"switch__inner-icons\">\n <svg class=\"switch__icon\" width=\"24px\" height=\"24px\" aria-hidden=\"true\">\n <use href=\"#light\" />\n </svg>\n <svg class=\"switch__icon\" width=\"24px\" height=\"24px\" aria-hidden=\"true\">\n <use href=\"#dark\" />\n </svg>\n </span>\n </label>\n</div>\n", styles: ["input{font:1em/1.5 sans-serif}.theme-toggler{font-size:24px;background-color:transparent;color:hsl(var(--hue),10%,10%);display:flex;transition:background-color .3s,color .3s}.theme-toggler:has(.switch__input:checked){background-color:transparent;color:hsl(var(--hue),10%,90%)}.switch,.switch__input{display:block;-webkit-tap-highlight-color:transparent}.switch{margin:auto;position:relative;-webkit-user-select:none;-moz-user-select:none;user-select:none}.switch__icon{color:hsla(var(--hue),10%,80%);pointer-events:none;position:absolute;top:.475em;left:.475em;width:.75em;height:.75em;transition:color .3s,transform .3s cubic-bezier(.65,0,.35,1)}.switch__icon:nth-of-type(2){right:.375em;left:auto}.switch__inner,.switch__inner-icons{border-radius:.5em;display:block;overflow:hidden;position:absolute;top:.375em;left:.375em;width:2.25em;height:1em}.switch__inner:before,.switch__inner-icons{transition:transform .3s cubic-bezier(.65,0,.35,1);transform:translate(-1.25em)}.switch__inner:before{background-color:var(--primary);border-radius:inherit;content:\"\";display:block;width:100%;height:100%}.switch__inner-icons{pointer-events:none}.switch__inner-icons .switch__icon{color:#fff;top:.125em;left:.125em;transform:translate(1.25em)}.switch__inner-icons .switch__icon:nth-child(2){right:.125em;left:auto}.switch__input{background-color:#fff;border-radius:calc(var(--radius) * 4);box-shadow:0 0 8px #00000071;outline:transparent;width:2.75em;height:1.5em;-webkit-appearance:none;appearance:none;transition:background-color .3s,box-shadow .3s}.switch__input:checked{background-color:hsl(var(--hue),10%,10%)}.switch__input:checked~.switch__icon{color:hsla(var(--hue),10%,40%)}.switch__input:checked~.switch__inner:before,.switch__input:checked~.switch__inner-icons{transform:translate(1.25em)}.switch__input:not(:checked)~.switch__icon:first-of-type,.switch__input:checked~.switch__icon:nth-of-type(2){transform:rotate(360deg)}.switch__input:checked~.switch__inner-icons .switch__icon:first-of-type{transform:translate(-1.25em) rotate(-360deg)}.switch__input:checked~.switch__inner-icons .switch__icon:nth-of-type(2){transform:translate(-1.25em) rotate(360deg)}.switch__input:focus-visible{box-shadow:0 0 0 .0625em hsla(var(--hue),90%,50%,1),0 .125em .5em hsla(var(--hue),10%,10%,.1)}\n"] }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: ThemeTogglerComponent, decorators: [{ type: Component, args: [{ selector: 'al-theme-toggler', template: "<div class=\"theme-toggler\">\n <svg display=\"none\">\n <symbol id=\"light\" viewBox=\"0 0 24 24\">\n <g stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\">\n <line x1=\"12\" y1=\"17\" x2=\"12\" y2=\"20\" transform=\"rotate(0,12,12)\" />\n <line x1=\"12\" y1=\"17\" x2=\"12\" y2=\"20\" transform=\"rotate(45,12,12)\" />\n <line x1=\"12\" y1=\"17\" x2=\"12\" y2=\"20\" transform=\"rotate(90,12,12)\" />\n <line x1=\"12\" y1=\"17\" x2=\"12\" y2=\"20\" transform=\"rotate(135,12,12)\" />\n <line x1=\"12\" y1=\"17\" x2=\"12\" y2=\"20\" transform=\"rotate(180,12,12)\" />\n <line x1=\"12\" y1=\"17\" x2=\"12\" y2=\"20\" transform=\"rotate(225,12,12)\" />\n <line x1=\"12\" y1=\"17\" x2=\"12\" y2=\"20\" transform=\"rotate(270,12,12)\" />\n <line x1=\"12\" y1=\"17\" x2=\"12\" y2=\"20\" transform=\"rotate(315,12,12)\" />\n </g>\n <circle fill=\"currentColor\" cx=\"12\" cy=\"12\" r=\"5\" />\n </symbol>\n <symbol id=\"dark\" viewBox=\"0 0 24 24\">\n <path\n fill=\"currentColor\"\n d=\"M15.1,14.9c-3-0.5-5.5-3-6-6C8.8,7.1,9.1,5.4,9.9,4c0.4-0.8-0.4-1.7-1.2-1.4C4.6,4,1.8,7.9,2,12.5c0.2,5.1,4.4,9.3,9.5,9.5c4.5,0.2,8.5-2.6,9.9-6.6c0.3-0.8-0.6-1.7-1.4-1.2C18.6,14.9,16.9,15.2,15.1,14.9z\" />\n </symbol>\n </svg>\n <label class=\"switch\">\n <input\n class=\"switch__input\"\n (change)=\"toggleTheme()\"\n type=\"checkbox\"\n role=\"switch\"\n name=\"dark\" />\n <svg class=\"switch__icon\" width=\"24px\" height=\"24px\" aria-hidden=\"true\">\n <use href=\"#light\" />\n </svg>\n <svg class=\"switch__icon\" width=\"24px\" height=\"24px\" aria-hidden=\"true\">\n <use href=\"#dark\" />\n </svg>\n <span class=\"switch__inner\"></span>\n <span class=\"switch__inner-icons\">\n <svg class=\"switch__icon\" width=\"24px\" height=\"24px\" aria-hidden=\"true\">\n <use href=\"#light\" />\n </svg>\n <svg class=\"switch__icon\" width=\"24px\" height=\"24px\" aria-hidden=\"true\">\n <use href=\"#dark\" />\n </svg>\n </span>\n </label>\n</div>\n", styles: ["input{font:1em/1.5 sans-serif}.theme-toggler{font-size:24px;background-color:transparent;color:hsl(var(--hue),10%,10%);display:flex;transition:background-color .3s,color .3s}.theme-toggler:has(.switch__input:checked){background-color:transparent;color:hsl(var(--hue),10%,90%)}.switch,.switch__input{display:block;-webkit-tap-highlight-color:transparent}.switch{margin:auto;position:relative;-webkit-user-select:none;-moz-user-select:none;user-select:none}.switch__icon{color:hsla(var(--hue),10%,80%);pointer-events:none;position:absolute;top:.475em;left:.475em;width:.75em;height:.75em;transition:color .3s,transform .3s cubic-bezier(.65,0,.35,1)}.switch__icon:nth-of-type(2){right:.375em;left:auto}.switch__inner,.switch__inner-icons{border-radius:.5em;display:block;overflow:hidden;position:absolute;top:.375em;left:.375em;width:2.25em;height:1em}.switch__inner:before,.switch__inner-icons{transition:transform .3s cubic-bezier(.65,0,.35,1);transform:translate(-1.25em)}.switch__inner:before{background-color:var(--primary);border-radius:inherit;content:\"\";display:block;width:100%;height:100%}.switch__inner-icons{pointer-events:none}.switch__inner-icons .switch__icon{color:#fff;top:.125em;left:.125em;transform:translate(1.25em)}.switch__inner-icons .switch__icon:nth-child(2){right:.125em;left:auto}.switch__input{background-color:#fff;border-radius:calc(var(--radius) * 4);box-shadow:0 0 8px #00000071;outline:transparent;width:2.75em;height:1.5em;-webkit-appearance:none;appearance:none;transition:background-color .3s,box-shadow .3s}.switch__input:checked{background-color:hsl(var(--hue),10%,10%)}.switch__input:checked~.switch__icon{color:hsla(var(--hue),10%,40%)}.switch__input:checked~.switch__inner:before,.switch__input:checked~.switch__inner-icons{transform:translate(1.25em)}.switch__input:not(:checked)~.switch__icon:first-of-type,.switch__input:checked~.switch__icon:nth-of-type(2){transform:rotate(360deg)}.switch__input:checked~.switch__inner-icons .switch__icon:first-of-type{transform:translate(-1.25em) rotate(-360deg)}.switch__input:checked~.switch__inner-icons .switch__icon:nth-of-type(2){transform:translate(-1.25em) rotate(360deg)}.switch__input:focus-visible{box-shadow:0 0 0 .0625em hsla(var(--hue),90%,50%,1),0 .125em .5em hsla(var(--hue),10%,10%,.1)}\n"] }] }], ctorParameters: () => [{ type: NgSidebarService }] }); /** * A dynamic and interactive sidebar component for Angular applications. * It supports features such as menu navigation, search, favorites, and theme management. * * @export * @class NgSidebarComponent * @implements {DoCheck} * @implements {OnInit} */ class NgSidebarComponent { /** * Sets the sidebar options and initializes the sidebar data. * Ensures the default theme is applied if not provided. * * @param {SidebarModel} val - The sidebar configuration model. */ set options(val) { this.sidebarData = this.ngSidebarService.initilazeSidebarData(val); if (!val.options.theme) { val.options.theme = 'light'; } this.SIDEBAR_DATA = this.deepClone(this.sidebarData); } /** * Applies the `.collapsed` class when the sidebar is collapsed. */ get isCollapsed() { return !this.sidebarData.options.expand; } /** * Applies the `.transition` class when the sidebar is transitioning. */ get isTransition() { return !this.ngSidebarService.isResizing; } /** * Applies the `.al-dark-theme` class when the dark theme is active. */ get isDarkTheme() { return this.sidebarData.options.theme === 'dark'; } /** * Binds the sidebar's width dynamically based on its expanded state. */ get sidebarWidth() { return this.sidebarData.options.expand ? `${this.sidebarData.options.width}px` : this.sidebarData.options.viewMode === 'mobile' ? '0px' : 'var(--collapse-width)'; } /** * Binds the sidebar's max width dynamically. */ get sidebarMaxWidth() { return this.sidebarData.options.expand ? `${this.sidebarData.options.maxWidth}px` : 'unset'; } /** * Binds the sidebar's min width dynamically. */ get sidebarMinWidth() { return this.sidebarData.options.expand ? `${this.sidebarData.options.minWidth}px` : 'unset'; } /** * Creates an instance of `NgSidebarComponent`. * * @param {NgSidebarService} ngSidebarService - The sidebar service for handling state and actions. */ constructor(ngSidebarService) { this.ngSidebarService = ngSidebarService; /** * Stores the list of favorite menu items. */ this.favorites = []; /** * Adds the `.ng-sidebar` class to the component. */ this.ngSidebarClass = true; } /** * Lifecycle hook that runs when the component initializes. * It updates the list of favorite menu items. */ ngOnInit() { this.updateFavorites(); } /** * Lifecycle hook that detects changes and manages auto-positioning. */ ngDoCheck() { if (this.sidebarData.options.autoPosition !== this.ngSidebarService.autoPositionActive) { this.sidebarData.options.autoPosition ? this.ngSidebarService.setAutoPosition() : this.ngSidebarService.destroyAutoPosition(); } } /** * Handles click events on the banner elements (logo or title). * Triggers the `onClick` event if it exists in `bannerOptions`. * * @param {'logo' | 'title'} element - The clicked banner element. */ onBannerClick(element) { this.sidebarData.bannerOptions?.onClick?.(element); } /** * Handles click events on the user profile elements (avatar or name). * Triggers the `onClick` event if it exists in `userOptions`. * * @param {'avatar' | 'name'} element - The clicked user profile element. */ onUserClick(element) { this.sidebarData.userOptions?.onClick?.(element); } /** * Handles search input events and filters the sidebar menu items. * Triggers `onSearchStart` and `onSearchEnd` events if provided. * * @param {KeyboardEvent} event - The keyboard event triggered by the search input. */ async onSearch(event) { const element = event.currentTarget; const searchValue = element.value.trim(); let searchStartEvent = { nativeElement: element, searchValue: searchValue, cancel: false, }; if (this.sidebarData.searchOptions?.onSearchStart) { await Promise.resolve(this.sidebarData.searchOptions.onSearchStart(searchStartEvent)); } if (!searchStartEvent.cancel) { let filteredResults = []; if (searchValue.length > 0) { filteredResults = this.ngSidebarService.searchByName(this.deepClone(this.SIDEBAR_DATA), searchValue); if (filteredResults.length > 0) { this.sidebarData.sidebarData = this.SIDEBAR_DATA.sidebarData.map(sidebarItem => { const matchingItems = filteredResults.filter(item => sidebarItem.data.some(dataItem => dataItem.name === item.name)); const updateExpandedState = (item) => { item.isExpanded = true; if (item.children) { item.children.forEach(updateExpandedState); } }; matchingItems.forEach(updateExpandedState); return { ...sidebarItem, data: [...new Set(matchingItems)], }; }); this.sidebarData.sidebarData = this.sidebarData.sidebarData.filter(d => d.data.length > 0); } else { this.sidebarData.sidebarData = []; } } else { this.sidebarData = this.deepClone(this.SIDEBAR_DATA); this.ngSidebarService.sidebarData = this.sidebarData; } if (this.sidebarData.searchOptions?.onSearchEnd) { let searchEndEvent = { menuData: filteredResults, }; this.sidebarData.searchOptions.onSearchEnd(searchEndEvent); } } } /** * Resets the search input and restores the sidebar menu items. * Triggers `onSearchStart` and `onSearchEnd` events if provided. * * @param {HTMLInputElement} searchInput - The search input element to clear. */ async onCancelSearch(searchInput) { if (searchInput.value === '') return; searchInput.value = ''; let searchStartEvent = { nativeElement: searchInput, searchValue: searchInput.value, cancel: false, }; if (this.sidebarData.searchOptions?.onSearchStart) { await Promise.resolve(this.sidebarData.searchOptions.onSearchStart(searchStartEvent)); } if (!searchStartEvent.cancel) { this.sidebarData = this.deepClone(this.SIDEBAR_DATA); this.ngSidebarService.sidebarData = this.sidebarData; if (this.sidebarData.searchOptions?.onSearchEnd) { let searchEndEvent = { menuData: this.SIDEBAR_DATA.sidebarData, }; this.sidebarData.searchOptions.onSearchEnd(searchEndEvent); } } } /** * Handles click events on menu items. * If the item has children, it toggles the node. * If the item has a route, it navigates to it. * * @param {MenuData} node - The clicked menu item. * @param {MouseEvent} mouseEvent - The mouse event triggering the click. */ async onMenuClick(node, mouseEvent) { let event = { menuData: node, cancel: false, }; await Promise.resolve(node.onClick?.(event)); await Promise.resolve(this.sidebarData.options.onMenuNodeClick?.(event)); if (event.cancel) return; if (node.children && node.children.length > 0) { this.nodeToggle(node, mouseEvent); } else if (node.route) { this.ngSidebarService.router.navigate([node.route]); } } /** * Handles click events on favorite menu items. * Triggers the `onClick` event if it exists for the menu item. * * @param {MenuData & { cancel: boolean }} favorite - The favorite menu item clicked. */ async onFavoriteClick(favorite) { let event = { menuData: favorite, cancel: false, }; await Promise.resolve(favorite.onClick?.(event)); if (event.cancel) return; } /** * Expands the sidebar when the mouse enters, if viewMode is set to "hover". */ onEnter() { if (this.sidebarData.options.viewMode === 'hover' && !this.sidebarData.options.pinned) { this.sidebarData.options.expand = true; } } /** * Collapses the sidebar when the mouse leaves, if viewMode is set to "hover". */ onLeave() { if (this.sidebarData.options.viewMode === 'hover' && !this.sidebarData.options.pinned) { this.sidebarData.options.expand = false; } } /** * Toggles the expansion state of a menu node. * If the node is already expanded, it collapses it with an animation. * * @param {MenuData} node - The menu node to toggle. * @param {MouseEvent} event - The mouse event triggering the toggle. */ async nodeToggle(node, event) { let nodeTogglerClickEvent = { menuData: node, cancel: false, }; await Promise.resolve(node.onToggle?.(nodeTogglerClickEvent)); if (nodeTogglerClickEvent.cancel) return; const nodeElement = event.currentTarget.querySelector('.node-toggler'); const parentNode = event.currentTarget; if (node.isExpanded) { nodeElement.classList.remove('expand'); Array.from(parentNode.parentElement.parentElement.children) .filter(child => child.classList.contains('node')) .forEach(child => child.classList.add('out-left')); setTimeout(() => { node.isExpanded = false; }, 300); } else { setTimeout(() => { Array.from(parentNode.parentElement.parentElement.children) .filter(child => child.classList.contains('node')) .forEach(child => child.classList.add('in-left')); }, 1); node.isExpanded = true; } } /** * Adds or removes a menu item from the favorites list. * * @param {MenuData} node - The menu item to toggle as a favorite. */ onFavoriteNode(node) { if (!this.favorites.some(fav => fav.name === node.name)) { this.favorites.push(node); } else { this.favorites = this.favorites.filter(fav => fav.name !== node.name); } } /** * Checks if a menu item is in the favorites list. * * @param {string} name - The name of the menu item to check. * @returns {boolean} - True if the item is in favorites, false otherwise. */ isOnFav(name) { return this.favorites.some(fav => fav.name === name); } /** * Updates the favorites list by scanning the sidebar menu items. */ updateFavorites() { this.favorites = []; this.sidebarData.sidebarData.forEach((data) => { if (data.data) { this.collectFavorites(data.data); } }); } /** * Recursively collects favorite menu items from a list of nodes. * * @param {MenuData[]} nodes - The list of menu nodes to scan. */ collectFavorites(nodes) { nodes.forEach(node => { if (node.isFavorited) { this.favorites.push(node); } if (node.children && node.children.length > 0) { this.collectFavorites(node.children); } }); } /** * Toggles the pinned state of the sidebar. * When pinned, the sidebar remains expanded regardless of hover interactions. */ togglePin() { this.sidebarData.options.pinned = !this.sidebarData.options.pinned; } /** * Performs a deep clone of an object to prevent reference issues. * * @template T * @param {T} obj - The object to clone. * @returns {T} - A deep copy of the input object. */ deepClone(obj) { if (obj === null || typeof obj !== 'object') { return obj; } if (Array.isArray(obj)) { return obj.map(item => this.deepClone(item)); } const clonedObj = {}; for (const key in obj) { if (obj.hasOwnProperty(key)) { clonedObj[key] = this.deepClone(obj[key]); } } return clonedObj; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: NgSidebarComponent, deps: [{ token: NgSidebarService }], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.13", type: NgSidebarComponent, selector: "ng-sidebar", inputs: { options: "options", nodeContent: "nodeContent" }, host: { listeners: { "mouseenter": "onEnter()", "mouseleave": "onLeave()" }, properties: { "class.ng-sidebar": "this.ngSidebarClass", "class.collapsed": "this.isCollapsed", "class.transition": "this.isTransition", "class.al-dark-theme": "this.isDarkTheme", "style.width": "this.sidebarWidth", "style.maxWidth": "this.sidebarMaxWidth", "style.minWidth": "this.sidebarMinWidth" } }, ngImport: i0, template: "<!-- Sidebar Wrapper -->\r\n<ng-container\r\n *ngIf=\"\r\n sidebarData.options.expand || sidebarData.options.viewMode !== 'mobile'\r\n \">\r\n <!-- Sidebar Resizer -->\r\n <!-- Allows users to resize the sidebar when it is expanded -->\r\n <div\r\n *ngIf=\"sidebarData.options.resize && this.sidebarData.options.expand\"\r\n class=\"resizer\"\r\n (mousedown)=\"ngSidebarService.resize(sidebarData)\"\r\n [ngClass]=\"{ active: ngSidebarService.isResizing }\"></div>\r\n\r\n <!-- Sidebar Banner (Logo & Title) -->\r\n <div class=\"banner\">\r\n <div *ngIf=\"sidebarData.bannerOptions\" class=\"banner-container\">\r\n <!-- Sidebar Logo -->\r\n <al-icon\r\n *ngIf=\"sidebarData.bannerOptions.logo\"\r\n [icon]=\"sidebarData.bannerOptions.logo\"\r\n (click)=\"onBannerClick('logo')\"\r\n class=\"logo\" />\r\n\r\n <!-- Sidebar Title -->\r\n <div\r\n *ngIf=\"sidebarData.bannerOptions.title && sidebarData.options.expand\"\r\n class=\"title text-owerflow\"\r\n (click)=\"onBannerClick('title')\">\r\n {{ sidebarData.bannerOptions.title }}\r\n </div>\r\n </div>\r\n\r\n <!-- Sidebar Toggle Buttons -->\r\n <al-icon\r\n *ngIf=\"sidebarData.options.viewMode === 'toggle'\"\r\n sidebarToggler\r\n class=\"toggle-icon\"\r\n [icon]=\"\r\n sidebarData.options.expand\r\n ? sidebarData.options.toggleCollapseIcon\r\n : sidebarData.options.toggleExpandIcon\r\n \" />\r\n\r\n <al-icon\r\n *ngIf=\"sidebarData.options.viewMode === 'mobile'\"\r\n sidebarToggler\r\n class=\"toggle-icon mobile\"\r\n [icon]=\"sidebarData.options.closeIcon\" />\r\n\r\n <al-icon\r\n *ngIf=\"sidebarData.options.viewMode === 'hover'\"\r\n (click)=\"togglePin()\"\r\n class=\"toggle-icon\"\r\n [icon]=\"\r\n sidebarData.options.pinned\r\n ? sidebarData.options.unpinIcon\r\n : sidebarData.options.pinIcon\r\n \" />\r\n\r\n <ng-content select=\"[al-sidebar-banner]\"></ng-content>\r\n </div>\r\n\r\n <!-- User Profile Section (Top) -->\r\n <ng-content select=\"[al-sidebar-top-user]\"></ng-content>\r\n <ng-container *ngIf=\"sidebarData.userOptions?.position == 'top'\">\r\n <ng-container\r\n *ngTemplateOutlet=\"\r\n userTemplate;\r\n context: { $implicit: sidebarData.userOptions }\r\n \">\r\n </ng-container>\r\n </ng-container>\r\n\r\n <!-- Search Bar -->\r\n <div\r\n class=\"search\"\r\n *ngIf=\"sidebarData.options.search && sidebarData.options.expand\"\r\n [ngClass]=\"sidebarData.searchOptions?.cssClass\">\r\n <!-- Search Icon -->\r\n <al-icon class=\"search-icon\" icon=\"assets/icons/search.svg\" />\r\n\r\n <!-- Search Input -->\r\n <input\r\n #searchInput\r\n (keyup)=\"onSearch($event)\"\r\n [placeholder]=\"sidebarData.searchOptions?.placeholder ?? ''\"\r\n type=\"text\" />\r\n\r\n <!-- Cancel Search Button -->\r\n <al-icon\r\n *ngIf=\"searchInput.value.length > 0\"\r\n (click)=\"onCancelSearch(searchInput)\"\r\n class=\"cancel-icon\"\r\n icon=\"assets/icons/cancel.svg\"\r\n alt />\r\n\r\n <ng-content select=\"[al-sidebar-search]\"></ng-content>\r\n </div>\r\n\r\n <!-- Sidebar Menu Items -->\r\n <div class=\"menu-container\">\r\n <!-- Favorites Section -->\r\n <div\r\n *ngIf=\"favorites.length > 0 && sidebarData.options.expand\"\r\n class=\"wrapper favorites\">\r\n <div class=\"title\">{{ sidebarData.options.favoritesTitle }}</div>\r\n <ng-container\r\n *ngTemplateOutlet=\"dataTemplate; context: { $implicit: favorites }\">\r\n </ng-container>\r\n </div>\r\n\r\n <!-- Sidebar Sections & Items -->\r\n <div\r\n [class]=\"'wrapper ' + data.cssClass\"\r\n [ngClass]=\"{\r\n collapsed:\r\n !sidebarData.options.expand &&\r\n sidebarData.options.viewMode !== 'mobile',\r\n }\"\r\n *ngFor=\"let data of sidebarData.sidebarData\">\r\n <ng-container *ngIf=\"data.visible\">\r\n <div *ngIf=\"sidebarData.options.expand\" class=\"title\">\r\n {{ data