@salesforcedevs/dx-components 1.3.217 → 1.3.219

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.217",
3
+ "version": "1.3.219",
4
4
  "description": "DX Lightning web components",
5
5
  "license": "MIT",
6
6
  "engines": {
@@ -44,5 +44,5 @@
44
44
  "volta": {
45
45
  "node": "16.19.1"
46
46
  },
47
- "gitHead": "97fa28da77d1550d42ebc27eb8878f50fdcac89f"
47
+ "gitHead": "c0f8f19b7bfc14f9a6ca02a2dc328f726d72270a"
48
48
  }
@@ -164,12 +164,13 @@ export default class ScrollManager extends LightningElement {
164
164
  }
165
165
 
166
166
  saveScroll = throttle(100, () => {
167
+ const scrollingElement = document.scrollingElement || document.body;
167
168
  window.history.replaceState(
168
169
  {
169
170
  ...window.history.state,
170
171
  scroll: {
171
- value: document.body.scrollTop,
172
- docSize: document.body.scrollHeight
172
+ value: scrollingElement.scrollTop,
173
+ docSize: scrollingElement.scrollHeight
173
174
  }
174
175
  },
175
176
  "",
@@ -422,11 +422,17 @@ a.CoveoResultLink,
422
422
  width: fit-content;
423
423
  }
424
424
 
425
- .dx-badge {
426
- display: block;
425
+ .dx-result-info {
426
+ display: flex;
427
+ gap: 12px;
427
428
  margin-bottom: var(--dx-g-spacing-smd);
428
429
  }
429
430
 
431
+ .breadcrumb {
432
+ color: #555;
433
+ font-size: var(--dx-g-text-xs);
434
+ }
435
+
430
436
  .no-results {
431
437
  display: flex;
432
438
  justify-content: center;
@@ -6,6 +6,7 @@ import {
6
6
  CONTENT_TYPE_ICONS
7
7
  } from "dxConstants/contentTypes";
8
8
  import { getContentTypeColorVariables } from "dxUtils/contentTypes";
9
+ import { pollUntil } from "dxUtils/async";
9
10
 
10
11
  interface CoveoSearch {
11
12
  state: typeof CoveoSDK.state;
@@ -40,16 +41,27 @@ const resultsTemplatesInnerHtml = `
40
41
  <script
41
42
  id="myDocumentResultTemplate"
42
43
  class="result-template"
43
- type="text/html"
44
+ type="text/underscore"
44
45
  data-field-publicurl=""
45
46
  >
46
47
  <div class="dx-result">
47
- <span class="CoveoFieldValue" data-field="@content_type" data-helper="badge" data-html-value="true"></span>
48
+ <div class="dx-result-info">
49
+ <span class="CoveoFieldValue" data-field="@content_type" data-helper="badge" data-html-value="true"></span>
50
+ <% if (!raw.breadcrumbs && !raw.metabreadcrumbs) { %>
51
+ <span class="CoveoFieldValue" data-field="@uri" data-helper="uriBreadcrumbs" data-html-value="true"></span>
52
+ <% } else { %>
53
+ <% if (raw.uri.includes('/references/')) { %>
54
+ <span class="CoveoFieldValue" data-field="@metabreadcrumbs" data-helper="metabreadcrumbs" data-html-value="true"></span>
55
+ <% } else { %>
56
+ <span class="CoveoFieldValue" data-field="@breadcrumbs" data-helper="breadcrumbs" data-html-value="true"></span>
57
+ <% } %>
58
+ <% } %>
59
+ </div>
48
60
  <p class="dx-result-title">
49
- <a
61
+ <span
50
62
  class="CoveoResultLink"
51
- data-field="@publicurl"
52
- ></a>
63
+ data-field="@uri"
64
+ ></span>
53
65
  </p>
54
66
  <p class="dx-result-excerpt CoveoExcerpt"></p>
55
67
  </div>
@@ -57,10 +69,21 @@ const resultsTemplatesInnerHtml = `
57
69
  <script
58
70
  id="myDefaultResultTemplate"
59
71
  class="result-template"
60
- type="text/html"
72
+ type="text/underscore"
61
73
  >
62
74
  <div class="dx-result">
63
- <span class="CoveoFieldValue" data-field="@content_type" data-helper="badge" data-html-value="true"></span>
75
+ <div class="dx-result-info">
76
+ <span class="CoveoFieldValue" data-field="@content_type" data-helper="badge" data-html-value="true"></span>
77
+ <% if (!raw.breadcrumbs && !raw.metabreadcrumbs) { %>
78
+ <span class="CoveoFieldValue" data-field="@uri" data-helper="uriBreadcrumbs" data-html-value="true"></span>
79
+ <% } else { %>
80
+ <% if (raw.uri.includes('/references/')) { %>
81
+ <span class="CoveoFieldValue" data-field="@metabreadcrumbs" data-helper="metabreadcrumbs" data-html-value="true"></span>
82
+ <% } else { %>
83
+ <span class="CoveoFieldValue" data-field="@breadcrumbs" data-helper="breadcrumbs" data-html-value="true"></span>
84
+ <% } %>
85
+ <% } %>
86
+ </div>
64
87
  <p class="dx-result-title">
65
88
  <a class="CoveoResultLink"></a>
66
89
  </p>
@@ -69,6 +92,14 @@ const resultsTemplatesInnerHtml = `
69
92
  </script>
70
93
  `;
71
94
 
95
+ const isInternalDomain = (domain: string) =>
96
+ domain === "developer.salesforce.com" ||
97
+ domain === "developer-website-s.herokuapp.com";
98
+
99
+ const isTrailheadDomain = (domain: string) =>
100
+ domain === "trailhead.salesforce.com" ||
101
+ domain === "dev.trailhead.salesforce.com";
102
+
72
103
  const buildTemplateHelperBadge = (value: keyof typeof CONTENT_TYPE_LABELS) => {
73
104
  const style = getContentTypeColorVariables(value);
74
105
  const label = CONTENT_TYPE_LABELS[value];
@@ -88,6 +119,107 @@ const buildTemplateHelperBadge = (value: keyof typeof CONTENT_TYPE_LABELS) => {
88
119
  `;
89
120
  };
90
121
 
122
+ const processParts = (parts: string[], internalFlag = false) => {
123
+ // filter /docs/ breadcrumb item from internal domains
124
+ const filterFn = internalFlag
125
+ ? (part: string) => part !== "docs"
126
+ : (part: string) => part;
127
+
128
+ return parts.filter(filterFn).map((part) => {
129
+ // Remove special characters & .htm/.xml extension
130
+ part = part
131
+ .replace(/_/g, " ")
132
+ .replace(/-/g, " ")
133
+ .replace(/.html*/g, " ")
134
+ .replace(/.xml/g, " ")
135
+ .replace(/b2c/g, "B2C");
136
+
137
+ // Capitalize first letter of each word
138
+ part = part.replace(/\w\S*/g, (w) => {
139
+ return w.replace(/^\w/, (c) => c.toUpperCase());
140
+ });
141
+
142
+ return `<span class="breadcrumb-item">${decodeURI(part)}</span>`;
143
+ });
144
+ };
145
+
146
+ const buildTemplateHelperUriBreadcrumbs = (value: string) => {
147
+ const url = new URL(value);
148
+
149
+ // exclude youtube links from breadcrumbs
150
+ const hostnamePattern = /^((www\.)?(youtube\.com|youtu\.be))$/;
151
+
152
+ // we don't want to show atlas docs because the url structure is mad ugly
153
+ if (hostnamePattern.test(url.hostname) || url.pathname.includes("atlas.")) {
154
+ return "";
155
+ }
156
+
157
+ let parts = url.pathname.split("/").filter((part) => part !== "");
158
+
159
+ // Remove language prefix from trailhead URLs
160
+ if (isTrailheadDomain(url.hostname)) {
161
+ parts = parts
162
+ .slice(1)
163
+ .filter((part) => part !== "content" && part !== "learn");
164
+ }
165
+
166
+ const breadcrumbs = processParts(parts, isInternalDomain(url.hostname));
167
+
168
+ if (!isInternalDomain(url.hostname)) {
169
+ // Remove the first breadcrumb item if it's an internal domain (i.e drop developer.salesforce.com from developer.salesforce.com / B2C Commerce / Open Commerce API / Filtering)
170
+ breadcrumbs.unshift(
171
+ `<span class="breadcrumb-item">${url.hostname}</span>`
172
+ );
173
+
174
+ return `
175
+ <span class="breadcrumb">
176
+ ${breadcrumbs.join(" / ")}
177
+ </span>
178
+ `;
179
+ } else if (breadcrumbs.length === 1) {
180
+ // Hide breadcrumbs if there is only one breadcrumb item
181
+ return "";
182
+ }
183
+
184
+ // remove the last breadcrumb item (the search result title makes it redundant)
185
+ breadcrumbs.pop();
186
+
187
+ return `<span class="breadcrumb">/ ${breadcrumbs.join(" / ")} /</span>`;
188
+ };
189
+
190
+ const buildTemplateHelperBreadcrumbs = (value: string) => {
191
+ const parts = value.split("/").filter((part) => part !== "");
192
+
193
+ // Don't show breadcrumbs if there's only one part
194
+ if (parts.length === 1) {
195
+ return "";
196
+ }
197
+
198
+ const breadcrumbs = processParts(parts);
199
+
200
+ // remove last breadcrumb item
201
+ breadcrumbs.pop();
202
+
203
+ return `
204
+ <span class="breadcrumb">/ ${breadcrumbs.join(" / ")} /</span>
205
+ `;
206
+ };
207
+
208
+ const buildTemplateHelperMetaBreadcrumbs = (value: string) => {
209
+ const parts = value.split("/").filter((part) => part !== "");
210
+
211
+ // Don't show breadcrumbs if there's only one part
212
+ if (parts.length === 1) {
213
+ return "";
214
+ }
215
+
216
+ const breadcrumbs = processParts(parts);
217
+
218
+ return `
219
+ <span class="breadcrumb">/ ${breadcrumbs.join(" / ")}</span>
220
+ `;
221
+ };
222
+
91
223
  // @ts-ignore Dark Magic (TM) we are overriding the 'title' field with a custom getter. We should really stop doing this.
92
224
  export default class SearchResults extends LightningElement {
93
225
  @api coveoOrganizationId!: string;
@@ -139,9 +271,12 @@ export default class SearchResults extends LightningElement {
139
271
  BreadcrumbManager.clearBreadcrumbs();
140
272
  }
141
273
 
142
- private currentPage: number = 1;
274
+ private currentPage: number = 25;
143
275
  private totalPages: number = 1;
144
276
 
277
+ private originalBreadcrumbs: string[] = [];
278
+ private initialWindowWidth = window.innerWidth;
279
+
145
280
  private goToPage(e: CustomEvent) {
146
281
  const page = e.detail;
147
282
  const Pager = Coveo.get(
@@ -179,6 +314,7 @@ export default class SearchResults extends LightningElement {
179
314
  if (Coveo.state(this.root!, "q") === "") {
180
315
  Coveo.state(this.root!, "sort", "date descending");
181
316
  }
317
+
182
318
  this.isInitialized = true;
183
319
  }
184
320
  );
@@ -204,6 +340,110 @@ export default class SearchResults extends LightningElement {
204
340
 
205
341
  this.trackSearchResults(event, this.query, this.totalResults);
206
342
  });
343
+
344
+ Coveo.$$(root).on(Coveo.QueryEvents.deferredQuerySuccess, async () => {
345
+ // wait specified time to ensure breadcrumbs are rendered before processing them
346
+ await pollUntil(
347
+ () => {
348
+ const coveoResults =
349
+ this.root!.querySelector(".CoveoResult");
350
+
351
+ return Boolean(coveoResults);
352
+ },
353
+ 20,
354
+ 1000
355
+ );
356
+
357
+ this.processBreadcrumbs(this.root!);
358
+
359
+ window.onresize = () => this.processBreadcrumbs(root);
360
+ });
361
+ }
362
+
363
+ // Checks if text is wrapping by comparing it with an element's text that doesn't wrap
364
+ private isTextWrapping = (
365
+ elementOne: HTMLElement,
366
+ elementTwo: HTMLElement
367
+ ) => elementOne.offsetHeight > elementTwo.offsetHeight;
368
+
369
+ private truncateBreadcrumbText = (breadcrumbItems: HTMLElement[]) => {
370
+ breadcrumbItems.forEach((breadcrumbItem: HTMLElement) => {
371
+ const breadcrumbItemText = breadcrumbItem.textContent!;
372
+ if (breadcrumbItemText.length > 30) {
373
+ breadcrumbItem.textContent = `${breadcrumbItemText.substring(
374
+ 0,
375
+ 30
376
+ )}...`;
377
+ }
378
+ });
379
+ };
380
+
381
+ private addBreadcrumbEllipsis = (
382
+ breadcrumbItems: HTMLElement[],
383
+ breadcrumb: HTMLElement
384
+ ) => {
385
+ for (let i = 1; i < breadcrumbItems.length; i++) {
386
+ if (this.isTextWrapping(breadcrumb, breadcrumbItems[0])) {
387
+ breadcrumbItems[i].textContent = "...";
388
+ } else {
389
+ break; // Exit the loop if the breadcrumb is no longer overflowing
390
+ }
391
+ }
392
+ };
393
+
394
+ private formatBreadcrumbs = (breadcrumbs: HTMLElement[]) => {
395
+ breadcrumbs?.forEach((breadcrumb: HTMLElement) => {
396
+ // Get all breadcrumb items that are separated by '/'
397
+ const breadcrumbItems: any =
398
+ breadcrumb.querySelectorAll(".breadcrumb-item");
399
+
400
+ // Check if the breadcrumb is overflowing by comparing it's height to the height of the first breadcrumb item
401
+ if (this.isTextWrapping(breadcrumb, breadcrumbItems[0])) {
402
+ // it is overflowing, so we need to truncate long titles to 30 characters
403
+ this.truncateBreadcrumbText(breadcrumbItems);
404
+
405
+ // Iteratively check if the breadcrumb is still overflowing and replace text with '...' starting from the second breadcrumb item
406
+ this.addBreadcrumbEllipsis(breadcrumbItems, breadcrumb);
407
+
408
+ // After processing all breadcrumb items, if it's still overflowing, hide the breadcrumb element
409
+ if (this.isTextWrapping(breadcrumb, breadcrumbItems[0])) {
410
+ breadcrumb.style.display = "none";
411
+ }
412
+ }
413
+ });
414
+ };
415
+
416
+ private restoreBreadcrumbs = (breadcrumbs: HTMLElement[]) => {
417
+ breadcrumbs.forEach((breadcrumb: HTMLElement, index: number) => {
418
+ // eslint-disable-next-line @lwc/lwc/no-inner-html
419
+ breadcrumb.innerHTML = this.originalBreadcrumbs[index];
420
+ });
421
+ };
422
+
423
+ private windowSizeIncreased = () =>
424
+ window.innerWidth > this.initialWindowWidth;
425
+
426
+ private processBreadcrumbs(root: HTMLElement) {
427
+ // Get all breadcrumbs from search results
428
+ const breadcrumbs = Array.from(
429
+ root.querySelectorAll(".breadcrumb")
430
+ ) as HTMLElement[];
431
+
432
+ if (this.originalBreadcrumbs.length === 0) {
433
+ this.originalBreadcrumbs = breadcrumbs.map(
434
+ (breadcrumb) => breadcrumb.innerHTML
435
+ );
436
+ }
437
+
438
+ if (this.windowSizeIncreased()) {
439
+ /*
440
+ Reset the breadcrumbs to their original state and process them again.
441
+ The additional space means we can replace ellipsis with full text.
442
+ */
443
+ this.restoreBreadcrumbs(breadcrumbs);
444
+ }
445
+
446
+ this.formatBreadcrumbs(breadcrumbs);
207
447
  }
208
448
 
209
449
  private initializeCoveo() {
@@ -212,6 +452,7 @@ export default class SearchResults extends LightningElement {
212
452
  const resultsList = this.template.querySelector(".CoveoResultList");
213
453
 
214
454
  if (resultsList) {
455
+ // eslint-disable-next-line @lwc/lwc/no-inner-html
215
456
  resultsList.innerHTML = resultsTemplatesInnerHtml;
216
457
  }
217
458
 
@@ -227,6 +468,21 @@ export default class SearchResults extends LightningElement {
227
468
  buildTemplateHelperBadge
228
469
  );
229
470
 
471
+ Coveo.TemplateHelpers.registerTemplateHelper(
472
+ "breadcrumbs",
473
+ buildTemplateHelperBreadcrumbs
474
+ );
475
+
476
+ Coveo.TemplateHelpers.registerTemplateHelper(
477
+ "metabreadcrumbs",
478
+ buildTemplateHelperMetaBreadcrumbs
479
+ );
480
+
481
+ Coveo.TemplateHelpers.registerTemplateHelper(
482
+ "uriBreadcrumbs",
483
+ buildTemplateHelperUriBreadcrumbs
484
+ );
485
+
230
486
  Coveo.init(this.root);
231
487
  }
232
488
 
@@ -235,6 +491,7 @@ export default class SearchResults extends LightningElement {
235
491
  if (Object.prototype.hasOwnProperty.call(window, "Coveo")) {
236
492
  this.initializeCoveo();
237
493
  } else {
494
+ // eslint-disable-next-line @lwc/lwc/no-document-query
238
495
  const script = document.querySelector("script.coveo-script");
239
496
  script?.addEventListener("load", () => {
240
497
  this.initializeCoveo();
@@ -9,6 +9,7 @@ import cx from "classnames";
9
9
  export default class StepSequence extends LightningElement {
10
10
  @api animateTransitions = false;
11
11
  @api communicateStepChanges = false;
12
+ @api containerScrollToId = "";
12
13
  @api forceInitiallyVisible = false;
13
14
  @api initialStepIndex: string | undefined;
14
15
  @api optimizePositionAfterAnimation = false;
@@ -39,15 +40,9 @@ export default class StepSequence extends LightningElement {
39
40
 
40
41
  if (this.useHistory) {
41
42
  this.defaultScrollRestorationValue = history.scrollRestoration;
42
- history.scrollRestoration = "manual";
43
- history.replaceState(
44
- {
45
- ...window.history.state,
46
- currentStepIndex: this.currentStepIndex
47
- },
48
- ""
49
- );
43
+ history.scrollRestoration = "manual"; // Step component must override scroll behavior to work properly with history and maintain scrollTo behavior of component
50
44
  window.addEventListener("popstate", this.handleHistoryPopstate);
45
+ window.addEventListener("hashchange", this.handleHashChange);
51
46
  }
52
47
 
53
48
  if (this.sessionStorageId) {
@@ -87,6 +82,7 @@ export default class StepSequence extends LightningElement {
87
82
 
88
83
  disconnectedCallback() {
89
84
  window.removeEventListener("popstate", this.handleHistoryPopstate);
85
+ window.removeEventListener("hashchange", this.handleHashChange);
90
86
  this.removeEventListener("transitionend", this.handleTransitionEnd);
91
87
  history.scrollRestoration = this.defaultScrollRestorationValue;
92
88
  }
@@ -166,6 +162,17 @@ export default class StepSequence extends LightningElement {
166
162
  }
167
163
 
168
164
  if (this.useHistory && updateHistory) {
165
+ if (typeof history.state?.currentStepIndex !== "number") {
166
+ // This is a "new" interaction with the step component, so we store the current
167
+ // component state in history so that going back from the next step works right
168
+ history.pushState(
169
+ {
170
+ currentStepIndex: this.currentStepIndex
171
+ },
172
+ "",
173
+ this.containerScrollToId // update browser hash for correct "feel" to in-page navigation
174
+ );
175
+ }
169
176
  history.pushState(
170
177
  {
171
178
  currentStepIndex: nextStepIndex
@@ -247,9 +254,34 @@ export default class StepSequence extends LightningElement {
247
254
  }
248
255
 
249
256
  handleHistoryPopstate = ({ state }: PopStateEvent) => {
250
- if (typeof state?.currentStepIndex === "number") {
251
- this.remeasureSteps();
252
- this.changeActiveStep(state.currentStepIndex, false);
257
+ if (
258
+ typeof state?.currentStepIndex !== "number" ||
259
+ state.currentStepIndex === this.currentStepIndex
260
+ ) {
261
+ // This history item is not a step change, so bail early.
262
+ return;
263
+ }
264
+
265
+ this.remeasureSteps();
266
+ this.changeActiveStep(state.currentStepIndex, false);
267
+ this.scrollToStepTop();
268
+ };
269
+
270
+ handleHashChange = ({ newURL }: HashChangeEvent) => {
271
+ const newHash = new URL(newURL).hash;
272
+ const oldScrollValue = history.state?.scroll?.value;
273
+
274
+ if (typeof oldScrollValue === "number") {
275
+ window.scrollTo({
276
+ top: oldScrollValue
277
+ });
278
+ } else if (newHash) {
279
+ document.querySelector(newHash)?.scrollIntoView();
280
+ } else {
281
+ // No hash and no old scroll value means this was the initial history item
282
+ window.scrollTo({
283
+ top: 0
284
+ });
253
285
  }
254
286
  };
255
287