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