diff options
Diffstat (limited to 'client/src/assets/player/settings/settings-menu-button.ts')
-rw-r--r-- | client/src/assets/player/settings/settings-menu-button.ts | 279 |
1 files changed, 279 insertions, 0 deletions
diff --git a/client/src/assets/player/settings/settings-menu-button.ts b/client/src/assets/player/settings/settings-menu-button.ts new file mode 100644 index 000000000..6de390f4d --- /dev/null +++ b/client/src/assets/player/settings/settings-menu-button.ts | |||
@@ -0,0 +1,279 @@ | |||
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 (element?.classList?.contains('vjs-settings') || element?.parentElement?.classList?.contains('vjs-settings')) { | ||
68 | return | ||
69 | } | ||
70 | |||
71 | if (!this.dialog.hasClass('vjs-hidden')) { | ||
72 | this.hideDialog() | ||
73 | } | ||
74 | } | ||
75 | |||
76 | onDisposeSettingsItem (event: any, name: string) { | ||
77 | if (name === undefined) { | ||
78 | const children = this.menu.children() | ||
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 { | ||
87 | const item = this.menu.getChild(name) | ||
88 | |||
89 | if (item) { | ||
90 | item.dispose() | ||
91 | this.menu.removeChild(item) | ||
92 | } | ||
93 | } | ||
94 | |||
95 | this.hideDialog() | ||
96 | |||
97 | if (this.settingsButtonOptions.entries.length === 0) { | ||
98 | this.addClass('vjs-hidden') | ||
99 | } | ||
100 | } | ||
101 | |||
102 | dispose () { | ||
103 | document.removeEventListener('click', this.documentClickHandler) | ||
104 | |||
105 | if (this.isInIframe()) { | ||
106 | window.removeEventListener('blur', this.documentClickHandler) | ||
107 | } | ||
108 | } | ||
109 | |||
110 | onAddSettingsItem (event: any, data: any) { | ||
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 () { | ||
124 | document.addEventListener('click', this.documentClickHandler) | ||
125 | if (this.isInIframe()) { | ||
126 | window.addEventListener('blur', this.documentClickHandler) | ||
127 | } | ||
128 | |||
129 | this.player().on('addsettingsitem', this.addSettingsItemHandler) | ||
130 | this.player().on('disposesettingsitem', this.disposeSettingsItemHandler) | ||
131 | this.player().on('userinactive', this.userInactiveHandler) | ||
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 () { | ||
147 | this.player().peertube().onMenuOpened(); | ||
148 | |||
149 | (this.menu.el() as HTMLElement).style.opacity = '1' | ||
150 | |||
151 | this.dialog.show() | ||
152 | this.el().setAttribute('aria-expanded', 'true') | ||
153 | |||
154 | this.setDialogSize(this.getComponentSize(this.menu)) | ||
155 | |||
156 | const firstChild = this.menu.children()[0] | ||
157 | if (firstChild) firstChild.focus() | ||
158 | } | ||
159 | |||
160 | hideDialog () { | ||
161 | this.player_.peertube().onMenuClosed() | ||
162 | |||
163 | this.dialog.hide() | ||
164 | this.el().setAttribute('aria-expanded', 'false') | ||
165 | |||
166 | this.setDialogSize(this.getComponentSize(this.menu)); | ||
167 | (this.menu.el() as HTMLElement).style.opacity = '1' | ||
168 | this.resetChildren() | ||
169 | } | ||
170 | |||
171 | getComponentSize (element: videojs.Component | HTMLElement) { | ||
172 | let width: number = null | ||
173 | let height: number = null | ||
174 | |||
175 | // Could be component or just DOM element | ||
176 | if (element instanceof Component) { | ||
177 | const el = element.el() as HTMLElement | ||
178 | |||
179 | width = el.offsetWidth | ||
180 | height = el.offsetHeight; | ||
181 | |||
182 | (element as any).width = width; | ||
183 | (element as any).height = height | ||
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 | |||
197 | const offset = this.settingsButtonOptions.setup.maxHeightOffset | ||
198 | const maxHeight = (this.player().el() as HTMLElement).offsetHeight - offset | ||
199 | |||
200 | const panelEl = this.panel.el() as HTMLElement | ||
201 | |||
202 | if (height > maxHeight) { | ||
203 | height = maxHeight | ||
204 | width += 17 | ||
205 | panelEl.style.maxHeight = `${height}px` | ||
206 | } else if (panelEl.style.maxHeight !== '') { | ||
207 | panelEl.style.maxHeight = '' | ||
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') | ||
217 | const entries = this.settingsButtonOptions.entries | ||
218 | |||
219 | if (entries.length === 0) { | ||
220 | this.addClass('vjs-hidden') | ||
221 | this.panelChild.addChild(this.menu) | ||
222 | return | ||
223 | } | ||
224 | |||
225 | for (const entry of entries) { | ||
226 | this.addMenuItem(entry, this.settingsButtonOptions) | ||
227 | } | ||
228 | |||
229 | this.panelChild.addChild(this.menu) | ||
230 | } | ||
231 | |||
232 | addMenuItem (entry: any, options: any) { | ||
233 | const openSubMenu = function (this: any) { | ||
234 | if (videojs.dom.hasClass(this.el_, 'open')) { | ||
235 | videojs.dom.removeClass(this.el_, 'open') | ||
236 | } else { | ||
237 | videojs.dom.addClass(this.el_, 'open') | ||
238 | } | ||
239 | } | ||
240 | |||
241 | options.name = toTitleCase(entry) | ||
242 | |||
243 | const newOptions = Object.assign({}, options, { entry, menuButton: this }) | ||
244 | const settingsMenuItem = new SettingsMenuItem(this.player(), newOptions) | ||
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 () { | ||
257 | for (const menuChild of this.menu.children()) { | ||
258 | (menuChild as SettingsMenuItem).reset() | ||
259 | } | ||
260 | } | ||
261 | |||
262 | /** | ||
263 | * Hide all the sub menus | ||
264 | */ | ||
265 | hideChildren () { | ||
266 | for (const menuChild of this.menu.children()) { | ||
267 | (menuChild as SettingsMenuItem).hideSubMenu() | ||
268 | } | ||
269 | } | ||
270 | |||
271 | isInIframe () { | ||
272 | return window.self !== window.top | ||
273 | } | ||
274 | |||
275 | } | ||
276 | |||
277 | Component.registerComponent('SettingsButton', SettingsButton) | ||
278 | |||
279 | export { SettingsButton } | ||