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