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