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