Automatic resolution according to user bandwidth V1
[oweals/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         if (subMenuItem.hasClass('vjs-selected')) {
245           // Prefer to use the function
246           if (typeof subMenuItem.getLabel === 'function') {
247             this.settingsSubMenuValueEl_.innerHTML = subMenuItem.getLabel()
248             break
249           }
250
251           this.settingsSubMenuValueEl_.innerHTML = subMenuItem.options_.label
252         }
253       }
254     }
255
256     if (target && !target.classList.contains('vjs-back-button')) {
257       this.settingsButton.hideDialog()
258     }
259   }
260
261   bindClickEvents () {
262     for (let item of this.subMenu.menu.children()) {
263       if (!(item instanceof component)) {
264         continue
265       }
266       item.on(['tap', 'click'], this.submenuClickHandler)
267     }
268   }
269
270   // save size of submenus on first init
271   // if number of submenu items change dynamically more logic will be needed
272   getSize () {
273     this.dialog.removeClass('vjs-hidden')
274     this.size = this.settingsButton.getComponentSize(this.settingsSubMenuEl_)
275     this.setMargin()
276     this.dialog.addClass('vjs-hidden')
277     videojsUntyped.dom.addClass(this.settingsSubMenuEl_, 'vjs-hidden')
278   }
279
280   setMargin () {
281     let [width] = this.size
282
283     this.settingsSubMenuEl_.style.marginRight = `-${width}px`
284   }
285
286   /**
287    * Hide the sub menu
288    */
289   hideSubMenu () {
290     // after removing settings item this.el_ === null
291     if (!this.el_) {
292       return
293     }
294
295     if (videojsUntyped.dom.hasClass(this.el_, 'open')) {
296       videojsUntyped.dom.addClass(this.settingsSubMenuEl_, 'vjs-hidden')
297       videojsUntyped.dom.removeClass(this.el_, 'open')
298     }
299   }
300
301 }
302
303 SettingsMenuItem.prototype.contentElType = 'button'
304 videojsUntyped.registerComponent('SettingsMenuItem', SettingsMenuItem)
305
306 export { SettingsMenuItem }