diff options
Diffstat (limited to 'client/src/assets/player/settings-menu-button.ts')
-rw-r--r-- | client/src/assets/player/settings-menu-button.ts | 285 |
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 | |||
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 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 Button' | ||
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 } | ||