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