/** * @license * Copyright Akveo. All Rights Reserved. * Licensed under the MIT License. See License.txt in the project root for license information. */ import { Component, HostBinding, Input, OnInit, OnDestroy, ElementRef } from '@angular/core'; import { Subscription } from 'rxjs/Subscription'; import { convertToBoolProperty } from '../helpers'; import { NbThemeService } from '../../services/theme.service'; import { NbMediaBreakpoint } from '../../services/breakpoints.service'; import { NbSidebarService } from './sidebar.service'; /** * Sidebar header container. * * Placeholder which contains a sidebar header content, * placed at the very top of the sidebar outside of the scroll area. */ @Component({ selector: 'nb-sidebar-header', template: ` `, }) export class NbSidebarHeaderComponent { } /** * Sidebar footer container. * * Placeholder which contains a sidebar footer content, * placed at the very bottom of the sidebar outside of the scroll area. */ @Component({ selector: 'nb-sidebar-footer', template: ` `, }) export class NbSidebarFooterComponent { } /** * Layout sidebar component. * * Sidebar can be place on the left or the right side of the layout, can be fixed (shown above the content) * or can push the layout when opened. * * There are three states - `expanded`, `collapsed`, `compacted`. * By default sidebar content is fixed and saves its position while the page is being scrolled. * * Sidebar also supports a `responsive` behavior, listening to window size change and changing its size respectably. * * @example Minimal sidebar example * ``` * * Sidebar content. * * ``` * * @example Example of fixed sidebar located on the left side, initially collapsed. * * ``` * * Header * * Menu or another component here * * * Footer components here * * * ``` * * @styles * * sidebar-font-size: Sidebar content font size * sidebar-line-height: Sidebar content line height * sidebar-fg: Foreground color * sidebar-bg: Background color * sidebar-height: Content height * sidebar-width: Expanded width * sidebar-width-compact: Compacted width * sidebar-padding: Sidebar content padding * sidebar-header-height: Sidebar header height * sidebar-footer-height: Sidebar footer height * sidebar-shadow: Sidebar container shadow * */ @Component({ selector: 'nb-sidebar', styleUrls: ['./sidebar.component.scss'], template: `
`, }) export class NbSidebarComponent implements OnInit, OnDestroy { static readonly STATE_EXPANDED: string = 'expanded'; static readonly STATE_COLLAPSED: string = 'collapsed'; static readonly STATE_COMPACTED: string = 'compacted'; static readonly RESPONSIVE_STATE_MOBILE: string = 'mobile'; static readonly RESPONSIVE_STATE_TABLET: string = 'tablet'; static readonly RESPONSIVE_STATE_PC: string = 'pc'; protected stateValue: string; protected responsiveValue: boolean = false; @HostBinding('class.fixed') fixedValue: boolean = false; @HostBinding('class.right') rightValue: boolean = false; @HostBinding('class.left') leftValue: boolean = true; // TODO: rename stateValue to state (take a look to the card component) @HostBinding('class.expanded') get expanded() { return this.stateValue === NbSidebarComponent.STATE_EXPANDED; } @HostBinding('class.collapsed') get collapsed() { return this.stateValue === NbSidebarComponent.STATE_COLLAPSED; } @HostBinding('class.compacted') get compacted() { return this.stateValue === NbSidebarComponent.STATE_COMPACTED; } /** * Places sidebar on the left side * @type {boolean} */ @Input() set right(val: boolean) { this.rightValue = convertToBoolProperty(val); this.leftValue = !this.rightValue; } /** * Places sidebar on the right side * @type {boolean} */ @Input() set left(val: boolean) { this.leftValue = convertToBoolProperty(val); this.rightValue = !this.leftValue; } /** * Makes sidebar fixed (shown above the layout content) * @type {boolean} */ @Input() set fixed(val: boolean) { this.fixedValue = convertToBoolProperty(val); } /** * Initial sidebar state, `expanded`|`collapsed`|`compacted` * @type {string} */ @Input() set state(val: string) { this.stateValue = val; } /** * Makes sidebar listen to media query events and change its behaviour * @type {boolean} */ @Input() set responsive(val: boolean) { this.responsiveValue = convertToBoolProperty(val); this.toggleResponsive(this.responsiveValue); } /** * Tags a sidebar with some ID, can be later used in the sidebar service * to determine which sidebar triggered the action, if multiple sidebars exist on the page. * * @type {string} */ @Input() tag: string; private toggleSubscription: Subscription; private expandSubscription: Subscription; private collapseSubscription: Subscription; private mediaQuerySubscription: Subscription; private responsiveState = NbSidebarComponent.RESPONSIVE_STATE_PC; constructor(private sidebarService: NbSidebarService, private themeService: NbThemeService, private element: ElementRef) { } toggleResponsive(enabled: boolean) { if (enabled) { this.mediaQuerySubscription = this.onMediaQueryChanges(); } else if (this.mediaQuerySubscription) { this.mediaQuerySubscription.unsubscribe(); } } ngOnInit() { this.toggleSubscription = this.sidebarService.onToggle() .subscribe((data: { compact: boolean, tag: string }) => { if (!this.tag || this.tag === data.tag) { this.toggle(data.compact); } }); this.expandSubscription = this.sidebarService.onExpand() .subscribe((data: { tag: string }) => { if (!this.tag || this.tag === data.tag) { this.expand(); } }); this.collapseSubscription = this.sidebarService.onCollapse() .subscribe((data: { tag: string }) => { if (!this.tag || this.tag === data.tag) { this.collapse(); } }); } ngOnDestroy() { this.toggleSubscription.unsubscribe(); this.expandSubscription.unsubscribe(); this.collapseSubscription.unsubscribe(); if (this.mediaQuerySubscription) { this.mediaQuerySubscription.unsubscribe(); } } onClick(event): void { const menu = this.element.nativeElement.querySelector('nb-menu'); if (menu && menu.contains(event.target)) { this.expand(); } } /** * Collapses the sidebar */ collapse() { this.state = NbSidebarComponent.STATE_COLLAPSED; } /** * Expands the sidebar */ expand() { this.state = NbSidebarComponent.STATE_EXPANDED; } /** * Compacts the sidebar (minimizes) */ compact() { this.state = NbSidebarComponent.STATE_COMPACTED; } /** * Toggles sidebar state (expanded|collapsed|compacted) * @param {boolean} compact If true, then sidebar state will be changed between expanded & compacted, * otherwise - between expanded & collapsed. False by default. * * @example Toggle sidebar state * * ``` * this.sidebar.toggle(true); * ``` */ toggle(compact: boolean = false) { if (this.responsiveEnabled()) { if (this.responsiveState === NbSidebarComponent.RESPONSIVE_STATE_MOBILE) { compact = false; } } const closedStates = [NbSidebarComponent.STATE_COMPACTED, NbSidebarComponent.STATE_COLLAPSED]; if (compact) { this.state = closedStates.indexOf(this.stateValue) >= 0 ? NbSidebarComponent.STATE_EXPANDED : NbSidebarComponent.STATE_COMPACTED; } else { this.state = closedStates.indexOf(this.stateValue) >= 0 ? NbSidebarComponent.STATE_EXPANDED : NbSidebarComponent.STATE_COLLAPSED; } } protected onMediaQueryChanges(): Subscription { return this.themeService.onMediaQueryChange() .subscribe(([prev, current]: [NbMediaBreakpoint, NbMediaBreakpoint]) => { // TODO: get width by the key and define only max width for the tablets and mobiles const tablet = ['xs', 'is', 'sm', 'md', 'lg']; const mobile = ['xs', 'is']; if (tablet.indexOf(current.name) !== -1) { this.fixed = true; this.compact(); this.responsiveState = NbSidebarComponent.RESPONSIVE_STATE_TABLET; } if (mobile.indexOf(current.name) !== -1) { this.collapse(); this.responsiveState = NbSidebarComponent.RESPONSIVE_STATE_MOBILE; } if (tablet.indexOf(current.name) === -1 && prev.width < current.width) { this.expand(); this.fixed = false; this.responsiveState = NbSidebarComponent.RESPONSIVE_STATE_PC; } }); } protected responsiveEnabled(): boolean { return this.responsiveValue; } }