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