]>
Commit | Line | Data |
---|---|---|
c6352f2c C |
1 | // Author: Yanko Shterev |
2 | // Thanks https://github.com/yshterev/videojs-settings-menu | |
3 | ||
c199c427 C |
4 | // FIXME: something weird with our path definition in tsconfig and typings |
5 | // @ts-ignore | |
6 | import * as videojs from 'video.js' | |
7 | ||
2adfc7ea C |
8 | import { toTitleCase } from '../utils' |
9 | import { VideoJSComponentInterface, videojsUntyped } from '../peertube-videojs-typings' | |
c6352f2c C |
10 | |
11 | const MenuItem: VideoJSComponentInterface = videojsUntyped.getComponent('MenuItem') | |
12 | const component: VideoJSComponentInterface = videojsUntyped.getComponent('Component') | |
13 | ||
14 | class SettingsMenuItem extends MenuItem { | |
16b55259 C |
15 | settingsButton: any |
16 | dialog: any | |
17 | mainMenu: any | |
18 | panel: any | |
19 | panelChild: any | |
20 | panelChildEl: any | |
21 | size: any | |
22 | menuToLoad: string | |
23 | subMenu: any | |
24 | ||
25 | submenuClickHandler: Function | |
26 | transitionEndHandler: Function | |
27 | ||
28 | settingsSubMenuTitleEl_: any | |
29 | settingsSubMenuValueEl_: any | |
30 | settingsSubMenuEl_: any | |
c6352f2c | 31 | |
c199c427 | 32 | constructor (player: videojs.Player, options: any, entry: string, menuButton: VideoJSComponentInterface) { |
c6352f2c C |
33 | super(player, options) |
34 | ||
35 | this.settingsButton = menuButton | |
36 | this.dialog = this.settingsButton.dialog | |
37 | this.mainMenu = this.settingsButton.menu | |
38 | this.panel = this.dialog.getChild('settingsPanel') | |
39 | this.panelChild = this.panel.getChild('settingsPanelChild') | |
40 | this.panelChildEl = this.panelChild.el_ | |
41 | ||
42 | this.size = null | |
43 | ||
44 | // keep state of what menu type is loading next | |
45 | this.menuToLoad = 'mainmenu' | |
46 | ||
47 | const subMenuName = toTitleCase(entry) | |
48 | const SubMenuComponent = videojsUntyped.getComponent(subMenuName) | |
49 | ||
50 | if (!SubMenuComponent) { | |
51 | throw new Error(`Component ${subMenuName} does not exist`) | |
52 | } | |
53 | this.subMenu = new SubMenuComponent(this.player(), options, menuButton, this) | |
16f7022b C |
54 | const subMenuClass = this.subMenu.buildCSSClass().split(' ')[0] |
55 | this.settingsSubMenuEl_.className += ' ' + subMenuClass | |
c6352f2c C |
56 | |
57 | this.eventHandlers() | |
58 | ||
59 | player.ready(() => { | |
b335ccec C |
60 | // Voodoo magic for IOS |
61 | setTimeout(() => { | |
bfbd9128 C |
62 | // Player was destroyed |
63 | if (!this.player_) return | |
64 | ||
b335ccec | 65 | this.build() |
5363a766 C |
66 | |
67 | // Update on rate change | |
68 | player.on('ratechange', this.submenuClickHandler) | |
69 | ||
c32bf839 C |
70 | if (subMenuName === 'CaptionsButton') { |
71 | // Hack to regenerate captions on HTTP fallback | |
72 | player.on('captionsChanged', () => { | |
73 | setTimeout(() => { | |
74 | this.settingsSubMenuEl_.innerHTML = '' | |
75 | this.settingsSubMenuEl_.appendChild(this.subMenu.menu.el_) | |
76 | this.update() | |
77 | this.bindClickEvents() | |
c32bf839 C |
78 | }, 0) |
79 | }) | |
80 | } | |
81 | ||
b335ccec C |
82 | this.reset() |
83 | }, 0) | |
c6352f2c C |
84 | }) |
85 | } | |
86 | ||
87 | eventHandlers () { | |
88 | this.submenuClickHandler = this.onSubmenuClick.bind(this) | |
89 | this.transitionEndHandler = this.onTransitionEnd.bind(this) | |
90 | } | |
91 | ||
244b4ae3 | 92 | onSubmenuClick (event: any) { |
c6352f2c C |
93 | let target = null |
94 | ||
95 | if (event.type === 'tap') { | |
96 | target = event.target | |
97 | } else { | |
98 | target = event.currentTarget | |
99 | } | |
100 | ||
5363a766 | 101 | if (target && target.classList.contains('vjs-back-button')) { |
c6352f2c C |
102 | this.loadMainMenu() |
103 | return | |
104 | } | |
105 | ||
106 | // To update the sub menu value on click, setTimeout is needed because | |
107 | // updating the value is not instant | |
108 | setTimeout(() => this.update(event), 0) | |
42aac9fc C |
109 | |
110 | // Seems like videojs adds a vjs-hidden class on the caption menu after a click | |
111 | // We don't need it | |
112 | this.subMenu.menu.removeClass('vjs-hidden') | |
c6352f2c C |
113 | } |
114 | ||
115 | /** | |
116 | * Create the component's DOM element | |
117 | * | |
118 | * @return {Element} | |
119 | * @method createEl | |
120 | */ | |
121 | createEl () { | |
122 | const el = videojsUntyped.dom.createEl('li', { | |
123 | className: 'vjs-menu-item' | |
124 | }) | |
125 | ||
126 | this.settingsSubMenuTitleEl_ = videojsUntyped.dom.createEl('div', { | |
127 | className: 'vjs-settings-sub-menu-title' | |
128 | }) | |
129 | ||
130 | el.appendChild(this.settingsSubMenuTitleEl_) | |
131 | ||
132 | this.settingsSubMenuValueEl_ = videojsUntyped.dom.createEl('div', { | |
133 | className: 'vjs-settings-sub-menu-value' | |
134 | }) | |
135 | ||
136 | el.appendChild(this.settingsSubMenuValueEl_) | |
137 | ||
138 | this.settingsSubMenuEl_ = videojsUntyped.dom.createEl('div', { | |
139 | className: 'vjs-settings-sub-menu' | |
140 | }) | |
141 | ||
142 | return el | |
143 | } | |
144 | ||
145 | /** | |
146 | * Handle click on menu item | |
147 | * | |
148 | * @method handleClick | |
149 | */ | |
150 | handleClick () { | |
151 | this.menuToLoad = 'submenu' | |
152 | // Remove open class to ensure only the open submenu gets this class | |
153 | videojsUntyped.dom.removeClass(this.el_, 'open') | |
154 | ||
155 | super.handleClick() | |
156 | ||
157 | this.mainMenu.el_.style.opacity = '0' | |
158 | // Whether to add or remove vjs-hidden class on the settingsSubMenuEl element | |
159 | if (videojsUntyped.dom.hasClass(this.settingsSubMenuEl_, 'vjs-hidden')) { | |
160 | videojsUntyped.dom.removeClass(this.settingsSubMenuEl_, 'vjs-hidden') | |
161 | ||
162 | // animation not played without timeout | |
163 | setTimeout(() => { | |
164 | this.settingsSubMenuEl_.style.opacity = '1' | |
165 | this.settingsSubMenuEl_.style.marginRight = '0px' | |
166 | }, 0) | |
167 | ||
168 | this.settingsButton.setDialogSize(this.size) | |
169 | } else { | |
170 | videojsUntyped.dom.addClass(this.settingsSubMenuEl_, 'vjs-hidden') | |
171 | } | |
172 | } | |
173 | ||
174 | /** | |
175 | * Create back button | |
176 | * | |
177 | * @method createBackButton | |
178 | */ | |
179 | createBackButton () { | |
180 | const button = this.subMenu.menu.addChild('MenuItem', {}, 0) | |
181 | button.name_ = 'BackButton' | |
182 | button.addClass('vjs-back-button') | |
e945b184 | 183 | button.el_.innerHTML = this.player_.localize(this.subMenu.controlText_) |
c6352f2c C |
184 | } |
185 | ||
186 | /** | |
187 | * Add/remove prefixed event listener for CSS Transition | |
188 | * | |
189 | * @method PrefixedEvent | |
190 | */ | |
244b4ae3 | 191 | PrefixedEvent (element: any, type: any, callback: any, action = 'addEvent') { |
c4710631 | 192 | const prefix = ['webkit', 'moz', 'MS', 'o', ''] |
c6352f2c C |
193 | |
194 | for (let p = 0; p < prefix.length; p++) { | |
195 | if (!prefix[p]) { | |
196 | type = type.toLowerCase() | |
197 | } | |
198 | ||
199 | if (action === 'addEvent') { | |
200 | element.addEventListener(prefix[p] + type, callback, false) | |
201 | } else if (action === 'removeEvent') { | |
202 | element.removeEventListener(prefix[p] + type, callback, false) | |
203 | } | |
204 | } | |
205 | } | |
206 | ||
244b4ae3 | 207 | onTransitionEnd (event: any) { |
c6352f2c C |
208 | if (event.propertyName !== 'margin-right') { |
209 | return | |
210 | } | |
211 | ||
212 | if (this.menuToLoad === 'mainmenu') { | |
213 | // hide submenu | |
214 | videojsUntyped.dom.addClass(this.settingsSubMenuEl_, 'vjs-hidden') | |
215 | ||
216 | // reset opacity to 0 | |
217 | this.settingsSubMenuEl_.style.opacity = '0' | |
218 | } | |
219 | } | |
220 | ||
221 | reset () { | |
222 | videojsUntyped.dom.addClass(this.settingsSubMenuEl_, 'vjs-hidden') | |
223 | this.settingsSubMenuEl_.style.opacity = '0' | |
224 | this.setMargin() | |
225 | } | |
226 | ||
227 | loadMainMenu () { | |
228 | this.menuToLoad = 'mainmenu' | |
229 | this.mainMenu.show() | |
230 | this.mainMenu.el_.style.opacity = '0' | |
231 | ||
232 | // back button will always take you to main menu, so set dialog sizes | |
233 | this.settingsButton.setDialogSize([this.mainMenu.width, this.mainMenu.height]) | |
234 | ||
235 | // animation not triggered without timeout (some async stuff ?!?) | |
236 | setTimeout(() => { | |
237 | // animate margin and opacity before hiding the submenu | |
238 | // this triggers CSS Transition event | |
239 | this.setMargin() | |
240 | this.mainMenu.el_.style.opacity = '1' | |
241 | }, 0) | |
242 | } | |
243 | ||
244 | build () { | |
2adfc7ea | 245 | this.subMenu.on('updateLabel', () => { |
c6352f2c | 246 | this.update() |
2adfc7ea | 247 | }) |
3b6f205c C |
248 | this.subMenu.on('menuChanged', () => { |
249 | this.bindClickEvents() | |
250 | this.setSize() | |
251 | this.update() | |
252 | }) | |
c6352f2c | 253 | |
e945b184 | 254 | this.settingsSubMenuTitleEl_.innerHTML = this.player_.localize(this.subMenu.controlText_) |
c6352f2c C |
255 | this.settingsSubMenuEl_.appendChild(this.subMenu.menu.el_) |
256 | this.panelChildEl.appendChild(this.settingsSubMenuEl_) | |
257 | this.update() | |
258 | ||
259 | this.createBackButton() | |
3b6f205c | 260 | this.setSize() |
c6352f2c C |
261 | this.bindClickEvents() |
262 | ||
263 | // prefixed event listeners for CSS TransitionEnd | |
264 | this.PrefixedEvent( | |
265 | this.settingsSubMenuEl_, | |
266 | 'TransitionEnd', | |
267 | this.transitionEndHandler, | |
268 | 'addEvent' | |
269 | ) | |
270 | } | |
271 | ||
244b4ae3 | 272 | update (event?: any) { |
c199c427 | 273 | let target: HTMLElement = null |
c4710631 | 274 | const subMenu = this.subMenu.name() |
c6352f2c C |
275 | |
276 | if (event && event.type === 'tap') { | |
277 | target = event.target | |
278 | } else if (event) { | |
279 | target = event.currentTarget | |
280 | } | |
281 | ||
282 | // Playback rate menu button doesn't get a vjs-selected class | |
283 | // or sets options_['selected'] on the selected playback rate. | |
284 | // Thus we get the submenu value based on the labelEl of playbackRateMenuButton | |
285 | if (subMenu === 'PlaybackRateMenuButton') { | |
286 | setTimeout(() => this.settingsSubMenuValueEl_.innerHTML = this.subMenu.labelEl_.innerHTML, 250) | |
287 | } else { | |
288 | // Loop trough the submenu items to find the selected child | |
c4710631 | 289 | for (const subMenuItem of this.subMenu.menu.children_) { |
c6352f2c C |
290 | if (!(subMenuItem instanceof component)) { |
291 | continue | |
292 | } | |
293 | ||
a8462c8e C |
294 | if (subMenuItem.hasClass('vjs-selected')) { |
295 | // Prefer to use the function | |
296 | if (typeof subMenuItem.getLabel === 'function') { | |
297 | this.settingsSubMenuValueEl_.innerHTML = subMenuItem.getLabel() | |
c6352f2c | 298 | break |
a8462c8e | 299 | } |
c6352f2c | 300 | |
a8462c8e | 301 | this.settingsSubMenuValueEl_.innerHTML = subMenuItem.options_.label |
c6352f2c C |
302 | } |
303 | } | |
304 | } | |
305 | ||
306 | if (target && !target.classList.contains('vjs-back-button')) { | |
307 | this.settingsButton.hideDialog() | |
308 | } | |
309 | } | |
310 | ||
311 | bindClickEvents () { | |
c4710631 | 312 | for (const item of this.subMenu.menu.children()) { |
c6352f2c C |
313 | if (!(item instanceof component)) { |
314 | continue | |
315 | } | |
316 | item.on(['tap', 'click'], this.submenuClickHandler) | |
317 | } | |
318 | } | |
319 | ||
320 | // save size of submenus on first init | |
321 | // if number of submenu items change dynamically more logic will be needed | |
3b6f205c | 322 | setSize () { |
c6352f2c | 323 | this.dialog.removeClass('vjs-hidden') |
3b6f205c | 324 | videojsUntyped.dom.removeClass(this.settingsSubMenuEl_, 'vjs-hidden') |
c6352f2c C |
325 | this.size = this.settingsButton.getComponentSize(this.settingsSubMenuEl_) |
326 | this.setMargin() | |
327 | this.dialog.addClass('vjs-hidden') | |
328 | videojsUntyped.dom.addClass(this.settingsSubMenuEl_, 'vjs-hidden') | |
329 | } | |
330 | ||
331 | setMargin () { | |
c4710631 | 332 | const [ width ] = this.size |
c6352f2c C |
333 | |
334 | this.settingsSubMenuEl_.style.marginRight = `-${width}px` | |
335 | } | |
336 | ||
337 | /** | |
338 | * Hide the sub menu | |
339 | */ | |
340 | hideSubMenu () { | |
341 | // after removing settings item this.el_ === null | |
342 | if (!this.el_) { | |
343 | return | |
344 | } | |
345 | ||
346 | if (videojsUntyped.dom.hasClass(this.el_, 'open')) { | |
347 | videojsUntyped.dom.addClass(this.settingsSubMenuEl_, 'vjs-hidden') | |
348 | videojsUntyped.dom.removeClass(this.el_, 'open') | |
349 | } | |
350 | } | |
351 | ||
352 | } | |
353 | ||
354 | SettingsMenuItem.prototype.contentElType = 'button' | |
355 | videojsUntyped.registerComponent('SettingsMenuItem', SettingsMenuItem) | |
356 | ||
357 | export { SettingsMenuItem } |