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