Skip to content

Commit a4cb50f

Browse files
committed
component(): sidenav component.
1 parent bc6d2bc commit a4cb50f

File tree

12 files changed

+521
-8
lines changed

12 files changed

+521
-8
lines changed
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<div class="md-sidenav-backdrop"
2+
(click)="(start_?.mode == 'side' || start_.close()) && (end_?.mode == 'side' || end_.close())"
3+
[class.md-sidenav-shown]="(start_?.mode != 'side' && start_.opened) ||
4+
(end_?.mode != 'side' && end_.opened)"></div>
5+
6+
<ng-content select="md-sidenav"></ng-content>
7+
8+
<md-content [style.margin-left.px]="(left_?.mode == 'side' && left_.opened) ? left_.width : 0"
9+
[style.margin-right.px]="(right_?.mode == 'side' && right_.opened) ? right_.width : 0"
10+
[style.left.px]="(left_?.mode == 'push' && left_.opened) ? left_.width : 0"
11+
[style.right.px]="(right_?.mode == 'push' && right_.opened) ? right_.width : 0">
12+
<ng-content></ng-content>
13+
</md-content>
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
@import "default-theme";
2+
@import "variables";
3+
@import "shadows";
4+
5+
6+
$md-sidenav-background-color: md-color($md-background, 100) !default;
7+
8+
9+
@mixin md-sidenav-transition($open, $close) {
10+
transform: translateX($close);
11+
12+
&.md-sidenav-closed {
13+
visibility: hidden;
14+
}
15+
&.md-sidenav-closing {
16+
transform: translateX($close);
17+
will-change: transform;
18+
}
19+
&.md-sidenav-opening {
20+
visibility: visible;
21+
transform: translateX($open);
22+
will-change: transform;
23+
box-shadow: $md-shadow-bottom-z-1;
24+
}
25+
&.md-sidenav-opened {
26+
transform: translateX($open);
27+
box-shadow: $md-shadow-bottom-z-1;
28+
}
29+
}
30+
31+
32+
:host {
33+
position: relative;
34+
display: block;
35+
// Use a transform to create a new stacking context.
36+
transform: translate3D(0, 0, 0);
37+
overflow-x: hidden;
38+
39+
transition: margin-left $swift-ease-out-duration $swift-ease-out-timing-function,
40+
margin-right $swift-ease-out-duration $swift-ease-out-timing-function;
41+
42+
& > .md-sidenav-backdrop {
43+
position: absolute;
44+
top: 0;
45+
left: 0;
46+
right: 0;
47+
bottom: 0;
48+
z-index: $z-index-drawer;
49+
visibility: hidden;
50+
display: block;
51+
52+
&.md-sidenav-shown {
53+
visibility: visible;
54+
background-color: rgba(0, 0, 0, 0.21);
55+
transition: background-color $swift-ease-out-duration $swift-ease-out-timing-function;
56+
}
57+
}
58+
59+
& > md-content {
60+
display: block;
61+
position: relative;
62+
transition: margin-left $swift-ease-out-duration $swift-ease-out-timing-function,
63+
margin-right $swift-ease-out-duration $swift-ease-out-timing-function,
64+
left $swift-ease-out-duration $swift-ease-out-timing-function,
65+
right $swift-ease-out-duration $swift-ease-out-timing-function;
66+
}
67+
68+
> md-sidenav {
69+
position: fixed;
70+
top: 0;
71+
bottom: 0;
72+
z-index: $z-index-drawer + 1;
73+
background-color: $md-sidenav-background-color;
74+
75+
transition: transform $swift-ease-out-duration $swift-ease-out-timing-function;
76+
77+
@include md-sidenav-transition(0, -100%);
78+
79+
&.md-sidenav-side {
80+
z-index: $z-index-drawer - 1;
81+
}
82+
83+
&.md-sidenav-end {
84+
right: 0;
85+
86+
@include md-sidenav-transition(0, 100%);
87+
}
88+
}
89+
}
90+
91+
92+
:host-context([dir="rtl"]) {
93+
> md-sidenav {
94+
@include md-sidenav-transition(0, 100%);
95+
96+
&.md-sidenav-end {
97+
left: 0;
98+
right: auto;
99+
100+
@include md-sidenav-transition(0, -100%);
101+
}
102+
}
103+
}

src/components/sidenav/sidenav.ts

Lines changed: 251 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,251 @@
1+
import {
2+
AfterContentInit,
3+
Component,
4+
ContentChildren,
5+
ElementRef,
6+
EventEmitter,
7+
Host,
8+
HostBinding,
9+
Input,
10+
View,
11+
ViewEncapsulation,
12+
OnChanges,
13+
Optional,
14+
Output,
15+
Query,
16+
QueryList,
17+
SimpleChange
18+
} from 'angular2/core';
19+
import {BaseException} from 'angular2/src/facade/exceptions';
20+
import {Dir} from "../../directives/dir/dir";
21+
import {OneOf} from "../../core/annotations/one-of";
22+
23+
24+
/**
25+
* Exception thrown when a MdSidenavLayout is missing both sidenavs.
26+
*/
27+
export class MdMissingSidenavException extends BaseException {}
28+
29+
/**
30+
* Exception thrown when two MdSidenav are matching the same side.
31+
*/
32+
export class MdDuplicatedSidenavException extends BaseException {
33+
constructor(align: string) {
34+
super(`A sidenav was already declared for 'align="${align}"'`);
35+
}
36+
}
37+
38+
39+
/**
40+
* <md-sidenav> component.
41+
*
42+
* This component corresponds to the drawer of the sidenav.
43+
*
44+
* Please refer to README.md for examples on how to use it.
45+
*/
46+
@Component({
47+
selector: 'md-sidenav',
48+
template: '<ng-content></ng-content>',
49+
})
50+
export class MdSidenav {
51+
/** Alignment of the sidenav (direction neutral); whether 'start' or 'end'. */
52+
@Input() @OneOf('start', 'end') align: string = 'start';
53+
54+
/** Mode of the sidenav; whether 'over' or 'side'. */
55+
@Input() @OneOf('push', 'over', 'side') mode: string = 'over';
56+
57+
/** Whether the sidenav is opened. */
58+
@Input() set opened(v: boolean) {
59+
this.opened_ = v;
60+
this.transition_ = true;
61+
if (v) {
62+
this.onOpenStart.emit(null);
63+
} else {
64+
this.onCloseStart.emit(null);
65+
}
66+
}
67+
get opened(): boolean { return this.opened_; }
68+
69+
/** Event emitted when the sidenav is being opened. Use this to synchronize animations. */
70+
@Output('open-start') onOpenStart = new EventEmitter<void>();
71+
72+
/** Event emitted when the sidenav is fully opened. */
73+
@Output('open') onOpen = new EventEmitter<void>();
74+
75+
/** Event emitted when the sidenav is being closed. Use this to synchronize animations. */
76+
@Output('close-start') onCloseStart = new EventEmitter<void>();
77+
78+
/** Event emitted when the sidenav is fully closed. */
79+
@Output('close') onClose = new EventEmitter<void>();
80+
81+
82+
// TODO(hansl): Get rid of ElementRef.
83+
constructor(private elementRef_: ElementRef) {
84+
if (elementRef_) {
85+
elementRef_.nativeElement.addEventListener('transitionend',
86+
(e: TransitionEvent) => {
87+
if (e.target == elementRef_.nativeElement && e.propertyName == 'transform') {
88+
this.transition_ = false;
89+
if (this.opened_) {
90+
this.onOpen.emit(null);
91+
} else {
92+
this.onClose.emit(null);
93+
}
94+
}
95+
}, false);
96+
}
97+
}
98+
99+
100+
/** Width of the sidenav. */
101+
get width() {
102+
if (this.elementRef_.nativeElement) {
103+
return this.elementRef_.nativeElement.offsetWidth;
104+
}
105+
return 0;
106+
}
107+
108+
/** Open this sidenav, and return a Promise that will resolve when it's fully opened (or get
109+
* rejected if it didn't). */
110+
open(): Promise<void> {
111+
return this.toggle(true);
112+
}
113+
114+
/**
115+
* Close this sidenav, and return a Promise that will resolve when it's fully closed (or get
116+
* rejected if it didn't).
117+
*/
118+
close(): Promise<void> {
119+
return this.toggle(false);
120+
}
121+
122+
/**
123+
* Toggle this sidenav. This is equivalent to calling open() when it's already opened, or
124+
* close() when it's closed.
125+
* @param isOpen
126+
* @returns {Promise<void>}
127+
*/
128+
toggle(isOpen: boolean = !this.opened): Promise<void> {
129+
// Shortcut it if we're already opened.
130+
if (isOpen === this.opened) {
131+
return Promise.resolve();
132+
}
133+
134+
this.opened = isOpen;
135+
136+
// We hook up both onOpen and onClose, but we reject the promise if
137+
// the animation was cut or cancelled for some reason.
138+
return new Promise<void>((resolve, reject) => {
139+
const property = isOpen ? this.onOpen : this.onClose;
140+
const other = isOpen ? this.onClose : this.onOpen;
141+
const subscription = property.subscribe(() => {
142+
resolve();
143+
property.remove(subscription);
144+
other.remove(otherSubscription);
145+
});
146+
const otherSubscription = property.subscribe(() => {
147+
reject();
148+
property.remove(subscription);
149+
other.remove(otherSubscription);
150+
});
151+
});
152+
}
153+
154+
155+
/************************************************************************************************
156+
* Private members.
157+
*/
158+
@HostBinding('class.md-sidenav-closing') private get isClosing_() {
159+
return !this.opened_ && this.transition_;
160+
}
161+
@HostBinding('class.md-sidenav-opening') private get isOpening_() {
162+
return this.opened_ && this.transition_;
163+
}
164+
@HostBinding('class.md-sidenav-closed') get isClosed() {
165+
return !this.opened_ && !this.transition_;
166+
}
167+
@HostBinding('class.md-sidenav-opened') get isOpened() {
168+
return this.opened_ && !this.transition_;
169+
}
170+
@HostBinding('class.md-sidenav-end') get isEnd() {
171+
return this.align == 'end';
172+
}
173+
@HostBinding('class.md-sidenav-side') get modeSide() {
174+
return this.mode == 'side';
175+
}
176+
@HostBinding('class.md-sidenav-over') get modeOver() {
177+
return this.mode == 'over';
178+
}
179+
@HostBinding('class.md-sidenav-push') get modePush() {
180+
return this.mode == 'push';
181+
}
182+
183+
private transition_: boolean = false;
184+
private opened_: boolean = false;
185+
}
186+
187+
188+
/**
189+
* <md-sidenav-layout> component.
190+
*/
191+
@Component({
192+
selector: 'md-sidenav-layout',
193+
directives: [MdSidenav],
194+
templateUrl: './components/sidenav/sidenav.html',
195+
styleUrls: ['./components/sidenav/sidenav.css'],
196+
})
197+
export class MdSidenavLayout implements AfterContentInit {
198+
@ContentChildren(MdSidenav) private drawers_: QueryList<MdSidenav>;
199+
200+
get start() { return this.start_; }
201+
get end() { return this.end_; }
202+
203+
private start_: MdSidenav;
204+
private end_: MdSidenav;
205+
private right_: MdSidenav;
206+
private left_: MdSidenav;
207+
208+
private validateDrawers_() {
209+
this.start_ = this.end_ = null;
210+
if (this.drawers_.length === 0) {
211+
throw new MdMissingSidenavException();
212+
}
213+
214+
for (const drawer of this.drawers_.toArray()) {
215+
if (drawer.align == 'end') {
216+
if (this.end_) {
217+
throw new MdDuplicatedSidenavException('end');
218+
}
219+
this.end_ = drawer;
220+
} else {
221+
if (this.start_) {
222+
throw new MdDuplicatedSidenavException('start');
223+
}
224+
this.start_ = drawer;
225+
}
226+
}
227+
228+
this.right_ = this.left_ = null;
229+
this.left_ = this.start_;
230+
this.right_ = this.end_;
231+
232+
// Detect if we're LTR or RTL.
233+
if (this.dir_.dir == 'ltr') {
234+
this.left_ = this.start_;
235+
this.right_ = this.end_;
236+
} else {
237+
this.left_ = this.end_;
238+
this.right_ = this.start_;
239+
}
240+
}
241+
242+
constructor(@Optional() @Host() private dir_: Dir) {
243+
this.dir_.onDirChange.subscribe(() => this.validateDrawers_());
244+
}
245+
246+
ngAfterContentInit() {
247+
// On changes, assert on consistency.
248+
this.drawers_.changes.subscribe(() => this.validateDrawers_());
249+
this.validateDrawers_();
250+
}
251+
}

0 commit comments

Comments
 (0)