]>
Commit | Line | Data |
---|---|---|
1 | // Author: Yanko Shterev | |
2 | // Thanks https://github.com/yshterev/videojs-settings-menu | |
3 | ||
4 | import * as videojs from 'video.js' | |
5 | import { SettingsMenuItem } from './settings-menu-item' | |
6 | import { VideoJSComponentInterface, videojsUntyped } from './peertube-videojs-typings' | |
7 | import { toTitleCase } from './utils' | |
8 | ||
9 | const Button: VideoJSComponentInterface = videojsUntyped.getComponent('Button') | |
10 | const Menu: VideoJSComponentInterface = videojsUntyped.getComponent('Menu') | |
11 | const Component: VideoJSComponentInterface = videojsUntyped.getComponent('Component') | |
12 | ||
13 | class 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 the 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 | ||
220 | class 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 | ||
234 | class 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 | ||
248 | class 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 | ||
278 | SettingsButton.prototype.controlText_ = 'Settings' | |
279 | ||
280 | Component.registerComponent('SettingsButton', SettingsButton) | |
281 | Component.registerComponent('SettingsDialog', SettingsDialog) | |
282 | Component.registerComponent('SettingsPanel', SettingsPanel) | |
283 | Component.registerComponent('SettingsPanelChild', SettingsPanelChild) | |
284 | ||
285 | export { SettingsButton, SettingsDialog, SettingsPanel, SettingsPanelChild } |