@salesforcedevs/dx-components 1.32.0-alpha.9 → 1.32.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.
@@ -1,223 +1,526 @@
1
1
  import { LightningElement, api, track } from "lwc";
2
- import debounce from "debounce";
2
+ import type * as CoveoSDK from "coveo-search-ui";
3
+ import { DateTime } from "luxon";
3
4
  import { track as trackGTM } from "dxUtils/analytics";
4
5
  import {
5
- type Data360SearchResultItem,
6
- fetchSearch,
7
- getQueryFromUrl
8
- } from "dxUtils/data360Search";
9
-
10
- const SEARCH_DEBOUNCE_DELAY = 400;
11
-
12
- /** Normalized result for template (with href, uniqueId, openInNewTab) */
13
- interface SearchResultDisplay {
14
- title: string;
15
- href: string;
16
- matchedText: string;
17
- uniqueId: string;
18
- resultIndex: number;
19
- openInNewTab: string | undefined;
20
- rel: string | undefined;
6
+ CONTENT_TYPE_LABELS,
7
+ CONTENT_TYPE_ICONS
8
+ } from "dxConstants/contentTypes";
9
+ import { getContentTypeColorVariables } from "dxUtils/contentTypes";
10
+ import { pollUntil } from "dxUtils/async";
11
+ import resultsTemplate from "./resultsTemplate";
12
+
13
+ // Max height for breadcrumb without wrapping
14
+ const MAX_BREADCRUMB_HEIGHT = 16;
15
+
16
+ interface CoveoSearch {
17
+ state: typeof CoveoSDK.state;
18
+ get: typeof CoveoSDK.get;
19
+ $$: typeof CoveoSDK.$$;
20
+ InitializationEvents: typeof CoveoSDK.InitializationEvents;
21
+ QueryEvents: typeof CoveoSDK.QueryEvents;
22
+ SearchEndpoint: typeof CoveoSDK.SearchEndpoint;
23
+ TemplateCache: typeof CoveoSDK.TemplateCache;
24
+ UnderscoreTemplate: typeof CoveoSDK.UnderscoreTemplate;
25
+ init: typeof CoveoSDK.init;
26
+ IQuerySuccessEventArgs: CoveoSDK.IQuerySuccessEventArgs;
27
+ TemplateHelpers: any;
28
+ }
29
+
30
+ declare const Coveo: CoveoSearch;
31
+
32
+ function getPaginationState(event: CoveoSDK.IQuerySuccessEventArgs): {
33
+ numberOfPages: number;
34
+ currentPage: number;
35
+ } {
36
+ const pageSize = event.query.numberOfResults!;
37
+ const totalResults = event.results.totalCount!;
38
+ const numberOfPages = Math.ceil(totalResults / pageSize);
39
+
40
+ const currentPage = event.query.firstResult! / pageSize + 1;
41
+
42
+ return { numberOfPages, currentPage };
21
43
  }
22
44
 
45
+ const isInternalDomain = (domain: string) =>
46
+ domain === "developer.salesforce.com" ||
47
+ domain === "developer-website-s.herokuapp.com";
48
+
49
+ const isTrailheadDomain = (domain: string) =>
50
+ domain === "trailhead.salesforce.com" ||
51
+ domain === "dev.trailhead.salesforce.com";
52
+
53
+ const buildTemplateHelperBadge = (value: keyof typeof CONTENT_TYPE_LABELS) => {
54
+ const style = getContentTypeColorVariables(value);
55
+ const label = CONTENT_TYPE_LABELS[value];
56
+ const { iconSprite, iconSymbol } = CONTENT_TYPE_ICONS[value];
57
+ return `
58
+ <div style="${style}" class="dx-badge">
59
+ <svg
60
+ aria-hidden="true"
61
+ style="display: inline; margin-bottom: 1px; fill: var(--color); height: var(--dx-g-icon-size-xs); width: var(--dx-g-icon-size-xs);"
62
+ xmlns="http://www.w3.org/2000/svg"
63
+ part="svg"
64
+ >
65
+ <use xlink:href="/assets/icons/${iconSprite}-sprite/svg/symbols.svg#${iconSymbol}"></use>
66
+ </svg>
67
+ <span>${label}</span>
68
+ </div>
69
+ `;
70
+ };
71
+
72
+ const processParts = (parts: string[], internalFlag = false) => {
73
+ // filter /docs/ breadcrumb item from internal domains
74
+ const filterFn = internalFlag
75
+ ? (part: string) => part !== "docs"
76
+ : (part: string) => part;
77
+
78
+ return parts.filter(filterFn).map((part) => {
79
+ // Remove special characters & .htm/.xml extension
80
+ part = part
81
+ .replace(/_/g, "")
82
+ .replace(/-/g, "")
83
+ .replace(/.html*/g, "")
84
+ .replace(/.xml/g, "")
85
+ .replace(/b2c/g, "B2C");
86
+
87
+ // Capitalize first letter of each word
88
+ part = part.replace(/\w\S*/g, (w) => {
89
+ return w.replace(/^\w/, (c) => c.toUpperCase());
90
+ });
91
+
92
+ return `<span class="breadcrumb-item">${decodeURI(part)}</span>`;
93
+ });
94
+ };
95
+
96
+ const buildTemplateHelperUriBreadcrumbs = (value: string) => {
97
+ const url = new URL(value);
98
+
99
+ // exclude youtube links from breadcrumbs
100
+ const hostnamePattern = /^((www\.)?(youtube\.com|youtu\.be))$/;
101
+
102
+ // we don't want to show atlas docs because the url structure is mad ugly
103
+ if (hostnamePattern.test(url.hostname) || url.pathname.includes("atlas.")) {
104
+ return "";
105
+ }
106
+
107
+ let parts = url.pathname.split("/").filter((part) => part !== "");
108
+
109
+ // Remove language prefix from trailhead URLs
110
+ if (isTrailheadDomain(url.hostname)) {
111
+ parts = parts
112
+ .slice(1)
113
+ .filter((part) => part !== "content" && part !== "learn");
114
+ }
115
+
116
+ const breadcrumbs = processParts(parts, isInternalDomain(url.hostname));
117
+
118
+ if (!isInternalDomain(url.hostname)) {
119
+ // 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)
120
+ breadcrumbs.unshift(
121
+ `<span class="breadcrumb-item">${url.hostname}</span>`
122
+ );
123
+
124
+ return `
125
+ <span class="breadcrumb">
126
+ ${breadcrumbs.join(" / ")}
127
+ </span>
128
+ `;
129
+ } else if (breadcrumbs.length === 1) {
130
+ // Hide breadcrumbs if there is only one breadcrumb item
131
+ return "";
132
+ }
133
+
134
+ // remove the last breadcrumb item (the search result title makes it redundant)
135
+ breadcrumbs.pop();
136
+
137
+ return `<span class="breadcrumb">/ ${breadcrumbs.join(" / ")} /</span>`;
138
+ };
139
+
140
+ const buildTemplateHelperCommunityBreadcrumbs = () => {
141
+ return `<span class="breadcrumb"><span class="breadcrumb-item">trailhead.salesforce.com</span> / <span class="breadcrumb-item">trailblazer-community</span> / <span class="breadcrumb-item">feed</span> / </span>`;
142
+ };
143
+
144
+ const buildTemplateHelperBreadcrumbs = (value: string) => {
145
+ const parts = value.split("/").filter((part) => part !== "");
146
+
147
+ // Don't show breadcrumbs if there's only one part
148
+ if (parts.length === 1) {
149
+ return "";
150
+ }
151
+
152
+ const breadcrumbs = processParts(parts);
153
+
154
+ // remove last breadcrumb item
155
+ breadcrumbs.pop();
156
+
157
+ return `
158
+ <span class="breadcrumb">/ ${breadcrumbs.join(" / ")} /</span>
159
+ `;
160
+ };
161
+
162
+ const buildTemplateHelperMetaBreadcrumbs = (value: string) => {
163
+ const parts = value.split("/").filter((part) => part !== "");
164
+
165
+ // Don't show breadcrumbs if there's only one part
166
+ if (parts.length === 1) {
167
+ return "";
168
+ }
169
+
170
+ const breadcrumbs = processParts(parts);
171
+
172
+ return `
173
+ <span class="breadcrumb">/ ${breadcrumbs.join(" / ")}</span>
174
+ `;
175
+ };
176
+
177
+ const buildTemplateHelperPostedDate = (value: string) => {
178
+ const time = DateTime.fromMillis(Number(value)).toLocaleString(
179
+ DateTime.DATE_MED
180
+ );
181
+
182
+ return `Posted on: ${time} - `;
183
+ };
184
+
185
+ const buildTemplateHelperReplies = (value: string) => {
186
+ const number = Number(value);
187
+
188
+ return `<strong>${value} ${number > 1 ? "Replies" : "Reply"}</strong>`;
189
+ };
190
+
191
+ const buildTemplateHelperLikes = (value: string) => {
192
+ const number = Number(value);
193
+
194
+ return `<strong>${value} ${number > 1 ? "Likes" : "Like"}</strong>`;
195
+ };
196
+
197
+ // @ts-ignore Dark Magic (TM) we are overriding the 'title' field with a custom getter. We should really stop doing this.
23
198
  export default class SearchResults extends LightningElement {
24
- @track private _query = "";
25
- @track private results: SearchResultDisplay[] = [];
26
- @track private isLoading = false;
27
- @track private hasSearched = false;
28
- @track private didTrackThisSearch = false;
199
+ @api coveoOrganizationId!: string;
200
+ @api coveoPublicAccessToken!: string;
201
+ @api coveoSearchPipeline!: string;
202
+
203
+ private root: HTMLElement | null = null;
204
+ private isInitialized = false;
205
+
206
+ @track _query: string = "";
29
207
 
30
208
  @api
31
209
  get searchQuery() {
32
210
  return this._query;
33
211
  }
34
212
  set searchQuery(q: string) {
35
- const next = q ?? "";
36
- if (next !== this._query) {
37
- this._query = next;
38
- this.syncUrlToQuery();
39
- if (this._query.trim()) {
40
- this.runSearch();
41
- } else {
42
- this.results = [];
43
- this.hasSearched = false;
44
- }
213
+ this._query = q;
214
+ if (this.isInitialized && this._query !== "") {
215
+ this.updateSearchQuery();
45
216
  }
46
217
  }
47
218
 
48
- private get query(): string {
49
- return this._query;
50
- }
219
+ private totalResults: number | null = null;
51
220
 
52
- private get totalResults(): number {
53
- return this.results?.length ?? 0;
221
+ private get title() {
222
+ return this.totalResults ? this.totalResults.toLocaleString() : "0";
54
223
  }
55
224
 
56
- private get resultCountLabel(): string {
57
- return this.totalResults.toLocaleString();
58
- }
225
+ private query: string = "";
226
+ private hasFilters: boolean = false;
59
227
 
60
228
  private get hasQuery(): boolean {
61
229
  return this.query !== "";
62
230
  }
63
231
 
64
- private get hasResults(): boolean {
65
- return this.results.length > 0;
232
+ private get coveoAnalyticsEndpoint() {
233
+ return `https://${this.coveoOrganizationId}.analytics.org.coveo.com/rest/ua`;
66
234
  }
67
235
 
68
- private get showNoResults(): boolean {
69
- return this.hasSearched && !this.isLoading && !this.hasResults;
236
+ private updateSearchQuery() {
237
+ Coveo.state(this.root!, "q", this._query);
70
238
  }
71
239
 
72
- private syncUrlToQuery() {
73
- const url = new URL(window.location.href);
74
- if (this._query.trim()) {
75
- url.searchParams.set("q", this._query.trim());
76
- } else {
77
- url.searchParams.delete("q");
78
- }
79
- const newHref = url.pathname + url.search + url.hash;
80
- if (
81
- window.location.pathname +
82
- window.location.search +
83
- window.location.hash !==
84
- newHref
85
- ) {
86
- window.history.replaceState(null, "", newHref);
87
- }
240
+ private clearFilters() {
241
+ const BreadcrumbManager = Coveo.get(
242
+ this.root!.querySelector(".CoveoBreadcrumb") as HTMLElement
243
+ ) as any;
244
+ BreadcrumbManager.clearBreadcrumbs();
88
245
  }
89
246
 
90
- private async doFetch() {
91
- const query = this._query.trim();
92
- if (!query) {
93
- this.results = [];
94
- this.isLoading = false;
95
- return;
96
- }
97
- this.isLoading = true;
98
- this.hasSearched = true;
99
- this.didTrackThisSearch = false;
100
- try {
101
- const raw = await fetchSearch(query);
102
- this.results = raw.map(
103
- (item: Data360SearchResultItem, index: number) =>
104
- this.normalizeResult(item, index)
105
- );
106
- this.trackSearchResultsOnce(query, this.results.length);
107
- } catch (err) {
108
- console.error("Data 360 Search request failed", err);
109
- this.results = [];
110
- } finally {
111
- this.isLoading = false;
112
- }
113
- }
247
+ private currentPage: number = 25;
248
+ private totalPages: number = 1;
114
249
 
115
- private runSearch = debounce(() => {
116
- this.doFetch();
117
- }, SEARCH_DEBOUNCE_DELAY);
118
-
119
- private normalizeResult(
120
- item: Data360SearchResultItem,
121
- index: number
122
- ): SearchResultDisplay {
123
- const href = item.url ?? "";
124
- const isExternal =
125
- href.startsWith("http") &&
126
- !href.includes("developer.salesforce.com") &&
127
- !href.includes("developer-website-s.herokuapp.com");
128
- return {
129
- title: item.title ?? "",
130
- href,
131
- matchedText: item.matchedText ?? "",
132
- uniqueId: href || `result-${index}`,
133
- resultIndex: index + 1,
134
- openInNewTab: isExternal ? "_blank" : undefined,
135
- rel: isExternal ? "noopener noreferrer" : undefined
136
- };
137
- }
138
-
139
- private onSearchResultClick(e: MouseEvent) {
140
- const anchor = e.currentTarget as HTMLAnchorElement;
141
- const index = Number(anchor.dataset.index ?? "0");
142
- const title = anchor.dataset.title ?? "";
143
- const href = anchor.href ?? "";
144
- trackGTM(anchor, "custEv_scopedSearchlinkClick", {
145
- click_text: title,
146
- click_url: href,
147
- element_title: title,
148
- element_type: "link",
149
- content_category: "documentation",
150
- search_term: this.query,
151
- search_result_position: index
152
- });
250
+ private originalBreadcrumbs: string[] = [];
251
+ private initialWindowWidth = window.innerWidth;
252
+
253
+ private goToPage(e: CustomEvent) {
254
+ const page = e.detail;
255
+ const Pager = Coveo.get(
256
+ this.root!.querySelector(".CoveoPager") as HTMLElement
257
+ ) as any;
258
+ Pager.setPage(page);
259
+ this.currentPage = page;
153
260
  }
154
261
 
155
- private trackSearchResultsOnce(term: string, resultCount: number) {
156
- if (this.didTrackThisSearch) {
157
- return;
158
- }
159
- this.didTrackThisSearch = true;
160
- if (document.readyState === "complete") {
161
- trackGTM(this.template.host, "custEv_search", {
162
- search_term: term,
163
- search_category: "",
164
- search_type: "site search",
165
- search_result_count: resultCount
262
+ private dismissFiltersOverlay = () => {
263
+ const overlay = this.root!.querySelector(
264
+ ".coveo-dropdown-background-active"
265
+ ) as HTMLElement | null;
266
+ overlay?.click();
267
+ };
268
+
269
+ private attachListeners(root: HTMLElement) {
270
+ Coveo.$$(root).on(
271
+ Coveo.InitializationEvents.afterInitialization,
272
+ () => {
273
+ const url = new URL(window.location.href);
274
+ const searchParams = url.searchParams;
275
+ const keywordsParam = searchParams.get("keywords");
276
+
277
+ // Accomodate for the global nav using 'keywords' param instead of 'q'
278
+ if (keywordsParam) {
279
+ this._query = searchParams.get("keywords")!;
280
+ searchParams.delete("keywords");
281
+ window.history.replaceState(null, "", url.href);
282
+ }
283
+
284
+ if (this._query !== "") {
285
+ Coveo.state(this.root!, "q", this.searchQuery);
286
+ }
287
+ if (Coveo.state(this.root!, "q") === "") {
288
+ Coveo.state(this.root!, "sort", "date descending");
289
+ }
290
+
291
+ this.isInitialized = true;
292
+ }
293
+ );
294
+
295
+ Coveo.$$(root).on(Coveo.QueryEvents.querySuccess, (event: any) => {
296
+ const { currentPage, numberOfPages } = getPaginationState(
297
+ event.detail
298
+ );
299
+ this.currentPage = currentPage;
300
+ this.totalPages = numberOfPages;
301
+ this.totalResults = event.detail.results.totalCount;
302
+
303
+ this.query = event.detail.query.q ?? "";
304
+ this.hasFilters = event.detail.query?.facets?.some((f: any) => {
305
+ return f.currentValues.some((cv: any) => {
306
+ return cv.state === "selected";
307
+ });
166
308
  });
167
- } else {
168
- window.addEventListener(
169
- "load",
309
+ // Note that this logic means that if someone clicks a search result before
310
+ // onetrust has loaded, and thus navigates away from the page, we will lose the tracking
311
+ if (document.readyState === "complete") {
312
+ this.trackSearchResults(event, this.query, this.totalResults);
313
+ } else {
314
+ const query = this.query;
315
+ const totalResults = this.totalResults;
316
+ window.addEventListener("load", () => {
317
+ this.trackSearchResults(event, query, totalResults);
318
+ });
319
+ }
320
+ });
321
+
322
+ Coveo.$$(root).on(Coveo.QueryEvents.deferredQuerySuccess, async () => {
323
+ // wait specified time to ensure breadcrumbs are rendered before processing them
324
+ await pollUntil(
170
325
  () => {
171
- trackGTM(this.template.host, "custEv_search", {
172
- search_term: term,
173
- search_category: "",
174
- search_type: "site search",
175
- search_result_count: resultCount
176
- });
326
+ const coveoResults =
327
+ this.root!.querySelector(".CoveoResult");
328
+
329
+ return Boolean(coveoResults);
177
330
  },
178
- { once: true }
331
+ 20,
332
+ 1000
179
333
  );
180
- }
334
+
335
+ this.processBreadcrumbs(root);
336
+
337
+ window.onresize = () => this.processBreadcrumbs(root);
338
+ });
181
339
  }
182
340
 
183
- private onSearchInputChange(e: CustomEvent) {
184
- const value = (e.detail as string) ?? "";
185
- this._query = value;
186
- this.syncUrlToQuery();
187
- if (value.trim()) {
188
- this.runSearch();
189
- } else {
190
- this.results = [];
191
- this.hasSearched = false;
341
+ // check if the breadcrumb is overflowing or not based on the height of a single character in the text element
342
+ private isTextWrapping = (element: HTMLElement) => {
343
+ return element.offsetHeight > MAX_BREADCRUMB_HEIGHT;
344
+ };
345
+
346
+ private truncateBreadcrumbText = (breadcrumbItems: HTMLElement[]) => {
347
+ breadcrumbItems.forEach((breadcrumbItem: HTMLElement) => {
348
+ const breadcrumbItemText = breadcrumbItem.textContent!;
349
+ if (breadcrumbItemText.length > 30) {
350
+ breadcrumbItem.textContent = `${breadcrumbItemText.substring(
351
+ 0,
352
+ 30
353
+ )}...`;
354
+ }
355
+ });
356
+ };
357
+
358
+ private addBreadcrumbEllipsis = (
359
+ breadcrumbItems: HTMLElement[],
360
+ breadcrumb: HTMLElement
361
+ ) => {
362
+ for (let i = 1; i < breadcrumbItems.length; i++) {
363
+ if (this.isTextWrapping(breadcrumb)) {
364
+ // if the previous element is an ellipsis, make it empty (in order to avoid multiple grouped ellipsis)
365
+ if (breadcrumbItems[i - 1]?.textContent === "...") {
366
+ breadcrumbItems[i].innerHTML = "";
367
+ } else {
368
+ breadcrumbItems[i].textContent = "...";
369
+ }
370
+ } else {
371
+ break; // Exit the loop if the breadcrumb is no longer overflowing
372
+ }
192
373
  }
374
+
375
+ // remove any empty breadcrumb items
376
+ breadcrumb.innerHTML = breadcrumb.innerHTML
377
+ .replace(
378
+ / ?\/ +<span class="breadcrumb-item"><\/span> +\/ ?/g,
379
+ " / "
380
+ )
381
+ // when first loading the page on mobile, the breadcrumb items are not grouped correctly
382
+ .replace(
383
+ `<span class="breadcrumb-item">...</span> / <span class="breadcrumb-item">...</span>`,
384
+ `<span class="breadcrumb-item">...</span>`
385
+ );
386
+ };
387
+
388
+ private formatBreadcrumbs = (breadcrumbs: HTMLElement[]) => {
389
+ breadcrumbs?.forEach((breadcrumb: HTMLElement) => {
390
+ // Get all breadcrumb items that are separated by '/'
391
+ const breadcrumbItems: any =
392
+ breadcrumb.querySelectorAll(".breadcrumb-item");
393
+
394
+ // Check if the breadcrumb is overflowing by comparing it's height to the height of the first breadcrumb item
395
+ if (this.isTextWrapping(breadcrumb)) {
396
+ // it is overflowing, so we need to truncate long titles to 30 characters
397
+ this.truncateBreadcrumbText(breadcrumbItems);
398
+
399
+ // Iteratively check if the breadcrumb is still overflowing and replace text with '...' starting from the second breadcrumb item
400
+ this.addBreadcrumbEllipsis(breadcrumbItems, breadcrumb);
401
+
402
+ // After processing all breadcrumb items, if it's still overflowing, hide the breadcrumb element
403
+ if (this.isTextWrapping(breadcrumb)) {
404
+ breadcrumb.style.display = "none";
405
+ }
406
+ }
407
+ });
408
+ };
409
+
410
+ private restoreBreadcrumbs = (breadcrumbs: HTMLElement[]) => {
411
+ breadcrumbs.forEach((breadcrumb: HTMLElement, index: number) => {
412
+ // eslint-disable-next-line @lwc/lwc/no-inner-html
413
+ breadcrumb.innerHTML = this.originalBreadcrumbs[index];
414
+ });
415
+ };
416
+
417
+ private windowSizeIncreased = () =>
418
+ window.innerWidth > this.initialWindowWidth;
419
+
420
+ private processBreadcrumbs(root: HTMLElement) {
421
+ // Get all breadcrumbs from search results
422
+ const breadcrumbs = Array.from(
423
+ root.querySelectorAll(".breadcrumb")
424
+ ) as HTMLElement[];
425
+
426
+ if (this.originalBreadcrumbs.length === 0) {
427
+ this.originalBreadcrumbs = breadcrumbs.map(
428
+ (breadcrumb) => breadcrumb.innerHTML
429
+ );
430
+ }
431
+
432
+ if (this.windowSizeIncreased()) {
433
+ /*
434
+ Reset the breadcrumbs to their original state and process them again.
435
+ The additional space means we can replace ellipsis with full text.
436
+ */
437
+ this.restoreBreadcrumbs(breadcrumbs);
438
+ }
439
+
440
+ this.formatBreadcrumbs(breadcrumbs);
193
441
  }
194
442
 
195
- private hasRunInitialSearch = false;
443
+ private initializeCoveo() {
444
+ this.root = this.template.querySelector(".CoveoSearchInterface")!;
196
445
 
197
- connectedCallback() {
198
- const q = getQueryFromUrl();
199
- if (q) {
200
- this._query = q;
201
- this.hasRunInitialSearch = true;
202
- this.doFetch();
446
+ const resultsList = this.template.querySelector(".CoveoResultList");
447
+
448
+ if (resultsList) {
449
+ // eslint-disable-next-line @lwc/lwc/no-inner-html
450
+ resultsList.innerHTML = resultsTemplate;
203
451
  }
452
+
453
+ Coveo.SearchEndpoint.configureCloudV2Endpoint(
454
+ this.coveoOrganizationId,
455
+ this.coveoPublicAccessToken,
456
+ `https://${this.coveoOrganizationId}.org.coveo.com/rest/search`
457
+ );
458
+
459
+ this.attachListeners(this.root);
460
+
461
+ Coveo.TemplateHelpers.registerTemplateHelper(
462
+ "badge",
463
+ buildTemplateHelperBadge
464
+ );
465
+
466
+ Coveo.TemplateHelpers.registerTemplateHelper(
467
+ "breadcrumbs",
468
+ buildTemplateHelperBreadcrumbs
469
+ );
470
+
471
+ Coveo.TemplateHelpers.registerTemplateHelper(
472
+ "metabreadcrumbs",
473
+ buildTemplateHelperMetaBreadcrumbs
474
+ );
475
+
476
+ Coveo.TemplateHelpers.registerTemplateHelper(
477
+ "uriBreadcrumbs",
478
+ buildTemplateHelperUriBreadcrumbs
479
+ );
480
+
481
+ Coveo.TemplateHelpers.registerTemplateHelper(
482
+ "communityBreadcrumbs",
483
+ buildTemplateHelperCommunityBreadcrumbs
484
+ );
485
+
486
+ Coveo.TemplateHelpers.registerTemplateHelper(
487
+ "postedDate",
488
+ buildTemplateHelperPostedDate
489
+ );
490
+
491
+ Coveo.TemplateHelpers.registerTemplateHelper(
492
+ "replies",
493
+ buildTemplateHelperReplies
494
+ );
495
+
496
+ Coveo.TemplateHelpers.registerTemplateHelper(
497
+ "likes",
498
+ buildTemplateHelperLikes
499
+ );
500
+
501
+ Coveo.init(this.root);
204
502
  }
205
503
 
206
504
  renderedCallback() {
207
- // Run search on first render if URL has q and we haven't yet (handles #q=, SPA nav, or late URL)
208
- if (this.hasRunInitialSearch) {
209
- return;
210
- }
211
- const q = getQueryFromUrl();
212
- if (!q) {
213
- return;
214
- }
215
- this.hasRunInitialSearch = true;
216
- if (this._query !== q) {
217
- this._query = q;
218
- }
219
- if (!this.hasSearched && !this.isLoading) {
220
- this.doFetch();
505
+ if (!this.isInitialized) {
506
+ if (Object.prototype.hasOwnProperty.call(window, "Coveo")) {
507
+ this.initializeCoveo();
508
+ } else {
509
+ // eslint-disable-next-line @lwc/lwc/no-document-query
510
+ const script = document.querySelector("script.coveo-script");
511
+ script?.addEventListener("load", () => {
512
+ this.initializeCoveo();
513
+ });
514
+ }
221
515
  }
222
516
  }
517
+
518
+ private trackSearchResults(event: Event, term: string, resultCount: any) {
519
+ trackGTM(event.target!, "custEv_search", {
520
+ search_term: term,
521
+ search_category: "",
522
+ search_type: "site search",
523
+ search_result_count: resultCount
524
+ });
525
+ }
223
526
  }