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