@salesforcedevs/dx-components 1.3.246-canary.1 → 1.3.246

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/LICENSE ADDED
@@ -0,0 +1,12 @@
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.
package/lwc.config.json CHANGED
@@ -121,7 +121,6 @@
121
121
  "dxUtils/normalizers",
122
122
  "dxUtils/queryCoordinator",
123
123
  "dxUtils/recentSearches",
124
- "dxUtils/withTypedRefs",
125
124
  "dxUtils/wordpress"
126
125
  ]
127
126
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@salesforcedevs/dx-components",
3
- "version": "1.3.246-canary.1",
3
+ "version": "1.3.246",
4
4
  "description": "DX Lightning web components",
5
5
  "license": "MIT",
6
6
  "engines": {
@@ -23,7 +23,6 @@
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",
27
26
  "microtip": "0.2.2",
28
27
  "salesforce-oauth2": "^0.2.0",
29
28
  "throttle-debounce": "^5.0.0",
@@ -44,5 +43,6 @@
44
43
  },
45
44
  "volta": {
46
45
  "node": "16.19.1"
47
- }
46
+ },
47
+ "gitHead": "150f3fc547df2a63263f27b35036a4c836fb2aba"
48
48
  }
@@ -1,237 +1,16 @@
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;
65
- }
66
-
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);
1
+ .audio-container {
2
+ width: 350px;
162
3
  }
163
4
 
164
- .player-threedot-menu a {
165
- color: initial;
166
- cursor: pointer;
5
+ /* TODO: we need mobile breakpoints for this, it looks bad on mobile */
6
+ .audio {
7
+ width: 350px;
167
8
  display: block;
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;
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;
174
16
  }
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,146 +1,6 @@
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>
142
2
  <div class="audio-container">
143
- <audio lwc:ref="audioElement" class="audio" controls>
3
+ <audio class="audio" controls>
144
4
  <source src={audioSrc} type="audio/mpeg" />
145
5
  Your browser does not support the audio element.
146
6
  </audio>
@@ -1,403 +1,63 @@
1
- import Popover from "dx/popover";
2
1
  import { track } from "dxUtils/analytics";
3
- import { api } from "lwc";
4
- import { LightningElementWithTypedRefs } from "dxUtils/withTypedRefs";
5
- import memoize from "memoize-one";
2
+ import { LightningElement, api } from "lwc";
6
3
 
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
- }> {
4
+ export default class Audio extends LightningElement {
53
5
  @api audioSrc!: string;
54
6
  @api postName!: string;
55
7
 
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
8
+ renderedCallback() {
9
+ const audioElement = this.template.querySelector("audio");
10
+ audioElement?.addEventListener(
11
+ "play",
12
+ this.trackPlay.bind(this),
13
+ false
110
14
  );
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(
15
+ audioElement?.addEventListener(
159
16
  "ended",
160
- this.handleAudioEnded
17
+ this.trackEnded.bind(this),
18
+ false
161
19
  );
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
20
+ audioElement?.addEventListener(
21
+ "pause",
22
+ this.trackPause.bind(this),
23
+ false
294
24
  );
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}`;
304
25
  }
305
26
 
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
- );
27
+ private trackClick(e: Event, eventType: string, action: string) {
28
+ const audioElement = this.template.querySelector(
29
+ "audio"
30
+ ) as HTMLMediaElement;
360
31
 
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) => {
374
32
  const payload = {
375
33
  event: eventType,
376
34
  media_action: action,
377
35
  media_name: this.postName,
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,
36
+ media_percentage_played: (
37
+ (audioElement!.currentTime * 100) /
38
+ audioElement!.duration
39
+ ).toFixed(0),
40
+ media_seconds_played: audioElement!.currentTime,
386
41
  media_type: "blog audio"
387
42
  };
388
43
 
389
44
  track(e.currentTarget!, "custEv_blogAudioPlay", payload);
390
- };
45
+ }
391
46
 
392
- private trackPlay = (e: Event) => {
47
+ private trackPlay(e: Event) {
393
48
  this.trackClick(e, "custEv_blogAudioPlay", "play");
394
- };
395
-
396
- private trackEnded = (e: Event) => {
49
+ }
50
+ private trackEnded(e: Event) {
397
51
  this.trackClick(e, "custEv_blogAudioComplete", "complete");
398
- };
52
+ }
53
+ private trackPause(e: Event) {
54
+ const audioElement = this.template.querySelector(
55
+ "audio"
56
+ ) as HTMLMediaElement;
399
57
 
400
- private trackPause = (e: Event) => {
401
- this.trackClick(e, "custEv_blogAudioPause", "pause");
402
- };
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
+ }
403
63
  }
@@ -14,6 +14,7 @@ export default class CardBlogPost extends LightningElement {
14
14
  @api title!: string;
15
15
  @api authors?: Array<any> | null = null;
16
16
  @api origin?: string = "wordpress";
17
+ @api clickEvent?: () => void;
17
18
 
18
19
  // LWC is being compiled so that datetime is being interpreted as date-time
19
20
  // ONLY from from the implementation in dx-card-blog-post-provider
@@ -22,6 +23,10 @@ export default class CardBlogPost extends LightningElement {
22
23
  }
23
24
 
24
25
  private onLinkClick(event: Event) {
26
+ if (this.clickEvent) {
27
+ this.clickEvent();
28
+ }
29
+
25
30
  const payload = {
26
31
  click_text: this.title,
27
32
  click_url: `${window.location.origin}${this.href}`,
@@ -23,6 +23,7 @@
23
23
  key={recommendation.id}
24
24
  title={recommendation.yoast_head_json.title}
25
25
  origin="coveo"
26
+ click-event={recommendation.clickEvent}
26
27
  ></dx-card-blog-post>
27
28
  </template>
28
29
  </dx-grid>
@@ -1,11 +1,25 @@
1
1
  import { LightningElement, api } from "lwc";
2
2
 
3
3
  const SEARCH_HUB = "developerWebsiteBlogs";
4
+
5
+ interface Recommendation {
6
+ originalCoveoData: {
7
+ uri: string;
8
+ title: string;
9
+ searchUid: string;
10
+ raw: {
11
+ permanentId: string;
12
+ };
13
+ "@source": string;
14
+ };
15
+ clickEvent: () => void;
16
+ }
17
+
4
18
  export default class CoveoRecommendations extends LightningElement {
5
19
  @api coveoAuthToken!: string;
6
20
  @api coveoOrganizationId!: string;
7
21
 
8
- _recommendations = [] as any[];
22
+ _recommendations = [] as Recommendation[];
9
23
 
10
24
  private get recommendations(): any[] {
11
25
  return this._recommendations || [];
@@ -46,10 +60,17 @@ export default class CoveoRecommendations extends LightningElement {
46
60
 
47
61
  const blogDataLoadTasks = results
48
62
  .slice(0, 3)
49
- .map(async (rec: any) => {
63
+ .map(async (rec: any, index: number) => {
50
64
  const slug = rec.uri.split("/").pop();
51
65
  const blogDataUrl = `https://developer.salesforce.com/blogs/wp-json/wp/v2/posts?slug=${slug}&state=publish&_embed=wp:featuredmedia`;
52
- return (await (await fetch(blogDataUrl)).json())[0];
66
+ const wordPressData = (
67
+ await (await fetch(blogDataUrl)).json()
68
+ )[0];
69
+ wordPressData.originalCoveoData = rec;
70
+ wordPressData.originalCoveoData.searchUid =
71
+ json.searchUid;
72
+ wordPressData.clickEvent =
73
+ this.buildClickAnalyticsLogger(index);
53
74
  });
54
75
  this._recommendations = await Promise.all(
55
76
  blogDataLoadTasks
@@ -92,4 +113,38 @@ export default class CoveoRecommendations extends LightningElement {
92
113
  }
93
114
  );
94
115
  };
116
+
117
+ buildClickAnalyticsLogger = (index: number) => () => {
118
+ const payload = {
119
+ anonymous: true,
120
+ documentPosition: index,
121
+ documentTitle: this._recommendations[index].originalCoveoData.title,
122
+ documentUrl: this._recommendations[index].originalCoveoData.uri,
123
+ language: "en",
124
+ originLevel1: SEARCH_HUB,
125
+ originLevel2: SEARCH_HUB,
126
+ actionCause: "recommendationOpen",
127
+ queryText: "", // This has to be included, but is empty, because recommendations use the 'search' endpoint with an empty query string
128
+ searchQueryUid: this._recommendations[index].originalCoveoData.uri,
129
+ sourceName:
130
+ this._recommendations[index].originalCoveoData["@source"],
131
+ customData: {
132
+ contentIDKey: "permanentId",
133
+ contentIDValue:
134
+ this._recommendations[index].originalCoveoData.raw
135
+ .permanentId
136
+ }
137
+ };
138
+ fetch(
139
+ `https://${this.coveoOrganizationId}.analytics.org.coveo.com/rest/ua/v15/analytics/click`,
140
+ {
141
+ headers: {
142
+ Authorization: `Bearer ${this.coveoAuthToken}`,
143
+ "Content-Type": "application/json"
144
+ },
145
+ method: "POST",
146
+ body: JSON.stringify(payload)
147
+ }
148
+ );
149
+ };
95
150
  }
@@ -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-c-popover-padding, --dx-g-spacing-sm);
9
+ --popover-padding: var(--dx-g-spacing-sm);
10
10
  }
11
11
 
12
12
  .popover-container {
@@ -1,20 +0,0 @@
1
- import { LightningElement } from "lwc";
2
-
3
- // mixin version for more flexibility
4
- export const withTypedRefs = <
5
- RefsType extends LightningElement["refs"] = LightningElement["refs"],
6
- BaseClass extends new (...args: any[]) => any = new (
7
- ...args: any[]
8
- ) => LightningElement
9
- >(
10
- BaseClass: BaseClass
11
- ) => {
12
- return class extends BaseClass {
13
- declare readonly refs: RefsType;
14
- };
15
- };
16
-
17
- // class version if you just want a lightning element with typed refs and nothing else
18
- export class LightningElementWithTypedRefs<RefsType extends LightningElement["refs"]> extends LightningElement {
19
- declare readonly refs: RefsType;
20
- }