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