@salesforcedevs/dx-components 1.3.218 → 1.3.219-alpha.2

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.218",
3
+ "version": "1.3.219-alpha.2",
4
4
  "description": "DX Lightning web components",
5
5
  "license": "MIT",
6
6
  "engines": {
@@ -43,6 +43,5 @@
43
43
  },
44
44
  "volta": {
45
45
  "node": "16.19.1"
46
- },
47
- "gitHead": "4da04a9390fd75ee83751992b259acb60a47fd98"
46
+ }
48
47
  }
@@ -274,18 +274,14 @@ li.coveo-dynamic-facet-breadcrumb-value-list-item {
274
274
  }
275
275
 
276
276
  .dx-result-title {
277
+ /* override the default Coveo style */
278
+ color: var(--dx-g-blue-vibrant-20) !important;
277
279
  font-family: var(--dx-g-font-display);
278
280
  font-size: var(--dx-g-text-lg);
279
281
  margin: 0;
280
282
  margin-bottom: var(--dx-g-spacing-sm);
281
283
  }
282
284
 
283
- .CoveoResultLink,
284
- a.CoveoResultLink,
285
- .CoveoResult a.CoveoResultLink {
286
- color: var(--dx-g-blue-vibrant-20);
287
- }
288
-
289
285
  .dx-result-excerpt {
290
286
  color: var(--dx-g-gray-10);
291
287
  font-size: 14px;
@@ -422,11 +418,17 @@ a.CoveoResultLink,
422
418
  width: fit-content;
423
419
  }
424
420
 
425
- .dx-badge {
426
- display: block;
421
+ .dx-result-info {
422
+ display: flex;
423
+ gap: 12px;
427
424
  margin-bottom: var(--dx-g-spacing-smd);
428
425
  }
429
426
 
427
+ .breadcrumb {
428
+ color: #555;
429
+ font-size: var(--dx-g-text-xs);
430
+ }
431
+
430
432
  .no-results {
431
433
  display: flex;
432
434
  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,35 +41,74 @@ 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
- <p class="dx-result-title">
49
- <a
50
- class="CoveoResultLink"
51
- data-field="@publicurl"
52
- ></a>
53
- </p>
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>
60
+ <a
61
+ href="<%= raw.uri %>"
62
+ class="dx-result-title"
63
+ >
64
+ <%= title %>
65
+ <% if (!raw.uri.includes('developer.salesforce.com') && !raw.uri.includes('developer-website-s.herokuapp.com')) { %>
66
+ <svg xmlns="http://www.w3.org/2000/svg" style="display: inline; vertical-align: baseline;" fill="var(--dx-g-blue-vibrant-20)" width="20" height="20" part="svg" aria-hidden="true"><use xlink:href="/assets/icons/utility-sprite/svg/symbols.svg#new_window"></use></svg>
67
+ <% } %>
68
+ </a>
54
69
  <p class="dx-result-excerpt CoveoExcerpt"></p>
55
70
  </div>
56
71
  </script>
57
72
  <script
58
73
  id="myDefaultResultTemplate"
59
74
  class="result-template"
60
- type="text/html"
75
+ type="text/underscore"
61
76
  >
62
77
  <div class="dx-result">
63
- <span class="CoveoFieldValue" data-field="@content_type" data-helper="badge" data-html-value="true"></span>
64
- <p class="dx-result-title">
65
- <a class="CoveoResultLink"></a>
66
- </p>
78
+ <div class="dx-result-info">
79
+ <span class="CoveoFieldValue" data-field="@content_type" data-helper="badge" data-html-value="true"></span>
80
+ <% if (!raw.breadcrumbs && !raw.metabreadcrumbs) { %>
81
+ <span class="CoveoFieldValue" data-field="@uri" data-helper="uriBreadcrumbs" data-html-value="true"></span>
82
+ <% } else { %>
83
+ <% if (raw.uri.includes('/references/')) { %>
84
+ <span class="CoveoFieldValue" data-field="@metabreadcrumbs" data-helper="metabreadcrumbs" data-html-value="true"></span>
85
+ <% } else { %>
86
+ <span class="CoveoFieldValue" data-field="@breadcrumbs" data-helper="breadcrumbs" data-html-value="true"></span>
87
+ <% } %>
88
+ <% } %>
89
+ </div>
90
+ <a
91
+ href="<%= raw.uri %>"
92
+ class="dx-result-title"
93
+ >
94
+ <%= uri %>
95
+ <% if (!raw.uri.includes('developer.salesforce.com') && !raw.uri.includes('developer-website-s.herokuapp.com')) { %>
96
+ <svg xmlns="http://www.w3.org/2000/svg" style="display: inline; vertical-align: baseline;" fill="var(--dx-g-blue-vibrant-20)" width="20" height="20" part="svg" aria-hidden="true"><use xlink:href="/assets/icons/utility-sprite/svg/symbols.svg#new_window"></use></svg>
97
+ <% } %>
98
+ </a>
67
99
  <p class="dx-result-excerpt CoveoExcerpt"></p>
68
100
  </div>
69
101
  </script>
70
102
  `;
71
103
 
104
+ const isInternalDomain = (domain: string) =>
105
+ domain === "developer.salesforce.com" ||
106
+ domain === "developer-website-s.herokuapp.com";
107
+
108
+ const isTrailheadDomain = (domain: string) =>
109
+ domain === "trailhead.salesforce.com" ||
110
+ domain === "dev.trailhead.salesforce.com";
111
+
72
112
  const buildTemplateHelperBadge = (value: keyof typeof CONTENT_TYPE_LABELS) => {
73
113
  const style = getContentTypeColorVariables(value);
74
114
  const label = CONTENT_TYPE_LABELS[value];
@@ -88,6 +128,107 @@ const buildTemplateHelperBadge = (value: keyof typeof CONTENT_TYPE_LABELS) => {
88
128
  `;
89
129
  };
90
130
 
131
+ const processParts = (parts: string[], internalFlag = false) => {
132
+ // filter /docs/ breadcrumb item from internal domains
133
+ const filterFn = internalFlag
134
+ ? (part: string) => part !== "docs"
135
+ : (part: string) => part;
136
+
137
+ return parts.filter(filterFn).map((part) => {
138
+ // Remove special characters & .htm/.xml extension
139
+ part = part
140
+ .replace(/_/g, " ")
141
+ .replace(/-/g, " ")
142
+ .replace(/.html*/g, " ")
143
+ .replace(/.xml/g, " ")
144
+ .replace(/b2c/g, "B2C");
145
+
146
+ // Capitalize first letter of each word
147
+ part = part.replace(/\w\S*/g, (w) => {
148
+ return w.replace(/^\w/, (c) => c.toUpperCase());
149
+ });
150
+
151
+ return `<span class="breadcrumb-item">${decodeURI(part)}</span>`;
152
+ });
153
+ };
154
+
155
+ const buildTemplateHelperUriBreadcrumbs = (value: string) => {
156
+ const url = new URL(value);
157
+
158
+ // exclude youtube links from breadcrumbs
159
+ const hostnamePattern = /^((www\.)?(youtube\.com|youtu\.be))$/;
160
+
161
+ // we don't want to show atlas docs because the url structure is mad ugly
162
+ if (hostnamePattern.test(url.hostname) || url.pathname.includes("atlas.")) {
163
+ return "";
164
+ }
165
+
166
+ let parts = url.pathname.split("/").filter((part) => part !== "");
167
+
168
+ // Remove language prefix from trailhead URLs
169
+ if (isTrailheadDomain(url.hostname)) {
170
+ parts = parts
171
+ .slice(1)
172
+ .filter((part) => part !== "content" && part !== "learn");
173
+ }
174
+
175
+ const breadcrumbs = processParts(parts, isInternalDomain(url.hostname));
176
+
177
+ if (!isInternalDomain(url.hostname)) {
178
+ // 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)
179
+ breadcrumbs.unshift(
180
+ `<span class="breadcrumb-item">${url.hostname}</span>`
181
+ );
182
+
183
+ return `
184
+ <span class="breadcrumb">
185
+ ${breadcrumbs.join(" / ")}
186
+ </span>
187
+ `;
188
+ } else if (breadcrumbs.length === 1) {
189
+ // Hide breadcrumbs if there is only one breadcrumb item
190
+ return "";
191
+ }
192
+
193
+ // remove the last breadcrumb item (the search result title makes it redundant)
194
+ breadcrumbs.pop();
195
+
196
+ return `<span class="breadcrumb">/ ${breadcrumbs.join(" / ")} /</span>`;
197
+ };
198
+
199
+ const buildTemplateHelperBreadcrumbs = (value: string) => {
200
+ const parts = value.split("/").filter((part) => part !== "");
201
+
202
+ // Don't show breadcrumbs if there's only one part
203
+ if (parts.length === 1) {
204
+ return "";
205
+ }
206
+
207
+ const breadcrumbs = processParts(parts);
208
+
209
+ // remove last breadcrumb item
210
+ breadcrumbs.pop();
211
+
212
+ return `
213
+ <span class="breadcrumb">/ ${breadcrumbs.join(" / ")} /</span>
214
+ `;
215
+ };
216
+
217
+ const buildTemplateHelperMetaBreadcrumbs = (value: string) => {
218
+ const parts = value.split("/").filter((part) => part !== "");
219
+
220
+ // Don't show breadcrumbs if there's only one part
221
+ if (parts.length === 1) {
222
+ return "";
223
+ }
224
+
225
+ const breadcrumbs = processParts(parts);
226
+
227
+ return `
228
+ <span class="breadcrumb">/ ${breadcrumbs.join(" / ")}</span>
229
+ `;
230
+ };
231
+
91
232
  // @ts-ignore Dark Magic (TM) we are overriding the 'title' field with a custom getter. We should really stop doing this.
92
233
  export default class SearchResults extends LightningElement {
93
234
  @api coveoOrganizationId!: string;
@@ -139,9 +280,12 @@ export default class SearchResults extends LightningElement {
139
280
  BreadcrumbManager.clearBreadcrumbs();
140
281
  }
141
282
 
142
- private currentPage: number = 1;
283
+ private currentPage: number = 25;
143
284
  private totalPages: number = 1;
144
285
 
286
+ private originalBreadcrumbs: string[] = [];
287
+ private initialWindowWidth = window.innerWidth;
288
+
145
289
  private goToPage(e: CustomEvent) {
146
290
  const page = e.detail;
147
291
  const Pager = Coveo.get(
@@ -179,6 +323,7 @@ export default class SearchResults extends LightningElement {
179
323
  if (Coveo.state(this.root!, "q") === "") {
180
324
  Coveo.state(this.root!, "sort", "date descending");
181
325
  }
326
+
182
327
  this.isInitialized = true;
183
328
  }
184
329
  );
@@ -204,6 +349,110 @@ export default class SearchResults extends LightningElement {
204
349
 
205
350
  this.trackSearchResults(event, this.query, this.totalResults);
206
351
  });
352
+
353
+ Coveo.$$(root).on(Coveo.QueryEvents.deferredQuerySuccess, async () => {
354
+ // wait specified time to ensure breadcrumbs are rendered before processing them
355
+ await pollUntil(
356
+ () => {
357
+ const coveoResults =
358
+ this.root!.querySelector(".CoveoResult");
359
+
360
+ return Boolean(coveoResults);
361
+ },
362
+ 20,
363
+ 1000
364
+ );
365
+
366
+ this.processBreadcrumbs(this.root!);
367
+
368
+ window.onresize = () => this.processBreadcrumbs(root);
369
+ });
370
+ }
371
+
372
+ // Checks if text is wrapping by comparing it with an element's text that doesn't wrap
373
+ private isTextWrapping = (
374
+ elementOne: HTMLElement,
375
+ elementTwo: HTMLElement
376
+ ) => elementOne.offsetHeight > elementTwo.offsetHeight;
377
+
378
+ private truncateBreadcrumbText = (breadcrumbItems: HTMLElement[]) => {
379
+ breadcrumbItems.forEach((breadcrumbItem: HTMLElement) => {
380
+ const breadcrumbItemText = breadcrumbItem.textContent!;
381
+ if (breadcrumbItemText.length > 30) {
382
+ breadcrumbItem.textContent = `${breadcrumbItemText.substring(
383
+ 0,
384
+ 30
385
+ )}...`;
386
+ }
387
+ });
388
+ };
389
+
390
+ private addBreadcrumbEllipsis = (
391
+ breadcrumbItems: HTMLElement[],
392
+ breadcrumb: HTMLElement
393
+ ) => {
394
+ for (let i = 1; i < breadcrumbItems.length; i++) {
395
+ if (this.isTextWrapping(breadcrumb, breadcrumbItems[0])) {
396
+ breadcrumbItems[i].textContent = "...";
397
+ } else {
398
+ break; // Exit the loop if the breadcrumb is no longer overflowing
399
+ }
400
+ }
401
+ };
402
+
403
+ private formatBreadcrumbs = (breadcrumbs: HTMLElement[]) => {
404
+ breadcrumbs?.forEach((breadcrumb: HTMLElement) => {
405
+ // Get all breadcrumb items that are separated by '/'
406
+ const breadcrumbItems: any =
407
+ breadcrumb.querySelectorAll(".breadcrumb-item");
408
+
409
+ // Check if the breadcrumb is overflowing by comparing it's height to the height of the first breadcrumb item
410
+ if (this.isTextWrapping(breadcrumb, breadcrumbItems[0])) {
411
+ // it is overflowing, so we need to truncate long titles to 30 characters
412
+ this.truncateBreadcrumbText(breadcrumbItems);
413
+
414
+ // Iteratively check if the breadcrumb is still overflowing and replace text with '...' starting from the second breadcrumb item
415
+ this.addBreadcrumbEllipsis(breadcrumbItems, breadcrumb);
416
+
417
+ // After processing all breadcrumb items, if it's still overflowing, hide the breadcrumb element
418
+ if (this.isTextWrapping(breadcrumb, breadcrumbItems[0])) {
419
+ breadcrumb.style.display = "none";
420
+ }
421
+ }
422
+ });
423
+ };
424
+
425
+ private restoreBreadcrumbs = (breadcrumbs: HTMLElement[]) => {
426
+ breadcrumbs.forEach((breadcrumb: HTMLElement, index: number) => {
427
+ // eslint-disable-next-line @lwc/lwc/no-inner-html
428
+ breadcrumb.innerHTML = this.originalBreadcrumbs[index];
429
+ });
430
+ };
431
+
432
+ private windowSizeIncreased = () =>
433
+ window.innerWidth > this.initialWindowWidth;
434
+
435
+ private processBreadcrumbs(root: HTMLElement) {
436
+ // Get all breadcrumbs from search results
437
+ const breadcrumbs = Array.from(
438
+ root.querySelectorAll(".breadcrumb")
439
+ ) as HTMLElement[];
440
+
441
+ if (this.originalBreadcrumbs.length === 0) {
442
+ this.originalBreadcrumbs = breadcrumbs.map(
443
+ (breadcrumb) => breadcrumb.innerHTML
444
+ );
445
+ }
446
+
447
+ if (this.windowSizeIncreased()) {
448
+ /*
449
+ Reset the breadcrumbs to their original state and process them again.
450
+ The additional space means we can replace ellipsis with full text.
451
+ */
452
+ this.restoreBreadcrumbs(breadcrumbs);
453
+ }
454
+
455
+ this.formatBreadcrumbs(breadcrumbs);
207
456
  }
208
457
 
209
458
  private initializeCoveo() {
@@ -212,6 +461,7 @@ export default class SearchResults extends LightningElement {
212
461
  const resultsList = this.template.querySelector(".CoveoResultList");
213
462
 
214
463
  if (resultsList) {
464
+ // eslint-disable-next-line @lwc/lwc/no-inner-html
215
465
  resultsList.innerHTML = resultsTemplatesInnerHtml;
216
466
  }
217
467
 
@@ -227,6 +477,21 @@ export default class SearchResults extends LightningElement {
227
477
  buildTemplateHelperBadge
228
478
  );
229
479
 
480
+ Coveo.TemplateHelpers.registerTemplateHelper(
481
+ "breadcrumbs",
482
+ buildTemplateHelperBreadcrumbs
483
+ );
484
+
485
+ Coveo.TemplateHelpers.registerTemplateHelper(
486
+ "metabreadcrumbs",
487
+ buildTemplateHelperMetaBreadcrumbs
488
+ );
489
+
490
+ Coveo.TemplateHelpers.registerTemplateHelper(
491
+ "uriBreadcrumbs",
492
+ buildTemplateHelperUriBreadcrumbs
493
+ );
494
+
230
495
  Coveo.init(this.root);
231
496
  }
232
497
 
@@ -235,6 +500,7 @@ export default class SearchResults extends LightningElement {
235
500
  if (Object.prototype.hasOwnProperty.call(window, "Coveo")) {
236
501
  this.initializeCoveo();
237
502
  } else {
503
+ // eslint-disable-next-line @lwc/lwc/no-document-query
238
504
  const script = document.querySelector("script.coveo-script");
239
505
  script?.addEventListener("load", () => {
240
506
  this.initializeCoveo();
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.