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