diff --git a/demo-shell/src/app/components/app-layout/app-layout.component.html b/demo-shell/src/app/components/app-layout/app-layout.component.html index 54bcac2615..2a9007d498 100644 --- a/demo-shell/src/app/components/app-layout/app-layout.component.html +++ b/demo-shell/src/app/components/app-layout/app-layout.component.html @@ -1,47 +1,55 @@ - - - - - {{link.icon}} - {{link.title | translate }} - - - exit_to_app - Logout - - - - - + - {{'APP_LAYOUT.APP_NAME' | translate }} + + + + -
+ {{'APP_LAYOUT.APP_NAME' | translate }} - +
- {{'APP_LAYOUT.HOME' | translate }} - {{'APP_LAYOUT.CONTENT_SERVICES' | translate }} - {{'APP_LAYOUT.PROCESS_SERVICES' | translate }} - Login + - - - - - + - -
+ + + + + +
+ + - -
+ + + + + {{link.icon}} +
{{link.title | translate }}
+
+ + exit_to_app +
Logout
+
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/demo-shell/src/app/components/app-layout/app-layout.component.scss b/demo-shell/src/app/components/app-layout/app-layout.component.scss index e316810173..68c59c7a14 100644 --- a/demo-shell/src/app/components/app-layout/app-layout.component.scss +++ b/demo-shell/src/app/components/app-layout/app-layout.component.scss @@ -1,5 +1,7 @@ @mixin adf-app-layout-theme($theme) { $primary: map-get($theme, primary); + $background: map-get($theme, background); + $foreground: map-get($theme, foreground); $minimumAppWidth: 320px; $toolbarHeight: 64px; @@ -22,7 +24,8 @@ } .adf-app-layout { - display: block; + display: flex; + flex: 1; min-width: $minimumAppWidth; height: 100%; @@ -32,10 +35,32 @@ height: 100%; } + .adf-sidenav-linklist { + height: 100%; + overflow: auto; + padding-bottom: 8px; + box-sizing: border-box; + } + .adf-sidenav-link { + &.active { color: mat-color($primary); } + + .sidenav-menu-icon { + margin-right: 20px; + font-size: 14px; + } + + .sidenav-menu-label { + font-size: 14px; + white-space: nowrap; + } + } + + .mat-nav-list .mat-list-item.adf-sidenav-link { + height: 40px; } &-user-profile { diff --git a/demo-shell/src/app/components/app-layout/app-layout.component.ts b/demo-shell/src/app/components/app-layout/app-layout.component.ts index 546f0c92c7..8de9820971 100644 --- a/demo-shell/src/app/components/app-layout/app-layout.component.ts +++ b/demo-shell/src/app/components/app-layout/app-layout.component.ts @@ -30,9 +30,9 @@ export class AppLayoutComponent { links: Array = [ { href: '/home', icon: 'home', title: 'APP_LAYOUT.HOME' }, { href: '/files', icon: 'folder_open', title: 'APP_LAYOUT.CONTENT_SERVICES' }, - { href: '/trashcan', icon: 'delete', title: 'APP_LAYOUT.TRASHCAN' }, { href: '/activiti', icon: 'device_hub', title: 'APP_LAYOUT.PROCESS_SERVICES' }, { href: '/login', icon: 'vpn_key', title: 'APP_LAYOUT.LOGIN' }, + { href: '/trashcan', icon: 'delete', title: 'APP_LAYOUT.TRASHCAN' }, { href: '/dl-custom-sources', icon: 'extension', title: 'APP_LAYOUT.CUSTOM_SOURCES' }, { href: '/datatable', icon: 'view_module', title: 'APP_LAYOUT.DATATABLE' }, { href: '/datatable-lazy', icon: 'view_module', title: 'APP_LAYOUT.DATATABLE_LAZY' }, diff --git a/demo-shell/src/app/components/home/home.component.html b/demo-shell/src/app/components/home/home.component.html index 42f7292723..7d7268f64b 100644 --- a/demo-shell/src/app/components/home/home.component.html +++ b/demo-shell/src/app/components/home/home.component.html @@ -1,4 +1,4 @@ -
+

ADF

diff --git a/demo-shell/src/app/components/home/home.component.scss b/demo-shell/src/app/components/home/home.component.scss index 9aee2a5db0..51e04c0ebe 100644 --- a/demo-shell/src/app/components/home/home.component.scss +++ b/demo-shell/src/app/components/home/home.component.scss @@ -1,3 +1,9 @@ +:host { + display: flex; + justify-content: center; + align-items: center; +} + .adf-home-header-background { overflow: hidden; } @@ -8,6 +14,7 @@ } .adf-home-headline { + h1 { font-size: 56px; font-weight: 300; diff --git a/docs/core/sidenav-layout.component.md b/docs/core/sidenav-layout.component.md new file mode 100644 index 0000000000..960eebf516 --- /dev/null +++ b/docs/core/sidenav-layout.component.md @@ -0,0 +1,78 @@ +--- +Added: v2.3.0 +Status: Active +--- +# Sidenav layout component + +A generalised component to help displayig the "ADF style" application frame. The layout consists of 3 regions: + +- header +- navigation +- content + +The navigation (depending on the screensize) either uses the Angular Material Sidenav (on small breakpoint), or the ADF style Sidenav (on bigger breakpoint). + +- For Angular Material Sidenav see examples on the Angular Material project's site. +- The ADF style Sidenav has 2 states: **expanded** and **compact**. Regardless of the state, it is always displayed on the screen, either in small width (compact) or in bigger width (expanded). + +The contents of the 3 regions can be injected through Angular's template transclusion. For more details see the usage example of the components. + +On desktop (above stepOver): + + +On mobile (below stepOver): + + +## Basic Usage + +```html + + + + +
+ +
+
+
+ + + +
+
+
+
+ + + + + + + +
+``` + +### Properties + +| Name | Type | Default | Description | +| --- | --- | --- | --- | +| sidenavMin | number | - | (**required**) the compact width in pixels | +| sidenavMax | number | - | (**required**) the expanded width in pixels | +| stepOver | number | - | (**required**) The breakpoint in pixels, where to step over to mobile/desktop | +| hideSidenav | boolean | false | Hide the navigation or not | +| expandedSidenav | boolean | true | The initial (expanded/compact) state of the navigation | + +## Template context + +Each template is given the context containing the following methods: + +### toggleMenu(): void + +Trigger menu toggling +### isMenuMinimized(): boolean +The expanded/compact (minimized) state of the navigation. This one only makes sense in case of desktop size, when the screen size is above the value of stepOver. \ No newline at end of file diff --git a/docs/docassets/images/sidenav-layout-mobile.png b/docs/docassets/images/sidenav-layout-mobile.png new file mode 100644 index 0000000000..a114499f02 Binary files /dev/null and b/docs/docassets/images/sidenav-layout-mobile.png differ diff --git a/docs/docassets/images/sidenav-layout.png b/docs/docassets/images/sidenav-layout.png new file mode 100644 index 0000000000..074b745081 Binary files /dev/null and b/docs/docassets/images/sidenav-layout.png differ diff --git a/lib/core/core.module.ts b/lib/core/core.module.ts index e73ba79be5..9873f3d9bf 100644 --- a/lib/core/core.module.ts +++ b/lib/core/core.module.ts @@ -37,6 +37,7 @@ import { ToolbarModule } from './toolbar/toolbar.module'; import { UserInfoModule } from './userinfo/userinfo.module'; import { ViewerModule } from './viewer/viewer.module'; import { FormModule } from './form/form.module'; +import { SidenavLayoutModule } from './sidenav-layout/sidenav-layout.module'; import { SideBarActionModule } from './sidebar/sidebar-action.module'; import { DirectiveModule } from './directives/directive.module'; @@ -122,6 +123,7 @@ export function providers() { @NgModule({ imports: [ ViewerModule, + SidenavLayoutModule, SideBarActionModule, PipeModule, CommonModule, @@ -155,6 +157,7 @@ export function providers() { exports: [ ViewerModule, SideBarActionModule, + SidenavLayoutModule, PipeModule, CommonModule, DirectiveModule, @@ -186,6 +189,7 @@ export class CoreModuleLazy { imports: [ ViewerModule, SideBarActionModule, + SidenavLayoutModule, PipeModule, CommonModule, DirectiveModule, @@ -218,6 +222,7 @@ export class CoreModuleLazy { exports: [ ViewerModule, SideBarActionModule, + SidenavLayoutModule, PipeModule, CommonModule, DirectiveModule, diff --git a/lib/core/sidenav-layout/components/layout-container/layout-container.component.html b/lib/core/sidenav-layout/components/layout-container/layout-container.component.html new file mode 100644 index 0000000000..83856bbd36 --- /dev/null +++ b/lib/core/sidenav-layout/components/layout-container/layout-container.component.html @@ -0,0 +1,13 @@ + + + + + + + + + diff --git a/lib/core/sidenav-layout/components/layout-container/layout-container.component.scss b/lib/core/sidenav-layout/components/layout-container/layout-container.component.scss new file mode 100644 index 0000000000..eca3fb0e31 --- /dev/null +++ b/lib/core/sidenav-layout/components/layout-container/layout-container.component.scss @@ -0,0 +1,39 @@ +:host { + display: block; + width: 100%; + height: 100%; + overflow: hidden; +} + +ng-content { + display: block; + width: 100%; + height: 100%; + overflow: hidden; +} + +.sidenav--hidden { + visibility: hidden !important; + width: 0 !important; + transform: unset !important; + opacity: 0 !important; +} + +.mat-sidenav-container { + display: block; + width: 100%; + height: 100%; + overflow: hidden; +} + +.mat-sidenav { + overflow: hidden; +} + +.mat-sidenav-content, +.mat-drawer-transition .mat-drawer-content { + transform: unset !important; + transition-property: unset !important; + transition-duration: unset !important; + transition-timing-function: unset !important; +} diff --git a/lib/core/sidenav-layout/components/layout-container/layout-container.component.ts b/lib/core/sidenav-layout/components/layout-container/layout-container.component.ts new file mode 100644 index 0000000000..2e10ef8aac --- /dev/null +++ b/lib/core/sidenav-layout/components/layout-container/layout-container.component.ts @@ -0,0 +1,109 @@ +/*! + * @license + * Copyright 2016 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Component, Input, ViewChild, OnInit, OnDestroy } from '@angular/core'; +import { MatSidenav } from '@angular/material'; +import { sidenavAnimation, contentAnimation } from '../../helpers/animations'; + +@Component({ + selector: 'adf-layout-container', + templateUrl: './layout-container.component.html', + styleUrls: ['./layout-container.component.scss'], + animations: [ sidenavAnimation, contentAnimation ] +}) +export class LayoutContainerComponent implements OnInit, OnDestroy { + @Input() sidenavMin: number; + @Input() sidenavMax: number; + @Input() mediaQueryList: MediaQueryList; + @Input() hideSidenav = false; + @Input() expandedSidenav = true; + + @ViewChild(MatSidenav) sidenav: MatSidenav; + + sidenavAnimationState: any; + contentAnimationState: any; + + SIDENAV_STATES = { MOBILE: {}, EXPANDED: {}, COMPACT: {} }; + CONTENT_STATES = { MOBILE: {}, EXPANDED: {}, COMPACT: {} }; + + constructor() { + this.onMediaQueryChange = this.onMediaQueryChange.bind(this); + } + + ngOnInit() { + this.SIDENAV_STATES.MOBILE = { value: 'expanded', params: { width: this.sidenavMax } }; + this.SIDENAV_STATES.EXPANDED = { value: 'expanded', params: { width: this.sidenavMax } }; + this.SIDENAV_STATES.COMPACT = { value: 'compact', params: {width: this.sidenavMin } }; + + this.CONTENT_STATES.MOBILE = { value: 'expanded', params: { marginLeft: 0 } }; + this.CONTENT_STATES.EXPANDED = { value: 'expanded', params: { marginLeft: this.sidenavMin } }; + this.CONTENT_STATES.COMPACT = { value: 'compact', params: { marginLeft: this.sidenavMax } }; + + this.mediaQueryList.addListener(this.onMediaQueryChange); + + if (this.isMobileScreenSize) { + this.sidenavAnimationState = this.SIDENAV_STATES.MOBILE; + this.contentAnimationState = this.CONTENT_STATES.MOBILE; + } else if (this.expandedSidenav) { + this.sidenavAnimationState = this.SIDENAV_STATES.EXPANDED; + this.contentAnimationState = this.CONTENT_STATES.COMPACT; + } else { + this.sidenavAnimationState = this.SIDENAV_STATES.COMPACT; + this.contentAnimationState = this.CONTENT_STATES.EXPANDED; + } + } + + ngOnDestroy(): void { + this.mediaQueryList.removeListener(this.onMediaQueryChange); + } + + toggleMenu(): void { + if (this.isMobileScreenSize) { + this.sidenav.toggle(); + } else { + this.sidenavAnimationState = this.toggledSidenavAnimation; + this.contentAnimationState = this.toggledContentAnimation; + } + } + + private get toggledSidenavAnimation() { + return this.sidenavAnimationState === this.SIDENAV_STATES.EXPANDED + ? this.SIDENAV_STATES.COMPACT + : this.SIDENAV_STATES.EXPANDED; + } + + private get toggledContentAnimation() { + if (this.isMobileScreenSize) { + return this.CONTENT_STATES.MOBILE; + } + + if (this.sidenavAnimationState === this.SIDENAV_STATES.EXPANDED) { + return this.CONTENT_STATES.COMPACT; + } else { + return this.CONTENT_STATES.EXPANDED; + } + } + + private get isMobileScreenSize(): boolean { + return this.mediaQueryList.matches; + } + + private onMediaQueryChange() { + this.sidenavAnimationState = this.SIDENAV_STATES.EXPANDED; + this.contentAnimationState = this.toggledContentAnimation; + } +} diff --git a/lib/core/sidenav-layout/components/sidenav-layout/sidenav-layout.component.html b/lib/core/sidenav-layout/components/sidenav-layout/sidenav-layout.component.html new file mode 100644 index 0000000000..55ff07fc51 --- /dev/null +++ b/lib/core/sidenav-layout/components/sidenav-layout/sidenav-layout.component.html @@ -0,0 +1,26 @@ +
+ + + + + + + + + + + + + + + +
+ + \ No newline at end of file diff --git a/lib/core/sidenav-layout/components/sidenav-layout/sidenav-layout.component.scss b/lib/core/sidenav-layout/components/sidenav-layout/sidenav-layout.component.scss new file mode 100644 index 0000000000..da152a9c2d --- /dev/null +++ b/lib/core/sidenav-layout/components/sidenav-layout/sidenav-layout.component.scss @@ -0,0 +1,18 @@ +:host { + display: flex; + flex: 1; + + .sidenav-layout { + width: 100%; + display: flex; + flex-direction: column; + + .layout__content { + flex: 1 1 auto; + } + } + + router-outlet { + flex: 0 0; + } +} diff --git a/lib/core/sidenav-layout/components/sidenav-layout/sidenav-layout.component.spec.ts b/lib/core/sidenav-layout/components/sidenav-layout/sidenav-layout.component.spec.ts new file mode 100644 index 0000000000..b0c924bc5d --- /dev/null +++ b/lib/core/sidenav-layout/components/sidenav-layout/sidenav-layout.component.spec.ts @@ -0,0 +1,280 @@ +/*! + * @license + * Copyright 2016 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/*tslint:disable: ban*/ + +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; +import { SidenavLayoutComponent } from './sidenav-layout.component'; +import { Component, Input } from '@angular/core'; +import { LayoutModule, MediaMatcher } from '@angular/cdk/layout'; +import { PlatformModule } from '@angular/cdk/platform'; +import { MaterialModule } from '../../../material.module'; +import { SidenavLayoutContentDirective } from '../../directives/sidenav-layout-content.directive'; +import { SidenavLayoutHeaderDirective } from '../../directives/sidenav-layout-header.directive'; +import { SidenavLayoutNavigationDirective } from '../../directives/sidenav-layout-navigation.directive'; +import { CommonModule } from '@angular/common'; + +@Component({ + selector: 'adf-layout-container', + template: ` + + ` +}) +export class DummyLayoutContainerComponent { + @Input() sidenavMin: number; + @Input() sidenavMax: number; + @Input() mediaQueryList: MediaQueryList; + @Input() hideSidenav: boolean; + @Input() expandedSidenav: boolean; + toggleMenu () {} +} + +describe('SidenavLayoutComponent', () => { + + let fixture: ComponentFixture, + mediaMatcher: MediaMatcher, + mediaQueryList: any; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + CommonModule, + PlatformModule, + LayoutModule, + MaterialModule + ], + declarations: [ + DummyLayoutContainerComponent, + SidenavLayoutComponent, + SidenavLayoutContentDirective, + SidenavLayoutHeaderDirective, + SidenavLayoutNavigationDirective + ], + providers: [ + MediaMatcher + ] + }); + })); + + beforeEach(() => { + mediaQueryList = { + matches: false, + addListener: () => {}, + removeListener: () => {} + }; + }); + + afterEach(() => { + fixture.destroy(); + TestBed.resetTestingModule(); + }); + + describe('Template transclusion', () => { + + @Component({ + selector: 'adf-test-component-for-sidenav', + template: ` + + + + +
+
+
+ + + + + + + + + +
+
+
+
` + }) + class SidenavLayoutTesterComponent {} + + beforeEach(async(() => { + TestBed.configureTestingModule({ declarations: [ SidenavLayoutTesterComponent ] }).compileComponents(); + })); + + beforeEach(() => { + mediaMatcher = TestBed.get(MediaMatcher); + spyOn(mediaMatcher, 'matchMedia').and.returnValue(mediaQueryList); + + fixture = TestBed.createComponent(SidenavLayoutTesterComponent); + fixture.detectChanges(); + }); + + describe('adf-sidenav-layout-navigation', () => { + + const injectedElementSelector = By.css('[data-automation-id="adf-layout-container"] #nav-test'); + + it('should contain the transcluded side navigation template', () => { + const injectedElement = fixture.debugElement.query(injectedElementSelector); + + expect(injectedElement === null).toBe(false); + }); + + it('should let the isMenuMinimized property of component to be accessed by the transcluded template', () => { + const injectedElement = fixture.debugElement.query(injectedElementSelector); + + expect(injectedElement.nativeElement.innerText.trim()).toBe('variable-is-injected'); + }); + }); + + describe('adf-sidenav-layout-header', () => { + + const outerHeaderSelector = By.css('.sidenav-layout > #header-test'), + innerHeaderSelector = By.css('.sidenav-layout [data-automation-id="adf-layout-container"] #header-test'); + + it('should contain the transcluded header template outside of the layout-container', () => { + mediaQueryList.matches = false; + fixture.detectChanges(); + const outerHeaderElement = fixture.debugElement.query(outerHeaderSelector); + const innerHeaderElement = fixture.debugElement.query(innerHeaderSelector); + + expect(outerHeaderElement === null).toBe(false, 'Outer header should be shown'); + expect(innerHeaderElement === null).toBe(true, 'Inner header should not be shown'); + }); + + it('should contain the transcluded header template inside of the layout-container', () => { + mediaQueryList.matches = true; + fixture.detectChanges(); + const outerHeaderElement = fixture.debugElement.query(outerHeaderSelector); + const innerHeaderElement = fixture.debugElement.query(innerHeaderSelector); + + expect(outerHeaderElement === null).toBe(true, 'Outer header should not be shown'); + expect(innerHeaderElement === null).toBe(false, 'Inner header should be shown'); + }); + + it('should call through the layout container\'s toggleMenu method', () => { + mediaQueryList.matches = false; + fixture.detectChanges(); + const layoutContainerComponent = fixture.debugElement.query(By.directive(DummyLayoutContainerComponent)).componentInstance; + spyOn(layoutContainerComponent, 'toggleMenu'); + + const outerHeaderElement = fixture.debugElement.query(outerHeaderSelector); + outerHeaderElement.triggerEventHandler('click', {}); + + expect(layoutContainerComponent.toggleMenu).toHaveBeenCalled(); + }); + }); + + describe('adf-sidenav-layout-content', () => { + + const injectedElementSelector = By.css('[data-automation-id="adf-layout-container"] #content-test'); + + it('should contain the transcluded content template', () => { + const injectedElement = fixture.debugElement.query(injectedElementSelector); + + expect(injectedElement === null).toBe(false); + }); + }); + }); + + describe('General behaviour', () => { + + let component: SidenavLayoutComponent; + + beforeEach(async(() => { + TestBed.compileComponents(); + })); + + beforeEach(() => { + mediaMatcher = TestBed.get(MediaMatcher); + spyOn(mediaMatcher, 'matchMedia').and.callFake((mediaQuery) => { + mediaQueryList.originalMediaQueryPassed = mediaQuery; + spyOn(mediaQueryList, 'addListener').and.stub(); + spyOn(mediaQueryList, 'removeListener').and.stub(); + return mediaQueryList; + }); + + fixture = TestBed.createComponent(SidenavLayoutComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should pass through input paramters', () => { + component.sidenavMin = 1; + component.sidenavMax = 2; + component.hideSidenav = true; + component.expandedSidenav = false; + fixture.detectChanges(); + + const layoutContainerComponent = fixture.debugElement.query(By.directive(DummyLayoutContainerComponent)).componentInstance; + + expect(layoutContainerComponent.sidenavMin).toBe(component.sidenavMin); + expect(layoutContainerComponent.sidenavMax).toBe(component.sidenavMax); + expect(layoutContainerComponent.hideSidenav).toBe(component.hideSidenav); + expect(layoutContainerComponent.expandedSidenav).toBe(component.expandedSidenav); + expect(layoutContainerComponent.mediaQueryList.originalMediaQueryPassed).toBe(`(max-width: 600px)`); + }); + + it('addListener of mediaQueryList should have been called', () => { + expect(mediaQueryList.addListener).toHaveBeenCalledTimes(1); + expect(mediaQueryList.addListener).toHaveBeenCalledWith(component.onMediaQueryChange); + }); + + it('addListener of mediaQueryList should have been called', () => { + fixture.destroy(); + + expect(mediaQueryList.removeListener).toHaveBeenCalledTimes(1); + expect(mediaQueryList.removeListener).toHaveBeenCalledWith(component.onMediaQueryChange); + }); + }); + + describe('toggleMenu', () => { + + let component; + + beforeEach(async(() => { + TestBed.compileComponents(); + })); + + beforeEach(() => { + mediaMatcher = TestBed.get(MediaMatcher); + spyOn(mediaMatcher, 'matchMedia').and.returnValue(mediaQueryList); + + fixture = TestBed.createComponent(SidenavLayoutComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should toggle the isMenuMinimized if the mediaQueryList.matches is false (we are on desktop)', () => { + mediaQueryList.matches = false; + component.isMenuMinimized = false; + + component.toggleMenu(); + + expect(component.isMenuMinimized).toBe(true); + }); + + it('should set the isMenuMinimized to false if the mediaQueryList.matches is true (we are on mobile)', () => { + mediaQueryList.matches = true; + component.isMenuMinimized = true; + + component.toggleMenu(); + + expect(component.isMenuMinimized).toBe(false); + }); + }); +}); diff --git a/lib/core/sidenav-layout/components/sidenav-layout/sidenav-layout.component.ts b/lib/core/sidenav-layout/components/sidenav-layout/sidenav-layout.component.ts new file mode 100644 index 0000000000..734ef08dae --- /dev/null +++ b/lib/core/sidenav-layout/components/sidenav-layout/sidenav-layout.component.ts @@ -0,0 +1,101 @@ +/*! + * @license + * Copyright 2016 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Component, ContentChild, Input, OnInit, AfterViewInit, ViewChild, OnDestroy, TemplateRef } from '@angular/core'; +import { MediaMatcher } from '@angular/cdk/layout'; +import { SidenavLayoutContentDirective } from '../../directives/sidenav-layout-content.directive'; +import { SidenavLayoutHeaderDirective } from '../../directives/sidenav-layout-header.directive'; +import { SidenavLayoutNavigationDirective } from '../../directives/sidenav-layout-navigation.directive'; + +@Component({ + selector: 'adf-sidenav-layout', + templateUrl: './sidenav-layout.component.html', + styleUrls: ['./sidenav-layout.component.scss'] +}) +export class SidenavLayoutComponent implements OnInit, AfterViewInit, OnDestroy { + + static STEP_OVER = 600; + + @Input() sidenavMin: number; + @Input() sidenavMax: number; + @Input() stepOver: number; + @Input() hideSidenav = false; + @Input() expandedSidenav = true; + + @ContentChild(SidenavLayoutHeaderDirective) headerDirective: SidenavLayoutHeaderDirective; + @ContentChild(SidenavLayoutNavigationDirective) navigationDirective: SidenavLayoutNavigationDirective; + @ContentChild(SidenavLayoutContentDirective) contentDirective: SidenavLayoutContentDirective; + + @ViewChild('container') container: any; + @ViewChild('emptyTemplate') emptyTemplate: any; + + mediaQueryList: MediaQueryList; + isMenuMinimized; + templateContext = { + toggleMenu: () => {}, + isMenuMinimized: () => this.isMenuMinimized + }; + + constructor(private mediaMatcher: MediaMatcher) { + this.onMediaQueryChange = this.onMediaQueryChange.bind(this); + } + + ngOnInit() { + const stepOver = this.stepOver || SidenavLayoutComponent.STEP_OVER; + this.isMenuMinimized = !this.expandedSidenav; + this.mediaQueryList = this.mediaMatcher.matchMedia(`(max-width: ${stepOver}px)`); + this.mediaQueryList.addListener(this.onMediaQueryChange); + } + + ngAfterViewInit() { + this.templateContext.toggleMenu = this.toggleMenu.bind(this); + } + + ngOnDestroy(): void { + this.mediaQueryList.removeListener(this.onMediaQueryChange); + } + + toggleMenu() { + if (!this.mediaQueryList.matches) { + this.isMenuMinimized = !this.isMenuMinimized; + } else { + this.isMenuMinimized = false; + } + + this.container.toggleMenu(); + } + + get isHeaderInside() { + return this.mediaQueryList.matches; + } + + get headerTemplate(): TemplateRef { + return this.headerDirective && this.headerDirective.template || this.emptyTemplate; + } + + get navigationTemplate(): TemplateRef { + return this.navigationDirective && this.navigationDirective.template || this.emptyTemplate; + } + + get contentTemplate(): TemplateRef { + return this.contentDirective && this.contentDirective.template || this.emptyTemplate; + } + + onMediaQueryChange() { + this.isMenuMinimized = false; + } +} diff --git a/lib/core/sidenav-layout/directives/sidenav-layout-content.directive.ts b/lib/core/sidenav-layout/directives/sidenav-layout-content.directive.ts new file mode 100644 index 0000000000..7f9e0fd747 --- /dev/null +++ b/lib/core/sidenav-layout/directives/sidenav-layout-content.directive.ts @@ -0,0 +1,26 @@ +/*! + * @license + * Copyright 2016 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ContentChild, Directive, TemplateRef } from '@angular/core'; + +@Directive({ + selector: 'adf-sidenav-layout-content' +}) +export class SidenavLayoutContentDirective { + @ContentChild(TemplateRef) + public template: TemplateRef; +} diff --git a/lib/core/sidenav-layout/directives/sidenav-layout-header.directive.ts b/lib/core/sidenav-layout/directives/sidenav-layout-header.directive.ts new file mode 100644 index 0000000000..0a70602657 --- /dev/null +++ b/lib/core/sidenav-layout/directives/sidenav-layout-header.directive.ts @@ -0,0 +1,26 @@ +/*! + * @license + * Copyright 2016 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ContentChild, Directive, TemplateRef } from '@angular/core'; + +@Directive({ + selector: 'adf-sidenav-layout-header' +}) +export class SidenavLayoutHeaderDirective { + @ContentChild(TemplateRef) + public template: TemplateRef; +} diff --git a/lib/core/sidenav-layout/directives/sidenav-layout-navigation.directive.ts b/lib/core/sidenav-layout/directives/sidenav-layout-navigation.directive.ts new file mode 100644 index 0000000000..baa9ed45d3 --- /dev/null +++ b/lib/core/sidenav-layout/directives/sidenav-layout-navigation.directive.ts @@ -0,0 +1,26 @@ +/*! + * @license + * Copyright 2016 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ContentChild, Directive, TemplateRef } from '@angular/core'; + +@Directive({ + selector: 'adf-sidenav-layout-navigation' +}) +export class SidenavLayoutNavigationDirective { + @ContentChild(TemplateRef) + public template: TemplateRef; +} diff --git a/lib/core/sidenav-layout/helpers/animations.ts b/lib/core/sidenav-layout/helpers/animations.ts new file mode 100644 index 0000000000..a529222be8 --- /dev/null +++ b/lib/core/sidenav-layout/helpers/animations.ts @@ -0,0 +1,30 @@ +/*! + * @license + * Copyright 2016 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { trigger, transition, animate, style, state, AnimationTriggerMetadata } from '@angular/animations'; + +export const sidenavAnimation: AnimationTriggerMetadata = trigger('sidenavAnimation', [ + state('expanded', style({ width: '{{ width }}px' }), { params : { width: 0 } }), + state('compact', style({ width: '{{ width }}px' }), { params : { width: 0 } }), + transition('compact <=> expanded', animate('0.4s cubic-bezier(0.25, 0.8, 0.25, 1)')) +]); + +export const contentAnimation: AnimationTriggerMetadata = trigger('contentAnimation', [ + state('expanded', style({ 'margin-left': '{{ marginLeft }}px' }), { params : { marginLeft: 0 } }), + state('compact', style({'margin-left': '{{ marginLeft }}px' }), { params : { marginLeft: 0 } }), + transition('expanded <=> compact', animate('400ms cubic-bezier(0.25, 0.8, 0.25, 1)')) +]); diff --git a/lib/core/sidenav-layout/index.ts b/lib/core/sidenav-layout/index.ts new file mode 100644 index 0000000000..4c6ac1d58f --- /dev/null +++ b/lib/core/sidenav-layout/index.ts @@ -0,0 +1,18 @@ +/*! + * @license + * Copyright 2016 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export * from './public-api'; diff --git a/lib/core/sidenav-layout/public-api.ts b/lib/core/sidenav-layout/public-api.ts new file mode 100644 index 0000000000..f4ddfbd0b1 --- /dev/null +++ b/lib/core/sidenav-layout/public-api.ts @@ -0,0 +1,18 @@ +/*! + * @license + * Copyright 2016 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export * from './components/sidenav-layout/sidenav-layout.component'; diff --git a/lib/core/sidenav-layout/sidenav-layout.module.ts b/lib/core/sidenav-layout/sidenav-layout.module.ts new file mode 100644 index 0000000000..b3f7b98f41 --- /dev/null +++ b/lib/core/sidenav-layout/sidenav-layout.module.ts @@ -0,0 +1,47 @@ +/*! + * @license + * Copyright 2016 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { MaterialModule } from '../material.module'; +import { SidenavLayoutContentDirective } from './directives/sidenav-layout-content.directive'; +import { SidenavLayoutHeaderDirective } from './directives/sidenav-layout-header.directive'; +import { SidenavLayoutNavigationDirective } from './directives/sidenav-layout-navigation.directive'; +import { SidenavLayoutComponent } from '.'; +import { LayoutContainerComponent } from './components/layout-container/layout-container.component'; + +@NgModule({ + imports: [ + CommonModule, + MaterialModule + ], + exports: [ + SidenavLayoutHeaderDirective, + SidenavLayoutContentDirective, + SidenavLayoutNavigationDirective, + SidenavLayoutComponent, + LayoutContainerComponent + ], + declarations: [ + SidenavLayoutHeaderDirective, + SidenavLayoutContentDirective, + SidenavLayoutNavigationDirective, + SidenavLayoutComponent, + LayoutContainerComponent + ] +}) +export class SidenavLayoutModule {}