@salesforcedevs/dx-components 1.3.245 → 1.3.246-canary.0

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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@salesforcedevs/dx-components",
3
- "version": "1.3.245",
3
+ "version": "1.3.246-canary.0",
4
4
  "description": "DX Lightning web components",
5
5
  "license": "MIT",
6
6
  "engines": {
@@ -23,6 +23,7 @@
23
23
  "lodash.defaults": "^4.2.0",
24
24
  "lodash.get": "^4.4.2",
25
25
  "lodash.kebabcase": "^4.1.1",
26
+ "memoize-one": "^6.0.0",
26
27
  "microtip": "0.2.2",
27
28
  "salesforce-oauth2": "^0.2.0",
28
29
  "throttle-debounce": "^5.0.0",
@@ -43,6 +44,5 @@
43
44
  },
44
45
  "volta": {
45
46
  "node": "16.19.1"
46
- },
47
- "gitHead": "7785e120dec8c1c204eeed4f8610603480fc5c7b"
47
+ }
48
48
  }
@@ -1,16 +1,237 @@
1
- .audio-container {
2
- width: 350px;
1
+ :host {
2
+ --dx-c-track-before-color: var(--dx-g-indigo-vibrant-30);
3
+ --dx-c-track-custom-dark-gray: rgba(62 62 62 / 100%);
4
+ --dx-c-track-custom-medium-gray: rgba(195 195 195 / 100%);
5
+ --dx-c-track-custom-light-gray: rgba(235 235 236 / 100%);
6
+ --dx-c-track-thumb-size: 10px;
7
+ --dx-c-threedot-menu-item-padding: 14px;
8
+ }
9
+
10
+ /* Outermost container/border */
11
+ .custom-audio-player {
12
+ background-color: var(--dx-g-indigo-vibrant-90, #e0e5f8);
13
+ border-radius: 12px;
14
+ padding: 6px 13px 13px;
15
+ }
16
+
17
+ .listen-icon {
18
+ padding: 0 6px 0 4px;
19
+ }
20
+
21
+ .listen-text {
22
+ display: inline-block;
23
+ font-family: var(--dx-g-font-display);
24
+ font-size: var(--dx-g-text-2xs, 11px);
25
+ font-weight: var(--dx-g-font-bold, bold);
26
+ letter-spacing: 0.6px;
27
+ position: relative;
28
+ text-transform: uppercase;
29
+ top: 1px;
30
+ }
31
+
32
+ /* The "inner" container/main part of the player, inside the outer, colored border */
33
+ /* Current design goal of this element and all other "inner" elements is to closely match native controls as they appear in Chrome */
34
+ .player {
35
+ --dx-c-popover-border-radius: 0;
36
+ --dx-c-popover-padding: 0;
37
+
38
+ align-items: center;
39
+ background-color: var(--sds-g-gray-1, #fff);
40
+ border-radius: 5px;
41
+ display: flex;
42
+ margin-top: 8px;
43
+ padding: 3px 4px;
44
+ }
45
+
46
+ /* Player button controls */
47
+ .player dx-button::part(container) {
48
+ height: 24px;
49
+ width: 24px;
50
+ }
51
+
52
+ .player-time,
53
+ .player-volume-slider,
54
+ .player-seek-slider {
55
+ position: relative;
56
+ top: 1px;
57
+ }
58
+
59
+ .player-time {
60
+ display: inline-block;
61
+ font-family: var(--dx-g-font-sans);
62
+ font-size: var(--dx-g-text-2xs, 11px);
63
+ margin-left: 4px;
64
+ margin-right: 8px;
3
65
  }
4
66
 
5
- /* TODO: we need mobile breakpoints for this, it looks bad on mobile */
6
- .audio {
7
- width: 350px;
67
+ /* Sliders and "thumb" controls */
68
+ .player-volume-slider,
69
+ .player-seek-slider {
70
+ appearance: none;
71
+ border-radius: 16px;
72
+ cursor: pointer;
73
+ height: 4px;
74
+ outline: none;
75
+ width: 100%;
76
+ }
77
+
78
+ .player-seek-slider {
79
+ background: var(--dx-c-track-custom-light-gray);
80
+ }
81
+
82
+ .player-volume-slider {
83
+ background: var(--sds-g-gray-7);
84
+ }
85
+
86
+ /* Create a large, solid "shadow" to the left of the "thumb" on the progress bar, simulating filled in space */
87
+ .player-volume-slider::-webkit-slider-thumb,
88
+ .player-seek-slider::-webkit-slider-thumb {
89
+ appearance: none;
90
+ background-color: var(--dx-g-indigo-vibrant-30);
91
+ border: none;
92
+ border-radius: 50%;
93
+ height: var(--dx-c-track-thumb-size);
94
+ opacity: 0;
95
+ transition: opacity 0.3s ease-in-out;
96
+ width: var(--dx-c-track-thumb-size);
97
+ }
98
+
99
+ .player-volume-slider::-moz-range-thumb,
100
+ .player-seek-slider::-moz-range-thumb {
101
+ background-color: var(--dx-g-indigo-vibrant-30);
102
+ border: none;
103
+ border-radius: 50%;
104
+ height: var(--dx-c-track-thumb-size);
105
+ opacity: 0;
106
+ transition: opacity 0.3s ease-in-out;
107
+ width: var(--dx-c-track-thumb-size);
108
+ }
109
+
110
+ /* NOTE: Even though the 'active' CSS rules for sliders on webkit and mozilla are the same here, they CANNOT be combined or the slider breaks */
111
+ .player-volume-slider:active::-webkit-slider-thumb,
112
+ .player-seek-slider:active::-webkit-slider-thumb {
113
+ opacity: 1;
114
+ }
115
+
116
+ .player-volume-slider:active::-moz-range-thumb,
117
+ .player-seek-slider:active::-moz-range-thumb {
118
+ opacity: 1;
119
+ }
120
+
121
+ /* Container for the volume slider, which transitions in and out on hover */
122
+ .player-volume-container {
123
+ align-items: center;
124
+ border-radius: var(--dx-g-spacing-xs);
125
+ display: flex;
126
+ padding-left: 2px;
127
+ transition: 0.2s ease-out;
128
+ width: 24px;
129
+ }
130
+
131
+ .player-volume-container .player-volume-slider {
132
+ margin: 0;
133
+ visibility: hidden;
134
+ }
135
+
136
+ .player-volume-container:hover {
137
+ background: var(--sds-g-gray-4);
138
+ width: 100px;
139
+ }
140
+
141
+ .player-volume-container:hover .player-volume-slider {
142
+ visibility: visible;
143
+ }
144
+
145
+ /* Three dot menu, for download and playback speed settings */
146
+ .player-threedot-menu ul {
147
+ font-family: var(--dx-g-font-sans);
148
+ font-size: var(--dx-g-text-2xs, 11px);
149
+ list-style: none;
150
+ margin: 0;
151
+ padding: 0;
152
+ }
153
+
154
+ .player-threedot-menu li {
155
+ cursor: pointer;
156
+ position: relative;
157
+ }
158
+
159
+ /* Speed selection items have a large left padding in Chrome, which we are mimicking */
160
+ .player-threedot-menu li.player-speed-item {
161
+ padding-left: calc(var(--dx-c-threedot-menu-item-padding) * 2);
162
+ }
163
+
164
+ .player-threedot-menu a {
165
+ color: initial;
166
+ cursor: pointer;
8
167
  display: block;
9
- margin-bottom: var(--dx-g-spacing-3xl);
10
- border: 5px solid
11
- var(
12
- --dx-c-featured-content-header-background-color,
13
- var(--dx-g-indigo-vibrant-90)
14
- );
15
- border-radius: 2em;
168
+ padding: var(--dx-c-threedot-menu-item-padding);
169
+ text-decoration: none;
170
+ }
171
+
172
+ .player-threedot-menu dx-icon {
173
+ display: inline-block;
16
174
  }
175
+
176
+ .player-threedot-menu dx-icon:not(.player-selected-speed) {
177
+ margin-right: var(--dx-c-threedot-menu-item-padding);
178
+ position: relative;
179
+ top: -1px; /* fix icon vertical position */
180
+ }
181
+
182
+ /* The selected speed has a check mark icon to the right side */
183
+ .player-threedot-menu dx-icon.player-selected-speed {
184
+ color: var(--dx-g-indigo-vibrant-40);
185
+ position: absolute;
186
+ right: 40px;
187
+ top: 50%;
188
+ transform: translateY(-50%);
189
+ }
190
+
191
+ /* All clickable elements inside of the three dot menu are buttons, except for the download link */
192
+ /* dx-button is not used here because these things barely look like buttons */
193
+ .player-threedot-menu button {
194
+ background: transparent;
195
+ border: 0;
196
+ color: initial; /* only necessary on iOS Safari to remove blue text color */
197
+ cursor: pointer;
198
+ font-family: var(--dx-g-font-sans);
199
+ font-size: var(--dx-g-text-2xs, 11px);
200
+ padding: var(--dx-c-threedot-menu-item-padding);
201
+ text-align: left;
202
+ width: 100%;
203
+ }
204
+
205
+ .audio-container {
206
+ display: none;
207
+ }
208
+
209
+ @media (hover: hover) {
210
+ .player dx-button::part(container):hover {
211
+ background: var(--sds-g-gray-4);
212
+ }
213
+
214
+ /* NOTE: Even though the 'hover' CSS rules for sliders on webkit and mozilla are the same here, they CANNOT be combined or the slider breaks */
215
+ .player-volume-slider:hover::-webkit-slider-thumb,
216
+ .player-seek-slider:hover::-webkit-slider-thumb {
217
+ opacity: 1;
218
+ }
219
+
220
+ .player-volume-slider:hover::-moz-range-thumb,
221
+ .player-seek-slider:hover::-moz-range-thumb {
222
+ opacity: 1;
223
+ }
224
+
225
+ .player-volume-container:hover {
226
+ background: var(--sds-g-gray-4);
227
+ width: 100px;
228
+ }
229
+
230
+ .player-volume-container:hover .player-volume-slider {
231
+ visibility: visible;
232
+ }
233
+
234
+ .player-threedot-menu li:hover {
235
+ background: var(--sds-g-gray-4);
236
+ }
237
+ }
@@ -1,6 +1,146 @@
1
1
  <template>
2
+ <div class="custom-audio-player">
3
+ <dx-icon
4
+ class="listen-icon"
5
+ size="xsmall"
6
+ sprite="general"
7
+ symbol="headphones"
8
+ ></dx-icon>
9
+ <span class="listen-text">Listen to this article</span>
10
+ <div class="player">
11
+ <template lwc:if={isPlaying}>
12
+ <dx-button
13
+ aria-label="Pause"
14
+ class="player-pause-button"
15
+ icon-symbol="pause"
16
+ variant="custom"
17
+ onclick={handleAudioPause}
18
+ ></dx-button>
19
+ </template>
20
+ <template lwc:else>
21
+ <dx-button
22
+ aria-label="Play"
23
+ class="player-play-button"
24
+ icon-symbol="play"
25
+ variant="custom"
26
+ onclick={handleAudioPlay}
27
+ ></dx-button>
28
+ </template>
29
+ <div class="player-time">
30
+ <span class="player-current-time">{formattedCurrentTime}</span>
31
+ &nbsp;/&nbsp;
32
+ <span class="player-duration">{formattedDuration}</span>
33
+ </div>
34
+ <input
35
+ aria-label="Current Time"
36
+ class="player-seek-slider"
37
+ type="range"
38
+ max={durationSeconds}
39
+ lwc:ref="playerSeekSlider"
40
+ value={currentTimeSeconds}
41
+ onchange={handleSeekChange}
42
+ oninput={handleSeekInput}
43
+ />
44
+ <div class="player-volume-container">
45
+ <input
46
+ aria-label="Volume Level"
47
+ class="player-volume-slider"
48
+ type="range"
49
+ max="100"
50
+ lwc:ref="playerVolumeSlider"
51
+ value={volume}
52
+ oninput={handleVolumeInput}
53
+ />
54
+ <dx-button
55
+ aria-label="Volume"
56
+ class="player-volume-button"
57
+ icon-symbol={volumeIcon}
58
+ onclick={handleVolumeClick}
59
+ variant="custom"
60
+ ></dx-button>
61
+ </div>
62
+ <dx-popover
63
+ lwc:ref="playbackSpeedPopover"
64
+ width="200px"
65
+ onclose={resetIsSettingPlaybackSpeed}
66
+ >
67
+ <dx-button
68
+ aria-label="More"
69
+ icon-symbol="threedots_vertical"
70
+ slot="control"
71
+ variant="custom"
72
+ ></dx-button>
73
+ <div class="player-threedot-menu" slot="content">
74
+ <template lwc:if={isSettingPlaybackSpeed}>
75
+ <div>
76
+ <ul>
77
+ <li>
78
+ <button
79
+ onclick={resetIsSettingPlaybackSpeed}
80
+ >
81
+ <dx-icon
82
+ size="small"
83
+ symbol="back"
84
+ ></dx-icon>
85
+ Options
86
+ </button>
87
+ </li>
88
+ <template
89
+ for:each={playbackSpeedData}
90
+ for:item="playbackSpeedDatum"
91
+ >
92
+ <li
93
+ key={playbackSpeedDatum.value}
94
+ class="player-speed-item"
95
+ >
96
+ <button onclick={handleSetPlaybackSpeed}>
97
+ {playbackSpeedDatum.value}
98
+ </button>
99
+ <dx-icon
100
+ class="player-selected-speed"
101
+ lwc:if={playbackSpeedDatum.selected}
102
+ size="small"
103
+ symbol="check"
104
+ ></dx-icon>
105
+ </li>
106
+ </template>
107
+ </ul>
108
+ </div>
109
+ </template>
110
+ <template lwc:else>
111
+ <ul>
112
+ <li>
113
+ <a
114
+ href={audioSrc}
115
+ download
116
+ target="_blank"
117
+ rel="noopener"
118
+ >
119
+ <dx-icon
120
+ size="small"
121
+ symbol="download"
122
+ ></dx-icon>
123
+ Download
124
+ </a>
125
+ </li>
126
+ <li>
127
+ <button onclick={handlePlaybackSpeedClick}>
128
+ <dx-icon
129
+ size="small"
130
+ sprite="general"
131
+ symbol="tachometer-alt"
132
+ ></dx-icon>
133
+ Playback Speed
134
+ </button>
135
+ </li>
136
+ </ul>
137
+ </template>
138
+ </div>
139
+ </dx-popover>
140
+ </div>
141
+ </div>
2
142
  <div class="audio-container">
3
- <audio class="audio" controls>
143
+ <audio lwc:ref="audioElement" class="audio" controls>
4
144
  <source src={audioSrc} type="audio/mpeg" />
5
145
  Your browser does not support the audio element.
6
146
  </audio>
@@ -1,63 +1,403 @@
1
+ import Popover from "dx/popover";
1
2
  import { track } from "dxUtils/analytics";
2
- import { LightningElement, api } from "lwc";
3
+ import { api } from "lwc";
4
+ import { LightningElementWithTypedRefs } from "dxUtilsInternal/withTypedRefs";
5
+ import memoize from "memoize-one";
3
6
 
4
- export default class Audio extends LightningElement {
7
+ type TrackColors = {
8
+ before: string;
9
+ buffer?: string;
10
+ after: string;
11
+ };
12
+ type PlaybackSpeed = (typeof formattedPlaybackSpeeds)[number];
13
+
14
+ /*
15
+ * Color settings for the "track" of the slider, which dynamically updates based on amount buffered,
16
+ * amount played, and amount remaining. The "before" color is what goes _before_ the "thumb" on the
17
+ * slider, and so on.
18
+ */
19
+ const uiConfig = {
20
+ seekSlider: {
21
+ trackColors: {
22
+ before: "var(--dx-g-indigo-vibrant-30)",
23
+ buffer: "var(--dx-c-track-custom-medium-gray)",
24
+ after: "var(--dx-c-track-custom-light-gray)"
25
+ } as TrackColors
26
+ },
27
+ volumeSlider: {
28
+ trackColors: {
29
+ before: "var(--dx-g-indigo-vibrant-30)",
30
+ after: "var(--dx-c-track-custom-medium-gray)"
31
+ } as TrackColors
32
+ }
33
+ };
34
+
35
+ // Standard playback speeds, though the "formatted" version for 1 is "Normal" (mimicking Chrome)
36
+ const formattedPlaybackSpeeds = [
37
+ "0.25",
38
+ "0.5",
39
+ "0.75",
40
+ "Normal",
41
+ "1.25",
42
+ "1.5",
43
+ "1.75",
44
+ "2"
45
+ ] as const;
46
+
47
+ export default class Audio extends LightningElementWithTypedRefs<{
48
+ audioElement: HTMLAudioElement;
49
+ playbackSpeedPopover: Popover;
50
+ playerSeekSlider: HTMLInputElement;
51
+ playerVolumeSlider: HTMLInputElement;
52
+ }> {
5
53
  @api audioSrc!: string;
6
54
  @api postName!: string;
7
55
 
8
- renderedCallback() {
9
- const audioElement = this.template.querySelector("audio");
10
- audioElement?.addEventListener(
11
- "play",
12
- this.trackPlay.bind(this),
13
- false
56
+ private _bufferedTimeRanges?: TimeRanges;
57
+ private _currentTimeSeconds = 0;
58
+ private _durationSeconds = 0;
59
+ private _volume = 100; // 100 is browser default (and max), at least in Chrome, for audio elements
60
+ private animationFrameId: number | null = null; // controls movement of the timeline when audio is playing
61
+ private currentPlaybackSpeed: PlaybackSpeed = "Normal";
62
+ private didRender = false;
63
+ private isPlaying = false;
64
+ private isSettingPlaybackSpeed = false; // popover menu has two panes; this manages that state
65
+ // `playbackSpeedData` tracks which value is selected, in a way useable by LWC for:each without requiring a separate sub-component
66
+ private playbackSpeedData = formattedPlaybackSpeeds.map((value) => ({
67
+ selected: value === "Normal",
68
+ value
69
+ }));
70
+ private prevVolume = this._volume; // previous volume is tracked so that it can be restored on unmute
71
+ private volumeIcon: "volume_high" | "volume_off" = "volume_high"; // built-in browser UI doesn't distinguish between high/low, just "on"/"off" (we use the "high" icon for "on")
72
+
73
+ private get formattedCurrentTime() {
74
+ return this.getFormatted(this.currentTimeSeconds);
75
+ }
76
+
77
+ private get formattedDuration() {
78
+ return this.getFormatted(this.durationSeconds);
79
+ }
80
+
81
+ // NOTE that values with getters and setters here have side effects that update the UI elements.
82
+ // This essentially makes these items the source of truth for the UI.
83
+ private get bufferedTimeRanges() {
84
+ return this._bufferedTimeRanges;
85
+ }
86
+ private set bufferedTimeRanges(value: TimeRanges | undefined) {
87
+ this._bufferedTimeRanges = value;
88
+ this.updateRangeInputStyles(
89
+ this.refs.playerSeekSlider,
90
+ this.currentTimeSeconds
91
+ );
92
+ }
93
+
94
+ private get currentTimeSeconds() {
95
+ return this._currentTimeSeconds;
96
+ }
97
+ private set currentTimeSeconds(value: number) {
98
+ this._currentTimeSeconds = value;
99
+ this.updateRangeInputStyles(this.refs.playerSeekSlider, value);
100
+ }
101
+
102
+ private get durationSeconds() {
103
+ return this._durationSeconds;
104
+ }
105
+ private set durationSeconds(value: number) {
106
+ this._durationSeconds = value;
107
+ this.updateRangeInputStyles(
108
+ this.refs.playerSeekSlider,
109
+ this.currentTimeSeconds
14
110
  );
15
- audioElement?.addEventListener(
111
+ }
112
+
113
+ private get volume() {
114
+ return this._volume;
115
+ }
116
+ private set volume(value: number) {
117
+ this.prevVolume = this.volume;
118
+ this._volume = value;
119
+ this.updateRangeInputStyles(this.refs.playerVolumeSlider, value);
120
+ }
121
+
122
+ renderedCallback(): void {
123
+ if (this.didRender) {
124
+ return;
125
+ }
126
+
127
+ this.didRender = true;
128
+ this.volume = this.refs.audioElement.volume * 100;
129
+
130
+ // We only need to listen for the metadata event if the metadata isn't already loaded;
131
+ // sometimes, browsers can load it _before_ the event handler can even be added.
132
+ if (this.refs.audioElement.readyState > 0) {
133
+ this.handleAudioLoadedMetadata();
134
+ } else {
135
+ this.refs.audioElement.addEventListener(
136
+ "loadedmetadata",
137
+ this.handleAudioLoadedMetadata
138
+ );
139
+ }
140
+
141
+ // "progress" handles buffering of audio data, lets us update the UI for the buffered amount
142
+ this.refs.audioElement.addEventListener(
143
+ "progress",
144
+ this.handleAudioProgress
145
+ );
146
+ this.refs.audioElement.addEventListener("ended", this.handleAudioEnded);
147
+ }
148
+
149
+ disconnectedCallback(): void {
150
+ this.refs.audioElement.removeEventListener(
151
+ "loadedmetadata",
152
+ this.handleAudioLoadedMetadata
153
+ );
154
+ this.refs.audioElement.removeEventListener(
155
+ "progress",
156
+ this.handleAudioProgress
157
+ );
158
+ this.refs.audioElement.removeEventListener(
16
159
  "ended",
17
- this.trackEnded.bind(this),
18
- false
160
+ this.handleAudioEnded
19
161
  );
20
- audioElement?.addEventListener(
21
- "pause",
22
- this.trackPause.bind(this),
23
- false
162
+ }
163
+
164
+ /* BEGIN event handlers */
165
+
166
+ handleAudioLoadedMetadata = () => {
167
+ // `loadedmetadata` is the first spot at which duration is available.
168
+ this.durationSeconds = this.refs.audioElement.duration;
169
+ };
170
+
171
+ handleAudioProgress = () => {
172
+ // New buffered amount received, store the buffered time ranges.
173
+ this.bufferedTimeRanges = this.refs.audioElement.buffered;
174
+ };
175
+
176
+ handleAudioPlay = (event: Event) => {
177
+ this.refs.audioElement.play();
178
+ this.syncTimeWithAudio();
179
+ this.isPlaying = true;
180
+ this.trackPlay(event);
181
+ };
182
+
183
+ handleAudioPause = (event: Event) => {
184
+ this.refs.audioElement.pause();
185
+ cancelAnimationFrame(this.animationFrameId as number);
186
+ this.isPlaying = false;
187
+ this.trackPause(event);
188
+ };
189
+
190
+ handleAudioEnded = (event: Event) => {
191
+ cancelAnimationFrame(this.animationFrameId as number);
192
+ this.isPlaying = false;
193
+ this.trackEnded(event);
194
+ };
195
+
196
+ // Handles any user-driven movement on the "seek" slider (timeline)
197
+ handleSeekInput = () => {
198
+ if (!this.durationSeconds) {
199
+ // No moving the "thumb" when metadata hasn't even loaded.
200
+ return;
201
+ }
202
+
203
+ if (this.isPlaying) {
204
+ // To allow the user to interact with the slider, we temporarily have to stop moving the
205
+ // "thumb," but leave the player in the "isPlaying" state so that the play button
206
+ // remains and audio picks back up once the user is done interacting (the last bit is
207
+ // handled in the `handleSeekChange` handler, when the user actually commits the change)
208
+ cancelAnimationFrame(this.animationFrameId as number);
209
+ }
210
+
211
+ this.currentTimeSeconds = parseInt(
212
+ this.refs.playerSeekSlider.value,
213
+ 10
214
+ );
215
+ };
216
+
217
+ // `change` is only fired once the user *commits* to a value (e.g., by releasing the mouse click), unlike `input` above
218
+ handleSeekChange = () => {
219
+ if (this.isPlaying) {
220
+ // Resume playing after user finishes interacting with seek slider, if the audio was
221
+ // already playing (see note in `handleSeekInput`)
222
+ this.syncTimeWithAudio();
223
+ }
224
+
225
+ this.currentTimeSeconds = parseInt(
226
+ this.refs.playerSeekSlider.value,
227
+ 10
228
+ );
229
+ this.refs.audioElement!.currentTime = this.currentTimeSeconds;
230
+ };
231
+
232
+ // Handles any user-driven movement on the volume slider
233
+ handleVolumeInput = ({
234
+ currentTarget
235
+ }: InputEvent & { currentTarget: HTMLInputElement }) => {
236
+ this.volume = parseInt(currentTarget.value, 10);
237
+ this.refs.audioElement.volume = this.volume / 100;
238
+ };
239
+
240
+ // Direct click on the volume button is mute/unmute
241
+ handleVolumeClick = () => {
242
+ if (this.refs.audioElement.muted) {
243
+ this.refs.audioElement.muted = false;
244
+ this.volumeIcon = "volume_high";
245
+ this.volume = this.prevVolume; // restore to previous volume level
246
+ } else {
247
+ this.refs.audioElement.muted = true;
248
+ this.volumeIcon = "volume_off";
249
+ this.volume = 0;
250
+ }
251
+ };
252
+
253
+ // Set "pane" state for playback popover menu
254
+ handlePlaybackSpeedClick = () => {
255
+ this.isSettingPlaybackSpeed = true;
256
+ };
257
+
258
+ // Handle selection of a playback speed in the "three dot" menu
259
+ handleSetPlaybackSpeed = (event: Event) => {
260
+ const currentTarget = event.currentTarget as Node;
261
+ const selectedPlaybackSpeed =
262
+ currentTarget.textContent as PlaybackSpeed;
263
+
264
+ if (selectedPlaybackSpeed === this.currentPlaybackSpeed) {
265
+ return;
266
+ }
267
+
268
+ this.playbackSpeedData = formattedPlaybackSpeeds.map((value) => ({
269
+ selected: value === selectedPlaybackSpeed,
270
+ value
271
+ }));
272
+ this.currentPlaybackSpeed = selectedPlaybackSpeed;
273
+ this.refs.playbackSpeedPopover.closePopover();
274
+ this.resetIsSettingPlaybackSpeed();
275
+ this.refs.audioElement.playbackRate =
276
+ selectedPlaybackSpeed === "Normal"
277
+ ? 1
278
+ : parseFloat(selectedPlaybackSpeed);
279
+ };
280
+
281
+ // Sets "pane" state for popover back to the default
282
+ resetIsSettingPlaybackSpeed = () => {
283
+ this.isSettingPlaybackSpeed = false;
284
+ };
285
+
286
+ /* END event handlers, BEGIN private utility methods */
287
+
288
+ // Animates the seek slider (timeline) each frame so that it is N*Sync with current play
289
+ // time. We do this using requestAnimationFrame so that we can temporarily cancel the
290
+ // animating whenever the user is interacting with the slider.
291
+ private syncTimeWithAudio = () => {
292
+ this.currentTimeSeconds = Math.floor(
293
+ this.refs.audioElement.currentTime
24
294
  );
295
+ this.animationFrameId = requestAnimationFrame(this.syncTimeWithAudio);
296
+ };
297
+
298
+ // Convert raw numerical seconds to strings formatted as "X:XX"
299
+ private getFormatted(totalSeconds: number) {
300
+ const minutes = Math.floor(totalSeconds / 60);
301
+ const seconds = Math.floor(totalSeconds % 60);
302
+ const doubleDigitSeconds = seconds < 10 ? `0${seconds}` : `${seconds}`;
303
+ return `${minutes}:${doubleDigitSeconds}`;
25
304
  }
26
305
 
27
- private trackClick(e: Event, eventType: string, action: string) {
28
- const audioElement = this.template.querySelector(
29
- "audio"
30
- ) as HTMLMediaElement;
306
+ // Find the end of the buffered amount of audio, if any, that overlaps with the current
307
+ // position. We care about the overlap because buffered time ranges can have gaps.
308
+ // Memoized because it is called whenever input styles are updated, and bufferedTimeRanges
309
+ // rarely changes.
310
+ private getOverlappingBufferEnd = memoize(
311
+ (currentTimeSeconds: number, bufferedTimeRanges?: TimeRanges) => {
312
+ let nearestBufferEnd = 0;
313
+
314
+ if (bufferedTimeRanges?.length) {
315
+ for (let i = 0; i < bufferedTimeRanges.length; i++) {
316
+ if (
317
+ bufferedTimeRanges.start(i) <= currentTimeSeconds &&
318
+ bufferedTimeRanges.end(i) > currentTimeSeconds
319
+ ) {
320
+ nearestBufferEnd = bufferedTimeRanges.end(i);
321
+ break;
322
+ }
323
+ }
324
+ }
325
+
326
+ return nearestBufferEnd;
327
+ }
328
+ );
329
+
330
+ // Visually updates sliders (timeline or volume) so that the amount "covered" (before the
331
+ // "thumb") is visually different than the amount remaining; also handles visually showing the
332
+ // buffered amount if there is buffer data and the element is the "seek"/timeline slider
333
+ private updateRangeInputStyles(
334
+ target: HTMLInputElement | null,
335
+ currentValue: number
336
+ ) {
337
+ if (
338
+ !target ||
339
+ (target === this.refs.playerSeekSlider && !this.durationSeconds)
340
+ ) {
341
+ return;
342
+ }
343
+
344
+ const maximumValue =
345
+ target === this.refs.playerVolumeSlider
346
+ ? 100
347
+ : this.durationSeconds;
348
+ const trackColors =
349
+ target === this.refs.playerVolumeSlider
350
+ ? uiConfig.volumeSlider.trackColors
351
+ : uiConfig.seekSlider.trackColors;
352
+ const percentageCovered =
353
+ maximumValue === 100
354
+ ? currentValue
355
+ : (currentValue / maximumValue) * 100;
356
+ const overlappingBufferEnd = this.getOverlappingBufferEnd(
357
+ this.currentTimeSeconds,
358
+ this.bufferedTimeRanges
359
+ );
31
360
 
361
+ if (trackColors.buffer && overlappingBufferEnd) {
362
+ // Slider with three colors: the before amount, the buffered amount, and the full
363
+ // "remaining" slider color.
364
+ const bufferPercentage =
365
+ (overlappingBufferEnd / maximumValue) * 100;
366
+ target.style.background = `linear-gradient(to right, ${trackColors.before} ${percentageCovered}%, ${trackColors.buffer} ${percentageCovered}% ${bufferPercentage}%, ${trackColors.after} ${bufferPercentage}%)`;
367
+ } else {
368
+ // "Basic" slider, with only two colors, no buffering.
369
+ target.style.background = `linear-gradient(to right, ${trackColors.before} ${percentageCovered}%, ${trackColors.after} ${percentageCovered}%)`;
370
+ }
371
+ }
372
+
373
+ private trackClick = (e: Event, eventType: string, action: string) => {
32
374
  const payload = {
33
375
  event: eventType,
34
376
  media_action: action,
35
377
  media_name: this.postName,
36
- media_percentage_played: (
37
- (audioElement!.currentTime * 100) /
38
- audioElement!.duration
39
- ).toFixed(0),
40
- media_seconds_played: audioElement!.currentTime,
378
+ media_percentage_played:
379
+ this.durationSeconds === 0
380
+ ? this.durationSeconds
381
+ : (
382
+ (this.currentTimeSeconds * 100) /
383
+ this.durationSeconds
384
+ ).toFixed(0),
385
+ media_seconds_played: this.currentTimeSeconds,
41
386
  media_type: "blog audio"
42
387
  };
43
388
 
44
389
  track(e.currentTarget!, "custEv_blogAudioPlay", payload);
45
- }
390
+ };
46
391
 
47
- private trackPlay(e: Event) {
392
+ private trackPlay = (e: Event) => {
48
393
  this.trackClick(e, "custEv_blogAudioPlay", "play");
49
- }
50
- private trackEnded(e: Event) {
394
+ };
395
+
396
+ private trackEnded = (e: Event) => {
51
397
  this.trackClick(e, "custEv_blogAudioComplete", "complete");
52
- }
53
- private trackPause(e: Event) {
54
- const audioElement = this.template.querySelector(
55
- "audio"
56
- ) as HTMLMediaElement;
398
+ };
57
399
 
58
- //suppress 'pause' events that happen in the last 99% of the duration
59
- if ((audioElement!.currentTime * 99) / audioElement!.duration < 99) {
60
- this.trackClick(e, "custEv_blogAudioPause", "pause");
61
- }
62
- }
400
+ private trackPause = (e: Event) => {
401
+ this.trackClick(e, "custEv_blogAudioPause", "pause");
402
+ };
63
403
  }
@@ -6,7 +6,7 @@
6
6
  var(--dx-g-spacing-sm)
7
7
  );
8
8
  --popover-border: var(--dx-c-popover-border, 1px solid var(--dx-g-gray-90));
9
- --popover-padding: var(--dx-g-spacing-sm);
9
+ --popover-padding: var(--dx-c-popover-padding, --dx-g-spacing-sm);
10
10
  }
11
11
 
12
12
  .popover-container {
package/LICENSE DELETED
@@ -1,12 +0,0 @@
1
- Copyright (c) 2020, Salesforce.com, Inc.
2
- All rights reserved.
3
-
4
- Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
5
-
6
- * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
7
-
8
- * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
9
-
10
- * Neither the name of Salesforce.com nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
11
-
12
- THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.