1 // Thanks to Yanko Shterev: https://github.com/yshterev/videojs-settings-menu
2 import { SettingsMenuItem } from './settings-menu-item'
3 import { toTitleCase } from '../utils'
4 import videojs from 'video.js'
6 import { SettingsDialog } from './settings-dialog'
7 import { SettingsPanel } from './settings-panel'
8 import { SettingsPanelChild } from './settings-panel-child'
10 const Button = videojs.getComponent('Button')
11 const Menu = videojs.getComponent('Menu')
12 const Component = videojs.getComponent('Component')
14 export interface SettingsButtonOptions extends videojs.ComponentOptions {
17 maxHeightOffset: number
21 class SettingsButton extends Button {
22 dialog: SettingsDialog
26 panelChild: SettingsPanelChild
28 addSettingsItemHandler: typeof SettingsButton.prototype.onAddSettingsItem
29 disposeSettingsItemHandler: typeof SettingsButton.prototype.onDisposeSettingsItem
30 documentClickHandler: typeof SettingsButton.prototype.onDocumentClick
31 userInactiveHandler: typeof SettingsButton.prototype.onUserInactive
33 private settingsButtonOptions: SettingsButtonOptions
35 constructor (player: videojs.Player, options?: SettingsButtonOptions) {
36 super(player, options)
38 this.settingsButtonOptions = options
40 this.controlText('Settings')
42 this.dialog = this.player().addChild('settingsDialog')
43 this.dialogEl = this.dialog.el() as HTMLElement
45 this.panel = this.dialog.addChild('settingsPanel')
46 this.panelChild = this.panel.addChild('settingsPanelChild')
48 this.addClass('vjs-settings')
49 this.el().setAttribute('aria-label', 'Settings Button')
52 this.addSettingsItemHandler = this.onAddSettingsItem.bind(this)
53 this.disposeSettingsItemHandler = this.onDisposeSettingsItem.bind(this)
54 this.documentClickHandler = this.onDocumentClick.bind(this)
55 this.userInactiveHandler = this.onUserInactive.bind(this)
61 this.player().one('play', () => this.hideDialog())
64 onDocumentClick (event: MouseEvent) {
65 const element = event.target as HTMLElement
67 if (element?.classList?.contains('vjs-settings') || element?.parentElement?.classList?.contains('vjs-settings')) {
71 if (!this.dialog.hasClass('vjs-hidden')) {
76 onDisposeSettingsItem (event: any, name: string) {
77 if (name === undefined) {
78 const children = this.menu.children()
80 while (children.length > 0) {
82 this.menu.removeChild(children[0])
85 this.addClass('vjs-hidden')
87 const item = this.menu.getChild(name)
91 this.menu.removeChild(item)
97 if (this.settingsButtonOptions.entries.length === 0) {
98 this.addClass('vjs-hidden')
103 document.removeEventListener('click', this.documentClickHandler)
105 if (this.isInIframe()) {
106 window.removeEventListener('blur', this.documentClickHandler)
110 onAddSettingsItem (event: any, data: any) {
111 const [ entry, options ] = data
113 this.addMenuItem(entry, options)
114 this.removeClass('vjs-hidden')
118 if (!this.dialog.hasClass('vjs-hidden')) {
124 document.addEventListener('click', this.documentClickHandler)
125 if (this.isInIframe()) {
126 window.addEventListener('blur', this.documentClickHandler)
129 this.player().on('addsettingsitem', this.addSettingsItemHandler)
130 this.player().on('disposesettingsitem', this.disposeSettingsItemHandler)
131 this.player().on('userinactive', this.userInactiveHandler)
135 return `vjs-icon-settings ${super.buildCSSClass()}`
139 if (this.dialog.hasClass('vjs-hidden')) {
147 this.player().peertube().onMenuOpen();
149 (this.menu.el() as HTMLElement).style.opacity = '1'
152 this.setDialogSize(this.getComponentSize(this.menu))
156 this.player_.peertube().onMenuClosed()
159 this.setDialogSize(this.getComponentSize(this.menu));
160 (this.menu.el() as HTMLElement).style.opacity = '1'
164 getComponentSize (element: videojs.Component | HTMLElement) {
165 let width: number = null
166 let height: number = null
168 // Could be component or just DOM element
169 if (element instanceof Component) {
170 const el = element.el() as HTMLElement
172 width = el.offsetWidth
173 height = el.offsetHeight;
175 (element as any).width = width;
176 (element as any).height = height
178 width = element.offsetWidth
179 height = element.offsetHeight
182 return [ width, height ]
185 setDialogSize ([ width, height ]: number[]) {
186 if (typeof height !== 'number') {
190 const offset = this.settingsButtonOptions.setup.maxHeightOffset
191 const maxHeight = (this.player().el() as HTMLElement).offsetHeight - offset
193 const panelEl = this.panel.el() as HTMLElement
195 if (height > maxHeight) {
198 panelEl.style.maxHeight = `${height}px`
199 } else if (panelEl.style.maxHeight !== '') {
200 panelEl.style.maxHeight = ''
203 this.dialogEl.style.width = `${width}px`
204 this.dialogEl.style.height = `${height}px`
208 this.menu = new Menu(this.player())
209 this.menu.addClass('vjs-main-menu')
210 const entries = this.settingsButtonOptions.entries
212 if (entries.length === 0) {
213 this.addClass('vjs-hidden')
214 this.panelChild.addChild(this.menu)
218 for (const entry of entries) {
219 this.addMenuItem(entry, this.settingsButtonOptions)
222 this.panelChild.addChild(this.menu)
225 addMenuItem (entry: any, options: any) {
226 const openSubMenu = function (this: any) {
227 if (videojs.dom.hasClass(this.el_, 'open')) {
228 videojs.dom.removeClass(this.el_, 'open')
230 videojs.dom.addClass(this.el_, 'open')
234 options.name = toTitleCase(entry)
236 const newOptions = Object.assign({}, options, { entry, menuButton: this })
237 const settingsMenuItem = new SettingsMenuItem(this.player(), newOptions)
239 this.menu.addChild(settingsMenuItem)
241 // Hide children to avoid sub menus stacking on top of each other
242 // or having multiple menus open
243 settingsMenuItem.on('click', videojs.bind(this, this.hideChildren))
245 // Whether to add or remove selected class on the settings sub menu element
246 settingsMenuItem.on('click', openSubMenu)
250 for (const menuChild of this.menu.children()) {
251 (menuChild as SettingsMenuItem).reset()
256 * Hide all the sub menus
259 for (const menuChild of this.menu.children()) {
260 (menuChild as SettingsMenuItem).hideSubMenu()
265 return window.self !== window.top
270 Component.registerComponent('SettingsButton', SettingsButton)
272 export { SettingsButton }