1 // Author: Yanko Shterev
2 // Thanks https://github.com/yshterev/videojs-settings-menu
4 // FIXME: something weird with our path definition in tsconfig and typings
6 import * as videojs from 'video.js'
8 import { SettingsMenuItem } from './settings-menu-item'
9 import { VideoJSComponentInterface, videojsUntyped } from '../peertube-videojs-typings'
10 import { toTitleCase } from '../utils'
12 const Button: VideoJSComponentInterface = videojsUntyped.getComponent('Button')
13 const Menu: VideoJSComponentInterface = videojsUntyped.getComponent('Menu')
14 const Component: VideoJSComponentInterface = videojsUntyped.getComponent('Component')
16 class SettingsButton extends Button {
17 playerComponent = videojs.Player
24 addSettingsItemHandler: Function
25 disposeSettingsItemHandler: Function
26 playerClickHandler: Function
27 userInactiveHandler: Function
29 constructor (player: videojs.Player, options: any) {
30 super(player, options)
32 this.playerComponent = player
33 this.dialog = this.playerComponent.addChild('settingsDialog')
34 this.dialogEl = this.dialog.el_
36 this.panel = this.dialog.addChild('settingsPanel')
37 this.panelChild = this.panel.addChild('settingsPanelChild')
39 this.addClass('vjs-settings')
40 this.el_.setAttribute('aria-label', 'Settings Button')
43 this.addSettingsItemHandler = this.onAddSettingsItem.bind(this)
44 this.disposeSettingsItemHandler = this.onDisposeSettingsItem.bind(this)
45 this.playerClickHandler = this.onPlayerClick.bind(this)
46 this.userInactiveHandler = this.onUserInactive.bind(this)
52 this.player().one('play', () => this.hideDialog())
55 onPlayerClick (event: MouseEvent) {
56 const element = event.target as HTMLElement
57 if (element.classList.contains('vjs-settings') || element.parentElement.classList.contains('vjs-settings')) {
61 if (!this.dialog.hasClass('vjs-hidden')) {
66 onDisposeSettingsItem (event: any, name: string) {
67 if (name === undefined) {
68 const children = this.menu.children()
70 while (children.length > 0) {
72 this.menu.removeChild(children[0])
75 this.addClass('vjs-hidden')
77 const item = this.menu.getChild(name)
81 this.menu.removeChild(item)
87 if (this.options_.entries.length === 0) {
88 this.addClass('vjs-hidden')
92 onAddSettingsItem (event: any, data: any) {
93 const [ entry, options ] = data
95 this.addMenuItem(entry, options)
96 this.removeClass('vjs-hidden')
100 if (!this.dialog.hasClass('vjs-hidden')) {
106 this.playerComponent.on('click', this.playerClickHandler)
107 this.playerComponent.on('addsettingsitem', this.addSettingsItemHandler)
108 this.playerComponent.on('disposesettingsitem', this.disposeSettingsItemHandler)
109 this.playerComponent.on('userinactive', this.userInactiveHandler)
113 return `vjs-icon-settings ${super.buildCSSClass()}`
117 if (this.dialog.hasClass('vjs-hidden')) {
125 this.player_.peertube().onMenuOpen()
127 this.menu.el_.style.opacity = '1'
130 this.setDialogSize(this.getComponentSize(this.menu))
134 this.player_.peertube().onMenuClosed()
137 this.setDialogSize(this.getComponentSize(this.menu))
138 this.menu.el_.style.opacity = '1'
142 getComponentSize (element: any) {
143 let width: number = null
144 let height: number = null
146 // Could be component or just DOM element
147 if (element instanceof Component) {
148 width = element.el_.offsetWidth
149 height = element.el_.offsetHeight
151 // keep width/height as properties for direct use
152 element.width = width
153 element.height = height
155 width = element.offsetWidth
156 height = element.offsetHeight
159 return [ width, height ]
162 setDialogSize ([ width, height ]: number[]) {
163 if (typeof height !== 'number') {
167 const offset = this.options_.setup.maxHeightOffset
168 const maxHeight = this.playerComponent.el_.offsetHeight - offset
170 if (height > maxHeight) {
173 this.panel.el_.style.maxHeight = `${height}px`
174 } else if (this.panel.el_.style.maxHeight !== '') {
175 this.panel.el_.style.maxHeight = ''
178 this.dialogEl.style.width = `${width}px`
179 this.dialogEl.style.height = `${height}px`
183 this.menu = new Menu(this.player())
184 this.menu.addClass('vjs-main-menu')
185 const entries = this.options_.entries
187 if (entries.length === 0) {
188 this.addClass('vjs-hidden')
189 this.panelChild.addChild(this.menu)
193 for (const entry of entries) {
194 this.addMenuItem(entry, this.options_)
197 this.panelChild.addChild(this.menu)
200 addMenuItem (entry: any, options: any) {
201 const openSubMenu = function (this: any) {
202 if (videojsUntyped.dom.hasClass(this.el_, 'open')) {
203 videojsUntyped.dom.removeClass(this.el_, 'open')
205 videojsUntyped.dom.addClass(this.el_, 'open')
209 options.name = toTitleCase(entry)
210 const settingsMenuItem = new SettingsMenuItem(this.player(), options, entry, this as any)
212 this.menu.addChild(settingsMenuItem)
214 // Hide children to avoid sub menus stacking on top of each other
215 // or having multiple menus open
216 settingsMenuItem.on('click', videojs.bind(this, this.hideChildren))
218 // Whether to add or remove selected class on the settings sub menu element
219 settingsMenuItem.on('click', openSubMenu)
223 for (const menuChild of this.menu.children()) {
229 * Hide all the sub menus
232 for (const menuChild of this.menu.children()) {
233 menuChild.hideSubMenu()
239 class SettingsPanel extends Component {
240 constructor (player: videojs.Player, options: any) {
241 super(player, options)
245 return super.createEl('div', {
246 className: 'vjs-settings-panel',
253 class SettingsPanelChild extends Component {
254 constructor (player: videojs.Player, options: any) {
255 super(player, options)
259 return super.createEl('div', {
260 className: 'vjs-settings-panel-child',
267 class SettingsDialog extends Component {
268 constructor (player: videojs.Player, options: any) {
269 super(player, options)
274 * Create the component's DOM element
280 const uniqueId = this.id_
281 const dialogLabelId = 'TTsettingsDialogLabel-' + uniqueId
282 const dialogDescriptionId = 'TTsettingsDialogDescription-' + uniqueId
284 return super.createEl('div', {
285 className: 'vjs-settings-dialog vjs-modal-overlay',
290 'aria-labelledby': dialogLabelId,
291 'aria-describedby': dialogDescriptionId
297 SettingsButton.prototype.controlText_ = 'Settings'
299 Component.registerComponent('SettingsButton', SettingsButton)
300 Component.registerComponent('SettingsDialog', SettingsDialog)
301 Component.registerComponent('SettingsPanel', SettingsPanel)
302 Component.registerComponent('SettingsPanelChild', SettingsPanelChild)
304 export { SettingsButton, SettingsDialog, SettingsPanel, SettingsPanelChild }