settings-menu-button.ts 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255
  1. // Thanks to Yanko Shterev: https://github.com/yshterev/videojs-settings-menu
  2. import { SettingsMenuItem } from './settings-menu-item'
  3. import { toTitleCase } from '../utils'
  4. import videojs, { VideoJsPlayer } from 'video.js'
  5. import { SettingsDialog } from './settings-dialog'
  6. import { SettingsPanel } from './settings-panel'
  7. import { SettingsPanelChild } from './settings-panel-child'
  8. const Button = videojs.getComponent('Button')
  9. const Menu = videojs.getComponent('Menu')
  10. const Component = videojs.getComponent('Component')
  11. export interface SettingsButtonOptions extends videojs.ComponentOptions {
  12. entries: any[]
  13. setup?: {
  14. maxHeightOffset: number
  15. }
  16. }
  17. class SettingsButton extends Button {
  18. dialog: SettingsDialog
  19. dialogEl: HTMLElement
  20. menu: videojs.Menu
  21. panel: SettingsPanel
  22. panelChild: SettingsPanelChild
  23. addSettingsItemHandler: typeof SettingsButton.prototype.onAddSettingsItem
  24. disposeSettingsItemHandler: typeof SettingsButton.prototype.onDisposeSettingsItem
  25. playerClickHandler: typeof SettingsButton.prototype.onPlayerClick
  26. userInactiveHandler: typeof SettingsButton.prototype.onUserInactive
  27. private settingsButtonOptions: SettingsButtonOptions
  28. constructor (player: VideoJsPlayer, options?: SettingsButtonOptions) {
  29. super(player, options)
  30. this.settingsButtonOptions = options
  31. this.controlText('Settings')
  32. this.dialog = this.player().addChild('settingsDialog')
  33. this.dialogEl = this.dialog.el() as HTMLElement
  34. this.menu = null
  35. this.panel = this.dialog.addChild('settingsPanel')
  36. this.panelChild = this.panel.addChild('settingsPanelChild')
  37. this.addClass('vjs-settings')
  38. this.el().setAttribute('aria-label', 'Settings Button')
  39. // Event handlers
  40. this.addSettingsItemHandler = this.onAddSettingsItem.bind(this)
  41. this.disposeSettingsItemHandler = this.onDisposeSettingsItem.bind(this)
  42. this.playerClickHandler = this.onPlayerClick.bind(this)
  43. this.userInactiveHandler = this.onUserInactive.bind(this)
  44. this.buildMenu()
  45. this.bindEvents()
  46. // Prepare the dialog
  47. this.player().one('play', () => this.hideDialog())
  48. }
  49. onPlayerClick (event: MouseEvent) {
  50. const element = event.target as HTMLElement
  51. if (element.classList.contains('vjs-settings') || element.parentElement.classList.contains('vjs-settings')) {
  52. return
  53. }
  54. if (!this.dialog.hasClass('vjs-hidden')) {
  55. this.hideDialog()
  56. }
  57. }
  58. onDisposeSettingsItem (event: any, name: string) {
  59. if (name === undefined) {
  60. const children = this.menu.children()
  61. while (children.length > 0) {
  62. children[0].dispose()
  63. this.menu.removeChild(children[0])
  64. }
  65. this.addClass('vjs-hidden')
  66. } else {
  67. const item = this.menu.getChild(name)
  68. if (item) {
  69. item.dispose()
  70. this.menu.removeChild(item)
  71. }
  72. }
  73. this.hideDialog()
  74. if (this.settingsButtonOptions.entries.length === 0) {
  75. this.addClass('vjs-hidden')
  76. }
  77. }
  78. onAddSettingsItem (event: any, data: any) {
  79. const [ entry, options ] = data
  80. this.addMenuItem(entry, options)
  81. this.removeClass('vjs-hidden')
  82. }
  83. onUserInactive () {
  84. if (!this.dialog.hasClass('vjs-hidden')) {
  85. this.hideDialog()
  86. }
  87. }
  88. bindEvents () {
  89. this.player().on('click', this.playerClickHandler)
  90. this.player().on('addsettingsitem', this.addSettingsItemHandler)
  91. this.player().on('disposesettingsitem', this.disposeSettingsItemHandler)
  92. this.player().on('userinactive', this.userInactiveHandler)
  93. }
  94. buildCSSClass () {
  95. return `vjs-icon-settings ${super.buildCSSClass()}`
  96. }
  97. handleClick () {
  98. if (this.dialog.hasClass('vjs-hidden')) {
  99. this.showDialog()
  100. } else {
  101. this.hideDialog()
  102. }
  103. }
  104. showDialog () {
  105. this.player().peertube().onMenuOpen();
  106. (this.menu.el() as HTMLElement).style.opacity = '1'
  107. this.dialog.show()
  108. this.setDialogSize(this.getComponentSize(this.menu))
  109. }
  110. hideDialog () {
  111. this.player_.peertube().onMenuClosed()
  112. this.dialog.hide()
  113. this.setDialogSize(this.getComponentSize(this.menu));
  114. (this.menu.el() as HTMLElement).style.opacity = '1'
  115. this.resetChildren()
  116. }
  117. getComponentSize (element: videojs.Component | HTMLElement) {
  118. let width: number = null
  119. let height: number = null
  120. // Could be component or just DOM element
  121. if (element instanceof Component) {
  122. const el = element.el() as HTMLElement
  123. width = el.offsetWidth
  124. height = el.offsetHeight;
  125. (element as any).width = width;
  126. (element as any).height = height
  127. } else {
  128. width = element.offsetWidth
  129. height = element.offsetHeight
  130. }
  131. return [ width, height ]
  132. }
  133. setDialogSize ([ width, height ]: number[]) {
  134. if (typeof height !== 'number') {
  135. return
  136. }
  137. const offset = this.settingsButtonOptions.setup.maxHeightOffset
  138. const maxHeight = (this.player().el() as HTMLElement).offsetHeight - offset // FIXME: typings
  139. const panelEl = this.panel.el() as HTMLElement
  140. if (height > maxHeight) {
  141. height = maxHeight
  142. width += 17
  143. panelEl.style.maxHeight = `${height}px`
  144. } else if (panelEl.style.maxHeight !== '') {
  145. panelEl.style.maxHeight = ''
  146. }
  147. this.dialogEl.style.width = `${width}px`
  148. this.dialogEl.style.height = `${height}px`
  149. }
  150. buildMenu () {
  151. this.menu = new Menu(this.player())
  152. this.menu.addClass('vjs-main-menu')
  153. const entries = this.settingsButtonOptions.entries
  154. if (entries.length === 0) {
  155. this.addClass('vjs-hidden')
  156. this.panelChild.addChild(this.menu)
  157. return
  158. }
  159. for (const entry of entries) {
  160. this.addMenuItem(entry, this.settingsButtonOptions)
  161. }
  162. this.panelChild.addChild(this.menu)
  163. }
  164. addMenuItem (entry: any, options: any) {
  165. const openSubMenu = function (this: any) {
  166. if (videojs.dom.hasClass(this.el_, 'open')) {
  167. videojs.dom.removeClass(this.el_, 'open')
  168. } else {
  169. videojs.dom.addClass(this.el_, 'open')
  170. }
  171. }
  172. options.name = toTitleCase(entry)
  173. const newOptions = Object.assign({}, options, { entry, menuButton: this })
  174. const settingsMenuItem = new SettingsMenuItem(this.player(), newOptions)
  175. this.menu.addChild(settingsMenuItem)
  176. // Hide children to avoid sub menus stacking on top of each other
  177. // or having multiple menus open
  178. settingsMenuItem.on('click', videojs.bind(this, this.hideChildren))
  179. // Whether to add or remove selected class on the settings sub menu element
  180. settingsMenuItem.on('click', openSubMenu)
  181. }
  182. resetChildren () {
  183. for (const menuChild of this.menu.children()) {
  184. (menuChild as SettingsMenuItem).reset()
  185. }
  186. }
  187. /**
  188. * Hide all the sub menus
  189. */
  190. hideChildren () {
  191. for (const menuChild of this.menu.children()) {
  192. (menuChild as SettingsMenuItem).hideSubMenu()
  193. }
  194. }
  195. }
  196. Component.registerComponent('SettingsButton', SettingsButton)
  197. export { SettingsButton }