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