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