@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.
@@ -0,0 +1,266 @@
1
+ import '@brightspace-ui/core/components/dropdown/dropdown-button-subtle.js';
2
+ import '@brightspace-ui/core/components/dropdown/dropdown-menu.js';
3
+ import '@brightspace-ui/core/components/icons/icon.js';
4
+ import '@brightspace-ui/core/components/menu/menu.js';
5
+ import '@brightspace-ui/core/components/menu/menu-item.js';
6
+ import { css, html, nothing } from 'lit';
7
+
8
+ // Expects the host to provide _media, _tracks, mediaType, currentTime, _selectedTrackIdentifier, _getSrclangFromTrackIdentifier(), _onCueChange() and localize()
9
+ export const MediaPlayerTranscriptMixin = superclass => class extends superclass {
10
+
11
+ static properties = {
12
+ transcriptViewer: { type: Boolean, attribute: 'transcript-viewer' },
13
+ transcriptViewerOn: { type: Boolean, attribute: 'transcript-viewer-on', reflect: true },
14
+ };
15
+
16
+ static styles = [super.styles, css`
17
+ :host([transcript-viewer-on]) #video,
18
+ :host([transcript-viewer-on]) #video-poster {
19
+ height: 30%;
20
+ left: 0;
21
+ min-height: 30%;
22
+ position: absolute;
23
+ top: 0;
24
+ width: 30%;
25
+ }
26
+ .transcript-cue-container {
27
+ padding-left: 10px;
28
+ }
29
+ .video-transcript-cue {
30
+ padding-left: 5px;
31
+ }
32
+ .audio-transcript-cue {
33
+ padding-left: 5px;
34
+ }
35
+ .video-transcript-cue[active] {
36
+ background-color: gray;
37
+ box-shadow: -5px 0 0 white;
38
+ }
39
+ .audio-transcript-cue[active] {
40
+ background-color: lightgray;
41
+ box-shadow: -5px 0 0 black;
42
+ }
43
+ #video-transcript-viewer {
44
+ bottom: 55px;
45
+ color: white;
46
+ overflow-anchor: none;
47
+ overflow-y: auto;
48
+ position: absolute;
49
+ right: 0;
50
+ top: 50px;
51
+ width: 65%;
52
+ z-index: 1;
53
+ }
54
+ #audio-transcript-viewer {
55
+ bottom: 60px;
56
+ color: black;
57
+ overflow-anchor: none;
58
+ overflow-y: auto;
59
+ position: absolute;
60
+ right: 0;
61
+ top: 45px;
62
+ width: 100%;
63
+ z-index: 1;
64
+ }
65
+ #close-transcript {
66
+ position: absolute;
67
+ right: 7px;
68
+ top: 0;
69
+ z-index: 1;
70
+ }
71
+ #video-transcript-download-button {
72
+ left: 35%;
73
+ position: absolute;
74
+ top: 5px;
75
+ z-index: 2;
76
+ }
77
+ #audio-transcript-download-button {
78
+ left: 10px;
79
+ position: absolute;
80
+ top: 0;
81
+ z-index: 2;
82
+ }
83
+ #audio-transcript-download-menu {
84
+ left: 35px;
85
+ }
86
+ #video-close-transcript-icon {
87
+ color: white;
88
+ }
89
+ #audio-close-transcript-icon {
90
+ color: black;
91
+ }
92
+ `];
93
+
94
+ constructor() {
95
+ super();
96
+ this.afterCaptions = [];
97
+ this.beforeCaptions = [];
98
+ this.transcriptViewer = false;
99
+ this.transcriptViewerOn = false;
100
+ }
101
+
102
+ willUpdate(changedProperties) {
103
+ super.willUpdate(changedProperties);
104
+ // The viewer element is re-created each time it is shown
105
+ if (changedProperties.has('transcriptViewerOn')) this._transcriptViewer = null;
106
+ }
107
+
108
+ _closeTranscript() {
109
+ this._setTranscriptViewerOn(false);
110
+ this.dispatchEvent(new CustomEvent('close-transcript', { bubbles: true, composed: true }));
111
+ }
112
+
113
+ _downloadCaptions() {
114
+ this.dispatchEvent(new CustomEvent('download-captions', { bubbles: true, composed: true }));
115
+ }
116
+
117
+ _downloadTranscript() {
118
+ this.dispatchEvent(new CustomEvent('download-transcript', { bubbles: true, composed: true }));
119
+ }
120
+
121
+ async _onTranscriptCueChange() {
122
+ if (!this._transcriptViewer) {
123
+ this._transcriptViewer = this.shadowRoot.getElementById('video-transcript-viewer')
124
+ || this.shadowRoot.getElementById('audio-transcript-viewer');
125
+ }
126
+ this._updateTranscriptViewerCues();
127
+ await this.requestUpdate();
128
+ this._scrollTranscriptViewer();
129
+ }
130
+
131
+ _renderTranscriptViewer() {
132
+ if (!this._media) {
133
+ return;
134
+ }
135
+ const captionsMenu = this.shadowRoot.getElementById('captions-menu');
136
+ if (captionsMenu) {
137
+ this._captionsMenuReturnItem = captionsMenu.shadowRoot.querySelector('d2l-menu-item-return');
138
+ this._captionsMenuReturnItem?.setAttribute('text', this.localize('components:mediaPlayer:language'));
139
+ }
140
+
141
+ if (!this._transcriptViewer) {
142
+ this._onCueChange();
143
+ }
144
+
145
+ const isVideo = this.mediaType === 'video';
146
+ const captionsToHtml = (item) => {
147
+ const updateTime = async() => {
148
+ this.currentTime = item.startTime;
149
+ this._media.currentTime = item.startTime;
150
+ };
151
+ return html`
152
+ <div class=${isVideo ? 'video-transcript-cue' : 'audio-transcript-cue'}
153
+ @click=${updateTime}>
154
+ ${item.text}<br>
155
+ </div>`;
156
+ };
157
+
158
+ return html`
159
+ <span id="close-transcript"
160
+ @click=${this._closeTranscript}>
161
+ <d2l-icon class="d2l-button-icon"
162
+ id=${isVideo ? 'video-close-transcript-icon' : 'audio-close-transcript-icon'}
163
+ icon="tier1:close-small"></d2l-icon>
164
+ </span>
165
+ <div
166
+ id=${isVideo ? 'video-transcript-viewer' : 'audio-transcript-viewer'}
167
+ >
168
+ <div class="transcript-cue-container">
169
+ ${this.beforeCaptions.map(captionsToHtml)}
170
+ <div class=${isVideo ? 'video-transcript-cue' : 'audio-transcript-cue'} active
171
+ id="transcript-viewer-active-cue">
172
+ ${this.transcriptActiveCue?.text}
173
+ </div>
174
+ ${this.afterCaptions.map(captionsToHtml)}
175
+ </div>
176
+ </div>
177
+ <d2l-dropdown-button-subtle
178
+ id=${isVideo ? 'video-transcript-download-button' : 'audio-transcript-download-button'}
179
+ text="${this.localize('components:mediaPlayer:download')}">
180
+ <d2l-dropdown-menu id=${isVideo ? 'video-transcript-download-menu' : 'audio-transcript-download-menu'}>
181
+ <d2l-menu>
182
+ <d2l-menu-item @click=${this._downloadTranscript} text="${this.localize('components:mediaPlayer:transcriptTxt')}"></d2l-menu-item>
183
+ <d2l-menu-item @click=${this._downloadCaptions} text="${this.localize('components:mediaPlayer:captionsVtt')}"></d2l-menu-item>
184
+ </d2l-menu>
185
+ </d2l-dropdown-menu>
186
+ </d2l-dropdown-button-subtle>
187
+ `;
188
+ }
189
+
190
+ _renderTranscriptViewerMenuItem() {
191
+ if (!this.transcriptViewer || this._tracks.length === 0) return nothing;
192
+ return html`
193
+ <d2l-menu-item
194
+ id="transcript-viewer-menu-item"
195
+ text="${this.transcriptViewerOn ? this.localize('components:mediaPlayer:hideTranscript') : this.localize('components:mediaPlayer:viewTranscript')}"
196
+ @d2l-menu-item-select=${this._toggleTranscriptViewer}
197
+ ></d2l-menu-item>
198
+ `;
199
+ }
200
+
201
+ _scrollTranscriptViewer() {
202
+ const cue = this.shadowRoot.getElementById('transcript-viewer-active-cue');
203
+ const cueRect = cue?.getBoundingClientRect();
204
+ const transcriptRect = this._transcriptViewer?.getBoundingClientRect();
205
+ if (cue && cueRect && transcriptRect) {
206
+ if (cueRect.bottom > transcriptRect.bottom && cueRect.height <= transcriptRect.height) {
207
+ this._transcriptViewer.scrollBy({ top: cueRect.bottom - transcriptRect.bottom + transcriptRect.height - cueRect.height, left: 0, behavior: 'smooth' });
208
+ } else if (cueRect.top < transcriptRect.top) {
209
+ this._transcriptViewer.scrollBy({ top: cueRect.top - transcriptRect.top, left: 0, behavior: 'smooth' });
210
+ }
211
+ }
212
+ }
213
+
214
+ _setTranscriptViewerOn(on) {
215
+ if (this.transcriptViewerOn === on) return;
216
+ this.transcriptViewerOn = on;
217
+ this.dispatchEvent(new CustomEvent('transcriptviewertoggle', { bubbles: true, composed: true, detail: { on } }));
218
+ }
219
+
220
+ _toggleTranscriptViewer() {
221
+ this._setTranscriptViewerOn(!this.transcriptViewerOn);
222
+ }
223
+
224
+ _updateTranscriptViewerCues() {
225
+ let cues = null;
226
+ const lang = this._getSrclangFromTrackIdentifier(this._selectedTrackIdentifier);
227
+ for (let i = 0; i < this._media.textTracks.length; i += 1) {
228
+ const currTrack = this._media.textTracks[i];
229
+ if (currTrack?.cues) {
230
+ const activeCues = currTrack.activeCues;
231
+ if (lang === currTrack.language) {
232
+ this.transcriptActiveCue = activeCues?.[activeCues?.length - 1];
233
+ cues = currTrack.cues;
234
+ break;
235
+ }
236
+ }
237
+ }
238
+ if (!cues) {
239
+ let defaultTrack;
240
+ for (let i = 0; i < this._media.textTracks.length; i++) {
241
+ if (this._media.textTracks[i].default) {
242
+ defaultTrack = this._media.textTracks[i];
243
+ break;
244
+ }
245
+ }
246
+ defaultTrack = defaultTrack || this._media.textTracks[0];
247
+ if (defaultTrack) defaultTrack.mode = 'hidden';
248
+ this._selectedTrackIdentifier = { kind: defaultTrack?.kind, srclang: defaultTrack?.language };
249
+ this.requestUpdate();
250
+ return;
251
+ }
252
+
253
+ this.beforeCaptions = [];
254
+ this.afterCaptions = [];
255
+ for (let i = 0; i < cues.length; i += 1) {
256
+ const currCue = cues[i];
257
+ const currTime = this._media?.currentTime;
258
+ const before = currCue !== this.transcriptActiveCue && (currCue.endTime < currTime || currCue.endTime <= this.transcriptActiveCue?.endTime);
259
+ if (before) {
260
+ this.beforeCaptions.push(currCue);
261
+ } else if (currCue !== this.transcriptActiveCue) {
262
+ this.afterCaptions.push(currCue);
263
+ }
264
+ }
265
+ }
266
+ };