]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - client/src/assets/player/videojs-components/settings-menu-button.ts
Merge branch 'master' into develop
[github/Chocobozzz/PeerTube.git] / client / src / assets / player / videojs-components / settings-menu-button.ts
1 // Author: Yanko Shterev
2 // Thanks https://github.com/yshterev/videojs-settings-menu
3
4 // FIXME: something weird with our path definition in tsconfig and typings
5 // @ts-ignore
6 import * as videojs from 'video.js'
7
8 import { SettingsMenuItem } from './settings-menu-item'
9 import { VideoJSComponentInterface, videojsUntyped } from '../peertube-videojs-typings'
10 import { toTitleCase } from '../utils'
11
12 const Button: VideoJSComponentInterface = videojsUntyped.getComponent('Button')
13 const Menu: VideoJSComponentInterface = videojsUntyped.getComponent('Menu')
14 const Component: VideoJSComponentInterface = videojsUntyped.getComponent('Component')
15
16 class SettingsButton extends Button {
17 constructor (player: videojs.Player, options: any) {
18 super(player, options)
19
20 this.playerComponent = player
21 this.dialog = this.playerComponent.addChild('settingsDialog')
22 this.dialogEl = this.dialog.el_
23 this.menu = null
24 this.panel = this.dialog.addChild('settingsPanel')
25 this.panelChild = this.panel.addChild('settingsPanelChild')
26
27 this.addClass('vjs-settings')
28 this.el_.setAttribute('aria-label', 'Settings Button')
29
30 // Event handlers
31 this.addSettingsItemHandler = this.onAddSettingsItem.bind(this)
32 this.disposeSettingsItemHandler = this.onDisposeSettingsItem.bind(this)
33 this.playerClickHandler = this.onPlayerClick.bind(this)
34 this.userInactiveHandler = this.onUserInactive.bind(this)
35
36 this.buildMenu()
37 this.bindEvents()
38
39 // Prepare the dialog
40 this.player().one('play', () => this.hideDialog())
41 }
42
43 onPlayerClick (event: MouseEvent) {
44 const element = event.target as HTMLElement
45 if (element.classList.contains('vjs-settings') || element.parentElement.classList.contains('vjs-settings')) {
46 return
47 }
48
49 if (!this.dialog.hasClass('vjs-hidden')) {
50 this.hideDialog()
51 }
52 }
53
54 onDisposeSettingsItem (event: any, name: string) {
55 if (name === undefined) {
56 const children = this.menu.children()
57
58 while (children.length > 0) {
59 children[0].dispose()
60 this.menu.removeChild(children[0])
61 }
62
63 this.addClass('vjs-hidden')
64 } else {
65 const item = this.menu.getChild(name)
66
67 if (item) {
68 item.dispose()
69 this.menu.removeChild(item)
70 }
71 }
72
73 this.hideDialog()
74
75 if (this.options_.entries.length === 0) {
76 this.addClass('vjs-hidden')
77 }
78 }
79
80 onAddSettingsItem (event: any, data: any) {
81 const [ entry, options ] = data
82
83 this.addMenuItem(entry, options)
84 this.removeClass('vjs-hidden')
85 }
86
87 onUserInactive () {
88 if (!this.dialog.hasClass('vjs-hidden')) {
89 this.hideDialog()
90 }
91 }
92
93 bindEvents () {
94 this.playerComponent.on('click', this.playerClickHandler)
95 this.playerComponent.on('addsettingsitem', this.addSettingsItemHandler)
96 this.playerComponent.on('disposesettingsitem', this.disposeSettingsItemHandler)
97 this.playerComponent.on('userinactive', this.userInactiveHandler)
98 }
99
100 buildCSSClass () {
101 return `vjs-icon-settings ${super.buildCSSClass()}`
102 }
103
104 handleClick () {
105 if (this.dialog.hasClass('vjs-hidden')) {
106 this.showDialog()
107 } else {
108 this.hideDialog()
109 }
110 }
111
112 showDialog () {
113 this.menu.el_.style.opacity = '1'
114 this.dialog.show()
115
116 this.setDialogSize(this.getComponentSize(this.menu))
117 }
118
119 hideDialog () {
120 this.dialog.hide()
121 this.setDialogSize(this.getComponentSize(this.menu))
122 this.menu.el_.style.opacity = '1'
123 this.resetChildren()
124 }
125
126 getComponentSize (element: any) {
127 let width: number = null
128 let height: number = null
129
130 // Could be component or just DOM element
131 if (element instanceof Component) {
132 width = element.el_.offsetWidth
133 height = element.el_.offsetHeight
134
135 // keep width/height as properties for direct use
136 element.width = width
137 element.height = height
138 } else {
139 width = element.offsetWidth
140 height = element.offsetHeight
141 }
142
143 return [ width, height ]
144 }
145
146 setDialogSize ([ width, height ]: number[]) {
147 if (typeof height !== 'number') {
148 return
149 }
150
151 const offset = this.options_.setup.maxHeightOffset
152 const maxHeight = this.playerComponent.el_.offsetHeight - offset
153
154 if (height > maxHeight) {
155 height = maxHeight
156 width += 17
157 this.panel.el_.style.maxHeight = `${height}px`
158 } else if (this.panel.el_.style.maxHeight !== '') {
159 this.panel.el_.style.maxHeight = ''
160 }
161
162 this.dialogEl.style.width = `${width}px`
163 this.dialogEl.style.height = `${height}px`
164 }
165
166 buildMenu () {
167 this.menu = new Menu(this.player())
168 this.menu.addClass('vjs-main-menu')
169 const entries = this.options_.entries
170
171 if (entries.length === 0) {
172 this.addClass('vjs-hidden')
173 this.panelChild.addChild(this.menu)
174 return
175 }
176
177 for (const entry of entries) {
178 this.addMenuItem(entry, this.options_)
179 }
180
181 this.panelChild.addChild(this.menu)
182 }
183
184 addMenuItem (entry: any, options: any) {
185 const openSubMenu = function (this: any) {
186 if (videojsUntyped.dom.hasClass(this.el_, 'open')) {
187 videojsUntyped.dom.removeClass(this.el_, 'open')
188 } else {
189 videojsUntyped.dom.addClass(this.el_, 'open')
190 }
191 }
192
193 options.name = toTitleCase(entry)
194 const settingsMenuItem = new SettingsMenuItem(this.player(), options, entry, this as any)
195
196 this.menu.addChild(settingsMenuItem)
197
198 // Hide children to avoid sub menus stacking on top of each other
199 // or having multiple menus open
200 settingsMenuItem.on('click', videojs.bind(this, this.hideChildren))
201
202 // Whether to add or remove selected class on the settings sub menu element
203 settingsMenuItem.on('click', openSubMenu)
204 }
205
206 resetChildren () {
207 for (const menuChild of this.menu.children()) {
208 menuChild.reset()
209 }
210 }
211
212 /**
213 * Hide all the sub menus
214 */
215 hideChildren () {
216 for (const menuChild of this.menu.children()) {
217 menuChild.hideSubMenu()
218 }
219 }
220
221 }
222
223 class SettingsPanel extends Component {
224 constructor (player: videojs.Player, options: any) {
225 super(player, options)
226 }
227
228 createEl () {
229 return super.createEl('div', {
230 className: 'vjs-settings-panel',
231 innerHTML: '',
232 tabIndex: -1
233 })
234 }
235 }
236
237 class SettingsPanelChild extends Component {
238 constructor (player: videojs.Player, options: any) {
239 super(player, options)
240 }
241
242 createEl () {
243 return super.createEl('div', {
244 className: 'vjs-settings-panel-child',
245 innerHTML: '',
246 tabIndex: -1
247 })
248 }
249 }
250
251 class SettingsDialog extends Component {
252 constructor (player: videojs.Player, options: any) {
253 super(player, options)
254 this.hide()
255 }
256
257 /**
258 * Create the component's DOM element
259 *
260 * @return {Element}
261 * @method createEl
262 */
263 createEl () {
264 const uniqueId = this.id_
265 const dialogLabelId = 'TTsettingsDialogLabel-' + uniqueId
266 const dialogDescriptionId = 'TTsettingsDialogDescription-' + uniqueId
267
268 return super.createEl('div', {
269 className: 'vjs-settings-dialog vjs-modal-overlay',
270 innerHTML: '',
271 tabIndex: -1
272 }, {
273 'role': 'dialog',
274 'aria-labelledby': dialogLabelId,
275 'aria-describedby': dialogDescriptionId
276 })
277 }
278
279 }
280
281 SettingsButton.prototype.controlText_ = 'Settings'
282
283 Component.registerComponent('SettingsButton', SettingsButton)
284 Component.registerComponent('SettingsDialog', SettingsDialog)
285 Component.registerComponent('SettingsPanel', SettingsPanel)
286 Component.registerComponent('SettingsPanelChild', SettingsPanelChild)
287
288 export { SettingsButton, SettingsDialog, SettingsPanel, SettingsPanelChild }