|  | 
|  | 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