aboutsummaryrefslogtreecommitdiffhomepage
path: root/client/src/assets/player/shared/settings
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2022-03-14 14:28:20 +0100
committerChocobozzz <me@florianbigard.com>2022-03-14 14:36:35 +0100
commit57d6503286b114fee61b5e4725825e2490dcac29 (patch)
tree2d3d23f697b2986d7e41bb443754394296b66ec3 /client/src/assets/player/shared/settings
parent9597920ee3d4ac99803e7107983ddf98a9dfb3c4 (diff)
downloadPeerTube-57d6503286b114fee61b5e4725825e2490dcac29.tar.gz
PeerTube-57d6503286b114fee61b5e4725825e2490dcac29.tar.zst
PeerTube-57d6503286b114fee61b5e4725825e2490dcac29.zip
Reorganize player files
Diffstat (limited to 'client/src/assets/player/shared/settings')
-rw-r--r--client/src/assets/player/shared/settings/index.ts7
-rw-r--r--client/src/assets/player/shared/settings/resolution-menu-button.ts86
-rw-r--r--client/src/assets/player/shared/settings/resolution-menu-item.ts77
-rw-r--r--client/src/assets/player/shared/settings/settings-dialog.ts35
-rw-r--r--client/src/assets/player/shared/settings/settings-menu-button.ts277
-rw-r--r--client/src/assets/player/shared/settings/settings-menu-item.ts377
-rw-r--r--client/src/assets/player/shared/settings/settings-panel-child.ts18
-rw-r--r--client/src/assets/player/shared/settings/settings-panel.ts18
8 files changed, 895 insertions, 0 deletions
diff --git a/client/src/assets/player/shared/settings/index.ts b/client/src/assets/player/shared/settings/index.ts
new file mode 100644
index 000000000..736d50c16
--- /dev/null
+++ b/client/src/assets/player/shared/settings/index.ts
@@ -0,0 +1,7 @@
1export * from './resolution-menu-button'
2export * from './resolution-menu-item'
3export * from './settings-dialog'
4export * from './settings-menu-button'
5export * from './settings-menu-item'
6export * from './settings-panel-child'
7export * from './settings-panel'
diff --git a/client/src/assets/player/shared/settings/resolution-menu-button.ts b/client/src/assets/player/shared/settings/resolution-menu-button.ts
new file mode 100644
index 000000000..8bd5b4f03
--- /dev/null
+++ b/client/src/assets/player/shared/settings/resolution-menu-button.ts
@@ -0,0 +1,86 @@
1import videojs from 'video.js'
2import { ResolutionMenuItem } from './resolution-menu-item'
3
4const Menu = videojs.getComponent('Menu')
5const MenuButton = videojs.getComponent('MenuButton')
6class ResolutionMenuButton extends MenuButton {
7 labelEl_: HTMLElement
8
9 constructor (player: videojs.Player, options?: videojs.MenuButtonOptions) {
10 super(player, options)
11
12 this.controlText('Quality')
13
14 player.peertubeResolutions().on('resolutionsAdded', () => this.buildQualities())
15
16 // For parent
17 player.peertubeResolutions().on('resolutionChanged', () => {
18 setTimeout(() => this.trigger('labelUpdated'))
19 })
20 }
21
22 createEl () {
23 const el = super.createEl()
24
25 this.labelEl_ = videojs.dom.createEl('div', {
26 className: 'vjs-resolution-value'
27 }) as HTMLElement
28
29 el.appendChild(this.labelEl_)
30
31 return el
32 }
33
34 updateARIAAttributes () {
35 this.el().setAttribute('aria-label', 'Quality')
36 }
37
38 createMenu () {
39 return new Menu(this.player_)
40 }
41
42 buildCSSClass () {
43 return super.buildCSSClass() + ' vjs-resolution-button'
44 }
45
46 buildWrapperCSSClass () {
47 return 'vjs-resolution-control ' + super.buildWrapperCSSClass()
48 }
49
50 private addClickListener (component: any) {
51 component.on('click', () => {
52 const children = this.menu.children()
53
54 for (const child of children) {
55 if (component !== child) {
56 (child as videojs.MenuItem).selected(false)
57 }
58 }
59 })
60 }
61
62 private buildQualities () {
63 for (const d of this.player().peertubeResolutions().getResolutions()) {
64 const label = d.label === '0p'
65 ? this.player().localize('Audio-only')
66 : d.label
67
68 this.menu.addChild(new ResolutionMenuItem(
69 this.player_,
70 {
71 id: d.id,
72 label,
73 selected: d.selected
74 })
75 )
76 }
77
78 for (const m of this.menu.children()) {
79 this.addClickListener(m)
80 }
81
82 this.trigger('menuChanged')
83 }
84}
85
86videojs.registerComponent('ResolutionMenuButton', ResolutionMenuButton)
diff --git a/client/src/assets/player/shared/settings/resolution-menu-item.ts b/client/src/assets/player/shared/settings/resolution-menu-item.ts
new file mode 100644
index 000000000..6047f52f7
--- /dev/null
+++ b/client/src/assets/player/shared/settings/resolution-menu-item.ts
@@ -0,0 +1,77 @@
1import videojs from 'video.js'
2
3const MenuItem = videojs.getComponent('MenuItem')
4
5export interface ResolutionMenuItemOptions extends videojs.MenuItemOptions {
6 id: number
7}
8
9class ResolutionMenuItem extends MenuItem {
10 private readonly resolutionId: number
11 private readonly label: string
12
13 private autoResolutionEnabled: boolean
14 private autoResolutionChosen: string
15
16 constructor (player: videojs.Player, options?: ResolutionMenuItemOptions) {
17 options.selectable = true
18
19 super(player, options)
20
21 this.autoResolutionEnabled = true
22 this.autoResolutionChosen = ''
23
24 this.resolutionId = options.id
25 this.label = options.label
26
27 player.peertubeResolutions().on('resolutionChanged', () => this.updateSelection())
28
29 // We only want to disable the "Auto" item
30 if (this.resolutionId === -1) {
31 player.peertubeResolutions().on('autoResolutionEnabledChanged', () => this.updateAutoResolution())
32 }
33 }
34
35 handleClick (event: any) {
36 // Auto button disabled?
37 if (this.autoResolutionEnabled === false && this.resolutionId === -1) return
38
39 super.handleClick(event)
40
41 this.player().peertubeResolutions().select({ id: this.resolutionId, byEngine: false })
42 }
43
44 updateSelection () {
45 const selectedResolution = this.player().peertubeResolutions().getSelected()
46
47 if (this.resolutionId === -1) {
48 this.autoResolutionChosen = this.player().peertubeResolutions().getAutoResolutionChosen()?.label
49 }
50
51 this.selected(this.resolutionId === selectedResolution.id)
52 }
53
54 updateAutoResolution () {
55 const enabled = this.player().peertubeResolutions().isAutoResolutionEnabeld()
56
57 // Check if the auto resolution is enabled or not
58 if (enabled === false) {
59 this.addClass('disabled')
60 } else {
61 this.removeClass('disabled')
62 }
63
64 this.autoResolutionEnabled = enabled
65 }
66
67 getLabel () {
68 if (this.resolutionId === -1) {
69 return this.label + ' <small>' + this.autoResolutionChosen + '</small>'
70 }
71
72 return this.label
73 }
74}
75videojs.registerComponent('ResolutionMenuItem', ResolutionMenuItem)
76
77export { ResolutionMenuItem }
diff --git a/client/src/assets/player/shared/settings/settings-dialog.ts b/client/src/assets/player/shared/settings/settings-dialog.ts
new file mode 100644
index 000000000..8cd98967f
--- /dev/null
+++ b/client/src/assets/player/shared/settings/settings-dialog.ts
@@ -0,0 +1,35 @@
1import videojs from 'video.js'
2
3const Component = videojs.getComponent('Component')
4
5class SettingsDialog extends Component {
6 constructor (player: videojs.Player) {
7 super(player)
8
9 this.hide()
10 }
11
12 /**
13 * Create the component's DOM element
14 *
15 */
16 createEl () {
17 const uniqueId = this.id()
18 const dialogLabelId = 'TTsettingsDialogLabel-' + uniqueId
19 const dialogDescriptionId = 'TTsettingsDialogDescription-' + uniqueId
20
21 return super.createEl('div', {
22 className: 'vjs-settings-dialog vjs-modal-overlay',
23 innerHTML: '',
24 tabIndex: -1
25 }, {
26 role: 'dialog',
27 'aria-labelledby': dialogLabelId,
28 'aria-describedby': dialogDescriptionId
29 })
30 }
31}
32
33Component.registerComponent('SettingsDialog', SettingsDialog)
34
35export { SettingsDialog }
diff --git a/client/src/assets/player/shared/settings/settings-menu-button.ts b/client/src/assets/player/shared/settings/settings-menu-button.ts
new file mode 100644
index 000000000..64866aab2
--- /dev/null
+++ b/client/src/assets/player/shared/settings/settings-menu-button.ts
@@ -0,0 +1,277 @@
1import videojs from 'video.js'
2import { toTitleCase } from '../common'
3import { SettingsDialog } from './settings-dialog'
4import { SettingsMenuItem } from './settings-menu-item'
5import { SettingsPanel } from './settings-panel'
6import { SettingsPanelChild } from './settings-panel-child'
7
8const Button = videojs.getComponent('Button')
9const Menu = videojs.getComponent('Menu')
10const Component = videojs.getComponent('Component')
11
12export interface SettingsButtonOptions extends videojs.ComponentOptions {
13 entries: any[]
14 setup?: {
15 maxHeightOffset: number
16 }
17}
18
19class SettingsButton extends Button {
20 dialog: SettingsDialog
21 dialogEl: HTMLElement
22 menu: videojs.Menu
23 panel: SettingsPanel
24 panelChild: SettingsPanelChild
25
26 addSettingsItemHandler: typeof SettingsButton.prototype.onAddSettingsItem
27 disposeSettingsItemHandler: typeof SettingsButton.prototype.onDisposeSettingsItem
28 documentClickHandler: typeof SettingsButton.prototype.onDocumentClick
29 userInactiveHandler: typeof SettingsButton.prototype.onUserInactive
30
31 private settingsButtonOptions: SettingsButtonOptions
32
33 constructor (player: videojs.Player, options?: SettingsButtonOptions) {
34 super(player, options)
35
36 this.settingsButtonOptions = options
37
38 this.controlText('Settings')
39
40 this.dialog = this.player().addChild('settingsDialog')
41 this.dialogEl = this.dialog.el() as HTMLElement
42 this.menu = null
43 this.panel = this.dialog.addChild('settingsPanel')
44 this.panelChild = this.panel.addChild('settingsPanelChild')
45
46 this.addClass('vjs-settings')
47 this.el().setAttribute('aria-label', 'Settings Button')
48
49 // Event handlers
50 this.addSettingsItemHandler = this.onAddSettingsItem.bind(this)
51 this.disposeSettingsItemHandler = this.onDisposeSettingsItem.bind(this)
52 this.documentClickHandler = this.onDocumentClick.bind(this)
53 this.userInactiveHandler = this.onUserInactive.bind(this)
54
55 this.buildMenu()
56 this.bindEvents()
57
58 // Prepare the dialog
59 this.player().one('play', () => this.hideDialog())
60 }
61
62 onDocumentClick (event: MouseEvent) {
63 const element = event.target as HTMLElement
64
65 if (element?.classList?.contains('vjs-settings') || element?.parentElement?.classList?.contains('vjs-settings')) {
66 return
67 }
68
69 if (!this.dialog.hasClass('vjs-hidden')) {
70 this.hideDialog()
71 }
72 }
73
74 onDisposeSettingsItem (event: any, name: string) {
75 if (name === undefined) {
76 const children = this.menu.children()
77
78 while (children.length > 0) {
79 children[0].dispose()
80 this.menu.removeChild(children[0])
81 }
82
83 this.addClass('vjs-hidden')
84 } else {
85 const item = this.menu.getChild(name)
86
87 if (item) {
88 item.dispose()
89 this.menu.removeChild(item)
90 }
91 }
92
93 this.hideDialog()
94
95 if (this.settingsButtonOptions.entries.length === 0) {
96 this.addClass('vjs-hidden')
97 }
98 }
99
100 dispose () {
101 document.removeEventListener('click', this.documentClickHandler)
102
103 if (this.isInIframe()) {
104 window.removeEventListener('blur', this.documentClickHandler)
105 }
106 }
107
108 onAddSettingsItem (event: any, data: any) {
109 const [ entry, options ] = data
110
111 this.addMenuItem(entry, options)
112 this.removeClass('vjs-hidden')
113 }
114
115 onUserInactive () {
116 if (!this.dialog.hasClass('vjs-hidden')) {
117 this.hideDialog()
118 }
119 }
120
121 bindEvents () {
122 document.addEventListener('click', this.documentClickHandler)
123 if (this.isInIframe()) {
124 window.addEventListener('blur', this.documentClickHandler)
125 }
126
127 this.player().on('addsettingsitem', this.addSettingsItemHandler)
128 this.player().on('disposesettingsitem', this.disposeSettingsItemHandler)
129 this.player().on('userinactive', this.userInactiveHandler)
130 }
131
132 buildCSSClass () {
133 return `vjs-icon-settings ${super.buildCSSClass()}`
134 }
135
136 handleClick () {
137 if (this.dialog.hasClass('vjs-hidden')) {
138 this.showDialog()
139 } else {
140 this.hideDialog()
141 }
142 }
143
144 showDialog () {
145 this.player().peertube().onMenuOpened();
146
147 (this.menu.el() as HTMLElement).style.opacity = '1'
148
149 this.dialog.show()
150 this.el().setAttribute('aria-expanded', 'true')
151
152 this.setDialogSize(this.getComponentSize(this.menu))
153
154 const firstChild = this.menu.children()[0]
155 if (firstChild) firstChild.focus()
156 }
157
158 hideDialog () {
159 this.player_.peertube().onMenuClosed()
160
161 this.dialog.hide()
162 this.el().setAttribute('aria-expanded', 'false')
163
164 this.setDialogSize(this.getComponentSize(this.menu));
165 (this.menu.el() as HTMLElement).style.opacity = '1'
166 this.resetChildren()
167 }
168
169 getComponentSize (element: videojs.Component | HTMLElement) {
170 let width: number = null
171 let height: number = null
172
173 // Could be component or just DOM element
174 if (element instanceof Component) {
175 const el = element.el() as HTMLElement
176
177 width = el.offsetWidth
178 height = el.offsetHeight;
179
180 (element as any).width = width;
181 (element as any).height = height
182 } else {
183 width = element.offsetWidth
184 height = element.offsetHeight
185 }
186
187 return [ width, height ]
188 }
189
190 setDialogSize ([ width, height ]: number[]) {
191 if (typeof height !== 'number') {
192 return
193 }
194
195 const offset = this.settingsButtonOptions.setup.maxHeightOffset
196 const maxHeight = (this.player().el() as HTMLElement).offsetHeight - offset
197
198 const panelEl = this.panel.el() as HTMLElement
199
200 if (height > maxHeight) {
201 height = maxHeight
202 width += 17
203 panelEl.style.maxHeight = `${height}px`
204 } else if (panelEl.style.maxHeight !== '') {
205 panelEl.style.maxHeight = ''
206 }
207
208 this.dialogEl.style.width = `${width}px`
209 this.dialogEl.style.height = `${height}px`
210 }
211
212 buildMenu () {
213 this.menu = new Menu(this.player())
214 this.menu.addClass('vjs-main-menu')
215 const entries = this.settingsButtonOptions.entries
216
217 if (entries.length === 0) {
218 this.addClass('vjs-hidden')
219 this.panelChild.addChild(this.menu)
220 return
221 }
222
223 for (const entry of entries) {
224 this.addMenuItem(entry, this.settingsButtonOptions)
225 }
226
227 this.panelChild.addChild(this.menu)
228 }
229
230 addMenuItem (entry: any, options: any) {
231 const openSubMenu = function (this: any) {
232 if (videojs.dom.hasClass(this.el_, 'open')) {
233 videojs.dom.removeClass(this.el_, 'open')
234 } else {
235 videojs.dom.addClass(this.el_, 'open')
236 }
237 }
238
239 options.name = toTitleCase(entry)
240
241 const newOptions = Object.assign({}, options, { entry, menuButton: this })
242 const settingsMenuItem = new SettingsMenuItem(this.player(), newOptions)
243
244 this.menu.addChild(settingsMenuItem)
245
246 // Hide children to avoid sub menus stacking on top of each other
247 // or having multiple menus open
248 settingsMenuItem.on('click', videojs.bind(this, this.hideChildren))
249
250 // Whether to add or remove selected class on the settings sub menu element
251 settingsMenuItem.on('click', openSubMenu)
252 }
253
254 resetChildren () {
255 for (const menuChild of this.menu.children()) {
256 (menuChild as SettingsMenuItem).reset()
257 }
258 }
259
260 /**
261 * Hide all the sub menus
262 */
263 hideChildren () {
264 for (const menuChild of this.menu.children()) {
265 (menuChild as SettingsMenuItem).hideSubMenu()
266 }
267 }
268
269 isInIframe () {
270 return window.self !== window.top
271 }
272
273}
274
275Component.registerComponent('SettingsButton', SettingsButton)
276
277export { SettingsButton }
diff --git a/client/src/assets/player/shared/settings/settings-menu-item.ts b/client/src/assets/player/shared/settings/settings-menu-item.ts
new file mode 100644
index 000000000..8d1819a2d
--- /dev/null
+++ b/client/src/assets/player/shared/settings/settings-menu-item.ts
@@ -0,0 +1,377 @@
1import videojs from 'video.js'
2import { toTitleCase } from '../common'
3import { SettingsDialog } from './settings-dialog'
4import { SettingsButton } from './settings-menu-button'
5import { SettingsPanel } from './settings-panel'
6import { SettingsPanelChild } from './settings-panel-child'
7
8const MenuItem = videojs.getComponent('MenuItem')
9const component = videojs.getComponent('Component')
10
11export interface SettingsMenuItemOptions extends videojs.MenuItemOptions {
12 entry: string
13 menuButton: SettingsButton
14}
15
16class SettingsMenuItem extends MenuItem {
17 settingsButton: SettingsButton
18 dialog: SettingsDialog
19 mainMenu: videojs.Menu
20 panel: SettingsPanel
21 panelChild: SettingsPanelChild
22 panelChildEl: HTMLElement
23 size: number[]
24 menuToLoad: string
25 subMenu: SettingsButton
26
27 submenuClickHandler: typeof SettingsMenuItem.prototype.onSubmenuClick
28 transitionEndHandler: typeof SettingsMenuItem.prototype.onTransitionEnd
29
30 settingsSubMenuTitleEl_: HTMLElement
31 settingsSubMenuValueEl_: HTMLElement
32 settingsSubMenuEl_: HTMLElement
33
34 constructor (player: videojs.Player, options?: SettingsMenuItemOptions) {
35 super(player, options)
36
37 this.settingsButton = options.menuButton
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')
42 this.panelChildEl = this.panelChild.el() as HTMLElement
43
44 this.size = null
45
46 // keep state of what menu type is loading next
47 this.menuToLoad = 'mainmenu'
48
49 const subMenuName = toTitleCase(options.entry)
50 const SubMenuComponent = videojs.getComponent(subMenuName)
51
52 if (!SubMenuComponent) {
53 throw new Error(`Component ${subMenuName} does not exist`)
54 }
55
56 const newOptions = Object.assign({}, options, { entry: options.menuButton, menuButton: this })
57
58 this.subMenu = new SubMenuComponent(this.player(), newOptions) as SettingsButton
59 const subMenuClass = this.subMenu.buildCSSClass().split(' ')[0]
60 this.settingsSubMenuEl_.className += ' ' + subMenuClass
61
62 this.eventHandlers()
63
64 player.ready(() => {
65 // Voodoo magic for IOS
66 setTimeout(() => {
67 // Player was destroyed
68 if (!this.player_) return
69
70 this.build()
71
72 // Update on rate change
73 player.on('ratechange', this.submenuClickHandler)
74
75 if (subMenuName === 'CaptionsButton') {
76 // Hack to regenerate captions on HTTP fallback
77 player.on('captionsChanged', () => {
78 setTimeout(() => {
79 this.settingsSubMenuEl_.innerHTML = ''
80 this.settingsSubMenuEl_.appendChild(this.subMenu.menu.el())
81 this.update()
82 this.bindClickEvents()
83 }, 0)
84 })
85 }
86
87 this.reset()
88 }, 0)
89 })
90 }
91
92 eventHandlers () {
93 this.submenuClickHandler = this.onSubmenuClick.bind(this)
94 this.transitionEndHandler = this.onTransitionEnd.bind(this)
95 }
96
97 onSubmenuClick (event: any) {
98 let target = null
99
100 if (event.type === 'tap') {
101 target = event.target
102 } else {
103 target = event.currentTarget || event.target
104 }
105
106 if (target?.classList.contains('vjs-back-button')) {
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)
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')
118 }
119
120 /**
121 * Create the component's DOM element
122 *
123 */
124 createEl () {
125 const el = videojs.dom.createEl('li', {
126 className: 'vjs-menu-item',
127 tabIndex: -1
128 })
129
130 this.settingsSubMenuTitleEl_ = videojs.dom.createEl('div', {
131 className: 'vjs-settings-sub-menu-title'
132 }) as HTMLElement
133
134 el.appendChild(this.settingsSubMenuTitleEl_)
135
136 this.settingsSubMenuValueEl_ = videojs.dom.createEl('div', {
137 className: 'vjs-settings-sub-menu-value'
138 }) as HTMLElement
139
140 el.appendChild(this.settingsSubMenuValueEl_)
141
142 this.settingsSubMenuEl_ = videojs.dom.createEl('div', {
143 className: 'vjs-settings-sub-menu'
144 }) as HTMLElement
145
146 return el as HTMLLIElement
147 }
148
149 /**
150 * Handle click on menu item
151 *
152 * @method handleClick
153 */
154 handleClick (event: videojs.EventTarget.Event) {
155 this.menuToLoad = 'submenu'
156 // Remove open class to ensure only the open submenu gets this class
157 videojs.dom.removeClass(this.el(), 'open')
158
159 super.handleClick(event);
160
161 (this.mainMenu.el() as HTMLElement).style.opacity = '0'
162 // Whether to add or remove vjs-hidden class on the settingsSubMenuEl element
163 if (videojs.dom.hasClass(this.settingsSubMenuEl_, 'vjs-hidden')) {
164 videojs.dom.removeClass(this.settingsSubMenuEl_, 'vjs-hidden')
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)
173
174 const firstChild = this.subMenu.menu.children()[0]
175 if (firstChild) firstChild.focus()
176 } else {
177 videojs.dom.addClass(this.settingsSubMenuEl_, 'vjs-hidden')
178 }
179 }
180
181 /**
182 * Create back button
183 *
184 * @method createBackButton
185 */
186 createBackButton () {
187 const button = this.subMenu.menu.addChild('MenuItem', {}, 0)
188
189 button.addClass('vjs-back-button');
190 (button.el() as HTMLElement).innerHTML = this.player().localize(this.subMenu.controlText())
191 }
192
193 /**
194 * Add/remove prefixed event listener for CSS Transition
195 *
196 * @method PrefixedEvent
197 */
198 PrefixedEvent (element: any, type: any, callback: any, action = 'addEvent') {
199 const prefix = [ 'webkit', 'moz', 'MS', 'o', '' ]
200
201 for (let p = 0; p < prefix.length; p++) {
202 if (!prefix[p]) {
203 type = type.toLowerCase()
204 }
205
206 if (action === 'addEvent') {
207 element.addEventListener(prefix[p] + type, callback, false)
208 } else if (action === 'removeEvent') {
209 element.removeEventListener(prefix[p] + type, callback, false)
210 }
211 }
212 }
213
214 onTransitionEnd (event: any) {
215 if (event.propertyName !== 'margin-right') {
216 return
217 }
218
219 if (this.menuToLoad === 'mainmenu') {
220 // hide submenu
221 videojs.dom.addClass(this.settingsSubMenuEl_, 'vjs-hidden')
222
223 // reset opacity to 0
224 this.settingsSubMenuEl_.style.opacity = '0'
225 }
226 }
227
228 reset () {
229 videojs.dom.addClass(this.settingsSubMenuEl_, 'vjs-hidden')
230 this.settingsSubMenuEl_.style.opacity = '0'
231 this.setMargin()
232 }
233
234 loadMainMenu () {
235 const mainMenuEl = this.mainMenu.el() as HTMLElement
236 this.menuToLoad = 'mainmenu'
237 this.mainMenu.show()
238 mainMenuEl.style.opacity = '0'
239
240 // back button will always take you to main menu, so set dialog sizes
241 const mainMenuAny = this.mainMenu as any
242 this.settingsButton.setDialogSize([ mainMenuAny.width, mainMenuAny.height ])
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()
249 mainMenuEl.style.opacity = '1'
250
251 const firstChild = this.mainMenu.children()[0]
252 if (firstChild) firstChild.focus()
253 }, 0)
254 }
255
256 build () {
257 this.subMenu.on('labelUpdated', () => {
258 this.update()
259 })
260 this.subMenu.on('menuChanged', () => {
261 this.bindClickEvents()
262 this.setSize()
263 this.update()
264 })
265
266 this.settingsSubMenuTitleEl_.innerHTML = this.player().localize(this.subMenu.controlText())
267 this.settingsSubMenuEl_.appendChild(this.subMenu.menu.el())
268 this.panelChildEl.appendChild(this.settingsSubMenuEl_)
269 this.update()
270
271 this.createBackButton()
272 this.setSize()
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
284 update (event?: any) {
285 let target: HTMLElement = null
286 const subMenu = this.subMenu.name()
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') {
298 const html = (this.subMenu as any).labelEl_.innerHTML
299
300 setTimeout(() => {
301 this.settingsSubMenuValueEl_.innerHTML = html
302 }, 250)
303 } else {
304 // Loop trough the submenu items to find the selected child
305 for (const subMenuItem of this.subMenu.menu.children_) {
306 if (!(subMenuItem instanceof component)) {
307 continue
308 }
309
310 if (subMenuItem.hasClass('vjs-selected')) {
311 const subMenuItemUntyped = subMenuItem as any
312
313 // Prefer to use the function
314 if (typeof subMenuItemUntyped.getLabel === 'function') {
315 this.settingsSubMenuValueEl_.innerHTML = subMenuItemUntyped.getLabel()
316 break
317 }
318
319 this.settingsSubMenuValueEl_.innerHTML = this.player().localize(subMenuItemUntyped.options_.label)
320 }
321 }
322 }
323
324 if (target && !target.classList.contains('vjs-back-button')) {
325 this.settingsButton.hideDialog()
326 }
327 }
328
329 bindClickEvents () {
330 for (const item of this.subMenu.menu.children()) {
331 if (!(item instanceof component)) {
332 continue
333 }
334 item.on([ 'tap', 'click' ], this.submenuClickHandler)
335 }
336 }
337
338 // save size of submenus on first init
339 // if number of submenu items change dynamically more logic will be needed
340 setSize () {
341 this.dialog.removeClass('vjs-hidden')
342 videojs.dom.removeClass(this.settingsSubMenuEl_, 'vjs-hidden')
343 this.size = this.settingsButton.getComponentSize(this.settingsSubMenuEl_)
344 this.setMargin()
345 this.dialog.addClass('vjs-hidden')
346 videojs.dom.addClass(this.settingsSubMenuEl_, 'vjs-hidden')
347 }
348
349 setMargin () {
350 if (!this.size) return
351
352 const [ width ] = this.size
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
362 if (!this.el()) {
363 return
364 }
365
366 if (videojs.dom.hasClass(this.el(), 'open')) {
367 videojs.dom.addClass(this.settingsSubMenuEl_, 'vjs-hidden')
368 videojs.dom.removeClass(this.el(), 'open')
369 }
370 }
371
372}
373
374(SettingsMenuItem as any).prototype.contentElType = 'button'
375videojs.registerComponent('SettingsMenuItem', SettingsMenuItem)
376
377export { SettingsMenuItem }
diff --git a/client/src/assets/player/shared/settings/settings-panel-child.ts b/client/src/assets/player/shared/settings/settings-panel-child.ts
new file mode 100644
index 000000000..161420c38
--- /dev/null
+++ b/client/src/assets/player/shared/settings/settings-panel-child.ts
@@ -0,0 +1,18 @@
1import videojs from 'video.js'
2
3const Component = videojs.getComponent('Component')
4
5class SettingsPanelChild extends Component {
6
7 createEl () {
8 return super.createEl('div', {
9 className: 'vjs-settings-panel-child',
10 innerHTML: '',
11 tabIndex: -1
12 })
13 }
14}
15
16Component.registerComponent('SettingsPanelChild', SettingsPanelChild)
17
18export { SettingsPanelChild }
diff --git a/client/src/assets/player/shared/settings/settings-panel.ts b/client/src/assets/player/shared/settings/settings-panel.ts
new file mode 100644
index 000000000..28b579bdd
--- /dev/null
+++ b/client/src/assets/player/shared/settings/settings-panel.ts
@@ -0,0 +1,18 @@
1import videojs from 'video.js'
2
3const Component = videojs.getComponent('Component')
4
5class SettingsPanel extends Component {
6
7 createEl () {
8 return super.createEl('div', {
9 className: 'vjs-settings-panel',
10 innerHTML: '',
11 tabIndex: -1
12 })
13 }
14}
15
16Component.registerComponent('SettingsPanel', SettingsPanel)
17
18export { SettingsPanel }