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