3 * Copyright 2015 Google Inc. All Rights Reserved.
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
9 * http://www.apache.org/licenses/LICENSE-2.0
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
22 * Class constructor for Layout MDL component.
23 * Implements MDL component design pattern defined at:
24 * https://github.com/jasonmayes/mdl-component-design-pattern
27 * @param {HTMLElement} element The element that will be upgraded.
29 var MaterialLayout = function MaterialLayout(element) {
30 this.element_ = element;
32 // Initialize instance.
35 window['MaterialLayout'] = MaterialLayout;
38 * Store constants in one place so they can be updated easily.
40 * @enum {string | number}
43 MaterialLayout.prototype.Constant_ = {
44 MAX_WIDTH: '(max-width: 1024px)',
45 TAB_SCROLL_PIXELS: 100,
48 MENU_ICON: '',
49 CHEVRON_LEFT: 'chevron_left',
50 CHEVRON_RIGHT: 'chevron_right'
54 * Keycodes, for code readability.
59 MaterialLayout.prototype.Keycodes_ = {
71 MaterialLayout.prototype.Mode_ = {
79 * Store strings for class names defined by this component that are used in
80 * JavaScript. This allows us to simply change it in one place should we
81 * decide to modify at a later date.
86 MaterialLayout.prototype.CssClasses_ = {
87 CONTAINER: 'mdl-layout__container',
88 HEADER: 'mdl-layout__header',
89 DRAWER: 'mdl-layout__drawer',
90 CONTENT: 'mdl-layout__content',
91 DRAWER_BTN: 'mdl-layout__drawer-button',
93 ICON: 'material-icons',
95 JS_RIPPLE_EFFECT: 'mdl-js-ripple-effect',
96 RIPPLE_CONTAINER: 'mdl-layout__tab-ripple-container',
98 RIPPLE_IGNORE_EVENTS: 'mdl-js-ripple-effect--ignore-events',
100 HEADER_SEAMED: 'mdl-layout__header--seamed',
101 HEADER_WATERFALL: 'mdl-layout__header--waterfall',
102 HEADER_SCROLL: 'mdl-layout__header--scroll',
104 FIXED_HEADER: 'mdl-layout--fixed-header',
105 OBFUSCATOR: 'mdl-layout__obfuscator',
107 TAB_BAR: 'mdl-layout__tab-bar',
108 TAB_CONTAINER: 'mdl-layout__tab-bar-container',
109 TAB: 'mdl-layout__tab',
110 TAB_BAR_BUTTON: 'mdl-layout__tab-bar-button',
111 TAB_BAR_LEFT_BUTTON: 'mdl-layout__tab-bar-left-button',
112 TAB_BAR_RIGHT_BUTTON: 'mdl-layout__tab-bar-right-button',
113 TAB_MANUAL_SWITCH: 'mdl-layout__tab-manual-switch',
114 PANEL: 'mdl-layout__tab-panel',
116 HAS_DRAWER: 'has-drawer',
117 HAS_TABS: 'has-tabs',
118 HAS_SCROLLING_HEADER: 'has-scrolling-header',
119 CASTING_SHADOW: 'is-casting-shadow',
120 IS_COMPACT: 'is-compact',
121 IS_SMALL_SCREEN: 'is-small-screen',
122 IS_DRAWER_OPEN: 'is-visible',
123 IS_ACTIVE: 'is-active',
124 IS_UPGRADED: 'is-upgraded',
125 IS_ANIMATING: 'is-animating',
127 ON_LARGE_SCREEN: 'mdl-layout--large-screen-only',
128 ON_SMALL_SCREEN: 'mdl-layout--small-screen-only'
133 * Handles scrolling on the content.
137 MaterialLayout.prototype.contentScrollHandler_ = function() {
138 if (this.header_.classList.contains(this.CssClasses_.IS_ANIMATING)) {
143 !this.element_.classList.contains(this.CssClasses_.IS_SMALL_SCREEN) ||
144 this.element_.classList.contains(this.CssClasses_.FIXED_HEADER);
146 if (this.content_.scrollTop > 0 &&
147 !this.header_.classList.contains(this.CssClasses_.IS_COMPACT)) {
148 this.header_.classList.add(this.CssClasses_.CASTING_SHADOW);
149 this.header_.classList.add(this.CssClasses_.IS_COMPACT);
151 this.header_.classList.add(this.CssClasses_.IS_ANIMATING);
153 } else if (this.content_.scrollTop <= 0 &&
154 this.header_.classList.contains(this.CssClasses_.IS_COMPACT)) {
155 this.header_.classList.remove(this.CssClasses_.CASTING_SHADOW);
156 this.header_.classList.remove(this.CssClasses_.IS_COMPACT);
158 this.header_.classList.add(this.CssClasses_.IS_ANIMATING);
164 * Handles a keyboard event on the drawer.
166 * @param {Event} evt The event that fired.
169 MaterialLayout.prototype.keyboardEventHandler_ = function(evt) {
170 // Only react when the drawer is open.
171 if (evt.keyCode === this.Keycodes_.ESCAPE &&
172 this.drawer_.classList.contains(this.CssClasses_.IS_DRAWER_OPEN)) {
178 * Handles changes in screen size.
182 MaterialLayout.prototype.screenSizeHandler_ = function() {
183 if (this.screenSizeMediaQuery_.matches) {
184 this.element_.classList.add(this.CssClasses_.IS_SMALL_SCREEN);
186 this.element_.classList.remove(this.CssClasses_.IS_SMALL_SCREEN);
187 // Collapse drawer (if any) when moving to a large screen size.
189 this.drawer_.classList.remove(this.CssClasses_.IS_DRAWER_OPEN);
190 this.obfuscator_.classList.remove(this.CssClasses_.IS_DRAWER_OPEN);
196 * Handles events of drawer button.
198 * @param {Event} evt The event that fired.
201 MaterialLayout.prototype.drawerToggleHandler_ = function(evt) {
202 if (evt && (evt.type === 'keydown')) {
203 if (evt.keyCode === this.Keycodes_.SPACE || evt.keyCode === this.Keycodes_.ENTER) {
204 // prevent scrolling in drawer nav
205 evt.preventDefault();
207 // prevent other keys
216 * Handles (un)setting the `is-animating` class
220 MaterialLayout.prototype.headerTransitionEndHandler_ = function() {
221 this.header_.classList.remove(this.CssClasses_.IS_ANIMATING);
225 * Handles expanding the header on click
229 MaterialLayout.prototype.headerClickHandler_ = function() {
230 if (this.header_.classList.contains(this.CssClasses_.IS_COMPACT)) {
231 this.header_.classList.remove(this.CssClasses_.IS_COMPACT);
232 this.header_.classList.add(this.CssClasses_.IS_ANIMATING);
237 * Reset tab state, dropping active classes
241 MaterialLayout.prototype.resetTabState_ = function(tabBar) {
242 for (var k = 0; k < tabBar.length; k++) {
243 tabBar[k].classList.remove(this.CssClasses_.IS_ACTIVE);
248 * Reset panel state, droping active classes
252 MaterialLayout.prototype.resetPanelState_ = function(panels) {
253 for (var j = 0; j < panels.length; j++) {
254 panels[j].classList.remove(this.CssClasses_.IS_ACTIVE);
259 * Toggle drawer state
263 MaterialLayout.prototype.toggleDrawer = function() {
264 var drawerButton = this.element_.querySelector('.' + this.CssClasses_.DRAWER_BTN);
265 this.drawer_.classList.toggle(this.CssClasses_.IS_DRAWER_OPEN);
266 this.obfuscator_.classList.toggle(this.CssClasses_.IS_DRAWER_OPEN);
268 // Set accessibility properties.
269 if (this.drawer_.classList.contains(this.CssClasses_.IS_DRAWER_OPEN)) {
270 this.drawer_.setAttribute('aria-hidden', 'false');
271 drawerButton.setAttribute('aria-expanded', 'true');
273 this.drawer_.setAttribute('aria-hidden', 'true');
274 drawerButton.setAttribute('aria-expanded', 'false');
277 MaterialLayout.prototype['toggleDrawer'] =
278 MaterialLayout.prototype.toggleDrawer;
281 * Initialize element.
283 MaterialLayout.prototype.init = function() {
285 var container = document.createElement('div');
286 container.classList.add(this.CssClasses_.CONTAINER);
288 var focusedElement = this.element_.querySelector(':focus');
290 this.element_.parentElement.insertBefore(container, this.element_);
291 this.element_.parentElement.removeChild(this.element_);
292 container.appendChild(this.element_);
294 if (focusedElement) {
295 focusedElement.focus();
298 var directChildren = this.element_.childNodes;
299 var numChildren = directChildren.length;
300 for (var c = 0; c < numChildren; c++) {
301 var child = directChildren[c];
302 if (child.classList &&
303 child.classList.contains(this.CssClasses_.HEADER)) {
304 this.header_ = child;
307 if (child.classList &&
308 child.classList.contains(this.CssClasses_.DRAWER)) {
309 this.drawer_ = child;
312 if (child.classList &&
313 child.classList.contains(this.CssClasses_.CONTENT)) {
314 this.content_ = child;
318 window.addEventListener('pageshow', function(e) {
319 if (e.persisted) { // when page is loaded from back/forward cache
320 // trigger repaint to let layout scroll in safari
321 this.element_.style.overflowY = 'hidden';
322 requestAnimationFrame(function() {
323 this.element_.style.overflowY = '';
326 }.bind(this), false);
329 this.tabBar_ = this.header_.querySelector('.' + this.CssClasses_.TAB_BAR);
332 var mode = this.Mode_.STANDARD;
335 if (this.header_.classList.contains(this.CssClasses_.HEADER_SEAMED)) {
336 mode = this.Mode_.SEAMED;
337 } else if (this.header_.classList.contains(
338 this.CssClasses_.HEADER_WATERFALL)) {
339 mode = this.Mode_.WATERFALL;
340 this.header_.addEventListener('transitionend',
341 this.headerTransitionEndHandler_.bind(this));
342 this.header_.addEventListener('click',
343 this.headerClickHandler_.bind(this));
344 } else if (this.header_.classList.contains(
345 this.CssClasses_.HEADER_SCROLL)) {
346 mode = this.Mode_.SCROLL;
347 container.classList.add(this.CssClasses_.HAS_SCROLLING_HEADER);
350 if (mode === this.Mode_.STANDARD) {
351 this.header_.classList.add(this.CssClasses_.CASTING_SHADOW);
353 this.tabBar_.classList.add(this.CssClasses_.CASTING_SHADOW);
355 } else if (mode === this.Mode_.SEAMED || mode === this.Mode_.SCROLL) {
356 this.header_.classList.remove(this.CssClasses_.CASTING_SHADOW);
358 this.tabBar_.classList.remove(this.CssClasses_.CASTING_SHADOW);
360 } else if (mode === this.Mode_.WATERFALL) {
361 // Add and remove shadows depending on scroll position.
362 // Also add/remove auxiliary class for styling of the compact version of
364 this.content_.addEventListener('scroll',
365 this.contentScrollHandler_.bind(this));
366 this.contentScrollHandler_();
370 // Add drawer toggling button to our layout, if we have an openable drawer.
372 var drawerButton = this.element_.querySelector('.' +
373 this.CssClasses_.DRAWER_BTN);
375 drawerButton = document.createElement('div');
376 drawerButton.setAttribute('aria-expanded', 'false');
377 drawerButton.setAttribute('role', 'button');
378 drawerButton.setAttribute('tabindex', '0');
379 drawerButton.classList.add(this.CssClasses_.DRAWER_BTN);
381 var drawerButtonIcon = document.createElement('i');
382 drawerButtonIcon.classList.add(this.CssClasses_.ICON);
383 drawerButtonIcon.innerHTML = this.Constant_.MENU_ICON;
384 drawerButton.appendChild(drawerButtonIcon);
387 if (this.drawer_.classList.contains(this.CssClasses_.ON_LARGE_SCREEN)) {
388 //If drawer has ON_LARGE_SCREEN class then add it to the drawer toggle button as well.
389 drawerButton.classList.add(this.CssClasses_.ON_LARGE_SCREEN);
390 } else if (this.drawer_.classList.contains(this.CssClasses_.ON_SMALL_SCREEN)) {
391 //If drawer has ON_SMALL_SCREEN class then add it to the drawer toggle button as well.
392 drawerButton.classList.add(this.CssClasses_.ON_SMALL_SCREEN);
395 drawerButton.addEventListener('click',
396 this.drawerToggleHandler_.bind(this));
398 drawerButton.addEventListener('keydown',
399 this.drawerToggleHandler_.bind(this));
401 // Add a class if the layout has a drawer, for altering the left padding.
402 // Adds the HAS_DRAWER to the elements since this.header_ may or may
404 this.element_.classList.add(this.CssClasses_.HAS_DRAWER);
406 // If we have a fixed header, add the button to the header rather than
408 if (this.element_.classList.contains(this.CssClasses_.FIXED_HEADER)) {
409 this.header_.insertBefore(drawerButton, this.header_.firstChild);
411 this.element_.insertBefore(drawerButton, this.content_);
414 var obfuscator = document.createElement('div');
415 obfuscator.classList.add(this.CssClasses_.OBFUSCATOR);
416 this.element_.appendChild(obfuscator);
417 obfuscator.addEventListener('click',
418 this.drawerToggleHandler_.bind(this));
419 this.obfuscator_ = obfuscator;
421 this.drawer_.addEventListener('keydown', this.keyboardEventHandler_.bind(this));
422 this.drawer_.setAttribute('aria-hidden', 'true');
425 // Keep an eye on screen size, and add/remove auxiliary class for styling
427 this.screenSizeMediaQuery_ = window.matchMedia(
428 /** @type {string} */ (this.Constant_.MAX_WIDTH));
429 this.screenSizeMediaQuery_.addListener(this.screenSizeHandler_.bind(this));
430 this.screenSizeHandler_();
432 // Initialize tabs, if any.
433 if (this.header_ && this.tabBar_) {
434 this.element_.classList.add(this.CssClasses_.HAS_TABS);
436 var tabContainer = document.createElement('div');
437 tabContainer.classList.add(this.CssClasses_.TAB_CONTAINER);
438 this.header_.insertBefore(tabContainer, this.tabBar_);
439 this.header_.removeChild(this.tabBar_);
441 var leftButton = document.createElement('div');
442 leftButton.classList.add(this.CssClasses_.TAB_BAR_BUTTON);
443 leftButton.classList.add(this.CssClasses_.TAB_BAR_LEFT_BUTTON);
444 var leftButtonIcon = document.createElement('i');
445 leftButtonIcon.classList.add(this.CssClasses_.ICON);
446 leftButtonIcon.textContent = this.Constant_.CHEVRON_LEFT;
447 leftButton.appendChild(leftButtonIcon);
448 leftButton.addEventListener('click', function() {
449 this.tabBar_.scrollLeft -= this.Constant_.TAB_SCROLL_PIXELS;
452 var rightButton = document.createElement('div');
453 rightButton.classList.add(this.CssClasses_.TAB_BAR_BUTTON);
454 rightButton.classList.add(this.CssClasses_.TAB_BAR_RIGHT_BUTTON);
455 var rightButtonIcon = document.createElement('i');
456 rightButtonIcon.classList.add(this.CssClasses_.ICON);
457 rightButtonIcon.textContent = this.Constant_.CHEVRON_RIGHT;
458 rightButton.appendChild(rightButtonIcon);
459 rightButton.addEventListener('click', function() {
460 this.tabBar_.scrollLeft += this.Constant_.TAB_SCROLL_PIXELS;
463 tabContainer.appendChild(leftButton);
464 tabContainer.appendChild(this.tabBar_);
465 tabContainer.appendChild(rightButton);
467 // Add and remove tab buttons depending on scroll position and total
469 var tabUpdateHandler = function() {
470 if (this.tabBar_.scrollLeft > 0) {
471 leftButton.classList.add(this.CssClasses_.IS_ACTIVE);
473 leftButton.classList.remove(this.CssClasses_.IS_ACTIVE);
476 if (this.tabBar_.scrollLeft <
477 this.tabBar_.scrollWidth - this.tabBar_.offsetWidth) {
478 rightButton.classList.add(this.CssClasses_.IS_ACTIVE);
480 rightButton.classList.remove(this.CssClasses_.IS_ACTIVE);
484 this.tabBar_.addEventListener('scroll', tabUpdateHandler);
487 // Update tabs when the window resizes.
488 var windowResizeHandler = function() {
489 // Use timeouts to make sure it doesn't happen too often.
490 if (this.resizeTimeoutId_) {
491 clearTimeout(this.resizeTimeoutId_);
493 this.resizeTimeoutId_ = setTimeout(function() {
495 this.resizeTimeoutId_ = null;
496 }.bind(this), /** @type {number} */ (this.Constant_.RESIZE_TIMEOUT));
499 window.addEventListener('resize', windowResizeHandler);
501 if (this.tabBar_.classList.contains(this.CssClasses_.JS_RIPPLE_EFFECT)) {
502 this.tabBar_.classList.add(this.CssClasses_.RIPPLE_IGNORE_EVENTS);
505 // Select element tabs, document panels
506 var tabs = this.tabBar_.querySelectorAll('.' + this.CssClasses_.TAB);
507 var panels = this.content_.querySelectorAll('.' + this.CssClasses_.PANEL);
509 // Create new tabs for each tab element
510 for (var i = 0; i < tabs.length; i++) {
511 new MaterialLayoutTab(tabs[i], tabs, panels, this);
515 this.element_.classList.add(this.CssClasses_.IS_UPGRADED);
520 * Constructor for an individual tab.
523 * @param {HTMLElement} tab The HTML element for the tab.
524 * @param {!Array<HTMLElement>} tabs Array with HTML elements for all tabs.
525 * @param {!Array<HTMLElement>} panels Array with HTML elements for all panels.
526 * @param {MaterialLayout} layout The MaterialLayout object that owns the tab.
528 function MaterialLayoutTab(tab, tabs, panels, layout) {
531 * Auxiliary method to programmatically select a tab in the UI.
533 function selectTab() {
534 var href = tab.href.split('#')[1];
535 var panel = layout.content_.querySelector('#' + href);
536 layout.resetTabState_(tabs);
537 layout.resetPanelState_(panels);
538 tab.classList.add(layout.CssClasses_.IS_ACTIVE);
539 panel.classList.add(layout.CssClasses_.IS_ACTIVE);
542 if (layout.tabBar_.classList.contains(
543 layout.CssClasses_.JS_RIPPLE_EFFECT)) {
544 var rippleContainer = document.createElement('span');
545 rippleContainer.classList.add(layout.CssClasses_.RIPPLE_CONTAINER);
546 rippleContainer.classList.add(layout.CssClasses_.JS_RIPPLE_EFFECT);
547 var ripple = document.createElement('span');
548 ripple.classList.add(layout.CssClasses_.RIPPLE);
549 rippleContainer.appendChild(ripple);
550 tab.appendChild(rippleContainer);
553 if (!layout.tabBar_.classList.contains(
554 layout.CssClasses_.TAB_MANUAL_SWITCH)) {
555 tab.addEventListener('click', function(e) {
556 if (tab.getAttribute('href').charAt(0) === '#') {
563 tab.show = selectTab;
565 window['MaterialLayoutTab'] = MaterialLayoutTab;
567 // The component registers itself. It can assume componentHandler is available
568 // in the global scope.
569 componentHandler.register({
570 constructor: MaterialLayout,
571 classAsString: 'MaterialLayout',
572 cssClass: 'mdl-js-layout'