/*! @license
* Shaka Player
* Copyright 2016 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
goog.provide('shaka.ui.MenuGroup');
goog.require('goog.asserts');
goog.require('shaka.log');
goog.require('shaka.ui.Locales');
goog.require('shaka.ui.OverflowMenu');
goog.require('shaka.ui.SettingsMenu');
goog.require('shaka.ui.Utils');
goog.requireType('shaka.ui.Controls');
/**
* A menu button that opens a submenu holding other UI elements, which may be
* menu groups too. Subclasses define the icon, the name and the elements
* inside the group. The group is only shown when at least one of its
* elements is shown.
*
* @extends {shaka.ui.SettingsMenu}
* @implements {shaka.extern.IUIMenuGroup}
* @abstract
* @export
*/
shaka.ui.MenuGroup = class extends shaka.ui.SettingsMenu {
/**
* @param {!HTMLElement} parent
* @param {!shaka.ui.Controls} controls
* @param {shaka.extern.UIIcon | string} icon
*/
constructor(parent, controls, icon) {
super(parent, controls, icon);
this.button.classList.add('shaka-menu-group-button');
this.menu.classList.add('shaka-menu-group');
/** @private {!Array<shaka.extern.IUIElement>} */
this.children_ = [];
/**
* The current selection span of the element given by
* getSummaryElementName().
* @private {?Element}
*/
this.summarySpan_ = null;
/**
* Whether a submenu of one of the elements in the group is open.
* @private {boolean}
*/
this.isChildMenuOpened_ = false;
this.createChildren_();
this.button.classList.add(
this.summarySpan_ ? 'shaka-tooltip-status' : 'shaka-tooltip');
this.eventManager.listen(this.controls, 'submenuopen', (event) => {
if (event['container'] == this.menu) {
this.isChildMenuOpened_ = true;
shaka.ui.Utils.setDisplay(this.backButton, false);
}
});
this.eventManager.listen(this.controls, 'submenuclose', (event) => {
const container = event['container'];
if (!container || container == this.menu) {
this.isChildMenuOpened_ = false;
shaka.ui.Utils.setDisplay(this.backButton, true);
}
});
// Follow the visibility and the current selection of the elements in the
// group.
/** @private {MutationObserver} */
this.childrenObserver_ = new MutationObserver(() => this.update_());
this.childrenObserver_.observe(this.menu, {
attributes: true,
attributeFilter: ['class'],
characterData: true,
childList: true,
subtree: true,
});
this.updateLocalizedStrings();
this.update_();
}
/** @override */
release() {
if (this.childrenObserver_) {
this.childrenObserver_.disconnect();
this.childrenObserver_ = null;
}
for (const child of this.children_) {
child.release();
}
this.children_ = [];
this.summarySpan_ = null;
super.release();
}
/**
* @override
* @abstract
*/
getName() {}
/**
* @override
* @abstract
*/
getChildElementNames() {}
/** @override */
getSummaryElementName() {
return null;
}
/** @private */
createChildren_() {
const MenuGroup = shaka.ui.MenuGroup;
const summaryName = this.getSummaryElementName();
for (const name of this.getChildElementNames()) {
const factory = shaka.ui.OverflowMenu.getElementFactory(name);
if (!factory) {
shaka.log.alwaysWarn('Unrecognized menu group element requested:',
name);
continue;
}
// Groups are created recursively, so a group that contains itself,
// directly or not, would never stop.
if (MenuGroup.namesInCreation_.includes(name)) {
shaka.log.alwaysWarn('Menu group element contains itself:', name);
continue;
}
const previousNodes = new Set(this.menu.childNodes);
MenuGroup.namesInCreation_.push(name);
try {
goog.asserts.assert(this.controls, 'Controls should not be null!');
this.children_.push(factory.create(this.menu, this.controls));
} finally {
MenuGroup.namesInCreation_.pop();
}
if (name == summaryName) {
const newNodes = Array.from(this.menu.childNodes)
.filter((node) => !previousNodes.has(node));
for (const node of newNodes) {
if (!(node instanceof HTMLElement)) {
continue;
}
const span = node.querySelector('.shaka-current-selection-span');
if (span && !node.classList.contains('shaka-sub-menu')) {
this.summarySpan_ = span;
break;
}
}
}
}
}
/**
* @return {!Array<!Element>} The elements in the group that are shown.
* @private
*/
getShownChildren_() {
const nodes = /** @type {!Array<!Element>} */ (Array.from(
this.menu.childNodes).filter((node) => node instanceof HTMLElement));
return nodes.filter((node) => {
return node != this.backButton &&
!node.classList.contains('shaka-sub-menu') &&
!node.classList.contains('shaka-hidden');
});
}
/** @private */
update_() {
if (!this.childrenObserver_) {
return;
}
if (this.summarySpan_) {
const summary = this.summarySpan_.textContent;
if (this.currentSelection.textContent != summary) {
this.currentSelection.textContent = summary;
this.button.setAttribute('shaka-status', summary);
}
}
this.checkAvailability();
// Leave the group when it gets empty while it is open, e.g. when the
// text is disabled while the subtitle style options are displayed.
const isOpen = !this.menu.classList.contains('shaka-hidden');
if (isOpen && !this.isChildMenuOpened_ &&
!this.getShownChildren_().length) {
const focusIsInMenu =
this.menu.contains(this.menu.ownerDocument.activeElement);
this.backButton.click();
if (focusIsInMenu) {
this.controls.restoreFocus();
}
}
}
/** @override */
checkAvailability() {
if (!this.children_) {
return;
}
const available = !this.isSubMenuOpened &&
this.getShownChildren_().length > 0;
shaka.ui.Utils.setDisplay(this.button, available);
}
/** @override */
focusOnMenuOpen() {
const shownChildren = this.getShownChildren_();
const target = shownChildren.length ?
/** @type {!HTMLElement} */ (shownChildren[0]) : this.backButton;
target.focus();
}
/** @override */
shouldCloseOnMenuClick(event) {
// Only the back button leaves the group. Clicks on its elements open
// their own submenus, or act on them.
return this.backButton.contains(/** @type {?Node} */ (event.target));
}
/** @override */
updateLocalizedStrings() {
const LocIds = shaka.ui.Locales.Ids;
this.backButton.ariaLabel = this.localization.resolve(LocIds.BACK);
const name = this.getName();
this.button.ariaLabel = name;
this.nameSpan.textContent = name;
this.backSpan.textContent = name;
}
};
/**
* The names of the elements being created, from the outermost menu group to
* the innermost one.
*
* @private {!Array<string>}
*/
shaka.ui.MenuGroup.namesInCreation_ = [];