@angulogic/ng-sidebar
Version:
angular sidebar
924 lines (919 loc) • 91.2 kB
JavaScript
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