Fix captions in HTTP fallback
[oweals/peertube.git] / client / src / assets / player / settings-menu-button.ts
1 // Author: Yanko Shterev
2 // Thanks https://github.com/yshterev/videojs-settings-menu
3
4 // FIXME: something weird with our path definition in tsconfig and typings
5 // @ts-ignore
6 import * as videojs from 'video.js'
7
8 import { SettingsMenuItem } from './settings-menu-item'
9 import { VideoJSComponentInterface, videojsUntyped } from './peertube-videojs-typings'
10 import { toTitleCase } from './utils'
11
12 const Button: VideoJSComponentInterface = videojsUntyped.getComponent('Button')
13 const Menu: VideoJSComponentInterface = videojsUntyped.getComponent('Menu')
14 const Component: VideoJSComponentInterface = videojsUntyped.getComponent('Component')
15
16 class SettingsButton extends Button {
17   constructor (player: videojs.Player, options: any) {
18     super(player, options)
19
20     this.playerComponent = player
21     this.dialog = this.playerComponent.addChild('settingsDialog')
22     this.dialogEl = this.dialog.el_
23     this.menu = null
24     this.panel = this.dialog.addChild('settingsPanel')
25     this.panelChild = this.panel.addChild('settingsPanelChild')
26
27     this.addClass('vjs-settings')
28     this.el_.setAttribute('aria-label', 'Settings Button')
29
30     // Event handlers
31     this.addSettingsItemHandler = this.onAddSettingsItem.bind(this)
32     this.disposeSettingsItemHandler = this.onDisposeSettingsItem.bind(this)
33     this.playerClickHandler = this.onPlayerClick.bind(this)
34     this.userInactiveHandler = this.onUserInactive.bind(this)
35
36     this.buildMenu()
37     this.bindEvents()
38
39     // Prepare the dialog
40     this.player().one('play', () => this.hideDialog())
41   }
42
43   onPlayerClick (event: MouseEvent) {
44     const element = event.target as HTMLElement
45     if (element.classList.contains('vjs-settings') || element.parentElement.classList.contains('vjs-settings')) {
46       return
47     }
48
49     if (!this.dialog.hasClass('vjs-hidden')) {
50       this.hideDialog()
51     }
52   }
53
54   onDisposeSettingsItem (event: any, name: string) {
55     if (name === undefined) {
56       let children = this.menu.children()
57
58       while (children.length > 0) {
59         children[0].dispose()
60         this.menu.removeChild(children[0])
61       }
62
63       this.addClass('vjs-hidden')
64     } else {
65       let item = this.menu.getChild(name)
66
67       if (item) {
68         item.dispose()
69         this.menu.removeChild(item)
70       }
71     }
72
73     this.hideDialog()
74
75     if (this.options_.entries.length === 0) {
76       this.addClass('vjs-hidden')
77     }
78   }
79
80   onAddSettingsItem (event: any, data: any) {
81     const [ entry, options ] = data
82
83     this.addMenuItem(entry, options)
84     this.removeClass('vjs-hidden')
85   }
86
87   onUserInactive () {
88     if (!this.dialog.hasClass('vjs-hidden')) {
89       this.hideDialog()
90     }
91   }
92
93   bindEvents () {
94     this.playerComponent.on('click', this.playerClickHandler)
95     this.playerComponent.on('addsettingsitem', this.addSettingsItemHandler)
96     this.playerComponent.on('disposesettingsitem', this.disposeSettingsItemHandler)
97     this.playerComponent.on('userinactive', this.userInactiveHandler)
98   }
99
100   buildCSSClass () {
101     return `vjs-icon-settings ${super.buildCSSClass()}`
102   }
103
104   handleClick () {
105     if (this.dialog.hasClass('vjs-hidden')) {
106       this.showDialog()
107     } else {
108       this.hideDialog()
109     }
110   }
111
112   showDialog () {
113     this.menu.el_.style.opacity = '1'
114     this.dialog.show()
115
116     this.setDialogSize(this.getComponentSize(this.menu))
117   }
118
119   hideDialog () {
120     this.dialog.hide()
121     this.setDialogSize(this.getComponentSize(this.menu))
122     this.menu.el_.style.opacity = '1'
123     this.resetChildren()
124   }
125
126   getComponentSize (element: any) {
127     let width: number = null
128     let height: number = null
129
130     // Could be component or just DOM element
131     if (element instanceof Component) {
132       width = element.el_.offsetWidth
133       height = element.el_.offsetHeight
134
135       // keep width/height as properties for direct use
136       element.width = width
137       element.height = height
138     } else {
139       width = element.offsetWidth
140       height = element.offsetHeight
141     }
142
143     return [ width, height ]
144   }
145
146   setDialogSize ([ width, height ]: number[]) {
147     if (typeof height !== 'number') {
148       return
149     }
150
151     let offset = this.options_.setup.maxHeightOffset
152     let maxHeight = this.playerComponent.el_.offsetHeight - offset
153
154     if (height > maxHeight) {
155       height = maxHeight
156       width += 17
157       this.panel.el_.style.maxHeight = `${height}px`
158     } else if (this.panel.el_.style.maxHeight !== '') {
159       this.panel.el_.style.maxHeight = ''
160     }
161
162     this.dialogEl.style.width = `${width}px`
163     this.dialogEl.style.height = `${height}px`
164   }
165
166   buildMenu () {
167     this.menu = new Menu(this.player())
168     this.menu.addClass('vjs-main-menu')
169     let entries = this.options_.entries
170
171     if (entries.length === 0) {
172       this.addClass('vjs-hidden')
173       this.panelChild.addChild(this.menu)
174       return
175     }
176
177     for (let entry of entries) {
178       this.addMenuItem(entry, this.options_)
179     }
180
181     this.panelChild.addChild(this.menu)
182   }
183
184   addMenuItem (entry: any, options: any) {
185     const openSubMenu = function (this: any) {
186       if (videojsUntyped.dom.hasClass(this.el_, 'open')) {
187         videojsUntyped.dom.removeClass(this.el_, 'open')
188       } else {
189         videojsUntyped.dom.addClass(this.el_, 'open')
190       }
191     }
192
193     options.name = toTitleCase(entry)
194     let settingsMenuItem = new SettingsMenuItem(this.player(), options, entry, this as any)
195
196     this.menu.addChild(settingsMenuItem)
197
198     // Hide children to avoid sub menus stacking on top of each other
199     // or having multiple menus open
200     settingsMenuItem.on('click', videojs.bind(this, this.hideChildren))
201
202     // Whether to add or remove selected class on the settings sub menu element
203     settingsMenuItem.on('click', openSubMenu)
204   }
205
206   resetChildren () {
207     for (let menuChild of this.menu.children()) {
208       menuChild.reset()
209     }
210   }
211
212   /**
213    * Hide all the sub menus
214    */
215   hideChildren () {
216     for (let menuChild of this.menu.children()) {
217       menuChild.hideSubMenu()
218     }
219   }
220
221 }
222
223 class SettingsPanel extends Component {
224   constructor (player: videojs.Player, options: any) {
225     super(player, options)
226   }
227
228   createEl () {
229     return super.createEl('div', {
230       className: 'vjs-settings-panel',
231       innerHTML: '',
232       tabIndex: -1
233     })
234   }
235 }
236
237 class SettingsPanelChild extends Component {
238   constructor (player: videojs.Player, options: any) {
239     super(player, options)
240   }
241
242   createEl () {
243     return super.createEl('div', {
244       className: 'vjs-settings-panel-child',
245       innerHTML: '',
246       tabIndex: -1
247     })
248   }
249 }
250
251 class SettingsDialog extends Component {
252   constructor (player: videojs.Player, options: any) {
253     super(player, options)
254     this.hide()
255   }
256
257   /**
258    * Create the component's DOM element
259    *
260    * @return {Element}
261    * @method createEl
262    */
263   createEl () {
264     const uniqueId = this.id_
265     const dialogLabelId = 'TTsettingsDialogLabel-' + uniqueId
266     const dialogDescriptionId = 'TTsettingsDialogDescription-' + uniqueId
267
268     return super.createEl('div', {
269       className: 'vjs-settings-dialog vjs-modal-overlay',
270       innerHTML: '',
271       tabIndex: -1
272     }, {
273       'role': 'dialog',
274       'aria-labelledby': dialogLabelId,
275       'aria-describedby': dialogDescriptionId
276     })
277   }
278
279 }
280
281 SettingsButton.prototype.controlText_ = 'Settings'
282
283 Component.registerComponent('SettingsButton', SettingsButton)
284 Component.registerComponent('SettingsDialog', SettingsDialog)
285 Component.registerComponent('SettingsPanel', SettingsPanel)
286 Component.registerComponent('SettingsPanelChild', SettingsPanelChild)
287
288 export { SettingsButton, SettingsDialog, SettingsPanel, SettingsPanelChild }