Wrap menu in IIFE
parent
d45ab8000c
commit
2ec60aa32b
873
src/menu/menu.js
873
src/menu/menu.js
|
@ -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
|
|
||||||
});
|
|
||||||
|
|
Loading…
Reference in New Issue