/**
* @file menu-button.js
*/
import ClickableComponent from '../clickable-component.js';
import Component from '../component.js';
import Menu from './menu.js';
import * as Dom from '../utils/dom.js';
import * as Fn from '../utils/fn.js';
import toTitleCase from '../utils/to-title-case.js';
/**
* A `MenuButton` class for any popup {@link Menu}.
*
* @extends ClickableComponent
*/
class MenuButton extends ClickableComponent {
/**
* Creates an instance of this class.
*
* @param {Player} player
* The `Player` that this class should be attached to.
*
* @param {Object} [options={}]
* The key/value store of player options.
*/
constructor(player, options = {}) {
super(player, options);
this.update();
this.enabled_ = true;
this.el_.setAttribute('aria-haspopup', 'true');
this.el_.setAttribute('role', 'menuitem');
this.on('keydown', this.handleSubmenuKeyPress);
}
/**
* Update the menu based on the current state of its items.
*/
update() {
const menu = this.createMenu();
if (this.menu) {
this.removeChild(this.menu);
}
this.menu = menu;
this.addChild(menu);
/**
* Track the state of the menu button
*
* @type {Boolean}
* @private
*/
this.buttonPressed_ = false;
this.el_.setAttribute('aria-expanded', 'false');
if (this.items && this.items.length === 0) {
this.hide();
} else if (this.items && this.items.length > 1) {
this.show();
}
}
/**
* Create the menu and add all items to it.
*
* @return {Menu}
* The constructed menu
*/
createMenu() {
const menu = new Menu(this.player_);
// Add a title list item to the top
if (this.options_.title) {
const title = Dom.createEl('li', {
className: 'vjs-menu-title',
innerHTML: toTitleCase(this.options_.title),
tabIndex: -1
});
menu.children_.unshift(title);
Dom.insertElFirst(title, menu.contentEl());
}
this.items = this.createItems();
if (this.items) {
// Add menu items to the menu
for (let i = 0; i < this.items.length; i++) {
menu.addItem(this.items[i]);
}
}
return menu;
}
/**
* Create the list of menu items. Specific to each subclass.
*
* @abstract
*/
createItems() {}
/**
* Create the `MenuButtons`s DOM element.
*
* @return {Element}
* The element that gets created.
*/
createEl() {
return super.createEl('div', {
className: this.buildCSSClass()
});
}
/**
* Builds the default DOM `className`.
*
* @return {string}
* The DOM `className` for this object.
*/
buildCSSClass() {
let menuButtonClass = 'vjs-menu-button';
// If the inline option is passed, we want to use different styles altogether.
if (this.options_.inline === true) {
menuButtonClass += '-inline';
} else {
menuButtonClass += '-popup';
}
return `vjs-menu-button ${menuButtonClass} ${super.buildCSSClass()}`;
}
/**
* Handle a click on a `MenuButton`.
* See {@link ClickableComponent#handleClick} for instances where this is called.
*
* @param {EventTarget~Event} event
* The `keydown`, `tap`, or `click` event that caused this function to be
* called.
*
* @listens tap
* @listens click
*/
handleClick(event) {
// When you click the button it adds focus, which will show the menu.
// So we'll remove focus when the mouse leaves the button. Focus is needed
// for tab navigation.
this.one(this.menu.contentEl(), 'mouseleave', Fn.bind(this, function(e) {
this.unpressButton();
this.el_.blur();
}));
if (this.buttonPressed_) {
this.unpressButton();
} else {
this.pressButton();
}
}
/**
* Handle tab, escape, down arrow, and up arrow keys for `MenuButton`. See
* {@link ClickableComponent#handleKeyPress} for instances where this is called.
*
* @param {EventTarget~Event} event
* The `keydown` event that caused this function to be called.
*
* @listens keydown
*/
handleKeyPress(event) {
// Escape (27) key or Tab (9) key unpress the 'button'
if (event.which === 27 || event.which === 9) {
if (this.buttonPressed_) {
this.unpressButton();
}
// Don't preventDefault for Tab key - we still want to lose focus
if (event.which !== 9) {
event.preventDefault();
}
// Up (38) key or Down (40) key press the 'button'
} else if (event.which === 38 || event.which === 40) {
if (!this.buttonPressed_) {
this.pressButton();
event.preventDefault();
}
} else {
super.handleKeyPress(event);
}
}
/**
* Handle a `keydown` event on a sub-menu. The listener for this is added in
* the constructor.
*
* @param {EventTarget~Event} event
* Key press event
*
* @listens keydown
*/
handleSubmenuKeyPress(event) {
// Escape (27) key or Tab (9) key unpress the 'button'
if (event.which === 27 || event.which === 9) {
if (this.buttonPressed_) {
this.unpressButton();
}
// Don't preventDefault for Tab key - we still want to lose focus
if (event.which !== 9) {
event.preventDefault();
}
}
}
/**
* Put the current `MenuButton` into a pressed state.
*/
pressButton() {
if (this.enabled_) {
this.buttonPressed_ = true;
this.menu.lockShowing();
this.el_.setAttribute('aria-expanded', 'true');
// set the focus into the submenu
this.menu.focus();
}
}
/**
* Take the current `MenuButton` out of a pressed state.
*/
unpressButton() {
if (this.enabled_) {
this.buttonPressed_ = false;
this.menu.unlockShowing();
this.el_.setAttribute('aria-expanded', 'false');
// Set focus back to this menu button
this.el_.focus();
}
}
/**
* Disable the `MenuButton`. Don't allow it to be clicked.
*
* @return {MenuButton}
* Returns itself; method can be chained.
*/
disable() {
// Unpress, but don't force focus on this button
this.buttonPressed_ = false;
this.menu.unlockShowing();
this.el_.setAttribute('aria-expanded', 'false');
this.enabled_ = false;
return super.disable();
}
/**
* Enable the `MenuButton`. Allow it to be clicked.
*
* @return {MenuButton}
* Returns itself; method can be chained.
*/
enable() {
this.enabled_ = true;
return super.enable();
}
}
Component.registerComponent('MenuButton', MenuButton);
export default MenuButton;