@brightspace-ui/labs 2.63.4 → 2.63.6

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.
package/package.json CHANGED
@@ -112,5 +112,5 @@
112
112
  "parse-srt": "^1.0.0-alpha",
113
113
  "webvtt-parser": "^2"
114
114
  },
115
- "version": "2.63.4"
115
+ "version": "2.63.6"
116
116
  }
@@ -51,6 +51,8 @@ A Lit element based media player component, designed for similarity across brows
51
51
  | poster | String | null | URL of the image to display in place of the media before it has loaded. |
52
52
  | src | String | | URL of the media to play. If multiple sources are desired, use `<source>` tags instead (see below). |
53
53
  | thumbnails | String | | If set, will show thumbnails on preview. See below for required format. |
54
+ | transcript-viewer | Boolean | false | If set and captions tracks are available, adds a "View transcript" item to the settings menu that toggles the transcript viewer. |
55
+ | transcript-viewer-on | Boolean | false | Whether the transcript viewer is shown. Reflected to the attribute. |
54
56
  | play-in-view | Boolean | false | If set, will stop the media playback if not in view
55
57
 
56
58
  ```
@@ -98,7 +100,10 @@ this.document.querySelector('d2l-labs-media-player').pause();
98
100
  | Event | Description |
99
101
  |--|--|
100
102
  | cuechange | Dispatched when the currently-displayed captions cue changes. |
103
+ | close-transcript | Dispatched when the transcript viewer's close button is clicked. |
101
104
  | descriptioncuechange | Dispatched when a new audio description cue is reached. Read the cue from `activeDescriptionCue`. |
105
+ | download-captions | Dispatched when "Captions (.vtt)" is selected in the transcript viewer's download menu. |
106
+ | download-transcript | Dispatched when "Transcript (.txt)" is selected in the transcript viewer's download menu. |
102
107
  | durationchange | Dispatched when the video or media displayed has changed its duration |
103
108
  | ended | Dispatched when the media has reached the end of its duration. |
104
109
  | error | Dispatched when the media failed to load. |
@@ -112,6 +117,7 @@ this.document.querySelector('d2l-labs-media-player').pause();
112
117
  | trackloaded | Dispatched when a track element has loaded. |
113
118
  | trackloadfailed | Dispatched when a track element could not be loaded from the provided src attribute. |
114
119
  | tracksmenuitemchanged | Dispatched when the tracks menu item has changed. |
120
+ | transcriptviewertoggle | Dispatched when the transcript viewer is opened or closed from within the player. |
115
121
  ```
116
122
  // Listen for the loadeddata event
117
123
 
@@ -1,4 +1,5 @@
1
1
  import '@brightspace-ui/core/components/button/button-icon.js';
2
+ import '@brightspace-ui/core/components/dialog/dialog.js';
2
3
  import '@brightspace-ui/core/components/dropdown/dropdown.js';
3
4
  import '@brightspace-ui/core/components/dropdown/dropdown-content.js';
4
5
  import '@brightspace-ui/core/components/icons/icon-custom.js';
@@ -6,8 +7,8 @@ import '@brightspace-ui/core/components/menu/menu.js';
6
7
  import '@brightspace-ui/core/components/menu/menu-item-radio.js';
7
8
  import '@brightspace-ui/core/components/menu/menu-item.js';
8
9
  import '@brightspace-ui/core/components/menu/menu-item-separator.js';
9
- import { css, html } from 'lit';
10
- import { bodySmallStyles } from '@brightspace-ui/core/components/typography/styles.js';
10
+ import { css, html, nothing } from 'lit';
11
+ import { bodyCompactStyles } from '@brightspace-ui/core/components/typography/styles.js';
11
12
  import { ifDefined } from 'lit/directives/if-defined.js';
12
13
 
13
14
  export const AUDIO_DESCRIPTION_REPLAY_KEY = 'r';
@@ -26,13 +27,14 @@ export const MediaPlayerAudioDescriptionsMixin = superclass => class extends sup
26
27
 
27
28
  static properties = {
28
29
  _audioDescriptionControlsInMenu: { state: true },
30
+ _audioDescriptionDialogOpened: { state: true },
29
31
  _audioDescriptionPlaying: { state: true },
30
32
  _audioDescriptionTracks: { state: true },
31
33
  _canReplayAudioDescription: { state: true },
32
34
  _selectedAudioDescriptionLanguage: { state: true },
33
35
  };
34
36
 
35
- static styles = [bodySmallStyles, css`
37
+ static styles = [bodyCompactStyles, css`
36
38
  #audio-description-skip-button {
37
39
  margin-inline-end: 12px;
38
40
  }
@@ -49,11 +51,8 @@ export const MediaPlayerAudioDescriptionsMixin = superclass => class extends sup
49
51
  width: calc(2rem - 9px);
50
52
  }
51
53
  .audio-description-header {
52
- background-color: var(--d2l-color-tungsten);
53
54
  border-bottom: 1px solid var(--d2l-color-chromite);
54
- border-start-end-radius: var(--d2l-popover-border-radius, var(--d2l-popover-default-border-radius));
55
- border-start-start-radius: var(--d2l-popover-border-radius, var(--d2l-popover-default-border-radius));
56
- color: white;
55
+ color: inherit;
57
56
  padding: 1rem;
58
57
  }
59
58
  #audio-description-button[data-enabled]::after {
@@ -77,6 +76,7 @@ export const MediaPlayerAudioDescriptionsMixin = superclass => class extends sup
77
76
  this._audioDescriptionPausedVideo = false;
78
77
  this._audioDescriptionPlaying = false;
79
78
  this._audioDescriptionControlsInMenu = false;
79
+ this._audioDescriptionDialogOpened = false;
80
80
  this._canReplayAudioDescription = false;
81
81
  }
82
82
 
@@ -111,7 +111,7 @@ export const MediaPlayerAudioDescriptionsMixin = superclass => class extends sup
111
111
  }
112
112
 
113
113
  _getAudioDescriptionsButtonView() {
114
- if (!this._audioDescriptionTracks?.length || this.hideAudioDescriptionSelection) return null;
114
+ if (!this._audioDescriptionTracks?.length || this.hideAudioDescriptionSelection) return nothing;
115
115
 
116
116
  const tooltip = this.localize('components:mediaPlayer:audioDescription');
117
117
  const replayTooltip = this.localize('components:mediaPlayer:replayAudioDescription');
@@ -166,7 +166,7 @@ export const MediaPlayerAudioDescriptionsMixin = superclass => class extends sup
166
166
  </clipPath>
167
167
  </defs>
168
168
  </svg></d2l-icon-custom></d2l-button-icon>
169
- ` : null}
169
+ ` : nothing}
170
170
  <d2l-dropdown>
171
171
  <d2l-button-icon
172
172
  class="d2l-dropdown-opener"
@@ -178,8 +178,16 @@ export const MediaPlayerAudioDescriptionsMixin = superclass => class extends sup
178
178
  <path d="M8.89199 8.55L7.21999 3.9805C7.13766 3.77783 7.05216 3.53717 6.96349 3.2585C6.87483 2.97983 6.78616 2.679 6.69749 2.356C6.61516 2.679 6.52966 2.983 6.44099 3.268C6.35233 3.54667 6.26683 3.7905 6.18449 3.9995L4.52199 8.55H8.89199ZM13.4045 13.737H11.4285C11.2068 13.737 11.0263 13.6832 10.887 13.5755C10.7477 13.4615 10.6432 13.3222 10.5735 13.1575L9.54749 10.355H3.85699L2.83099 13.1575C2.78033 13.3032 2.68216 13.4362 2.53649 13.5565C2.39083 13.6768 2.21033 13.737 1.99499 13.737H-7.82078e-06L5.40549 -1.43051e-06H8.00849L13.4045 13.737ZM27.2244 6.8685C27.2244 7.8755 27.0565 8.80017 26.7209 9.6425C26.3852 10.4848 25.9134 11.21 25.3054 11.818C24.6974 12.426 23.9659 12.8978 23.1109 13.2335C22.2559 13.5692 21.3059 13.737 20.2609 13.737H15.0264V-1.43051e-06H20.2609C21.3059 -1.43051e-06 22.2559 0.170999 23.1109 0.512999C23.9659 0.848665 24.6974 1.3205 25.3054 1.9285C25.9134 2.53017 26.3852 3.25217 26.7209 4.0945C27.0565 4.93683 27.2244 5.8615 27.2244 6.8685ZM24.6024 6.8685C24.6024 6.11483 24.501 5.44033 24.2984 4.845C24.102 4.24333 23.8139 3.73667 23.4339 3.325C23.0602 2.907 22.6042 2.58717 22.0659 2.3655C21.5339 2.14383 20.9322 2.033 20.2609 2.033H17.5914V11.704H20.2609C20.9322 11.704 21.5339 11.5932 22.0659 11.3715C22.6042 11.1498 23.0602 10.8332 23.4339 10.4215C23.8139 10.0035 24.102 9.49683 24.2984 8.9015C24.501 8.29983 24.6024 7.62217 24.6024 6.8685Z" fill="white"/>
179
179
  </svg></d2l-icon-custom></d2l-button-icon>
180
180
  <d2l-dropdown-content id="audio-description-dropdown-content" class="vdiff-target" no-padding no-padding-header no-pointer theme="${ifDefined(this._getTheme())}">
181
- ${showControls ? html`<div class="d2l-body-small audio-description-header" slot="header">${this.localize('components:mediaPlayer:extendedAudioDescription')}</div>` : null}
181
+ ${showControls ? nothing : html`<div class="d2l-body-compact audio-description-header" slot="header">${this.localize('components:mediaPlayer:audioDescriptions')}</div>`}
182
182
  <d2l-menu label="${tooltip}" @d2l-menu-item-change=${this.#onAudioDescriptionMenuItemChange} theme="${ifDefined(this._getTheme())}">
183
+ ${showControls ? html`
184
+ <d2l-menu-item
185
+ id="audio-description-about-menu-item"
186
+ text="${this.localize('components:mediaPlayer:aboutExtendedAudioDescriptions')}"
187
+ @d2l-menu-item-select="${this.#onAboutExtendedAudioDescriptionsSelect}"
188
+ ></d2l-menu-item>
189
+ <d2l-menu-item-separator></d2l-menu-item-separator>
190
+ ` : nothing}
183
191
  ${showControls && this._audioDescriptionControlsInMenu ? html`
184
192
  <d2l-menu-item
185
193
  text="${this.localize('components:mediaPlayer:replay')}"
@@ -192,7 +200,7 @@ export const MediaPlayerAudioDescriptionsMixin = superclass => class extends sup
192
200
  @d2l-menu-item-select="${this._skipAudioDescription}"
193
201
  ></d2l-menu-item>
194
202
  <d2l-menu-item-separator></d2l-menu-item-separator>
195
- ` : null}
203
+ ` : nothing}
196
204
  <d2l-menu-item-radio
197
205
  ?selected="${!this._selectedAudioDescriptionLanguage}"
198
206
  text="${this.localize('components:mediaPlayer:off')}"
@@ -207,6 +215,17 @@ export const MediaPlayerAudioDescriptionsMixin = superclass => class extends sup
207
215
  </d2l-menu>
208
216
  </d2l-dropdown-content>
209
217
  </d2l-dropdown>
218
+ ${showControls ? html`
219
+ <d2l-dialog
220
+ id="audio-description-about-dialog"
221
+ width="500"
222
+ title-text="${this.localize('components:mediaPlayer:aboutExtendedAudioDescriptions')}"
223
+ ?opened="${this._audioDescriptionDialogOpened}"
224
+ @d2l-dialog-close="${this.#onAboutExtendedAudioDescriptionsDialogClose}"
225
+ >
226
+ <p class="d2l-body-compact">${this.localize('components:mediaPlayer:aboutExtendedAudioDescriptionsText')}</p>
227
+ </d2l-dialog>
228
+ ` : nothing}
210
229
  `;
211
230
  }
212
231
 
@@ -346,6 +365,15 @@ export const MediaPlayerAudioDescriptionsMixin = superclass => class extends sup
346
365
  return this._audioDescriptionTracks?.find(track => track.srclang === this._selectedAudioDescriptionLanguage);
347
366
  }
348
367
 
368
+ #onAboutExtendedAudioDescriptionsDialogClose() {
369
+ this._audioDescriptionDialogOpened = false;
370
+ }
371
+
372
+ #onAboutExtendedAudioDescriptionsSelect() {
373
+ this.shadowRoot?.querySelector('#audio-description-dropdown-content')?.close();
374
+ this._audioDescriptionDialogOpened = true;
375
+ }
376
+
349
377
  #onAudioDescriptionMenuItemChange(e) {
350
378
  this._cancelAudioDescription();
351
379
  this._selectedAudioDescriptionLanguage = e.target.value || null;
@@ -0,0 +1,158 @@
1
+ import { css, html, nothing } from 'lit';
2
+ import { ifDefined } from 'lit/directives/if-defined.js';
3
+ import { styleMap } from 'lit/directives/style-map.js';
4
+
5
+ // Expects the host to provide _duration, _hovering, _hoverTime, _getPercentageTime() and _getTheme()
6
+ export const MediaPlayerChaptersMixin = superclass => class extends superclass {
7
+
8
+ static properties = {
9
+ metadata: { type: Object },
10
+ _chapters: { state: true },
11
+ };
12
+
13
+ static styles = [super.styles, css`
14
+ .chapter-marker, .chapter-marker-highlight {
15
+ cursor: pointer;
16
+ height: 6px;
17
+ pointer-events: none;
18
+ position: absolute;
19
+ top: 4px;
20
+ width: 3px;
21
+ z-index: 2;
22
+ }
23
+
24
+ .chapter-marker {
25
+ background-color: var(--d2l-color-ferrite);
26
+ }
27
+
28
+ .chapter-marker-highlight {
29
+ background-color: var(--d2l-color-celestine-minus-1);
30
+ }
31
+
32
+ .chapter-marker[theme="dark"] {
33
+ background-color: white;
34
+ }
35
+ `];
36
+
37
+ constructor() {
38
+ super();
39
+ this._chapters = [];
40
+ }
41
+
42
+ willUpdate(changedProperties) {
43
+ super.willUpdate(changedProperties);
44
+
45
+ if (changedProperties.has('metadata')) {
46
+ this._getMetadata();
47
+ }
48
+
49
+ // A chapter starting at the very end of the media has no visible range
50
+ if (this._chapters.at(-1)?.time === Math.floor(this._duration)) {
51
+ this._chapters = this._chapters.slice(0, -1);
52
+ }
53
+ }
54
+
55
+ _getChapterMarkersView() {
56
+ if (this._chapters.length === 0) return;
57
+
58
+ let start, end;
59
+ for (let i = 0; i < this._chapters.length; i++) {
60
+ if (i === this._chapters.length - 1) {
61
+ start = this._chapters[i].time;
62
+ break;
63
+ }
64
+ else if (this._hoverTime >= this._chapters[i].time && this._hoverTime < this._chapters[i + 1].time) {
65
+ start = this._chapters[i].time;
66
+ end = this._chapters[i + 1].time;
67
+ break;
68
+ }
69
+ }
70
+
71
+ return this._chapters.map(chapter => {
72
+ const highlight = this._hovering && this._hoverTime >= this._chapters[0].time && (chapter.time === start || chapter.time === end);
73
+ return chapter.time > 0 ? html`
74
+ <div
75
+ class=${highlight ? 'chapter-marker-highlight' : 'chapter-marker'}
76
+ theme="${ifDefined(this._getTheme())}"
77
+ style=${styleMap({ left: `${this._getPercentageTime(chapter.time)}%` })}
78
+ ></div>
79
+ ` : nothing;
80
+ });
81
+ }
82
+
83
+ _getChapterTitle() {
84
+ if (!(this._chapters.length > 0 && this._hoverTime >= this._chapters[0].time)) return;
85
+
86
+ const chapter = this._chapters.find((_chapter, index, chapters) => (
87
+ index === chapters.length - 1 || (this._hoverTime >= chapters[0].time && this._hoverTime < chapters[index + 1].time)
88
+ ));
89
+ const chapterTitle = chapter && chapter.title;
90
+
91
+ if (!chapterTitle) return;
92
+
93
+ if (typeof chapterTitle === 'string') {
94
+ return chapterTitle;
95
+ }
96
+
97
+ for (const locale in chapterTitle) {
98
+ if (locale.split('-')[0] === 'en') {
99
+ return chapterTitle[locale];
100
+ }
101
+ }
102
+ }
103
+
104
+ _getMetadata() {
105
+ if (!this.metadata) return;
106
+
107
+ const data = (typeof this.metadata === 'string' || this.metadata instanceof String) ? JSON.parse(this.metadata) : this.metadata;
108
+ if (!(data && data.chapters && data.chapters.length > 0)) return;
109
+ let chapters = data.chapters.map(({ time, title }) => {
110
+ return {
111
+ time: parseInt(time),
112
+ title
113
+ };
114
+ }).sort((a, b) => a.time - b.time);
115
+
116
+ if (!data.cuts) {
117
+ data.cuts = [];
118
+ }
119
+
120
+ // updating the chapter times based on the cuts, loops over all chapters per cut because it can change multiple chapters
121
+ let cutDiff = 0;
122
+ for (const cut of data.cuts) {
123
+ const cutIn = cut.in - cutDiff;
124
+
125
+ const newChapters = new Map(); // using map to preserve sort ordering
126
+
127
+ if (!cut.out) { // if cut is until the end of the video
128
+ for (const chapter of chapters) {
129
+ if (chapter.time < cutIn) {
130
+ newChapters.set(chapter.time, chapter.title);
131
+ }
132
+ }
133
+ } else {
134
+ const cutOut = cut.out - cutDiff;
135
+ const cutLength = cutOut - cutIn;
136
+
137
+ for (const chapter of chapters) {
138
+ let newTime = chapter.time;
139
+ if (chapter.time > cutIn && chapter.time <= cutOut) {
140
+ newTime = cutIn;
141
+ } else if (chapter.time > cutOut) {
142
+ newTime = chapter.time - cutLength;
143
+ }
144
+
145
+ newChapters.set(newTime, chapter.title);
146
+ }
147
+
148
+ cutDiff += cutLength;
149
+ }
150
+
151
+ chapters = [...newChapters].map(([chapterTime, chapterTitle]) => ({
152
+ time: chapterTime,
153
+ title: chapterTitle
154
+ }));
155
+ }
156
+ this._chapters = chapters;
157
+ }
158
+ };
@@ -0,0 +1,116 @@
1
+ import { css, html } from 'lit';
2
+
3
+ const DEFAULT_PREVIEW_WIDTH = 160;
4
+ const DEFAULT_PREVIEW_HEIGHT = 90;
5
+
6
+ // Expects the host to provide _hovering, _hoverTime, _timelinePreviewOffset, _getChapterTitle() and static _formatTime()
7
+ export const MediaPlayerThumbnailsMixin = superclass => class extends superclass {
8
+
9
+ static properties = {
10
+ thumbnails: { type: String },
11
+ _thumbnailsImage: { state: true },
12
+ };
13
+
14
+ static styles = [super.styles, css`
15
+ #thumbnails-preview-container {
16
+ bottom: 60px;
17
+ position: absolute;
18
+ transform: translateX(-50%);
19
+ z-index: 2;
20
+ }
21
+
22
+ #thumbnails-preview-chapter {
23
+ background: #00000072;
24
+ position: absolute;
25
+ text-align: center;
26
+ text-shadow: 0 0 5px rgb(0 0 0 / 75%);
27
+ width: 100%;
28
+ z-index: 2;
29
+ }
30
+
31
+ #thumbnails-preview-time {
32
+ background: #00000042;
33
+ bottom: 3px;
34
+ font-size: 14px;
35
+ left: 0;
36
+ position: absolute;
37
+ text-align: center;
38
+ text-shadow: 0 0 4px rgb(0 0 0 / 75%);
39
+ width: 100%;
40
+ z-index: 2;
41
+ }
42
+
43
+ #thumbnails-preview-image {
44
+ background-repeat: no-repeat;
45
+ position: relative;
46
+ width: 100%;
47
+ z-index: 2;
48
+ }
49
+ `];
50
+
51
+ updated(changedProperties) {
52
+ super.updated(changedProperties);
53
+
54
+ if (changedProperties.has('thumbnails')) {
55
+ this._getThumbnails();
56
+ }
57
+ }
58
+
59
+ _getThumbnails() {
60
+ if (!this.thumbnails) return;
61
+ this._thumbnailsImage = new Image();
62
+ this._thumbnailsImage.src = this.thumbnails;
63
+ }
64
+
65
+ _getTimelinePreview() {
66
+ if (!this._hovering) return;
67
+ const chapterTitleLabel = this._getChapterTitle();
68
+ const hoverTimeText = this.constructor._formatTime(this._hoverTime);
69
+
70
+ if (!(this.thumbnails && this._thumbnailsImage))
71
+ return html`
72
+ <div id="thumbnails-preview-container"
73
+ style="width: ${DEFAULT_PREVIEW_WIDTH}px; left: clamp(${DEFAULT_PREVIEW_WIDTH / 2}px, ${this._timelinePreviewOffset}%, calc(100% - ${DEFAULT_PREVIEW_WIDTH / 2}px));">
74
+ <div
75
+ id="thumbnails-preview-image"
76
+ >
77
+ <span id="thumbnails-preview-time">${hoverTimeText}</span>
78
+ </div>
79
+ ${chapterTitleLabel &&
80
+ html`<span class="d2l-label-text" id="thumbnails-preview-chapter" style="bottom: ${DEFAULT_PREVIEW_HEIGHT - 60}px">${chapterTitleLabel}</span>`}
81
+ </div>
82
+ `;
83
+
84
+ // format of the thumbnail is either [url]/timelineThumbnails-h<height>w<height>i<interval>-<hash>.[png|jpg]
85
+ // or [url]/th<height>w<height>i<interval>-<hash>.[png|jpg]
86
+ const matches = this.thumbnails.match(/(timelineThumbnails-|t)h(\d+)w(\d+)i(\d+)[^/]*$/i);
87
+ if (matches && matches.length !== 5) return; // no matches
88
+ const [ , , thumbHeight, thumbWidth, interval] = matches;
89
+
90
+ const width = this._thumbnailsImage.width;
91
+ const height = this._thumbnailsImage.height;
92
+
93
+ const rows = height / thumbHeight;
94
+ const columns = width / thumbWidth;
95
+
96
+ let thumbNum = Math.floor(this._hoverTime / interval);
97
+ if (thumbNum >= rows * columns) thumbNum = rows * columns - 1;
98
+
99
+ const row = Math.floor(thumbNum / columns);
100
+ const column = thumbNum % columns;
101
+
102
+ return html`
103
+ <div id="thumbnails-preview-container"
104
+ style="width: ${thumbWidth}px; left: clamp(${thumbWidth / 2}px, ${this._timelinePreviewOffset}%, calc(100% - ${thumbWidth / 2}px));">
105
+ <div
106
+ id="thumbnails-preview-image"
107
+ style="height: ${thumbHeight}px; background: url(${this._thumbnailsImage.src}) ${-column * thumbWidth}px ${-row * thumbHeight}px / ${width}px ${height}px;"
108
+ >
109
+ <span id="thumbnails-preview-time">${hoverTimeText}</span>
110
+ </div>
111
+ ${chapterTitleLabel &&
112
+ html`<span class="d2l-label-text" id="thumbnails-preview-chapter" style="bottom: ${thumbHeight}px">${chapterTitleLabel}</span>`}
113
+ </div>
114
+ `;
115
+ }
116
+ };