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