@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.
- package/lwc.config.json +1 -1
- package/package.json +3 -3
- package/src/modules/dx/agentMiawUi/agentMiawUi.css +4 -0
- package/src/modules/dx/agentMiawUi/agentMiawUi.html +3 -0
- package/src/modules/dx/agentMiawUi/agentMiawUi.ts +167 -0
- package/src/modules/dx/codeBlock/codeBlock.css +7 -0
- package/src/modules/dx/dropdownOption/dropdownOption.css +8 -13
- package/src/modules/dx/footer/footer.ts +55 -32
- package/src/modules/dx/globalHeader/globalHeader.html +2 -2
- package/src/modules/dx/globalHeader/globalHeader.ts +40 -30
- package/src/modules/dx/searchResults/searchResults.css +6 -107
- package/src/modules/dx/searchResults/searchResults.html +91 -87
- package/src/modules/dx/searchResults/searchResults.ts +477 -174
- package/src/modules/dx/sidebar/sidebar.css +0 -1
- package/src/modules/dx/sidebar/sidebar.html +5 -1
- package/src/modules/dx/sidebar/sidebar.ts +29 -44
- package/src/modules/dx/sidebarSearch/sidebarSearch.ts +343 -159
- package/src/modules/dx/sidebarSearchResult/sidebarSearchResult.css +24 -51
- package/src/modules/dx/sidebarSearchResult/sidebarSearchResult.ts +4 -12
- package/src/modules/dx/tree/treeHandler.ts +2 -1
- package/src/modules/dx/treeTile/treeTile.css +4 -0
- package/src/modules/dx/treeTile/treeTile.html +10 -0
- package/src/modules/dx/treeTile/treeTile.ts +4 -0
- package/src/modules/dxUtils/async/async.ts +32 -0
- package/src/modules/dxUtils/data360Search/data360Search.ts +0 -168
|
@@ -1,223 +1,526 @@
|
|
|
1
1
|
import { LightningElement, api, track } from "lwc";
|
|
2
|
-
import
|
|
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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
} from "dxUtils/
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
@
|
|
25
|
-
@
|
|
26
|
-
@
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
36
|
-
if (
|
|
37
|
-
this.
|
|
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
|
|
49
|
-
return this._query;
|
|
50
|
-
}
|
|
219
|
+
private totalResults: number | null = null;
|
|
51
220
|
|
|
52
|
-
private get
|
|
53
|
-
return this.
|
|
221
|
+
private get title() {
|
|
222
|
+
return this.totalResults ? this.totalResults.toLocaleString() : "0";
|
|
54
223
|
}
|
|
55
224
|
|
|
56
|
-
private
|
|
57
|
-
|
|
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
|
|
65
|
-
return this.
|
|
232
|
+
private get coveoAnalyticsEndpoint() {
|
|
233
|
+
return `https://${this.coveoOrganizationId}.analytics.org.coveo.com/rest/ua`;
|
|
66
234
|
}
|
|
67
235
|
|
|
68
|
-
private
|
|
69
|
-
|
|
236
|
+
private updateSearchQuery() {
|
|
237
|
+
Coveo.state(this.root!, "q", this._query);
|
|
70
238
|
}
|
|
71
239
|
|
|
72
|
-
private
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
|
91
|
-
|
|
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
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
search_result_count: resultCount
|
|
176
|
-
});
|
|
326
|
+
const coveoResults =
|
|
327
|
+
this.root!.querySelector(".CoveoResult");
|
|
328
|
+
|
|
329
|
+
return Boolean(coveoResults);
|
|
177
330
|
},
|
|
178
|
-
|
|
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
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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
|
|
443
|
+
private initializeCoveo() {
|
|
444
|
+
this.root = this.template.querySelector(".CoveoSearchInterface")!;
|
|
196
445
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
if (
|
|
200
|
-
|
|
201
|
-
|
|
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
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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
|
}
|