@gcorevideo/player 2.22.14 → 2.22.15

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.
@@ -1,20 +1,34 @@
1
- import {
2
- Events,
3
- template,
4
- $,
5
- Container,
6
- UIContainerPlugin,
7
- } from '@clappr/core'
1
+ import { Events, template, $, Container, UIContainerPlugin } from '@clappr/core'
8
2
 
9
3
  import { CLAPPR_VERSION } from '../../build.js'
10
4
 
11
5
  import '../../../assets/context-menu/context_menu.scss'
12
6
  import templateHtml from '../../../assets/context-menu/context_menu.ejs'
13
- import { version } from '../../version.js'
14
7
 
15
- type MenuOption = {
16
- label: string
8
+ /**
9
+ * @beta
10
+ */
11
+ export type MenuOption = {
12
+ /**
13
+ * Menu item label text. One of `label` or `labelKey` must be specified.
14
+ */
15
+ label?: string
16
+ /**
17
+ * Menu item label localisation key, if specified, the `label` will be ignored
18
+ */
19
+ labelKey?: string
20
+ /**
21
+ * Menu item name. Must be unique.
22
+ */
17
23
  name: string
24
+ /**
25
+ * Menu item handler function
26
+ */
27
+ handler?: () => void
28
+ /**
29
+ * Menu item icon, plain HTML string
30
+ */
31
+ icon?: string
18
32
  }
19
33
 
20
34
  /**
@@ -22,23 +36,21 @@ type MenuOption = {
22
36
  * @beta
23
37
  */
24
38
  export interface ContextMenuPluginSettings {
25
- label?: string
26
- url?: string
27
- preventShowContextMenu?: boolean
39
+ options?: MenuOption[]
28
40
  }
29
41
 
42
+ const T = 'plugins.context_menu'
43
+
30
44
  /**
31
45
  * `PLUGIN` that displays a small context menu when clicked on the player container.
32
46
  * @beta
33
47
  * @remarks
34
48
  * Configuration options - {@link ContextMenuPluginSettings}
49
+ *
50
+ * Should not be used together with {@link ClickToPause} plugin
35
51
  */
36
52
  export class ContextMenu extends UIContainerPlugin {
37
- private _label: string = ''
38
-
39
- private _url: string = ''
40
-
41
- private menuOptions: MenuOption[] = []
53
+ private open = false
42
54
 
43
55
  /**
44
56
  * @internal
@@ -63,38 +75,17 @@ export class ContextMenu extends UIContainerPlugin {
63
75
 
64
76
  private static readonly template = template(templateHtml)
65
77
 
66
- private get label() {
67
- return this._label || 'Gcore player ver. ' + version().gplayer
68
- }
69
-
70
- private get url() {
71
- return this._url || 'https://gcore.com/'
72
- }
73
-
74
- private get exposeVersion() {
75
- return {
76
- label: this.label,
77
- name: 'version',
78
- }
79
- }
80
-
81
78
  /**
82
79
  * @internal
83
80
  */
84
81
  override get events() {
85
82
  return {
86
- 'click [data-version]': 'onOpenMainPage',
83
+ 'click [role="menuitem"]': 'runAction',
87
84
  }
88
85
  }
89
86
 
90
87
  constructor(container: Container) {
91
88
  super(container)
92
- if (this.options.contextMenu && this.options.contextMenu.label) {
93
- this._label = this.options.contextMenu.label
94
- }
95
- if (this.options.contextMenu && this.options.contextMenu.url) {
96
- this._url = this.options.contextMenu.url
97
- }
98
89
  this.render()
99
90
  $('body').on('click', this.hideOnBodyClick)
100
91
  }
@@ -106,9 +97,9 @@ export class ContextMenu extends UIContainerPlugin {
106
97
  this.listenTo(
107
98
  this.container,
108
99
  Events.CONTAINER_CONTEXTMENU,
109
- this.toggleContextMenu,
100
+ this.onContextMenu,
110
101
  )
111
- this.listenTo(this.container, Events.CONTAINER_CLICK, this.hide)
102
+ this.listenTo(this.container, Events.CONTAINER_CLICK, this.onContainerClick)
112
103
  }
113
104
 
114
105
  /**
@@ -119,41 +110,66 @@ export class ContextMenu extends UIContainerPlugin {
119
110
  return super.destroy()
120
111
  }
121
112
 
122
- private toggleContextMenu(event: MouseEvent) {
113
+ private onContainerClick() {
114
+ this.hide()
115
+ }
116
+
117
+ private onContextMenu(event: MouseEvent) {
118
+ if (!this.options.contextMenu?.options?.length) {
119
+ return
120
+ }
123
121
  event.preventDefault()
124
- const offset = this.container?.$el.offset()
122
+ event.stopPropagation()
125
123
 
124
+ const offset = this.container?.$el.offset()
126
125
  this.show(event.pageY - offset.top, event.pageX - offset.left)
127
126
  }
128
127
 
129
128
  private show(top: number, left: number) {
130
- this.hide()
131
- if (
132
- this.options.contextMenu &&
133
- this.options.contextMenu.preventShowContextMenu
134
- ) {
135
- return
136
- }
129
+ this.open = true
137
130
  this.$el.css({ top, left })
138
131
  this.$el.show()
139
132
  }
140
133
 
141
134
  private hide() {
135
+ this.open = false
142
136
  this.$el.hide()
143
137
  }
144
138
 
145
- private onOpenMainPage() {
146
- window.open(this.url, '_blank')
139
+ private runAction(event: MouseEvent) {
140
+ event.preventDefault()
141
+ event.stopPropagation()
142
+
143
+ const itemName = (event.currentTarget as HTMLButtonElement).dataset.name
144
+ if (!itemName) {
145
+ return
146
+ }
147
+ const item = this.options.contextMenu?.options.find(
148
+ (option: MenuOption) => option.name === itemName,
149
+ )
150
+ if (item?.handler) {
151
+ item.handler()
152
+ }
153
+ this.hide()
147
154
  }
148
155
 
149
156
  /**
150
157
  * @internal
151
158
  */
152
159
  override render() {
153
- this.menuOptions = [this.exposeVersion]
154
- this.$el.html(ContextMenu.template({ options: this.menuOptions }))
160
+ if (!this.options.contextMenu?.options?.length) {
161
+ return this
162
+ }
163
+ const options = this.options.contextMenu.options
164
+ this.$el.html(
165
+ ContextMenu.template({
166
+ options,
167
+ i18n: this.container.i18n,
168
+ iconic: options.some((option: MenuOption) => option.icon),
169
+ }),
170
+ )
155
171
  this.container.$el.append(this.$el) // TODO append to the container, turn into a container plugin
156
- this.hide()
172
+ this.$el.hide()
157
173
 
158
174
  return this
159
175
  }
@@ -56,14 +56,26 @@ describe('MediaControl', () => {
56
56
  core.activePlayback.emit(Events.PLAYBACK_LOADEDMETADATA)
57
57
  core.activeContainer.emit(Events.CONTAINER_LOADEDMETADATA)
58
58
  })
59
- it('should wait a delay before rendering anything', async () => {
59
+ it('should wait a delay before rendering anything', async () => {
60
60
  expect(mediaControl.el.innerHTML).toBe('')
61
61
  await new Promise((resolve) => setTimeout(resolve, 35))
62
62
  expect(mediaControl.el.innerHTML).toMatchSnapshot()
63
- expect(mediaControl.$el.find('.media-control-left-panel [data-playpause]').length).toBeGreaterThan(0)
64
- expect(mediaControl.$el.find('.media-control-left-panel [data-volume]').length).toBeGreaterThan(0)
65
- expect(mediaControl.$el.find('.media-control-center-panel [data-seekbar]').length).toBeGreaterThan(0)
66
- expect(mediaControl.$el.find('.media-control-right-panel [data-fullscreen]').length).toBeGreaterThan(0)
63
+ expect(
64
+ mediaControl.$el.find('.media-control-left-panel [data-playpause]')
65
+ .length,
66
+ ).toBeGreaterThan(0)
67
+ expect(
68
+ mediaControl.$el.find('.media-control-left-panel [data-volume]')
69
+ .length,
70
+ ).toBeGreaterThan(0)
71
+ expect(
72
+ mediaControl.$el.find('.media-control-center-panel [data-seekbar]')
73
+ .length,
74
+ ).toBeGreaterThan(0)
75
+ expect(
76
+ mediaControl.$el.find('.media-control-right-panel [data-fullscreen]')
77
+ .length,
78
+ ).toBeGreaterThan(0)
67
79
  })
68
80
  })
69
81
  })
@@ -72,9 +84,7 @@ describe('MediaControl', () => {
72
84
  mediaControl = new MediaControl(core)
73
85
  core.emit(Events.CORE_READY)
74
86
  core.emit(Events.CORE_ACTIVE_CONTAINER_CHANGED, core.activeContainer)
75
- core.activePlayback.emit(Events.PLAYBACK_LOADEDMETADATA)
76
- core.activeContainer.emit(Events.CONTAINER_LOADEDMETADATA)
77
- await new Promise((resolve) => setTimeout(resolve, 25))
87
+ await runMetadataLoaded(core)
78
88
  })
79
89
  describe.each([
80
90
  [
@@ -92,20 +102,29 @@ describe('MediaControl', () => {
92
102
  core.activePlayback.emit(Events.PLAYBACK_SETTINGSUPDATE)
93
103
  core.activeContainer.emit(Events.CONTAINER_SETTINGSUPDATE)
94
104
  })
95
- it.each(
96
- settings.left
97
- )("should render %s control", (element) => {
98
- const el = mediaControl.$el.find(`.media-control-left-panel [data-${element}]`)
105
+ it.each(settings.left)('should render %s control', (element) => {
106
+ const el = mediaControl.$el.find(
107
+ `.media-control-left-panel [data-${element}]`,
108
+ )
99
109
  expect(el.length).toBeGreaterThan(0)
100
110
  })
101
111
  it.each(
102
- arraySubtract(['playpause', 'playstop', 'position', 'duration', 'volume'], settings.left)
103
- )("should not render %s control", (element) => {
104
- const el = mediaControl.$el.find(`.media-control-left-panel [data-${element}]`)
112
+ arraySubtract(
113
+ ['playpause', 'playstop', 'position', 'duration', 'volume'],
114
+ settings.left,
115
+ ),
116
+ )('should not render %s control', (element) => {
117
+ const el = mediaControl.$el.find(
118
+ `.media-control-left-panel [data-${element}]`,
119
+ )
105
120
  expect(el.length).toEqual(0)
106
121
  })
107
- it(`should ${settings.seekEnabled ? '' : 'not '}render the seek bar`, () => {
108
- const seekbar = mediaControl.$el.find('.media-control-center-panel [data-seekbar]')
122
+ it(`should ${
123
+ settings.seekEnabled ? '' : 'not '
124
+ }render the seek bar`, () => {
125
+ const seekbar = mediaControl.$el.find(
126
+ '.media-control-center-panel [data-seekbar]',
127
+ )
109
128
  expect(seekbar.length).toBeGreaterThan(1)
110
129
  if (settings.seekEnabled) {
111
130
  expect(seekbar.hasClass('seek-disabled')).toBe(false)
@@ -113,7 +132,9 @@ describe('MediaControl', () => {
113
132
  expect(seekbar.hasClass('seek-disabled')).toBe(true)
114
133
  }
115
134
  })
116
- it(`should ${settings.left.includes('volume') ? '' : 'not '}render the volume control`, () => {
135
+ it(`should ${
136
+ settings.left.includes('volume') ? '' : 'not '
137
+ }render the volume control`, () => {
117
138
  const volume = mediaControl.$el.find('.drawer-container[data-volume]')
118
139
  if (settings.left.includes('volume')) {
119
140
  expect(volume.length).toEqual(1)
@@ -121,8 +142,12 @@ describe('MediaControl', () => {
121
142
  expect(volume.length).toEqual(0)
122
143
  }
123
144
  })
124
- it(`should ${settings.right.includes('fullscreen') ? '' : 'not '}render the fullscreen control`, () => {
125
- const fullscreen = mediaControl.$el.find('.media-control-right-panel [data-fullscreen]')
145
+ it(`should ${
146
+ settings.right.includes('fullscreen') ? '' : 'not '
147
+ }render the fullscreen control`, () => {
148
+ const fullscreen = mediaControl.$el.find(
149
+ '.media-control-right-panel [data-fullscreen]',
150
+ )
126
151
  if (settings.right.includes('fullscreen')) {
127
152
  expect(fullscreen.length).toEqual(1)
128
153
  } else {
@@ -138,12 +163,13 @@ describe('MediaControl', () => {
138
163
  core.emit(Events.CORE_ACTIVE_CONTAINER_CHANGED, core.activeContainer)
139
164
  })
140
165
  describe('when live', () => {
141
- beforeEach(() => {
166
+ beforeEach(async () => {
142
167
  core.activeContainer.getPlaybackType.mockReturnValue(Playback.LIVE)
143
168
  core.activePlayback.getPlaybackType.mockReturnValue(Playback.LIVE)
144
169
  core.getPlaybackType.mockReturnValue(Playback.LIVE)
145
- core.activeContainer.emit(Events.CONTAINER_LOADEDMETADATA)
146
- core.activePlayback.emit(Events.PLAYBACK_LOADEDMETADATA)
170
+ // This is not strictly necessary as the CSS class is applied on the root element and does not require rendering.
171
+ // However, it makes the scenario more realistic
172
+ await runMetadataLoaded(core)
147
173
  // TODO playback.settings
148
174
  core.activePlayback.emit(Events.PLAYBACK_SETTINGSUPDATE)
149
175
  })
@@ -152,11 +178,11 @@ describe('MediaControl', () => {
152
178
  })
153
179
  })
154
180
  describe('when vod', () => {
155
- beforeEach(() => {
181
+ beforeEach(async () => {
156
182
  core.activeContainer.getPlaybackType.mockReturnValue(Playback.VOD)
157
183
  core.activePlayback.getPlaybackType.mockReturnValue(Playback.VOD)
158
184
  core.getPlaybackType.mockReturnValue(Playback.VOD)
159
- core.activeContainer.emit(Events.CONTAINER_LOADEDMETADATA)
185
+ await runMetadataLoaded(core)
160
186
  })
161
187
  it('should not apply live style class', () => {
162
188
  expect(mediaControl.$el.hasClass('live')).toBe(false)
@@ -164,11 +190,13 @@ describe('MediaControl', () => {
164
190
  })
165
191
  })
166
192
  describe('putElement', () => {
167
- beforeEach(() => {
193
+ beforeEach(async () => {
168
194
  mediaControl = new MediaControl(core)
169
- core.emit('core:ready')
195
+ core.emit(Events.CORE_READY)
196
+ core.emit(Events.CORE_ACTIVE_CONTAINER_CHANGED, core.activeContainer)
170
197
  core.activeContainer.settings = {}
171
- core.emit('core:active:container:changed', core.activeContainer)
198
+ core.emit(Events.CONTAINER_SETTINGSUPDATE)
199
+ await runMetadataLoaded(core)
172
200
  })
173
201
  describe.each([
174
202
  ['pip' as MediaControlElement],
@@ -200,12 +228,13 @@ describe('MediaControl', () => {
200
228
  core.emit(Events.CORE_READY)
201
229
  })
202
230
  describe('dvr', () => {
203
- beforeEach(() => {
231
+ beforeEach(async () => {
204
232
  core.activeContainer.settings = {
205
233
  left: ['playpause', 'position', 'duration'],
206
234
  seekEnabled: true,
207
235
  }
208
236
  core.emit(Events.CORE_ACTIVE_CONTAINER_CHANGED, core.activeContainer)
237
+ await runMetadataLoaded(core)
209
238
  })
210
239
  describe('when enabled', () => {
211
240
  beforeEach(() => {
@@ -241,10 +270,11 @@ describe('MediaControl', () => {
241
270
  })
242
271
  })
243
272
  describe('dvr mode', () => {
244
- beforeEach(() => {
273
+ beforeEach(async () => {
245
274
  mediaControl = new MediaControl(core)
246
275
  core.emit(Events.CORE_READY)
247
276
  core.emit(Events.CORE_ACTIVE_CONTAINER_CHANGED, core.activeContainer)
277
+ await runMetadataLoaded(core)
248
278
  })
249
279
  describe('by default', () => {
250
280
  it('should not apply DVR style class', () => {
@@ -270,3 +300,9 @@ describe('MediaControl', () => {
270
300
  function arraySubtract<T extends string>(arr1: T[], arr2: T[]) {
271
301
  return arr1.filter((item) => !arr2.includes(item))
272
302
  }
303
+
304
+ function runMetadataLoaded(core: any) {
305
+ core.activePlayback.emit(Events.PLAYBACK_LOADEDMETADATA)
306
+ core.activeContainer.emit(Events.CONTAINER_LOADEDMETADATA)
307
+ return new Promise((resolve) => setTimeout(resolve, 25))
308
+ }