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