Skip to content

Commit 7d88492

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

File tree

12 files changed

+530
-8
lines changed

12 files changed

+530
-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: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
@import "default-theme";
2+
@import "variables";
3+
@import "shadows";
4+
5+
6+
$md-sidenav-background-color: md-color($md-background, 100) !default;
7+
$md-sidenav-push-background-color: md-color($md-background, 100) !default;
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-push {
80+
background-color: $md-sidenav-push-background-color;
81+
}
82+
83+
&.md-sidenav-side {
84+
z-index: $z-index-drawer - 1;
85+
}
86+
87+
&.md-sidenav-end {
88+
right: 0;
89+
90+
@include md-sidenav-transition(0, 100%);
91+
}
92+
}
93+
}
94+
95+
96+
:host-context([dir="rtl"]) {
97+
> md-sidenav {
98+
@include md-sidenav-transition(0, 100%);
99+
100+
&.md-sidenav-end {
101+
left: 0;
102+
right: auto;
103+
104+
@include md-sidenav-transition(0, -100%);
105+
}
106+
}
107+
}

src/components/sidenav/sidenav.ts

Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
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+
/**
83+
* Constructor.
84+
* @param elementRef_ The DOM element reference. If not available we do not hook on transitions.
85+
*/
86+
// TODO(hansl): Get rid of ElementRef.
87+
constructor(private elementRef_: ElementRef) {
88+
if (elementRef_ && elementRef_.nativeElement) {
89+
elementRef_.nativeElement.addEventListener('transitionend',
90+
(e: TransitionEvent) => {
91+
if (e.target == elementRef_.nativeElement && e.propertyName == 'transform') {
92+
this.transition_ = false;
93+
if (this.opened_) {
94+
this.onOpen.emit(null);
95+
} else {
96+
this.onClose.emit(null);
97+
}
98+
}
99+
}, false);
100+
}
101+
}
102+
103+
104+
/** Width of the sidenav. */
105+
get width() {
106+
if (this.elementRef_.nativeElement) {
107+
return this.elementRef_.nativeElement.offsetWidth;
108+
}
109+
return 0;
110+
}
111+
112+
/** Open this sidenav, and return a Promise that will resolve when it's fully opened (or get
113+
* rejected if it didn't). */
114+
open(): Promise<void> {
115+
return this.toggle(true);
116+
}
117+
118+
/**
119+
* Close this sidenav, and return a Promise that will resolve when it's fully closed (or get
120+
* rejected if it didn't).
121+
*/
122+
close(): Promise<void> {
123+
return this.toggle(false);
124+
}
125+
126+
/**
127+
* Toggle this sidenav. This is equivalent to calling open() when it's already opened, or
128+
* close() when it's closed.
129+
* @param isOpen
130+
* @returns {Promise<void>}
131+
*/
132+
toggle(isOpen: boolean = !this.opened): Promise<void> {
133+
// Shortcut it if we're already opened.
134+
if (isOpen === this.opened) {
135+
return Promise.resolve();
136+
}
137+
138+
this.opened = isOpen;
139+
140+
// We hook up both onOpen and onClose, but we reject the promise if
141+
// the animation was cut or cancelled for some reason.
142+
return new Promise<void>((resolve, reject) => {
143+
const property = isOpen ? this.onOpen : this.onClose;
144+
const other = isOpen ? this.onClose : this.onOpen;
145+
const subscription = property.subscribe(() => {
146+
resolve();
147+
property.remove(subscription);
148+
other.remove(otherSubscription);
149+
});
150+
const otherSubscription = property.subscribe(() => {
151+
reject();
152+
property.remove(subscription);
153+
other.remove(otherSubscription);
154+
});
155+
});
156+
}
157+
158+
159+
/************************************************************************************************
160+
* Private members.
161+
*/
162+
@HostBinding('class.md-sidenav-closing') private get isClosing_() {
163+
return !this.opened_ && this.transition_;
164+
}
165+
@HostBinding('class.md-sidenav-opening') private get isOpening_() {
166+
return this.opened_ && this.transition_;
167+
}
168+
@HostBinding('class.md-sidenav-closed') get isClosed() {
169+
return !this.opened_ && !this.transition_;
170+
}
171+
@HostBinding('class.md-sidenav-opened') get isOpened() {
172+
return this.opened_ && !this.transition_;
173+
}
174+
@HostBinding('class.md-sidenav-end') get isEnd() {
175+
return this.align == 'end';
176+
}
177+
@HostBinding('class.md-sidenav-side') get modeSide() {
178+
return this.mode == 'side';
179+
}
180+
@HostBinding('class.md-sidenav-over') get modeOver() {
181+
return this.mode == 'over';
182+
}
183+
@HostBinding('class.md-sidenav-push') get modePush() {
184+
return this.mode == 'push';
185+
}
186+
187+
private transition_: boolean = false;
188+
private opened_: boolean = false;
189+
}
190+
191+
192+
/**
193+
* <md-sidenav-layout> component.
194+
*/
195+
@Component({
196+
selector: 'md-sidenav-layout',
197+
directives: [MdSidenav],
198+
templateUrl: './components/sidenav/sidenav.html',
199+
styleUrls: ['./components/sidenav/sidenav.css'],
200+
})
201+
export class MdSidenavLayout implements AfterContentInit {
202+
@ContentChildren(MdSidenav) private drawers_: QueryList<MdSidenav>;
203+
204+
get start() { return this.start_; }
205+
get end() { return this.end_; }
206+
207+
private start_: MdSidenav;
208+
private end_: MdSidenav;
209+
private right_: MdSidenav;
210+
private left_: MdSidenav;
211+
212+
private validateDrawers_() {
213+
this.start_ = this.end_ = null;
214+
if (this.drawers_.length === 0) {
215+
throw new MdMissingSidenavException();
216+
}
217+
218+
for (const drawer of this.drawers_.toArray()) {
219+
if (drawer.align == 'end') {
220+
if (this.end_) {
221+
throw new MdDuplicatedSidenavException('end');
222+
}
223+
this.end_ = drawer;
224+
} else {
225+
if (this.start_) {
226+
throw new MdDuplicatedSidenavException('start');
227+
}
228+
this.start_ = drawer;
229+
}
230+
}
231+
232+
this.right_ = this.left_ = null;
233+
this.left_ = this.start_;
234+
this.right_ = this.end_;
235+
236+
// Detect if we're LTR or RTL.
237+
if (this.dir_.dir == 'ltr') {
238+
this.left_ = this.start_;
239+
this.right_ = this.end_;
240+
} else {
241+
this.left_ = this.end_;
242+
this.right_ = this.start_;
243+
}
244+
}
245+
246+
constructor(@Optional() @Host() private dir_: Dir) {
247+
this.dir_.onDirChange.subscribe(() => this.validateDrawers_());
248+
}
249+
250+
ngAfterContentInit() {
251+
// On changes, assert on consistency.
252+
this.drawers_.changes.subscribe(() => this.validateDrawers_());
253+
this.validateDrawers_();
254+
}
255+
}

0 commit comments

Comments
 (0)