1 // Author: Yanko Shterev
2 // Thanks https://github.com/yshterev/videojs-settings-menu
4 // FIXME: something weird with our path definition in tsconfig and typings
6 import * as videojs from 'video.js'
8 import { toTitleCase } from '../utils'
9 import { VideoJSComponentInterface, videojsUntyped } from '../peertube-videojs-typings'
11 const MenuItem: VideoJSComponentInterface = videojsUntyped.getComponent('MenuItem')
12 const component: VideoJSComponentInterface = videojsUntyped.getComponent('Component')
14 class SettingsMenuItem extends MenuItem {
25 submenuClickHandler: Function
26 transitionEndHandler: Function
28 settingsSubMenuTitleEl_: any
29 settingsSubMenuValueEl_: any
30 settingsSubMenuEl_: any
32 constructor (player: videojs.Player, options: any, entry: string, menuButton: VideoJSComponentInterface) {
33 super(player, options)
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_
44 // keep state of what menu type is loading next
45 this.menuToLoad = 'mainmenu'
47 const subMenuName = toTitleCase(entry)
48 const SubMenuComponent = videojsUntyped.getComponent(subMenuName)
50 if (!SubMenuComponent) {
51 throw new Error(`Component ${subMenuName} does not exist`)
53 this.subMenu = new SubMenuComponent(this.player(), options, menuButton, this)
54 const subMenuClass = this.subMenu.buildCSSClass().split(' ')[0]
55 this.settingsSubMenuEl_.className += ' ' + subMenuClass
60 // Voodoo magic for IOS
62 // Player was destroyed
63 if (!this.player_) return
67 // Update on rate change
68 player.on('ratechange', this.submenuClickHandler)
70 if (subMenuName === 'CaptionsButton') {
71 // Hack to regenerate captions on HTTP fallback
72 player.on('captionsChanged', () => {
74 this.settingsSubMenuEl_.innerHTML = ''
75 this.settingsSubMenuEl_.appendChild(this.subMenu.menu.el_)
77 this.bindClickEvents()
88 this.submenuClickHandler = this.onSubmenuClick.bind(this)
89 this.transitionEndHandler = this.onTransitionEnd.bind(this)
92 onSubmenuClick (event: any) {
95 if (event.type === 'tap') {
98 target = event.currentTarget
101 if (target && target.classList.contains('vjs-back-button')) {
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)
110 // Seems like videojs adds a vjs-hidden class on the caption menu after a click
112 this.subMenu.menu.removeClass('vjs-hidden')
116 * Create the component's DOM element
122 const el = videojsUntyped.dom.createEl('li', {
123 className: 'vjs-menu-item'
126 this.settingsSubMenuTitleEl_ = videojsUntyped.dom.createEl('div', {
127 className: 'vjs-settings-sub-menu-title'
130 el.appendChild(this.settingsSubMenuTitleEl_)
132 this.settingsSubMenuValueEl_ = videojsUntyped.dom.createEl('div', {
133 className: 'vjs-settings-sub-menu-value'
136 el.appendChild(this.settingsSubMenuValueEl_)
138 this.settingsSubMenuEl_ = videojsUntyped.dom.createEl('div', {
139 className: 'vjs-settings-sub-menu'
146 * Handle click on menu item
148 * @method 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')
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')
162 // animation not played without timeout
164 this.settingsSubMenuEl_.style.opacity = '1'
165 this.settingsSubMenuEl_.style.marginRight = '0px'
168 this.settingsButton.setDialogSize(this.size)
170 videojsUntyped.dom.addClass(this.settingsSubMenuEl_, 'vjs-hidden')
177 * @method createBackButton
179 createBackButton () {
180 const button = this.subMenu.menu.addChild('MenuItem', {}, 0)
181 button.name_ = 'BackButton'
182 button.addClass('vjs-back-button')
183 button.el_.innerHTML = this.player_.localize(this.subMenu.controlText_)
187 * Add/remove prefixed event listener for CSS Transition
189 * @method PrefixedEvent
191 PrefixedEvent (element: any, type: any, callback: any, action = 'addEvent') {
192 const prefix = ['webkit', 'moz', 'MS', 'o', '']
194 for (let p = 0; p < prefix.length; p++) {
196 type = type.toLowerCase()
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)
207 onTransitionEnd (event: any) {
208 if (event.propertyName !== 'margin-right') {
212 if (this.menuToLoad === 'mainmenu') {
214 videojsUntyped.dom.addClass(this.settingsSubMenuEl_, 'vjs-hidden')
216 // reset opacity to 0
217 this.settingsSubMenuEl_.style.opacity = '0'
222 videojsUntyped.dom.addClass(this.settingsSubMenuEl_, 'vjs-hidden')
223 this.settingsSubMenuEl_.style.opacity = '0'
228 this.menuToLoad = 'mainmenu'
230 this.mainMenu.el_.style.opacity = '0'
232 // back button will always take you to main menu, so set dialog sizes
233 this.settingsButton.setDialogSize([this.mainMenu.width, this.mainMenu.height])
235 // animation not triggered without timeout (some async stuff ?!?)
237 // animate margin and opacity before hiding the submenu
238 // this triggers CSS Transition event
240 this.mainMenu.el_.style.opacity = '1'
245 this.subMenu.on('updateLabel', () => {
248 this.subMenu.on('menuChanged', () => {
249 this.bindClickEvents()
254 this.settingsSubMenuTitleEl_.innerHTML = this.player_.localize(this.subMenu.controlText_)
255 this.settingsSubMenuEl_.appendChild(this.subMenu.menu.el_)
256 this.panelChildEl.appendChild(this.settingsSubMenuEl_)
259 this.createBackButton()
261 this.bindClickEvents()
263 // prefixed event listeners for CSS TransitionEnd
265 this.settingsSubMenuEl_,
267 this.transitionEndHandler,
272 update (event?: any) {
273 let target: HTMLElement = null
274 const subMenu = this.subMenu.name()
276 if (event && event.type === 'tap') {
277 target = event.target
279 target = event.currentTarget
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)
288 // Loop trough the submenu items to find the selected child
289 for (const subMenuItem of this.subMenu.menu.children_) {
290 if (!(subMenuItem instanceof component)) {
294 if (subMenuItem.hasClass('vjs-selected')) {
295 // Prefer to use the function
296 if (typeof subMenuItem.getLabel === 'function') {
297 this.settingsSubMenuValueEl_.innerHTML = subMenuItem.getLabel()
301 this.settingsSubMenuValueEl_.innerHTML = subMenuItem.options_.label
306 if (target && !target.classList.contains('vjs-back-button')) {
307 this.settingsButton.hideDialog()
312 for (const item of this.subMenu.menu.children()) {
313 if (!(item instanceof component)) {
316 item.on(['tap', 'click'], this.submenuClickHandler)
320 // save size of submenus on first init
321 // if number of submenu items change dynamically more logic will be needed
323 this.dialog.removeClass('vjs-hidden')
324 videojsUntyped.dom.removeClass(this.settingsSubMenuEl_, 'vjs-hidden')
325 this.size = this.settingsButton.getComponentSize(this.settingsSubMenuEl_)
327 this.dialog.addClass('vjs-hidden')
328 videojsUntyped.dom.addClass(this.settingsSubMenuEl_, 'vjs-hidden')
332 const [ width ] = this.size
334 this.settingsSubMenuEl_.style.marginRight = `-${width}px`
341 // after removing settings item this.el_ === null
346 if (videojsUntyped.dom.hasClass(this.el_, 'open')) {
347 videojsUntyped.dom.addClass(this.settingsSubMenuEl_, 'vjs-hidden')
348 videojsUntyped.dom.removeClass(this.el_, 'open')
354 SettingsMenuItem.prototype.contentElType = 'button'
355 videojsUntyped.registerComponent('SettingsMenuItem', SettingsMenuItem)
357 export { SettingsMenuItem }