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