@gcorevideo/player 2.24.1 → 2.24.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/assets/big-mute-button/big-mute-button.ejs +2 -2
  2. package/assets/bottom-gear/gear-sub-menu.scss +1 -0
  3. package/dist/core.js +1 -1
  4. package/dist/index.css +754 -753
  5. package/dist/index.js +151 -130
  6. package/dist/player.d.ts +72 -21
  7. package/docs/api/player.bigmutebutton.md +13 -1
  8. package/docs/api/player.clapprstatssettings.md +51 -4
  9. package/docs/api/player.clapprstatssettings.runeach.md +16 -0
  10. package/docs/api/player.clipspluginsettings.md +1 -1
  11. package/docs/api/player.clipspluginsettings.text.md +1 -1
  12. package/docs/api/player.cmcdconfig.exportids.md +4 -0
  13. package/docs/api/player.cmcdconfig.md +19 -105
  14. package/docs/api/{player.cmcdconfig.version.md → player.cmcdconfigoptions.contentid.md} +5 -3
  15. package/docs/api/player.cmcdconfigoptions.md +79 -0
  16. package/docs/api/{player.cmcdconfigpluginsettings.md → player.cmcdconfigoptions.sessionid.md} +4 -6
  17. package/docs/api/player.extendedevents.md +9 -0
  18. package/docs/api/player.md +37 -31
  19. package/docs/api/player.mediacontrol.getavailableheight.md +24 -0
  20. package/docs/api/player.mediacontrol.md +14 -0
  21. package/docs/api/{player.cmcdconfig.name.md → player.posterpluginsettings.custom.md} +4 -3
  22. package/docs/api/player.posterpluginsettings.md +108 -7
  23. package/docs/api/player.posterpluginsettings.showfornoop.md +16 -0
  24. package/docs/api/player.posterpluginsettings.showonvideoend.md +16 -0
  25. package/docs/api/{player.cmcdconfig.bindevents.md → player.posterpluginsettings.url.md} +4 -7
  26. package/lib/plugins/big-mute-button/BigMuteButton.d.ts +15 -13
  27. package/lib/plugins/big-mute-button/BigMuteButton.d.ts.map +1 -1
  28. package/lib/plugins/big-mute-button/BigMuteButton.js +68 -83
  29. package/lib/plugins/bottom-gear/BottomGear.d.ts +1 -0
  30. package/lib/plugins/bottom-gear/BottomGear.d.ts.map +1 -1
  31. package/lib/plugins/bottom-gear/BottomGear.js +17 -17
  32. package/lib/plugins/clappr-stats/ClapprStats.d.ts +6 -2
  33. package/lib/plugins/clappr-stats/ClapprStats.d.ts.map +1 -1
  34. package/lib/plugins/clips/Clips.d.ts +1 -1
  35. package/lib/plugins/clips/Clips.d.ts.map +1 -1
  36. package/lib/plugins/clips/Clips.js +2 -1
  37. package/lib/plugins/cmcd-config/CmcdConfig.d.ts +34 -11
  38. package/lib/plugins/cmcd-config/CmcdConfig.d.ts.map +1 -1
  39. package/lib/plugins/cmcd-config/CmcdConfig.js +28 -18
  40. package/lib/plugins/media-control/MediaControl.d.ts +11 -0
  41. package/lib/plugins/media-control/MediaControl.d.ts.map +1 -1
  42. package/lib/plugins/media-control/MediaControl.js +19 -5
  43. package/lib/plugins/poster/Poster.d.ts +7 -3
  44. package/lib/plugins/poster/Poster.d.ts.map +1 -1
  45. package/lib/plugins/source-controller/SourceController.d.ts +1 -0
  46. package/lib/plugins/source-controller/SourceController.d.ts.map +1 -1
  47. package/lib/plugins/source-controller/SourceController.js +20 -9
  48. package/lib/testUtils.d.ts +1 -0
  49. package/lib/testUtils.d.ts.map +1 -1
  50. package/lib/testUtils.js +3 -0
  51. package/package.json +1 -1
  52. package/src/plugins/big-mute-button/BigMuteButton.ts +75 -110
  53. package/src/plugins/big-mute-button/__tests__/BigMuteButton.test.ts +38 -0
  54. package/src/plugins/big-mute-button/__tests__/__snapshots__/BigMuteButton.test.ts.snap +8 -0
  55. package/src/plugins/bottom-gear/BottomGear.ts +40 -28
  56. package/src/plugins/bottom-gear/__tests__/BottomGear.test.ts +34 -7
  57. package/src/plugins/bottom-gear/__tests__/__snapshots__/BottomGear.test.ts.snap +5 -2
  58. package/src/plugins/clappr-stats/ClapprStats.ts +5 -1
  59. package/src/plugins/clips/Clips.ts +3 -2
  60. package/src/plugins/cmcd-config/CmcdConfig.ts +33 -27
  61. package/src/plugins/media-control/MediaControl.ts +23 -6
  62. package/src/plugins/poster/Poster.ts +6 -2
  63. package/src/plugins/source-controller/SourceController.ts +25 -9
  64. package/src/plugins/source-controller/__tests__/SourceController.test.ts +28 -8
  65. package/src/testUtils.ts +3 -0
  66. package/temp/player.api.json +229 -154
  67. package/tsconfig.tsbuildinfo +1 -1
  68. package/docs/api/player.cmcdconfig.supportedversion.md +0 -14
@@ -1,11 +1,11 @@
1
1
  import { Events, template, UICorePlugin, Utils } from '@clappr/core'
2
2
  import { trace } from '@gcorevideo/utils'
3
+ import assert from 'assert'
3
4
 
4
5
  import { CLAPPR_VERSION } from '../../build.js'
5
- import { ZeptoResult } from '../../types.js'
6
6
 
7
7
  import volumeMuteIcon from '../../../assets/icons/new/volume-off.svg'
8
- import pluginHtml from '../../../assets/big-mute-button/big-mute-button.ejs'
8
+ import templateHtml from '../../../assets/big-mute-button/big-mute-button.ejs'
9
9
  import '../../../assets/big-mute-button/big-mute-button.scss'
10
10
 
11
11
  const T = 'plugins.big_mute_button'
@@ -13,19 +13,22 @@ const T = 'plugins.big_mute_button'
13
13
  // TODO rewrite as a container plugin
14
14
 
15
15
  /**
16
- * `PLUGIN` that displays a big mute button over the video when it's muted.
17
- * Once pressed, it unmutes the video.
16
+ * `PLUGIN` that displays a big mute button over the video when it's being played muted.
18
17
  * @beta
18
+ * @remarks
19
+ * When pressed, it unmutes the video.
20
+ * @example
21
+ * ```ts
22
+ * import { BigMuteButton } from '@gcorevideo/player'
23
+ * Player.registerPlugin(BigMuteButton)
24
+ * ```
19
25
  */
20
26
  export class BigMuteButton extends UICorePlugin {
21
- private isBigMuteButtonHidden = false
27
+ private hidden = false
22
28
 
29
+ // TODO get back to the ads-related logic later
23
30
  private _adIsPlaying = false
24
31
 
25
- private $bigMuteBtnContainer: ZeptoResult | null = null
26
-
27
- private $bigMuteButton: ZeptoResult | null = null
28
-
29
32
  /**
30
33
  * @internal
31
34
  */
@@ -40,15 +43,14 @@ export class BigMuteButton extends UICorePlugin {
40
43
  return { min: CLAPPR_VERSION }
41
44
  }
42
45
 
43
- private static readonly template = template(pluginHtml)
46
+ private static readonly template = template(templateHtml)
44
47
 
45
48
  /**
46
49
  * @internal
47
50
  */
48
51
  override get events() {
49
52
  return {
50
- 'click .big-mute-icon': 'clicked',
51
- 'click .big-mute-icon-wrapper': 'destroyBigMuteBtn',
53
+ 'click': 'clicked',
52
54
  }
53
55
  }
54
56
 
@@ -57,156 +59,119 @@ export class BigMuteButton extends UICorePlugin {
57
59
  */
58
60
  override bindEvents() {
59
61
  this.listenTo(this.core, Events.CORE_READY, this.onCoreReady)
62
+ this.listenTo(this.core, Events.CORE_ACTIVE_CONTAINER_CHANGED, this.onContainerChanged)
60
63
  this.listenTo(this.core, 'core:advertisement:start', this.onStartAd)
61
64
  this.listenTo(this.core, 'core:advertisement:finish', this.onFinishAd)
62
- trace(`${T} bindEvents`, {
63
- mediacontrol: !!this.core.mediaControl,
64
- })
65
- // TOOD use core.getPlugin('media_control')
66
- this.listenTo(
67
- this.core.mediaControl,
68
- Events.MEDIACONTROL_RENDERED,
69
- this.mediaControlRendered,
70
- )
71
65
  }
72
66
 
73
67
  private onCoreReady() {
68
+
69
+ }
70
+
71
+ private onContainerChanged() {
74
72
  this.listenTo(
75
73
  this.core.activeContainer,
76
74
  Events.CONTAINER_VOLUME,
77
75
  this.onContainerVolume,
78
76
  )
79
- this.listenTo(
80
- this.core.activeContainer,
81
- Events.CONTAINER_READY,
82
- this.onContainerStart,
83
- )
77
+ // this.listenTo(
78
+ // this.core.activeContainer,
79
+ // Events.CONTAINER_READY,
80
+ // this.onContainerReady,
81
+ // )
84
82
  this.listenTo(
85
83
  this.core.activePlayback,
86
84
  Events.PLAYBACK_ENDED,
87
85
  this.onPlaybackEnded,
88
86
  )
87
+ this.listenTo(
88
+ this.core.activeContainer,
89
+ Events.CONTAINER_PLAY,
90
+ this.onPlay
91
+ )
89
92
  }
90
93
 
91
- private onContainerVolume(value: number) {
92
- if (value !== 0) {
93
- this.destroyBigMuteBtn()
94
+ private onPlay(_: string, { autoPlay }: { autoPlay?: boolean}) {
95
+ const container = this.core.activeContainer
96
+ const { volume } = container
97
+ const { wasMuted } = this.options
98
+ trace(`${T} onPlay`, {
99
+ autoPlay,
100
+ wasMuted,
101
+ volume,
102
+ })
103
+ if (autoPlay && !wasMuted && volume === 0) {
104
+ this.mount()
105
+ } else {
106
+ this.destroy()
94
107
  }
95
108
  }
96
109
 
97
- private onContainerStart() {
98
- if (this.isBigMuteButtonHidden) {
99
- this.showBigMuteBtn()
110
+ private onContainerVolume(value: number) {
111
+ if (value !== 0) {
112
+ this.destroy()
100
113
  }
101
114
  }
102
115
 
103
116
  private onPlaybackEnded() {
104
- this.hideBigMuteBtn()
105
- }
106
-
107
- private mediaControlRendered() {
108
- const container = this.core.activeContainer
109
-
110
- trace(`${T} mediaControlRendered`, {
111
- container: !!container,
112
- })
113
-
114
- if (container) {
115
- this.listenTo(container.playback, Events.PLAYBACK_PLAY, () => {
116
- trace(`${T} PLAYBACK_PLAY`)
117
- this.render()
118
- })
119
- }
117
+ this.hide()
120
118
  }
121
119
 
122
120
  private onStartAd() {
123
121
  this._adIsPlaying = true
124
- if (this.$bigMuteBtnContainer) {
125
- this.$bigMuteBtnContainer.addClass('hide')
126
- }
122
+ this.hide()
127
123
  }
128
124
 
129
125
  private onFinishAd() {
130
126
  this._adIsPlaying = false
131
- if (this.$bigMuteBtnContainer) {
132
- this.$bigMuteBtnContainer.removeClass('hide')
133
- }
134
- }
135
-
136
- private shouldRender() {
137
- const container = this.core.activeContainer
138
-
139
- if (!container) {
140
- return false
141
- }
142
-
143
- const { autoPlay, wasMuted } = this.options
144
- const volume = container.volume
145
-
146
- trace(`${T} shouldRender`, {
147
- autoPlay,
148
- wasMuted,
149
- volume,
150
- })
151
-
152
- return autoPlay && !wasMuted && volume === 0
127
+ this.show()
153
128
  }
154
129
 
155
130
  /**
156
131
  * @internal
157
132
  */
158
133
  override render() {
159
- if (this.shouldRender()) {
160
- trace(`${T} render`, {
161
- el: !!this.$el,
162
- })
163
- this.$el.html(BigMuteButton.template())
164
-
165
- this.$bigMuteBtnContainer = this.$el.find(
166
- '.big-mute-icon-wrapper[data-big-mute]',
167
- )
168
- this._adIsPlaying && this.$bigMuteBtnContainer.addClass('hide')
169
-
170
- this.$bigMuteButton = this.$bigMuteBtnContainer.find('.big-mute-icon')
171
- this.$bigMuteButton.append(volumeMuteIcon)
172
-
173
- const container = this.core.activeContainer
174
-
175
- container.$el.append(this.$el.get(0))
176
- }
134
+ trace(`${T} render`)
135
+ this.$el.html(BigMuteButton.template())
136
+ this.$el.find('#gplayer-big-mute-icon').append(volumeMuteIcon)
137
+
138
+ // TODO
139
+ // this._adIsPlaying && this.hide()
177
140
 
178
141
  return this
179
142
  }
180
143
 
181
- private hideBigMuteBtn() {
182
- this.isBigMuteButtonHidden = true
183
- this.$bigMuteBtnContainer?.addClass('hide')
144
+ private mount() {
145
+ this.core.activeContainer.$el.append(this.$el)
146
+ this.show()
184
147
  }
185
148
 
186
- private showBigMuteBtn() {
187
- this.isBigMuteButtonHidden = false
188
- if (this.$bigMuteBtnContainer) {
189
- this.$bigMuteBtnContainer.removeClass('hide')
190
- }
149
+ private hide() {
150
+ this.hidden = true
151
+ this.$el.find('#gplayer-big-mute-button')?.addClass('hide')
191
152
  }
192
153
 
193
- private destroyBigMuteBtn(e?: MouseEvent) {
194
- this.hideBigMuteBtn()
195
-
196
- if (e && e.stopPropagation) {
197
- e.stopPropagation()
198
- }
199
-
200
- this.destroy()
154
+ private show() {
155
+ this.hidden = false
156
+ this.$el.find('#gplayer-big-mute-button')?.removeClass('hide')
201
157
  }
202
158
 
203
159
  private clicked(e: MouseEvent) {
160
+ trace(`${T} clicked`)
161
+ const mediaControl = this.core.getPlugin('media_control')
162
+ // TODO delegate to media_control plugin
204
163
  const localVolume = Utils.Config.restore('volume')
205
164
  const volume = !isNaN(localVolume) ? localVolume : 100
165
+ const unmuted = volume === 0 ? 100 : volume
206
166
 
207
- // TODO use container.setVolume() instead
208
- this.core.mediaControl.setVolume(volume === 0 ? 100 : volume)
167
+ if (mediaControl) {
168
+ mediaControl.setVolume(unmuted)
169
+ } else {
170
+ this.core.activeContainer.setVolume(unmuted)
171
+ }
172
+
173
+ e.stopPropagation?.()
209
174
 
210
- this.destroyBigMuteBtn(e)
175
+ this.destroy()
211
176
  }
212
177
  }
@@ -0,0 +1,38 @@
1
+ import { beforeEach, describe, it, expect } from 'vitest'
2
+ import { Events } from '@clappr/core'
3
+
4
+ import { BigMuteButton } from '../BigMuteButton.js'
5
+ import { createMockCore } from '../../../testUtils.js'
6
+
7
+ describe('BigMuteButton', () => {
8
+ let core: any
9
+ let bmb: BigMuteButton
10
+ describe('basically', () => {
11
+ beforeEach(() => {
12
+ core = createMockCore({})
13
+ bmb = new BigMuteButton(core)
14
+ // core.emit('core:ready')
15
+ // core.emit('core:active:container:changed')
16
+ })
17
+ it('should render', () => {
18
+ expect(bmb.$el.html()).toMatchSnapshot()
19
+ })
20
+ })
21
+ describe('when container starts playing', () => {
22
+ describe.each([
23
+ ['muted autoplay', 0, { autoPlay: true }, true],
24
+ ['audible autoplay', 50, { autoPlay: true }, false],
25
+ ['muted not autoplay', 0, { }, false],
26
+ ['audible not autoplay', 1, {}, false],
27
+ ])("%s", (_, volume, playMetadata, shouldMount) => {
28
+ beforeEach(() => {
29
+ core.emit(Events.CORE_ACTIVE_CONTAINER_CHANGED)
30
+ core.activeContainer.volume = volume
31
+ core.activeContainer.emit(Events.CONTAINER_PLAY, 'Container', playMetadata)
32
+ })
33
+ it(`should ${shouldMount ? 'mount' : 'not mount'} to container`, () => {
34
+ expect(core.activeContainer.$el.find('#gplayer-big-mute-button').length).toBe(shouldMount ? 1 : 0)
35
+ })
36
+ })
37
+ })
38
+ })
@@ -0,0 +1,8 @@
1
+ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
+
3
+ exports[`BigMuteButton > basically > should render 1`] = `
4
+ "<div class="big-mute-icon-wrapper" data-big-mute="" id="gplayer-big-mute-button">
5
+ <div class="big-mute-icon gcore-skin-border-color" data-big-mute-icon="" id="gplayer-big-mute-icon">/assets/icons/new/volume-off.svg</div>
6
+ </div>
7
+ "
8
+ `;
@@ -1,4 +1,10 @@
1
- import { UICorePlugin, template, Events as ClapprEvents, $, Container } from '@clappr/core'
1
+ import {
2
+ UICorePlugin,
3
+ template,
4
+ Events as ClapprEvents,
5
+ $,
6
+ Container,
7
+ } from '@clappr/core'
2
8
  import { trace } from '@gcorevideo/utils'
3
9
  import assert from 'assert'
4
10
 
@@ -16,6 +22,9 @@ const VERSION = '2.19.12'
16
22
 
17
23
  const T = 'plugins.bottom_gear'
18
24
 
25
+ const MENU_VMARGIN = 12
26
+ const MENU_BACKLINK_HEIGHT = 44
27
+
19
28
  /**
20
29
  * Events triggered by the plugin
21
30
  * @beta
@@ -27,8 +36,6 @@ export enum GearEvents {
27
36
  RENDERED = 'rendered',
28
37
  }
29
38
 
30
- // TODO disabled if no items added
31
-
32
39
  /**
33
40
  * `PLUGIN` that adds a button to extend the media controls UI with extra options.
34
41
  * @beta
@@ -100,9 +107,9 @@ export enum GearEvents {
100
107
  export class BottomGear extends UICorePlugin {
101
108
  private hd = false
102
109
 
103
- private numItems = 0;
110
+ private numItems = 0
104
111
 
105
- private collapsed = true;
112
+ private collapsed = true
106
113
 
107
114
  /**
108
115
  * @internal
@@ -175,7 +182,7 @@ export class BottomGear extends UICorePlugin {
175
182
  * ```
176
183
  */
177
184
  addItem(name: string, $subMenu?: ZeptoResult): ZeptoResult {
178
- const $existingItem = this.$el.find(`#gear-options li[data-${name}`)
185
+ const $existingItem = this.$el.find(`#gear-options li[data-${name}]`)
179
186
  if ($existingItem.length) {
180
187
  trace(`${T} addItem already exists`, { name })
181
188
  return $existingItem
@@ -190,21 +197,18 @@ export class BottomGear extends UICorePlugin {
190
197
  .hide()
191
198
  .appendTo(this.$el.find('#gear-options-wrapper'))
192
199
  $item.on('click', (e: MouseEvent) => {
193
- trace(`${T} addItem submenu clicked`, { name })
194
200
  e.stopPropagation()
201
+ this.alignSubmenu($subMenu)
195
202
  $subMenu.show()
196
203
  this.$el.find('#gear-options').hide()
197
204
  })
198
205
  }
199
- this.numItems++;
200
- if (this.numItems > 0) {
201
- this.$el.show()
202
- }
206
+ this.numItems++
207
+ this.$el.show()
203
208
  return $item
204
209
  }
205
210
 
206
211
  private bindContainerEvents(container: Container) {
207
- trace(`${T} bindContainerEvents`)
208
212
  this.listenTo(
209
213
  container,
210
214
  ClapprEvents.CONTAINER_HIGHDEFINITIONUPDATE,
@@ -225,14 +229,13 @@ export class BottomGear extends UICorePlugin {
225
229
  * @internal
226
230
  */
227
231
  override render() {
228
- trace(`${T} render`)
229
232
  const mediaControl = this.core.getPlugin('media_control')
230
233
  if (!mediaControl) {
231
234
  return this // TODO test
232
235
  }
233
236
  const icon = this.hd ? gearHdIcon : gearIcon
234
- this.collapsed = true;
235
- this.numItems = 0;
237
+ this.collapsed = true
238
+ this.numItems = 0
236
239
  this.$el
237
240
  .html(BottomGear.template({ icon }))
238
241
  .hide() // until numItems > 0
@@ -264,19 +267,19 @@ export class BottomGear extends UICorePlugin {
264
267
  this.core
265
268
  .getPlugin('media_control')
266
269
  .trigger(ExtendedEvents.MEDIACONTROL_MENU_COLLAPSE, this.name)
267
- this.collapsed = !this.collapsed;
270
+ this.collapsed = !this.collapsed
268
271
  if (this.collapsed) {
269
272
  this.$el.find('#gear-options-wrapper').hide()
270
273
  } else {
271
274
  this.$el.find('#gear-options-wrapper').show()
272
275
  }
273
- this.$el.find('#gear-button').attr('aria-expanded', (!this.collapsed).toString())
274
- trace(`${T} toggleMenu`, { hidden: this.collapsed })
276
+ this.$el
277
+ .find('#gear-button')
278
+ .attr('aria-expanded', (!this.collapsed).toString())
275
279
  }
276
280
 
277
281
  private collapse() {
278
- trace(`${T} collapse`)
279
- this.collapsed = true;
282
+ this.collapsed = true
280
283
  this.$el.find('#gear-options-wrapper').hide()
281
284
  this.$el.find('#gear-button').attr('aria-expanded', 'false')
282
285
  // TODO hide submenus
@@ -284,7 +287,6 @@ export class BottomGear extends UICorePlugin {
284
287
  }
285
288
 
286
289
  private onCoreReady() {
287
- trace(`${T} onCoreReady`)
288
290
  const mediaControl = this.core.getPlugin('media_control')
289
291
  assert(mediaControl, 'media_control plugin is required')
290
292
  this.listenTo(
@@ -293,9 +295,13 @@ export class BottomGear extends UICorePlugin {
293
295
  this.onMediaControlRendered,
294
296
  )
295
297
  this.listenTo(mediaControl, ClapprEvents.MEDIACONTROL_HIDE, this.collapse)
296
- this.listenTo(mediaControl, ClapprEvents.MEDIACONTROL_CONTAINERCHANGED, () => {
297
- this.bindContainerEvents(mediaControl.container)
298
- })
298
+ this.listenTo(
299
+ mediaControl,
300
+ ClapprEvents.MEDIACONTROL_CONTAINERCHANGED,
301
+ () => {
302
+ this.bindContainerEvents(mediaControl.container)
303
+ },
304
+ )
299
305
  this.listenTo(
300
306
  mediaControl,
301
307
  ExtendedEvents.MEDIACONTROL_MENU_COLLAPSE,
@@ -309,15 +315,21 @@ export class BottomGear extends UICorePlugin {
309
315
  }
310
316
 
311
317
  private onMediaControlRendered() {
312
- trace(`${T} onMediaControlRendered`)
313
318
  this.mount()
314
319
  }
315
320
 
316
321
  private mount() {
317
- trace(`${T} mount`, {
318
- numItems: this.numItems,
319
- })
320
322
  const mediaControl = this.core.getPlugin('media_control')
321
323
  mediaControl.mount('gear', this.$el)
322
324
  }
325
+
326
+ private alignSubmenu($subMenu: ZeptoResult) {
327
+ const availableHeight =
328
+ this.core.getPlugin('media_control').getAvailableHeight() -
329
+ MENU_VMARGIN * 2
330
+ $subMenu.css('max-height', `${availableHeight}px`)
331
+ $subMenu
332
+ .find('.gear-sub-menu')
333
+ .css('max-height', `${availableHeight - MENU_BACKLINK_HEIGHT}px`)
334
+ }
323
335
  }
@@ -31,6 +31,15 @@ describe('BottomGear', () => {
31
31
  bottomGear.on(GearEvents.RENDERED, onGearRendered, null)
32
32
  core.emit(Events.CORE_READY)
33
33
  bottomGear.addItem('test', null).html('<button>test</button>')
34
+ const $moreOptions = $(
35
+ `<div>
36
+ <button id="more-options-back">&lt; back</button>
37
+ <ul class="gear-sub-menu" id="more-options"><li>Item</li><li>Item</li><li>Item</li></ul>
38
+ </div>`,
39
+ )
40
+ bottomGear
41
+ .addItem('more', $moreOptions)
42
+ .html('<button id="more-button">more options</button>')
34
43
  })
35
44
  it('should render', () => {
36
45
  expect(bottomGear.el.innerHTML).toMatchSnapshot()
@@ -99,6 +108,26 @@ describe('BottomGear', () => {
99
108
  )
100
109
  })
101
110
  })
111
+ describe('when submenu is open', () => {
112
+ beforeEach(async () => {
113
+ mediaControl.getAvailableHeight.mockReturnValue(198)
114
+ bottomGear.$el.find('#gear-button').click()
115
+ await new Promise((resolve) => setTimeout(resolve, 0))
116
+ bottomGear.$el.find('#more-button').click()
117
+ await new Promise((resolve) => setTimeout(resolve, 0))
118
+ })
119
+ it('should show submenu', () => {
120
+ expect(
121
+ bottomGear.$el.find('#more-options').parent().css('display'),
122
+ ).not.toBe('none')
123
+ })
124
+ it('should align nicely within container', () => {
125
+ const submenu = bottomGear.$el.find('#more-options')
126
+ const wrapper = submenu.parent()
127
+ expect(wrapper.css('max-height')).toBe('174px') // available height minus vertical margins
128
+ expect(submenu.css('max-height')).toBe('130px') // wrapper height minus backlink height
129
+ })
130
+ })
102
131
  })
103
132
  describe('when there are no items', () => {
104
133
  beforeEach(() => {
@@ -128,9 +157,9 @@ describe('BottomGear', () => {
128
157
  await new Promise((resolve) => setTimeout(resolve, 0))
129
158
  })
130
159
  it('should collapse the gear menu', () => {
131
- expect(bottomGear.$el.find('#gear-options-wrapper').css('display')).toBe(
132
- 'none',
133
- )
160
+ expect(
161
+ bottomGear.$el.find('#gear-options-wrapper').css('display'),
162
+ ).toBe('none')
134
163
  expect(bottomGear.$el.find('#gear-button').attr('aria-expanded')).toBe(
135
164
  'false',
136
165
  )
@@ -140,14 +169,12 @@ describe('BottomGear', () => {
140
169
  describe('when submenu is open', () => {
141
170
  beforeEach(async () => {
142
171
  // bottomGear.$el.find('#test-submenu').click()
143
- bottomGear.$el.find('#test-options').show(); // as if it was clicked
172
+ bottomGear.$el.find('#test-options').show() // as if it was clicked
144
173
  await new Promise((resolve) => setTimeout(resolve, 0))
145
174
  mediaControl.container.trigger(Events.CONTAINER_CLICK)
146
175
  })
147
176
  it('should collapse it as well', () => {
148
- expect(bottomGear.$el.find('#test-options').css('display')).toBe(
149
- 'none',
150
- )
177
+ expect(bottomGear.$el.find('#test-options').css('display')).toBe('none')
151
178
  expect(bottomGear.$el.find('#gear-options').css('display')).not.toBe(
152
179
  'none',
153
180
  )
@@ -5,7 +5,10 @@ exports[`BottomGear > basically > should render 1`] = `
5
5
  /assets/icons/new/gear.svg
6
6
  </button>
7
7
  <div class="gear-wrapper gcore-skin-bg-color" id="gear-options-wrapper" style="display: none;">
8
- <ul class="gear-options-list" id="gear-options" role="menu"><li data-test=""><button>test</button></li></ul>
9
- </div>
8
+ <ul class="gear-options-list" id="gear-options" role="menu"><li data-test=""><button>test</button></li><li data-more=""><button id="more-button">more options</button></li></ul>
9
+ <div class="gear-sub-menu-wrapper" style="display: none;">
10
+ <button id="more-options-back">&lt; back</button>
11
+ <ul class="gear-sub-menu" id="more-options"><li>Item</li><li>Item</li><li>Item</li></ul>
12
+ </div></div>
10
13
  "
11
14
  `;
@@ -20,7 +20,11 @@ import { isFullscreen } from '../utils/fullscreen.js'
20
20
 
21
21
  // const T = 'plugins.clappr_stats'
22
22
 
23
- export type ClapprStatsSettings = {
23
+ /**
24
+ * Config options for the {@link ClapprStats} plugin
25
+ * @beta
26
+ */
27
+ export interface ClapprStatsSettings {
24
28
  /**
25
29
  * The interval in milliseconds of periodic measurements.
26
30
  * The plugin will emit a {@link ClapprStatsEvents.REPORT} event with the collected metrics at the specified interval.
@@ -17,7 +17,7 @@ const T = 'plugins.clips'
17
17
  */
18
18
  export interface ClipsPluginSettings {
19
19
  /**
20
- * The compiled text of the clips description, one clip per line in format :
20
+ * The compiled text of the clips description, one clip per line in format:
21
21
  * `HH:MM:SS text` or `MM:SS text` or `SS text`
22
22
  */
23
23
  text: string
@@ -123,7 +123,8 @@ export class Clips extends UICorePlugin {
123
123
  * @returns The text of the clip at the given time
124
124
  */
125
125
  getText(time: TimeValue): string | undefined {
126
- return this.clips.find((clip) => clip.start <= time && clip.end >= time)?.text
126
+ return this.clips.find((clip) => clip.start <= time && clip.end >= time)
127
+ ?.text
127
128
  }
128
129
 
129
130
  private onCoreReady() {