]>
Commit | Line | Data |
---|---|---|
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().onMenuOpen(); | |
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 } |