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