Wrap menu in IIFE

master
Surma 2015-08-05 11:49:30 +01:00
parent d45ab8000c
commit 2ec60aa32b
1 changed files with 438 additions and 435 deletions

View File

@ -15,467 +15,470 @@
* limitations under the License. * limitations under the License.
*/ */
/** (function() {
* Class constructor for dropdown MDL component.
* Implements MDL component design pattern defined at:
* https://github.com/jasonmayes/mdl-component-design-pattern
* @param {HTMLElement} element The element that will be upgraded.
*/
function MaterialMenu(element) {
'use strict'; 'use strict';
this.element_ = element; /**
* Class constructor for dropdown MDL component.
* Implements MDL component design pattern defined at:
* https://github.com/jasonmayes/mdl-component-design-pattern
*
* @param {HTMLElement} element The element that will be upgraded.
*/
var MaterialMenu = function MaterialMenu(element) {
this.element_ = element;
// Initialize instance. // Initialize instance.
this.init(); this.init();
} };
window.MaterialMenu = MaterialMenu;
/** /**
* Store constants in one place so they can be updated easily. * Store constants in one place so they can be updated easily.
* @enum {string | number} *
* @private * @enum {String | Number}
*/ * @private
MaterialMenu.prototype.Constant_ = { */
// Total duration of the menu animation. MaterialMenu.prototype.Constant_ = {
TRANSITION_DURATION_SECONDS: 0.3, // Total duration of the menu animation.
// The fraction of the total duration we want to use for menu item animations. TRANSITION_DURATION_SECONDS: 0.3,
TRANSITION_DURATION_FRACTION: 0.8, // The fraction of the total duration we want to use for menu item animations.
// How long the menu stays open after choosing an option (so the user can see TRANSITION_DURATION_FRACTION: 0.8,
// the ripple). // How long the menu stays open after choosing an option (so the user can see
CLOSE_TIMEOUT: 150 // the ripple).
}; CLOSE_TIMEOUT: 150
};
/** /**
* Keycodes, for code readability. * Keycodes, for code readability.
* @enum {number} *
* @private * @enum {Number}
*/ * @private
MaterialMenu.prototype.Keycodes_ = { */
ENTER: 13, MaterialMenu.prototype.Keycodes_ = {
ESCAPE: 27, ENTER: 13,
SPACE: 32, ESCAPE: 27,
UP_ARROW: 38, SPACE: 32,
DOWN_ARROW: 40 UP_ARROW: 38,
}; DOWN_ARROW: 40
};
/** /**
* Store strings for class names defined by this component that are used in * Store strings for class names defined by this component that are used in
* JavaScript. This allows us to simply change it in one place should we * JavaScript. This allows us to simply change it in one place should we
* decide to modify at a later date. * decide to modify at a later date.
* @enum {string} *
* @private * @enum {String}
*/ * @private
MaterialMenu.prototype.CssClasses_ = { */
CONTAINER: 'mdl-menu__container', MaterialMenu.prototype.CssClasses_ = {
OUTLINE: 'mdl-menu__outline', CONTAINER: 'mdl-menu__container',
ITEM: 'mdl-menu__item', OUTLINE: 'mdl-menu__outline',
ITEM_RIPPLE_CONTAINER: 'mdl-menu__item-ripple-container', ITEM: 'mdl-menu__item',
RIPPLE_EFFECT: 'mdl-js-ripple-effect', ITEM_RIPPLE_CONTAINER: 'mdl-menu__item-ripple-container',
RIPPLE_IGNORE_EVENTS: 'mdl-js-ripple-effect--ignore-events', RIPPLE_EFFECT: 'mdl-js-ripple-effect',
RIPPLE: 'mdl-ripple', RIPPLE_IGNORE_EVENTS: 'mdl-js-ripple-effect--ignore-events',
// Statuses RIPPLE: 'mdl-ripple',
IS_UPGRADED: 'is-upgraded', // Statuses
IS_VISIBLE: 'is-visible', IS_UPGRADED: 'is-upgraded',
IS_ANIMATING: 'is-animating', IS_VISIBLE: 'is-visible',
// Alignment options IS_ANIMATING: 'is-animating',
BOTTOM_LEFT: 'mdl-menu--bottom-left', // This is the default. // Alignment options
BOTTOM_RIGHT: 'mdl-menu--bottom-right', BOTTOM_LEFT: 'mdl-menu--bottom-left', // This is the default.
TOP_LEFT: 'mdl-menu--top-left', BOTTOM_RIGHT: 'mdl-menu--bottom-right',
TOP_RIGHT: 'mdl-menu--top-right', TOP_LEFT: 'mdl-menu--top-left',
UNALIGNED: 'mdl-menu--unaligned' TOP_RIGHT: 'mdl-menu--top-right',
}; UNALIGNED: 'mdl-menu--unaligned'
};
/** /**
* Initialize element. * Initialize element.
*/ */
MaterialMenu.prototype.init = function() { MaterialMenu.prototype.init = function() {
'use strict'; if (this.element_) {
// Create container for the menu.
var container = document.createElement('div');
container.classList.add(this.CssClasses_.CONTAINER);
this.element_.parentElement.insertBefore(container, this.element_);
this.element_.parentElement.removeChild(this.element_);
container.appendChild(this.element_);
this.container_ = container;
if (this.element_) { // Create outline for the menu (shadow and background).
// Create container for the menu. var outline = document.createElement('div');
var container = document.createElement('div'); outline.classList.add(this.CssClasses_.OUTLINE);
container.classList.add(this.CssClasses_.CONTAINER); this.outline_ = outline;
this.element_.parentElement.insertBefore(container, this.element_); container.insertBefore(outline, this.element_);
this.element_.parentElement.removeChild(this.element_);
container.appendChild(this.element_);
this.container_ = container;
// Create outline for the menu (shadow and background). // Find the "for" element and bind events to it.
var outline = document.createElement('div'); var forElId = this.element_.getAttribute('for');
outline.classList.add(this.CssClasses_.OUTLINE); var forEl = null;
this.outline_ = outline; if (forElId) {
container.insertBefore(outline, this.element_); forEl = document.getElementById(forElId);
if (forEl) {
// Find the "for" element and bind events to it. this.forElement_ = forEl;
var forElId = this.element_.getAttribute('for'); forEl.addEventListener('click', this.handleForClick_.bind(this));
var forEl = null; forEl.addEventListener('keydown',
if (forElId) { this.handleForKeyboardEvent_.bind(this));
forEl = document.getElementById(forElId);
if (forEl) {
this.forElement_ = forEl;
forEl.addEventListener('click', this.handleForClick_.bind(this));
forEl.addEventListener('keydown',
this.handleForKeyboardEvent_.bind(this));
}
}
var items = this.element_.querySelectorAll('.' + this.CssClasses_.ITEM);
this.boundItemKeydown = this.handleItemKeyboardEvent_.bind(this);
this.boundItemClick = this.handleItemClick_.bind(this);
for (var i = 0; i < items.length; i++) {
// Add a listener to each menu item.
items[i].addEventListener('click', this.boundItemClick);
// Add a tab index to each menu item.
items[i].tabIndex = '-1';
// Add a keyboard listener to each menu item.
items[i].addEventListener('keydown', this.boundItemKeydown);
}
// Add ripple classes to each item, if the user has enabled ripples.
if (this.element_.classList.contains(this.CssClasses_.RIPPLE_EFFECT)) {
this.element_.classList.add(this.CssClasses_.RIPPLE_IGNORE_EVENTS);
for (i = 0; i < items.length; i++) {
var item = items[i];
var rippleContainer = document.createElement('span');
rippleContainer.classList.add(this.CssClasses_.ITEM_RIPPLE_CONTAINER);
var ripple = document.createElement('span');
ripple.classList.add(this.CssClasses_.RIPPLE);
rippleContainer.appendChild(ripple);
item.appendChild(rippleContainer);
item.classList.add(this.CssClasses_.RIPPLE_EFFECT);
}
}
// Copy alignment classes to the container, so the outline can use them.
if (this.element_.classList.contains(this.CssClasses_.BOTTOM_LEFT)) {
this.outline_.classList.add(this.CssClasses_.BOTTOM_LEFT);
}
if (this.element_.classList.contains(this.CssClasses_.BOTTOM_RIGHT)) {
this.outline_.classList.add(this.CssClasses_.BOTTOM_RIGHT);
}
if (this.element_.classList.contains(this.CssClasses_.TOP_LEFT)) {
this.outline_.classList.add(this.CssClasses_.TOP_LEFT);
}
if (this.element_.classList.contains(this.CssClasses_.TOP_RIGHT)) {
this.outline_.classList.add(this.CssClasses_.TOP_RIGHT);
}
if (this.element_.classList.contains(this.CssClasses_.UNALIGNED)) {
this.outline_.classList.add(this.CssClasses_.UNALIGNED);
}
container.classList.add(this.CssClasses_.IS_UPGRADED);
}
};
/**
* Handles a click on the "for" element, by positioning the menu and then
* toggling it.
* @private
*/
MaterialMenu.prototype.handleForClick_ = function(evt) {
'use strict';
if (this.element_ && this.forElement_) {
var rect = this.forElement_.getBoundingClientRect();
var forRect = this.forElement_.parentElement.getBoundingClientRect();
if (this.element_.classList.contains(this.CssClasses_.UNALIGNED)) {
// Do not position the menu automatically. Requires the developer to
// manually specify position.
} else if (this.element_.classList.contains(
this.CssClasses_.BOTTOM_RIGHT)) {
// Position below the "for" element, aligned to its right.
this.container_.style.right = (forRect.right - rect.right) + 'px';
this.container_.style.top =
this.forElement_.offsetTop + this.forElement_.offsetHeight + 'px';
} else if (this.element_.classList.contains(this.CssClasses_.TOP_LEFT)) {
// Position above the "for" element, aligned to its left.
this.container_.style.left = this.forElement_.offsetLeft + 'px';
this.container_.style.bottom = (forRect.bottom - rect.top) + 'px';
} else if (this.element_.classList.contains(this.CssClasses_.TOP_RIGHT)) {
// Position above the "for" element, aligned to its right.
this.container_.style.right = (forRect.right - rect.right) + 'px';
this.container_.style.bottom = (forRect.bottom - rect.top) + 'px';
} else {
// Default: position below the "for" element, aligned to its left.
this.container_.style.left = this.forElement_.offsetLeft + 'px';
this.container_.style.top =
this.forElement_.offsetTop + this.forElement_.offsetHeight + 'px';
}
}
this.toggle(evt);
};
/**
* Handles a keyboard event on the "for" element.
* @private
*/
MaterialMenu.prototype.handleForKeyboardEvent_ = function(evt) {
'use strict';
if (this.element_ && this.container_ && this.forElement_) {
var items = this.element_.querySelectorAll('.' + this.CssClasses_.ITEM +
':not([disabled])');
if (items && items.length > 0 &&
this.container_.classList.contains(this.CssClasses_.IS_VISIBLE)) {
if (evt.keyCode === this.Keycodes_.UP_ARROW) {
evt.preventDefault();
items[items.length - 1].focus();
} else if (evt.keyCode === this.Keycodes_.DOWN_ARROW) {
evt.preventDefault();
items[0].focus();
}
}
}
};
/**
* Handles a keyboard event on an item.
* @private
*/
MaterialMenu.prototype.handleItemKeyboardEvent_ = function(evt) {
'use strict';
if (this.element_ && this.container_) {
var items = this.element_.querySelectorAll('.' + this.CssClasses_.ITEM +
':not([disabled])');
if (items && items.length > 0 &&
this.container_.classList.contains(this.CssClasses_.IS_VISIBLE)) {
var currentIndex = Array.prototype.slice.call(items).indexOf(evt.target);
if (evt.keyCode === this.Keycodes_.UP_ARROW) {
evt.preventDefault();
if (currentIndex > 0) {
items[currentIndex - 1].focus();
} else {
items[items.length - 1].focus();
} }
} else if (evt.keyCode === this.Keycodes_.DOWN_ARROW) { }
evt.preventDefault();
if (items.length > currentIndex + 1) { var items = this.element_.querySelectorAll('.' + this.CssClasses_.ITEM);
items[currentIndex + 1].focus(); this.boundItemKeydown = this.handleItemKeyboardEvent_.bind(this);
} else { this.boundItemClick = this.handleItemClick_.bind(this);
for (var i = 0; i < items.length; i++) {
// Add a listener to each menu item.
items[i].addEventListener('click', this.boundItemClick);
// Add a tab index to each menu item.
items[i].tabIndex = '-1';
// Add a keyboard listener to each menu item.
items[i].addEventListener('keydown', this.boundItemKeydown);
}
// Add ripple classes to each item, if the user has enabled ripples.
if (this.element_.classList.contains(this.CssClasses_.RIPPLE_EFFECT)) {
this.element_.classList.add(this.CssClasses_.RIPPLE_IGNORE_EVENTS);
for (i = 0; i < items.length; i++) {
var item = items[i];
var rippleContainer = document.createElement('span');
rippleContainer.classList.add(this.CssClasses_.ITEM_RIPPLE_CONTAINER);
var ripple = document.createElement('span');
ripple.classList.add(this.CssClasses_.RIPPLE);
rippleContainer.appendChild(ripple);
item.appendChild(rippleContainer);
item.classList.add(this.CssClasses_.RIPPLE_EFFECT);
}
}
// Copy alignment classes to the container, so the outline can use them.
if (this.element_.classList.contains(this.CssClasses_.BOTTOM_LEFT)) {
this.outline_.classList.add(this.CssClasses_.BOTTOM_LEFT);
}
if (this.element_.classList.contains(this.CssClasses_.BOTTOM_RIGHT)) {
this.outline_.classList.add(this.CssClasses_.BOTTOM_RIGHT);
}
if (this.element_.classList.contains(this.CssClasses_.TOP_LEFT)) {
this.outline_.classList.add(this.CssClasses_.TOP_LEFT);
}
if (this.element_.classList.contains(this.CssClasses_.TOP_RIGHT)) {
this.outline_.classList.add(this.CssClasses_.TOP_RIGHT);
}
if (this.element_.classList.contains(this.CssClasses_.UNALIGNED)) {
this.outline_.classList.add(this.CssClasses_.UNALIGNED);
}
container.classList.add(this.CssClasses_.IS_UPGRADED);
}
};
/**
* Handles a click on the "for" element, by positioning the menu and then
* toggling it.
*
* @param {Event} evt The event that fired.
* @private
*/
MaterialMenu.prototype.handleForClick_ = function(evt) {
if (this.element_ && this.forElement_) {
var rect = this.forElement_.getBoundingClientRect();
var forRect = this.forElement_.parentElement.getBoundingClientRect();
if (this.element_.classList.contains(this.CssClasses_.UNALIGNED)) {
// Do not position the menu automatically. Requires the developer to
// manually specify position.
} else if (this.element_.classList.contains(
this.CssClasses_.BOTTOM_RIGHT)) {
// Position below the "for" element, aligned to its right.
this.container_.style.right = (forRect.right - rect.right) + 'px';
this.container_.style.top =
this.forElement_.offsetTop + this.forElement_.offsetHeight + 'px';
} else if (this.element_.classList.contains(this.CssClasses_.TOP_LEFT)) {
// Position above the "for" element, aligned to its left.
this.container_.style.left = this.forElement_.offsetLeft + 'px';
this.container_.style.bottom = (forRect.bottom - rect.top) + 'px';
} else if (this.element_.classList.contains(this.CssClasses_.TOP_RIGHT)) {
// Position above the "for" element, aligned to its right.
this.container_.style.right = (forRect.right - rect.right) + 'px';
this.container_.style.bottom = (forRect.bottom - rect.top) + 'px';
} else {
// Default: position below the "for" element, aligned to its left.
this.container_.style.left = this.forElement_.offsetLeft + 'px';
this.container_.style.top =
this.forElement_.offsetTop + this.forElement_.offsetHeight + 'px';
}
}
this.toggle(evt);
};
/**
* Handles a keyboard event on the "for" element.
*
* @param {Event} evt The event that fired.
* @private
*/
MaterialMenu.prototype.handleForKeyboardEvent_ = function(evt) {
if (this.element_ && this.container_ && this.forElement_) {
var items = this.element_.querySelectorAll('.' + this.CssClasses_.ITEM +
':not([disabled])');
if (items && items.length > 0 &&
this.container_.classList.contains(this.CssClasses_.IS_VISIBLE)) {
if (evt.keyCode === this.Keycodes_.UP_ARROW) {
evt.preventDefault();
items[items.length - 1].focus();
} else if (evt.keyCode === this.Keycodes_.DOWN_ARROW) {
evt.preventDefault();
items[0].focus(); items[0].focus();
} }
} else if (evt.keyCode === this.Keycodes_.SPACE ||
evt.keyCode === this.Keycodes_.ENTER) {
evt.preventDefault();
// Send mousedown and mouseup to trigger ripple.
var e = new MouseEvent('mousedown');
evt.target.dispatchEvent(e);
e = new MouseEvent('mouseup');
evt.target.dispatchEvent(e);
// Send click.
evt.target.click();
} else if (evt.keyCode === this.Keycodes_.ESCAPE) {
evt.preventDefault();
this.hide();
} }
} }
} };
};
/** /**
* Handles a click event on an item. * Handles a keyboard event on an item.
* @private *
*/ * @param {Event} evt The event that fired.
MaterialMenu.prototype.handleItemClick_ = function(evt) { * @private
'use strict'; */
MaterialMenu.prototype.handleItemKeyboardEvent_ = function(evt) {
if (this.element_ && this.container_) {
var items = this.element_.querySelectorAll('.' + this.CssClasses_.ITEM +
':not([disabled])');
if (evt.target.getAttribute('disabled') !== null) { if (items && items.length > 0 &&
evt.stopPropagation(); this.container_.classList.contains(this.CssClasses_.IS_VISIBLE)) {
} else { var currentIndex = Array.prototype.slice.call(items).indexOf(evt.target);
// Wait some time before closing menu, so the user can see the ripple.
this.closing_ = true;
window.setTimeout(function(evt) {
this.hide();
this.closing_ = false;
}.bind(this), this.Constant_.CLOSE_TIMEOUT);
}
};
/** if (evt.keyCode === this.Keycodes_.UP_ARROW) {
* Calculates the initial clip (for opening the menu) or final clip (for closing evt.preventDefault();
* it), and applies it. This allows us to animate from or to the correct point, if (currentIndex > 0) {
* that is, the point it's aligned to in the "for" element. items[currentIndex - 1].focus();
* @private } else {
*/ items[items.length - 1].focus();
MaterialMenu.prototype.applyClip_ = function(height, width) { }
'use strict'; } else if (evt.keyCode === this.Keycodes_.DOWN_ARROW) {
evt.preventDefault();
if (this.element_.classList.contains(this.CssClasses_.UNALIGNED)) { if (items.length > currentIndex + 1) {
// Do not clip. items[currentIndex + 1].focus();
this.element_.style.clip = null; } else {
} else if (this.element_.classList.contains(this.CssClasses_.BOTTOM_RIGHT)) { items[0].focus();
// Clip to the top right corner of the menu. }
this.element_.style.clip = } else if (evt.keyCode === this.Keycodes_.SPACE ||
'rect(0 ' + width + 'px ' + '0 ' + width + 'px)'; evt.keyCode === this.Keycodes_.ENTER) {
} else if (this.element_.classList.contains(this.CssClasses_.TOP_LEFT)) { evt.preventDefault();
// Clip to the bottom left corner of the menu. // Send mousedown and mouseup to trigger ripple.
this.element_.style.clip = var e = new MouseEvent('mousedown');
'rect(' + height + 'px 0 ' + height + 'px 0)'; evt.target.dispatchEvent(e);
} else if (this.element_.classList.contains(this.CssClasses_.TOP_RIGHT)) { e = new MouseEvent('mouseup');
// Clip to the bottom right corner of the menu. evt.target.dispatchEvent(e);
this.element_.style.clip = 'rect(' + height + 'px ' + width + 'px ' + // Send click.
height + 'px ' + width + 'px)'; evt.target.click();
} else { } else if (evt.keyCode === this.Keycodes_.ESCAPE) {
// Default: do not clip (same as clipping to the top left corner). evt.preventDefault();
this.element_.style.clip = null; this.hide();
} }
};
/**
* Adds an event listener to clean up after the animation ends.
* @private
*/
MaterialMenu.prototype.addAnimationEndListener_ = function() {
'use strict';
var cleanup = function () {
this.element_.removeEventListener('transitionend', cleanup);
this.element_.removeEventListener('webkitTransitionEnd', cleanup);
this.element_.classList.remove(this.CssClasses_.IS_ANIMATING);
}.bind(this);
// Remove animation class once the transition is done.
this.element_.addEventListener('transitionend', cleanup);
this.element_.addEventListener('webkitTransitionEnd', cleanup);
};
/**
* Displays the menu.
* @public
*/
MaterialMenu.prototype.show = function(evt) {
'use strict';
if (this.element_ && this.container_ && this.outline_) {
// Measure the inner element.
var height = this.element_.getBoundingClientRect().height;
var width = this.element_.getBoundingClientRect().width;
// Apply the inner element's size to the container and outline.
this.container_.style.width = width + 'px';
this.container_.style.height = height + 'px';
this.outline_.style.width = width + 'px';
this.outline_.style.height = height + 'px';
var transitionDuration = this.Constant_.TRANSITION_DURATION_SECONDS *
this.Constant_.TRANSITION_DURATION_FRACTION;
// Calculate transition delays for individual menu items, so that they fade
// in one at a time.
var items = this.element_.querySelectorAll('.' + this.CssClasses_.ITEM);
for (var i = 0; i < items.length; i++) {
var itemDelay = null;
if (this.element_.classList.contains(this.CssClasses_.TOP_LEFT) ||
this.element_.classList.contains(this.CssClasses_.TOP_RIGHT)) {
itemDelay = ((height - items[i].offsetTop - items[i].offsetHeight) /
height * transitionDuration) + 's';
} else {
itemDelay = (items[i].offsetTop / height * transitionDuration) + 's';
} }
items[i].style.transitionDelay = itemDelay;
} }
};
// Apply the initial clip to the text before we start animating. /**
this.applyClip_(height, width); * Handles a click event on an item.
*
// Wait for the next frame, turn on animation, and apply the final clip. * @param {Event} evt The event that fired.
// Also make it visible. This triggers the transitions. * @private
window.requestAnimationFrame(function() { */
this.element_.classList.add(this.CssClasses_.IS_ANIMATING); MaterialMenu.prototype.handleItemClick_ = function(evt) {
this.element_.style.clip = 'rect(0 ' + width + 'px ' + height + 'px 0)'; if (evt.target.getAttribute('disabled') !== null) {
this.container_.classList.add(this.CssClasses_.IS_VISIBLE); evt.stopPropagation();
}.bind(this)); } else {
// Wait some time before closing menu, so the user can see the ripple.
// Clean up after the animation is complete. this.closing_ = true;
this.addAnimationEndListener_(); window.setTimeout(function(evt) {
// Add a click listener to the document, to close the menu.
var callback = function(e) {
// Check to see if the document is processing the same event that
// displayed the menu in the first place. If so, do nothing.
// Also check to see if the menu is in the process of closing itself, and
// do nothing in that case.
if (e !== evt && !this.closing_) {
document.removeEventListener('click', callback);
this.hide(); this.hide();
} this.closing_ = false;
}.bind(this), this.Constant_.CLOSE_TIMEOUT);
}
};
/**
* Calculates the initial clip (for opening the menu) or final clip (for closing
* it), and applies it. This allows us to animate from or to the correct point,
* that is, the point it's aligned to in the "for" element.
*
* @param {Number} height Height of the clip rectangle
* @param {Number} width Width of the clip rectangle
* @private
*/
MaterialMenu.prototype.applyClip_ = function(height, width) {
if (this.element_.classList.contains(this.CssClasses_.UNALIGNED)) {
// Do not clip.
this.element_.style.clip = null;
} else if (this.element_.classList.contains(this.CssClasses_.BOTTOM_RIGHT)) {
// Clip to the top right corner of the menu.
this.element_.style.clip =
'rect(0 ' + width + 'px ' + '0 ' + width + 'px)';
} else if (this.element_.classList.contains(this.CssClasses_.TOP_LEFT)) {
// Clip to the bottom left corner of the menu.
this.element_.style.clip =
'rect(' + height + 'px 0 ' + height + 'px 0)';
} else if (this.element_.classList.contains(this.CssClasses_.TOP_RIGHT)) {
// Clip to the bottom right corner of the menu.
this.element_.style.clip = 'rect(' + height + 'px ' + width + 'px ' +
height + 'px ' + width + 'px)';
} else {
// Default: do not clip (same as clipping to the top left corner).
this.element_.style.clip = null;
}
};
/**
* Adds an event listener to clean up after the animation ends.
*
* @private
*/
MaterialMenu.prototype.addAnimationEndListener_ = function() {
var cleanup = function() {
this.element_.removeEventListener('transitionend', cleanup);
this.element_.removeEventListener('webkitTransitionEnd', cleanup);
this.element_.classList.remove(this.CssClasses_.IS_ANIMATING);
}.bind(this); }.bind(this);
document.addEventListener('click', callback);
}
};
/** // Remove animation class once the transition is done.
* Hides the menu. this.element_.addEventListener('transitionend', cleanup);
* @public this.element_.addEventListener('webkitTransitionEnd', cleanup);
*/ };
MaterialMenu.prototype.hide = function() {
'use strict';
if (this.element_ && this.container_ && this.outline_) { /**
* Displays the menu.
*
* @public
*/
MaterialMenu.prototype.show = function(evt) {
if (this.element_ && this.container_ && this.outline_) {
// Measure the inner element.
var height = this.element_.getBoundingClientRect().height;
var width = this.element_.getBoundingClientRect().width;
// Apply the inner element's size to the container and outline.
this.container_.style.width = width + 'px';
this.container_.style.height = height + 'px';
this.outline_.style.width = width + 'px';
this.outline_.style.height = height + 'px';
var transitionDuration = this.Constant_.TRANSITION_DURATION_SECONDS *
this.Constant_.TRANSITION_DURATION_FRACTION;
// Calculate transition delays for individual menu items, so that they fade
// in one at a time.
var items = this.element_.querySelectorAll('.' + this.CssClasses_.ITEM);
for (var i = 0; i < items.length; i++) {
var itemDelay = null;
if (this.element_.classList.contains(this.CssClasses_.TOP_LEFT) ||
this.element_.classList.contains(this.CssClasses_.TOP_RIGHT)) {
itemDelay = ((height - items[i].offsetTop - items[i].offsetHeight) /
height * transitionDuration) + 's';
} else {
itemDelay = (items[i].offsetTop / height * transitionDuration) + 's';
}
items[i].style.transitionDelay = itemDelay;
}
// Apply the initial clip to the text before we start animating.
this.applyClip_(height, width);
// Wait for the next frame, turn on animation, and apply the final clip.
// Also make it visible. This triggers the transitions.
window.requestAnimationFrame(function() {
this.element_.classList.add(this.CssClasses_.IS_ANIMATING);
this.element_.style.clip = 'rect(0 ' + width + 'px ' + height + 'px 0)';
this.container_.classList.add(this.CssClasses_.IS_VISIBLE);
}.bind(this));
// Clean up after the animation is complete.
this.addAnimationEndListener_();
// Add a click listener to the document, to close the menu.
var callback = function(e) {
// Check to see if the document is processing the same event that
// displayed the menu in the first place. If so, do nothing.
// Also check to see if the menu is in the process of closing itself, and
// do nothing in that case.
if (e !== evt && !this.closing_) {
document.removeEventListener('click', callback);
this.hide();
}
}.bind(this);
document.addEventListener('click', callback);
}
};
/**
* Hides the menu.
*
* @public
*/
MaterialMenu.prototype.hide = function() {
if (this.element_ && this.container_ && this.outline_) {
var items = this.element_.querySelectorAll('.' + this.CssClasses_.ITEM);
// Remove all transition delays; menu items fade out concurrently.
for (var i = 0; i < items.length; i++) {
items[i].style.transitionDelay = null;
}
// Measure the inner element.
var height = this.element_.getBoundingClientRect().height;
var width = this.element_.getBoundingClientRect().width;
// Turn on animation, and apply the final clip. Also make invisible.
// This triggers the transitions.
this.element_.classList.add(this.CssClasses_.IS_ANIMATING);
this.applyClip_(height, width);
this.container_.classList.remove(this.CssClasses_.IS_VISIBLE);
// Clean up after the animation is complete.
this.addAnimationEndListener_();
}
};
/**
* Displays or hides the menu, depending on current state.
*
* @public
*/
MaterialMenu.prototype.toggle = function(evt) {
if (this.container_.classList.contains(this.CssClasses_.IS_VISIBLE)) {
this.hide();
} else {
this.show(evt);
}
};
/**
* Downgrade the component.
*
* @private
*/
MaterialMenu.prototype.mdlDowngrade_ = function() {
var items = this.element_.querySelectorAll('.' + this.CssClasses_.ITEM); var items = this.element_.querySelectorAll('.' + this.CssClasses_.ITEM);
// Remove all transition delays; menu items fade out concurrently.
for (var i = 0; i < items.length; i++) { for (var i = 0; i < items.length; i++) {
items[i].style.transitionDelay = null; items[i].removeEventListener('click', this.boundItemClick);
items[i].removeEventListener('keydown', this.boundItemKeydown);
} }
};
// Measure the inner element. // The component registers itself. It can assume componentHandler is available
var height = this.element_.getBoundingClientRect().height; // in the global scope.
var width = this.element_.getBoundingClientRect().width; componentHandler.register({
constructor: MaterialMenu,
// Turn on animation, and apply the final clip. Also make invisible. classAsString: 'MaterialMenu',
// This triggers the transitions. cssClass: 'mdl-js-menu',
this.element_.classList.add(this.CssClasses_.IS_ANIMATING); widget: true
this.applyClip_(height, width); });
this.container_.classList.remove(this.CssClasses_.IS_VISIBLE); })();
// Clean up after the animation is complete.
this.addAnimationEndListener_();
}
};
/**
* Displays or hides the menu, depending on current state.
* @public
*/
MaterialMenu.prototype.toggle = function(evt) {
'use strict';
if (this.container_.classList.contains(this.CssClasses_.IS_VISIBLE)) {
this.hide();
} else {
this.show(evt);
}
};
/*
* Downgrade the component.
*/
MaterialMenu.prototype.mdlDowngrade_ = function() {
'use strict';
var items = this.element_.querySelectorAll('.' + this.CssClasses_.ITEM);
for (var i = 0; i < items.length; i++) {
items[i].removeEventListener('click', this.boundItemClick);
items[i].removeEventListener('keydown', this.boundItemKeydown);
}
};
// The component registers itself. It can assume componentHandler is available
// in the global scope.
componentHandler.register({
constructor: MaterialMenu,
classAsString: 'MaterialMenu',
cssClass: 'mdl-js-menu',
widget: true
});