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