2020-01-28 17:29:50 +01:00
|
|
|
// Thanks to Yanko Shterev: https://github.com/yshterev/videojs-settings-menu
|
2019-01-23 15:36:45 +01:00
|
|
|
import { toTitleCase } from '../utils'
|
2020-04-21 11:02:28 +02:00
|
|
|
import videojs from 'video.js'
|
2020-01-28 17:29:50 +01:00
|
|
|
import { SettingsButton } from './settings-menu-button'
|
|
|
|
import { SettingsDialog } from './settings-dialog'
|
|
|
|
import { SettingsPanel } from './settings-panel'
|
|
|
|
import { SettingsPanelChild } from './settings-panel-child'
|
|
|
|
|
|
|
|
const MenuItem = videojs.getComponent('MenuItem')
|
|
|
|
const component = videojs.getComponent('Component')
|
|
|
|
|
|
|
|
export interface SettingsMenuItemOptions extends videojs.MenuItemOptions {
|
|
|
|
entry: string
|
|
|
|
menuButton: SettingsButton
|
|
|
|
}
|
2018-03-30 17:40:00 +02:00
|
|
|
|
|
|
|
class SettingsMenuItem extends MenuItem {
|
2020-01-28 17:29:50 +01:00
|
|
|
settingsButton: SettingsButton
|
|
|
|
dialog: SettingsDialog
|
|
|
|
mainMenu: videojs.Menu
|
|
|
|
panel: SettingsPanel
|
|
|
|
panelChild: SettingsPanelChild
|
|
|
|
panelChildEl: HTMLElement
|
|
|
|
size: number[]
|
2019-08-01 11:38:26 +02:00
|
|
|
menuToLoad: string
|
2020-01-28 17:29:50 +01:00
|
|
|
subMenu: SettingsButton
|
2019-08-01 11:38:26 +02:00
|
|
|
|
2020-01-28 17:29:50 +01:00
|
|
|
submenuClickHandler: typeof SettingsMenuItem.prototype.onSubmenuClick
|
|
|
|
transitionEndHandler: typeof SettingsMenuItem.prototype.onTransitionEnd
|
2019-08-01 11:38:26 +02:00
|
|
|
|
2020-01-28 17:29:50 +01:00
|
|
|
settingsSubMenuTitleEl_: HTMLElement
|
|
|
|
settingsSubMenuValueEl_: HTMLElement
|
|
|
|
settingsSubMenuEl_: HTMLElement
|
2018-03-30 17:40:00 +02:00
|
|
|
|
2020-04-17 11:20:12 +02:00
|
|
|
constructor (player: videojs.Player, options?: SettingsMenuItemOptions) {
|
2018-03-30 17:40:00 +02:00
|
|
|
super(player, options)
|
|
|
|
|
2020-01-28 17:29:50 +01:00
|
|
|
this.settingsButton = options.menuButton
|
2018-03-30 17:40:00 +02:00
|
|
|
this.dialog = this.settingsButton.dialog
|
|
|
|
this.mainMenu = this.settingsButton.menu
|
|
|
|
this.panel = this.dialog.getChild('settingsPanel')
|
|
|
|
this.panelChild = this.panel.getChild('settingsPanelChild')
|
2020-01-28 17:29:50 +01:00
|
|
|
this.panelChildEl = this.panelChild.el() as HTMLElement
|
2018-03-30 17:40:00 +02:00
|
|
|
|
|
|
|
this.size = null
|
|
|
|
|
|
|
|
// keep state of what menu type is loading next
|
|
|
|
this.menuToLoad = 'mainmenu'
|
|
|
|
|
2020-01-28 17:29:50 +01:00
|
|
|
const subMenuName = toTitleCase(options.entry)
|
|
|
|
const SubMenuComponent = videojs.getComponent(subMenuName)
|
2018-03-30 17:40:00 +02:00
|
|
|
|
|
|
|
if (!SubMenuComponent) {
|
|
|
|
throw new Error(`Component ${subMenuName} does not exist`)
|
|
|
|
}
|
2020-01-28 17:29:50 +01:00
|
|
|
|
|
|
|
const newOptions = Object.assign({}, options, { entry: options.menuButton, menuButton: this })
|
|
|
|
|
|
|
|
this.subMenu = new SubMenuComponent(this.player(), newOptions) as any // FIXME: typings
|
|
|
|
const subMenuClass = this.subMenu.buildCSSClass().split(' ')[ 0 ]
|
2018-07-13 18:21:19 +02:00
|
|
|
this.settingsSubMenuEl_.className += ' ' + subMenuClass
|
2018-03-30 17:40:00 +02:00
|
|
|
|
|
|
|
this.eventHandlers()
|
|
|
|
|
|
|
|
player.ready(() => {
|
2018-09-17 15:00:46 +02:00
|
|
|
// Voodoo magic for IOS
|
|
|
|
setTimeout(() => {
|
2019-07-31 15:57:32 +02:00
|
|
|
// Player was destroyed
|
|
|
|
if (!this.player_) return
|
|
|
|
|
2018-09-17 15:00:46 +02:00
|
|
|
this.build()
|
2018-09-24 17:44:50 +02:00
|
|
|
|
|
|
|
// Update on rate change
|
|
|
|
player.on('ratechange', this.submenuClickHandler)
|
|
|
|
|
2019-01-11 16:44:45 +01:00
|
|
|
if (subMenuName === 'CaptionsButton') {
|
|
|
|
// Hack to regenerate captions on HTTP fallback
|
|
|
|
player.on('captionsChanged', () => {
|
|
|
|
setTimeout(() => {
|
|
|
|
this.settingsSubMenuEl_.innerHTML = ''
|
2020-01-28 17:29:50 +01:00
|
|
|
this.settingsSubMenuEl_.appendChild(this.subMenu.menu.el())
|
2019-01-11 16:44:45 +01:00
|
|
|
this.update()
|
|
|
|
this.bindClickEvents()
|
|
|
|
}, 0)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2018-09-17 15:00:46 +02:00
|
|
|
this.reset()
|
|
|
|
}, 0)
|
2018-03-30 17:40:00 +02:00
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
eventHandlers () {
|
|
|
|
this.submenuClickHandler = this.onSubmenuClick.bind(this)
|
|
|
|
this.transitionEndHandler = this.onTransitionEnd.bind(this)
|
|
|
|
}
|
|
|
|
|
2018-10-18 09:08:59 +02:00
|
|
|
onSubmenuClick (event: any) {
|
2018-03-30 17:40:00 +02:00
|
|
|
let target = null
|
|
|
|
|
|
|
|
if (event.type === 'tap') {
|
|
|
|
target = event.target
|
|
|
|
} else {
|
|
|
|
target = event.currentTarget
|
|
|
|
}
|
|
|
|
|
2018-09-24 17:44:50 +02:00
|
|
|
if (target && target.classList.contains('vjs-back-button')) {
|
2018-03-30 17:40:00 +02:00
|
|
|
this.loadMainMenu()
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// To update the sub menu value on click, setTimeout is needed because
|
|
|
|
// updating the value is not instant
|
|
|
|
setTimeout(() => this.update(event), 0)
|
2019-10-28 09:37:33 +01:00
|
|
|
|
|
|
|
// Seems like videojs adds a vjs-hidden class on the caption menu after a click
|
|
|
|
// We don't need it
|
|
|
|
this.subMenu.menu.removeClass('vjs-hidden')
|
2018-03-30 17:40:00 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Create the component's DOM element
|
|
|
|
*
|
|
|
|
* @return {Element}
|
|
|
|
* @method createEl
|
|
|
|
*/
|
|
|
|
createEl () {
|
2020-01-28 17:29:50 +01:00
|
|
|
const el = videojs.dom.createEl('li', {
|
2018-03-30 17:40:00 +02:00
|
|
|
className: 'vjs-menu-item'
|
|
|
|
})
|
|
|
|
|
2020-01-28 17:29:50 +01:00
|
|
|
this.settingsSubMenuTitleEl_ = videojs.dom.createEl('div', {
|
2018-03-30 17:40:00 +02:00
|
|
|
className: 'vjs-settings-sub-menu-title'
|
2020-01-28 17:29:50 +01:00
|
|
|
}) as HTMLElement
|
2018-03-30 17:40:00 +02:00
|
|
|
|
|
|
|
el.appendChild(this.settingsSubMenuTitleEl_)
|
|
|
|
|
2020-01-28 17:29:50 +01:00
|
|
|
this.settingsSubMenuValueEl_ = videojs.dom.createEl('div', {
|
2018-03-30 17:40:00 +02:00
|
|
|
className: 'vjs-settings-sub-menu-value'
|
2020-01-28 17:29:50 +01:00
|
|
|
}) as HTMLElement
|
2018-03-30 17:40:00 +02:00
|
|
|
|
|
|
|
el.appendChild(this.settingsSubMenuValueEl_)
|
|
|
|
|
2020-01-28 17:29:50 +01:00
|
|
|
this.settingsSubMenuEl_ = videojs.dom.createEl('div', {
|
2018-03-30 17:40:00 +02:00
|
|
|
className: 'vjs-settings-sub-menu'
|
2020-01-28 17:29:50 +01:00
|
|
|
}) as HTMLElement
|
2018-03-30 17:40:00 +02:00
|
|
|
|
2020-01-28 17:29:50 +01:00
|
|
|
return el as HTMLLIElement
|
2018-03-30 17:40:00 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Handle click on menu item
|
|
|
|
*
|
|
|
|
* @method handleClick
|
|
|
|
*/
|
2020-01-28 17:29:50 +01:00
|
|
|
handleClick (event: videojs.EventTarget.Event) {
|
2018-03-30 17:40:00 +02:00
|
|
|
this.menuToLoad = 'submenu'
|
|
|
|
// Remove open class to ensure only the open submenu gets this class
|
2020-01-28 17:29:50 +01:00
|
|
|
videojs.dom.removeClass(this.el(), 'open')
|
2018-03-30 17:40:00 +02:00
|
|
|
|
2020-01-28 17:29:50 +01:00
|
|
|
super.handleClick(event);
|
2018-03-30 17:40:00 +02:00
|
|
|
|
2020-01-28 17:29:50 +01:00
|
|
|
(this.mainMenu.el() as HTMLElement).style.opacity = '0'
|
2018-03-30 17:40:00 +02:00
|
|
|
// Whether to add or remove vjs-hidden class on the settingsSubMenuEl element
|
2020-01-28 17:29:50 +01:00
|
|
|
if (videojs.dom.hasClass(this.settingsSubMenuEl_, 'vjs-hidden')) {
|
|
|
|
videojs.dom.removeClass(this.settingsSubMenuEl_, 'vjs-hidden')
|
2018-03-30 17:40:00 +02:00
|
|
|
|
|
|
|
// animation not played without timeout
|
|
|
|
setTimeout(() => {
|
|
|
|
this.settingsSubMenuEl_.style.opacity = '1'
|
|
|
|
this.settingsSubMenuEl_.style.marginRight = '0px'
|
|
|
|
}, 0)
|
|
|
|
|
|
|
|
this.settingsButton.setDialogSize(this.size)
|
|
|
|
} else {
|
2020-01-28 17:29:50 +01:00
|
|
|
videojs.dom.addClass(this.settingsSubMenuEl_, 'vjs-hidden')
|
2018-03-30 17:40:00 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Create back button
|
|
|
|
*
|
|
|
|
* @method createBackButton
|
|
|
|
*/
|
|
|
|
createBackButton () {
|
|
|
|
const button = this.subMenu.menu.addChild('MenuItem', {}, 0)
|
2020-01-28 17:29:50 +01:00
|
|
|
|
|
|
|
button.addClass('vjs-back-button');
|
|
|
|
(button.el() as HTMLElement).innerHTML = this.player().localize(this.subMenu.controlText())
|
2018-03-30 17:40:00 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Add/remove prefixed event listener for CSS Transition
|
|
|
|
*
|
|
|
|
* @method PrefixedEvent
|
|
|
|
*/
|
2018-10-18 09:08:59 +02:00
|
|
|
PrefixedEvent (element: any, type: any, callback: any, action = 'addEvent') {
|
2020-01-28 17:29:50 +01:00
|
|
|
const prefix = [ 'webkit', 'moz', 'MS', 'o', '' ]
|
2018-03-30 17:40:00 +02:00
|
|
|
|
|
|
|
for (let p = 0; p < prefix.length; p++) {
|
2020-01-28 17:29:50 +01:00
|
|
|
if (!prefix[ p ]) {
|
2018-03-30 17:40:00 +02:00
|
|
|
type = type.toLowerCase()
|
|
|
|
}
|
|
|
|
|
|
|
|
if (action === 'addEvent') {
|
2020-01-28 17:29:50 +01:00
|
|
|
element.addEventListener(prefix[ p ] + type, callback, false)
|
2018-03-30 17:40:00 +02:00
|
|
|
} else if (action === 'removeEvent') {
|
2020-01-28 17:29:50 +01:00
|
|
|
element.removeEventListener(prefix[ p ] + type, callback, false)
|
2018-03-30 17:40:00 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-10-18 09:08:59 +02:00
|
|
|
onTransitionEnd (event: any) {
|
2018-03-30 17:40:00 +02:00
|
|
|
if (event.propertyName !== 'margin-right') {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if (this.menuToLoad === 'mainmenu') {
|
|
|
|
// hide submenu
|
2020-01-28 17:29:50 +01:00
|
|
|
videojs.dom.addClass(this.settingsSubMenuEl_, 'vjs-hidden')
|
2018-03-30 17:40:00 +02:00
|
|
|
|
|
|
|
// reset opacity to 0
|
|
|
|
this.settingsSubMenuEl_.style.opacity = '0'
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
reset () {
|
2020-01-28 17:29:50 +01:00
|
|
|
videojs.dom.addClass(this.settingsSubMenuEl_, 'vjs-hidden')
|
2018-03-30 17:40:00 +02:00
|
|
|
this.settingsSubMenuEl_.style.opacity = '0'
|
|
|
|
this.setMargin()
|
|
|
|
}
|
|
|
|
|
|
|
|
loadMainMenu () {
|
2020-01-28 17:29:50 +01:00
|
|
|
const mainMenuEl = this.mainMenu.el() as HTMLElement
|
2018-03-30 17:40:00 +02:00
|
|
|
this.menuToLoad = 'mainmenu'
|
|
|
|
this.mainMenu.show()
|
2020-01-28 17:29:50 +01:00
|
|
|
mainMenuEl.style.opacity = '0'
|
2018-03-30 17:40:00 +02:00
|
|
|
|
|
|
|
// back button will always take you to main menu, so set dialog sizes
|
2020-01-28 17:29:50 +01:00
|
|
|
const mainMenuAny = this.mainMenu as any
|
|
|
|
this.settingsButton.setDialogSize([ mainMenuAny.width, mainMenuAny.height ])
|
2018-03-30 17:40:00 +02:00
|
|
|
|
|
|
|
// animation not triggered without timeout (some async stuff ?!?)
|
|
|
|
setTimeout(() => {
|
|
|
|
// animate margin and opacity before hiding the submenu
|
|
|
|
// this triggers CSS Transition event
|
|
|
|
this.setMargin()
|
2020-01-28 17:29:50 +01:00
|
|
|
mainMenuEl.style.opacity = '1'
|
2018-03-30 17:40:00 +02:00
|
|
|
}, 0)
|
|
|
|
}
|
|
|
|
|
|
|
|
build () {
|
2019-01-23 15:36:45 +01:00
|
|
|
this.subMenu.on('updateLabel', () => {
|
2018-03-30 17:40:00 +02:00
|
|
|
this.update()
|
2019-01-23 15:36:45 +01:00
|
|
|
})
|
2019-01-24 10:16:30 +01:00
|
|
|
this.subMenu.on('menuChanged', () => {
|
|
|
|
this.bindClickEvents()
|
|
|
|
this.setSize()
|
|
|
|
this.update()
|
|
|
|
})
|
2018-03-30 17:40:00 +02:00
|
|
|
|
2020-01-28 17:29:50 +01:00
|
|
|
this.settingsSubMenuTitleEl_.innerHTML = this.player().localize(this.subMenu.controlText())
|
|
|
|
this.settingsSubMenuEl_.appendChild(this.subMenu.menu.el())
|
2018-03-30 17:40:00 +02:00
|
|
|
this.panelChildEl.appendChild(this.settingsSubMenuEl_)
|
|
|
|
this.update()
|
|
|
|
|
|
|
|
this.createBackButton()
|
2019-01-24 10:16:30 +01:00
|
|
|
this.setSize()
|
2018-03-30 17:40:00 +02:00
|
|
|
this.bindClickEvents()
|
|
|
|
|
|
|
|
// prefixed event listeners for CSS TransitionEnd
|
|
|
|
this.PrefixedEvent(
|
|
|
|
this.settingsSubMenuEl_,
|
|
|
|
'TransitionEnd',
|
|
|
|
this.transitionEndHandler,
|
|
|
|
'addEvent'
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2018-10-18 09:08:59 +02:00
|
|
|
update (event?: any) {
|
2018-10-18 14:35:31 +02:00
|
|
|
let target: HTMLElement = null
|
2019-04-02 18:30:26 +02:00
|
|
|
const subMenu = this.subMenu.name()
|
2018-03-30 17:40:00 +02:00
|
|
|
|
|
|
|
if (event && event.type === 'tap') {
|
|
|
|
target = event.target
|
|
|
|
} else if (event) {
|
|
|
|
target = event.currentTarget
|
|
|
|
}
|
|
|
|
|
|
|
|
// Playback rate menu button doesn't get a vjs-selected class
|
|
|
|
// or sets options_['selected'] on the selected playback rate.
|
|
|
|
// Thus we get the submenu value based on the labelEl of playbackRateMenuButton
|
|
|
|
if (subMenu === 'PlaybackRateMenuButton') {
|
2020-01-28 17:29:50 +01:00
|
|
|
const html = (this.subMenu as any).labelEl_.innerHTML
|
|
|
|
setTimeout(() => this.settingsSubMenuValueEl_.innerHTML = html, 250)
|
2018-03-30 17:40:00 +02:00
|
|
|
} else {
|
|
|
|
// Loop trough the submenu items to find the selected child
|
2019-04-02 18:30:26 +02:00
|
|
|
for (const subMenuItem of this.subMenu.menu.children_) {
|
2018-03-30 17:40:00 +02:00
|
|
|
if (!(subMenuItem instanceof component)) {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
2018-04-05 16:15:51 +02:00
|
|
|
if (subMenuItem.hasClass('vjs-selected')) {
|
2020-01-28 17:29:50 +01:00
|
|
|
const subMenuItemUntyped = subMenuItem as any
|
|
|
|
|
2018-04-05 16:15:51 +02:00
|
|
|
// Prefer to use the function
|
2020-01-28 17:29:50 +01:00
|
|
|
if (typeof subMenuItemUntyped.getLabel === 'function') {
|
|
|
|
this.settingsSubMenuValueEl_.innerHTML = subMenuItemUntyped.getLabel()
|
2018-03-30 17:40:00 +02:00
|
|
|
break
|
2018-04-05 16:15:51 +02:00
|
|
|
}
|
2018-03-30 17:40:00 +02:00
|
|
|
|
2020-01-28 17:29:50 +01:00
|
|
|
this.settingsSubMenuValueEl_.innerHTML = subMenuItemUntyped.options_.label
|
2018-03-30 17:40:00 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (target && !target.classList.contains('vjs-back-button')) {
|
|
|
|
this.settingsButton.hideDialog()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
bindClickEvents () {
|
2019-04-02 18:30:26 +02:00
|
|
|
for (const item of this.subMenu.menu.children()) {
|
2018-03-30 17:40:00 +02:00
|
|
|
if (!(item instanceof component)) {
|
|
|
|
continue
|
|
|
|
}
|
2020-01-28 17:29:50 +01:00
|
|
|
item.on([ 'tap', 'click' ], this.submenuClickHandler)
|
2018-03-30 17:40:00 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// save size of submenus on first init
|
|
|
|
// if number of submenu items change dynamically more logic will be needed
|
2019-01-24 10:16:30 +01:00
|
|
|
setSize () {
|
2018-03-30 17:40:00 +02:00
|
|
|
this.dialog.removeClass('vjs-hidden')
|
2020-01-28 17:29:50 +01:00
|
|
|
videojs.dom.removeClass(this.settingsSubMenuEl_, 'vjs-hidden')
|
2018-03-30 17:40:00 +02:00
|
|
|
this.size = this.settingsButton.getComponentSize(this.settingsSubMenuEl_)
|
|
|
|
this.setMargin()
|
|
|
|
this.dialog.addClass('vjs-hidden')
|
2020-01-28 17:29:50 +01:00
|
|
|
videojs.dom.addClass(this.settingsSubMenuEl_, 'vjs-hidden')
|
2018-03-30 17:40:00 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
setMargin () {
|
2019-11-28 11:06:02 +01:00
|
|
|
if (!this.size) return
|
|
|
|
|
2019-04-02 18:30:26 +02:00
|
|
|
const [ width ] = this.size
|
2018-03-30 17:40:00 +02:00
|
|
|
|
|
|
|
this.settingsSubMenuEl_.style.marginRight = `-${width}px`
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Hide the sub menu
|
|
|
|
*/
|
|
|
|
hideSubMenu () {
|
|
|
|
// after removing settings item this.el_ === null
|
2020-01-28 17:29:50 +01:00
|
|
|
if (!this.el()) {
|
2018-03-30 17:40:00 +02:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2020-01-28 17:29:50 +01:00
|
|
|
if (videojs.dom.hasClass(this.el(), 'open')) {
|
|
|
|
videojs.dom.addClass(this.settingsSubMenuEl_, 'vjs-hidden')
|
|
|
|
videojs.dom.removeClass(this.el(), 'open')
|
2018-03-30 17:40:00 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
2020-01-28 17:29:50 +01:00
|
|
|
(SettingsMenuItem as any).prototype.contentElType = 'button'
|
|
|
|
videojs.registerComponent('SettingsMenuItem', SettingsMenuItem)
|
2018-03-30 17:40:00 +02:00
|
|
|
|
|
|
|
export { SettingsMenuItem }
|