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