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