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