blob: 9f7ed174718631eb61394595b68167c682ba6fde [file] [log] [blame]
'use strict';
var _classCallCheck2 = require('babel-runtime/helpers/classCallCheck');
var _classCallCheck3 = _interopRequireDefault(_classCallCheck2);
var _createClass2 = require('babel-runtime/helpers/createClass');
var _createClass3 = _interopRequireDefault(_createClass2);
var _toConsumableArray2 = require('babel-runtime/helpers/toConsumableArray');
var _toConsumableArray3 = _interopRequireDefault(_toConsumableArray2);
var _stringUtils = require('../utils/string-utils');
var _fullThrottle = require('../utils/full-throttle');
var _fullThrottle2 = _interopRequireDefault(_fullThrottle);
var _constants = require('../utils/constants');
var _domUtils = require('../utils/dom-utils');
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
/**
* @license
* Copyright 2016 Leif Olsen. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* This code is built with Google Material Design Lite,
* which is Licensed under the Apache License, Version 2.0
*/
/**
* A menu button is a button that opens a menu. It is often styled as a
* typical push button with a downward pointing arrow or triangle to hint
* that activating the button will display a menu.
*/
var JS_MENU_BUTTON = 'mdlext-js-menu-button';
var MENU_BUTTON_MENU = 'mdlext-menu';
var MENU_BUTTON_MENU_ITEM = 'mdlext-menu__item';
var MENU_BUTTON_MENU_ITEM_SEPARATOR = 'mdlext-menu__item-separator';
//const MDL_LAYOUT_CONTENT = 'mdl-layout__content';
/**
* Creates the menu controlled by the menu button
* @param element
* @return {{element: Element, selected: Element, open: (function(*=)), removeListeners: (function()), downgrade: (function())}}
*/
var menuFactory = function menuFactory(element) {
var ariaControls = null;
var parentNode = null;
var removeAllSelected = function removeAllSelected() {
[].concat((0, _toConsumableArray3.default)(element.querySelectorAll('.' + MENU_BUTTON_MENU_ITEM + '[aria-selected="true"]'))).forEach(function (selectedItem) {
return selectedItem.removeAttribute('aria-selected');
});
};
var setSelected = function setSelected(item) {
var force = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
if (force || item && !item.hasAttribute('aria-selected')) {
removeAllSelected();
if (item) {
item.setAttribute('aria-selected', 'true');
}
}
};
var getSelected = function getSelected() {
return element.querySelector('.' + MENU_BUTTON_MENU_ITEM + '[aria-selected="true"]');
};
var isDisabled = function isDisabled(item) {
return item && item.hasAttribute('disabled');
};
var isSeparator = function isSeparator(item) {
return item && item.classList.contains(MENU_BUTTON_MENU_ITEM_SEPARATOR);
};
var focus = function focus(item) {
if (item) {
item = item.closest('.' + MENU_BUTTON_MENU_ITEM);
}
if (item) {
item.focus();
}
};
var nextItem = function nextItem(current) {
var n = current.nextElementSibling;
if (!n) {
n = element.firstElementChild;
}
if (!isDisabled(n) && !isSeparator(n)) {
focus(n);
} else {
var i = element.children.length;
while (n && i-- > 0) {
if (isDisabled(n) || isSeparator(n)) {
n = n.nextElementSibling;
if (!n) {
n = element.firstElementChild;
}
} else {
focus(n);
break;
}
}
}
};
var previousItem = function previousItem(current) {
var p = current.previousElementSibling;
if (!p) {
p = element.lastElementChild;
}
if (!isDisabled(p) && !isSeparator(p)) {
focus(p);
} else {
var i = element.children.length;
while (p && i-- > 0) {
if (isDisabled(p) || isSeparator(p)) {
p = p.previousElementSibling;
if (!p) {
p = element.lastElementChild;
}
} else {
focus(p);
break;
}
}
}
};
var firstItem = function firstItem() {
var item = element.firstElementChild;
if (isDisabled(item) || isSeparator(item)) {
nextItem(item);
} else {
focus(item);
}
};
var lastItem = function lastItem() {
var item = element.lastElementChild;
if (isDisabled(item) || isSeparator(item)) {
previousItem(item);
} else {
focus(item);
}
};
var selectItem = function selectItem(item) {
if (item && !isDisabled(item) && !isSeparator(item)) {
setSelected(item);
close(true, item);
}
};
var keyDownHandler = function keyDownHandler(event) {
var item = event.target.closest('.' + MENU_BUTTON_MENU_ITEM);
switch (event.keyCode) {
case _constants.VK_ARROW_UP:
case _constants.VK_ARROW_LEFT:
if (item) {
previousItem(item);
} else {
firstItem();
}
break;
case _constants.VK_ARROW_DOWN:
case _constants.VK_ARROW_RIGHT:
if (item) {
nextItem(item);
} else {
lastItem();
}
break;
case _constants.VK_HOME:
firstItem();
break;
case _constants.VK_END:
lastItem();
break;
case _constants.VK_SPACE:
case _constants.VK_ENTER:
selectItem(item);
break;
case _constants.VK_ESC:
close(true);
break;
case _constants.VK_TAB:
// We do not have a "natural" tab order from menu, so the best we can do is to set focus back to the button
close(true);
break;
default:
return;
}
event.preventDefault();
};
var blurHandler = function blurHandler(event) {
// See: https://github.com/facebook/react/issues/2011
var t = event.relatedTarget || event.explicitOriginalTarget || // FF
document.activeElement; // IE11
//console.log('***** blur, target, relatedTarget', event.target, t);
try {
if (t) {
if (t.closest('.' + MENU_BUTTON_MENU) !== element && shouldClose(t)) {
close();
}
} else {
close();
}
} catch (err) {
// FF throws error: "TypeError: n.closest is not a function" if related target is a text node
close();
}
};
var clickHandler = function clickHandler(event) {
//console.log('***** click, target', event.target);
event.preventDefault();
var t = event.target;
if (t && t.closest('.' + MENU_BUTTON_MENU) === element) {
var item = t.closest('.' + MENU_BUTTON_MENU_ITEM);
if (item) {
selectItem(item);
}
} else {
if (shouldClose(t)) {
close();
}
}
};
var touchStartHandler = function touchStartHandler(event) {
//console.log('***** touchStart, target', event.target);
var t = event.target;
if (!(t && t.closest('.' + MENU_BUTTON_MENU) === element)) {
if (event.type === 'touchstart') {
event.preventDefault();
}
close();
}
};
var addListeners = function addListeners() {
element.addEventListener('keydown', keyDownHandler, false);
element.addEventListener('blur', blurHandler, true);
element.addEventListener('click', clickHandler, true);
document.documentElement.addEventListener('touchstart', touchStartHandler, true);
};
var _removeListeners = function _removeListeners() {
element.removeEventListener('keydown', keyDownHandler, false);
element.removeEventListener('blur', blurHandler, true);
element.removeEventListener('click', clickHandler, true);
document.documentElement.removeEventListener('touchstart', touchStartHandler, true);
};
var _open = function _open(controlElement) {
var position = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'first';
ariaControls = controlElement.closest('.' + JS_MENU_BUTTON);
element.style['min-width'] = Math.max(124, controlElement.getBoundingClientRect().width) + 'px';
element.removeAttribute('hidden');
(0, _domUtils.tether)(controlElement, element);
var item = void 0;
switch (position.toLowerCase()) {
case 'first':
firstItem();
break;
case 'last':
lastItem();
break;
case 'selected':
item = getSelected();
if (item && !item.hasAttribute('disabled')) {
focus(item);
} else {
firstItem();
}
break;
}
addListeners();
};
var shouldClose = function shouldClose(target) {
//console.log('***** shouldClose');
var result = false;
var btn = target && target.closest('.' + JS_MENU_BUTTON) || null;
if (!btn) {
result = true;
} else if (btn.getAttribute('aria-controls') === element.id) {
if (btn !== ariaControls) {
result = true;
}
} else {
result = true;
}
return result;
};
var close = function close() {
var forceFocus = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false;
var item = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null;
_removeListeners();
element.dispatchEvent(new CustomEvent('_closemenu', {
bubbles: true,
cancelable: true,
detail: { forceFocus: forceFocus, item: item }
}));
};
var addWaiAria = function addWaiAria() {
if (!element.hasAttribute('id')) {
// Generate a random id
element.id = 'menu-button-' + (0, _stringUtils.randomString)();
}
element.setAttribute('tabindex', '-1');
element.setAttribute('role', 'menu');
element.setAttribute('hidden', '');
[].concat((0, _toConsumableArray3.default)(element.querySelectorAll('.' + MENU_BUTTON_MENU_ITEM))).forEach(function (menuitem) {
menuitem.setAttribute('tabindex', '-1');
menuitem.setAttribute('role', 'menuitem');
});
[].concat((0, _toConsumableArray3.default)(element.querySelectorAll('.' + MENU_BUTTON_MENU_ITEM_SEPARATOR))).forEach(function (menuitem) {
menuitem.setAttribute('role', 'separator');
});
};
var init = function init() {
addWaiAria();
parentNode = element.parentNode;
element.classList.add('is-upgraded');
};
var _downgrade = function _downgrade() {
_removeListeners();
if (element.parentNode !== parentNode) {
parentNode.appendChild(element);
}
element.classList.remove('is-upgraded');
};
init();
return {
/**
* Get the menu element
* @returns {Element} the menu element
*/
get element() {
return element;
},
/**
* Set selected menu item
* @param item
*/
set selected(item) {
setSelected(item, true);
},
/**
* Open menu
* @param {Element} controlElement the element where the menu should be aligned to
* @param {String} position menuElement item to receive focus after menu element is opened
*/
open: function open(controlElement) {
var position = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'first';
return _open(controlElement, position);
},
/**
* Remove event listeners.
*/
removeListeners: function removeListeners() {
return _removeListeners();
},
/**
* Downgrade menu
*/
downgrade: function downgrade() {
return _downgrade();
}
};
};
/**
* The menubutton component
*/
var MenuButton = function () {
function MenuButton(element) {
var _this = this;
(0, _classCallCheck3.default)(this, MenuButton);
this.keyDownHandler = function (event) {
if (!_this.isDisabled()) {
switch (event.keyCode) {
case _constants.VK_ARROW_UP:
_this.openMenu('last');
break;
case _constants.VK_ARROW_DOWN:
_this.openMenu();
break;
case _constants.VK_SPACE:
case _constants.VK_ENTER:
_this.openMenu('selected');
break;
case _constants.VK_ESC:
_this.closeMenu();
break;
case _constants.VK_TAB:
_this.closeMenu();
return;
default:
return;
}
}
//event.stopPropagation();
event.preventDefault();
};
this.clickHandler = function () {
if (!_this.isDisabled()) {
if (_this.element.getAttribute('aria-expanded').toLowerCase() === 'true') {
_this.closeMenu(true);
} else {
_this.openMenu('selected');
}
}
};
this.recalcMenuPosition = (0, _fullThrottle2.default)(function () {
var c = _this.focusElement.getBoundingClientRect();
var dx = _this.focusElementLastScrollPosition.left - c.left;
var dy = _this.focusElementLastScrollPosition.top - c.top;
var left = (parseFloat(_this.menu.element.style.left) || 0) - dx;
var top = (parseFloat(_this.menu.element.style.top) || 0) - dy;
_this.menu.element.style.left = left + 'px';
_this.menu.element.style.top = top + 'px';
_this.focusElementLastScrollPosition = c;
});
this.positionChangeHandler = function () {
_this.recalcMenuPosition(_this);
};
this.closeMenuHandler = function (event) {
if (event && event.detail) {
if (event.detail.item && event.detail.item !== _this.selectedItem) {
_this.selectedItem = event.detail.item;
_this.dispatchMenuSelect();
}
_this.closeMenu(event.detail.forceFocus);
}
};
this.element = element;
this.focusElement = undefined;
this.focusElementLastScrollPosition = undefined;
this.scrollElements = [];
this.menu = undefined;
this.selectedItem = null;
this.init();
}
/**
* Re-position menu if content is scrolled, window is resized or orientation change
* @see https://javascriptweblog.wordpress.com/2015/11/02/of-classes-and-arrow-functions-a-cautionary-tale/
*/
(0, _createClass3.default)(MenuButton, [{
key: 'dispatchMenuSelect',
value: function dispatchMenuSelect() {
this.element.dispatchEvent(new CustomEvent('menuselect', {
bubbles: true,
cancelable: true,
detail: { source: this.selectedItem }
}));
}
}, {
key: 'isDisabled',
value: function isDisabled() {
return this.element.hasAttribute('disabled');
}
}, {
key: 'removeListeners',
value: function removeListeners() {
this.element.removeEventListener('keydown', this.keyDownHandler);
this.element.removeEventListener('click', this.clickHandler);
}
}, {
key: 'openMenu',
value: function openMenu() {
var _this2 = this;
var position = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 'first';
if (!this.isDisabled() && this.menu) {
// Close the menu if button position change
this.scrollElements = (0, _domUtils.getScrollParents)(this.element);
this.scrollElements.forEach(function (el) {
return el.addEventListener('scroll', _this2.positionChangeHandler);
});
window.addEventListener('resize', this.positionChangeHandler);
window.addEventListener('orientationchange', this.positionChangeHandler);
this.menu.element.addEventListener('_closemenu', this.closeMenuHandler);
this.menu.selected = this.selectedItem;
this.menu.open(this.focusElement, position);
this.element.setAttribute('aria-expanded', 'true');
this.focusElementLastScrollPosition = this.focusElement.getBoundingClientRect();
}
}
}, {
key: 'closeMenu',
value: function closeMenu() {
var _this3 = this;
var forceFocus = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false;
if (this.menu) {
this.menu.removeListeners();
this.scrollElements.forEach(function (el) {
return el.removeEventListener('scroll', _this3.positionChangeHandler);
});
window.removeEventListener('resize', this.positionChangeHandler);
window.removeEventListener('orientationchange', this.positionChangeHandler);
this.menu.element.removeEventListener('_closemenu', this.closeMenuHandler);
if (forceFocus) {
this.focus();
}
this.element.setAttribute('aria-expanded', 'false');
this.menu.element.setAttribute('hidden', '');
}
}
}, {
key: 'focus',
value: function focus() {
if (!this.isDisabled()) {
this.focusElement.focus();
}
}
}, {
key: 'init',
value: function init() {
var _this4 = this;
var addListeners = function addListeners() {
_this4.element.addEventListener('keydown', _this4.keyDownHandler);
_this4.element.addEventListener('click', _this4.clickHandler);
};
var addWaiAria = function addWaiAria() {
_this4.element.setAttribute('role', 'button');
_this4.element.setAttribute('aria-expanded', 'false');
_this4.element.setAttribute('aria-haspopup', 'true');
};
var addFocusElement = function addFocusElement() {
_this4.focusElement = _this4.element.querySelector('input[type="text"]');
if (!_this4.focusElement) {
_this4.focusElement = _this4.element;
if (!(_this4.focusElement.tagName.toLowerCase() === 'button' || _this4.focusElement.tagName.toLowerCase() === 'input')) {
if (!_this4.focusElement.hasAttribute('tabindex')) {
_this4.focusElement.setAttribute('tabindex', '0');
}
}
}
};
var moveElementToDocumentBody = function moveElementToDocumentBody(element) {
// To position an element on top of all other z-indexed elements, the element should be moved to document.body
// See: https://philipwalton.com/articles/what-no-one-told-you-about-z-index/
if (element.parentNode !== document.body) {
return document.body.appendChild(element);
}
return element;
};
var findMenuElement = function findMenuElement() {
var menuElement = void 0;
var menuElementId = _this4.element.getAttribute('aria-controls');
if (menuElementId !== null) {
menuElement = document.querySelector('#' + menuElementId);
} else {
menuElement = _this4.element.parentNode.querySelector('.' + MENU_BUTTON_MENU);
}
return menuElement;
};
var addMenu = function addMenu() {
var menuElement = findMenuElement();
if (menuElement) {
if (menuElement.componentInstance) {
_this4.menu = menuElement.componentInstance;
} else {
_this4.menu = menuFactory(menuElement);
menuElement.componentInstance = _this4.menu;
moveElementToDocumentBody(menuElement);
}
_this4.element.setAttribute('aria-controls', _this4.menu.element.id);
}
};
addFocusElement();
addWaiAria();
addMenu();
this.removeListeners();
addListeners();
}
}, {
key: 'downgrade',
value: function downgrade() {
var _this5 = this;
if (this.menu) {
// Do not downgrade menu if there are other buttons sharing this menu
var related = [].concat((0, _toConsumableArray3.default)(document.querySelectorAll('.' + JS_MENU_BUTTON + '[aria-controls="' + this.element.getAttribute('aria-controls') + '"]')));
if (related.filter(function (c) {
return c !== _this5.element && c.getAttribute('data-upgraded').indexOf('MaterialExtMenuButton') >= 0;
}).length === 0) {
this.menu.downgrade();
}
}
this.removeListeners();
}
}]);
return MenuButton;
}();
(function () {
'use strict';
/**
* https://github.com/google/material-design-lite/issues/4205
* @constructor
* @param {Element} element The element that will be upgraded.
*/
var MaterialExtMenuButton = function MaterialExtMenuButton(element) {
this.element_ = element;
this.menuButton_ = null;
// Initialize instance.
this.init();
};
window['MaterialExtMenuButton'] = MaterialExtMenuButton;
// Public methods.
/**
* Get the menu element controlled by this button, null if no menu is controlled by this button
* @public
*/
MaterialExtMenuButton.prototype.getMenuElement = function () {
return this.menuButton_.menu ? this.menuButton_.menu.element : null;
};
MaterialExtMenuButton.prototype['getMenuElement'] = MaterialExtMenuButton.prototype.getMenuElement;
/**
* Open menu
* @public
* @param {String} position one of "first", "last" or "selected"
*/
MaterialExtMenuButton.prototype.openMenu = function (position) {
this.menuButton_.openMenu(position);
};
MaterialExtMenuButton.prototype['openMenu'] = MaterialExtMenuButton.prototype.openMenu;
/**
* Close menu
* @public
*/
MaterialExtMenuButton.prototype.closeMenu = function () {
this.menuButton_.closeMenu(true);
};
MaterialExtMenuButton.prototype['closeMenu'] = MaterialExtMenuButton.prototype.closeMenu;
/**
* Get selected menu item
* @public
* @returns {Element} The selected menu item or null if no item selected
*/
MaterialExtMenuButton.prototype.getSelectedMenuItem = function () {
return this.menuButton_.selectedItem;
};
MaterialExtMenuButton.prototype['getSelectedMenuItem'] = MaterialExtMenuButton.prototype.getSelectedMenuItem;
/**
* Set (default) selected menu item
* @param {Element} item
*/
MaterialExtMenuButton.prototype.setSelectedMenuItem = function (item) {
this.menuButton_.selectedItem = item;
};
MaterialExtMenuButton.prototype['setSelectedMenuItem'] = MaterialExtMenuButton.prototype.setSelectedMenuItem;
/**
* Initialize component
*/
MaterialExtMenuButton.prototype.init = function () {
if (this.element_) {
this.menuButton_ = new MenuButton(this.element_);
this.element_.addEventListener('mdl-componentdowngraded', this.mdlDowngrade_.bind(this));
this.element_.classList.add(_constants.IS_UPGRADED);
}
};
/**
* Downgrade component
* E.g remove listeners and clean up resources
*/
MaterialExtMenuButton.prototype.mdlDowngrade_ = function () {
this.menuButton_.downgrade();
};
// The component registers itself. It can assume componentHandler is available
// in the global scope.
/* eslint no-undef: 0 */
componentHandler.register({
constructor: MaterialExtMenuButton,
classAsString: 'MaterialExtMenuButton',
cssClass: JS_MENU_BUTTON,
widget: true
});
})();