/**
* @license
* Copyright Akveo. All Rights Reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*/
import {
AfterViewInit, Component, ComponentFactoryResolver, ElementRef, HostBinding, HostListener, Input, OnDestroy,
Renderer2, ViewChild, ViewContainerRef, OnInit,
} from '@angular/core';
import { Router, NavigationEnd } from '@angular/router';
import { Subject } from 'rxjs/Subject';
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
import 'rxjs/add/operator/toPromise';
import 'rxjs/add/operator/filter';
import 'rxjs/add/operator/takeWhile';
import { convertToBoolProperty } from '../helpers';
import { NbThemeService } from '../../services/theme.service';
import { NbSpinnerService } from '../../services/spinner.service';
/**
* A container component which determines a content position inside of the layout.
* The layout could contain unlimited columns (not including the sidebars).
*
* @example By default the columns are ordered from the left to the right,
* but it's also possible to overwrite this behavior by setting a `left` attribute to the column,
* moving it to the very first position:
* ```
*
* Second
* Third
* First
*
* ```
*/
@Component({
selector: 'nb-layout-column',
template: `
`,
})
export class NbLayoutColumnComponent {
@HostBinding('class.left') leftValue: boolean;
/**
* Move the column to the very left position in the layout.
* @param {boolean} val
*/
@Input()
set left(val: boolean) {
this.leftValue = convertToBoolProperty(val);
}
}
/**
* Page header component.
* Located on top of the page above the layout columns and sidebars.
* Could be made `fixed` by setting the corresponding property. In the fixed mode the header becomes
* sticky to the top of the nb-layout (to of the page).
*
* @styles
*
* header-font-family
* header-line-height
* header-fg
* header-bg
* header-height
* header-padding
* header-shadow
*/
@Component({
selector: 'nb-layout-header',
template: `
`,
})
export class NbLayoutHeaderComponent {
@HostBinding('class.fixed') fixedValue: boolean;
/**
* Makes the header sticky to the top of the nb-layout.
* @param {boolean} val
*/
@Input()
set fixed(val: boolean) {
this.fixedValue = convertToBoolProperty(val);
}
}
/**
* Page footer.
* Located under the nb-layout content (specifically, under the columns).
* Could be made `fixed`, becoming sticky to the bottom of the view port (window).
*
* @styles
*
* footer-height
* footer-padding
* footer-fg
* footer-bg
* footer-separator
* footer-shadow
*/
@Component({
selector: 'nb-layout-footer',
template: `
`,
})
export class NbLayoutFooterComponent {
@HostBinding('class.fixed') fixedValue: boolean;
/**
* Makes the footer sticky to the bottom of the window.
* @param {boolean} val
*/
@Input()
set fixed(val: boolean) {
this.fixedValue = convertToBoolProperty(val);
}
}
/**
* The general Nebular component-container.
* It is required that all children component of the framework are located inside of the nb-layout.
*
* Can contain the following components inside:
*
* ```
* nb-layout-header
* nb-layout-column
* nb-sidebar
* nb-layout-footer
* ```
*
* By default the layout fills up the full view-port.
* The window scrollbars are disabled on the body and moved inside of the nb-layout, so that the scrollbars
* won't mess with the fixed nb-header.
*
* The children components are projected into the flexible layout structure allowing to adjust the layout behavior
* based on the settings provided.
*
* The layout content (columns) becomes centered when the window width is more than
* the value specified in the theme variable `layout-content-width`.
*
* The layout also contains the area on the very top (the first child of the nb-layout), which could be used
* to dynamically append some components like modals or spinners/loaders
* so that they are located on top of the elements hierarchy.
* More details are below under the `ThemeService` section.
*
* The layout component is also responsible for changing of the application themes.
* It listens to the `themeChange` event and change the theme CSS class appended to body.
* Based on the class appended a specific CSS-theme is applied to the application.
* More details of the Theme System could be found here [Enabling Theme System](#/docs/concepts/theme-system)
*
* @example A simple layout example:
*
* ```
*
* Great Company
*
*
* Hello World!
*
*
* Contact us
*
* ```
*
* @example For example, it is possible to ask the layout to center the columns (notice: we added a `center` attribute
* to the layout:
*
* ```
*
* Great Company
*
*
* Hello World!
*
*
* Contact us
*
* ```
*
* @styles
*
* layout-font-family
* layout-font-size
* layout-line-height
* layout-fg
* layout-bg
* layout-min-height
* layout-content-width
* layout-window-mode-min-width
* layout-window-mode-max-width: window mode only, after this value layout turns into floating window
* layout-window-mode-bg: window mode only, background
* layout-window-mode-padding-top: window mode only, max padding from top
* layout-window-shadow: window mode shadow
* layout-padding
* layout-medium-padding
* layout-small-padding
*/
@Component({
selector: 'nb-layout',
styleUrls: ['./layout.component.scss'],
template: `
`,
})
export class NbLayoutComponent implements AfterViewInit, OnInit, OnDestroy {
centerValue: boolean = false;
@HostBinding('class.window-mode') windowModeValue: boolean = false;
@HostBinding('class.with-scroll') withScrollValue: boolean = false;
/**
* Defines whether the layout columns will be centered after some width
* @param {boolean} val
*/
@Input()
set center(val: boolean) {
this.centerValue = convertToBoolProperty(val);
}
/**
* Defines whether the layout enters a 'window' mode, when the layout content (including sidebars and fixed header)
* becomes centered by width with a margin from the top of the screen, like a floating window.
* Automatically enables `withScroll` mode, as in the window mode scroll must be inside the layout and cannot be on
* window. (TODO: check this)
* @param {boolean} val
*/
@Input()
set windowMode(val: boolean) {
this.windowModeValue = convertToBoolProperty(val);
this.withScroll = true;
}
/**
* Defines whether to move the scrollbars to layout or leave it at the body level.
* Automatically set to true when `windowMode` is enabled.
* @param {boolean} val
*/
@Input()
set withScroll(val: boolean) {
this.withScrollValue = convertToBoolProperty(val);
// TODO: is this the best way of doing it? as we don't have access to body from theme styles
// TODO: add e2e test
const body = document.getElementsByTagName('body')[0];
if (this.withScrollValue) {
this.renderer.setStyle(body, 'overflow', 'hidden');
} else {
this.renderer.setStyle(body, 'overflow', 'initial');
}
}
@ViewChild('layoutTopDynamicArea', { read: ViewContainerRef }) veryTopRef: ViewContainerRef;
@ViewChild('scrollableContainer', { read: ElementRef }) scrollableContainerRef: ElementRef;
protected afterViewInit$ = new BehaviorSubject(null);
private alive: boolean = true;
constructor(
protected themeService: NbThemeService,
protected spinnerService: NbSpinnerService,
protected componentFactoryResolver: ComponentFactoryResolver,
protected elementRef: ElementRef,
protected renderer: Renderer2,
protected router: Router,
) {
this.themeService.onThemeChange()
.takeWhile(() => this.alive)
.subscribe((theme) => {
const body = document.getElementsByTagName('body')[0];
if (theme.previous) {
this.renderer.removeClass(body, `nb-theme-${theme.previous}`);
}
this.renderer.addClass(body, `nb-theme-${theme.name}`);
});
this.themeService.onAppendLayoutClass()
.takeWhile(() => this.alive)
.subscribe((className) => {
this.renderer.addClass(this.elementRef.nativeElement, className);
});
this.themeService.onRemoveLayoutClass()
.takeWhile(() => this.alive)
.subscribe((className) => {
this.renderer.removeClass(this.elementRef.nativeElement, className);
});
this.spinnerService.registerLoader(new Promise((resolve, reject) => {
this.afterViewInit$
.takeWhile(() => this.alive)
.subscribe((_) => resolve());
}));
this.spinnerService.load();
// trigger first time so that after the change we have the initial value
this.themeService.changeWindowWidth(window.innerWidth);
}
ngAfterViewInit() {
this.themeService.onAppendToTop()
.takeWhile(() => this.alive)
.subscribe((data: { component: any, listener: Subject }) => {
const componentFactory = this.componentFactoryResolver.resolveComponentFactory(data.component);
const componentRef = this.veryTopRef.createComponent(componentFactory);
data.listener.next(componentRef);
data.listener.complete();
});
this.themeService.onClearLayoutTop()
.takeWhile(() => this.alive)
.subscribe((data: { listener: Subject }) => {
this.veryTopRef.clear();
data.listener.next(true);
});
this.afterViewInit$.next(true);
}
ngOnInit() {
this.initScrollTop();
}
ngOnDestroy() {
this.themeService.clearLayoutTop();
this.alive = false;
}
@HostListener('window:resize', ['$event'])
onResize(event) {
this.themeService.changeWindowWidth(event.target.innerWidth);
}
private initScrollTop() {
this.router.events
.filter(event => event instanceof NavigationEnd)
.takeWhile(() => this.alive)
.subscribe(() => {
this.scrollableContainerRef.nativeElement.scrollTo && this.scrollableContainerRef.nativeElement.scrollTo(0, 0);
});
}
}