- 1 :
/**
- 2 :
* @file menu.js
- 3 :
*/
- 4 :
import Component from '../component.js';
- 5 :
import document from 'global/document';
- 6 :
import * as Dom from '../utils/dom.js';
- 7 :
import * as Events from '../utils/events.js';
- 8 :
import keycode from 'keycode';
- 9 :
- 10 :
/**
- 11 :
* The Menu component is used to build popup menus, including subtitle and
- 12 :
* captions selection menus.
- 13 :
*
- 14 :
* @extends Component
- 15 :
*/
- 16 :
class Menu extends Component {
- 17 :
- 18 :
/**
- 19 :
* Create an instance of this class.
- 20 :
*
- 21 :
* @param { import('../player').default } player
- 22 :
* the player that this component should attach to
- 23 :
*
- 24 :
* @param {Object} [options]
- 25 :
* Object of option names and values
- 26 :
*
- 27 :
*/
- 28 :
constructor(player, options) {
- 29 :
super(player, options);
- 30 :
- 31 :
if (options) {
- 32 :
this.menuButton_ = options.menuButton;
- 33 :
}
- 34 :
- 35 :
this.focusedChild_ = -1;
- 36 :
- 37 :
this.on('keydown', (e) => this.handleKeyDown(e));
- 38 :
- 39 :
// All the menu item instances share the same blur handler provided by the menu container.
- 40 :
this.boundHandleBlur_ = (e) => this.handleBlur(e);
- 41 :
this.boundHandleTapClick_ = (e) => this.handleTapClick(e);
- 42 :
}
- 43 :
- 44 :
/**
- 45 :
* Add event listeners to the {@link MenuItem}.
- 46 :
*
- 47 :
* @param {Object} component
- 48 :
* The instance of the `MenuItem` to add listeners to.
- 49 :
*
- 50 :
*/
- 51 :
addEventListenerForItem(component) {
- 52 :
if (!(component instanceof Component)) {
- 53 :
return;
- 54 :
}
- 55 :
- 56 :
this.on(component, 'blur', this.boundHandleBlur_);
- 57 :
this.on(component, ['tap', 'click'], this.boundHandleTapClick_);
- 58 :
}
- 59 :
- 60 :
/**
- 61 :
* Remove event listeners from the {@link MenuItem}.
- 62 :
*
- 63 :
* @param {Object} component
- 64 :
* The instance of the `MenuItem` to remove listeners.
- 65 :
*
- 66 :
*/
- 67 :
removeEventListenerForItem(component) {
- 68 :
if (!(component instanceof Component)) {
- 69 :
return;
- 70 :
}
- 71 :
- 72 :
this.off(component, 'blur', this.boundHandleBlur_);
- 73 :
this.off(component, ['tap', 'click'], this.boundHandleTapClick_);
- 74 :
}
- 75 :
- 76 :
/**
- 77 :
* This method will be called indirectly when the component has been added
- 78 :
* before the component adds to the new menu instance by `addItem`.
- 79 :
* In this case, the original menu instance will remove the component
- 80 :
* by calling `removeChild`.
- 81 :
*
- 82 :
* @param {Object} component
- 83 :
* The instance of the `MenuItem`
- 84 :
*/
- 85 :
removeChild(component) {
- 86 :
if (typeof component === 'string') {
- 87 :
component = this.getChild(component);
- 88 :
}
- 89 :
- 90 :
this.removeEventListenerForItem(component);
- 91 :
super.removeChild(component);
- 92 :
}
- 93 :
- 94 :
/**
- 95 :
* Add a {@link MenuItem} to the menu.
- 96 :
*
- 97 :
* @param {Object|string} component
- 98 :
* The name or instance of the `MenuItem` to add.
- 99 :
*
- 100 :
*/
- 101 :
addItem(component) {
- 102 :
const childComponent = this.addChild(component);
- 103 :
- 104 :
if (childComponent) {
- 105 :
this.addEventListenerForItem(childComponent);
- 106 :
}
- 107 :
}
- 108 :
- 109 :
/**
- 110 :
* Create the `Menu`s DOM element.
- 111 :
*
- 112 :
* @return {Element}
- 113 :
* the element that was created
- 114 :
*/
- 115 :
createEl() {
- 116 :
const contentElType = this.options_.contentElType || 'ul';
- 117 :
- 118 :
this.contentEl_ = Dom.createEl(contentElType, {
- 119 :
className: 'vjs-menu-content'
- 120 :
});
- 121 :
- 122 :
this.contentEl_.setAttribute('role', 'menu');
- 123 :
- 124 :
const el = super.createEl('div', {
- 125 :
append: this.contentEl_,
- 126 :
className: 'vjs-menu'
- 127 :
});
- 128 :
- 129 :
el.appendChild(this.contentEl_);
- 130 :
- 131 :
// Prevent clicks from bubbling up. Needed for Menu Buttons,
- 132 :
// where a click on the parent is significant
- 133 :
Events.on(el, 'click', function(event) {
- 134 :
event.preventDefault();
- 135 :
event.stopImmediatePropagation();
- 136 :
});
- 137 :
- 138 :
return el;
- 139 :
}
- 140 :
- 141 :
dispose() {
- 142 :
this.contentEl_ = null;
- 143 :
this.boundHandleBlur_ = null;
- 144 :
this.boundHandleTapClick_ = null;
- 145 :
- 146 :
super.dispose();
- 147 :
}
- 148 :
- 149 :
/**
- 150 :
* Called when a `MenuItem` loses focus.
- 151 :
*
- 152 :
* @param {Event} event
- 153 :
* The `blur` event that caused this function to be called.
- 154 :
*
- 155 :
* @listens blur
- 156 :
*/
- 157 :
handleBlur(event) {
- 158 :
const relatedTarget = event.relatedTarget || document.activeElement;
- 159 :
- 160 :
// Close menu popup when a user clicks outside the menu
- 161 :
if (!this.children().some((element) => {
- 162 :
return element.el() === relatedTarget;
- 163 :
})) {
- 164 :
const btn = this.menuButton_;
- 165 :
- 166 :
if (btn && btn.buttonPressed_ && relatedTarget !== btn.el().firstChild) {
- 167 :
btn.unpressButton();
- 168 :
}
- 169 :
}
- 170 :
}
- 171 :
- 172 :
/**
- 173 :
* Called when a `MenuItem` gets clicked or tapped.
- 174 :
*
- 175 :
* @param {Event} event
- 176 :
* The `click` or `tap` event that caused this function to be called.
- 177 :
*
- 178 :
* @listens click,tap
- 179 :
*/
- 180 :
handleTapClick(event) {
- 181 :
// Unpress the associated MenuButton, and move focus back to it
- 182 :
if (this.menuButton_) {
- 183 :
this.menuButton_.unpressButton();
- 184 :
- 185 :
const childComponents = this.children();
- 186 :
- 187 :
if (!Array.isArray(childComponents)) {
- 188 :
return;
- 189 :
}
- 190 :
- 191 :
const foundComponent = childComponents.filter(component => component.el() === event.target)[0];
- 192 :
- 193 :
if (!foundComponent) {
- 194 :
return;
- 195 :
}
- 196 :
- 197 :
// don't focus menu button if item is a caption settings item
- 198 :
// because focus will move elsewhere
- 199 :
if (foundComponent.name() !== 'CaptionSettingsMenuItem') {
- 200 :
this.menuButton_.focus();
- 201 :
}
- 202 :
}
- 203 :
}
- 204 :
- 205 :
/**
- 206 :
* Handle a `keydown` event on this menu. This listener is added in the constructor.
- 207 :
*
- 208 :
* @param {Event} event
- 209 :
* A `keydown` event that happened on the menu.
- 210 :
*
- 211 :
* @listens keydown
- 212 :
*/
- 213 :
handleKeyDown(event) {
- 214 :
- 215 :
// Left and Down Arrows
- 216 :
if (keycode.isEventKey(event, 'Left') || keycode.isEventKey(event, 'Down')) {
- 217 :
event.preventDefault();
- 218 :
event.stopPropagation();
- 219 :
this.stepForward();
- 220 :
- 221 :
// Up and Right Arrows
- 222 :
} else if (keycode.isEventKey(event, 'Right') || keycode.isEventKey(event, 'Up')) {
- 223 :
event.preventDefault();
- 224 :
event.stopPropagation();
- 225 :
this.stepBack();
- 226 :
}
- 227 :
}
- 228 :
- 229 :
/**
- 230 :
* Move to next (lower) menu item for keyboard users.
- 231 :
*/
- 232 :
stepForward() {
- 233 :
let stepChild = 0;
- 234 :
- 235 :
if (this.focusedChild_ !== undefined) {
- 236 :
stepChild = this.focusedChild_ + 1;
- 237 :
}
- 238 :
this.focus(stepChild);
- 239 :
}
- 240 :
- 241 :
/**
- 242 :
* Move to previous (higher) menu item for keyboard users.
- 243 :
*/
- 244 :
stepBack() {
- 245 :
let stepChild = 0;
- 246 :
- 247 :
if (this.focusedChild_ !== undefined) {
- 248 :
stepChild = this.focusedChild_ - 1;
- 249 :
}
- 250 :
this.focus(stepChild);
- 251 :
}
- 252 :
- 253 :
/**
- 254 :
* Set focus on a {@link MenuItem} in the `Menu`.
- 255 :
*
- 256 :
* @param {Object|string} [item=0]
- 257 :
* Index of child item set focus on.
- 258 :
*/
- 259 :
focus(item = 0) {
- 260 :
const children = this.children().slice();
- 261 :
const haveTitle = children.length && children[0].hasClass('vjs-menu-title');
- 262 :
- 263 :
if (haveTitle) {
- 264 :
children.shift();
- 265 :
}
- 266 :
- 267 :
if (children.length > 0) {
- 268 :
if (item < 0) {
- 269 :
item = 0;
- 270 :
} else if (item >= children.length) {
- 271 :
item = children.length - 1;
- 272 :
}
- 273 :
- 274 :
this.focusedChild_ = item;
- 275 :
- 276 :
children[item].el_.focus();
- 277 :
}
- 278 :
}
- 279 :
}
- 280 :
- 281 :
Component.registerComponent('Menu', Menu);
- 282 :
export default Menu;