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