@salesforcedevs/dx-components 1.40.11 → 1.42.0-final-test
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 +4 -2
- package/package.json +46 -47
- package/src/modules/dx/button/button.css +2 -2
- package/src/modules/dx/dropdown/dropdown.css +29 -1
- package/src/modules/dx/dropdown/dropdown.html +1 -1
- package/src/modules/dx/dropdown/dropdown.ts +16 -1
- package/src/modules/dx/dropdownOption/dropdownOption.css +30 -6
- package/src/modules/dx/filterMenu/filterMenu.css +19 -0
- package/src/modules/dx/filterMenu/filterMenu.html +17 -2
- package/src/modules/dx/filterMenu/filterMenu.ts +43 -0
- package/src/modules/dx/popover/popover.ts +119 -7
- package/src/modules/dx/searchHybrid/searchHybrid.css +56 -61
- package/src/modules/dx/searchHybrid/searchHybrid.html +133 -112
- package/src/modules/dx/searchHybrid/searchHybrid.mock.ts +244 -0
- package/src/modules/dx/searchHybrid/searchHybrid.ts +81 -26
- package/src/modules/dx/searchResults/searchResults.css +119 -6
- package/src/modules/dx/searchResults/searchResults.html +89 -91
- package/src/modules/dx/searchResults/searchResults.ts +200 -473
- package/src/modules/dx/sidebar/sidebar.css +1 -0
- package/src/modules/dx/sidebar/sidebar.html +2 -5
- package/src/modules/dx/sidebar/sidebar.ts +44 -29
- package/src/modules/dx/sidebarOld/sidebarOld.html +1 -0
- package/src/modules/dx/sidebarSearch/sidebarSearch.ts +163 -342
- package/src/modules/dx/sidebarSearchResult/sidebarSearchResult.css +61 -23
- package/src/modules/dx/sidebarSearchResult/sidebarSearchResult.ts +12 -4
- package/src/modules/dx/tree/tree.html +1 -0
- package/src/modules/dx/tree/tree.ts +5 -1
- package/src/modules/dx/tree/treeHandler.ts +92 -13
- package/src/modules/dx/treeItem/treeItem.css +41 -0
- package/src/modules/dx/treeItem/treeItem.html +17 -2
- package/src/modules/dx/treeItem/treeItem.ts +109 -11
- package/src/modules/dx/treeTile/treeTile.css +57 -0
- package/src/modules/dx/treeTile/treeTile.ts +12 -3
- package/src/modules/dx/typeBadge/typeBadge.ts +23 -2
- package/src/modules/dx/versionPicker/versionPicker.css +248 -0
- package/src/modules/dx/versionPicker/versionPicker.html +73 -0
- package/src/modules/dx/versionPicker/versionPicker.ts +157 -0
- package/src/modules/dxHelpers/commonSidebar/commonSidebar.css +3 -3
- package/src/modules/dxHelpers/commonTreeItem/commonTreeItem.css +4 -0
- package/src/modules/dxHelpers/typeBadge/typeBadge.css +22 -0
- package/src/modules/dxUtils/openSearch/openSearch.ts +343 -0
- package/src/modules/dxUtils/topicVersion/topicVersion.ts +758 -0
- package/LICENSE +0 -12
|
@@ -1,526 +1,253 @@
|
|
|
1
1
|
import { LightningElement, api, track } from "lwc";
|
|
2
|
-
import
|
|
3
|
-
import { DateTime } from "luxon";
|
|
2
|
+
import debounce from "debounce";
|
|
4
3
|
import { track as trackGTM } from "dxUtils/analytics";
|
|
5
4
|
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
@
|
|
200
|
-
@
|
|
201
|
-
@
|
|
202
|
-
|
|
203
|
-
private
|
|
204
|
-
|
|
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
|
-
|
|
214
|
-
if (
|
|
215
|
-
this.
|
|
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
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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
|
-
|
|
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
|
|
233
|
-
return
|
|
61
|
+
private get totalResults(): number {
|
|
62
|
+
return this.results?.length ?? 0;
|
|
234
63
|
}
|
|
235
64
|
|
|
236
|
-
private
|
|
237
|
-
|
|
65
|
+
private get resultCountLabel(): string {
|
|
66
|
+
return this.totalResults.toLocaleString();
|
|
238
67
|
}
|
|
239
68
|
|
|
240
|
-
private
|
|
241
|
-
|
|
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
|
|
248
|
-
|
|
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
|
|
263
|
-
|
|
264
|
-
|
|
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
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
300
|
-
|
|
301
|
-
this.
|
|
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
|
-
|
|
323
|
-
|
|
324
|
-
|
|
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
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
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
|
-
|
|
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
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
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
|
-
|
|
376
|
-
|
|
377
|
-
|
|
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
|
-
|
|
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
|
-
|
|
219
|
+
if (this.hasSearched && this.hasResults) {
|
|
220
|
+
return `${this.resultCountLabel} results for ${this.query}`;
|
|
221
|
+
}
|
|
222
|
+
return "";
|
|
441
223
|
}
|
|
442
224
|
|
|
443
|
-
private
|
|
444
|
-
this.root = this.template.querySelector(".CoveoSearchInterface")!;
|
|
225
|
+
private hasRunInitialSearch = false;
|
|
445
226
|
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
if (
|
|
449
|
-
|
|
450
|
-
|
|
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 (
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
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
|
}
|