1 import videojs from 'video.js'
2 // Thanks to Yanko Shterev: https://github.com/yshterev/videojs-settings-menu
3 import { toTitleCase } from '../utils'
4 import { SettingsDialog } from './settings-dialog'
5 import { SettingsButton } from './settings-menu-button'
6 import { SettingsPanel } from './settings-panel'
7 import { SettingsPanelChild } from './settings-panel-child'
9 const MenuItem = videojs.getComponent('MenuItem')
10 const component = videojs.getComponent('Component')
12 export interface SettingsMenuItemOptions extends videojs.MenuItemOptions {
14 menuButton: SettingsButton
17 class SettingsMenuItem extends MenuItem {
18 settingsButton: SettingsButton
19 dialog: SettingsDialog
20 mainMenu: videojs.Menu
22 panelChild: SettingsPanelChild
23 panelChildEl: HTMLElement
26 subMenu: SettingsButton
28 submenuClickHandler: typeof SettingsMenuItem.prototype.onSubmenuClick
29 transitionEndHandler: typeof SettingsMenuItem.prototype.onTransitionEnd
31 settingsSubMenuTitleEl_: HTMLElement
32 settingsSubMenuValueEl_: HTMLElement
33 settingsSubMenuEl_: HTMLElement
35 constructor (player: videojs.Player, options?: SettingsMenuItemOptions) {
36 super(player, options)
38 this.settingsButton = options.menuButton
39 this.dialog = this.settingsButton.dialog
40 this.mainMenu = this.settingsButton.menu
41 this.panel = this.dialog.getChild('settingsPanel')
42 this.panelChild = this.panel.getChild('settingsPanelChild')
43 this.panelChildEl = this.panelChild.el() as HTMLElement
47 // keep state of what menu type is loading next
48 this.menuToLoad = 'mainmenu'
50 const subMenuName = toTitleCase(options.entry)
51 const SubMenuComponent = videojs.getComponent(subMenuName)
53 if (!SubMenuComponent) {
54 throw new Error(`Component ${subMenuName} does not exist`)
57 const newOptions = Object.assign({}, options, { entry: options.menuButton, menuButton: this })
59 this.subMenu = new SubMenuComponent(this.player(), newOptions) as any // FIXME: typings
60 const subMenuClass = this.subMenu.buildCSSClass().split(' ')[0]
61 this.settingsSubMenuEl_.className += ' ' + subMenuClass
66 // Voodoo magic for IOS
68 // Player was destroyed
69 if (!this.player_) return
73 // Update on rate change
74 player.on('ratechange', this.submenuClickHandler)
76 if (subMenuName === 'CaptionsButton') {
77 // Hack to regenerate captions on HTTP fallback
78 player.on('captionsChanged', () => {
80 this.settingsSubMenuEl_.innerHTML = ''
81 this.settingsSubMenuEl_.appendChild(this.subMenu.menu.el())
83 this.bindClickEvents()
94 this.submenuClickHandler = this.onSubmenuClick.bind(this)
95 this.transitionEndHandler = this.onTransitionEnd.bind(this)
98 onSubmenuClick (event: any) {
101 if (event.type === 'tap') {
102 target = event.target
104 target = event.currentTarget
107 if (target?.classList.contains('vjs-back-button')) {
112 // To update the sub menu value on click, setTimeout is needed because
113 // updating the value is not instant
114 setTimeout(() => this.update(event), 0)
116 // Seems like videojs adds a vjs-hidden class on the caption menu after a click
118 this.subMenu.menu.removeClass('vjs-hidden')
122 * Create the component's DOM element
126 const el = videojs.dom.createEl('li', {
127 className: 'vjs-menu-item'
130 this.settingsSubMenuTitleEl_ = videojs.dom.createEl('div', {
131 className: 'vjs-settings-sub-menu-title'
134 el.appendChild(this.settingsSubMenuTitleEl_)
136 this.settingsSubMenuValueEl_ = videojs.dom.createEl('div', {
137 className: 'vjs-settings-sub-menu-value'
140 el.appendChild(this.settingsSubMenuValueEl_)
142 this.settingsSubMenuEl_ = videojs.dom.createEl('div', {
143 className: 'vjs-settings-sub-menu'
146 return el as HTMLLIElement
150 * Handle click on menu item
152 * @method handleClick
154 handleClick (event: videojs.EventTarget.Event) {
155 this.menuToLoad = 'submenu'
156 // Remove open class to ensure only the open submenu gets this class
157 videojs.dom.removeClass(this.el(), 'open')
159 super.handleClick(event);
161 (this.mainMenu.el() as HTMLElement).style.opacity = '0'
162 // Whether to add or remove vjs-hidden class on the settingsSubMenuEl element
163 if (videojs.dom.hasClass(this.settingsSubMenuEl_, 'vjs-hidden')) {
164 videojs.dom.removeClass(this.settingsSubMenuEl_, 'vjs-hidden')
166 // animation not played without timeout
168 this.settingsSubMenuEl_.style.opacity = '1'
169 this.settingsSubMenuEl_.style.marginRight = '0px'
172 this.settingsButton.setDialogSize(this.size)
174 videojs.dom.addClass(this.settingsSubMenuEl_, 'vjs-hidden')
181 * @method createBackButton
183 createBackButton () {
184 const button = this.subMenu.menu.addChild('MenuItem', {}, 0)
186 button.addClass('vjs-back-button');
187 (button.el() as HTMLElement).innerHTML = this.player().localize(this.subMenu.controlText())
191 * Add/remove prefixed event listener for CSS Transition
193 * @method PrefixedEvent
195 PrefixedEvent (element: any, type: any, callback: any, action = 'addEvent') {
196 const prefix = [ 'webkit', 'moz', 'MS', 'o', '' ]
198 for (let p = 0; p < prefix.length; p++) {
200 type = type.toLowerCase()
203 if (action === 'addEvent') {
204 element.addEventListener(prefix[p] + type, callback, false)
205 } else if (action === 'removeEvent') {
206 element.removeEventListener(prefix[p] + type, callback, false)
211 onTransitionEnd (event: any) {
212 if (event.propertyName !== 'margin-right') {
216 if (this.menuToLoad === 'mainmenu') {
218 videojs.dom.addClass(this.settingsSubMenuEl_, 'vjs-hidden')
220 // reset opacity to 0
221 this.settingsSubMenuEl_.style.opacity = '0'
226 videojs.dom.addClass(this.settingsSubMenuEl_, 'vjs-hidden')
227 this.settingsSubMenuEl_.style.opacity = '0'
232 const mainMenuEl = this.mainMenu.el() as HTMLElement
233 this.menuToLoad = 'mainmenu'
235 mainMenuEl.style.opacity = '0'
237 // back button will always take you to main menu, so set dialog sizes
238 const mainMenuAny = this.mainMenu as any
239 this.settingsButton.setDialogSize([ mainMenuAny.width, mainMenuAny.height ])
241 // animation not triggered without timeout (some async stuff ?!?)
243 // animate margin and opacity before hiding the submenu
244 // this triggers CSS Transition event
246 mainMenuEl.style.opacity = '1'
251 this.subMenu.on('labelUpdated', () => {
254 this.subMenu.on('menuChanged', () => {
255 this.bindClickEvents()
260 this.settingsSubMenuTitleEl_.innerHTML = this.player().localize(this.subMenu.controlText())
261 this.settingsSubMenuEl_.appendChild(this.subMenu.menu.el())
262 this.panelChildEl.appendChild(this.settingsSubMenuEl_)
265 this.createBackButton()
267 this.bindClickEvents()
269 // prefixed event listeners for CSS TransitionEnd
271 this.settingsSubMenuEl_,
273 this.transitionEndHandler,
278 update (event?: any) {
279 let target: HTMLElement = null
280 const subMenu = this.subMenu.name()
282 if (event && event.type === 'tap') {
283 target = event.target
285 target = event.currentTarget
288 // Playback rate menu button doesn't get a vjs-selected class
289 // or sets options_['selected'] on the selected playback rate.
290 // Thus we get the submenu value based on the labelEl of playbackRateMenuButton
291 if (subMenu === 'PlaybackRateMenuButton') {
292 const html = (this.subMenu as any).labelEl_.innerHTML
295 this.settingsSubMenuValueEl_.innerHTML = html
298 // Loop trough the submenu items to find the selected child
299 for (const subMenuItem of this.subMenu.menu.children_) {
300 if (!(subMenuItem instanceof component)) {
304 if (subMenuItem.hasClass('vjs-selected')) {
305 const subMenuItemUntyped = subMenuItem as any
307 // Prefer to use the function
308 if (typeof subMenuItemUntyped.getLabel === 'function') {
309 this.settingsSubMenuValueEl_.innerHTML = subMenuItemUntyped.getLabel()
313 this.settingsSubMenuValueEl_.innerHTML = subMenuItemUntyped.options_.label
318 if (target && !target.classList.contains('vjs-back-button')) {
319 this.settingsButton.hideDialog()
324 for (const item of this.subMenu.menu.children()) {
325 if (!(item instanceof component)) {
328 item.on([ 'tap', 'click' ], this.submenuClickHandler)
332 // save size of submenus on first init
333 // if number of submenu items change dynamically more logic will be needed
335 this.dialog.removeClass('vjs-hidden')
336 videojs.dom.removeClass(this.settingsSubMenuEl_, 'vjs-hidden')
337 this.size = this.settingsButton.getComponentSize(this.settingsSubMenuEl_)
339 this.dialog.addClass('vjs-hidden')
340 videojs.dom.addClass(this.settingsSubMenuEl_, 'vjs-hidden')
344 if (!this.size) return
346 const [ width ] = this.size
348 this.settingsSubMenuEl_.style.marginRight = `-${width}px`
355 // after removing settings item this.el_ === null
360 if (videojs.dom.hasClass(this.el(), 'open')) {
361 videojs.dom.addClass(this.settingsSubMenuEl_, 'vjs-hidden')
362 videojs.dom.removeClass(this.el(), 'open')
368 (SettingsMenuItem as any).prototype.contentElType = 'button'
369 videojs.registerComponent('SettingsMenuItem', SettingsMenuItem)
371 export { SettingsMenuItem }