Skip to content

Commit

Permalink
component(): sidenav component.
Browse files Browse the repository at this point in the history
  • Loading branch information
hansl committed Jan 27, 2016
1 parent bc6d2bc commit a4cb50f
Show file tree
Hide file tree
Showing 12 changed files with 521 additions and 8 deletions.
13 changes: 13 additions & 0 deletions src/components/sidenav/sidenav.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<div class="md-sidenav-backdrop"
(click)="(start_?.mode == 'side' || start_.close()) && (end_?.mode == 'side' || end_.close())"
[class.md-sidenav-shown]="(start_?.mode != 'side' && start_.opened) ||
(end_?.mode != 'side' && end_.opened)"></div>

<ng-content select="md-sidenav"></ng-content>

<md-content [style.margin-left.px]="(left_?.mode == 'side' && left_.opened) ? left_.width : 0"
[style.margin-right.px]="(right_?.mode == 'side' && right_.opened) ? right_.width : 0"
[style.left.px]="(left_?.mode == 'push' && left_.opened) ? left_.width : 0"
[style.right.px]="(right_?.mode == 'push' && right_.opened) ? right_.width : 0">
<ng-content></ng-content>
</md-content>
103 changes: 103 additions & 0 deletions src/components/sidenav/sidenav.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
@import "default-theme";
@import "variables";
@import "shadows";


$md-sidenav-background-color: md-color($md-background, 100) !default;


@mixin md-sidenav-transition($open, $close) {
transform: translateX($close);

&.md-sidenav-closed {
visibility: hidden;
}
&.md-sidenav-closing {
transform: translateX($close);
will-change: transform;
}
&.md-sidenav-opening {
visibility: visible;
transform: translateX($open);
will-change: transform;
box-shadow: $md-shadow-bottom-z-1;
}
&.md-sidenav-opened {
transform: translateX($open);
box-shadow: $md-shadow-bottom-z-1;
}
}


:host {
position: relative;
display: block;
// Use a transform to create a new stacking context.
transform: translate3D(0, 0, 0);
overflow-x: hidden;

transition: margin-left $swift-ease-out-duration $swift-ease-out-timing-function,
margin-right $swift-ease-out-duration $swift-ease-out-timing-function;

& > .md-sidenav-backdrop {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: $z-index-drawer;
visibility: hidden;
display: block;

&.md-sidenav-shown {
visibility: visible;
background-color: rgba(0, 0, 0, 0.21);
transition: background-color $swift-ease-out-duration $swift-ease-out-timing-function;
}
}

& > md-content {
display: block;
position: relative;
transition: margin-left $swift-ease-out-duration $swift-ease-out-timing-function,
margin-right $swift-ease-out-duration $swift-ease-out-timing-function,
left $swift-ease-out-duration $swift-ease-out-timing-function,
right $swift-ease-out-duration $swift-ease-out-timing-function;
}

> md-sidenav {
position: fixed;
top: 0;
bottom: 0;
z-index: $z-index-drawer + 1;
background-color: $md-sidenav-background-color;

transition: transform $swift-ease-out-duration $swift-ease-out-timing-function;

@include md-sidenav-transition(0, -100%);

&.md-sidenav-side {
z-index: $z-index-drawer - 1;
}

&.md-sidenav-end {
right: 0;

@include md-sidenav-transition(0, 100%);
}
}
}


:host-context([dir="rtl"]) {
> md-sidenav {
@include md-sidenav-transition(0, 100%);

&.md-sidenav-end {
left: 0;
right: auto;

@include md-sidenav-transition(0, -100%);
}
}
}
251 changes: 251 additions & 0 deletions src/components/sidenav/sidenav.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,251 @@
import {
AfterContentInit,
Component,
ContentChildren,
ElementRef,
EventEmitter,
Host,
HostBinding,
Input,
View,
ViewEncapsulation,
OnChanges,
Optional,
Output,
Query,
QueryList,
SimpleChange
} from 'angular2/core';
import {BaseException} from 'angular2/src/facade/exceptions';
import {Dir} from "../../directives/dir/dir";
import {OneOf} from "../../core/annotations/one-of";


/**
* Exception thrown when a MdSidenavLayout is missing both sidenavs.
*/
export class MdMissingSidenavException extends BaseException {}

/**
* Exception thrown when two MdSidenav are matching the same side.
*/
export class MdDuplicatedSidenavException extends BaseException {
constructor(align: string) {
super(`A sidenav was already declared for 'align="${align}"'`);
}
}


/**
* <md-sidenav> component.
*
* This component corresponds to the drawer of the sidenav.
*
* Please refer to README.md for examples on how to use it.
*/
@Component({
selector: 'md-sidenav',
template: '<ng-content></ng-content>',
})
export class MdSidenav {
/** Alignment of the sidenav (direction neutral); whether 'start' or 'end'. */
@Input() @OneOf('start', 'end') align: string = 'start';

/** Mode of the sidenav; whether 'over' or 'side'. */
@Input() @OneOf('push', 'over', 'side') mode: string = 'over';

/** Whether the sidenav is opened. */
@Input() set opened(v: boolean) {
this.opened_ = v;
this.transition_ = true;
if (v) {
this.onOpenStart.emit(null);
} else {
this.onCloseStart.emit(null);
}
}
get opened(): boolean { return this.opened_; }

/** Event emitted when the sidenav is being opened. Use this to synchronize animations. */
@Output('open-start') onOpenStart = new EventEmitter<void>();

/** Event emitted when the sidenav is fully opened. */
@Output('open') onOpen = new EventEmitter<void>();

/** Event emitted when the sidenav is being closed. Use this to synchronize animations. */
@Output('close-start') onCloseStart = new EventEmitter<void>();

/** Event emitted when the sidenav is fully closed. */
@Output('close') onClose = new EventEmitter<void>();


// TODO(hansl): Get rid of ElementRef.
constructor(private elementRef_: ElementRef) {
if (elementRef_) {
elementRef_.nativeElement.addEventListener('transitionend',
(e: TransitionEvent) => {
if (e.target == elementRef_.nativeElement && e.propertyName == 'transform') {
this.transition_ = false;
if (this.opened_) {
this.onOpen.emit(null);
} else {
this.onClose.emit(null);
}
}
}, false);
}
}


/** Width of the sidenav. */
get width() {
if (this.elementRef_.nativeElement) {
return this.elementRef_.nativeElement.offsetWidth;
}
return 0;
}

/** Open this sidenav, and return a Promise that will resolve when it's fully opened (or get
* rejected if it didn't). */
open(): Promise<void> {
return this.toggle(true);
}

/**
* Close this sidenav, and return a Promise that will resolve when it's fully closed (or get
* rejected if it didn't).
*/
close(): Promise<void> {
return this.toggle(false);
}

/**
* Toggle this sidenav. This is equivalent to calling open() when it's already opened, or
* close() when it's closed.
* @param isOpen
* @returns {Promise<void>}
*/
toggle(isOpen: boolean = !this.opened): Promise<void> {
// Shortcut it if we're already opened.
if (isOpen === this.opened) {
return Promise.resolve();
}

this.opened = isOpen;

// We hook up both onOpen and onClose, but we reject the promise if
// the animation was cut or cancelled for some reason.
return new Promise<void>((resolve, reject) => {
const property = isOpen ? this.onOpen : this.onClose;
const other = isOpen ? this.onClose : this.onOpen;
const subscription = property.subscribe(() => {
resolve();
property.remove(subscription);
other.remove(otherSubscription);
});
const otherSubscription = property.subscribe(() => {
reject();
property.remove(subscription);
other.remove(otherSubscription);
});
});
}


/************************************************************************************************
* Private members.
*/
@HostBinding('class.md-sidenav-closing') private get isClosing_() {
return !this.opened_ && this.transition_;
}
@HostBinding('class.md-sidenav-opening') private get isOpening_() {
return this.opened_ && this.transition_;
}
@HostBinding('class.md-sidenav-closed') get isClosed() {
return !this.opened_ && !this.transition_;
}
@HostBinding('class.md-sidenav-opened') get isOpened() {
return this.opened_ && !this.transition_;
}
@HostBinding('class.md-sidenav-end') get isEnd() {
return this.align == 'end';
}
@HostBinding('class.md-sidenav-side') get modeSide() {
return this.mode == 'side';
}
@HostBinding('class.md-sidenav-over') get modeOver() {
return this.mode == 'over';
}
@HostBinding('class.md-sidenav-push') get modePush() {
return this.mode == 'push';
}

private transition_: boolean = false;
private opened_: boolean = false;
}


/**
* <md-sidenav-layout> component.
*/
@Component({
selector: 'md-sidenav-layout',
directives: [MdSidenav],
templateUrl: './components/sidenav/sidenav.html',
styleUrls: ['./components/sidenav/sidenav.css'],
})
export class MdSidenavLayout implements AfterContentInit {
@ContentChildren(MdSidenav) private drawers_: QueryList<MdSidenav>;

get start() { return this.start_; }
get end() { return this.end_; }

private start_: MdSidenav;
private end_: MdSidenav;
private right_: MdSidenav;
private left_: MdSidenav;

private validateDrawers_() {
this.start_ = this.end_ = null;
if (this.drawers_.length === 0) {
throw new MdMissingSidenavException();
}

for (const drawer of this.drawers_.toArray()) {
if (drawer.align == 'end') {
if (this.end_) {
throw new MdDuplicatedSidenavException('end');
}
this.end_ = drawer;
} else {
if (this.start_) {
throw new MdDuplicatedSidenavException('start');
}
this.start_ = drawer;
}
}

this.right_ = this.left_ = null;
this.left_ = this.start_;
this.right_ = this.end_;

// Detect if we're LTR or RTL.
if (this.dir_.dir == 'ltr') {
this.left_ = this.start_;
this.right_ = this.end_;
} else {
this.left_ = this.end_;
this.right_ = this.start_;
}
}

constructor(@Optional() @Host() private dir_: Dir) {
this.dir_.onDirChange.subscribe(() => this.validateDrawers_());
}

ngAfterContentInit() {
// On changes, assert on consistency.
this.drawers_.changes.subscribe(() => this.validateDrawers_());
this.validateDrawers_();
}
}
Loading

0 comments on commit a4cb50f

Please sign in to comment.