@salesforcedevs/dx-components 1.41.0 → 1.42.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,526 +1,253 @@
1
1
  import { LightningElement, api, track } from "lwc";
2
- import type * as CoveoSDK from "coveo-search-ui";
3
- import { DateTime } from "luxon";
2
+ import debounce from "debounce";
4
3
  import { track as trackGTM } from "dxUtils/analytics";
5
4
  import {
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;
5
+ type OpenSearchResultItem,
6
+ fetchSearch,
7
+ resultHref,
8
+ getQueryFromUrl
9
+ } from "dxUtils/openSearch";
10
+
11
+ const SEARCH_DEBOUNCE_DELAY = 400;
12
+
13
+ /** Normalized result for template (with href, uniqueId, openInNewTab) */
14
+ interface SearchResultDisplay {
15
+ title: string;
16
+ href: string;
17
+ matchedText: string;
18
+ uniqueId: string;
19
+ resultIndex: number;
20
+ openInNewTab: string | undefined;
21
+ rel: string | undefined;
28
22
  }
29
23
 
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 };
43
- }
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.
198
24
  export default class SearchResults extends LightningElement {
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 = "";
25
+ @track private _query = "";
26
+ @track private results: SearchResultDisplay[] = [];
27
+ @track private isLoading = false;
28
+ @track private hasSearched = false;
29
+ @track private didTrackThisSearch = false;
30
+ /** Bumped for every search and on clear; only the latest request may update state. */
31
+ private searchRequestId = 0;
207
32
 
208
33
  @api
209
34
  get searchQuery() {
210
35
  return this._query;
211
36
  }
212
37
  set searchQuery(q: string) {
213
- this._query = q;
214
- if (this.isInitialized && this._query !== "") {
215
- this.updateSearchQuery();
38
+ const next = q ?? "";
39
+ if (next !== this._query) {
40
+ this._query = next;
41
+ this.syncUrlToQuery();
42
+ if (this._query.trim()) {
43
+ this.runSearch();
44
+ } else {
45
+ this.clearResults();
46
+ }
216
47
  }
217
48
  }
218
49
 
219
- private totalResults: number | null = null;
220
-
221
- private get title() {
222
- return this.totalResults ? this.totalResults.toLocaleString() : "0";
50
+ private clearResults() {
51
+ this.searchRequestId++;
52
+ this.results = [];
53
+ this.hasSearched = false;
54
+ this.isLoading = false;
223
55
  }
224
56
 
225
- private query: string = "";
226
- private hasFilters: boolean = false;
227
-
228
- private get hasQuery(): boolean {
229
- return this.query !== "";
57
+ private get query(): string {
58
+ return this._query;
230
59
  }
231
60
 
232
- private get coveoAnalyticsEndpoint() {
233
- return `https://${this.coveoOrganizationId}.analytics.org.coveo.com/rest/ua`;
61
+ private get totalResults(): number {
62
+ return this.results?.length ?? 0;
234
63
  }
235
64
 
236
- private updateSearchQuery() {
237
- Coveo.state(this.root!, "q", this._query);
65
+ private get resultCountLabel(): string {
66
+ return this.totalResults.toLocaleString();
238
67
  }
239
68
 
240
- private clearFilters() {
241
- const BreadcrumbManager = Coveo.get(
242
- this.root!.querySelector(".CoveoBreadcrumb") as HTMLElement
243
- ) as any;
244
- BreadcrumbManager.clearBreadcrumbs();
69
+ private get hasQuery(): boolean {
70
+ return this.query !== "";
245
71
  }
246
72
 
247
- private currentPage: number = 25;
248
- private totalPages: number = 1;
249
-
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;
73
+ private get hasResults(): boolean {
74
+ return this.results.length > 0;
260
75
  }
261
76
 
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
- }
77
+ private get showNoResults(): boolean {
78
+ return this.hasSearched && !this.isLoading && !this.hasResults;
79
+ }
283
80
 
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
- }
81
+ private syncUrlToQuery() {
82
+ const url = new URL(window.location.href);
83
+ if (this._query.trim()) {
84
+ url.searchParams.set("q", this._query.trim());
85
+ } else {
86
+ url.searchParams.delete("q");
87
+ }
88
+ const newHref = url.pathname + url.search + url.hash;
89
+ if (
90
+ window.location.pathname +
91
+ window.location.search +
92
+ window.location.hash !==
93
+ newHref
94
+ ) {
95
+ window.history.replaceState(null, "", newHref);
96
+ }
97
+ }
290
98
 
291
- this.isInitialized = true;
99
+ private async doFetch() {
100
+ const requestId = ++this.searchRequestId;
101
+ const query = this._query.trim();
102
+ if (!query) {
103
+ this.results = [];
104
+ this.isLoading = false;
105
+ return;
106
+ }
107
+ this.isLoading = true;
108
+ this.hasSearched = true;
109
+ this.didTrackThisSearch = false;
110
+ try {
111
+ const raw = await fetchSearch(query);
112
+ if (requestId !== this.searchRequestId) {
113
+ return;
292
114
  }
293
- );
294
-
295
- Coveo.$$(root).on(Coveo.QueryEvents.querySuccess, (event: any) => {
296
- const { currentPage, numberOfPages } = getPaginationState(
297
- event.detail
115
+ this.results = raw.map(
116
+ (item: OpenSearchResultItem, index: number) =>
117
+ this.normalizeResult(item, index)
298
118
  );
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
- });
308
- });
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
- });
119
+ this.trackSearchResultsOnce(query, this.results.length);
120
+ } catch (err) {
121
+ if (requestId !== this.searchRequestId) {
122
+ return;
319
123
  }
124
+ console.error("OpenSearch Search request failed", err);
125
+ this.results = [];
126
+ }
127
+ if (requestId === this.searchRequestId) {
128
+ this.isLoading = false;
129
+ }
130
+ }
131
+
132
+ private runSearch = debounce(() => {
133
+ this.doFetch();
134
+ }, SEARCH_DEBOUNCE_DELAY);
135
+
136
+ private normalizeResult(
137
+ item: OpenSearchResultItem,
138
+ index: number
139
+ ): SearchResultDisplay {
140
+ const href = resultHref(item);
141
+ const isExternal =
142
+ href.startsWith("http") &&
143
+ !href.includes("developer.salesforce.com") &&
144
+ !href.includes("developer-website-s.herokuapp.com");
145
+ return {
146
+ title: item.title ?? "",
147
+ href,
148
+ matchedText: item.matchedText ?? "",
149
+ // Several hits can share a page URL, so the index keeps keys unique.
150
+ uniqueId: `${index}:${href}`,
151
+ resultIndex: index + 1,
152
+ openInNewTab: isExternal ? "_blank" : undefined,
153
+ rel: isExternal ? "noopener noreferrer" : undefined
154
+ };
155
+ }
156
+
157
+ private onSearchResultClick(e: MouseEvent) {
158
+ const anchor = e.currentTarget as HTMLAnchorElement;
159
+ const index = Number(anchor.dataset.index ?? "0");
160
+ const title = anchor.dataset.title ?? "";
161
+ const href = anchor.href ?? "";
162
+ trackGTM(anchor, "custEv_scopedSearchlinkClick", {
163
+ click_text: title,
164
+ click_url: href,
165
+ element_title: title,
166
+ element_type: "link",
167
+ content_category: "documentation",
168
+ search_term: this.query,
169
+ search_result_position: index
320
170
  });
171
+ }
321
172
 
322
- Coveo.$$(root).on(Coveo.QueryEvents.deferredQuerySuccess, async () => {
323
- // wait specified time to ensure breadcrumbs are rendered before processing them
324
- await pollUntil(
173
+ private trackSearchResultsOnce(term: string, resultCount: number) {
174
+ if (this.didTrackThisSearch) {
175
+ return;
176
+ }
177
+ this.didTrackThisSearch = true;
178
+ if (document.readyState === "complete") {
179
+ trackGTM(this.template.host, "custEv_search", {
180
+ search_term: term,
181
+ search_category: "",
182
+ search_type: "site search",
183
+ search_result_count: resultCount
184
+ });
185
+ } else {
186
+ window.addEventListener(
187
+ "load",
325
188
  () => {
326
- const coveoResults =
327
- this.root!.querySelector(".CoveoResult");
328
-
329
- return Boolean(coveoResults);
189
+ trackGTM(this.template.host, "custEv_search", {
190
+ search_term: term,
191
+ search_category: "",
192
+ search_type: "site search",
193
+ search_result_count: resultCount
194
+ });
330
195
  },
331
- 20,
332
- 1000
196
+ { once: true }
333
197
  );
334
-
335
- this.processBreadcrumbs(root);
336
-
337
- window.onresize = () => this.processBreadcrumbs(root);
338
- });
198
+ }
339
199
  }
340
200
 
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
- }
201
+ private onSearchInputChange(e: CustomEvent) {
202
+ const value = (e.detail as string) ?? "";
203
+ this._query = value;
204
+ this.syncUrlToQuery();
205
+ if (value.trim()) {
206
+ this.runSearch();
207
+ } else {
208
+ this.clearResults();
373
209
  }
210
+ }
374
211
 
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
- );
212
+ private get statusMessage(): string {
213
+ if (this.isLoading) {
214
+ return "Loading search results";
430
215
  }
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);
216
+ if (this.showNoResults) {
217
+ return `No results found for ${this.query}`;
438
218
  }
439
-
440
- this.formatBreadcrumbs(breadcrumbs);
219
+ if (this.hasSearched && this.hasResults) {
220
+ return `${this.resultCountLabel} results for ${this.query}`;
221
+ }
222
+ return "";
441
223
  }
442
224
 
443
- private initializeCoveo() {
444
- this.root = this.template.querySelector(".CoveoSearchInterface")!;
225
+ private hasRunInitialSearch = false;
445
226
 
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;
227
+ connectedCallback() {
228
+ const q = getQueryFromUrl();
229
+ if (q) {
230
+ this._query = q;
231
+ this.hasRunInitialSearch = true;
232
+ this.doFetch();
451
233
  }
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);
502
234
  }
503
235
 
504
236
  renderedCallback() {
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
- }
237
+ // Run search on first render if URL has q and we haven't yet (handles #q=, SPA nav, or late URL)
238
+ if (this.hasRunInitialSearch) {
239
+ return;
240
+ }
241
+ const q = getQueryFromUrl();
242
+ if (!q) {
243
+ return;
244
+ }
245
+ this.hasRunInitialSearch = true;
246
+ if (this._query !== q) {
247
+ this._query = q;
248
+ }
249
+ if (!this.hasSearched && !this.isLoading) {
250
+ this.doFetch();
515
251
  }
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
252
  }
526
253
  }