Fix captions in HTTP fallback
[oweals/peertube.git] / client / src / assets / player / settings-menu-item.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 { toTitleCase } from './utils'
9 import { VideoJSComponentInterface, videojsUntyped } from './peertube-videojs-typings'
10
11 const MenuItem: VideoJSComponentInterface = videojsUntyped.getComponent('MenuItem')
12 const component: VideoJSComponentInterface = videojsUntyped.getComponent('Component')
13
14 class SettingsMenuItem extends MenuItem {
15
16   constructor (player: videojs.Player, options: any, entry: string, menuButton: VideoJSComponentInterface) {
17     super(player, options)
18
19     this.settingsButton = menuButton
20     this.dialog = this.settingsButton.dialog
21     this.mainMenu = this.settingsButton.menu
22     this.panel = this.dialog.getChild('settingsPanel')
23     this.panelChild = this.panel.getChild('settingsPanelChild')
24     this.panelChildEl = this.panelChild.el_
25
26     this.size = null
27
28     // keep state of what menu type is loading next
29     this.menuToLoad = 'mainmenu'
30
31     const subMenuName = toTitleCase(entry)
32     const SubMenuComponent = videojsUntyped.getComponent(subMenuName)
33
34     if (!SubMenuComponent) {
35       throw new Error(`Component ${subMenuName} does not exist`)
36     }
37     this.subMenu = new SubMenuComponent(this.player(), options, menuButton, this)
38     const subMenuClass = this.subMenu.buildCSSClass().split(' ')[0]
39     this.settingsSubMenuEl_.className += ' ' + subMenuClass
40
41     this.eventHandlers()
42
43     player.ready(() => {
44       // Voodoo magic for IOS
45       setTimeout(() => {
46         this.build()
47
48         // Update on rate change
49         player.on('ratechange', this.submenuClickHandler)
50
51         if (subMenuName === 'CaptionsButton') {
52           // Hack to regenerate captions on HTTP fallback
53           player.on('captionsChanged', () => {
54             setTimeout(() => {
55               this.settingsSubMenuEl_.innerHTML = ''
56               this.settingsSubMenuEl_.appendChild(this.subMenu.menu.el_)
57               this.update()
58               this.bindClickEvents()
59
60             }, 0)
61           })
62         }
63
64         this.reset()
65       }, 0)
66     })
67   }
68
69   eventHandlers () {
70     this.submenuClickHandler = this.onSubmenuClick.bind(this)
71     this.transitionEndHandler = this.onTransitionEnd.bind(this)
72   }
73
74   onSubmenuClick (event: any) {
75     let target = null
76
77     if (event.type === 'tap') {
78       target = event.target
79     } else {
80       target = event.currentTarget
81     }
82
83     if (target && target.classList.contains('vjs-back-button')) {
84       this.loadMainMenu()
85       return
86     }
87
88     // To update the sub menu value on click, setTimeout is needed because
89     // updating the value is not instant
90     setTimeout(() => this.update(event), 0)
91   }
92
93   /**
94    * Create the component's DOM element
95    *
96    * @return {Element}
97    * @method createEl
98    */
99   createEl () {
100     const el = videojsUntyped.dom.createEl('li', {
101       className: 'vjs-menu-item'
102     })
103
104     this.settingsSubMenuTitleEl_ = videojsUntyped.dom.createEl('div', {
105       className: 'vjs-settings-sub-menu-title'
106     })
107
108     el.appendChild(this.settingsSubMenuTitleEl_)
109
110     this.settingsSubMenuValueEl_ = videojsUntyped.dom.createEl('div', {
111       className: 'vjs-settings-sub-menu-value'
112     })
113
114     el.appendChild(this.settingsSubMenuValueEl_)
115
116     this.settingsSubMenuEl_ = videojsUntyped.dom.createEl('div', {
117       className: 'vjs-settings-sub-menu'
118     })
119
120     return el
121   }
122
123   /**
124    * Handle click on menu item
125    *
126    * @method handleClick
127    */
128   handleClick () {
129     this.menuToLoad = 'submenu'
130     // Remove open class to ensure only the open submenu gets this class
131     videojsUntyped.dom.removeClass(this.el_, 'open')
132
133     super.handleClick()
134
135     this.mainMenu.el_.style.opacity = '0'
136     // Whether to add or remove vjs-hidden class on the settingsSubMenuEl element
137     if (videojsUntyped.dom.hasClass(this.settingsSubMenuEl_, 'vjs-hidden')) {
138       videojsUntyped.dom.removeClass(this.settingsSubMenuEl_, 'vjs-hidden')
139
140       // animation not played without timeout
141       setTimeout(() => {
142         this.settingsSubMenuEl_.style.opacity = '1'
143         this.settingsSubMenuEl_.style.marginRight = '0px'
144       }, 0)
145
146       this.settingsButton.setDialogSize(this.size)
147     } else {
148       videojsUntyped.dom.addClass(this.settingsSubMenuEl_, 'vjs-hidden')
149     }
150   }
151
152   /**
153    * Create back button
154    *
155    * @method createBackButton
156    */
157   createBackButton () {
158     const button = this.subMenu.menu.addChild('MenuItem', {}, 0)
159     button.name_ = 'BackButton'
160     button.addClass('vjs-back-button')
161     button.el_.innerHTML = this.player_.localize(this.subMenu.controlText_)
162   }
163
164   /**
165    * Add/remove prefixed event listener for CSS Transition
166    *
167    * @method PrefixedEvent
168    */
169   PrefixedEvent (element: any, type: any, callback: any, action = 'addEvent') {
170     let prefix = ['webkit', 'moz', 'MS', 'o', '']
171
172     for (let p = 0; p < prefix.length; p++) {
173       if (!prefix[p]) {
174         type = type.toLowerCase()
175       }
176
177       if (action === 'addEvent') {
178         element.addEventListener(prefix[p] + type, callback, false)
179       } else if (action === 'removeEvent') {
180         element.removeEventListener(prefix[p] + type, callback, false)
181       }
182     }
183   }
184
185   onTransitionEnd (event: any) {
186     if (event.propertyName !== 'margin-right') {
187       return
188     }
189
190     if (this.menuToLoad === 'mainmenu') {
191       // hide submenu
192       videojsUntyped.dom.addClass(this.settingsSubMenuEl_, 'vjs-hidden')
193
194       // reset opacity to 0
195       this.settingsSubMenuEl_.style.opacity = '0'
196     }
197   }
198
199   reset () {
200     videojsUntyped.dom.addClass(this.settingsSubMenuEl_, 'vjs-hidden')
201     this.settingsSubMenuEl_.style.opacity = '0'
202     this.setMargin()
203   }
204
205   loadMainMenu () {
206     this.menuToLoad = 'mainmenu'
207     this.mainMenu.show()
208     this.mainMenu.el_.style.opacity = '0'
209
210     // back button will always take you to main menu, so set dialog sizes
211     this.settingsButton.setDialogSize([this.mainMenu.width, this.mainMenu.height])
212
213     // animation not triggered without timeout (some async stuff ?!?)
214     setTimeout(() => {
215       // animate margin and opacity before hiding the submenu
216       // this triggers CSS Transition event
217       this.setMargin()
218       this.mainMenu.el_.style.opacity = '1'
219     }, 0)
220   }
221
222   build () {
223     const saveUpdateLabel = this.subMenu.updateLabel
224     this.subMenu.updateLabel = () => {
225       this.update()
226
227       saveUpdateLabel.call(this.subMenu)
228     }
229
230     this.settingsSubMenuTitleEl_.innerHTML = this.player_.localize(this.subMenu.controlText_)
231     this.settingsSubMenuEl_.appendChild(this.subMenu.menu.el_)
232     this.panelChildEl.appendChild(this.settingsSubMenuEl_)
233     this.update()
234
235     this.createBackButton()
236     this.getSize()
237     this.bindClickEvents()
238
239     // prefixed event listeners for CSS TransitionEnd
240     this.PrefixedEvent(
241       this.settingsSubMenuEl_,
242       'TransitionEnd',
243       this.transitionEndHandler,
244       'addEvent'
245     )
246   }
247
248   update (event?: any) {
249     let target: HTMLElement = null
250     let subMenu = this.subMenu.name()
251
252     if (event && event.type === 'tap') {
253       target = event.target
254     } else if (event) {
255       target = event.currentTarget
256     }
257
258     // Playback rate menu button doesn't get a vjs-selected class
259     // or sets options_['selected'] on the selected playback rate.
260     // Thus we get the submenu value based on the labelEl of playbackRateMenuButton
261     if (subMenu === 'PlaybackRateMenuButton') {
262       setTimeout(() => this.settingsSubMenuValueEl_.innerHTML = this.subMenu.labelEl_.innerHTML, 250)
263     } else {
264       // Loop trough the submenu items to find the selected child
265       for (let subMenuItem of this.subMenu.menu.children_) {
266         if (!(subMenuItem instanceof component)) {
267           continue
268         }
269
270         if (subMenuItem.hasClass('vjs-selected')) {
271           // Prefer to use the function
272           if (typeof subMenuItem.getLabel === 'function') {
273             this.settingsSubMenuValueEl_.innerHTML = subMenuItem.getLabel()
274             break
275           }
276
277           this.settingsSubMenuValueEl_.innerHTML = subMenuItem.options_.label
278         }
279       }
280     }
281
282     if (target && !target.classList.contains('vjs-back-button')) {
283       this.settingsButton.hideDialog()
284     }
285   }
286
287   bindClickEvents () {
288     for (let item of this.subMenu.menu.children()) {
289       if (!(item instanceof component)) {
290         continue
291       }
292       item.on(['tap', 'click'], this.submenuClickHandler)
293     }
294   }
295
296   // save size of submenus on first init
297   // if number of submenu items change dynamically more logic will be needed
298   getSize () {
299     this.dialog.removeClass('vjs-hidden')
300     this.size = this.settingsButton.getComponentSize(this.settingsSubMenuEl_)
301     this.setMargin()
302     this.dialog.addClass('vjs-hidden')
303     videojsUntyped.dom.addClass(this.settingsSubMenuEl_, 'vjs-hidden')
304   }
305
306   setMargin () {
307     let [width] = this.size
308
309     this.settingsSubMenuEl_.style.marginRight = `-${width}px`
310   }
311
312   /**
313    * Hide the sub menu
314    */
315   hideSubMenu () {
316     // after removing settings item this.el_ === null
317     if (!this.el_) {
318       return
319     }
320
321     if (videojsUntyped.dom.hasClass(this.el_, 'open')) {
322       videojsUntyped.dom.addClass(this.settingsSubMenuEl_, 'vjs-hidden')
323       videojsUntyped.dom.removeClass(this.el_, 'open')
324     }
325   }
326
327 }
328
329 SettingsMenuItem.prototype.contentElType = 'button'
330 videojsUntyped.registerComponent('SettingsMenuItem', SettingsMenuItem)
331
332 export { SettingsMenuItem }