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