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