Source: ui/overflow_menu.js

  1. /*! @license
  2. * Shaka Player
  3. * Copyright 2016 Google LLC
  4. * SPDX-License-Identifier: Apache-2.0
  5. */
  6. goog.provide('shaka.ui.OverflowMenu');
  7. goog.require('goog.asserts');
  8. goog.require('shaka.ads.Utils');
  9. goog.require('shaka.log');
  10. goog.require('shaka.ui.Controls');
  11. goog.require('shaka.ui.Element');
  12. goog.require('shaka.ui.Enums');
  13. goog.require('shaka.ui.Locales');
  14. goog.require('shaka.ui.Localization');
  15. goog.require('shaka.ui.Utils');
  16. goog.require('shaka.util.Dom');
  17. goog.require('shaka.util.Iterables');
  18. /**
  19. * @extends {shaka.ui.Element}
  20. * @final
  21. * @export
  22. */
  23. shaka.ui.OverflowMenu = class extends shaka.ui.Element {
  24. /**
  25. * @param {!HTMLElement} parent
  26. * @param {!shaka.ui.Controls} controls
  27. */
  28. constructor(parent, controls) {
  29. super(parent, controls);
  30. /** @private {!shaka.extern.UIConfiguration} */
  31. this.config_ = this.controls.getConfig();
  32. /** @private {HTMLElement} */
  33. this.controlsContainer_ = this.controls.getControlsContainer();
  34. /** @private {HTMLElement } */
  35. this.videoContainer_ = this.controls.getVideoContainer();
  36. /** @private {!Array<shaka.extern.IUIElement>} */
  37. this.children_ = [];
  38. this.addOverflowMenuButton_();
  39. this.addOverflowMenu_();
  40. this.createChildren_();
  41. this.eventManager.listen(
  42. this.localization, shaka.ui.Localization.LOCALE_UPDATED, () => {
  43. this.updateAriaLabel_();
  44. });
  45. this.eventManager.listen(
  46. this.localization, shaka.ui.Localization.LOCALE_CHANGED, () => {
  47. this.updateAriaLabel_();
  48. });
  49. this.eventManager.listen(
  50. this.adManager, shaka.ads.Utils.AD_STARTED, () => {
  51. if (this.ad && this.ad.isLinear()) {
  52. shaka.ui.Utils.setDisplay(this.overflowMenuButton_, false);
  53. }
  54. });
  55. this.eventManager.listen(
  56. this.adManager, shaka.ads.Utils.AD_STOPPED, () => {
  57. shaka.ui.Utils.setDisplay(this.overflowMenuButton_, true);
  58. });
  59. this.eventManager.listen(
  60. this.controls, 'submenuopen', () => {
  61. // Hide the main overflow menu if one of the sub menus has
  62. // been opened.
  63. shaka.ui.Utils.setDisplay(this.overflowMenu_, false);
  64. });
  65. this.eventManager.listen(
  66. this.overflowMenu_, 'touchstart', (event) => {
  67. this.controls.setLastTouchEventTime(Date.now());
  68. event.stopPropagation();
  69. });
  70. this.eventManager.listen(this.overflowMenuButton_, 'click', () => {
  71. if (!this.controls.isOpaque()) {
  72. return;
  73. }
  74. this.onOverflowMenuButtonClick_();
  75. });
  76. this.updateAriaLabel_();
  77. if (this.ad && this.ad.isLinear()) {
  78. // There was already an ad.
  79. shaka.ui.Utils.setDisplay(this.overflowMenuButton_, false);
  80. }
  81. /** @private {ResizeObserver} */
  82. this.resizeObserver_ = null;
  83. const resize = () => this.computeMaxHeight_();
  84. // Use ResizeObserver if available, fallback to window resize event
  85. if (window.ResizeObserver) {
  86. this.resizeObserver_ = new ResizeObserver(resize);
  87. this.resizeObserver_.observe(this.controls.getVideoContainer());
  88. } else {
  89. // Fallback for older browsers
  90. this.eventManager.listen(window, 'resize', resize);
  91. }
  92. }
  93. /** @override */
  94. release() {
  95. this.controlsContainer_ = null;
  96. for (const element of this.children_) {
  97. element.release();
  98. }
  99. this.children_ = [];
  100. if (this.resizeObserver_) {
  101. this.resizeObserver_.disconnect();
  102. this.resizeObserver_ = null;
  103. }
  104. super.release();
  105. }
  106. /**
  107. * @param {string} name
  108. * @param {!shaka.extern.IUIElement.Factory} factory
  109. * @export
  110. */
  111. static registerElement(name, factory) {
  112. shaka.ui.OverflowMenu.elementNamesToFactories_.set(name, factory);
  113. }
  114. /**
  115. * @private
  116. */
  117. addOverflowMenu_() {
  118. /** @private {!HTMLElement} */
  119. this.overflowMenu_ = shaka.util.Dom.createHTMLElement('div');
  120. this.overflowMenu_.classList.add('shaka-overflow-menu');
  121. this.overflowMenu_.classList.add('shaka-no-propagation');
  122. this.overflowMenu_.classList.add('shaka-show-controls-on-mouse-over');
  123. this.overflowMenu_.classList.add('shaka-hidden');
  124. this.controlsContainer_.appendChild(this.overflowMenu_);
  125. }
  126. /**
  127. * @private
  128. */
  129. addOverflowMenuButton_() {
  130. /** @private {!HTMLButtonElement} */
  131. this.overflowMenuButton_ = shaka.util.Dom.createButton();
  132. this.overflowMenuButton_.classList.add('shaka-overflow-menu-button');
  133. this.overflowMenuButton_.classList.add('shaka-no-propagation');
  134. this.overflowMenuButton_.classList.add('material-icons-round');
  135. this.overflowMenuButton_.classList.add('shaka-tooltip');
  136. this.overflowMenuButton_.textContent =
  137. shaka.ui.Enums.MaterialDesignIcons.OPEN_OVERFLOW;
  138. const markEl = shaka.util.Dom.createHTMLElement('span');
  139. markEl.classList.add('shaka-overflow-quality-mark');
  140. markEl.style.display = 'none';
  141. this.overflowMenuButton_.appendChild(markEl);
  142. this.parent.appendChild(this.overflowMenuButton_);
  143. }
  144. /**
  145. * @private
  146. */
  147. createChildren_() {
  148. for (const name of this.config_.overflowMenuButtons) {
  149. if (shaka.ui.OverflowMenu.elementNamesToFactories_.get(name)) {
  150. const factory =
  151. shaka.ui.OverflowMenu.elementNamesToFactories_.get(name);
  152. goog.asserts.assert(this.controls, 'Controls should not be null!');
  153. this.children_.push(factory.create(this.overflowMenu_, this.controls));
  154. } else {
  155. shaka.log.alwaysWarn('Unrecognized overflow menu element requested:',
  156. name);
  157. }
  158. }
  159. }
  160. /** @private */
  161. onOverflowMenuButtonClick_() {
  162. if (this.controls.anySettingsMenusAreOpen()) {
  163. this.controls.hideSettingsMenus();
  164. } else {
  165. shaka.ui.Utils.setDisplay(this.overflowMenu_, true);
  166. this.controls.computeOpacity();
  167. // If overflow menu has currently visible buttons, focus on the
  168. // first one, when the menu opens.
  169. const isDisplayed =
  170. (element) => element.classList.contains('shaka-hidden') == false;
  171. const Iterables = shaka.util.Iterables;
  172. if (Iterables.some(this.overflowMenu_.childNodes, isDisplayed)) {
  173. // Focus on the first visible child of the overflow menu
  174. const visibleElements =
  175. Iterables.filter(this.overflowMenu_.childNodes, isDisplayed);
  176. /** @type {!HTMLElement} */ (visibleElements[0]).focus();
  177. }
  178. this.computeMaxHeight_();
  179. }
  180. }
  181. /**
  182. * @private
  183. */
  184. updateAriaLabel_() {
  185. const LocIds = shaka.ui.Locales.Ids;
  186. this.overflowMenuButton_.ariaLabel =
  187. this.localization.resolve(LocIds.MORE_SETTINGS);
  188. }
  189. /**
  190. * @private
  191. */
  192. computeMaxHeight_() {
  193. const rectMenu = this.overflowMenu_.getBoundingClientRect();
  194. const styleMenu = window.getComputedStyle(this.overflowMenu_);
  195. const paddingTop = parseFloat(styleMenu.paddingTop);
  196. const paddingBottom = parseFloat(styleMenu.paddingBottom);
  197. const rectContainer = this.videoContainer_.getBoundingClientRect();
  198. const heightIntersection =
  199. rectMenu.bottom - rectContainer.top - paddingTop - paddingBottom;
  200. this.overflowMenu_.style.maxHeight = heightIntersection + 'px';
  201. }
  202. };
  203. /**
  204. * @implements {shaka.extern.IUIElement.Factory}
  205. * @final
  206. */
  207. shaka.ui.OverflowMenu.Factory = class {
  208. /** @override */
  209. create(rootElement, controls) {
  210. return new shaka.ui.OverflowMenu(rootElement, controls);
  211. }
  212. };
  213. shaka.ui.Controls.registerElement(
  214. 'overflow_menu', new shaka.ui.OverflowMenu.Factory());
  215. /** @private {!Map<string, !shaka.extern.IUIElement.Factory>} */
  216. shaka.ui.OverflowMenu.elementNamesToFactories_ = new Map();