// Author: Yanko Shterev // Thanks https://github.com/yshterev/videojs-settings-menu // FIXME: something weird with our path definition in tsconfig and typings // @ts-ignore import * as videojs from 'video.js' import { SettingsMenuItem } from './settings-menu-item' import { VideoJSComponentInterface, videojsUntyped } from '../peertube-videojs-typings' import { toTitleCase } from '../utils' const Button: VideoJSComponentInterface = videojsUntyped.getComponent('Button') const Menu: VideoJSComponentInterface = videojsUntyped.getComponent('Menu') const Component: VideoJSComponentInterface = videojsUntyped.getComponent('Component') class SettingsButton extends Button { constructor (player: videojs.Player, options: any) { super(player, options) this.playerComponent = player this.dialog = this.playerComponent.addChild('settingsDialog') this.dialogEl = this.dialog.el_ this.menu = null this.panel = this.dialog.addChild('settingsPanel') this.panelChild = this.panel.addChild('settingsPanelChild') this.addClass('vjs-settings') this.el_.setAttribute('aria-label', 'Settings Button') // Event handlers this.addSettingsItemHandler = this.onAddSettingsItem.bind(this) this.disposeSettingsItemHandler = this.onDisposeSettingsItem.bind(this) this.playerClickHandler = this.onPlayerClick.bind(this) this.userInactiveHandler = this.onUserInactive.bind(this) this.buildMenu() this.bindEvents() // Prepare the dialog this.player().one('play', () => this.hideDialog()) } onPlayerClick (event: MouseEvent) { const element = event.target as HTMLElement if (element.classList.contains('vjs-settings') || element.parentElement.classList.contains('vjs-settings')) { return } if (!this.dialog.hasClass('vjs-hidden')) { this.hideDialog() } } onDisposeSettingsItem (event: any, name: string) { if (name === undefined) { let children = this.menu.children() while (children.length > 0) { children[0].dispose() this.menu.removeChild(children[0]) } this.addClass('vjs-hidden') } else { let item = this.menu.getChild(name) if (item) { item.dispose() this.menu.removeChild(item) } } this.hideDialog() if (this.options_.entries.length === 0) { this.addClass('vjs-hidden') } } onAddSettingsItem (event: any, data: any) { const [ entry, options ] = data this.addMenuItem(entry, options) this.removeClass('vjs-hidden') } onUserInactive () { if (!this.dialog.hasClass('vjs-hidden')) { this.hideDialog() } } bindEvents () { this.playerComponent.on('click', this.playerClickHandler) this.playerComponent.on('addsettingsitem', this.addSettingsItemHandler) this.playerComponent.on('disposesettingsitem', this.disposeSettingsItemHandler) this.playerComponent.on('userinactive', this.userInactiveHandler) } buildCSSClass () { return `vjs-icon-settings ${super.buildCSSClass()}` } handleClick () { if (this.dialog.hasClass('vjs-hidden')) { this.showDialog() } else { this.hideDialog() } } showDialog () { this.menu.el_.style.opacity = '1' this.dialog.show() this.setDialogSize(this.getComponentSize(this.menu)) } hideDialog () { this.dialog.hide() this.setDialogSize(this.getComponentSize(this.menu)) this.menu.el_.style.opacity = '1' this.resetChildren() } getComponentSize (element: any) { let width: number = null let height: number = null // Could be component or just DOM element if (element instanceof Component) { width = element.el_.offsetWidth height = element.el_.offsetHeight // keep width/height as properties for direct use element.width = width element.height = height } else { width = element.offsetWidth height = element.offsetHeight } return [ width, height ] } setDialogSize ([ width, height ]: number[]) { if (typeof height !== 'number') { return } let offset = this.options_.setup.maxHeightOffset let maxHeight = this.playerComponent.el_.offsetHeight - offset if (height > maxHeight) { height = maxHeight width += 17 this.panel.el_.style.maxHeight = `${height}px` } else if (this.panel.el_.style.maxHeight !== '') { this.panel.el_.style.maxHeight = '' } this.dialogEl.style.width = `${width}px` this.dialogEl.style.height = `${height}px` } buildMenu () { this.menu = new Menu(this.player()) this.menu.addClass('vjs-main-menu') let entries = this.options_.entries if (entries.length === 0) { this.addClass('vjs-hidden') this.panelChild.addChild(this.menu) return } for (let entry of entries) { this.addMenuItem(entry, this.options_) } this.panelChild.addChild(this.menu) } addMenuItem (entry: any, options: any) { const openSubMenu = function (this: any) { if (videojsUntyped.dom.hasClass(this.el_, 'open')) { videojsUntyped.dom.removeClass(this.el_, 'open') } else { videojsUntyped.dom.addClass(this.el_, 'open') } } options.name = toTitleCase(entry) let settingsMenuItem = new SettingsMenuItem(this.player(), options, entry, this as any) this.menu.addChild(settingsMenuItem) // Hide children to avoid sub menus stacking on top of each other // or having multiple menus open settingsMenuItem.on('click', videojs.bind(this, this.hideChildren)) // Whether to add or remove selected class on the settings sub menu element settingsMenuItem.on('click', openSubMenu) } resetChildren () { for (let menuChild of this.menu.children()) { menuChild.reset() } } /** * Hide all the sub menus */ hideChildren () { for (let menuChild of this.menu.children()) { menuChild.hideSubMenu() } } } class SettingsPanel extends Component { constructor (player: videojs.Player, options: any) { super(player, options) } createEl () { return super.createEl('div', { className: 'vjs-settings-panel', innerHTML: '', tabIndex: -1 }) } } class SettingsPanelChild extends Component { constructor (player: videojs.Player, options: any) { super(player, options) } createEl () { return super.createEl('div', { className: 'vjs-settings-panel-child', innerHTML: '', tabIndex: -1 }) } } class SettingsDialog extends Component { constructor (player: videojs.Player, options: any) { super(player, options) this.hide() } /** * Create the component's DOM element * * @return {Element} * @method createEl */ createEl () { const uniqueId = this.id_ const dialogLabelId = 'TTsettingsDialogLabel-' + uniqueId const dialogDescriptionId = 'TTsettingsDialogDescription-' + uniqueId return super.createEl('div', { className: 'vjs-settings-dialog vjs-modal-overlay', innerHTML: '', tabIndex: -1 }, { 'role': 'dialog', 'aria-labelledby': dialogLabelId, 'aria-describedby': dialogDescriptionId }) } } SettingsButton.prototype.controlText_ = 'Settings' Component.registerComponent('SettingsButton', SettingsButton) Component.registerComponent('SettingsDialog', SettingsDialog) Component.registerComponent('SettingsPanel', SettingsPanel) Component.registerComponent('SettingsPanelChild', SettingsPanelChild) export { SettingsButton, SettingsDialog, SettingsPanel, SettingsPanelChild }