@salesforcedevs/arch-components 1.28.5-node22-1 → 1.29.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/LICENSE +12 -0
- package/lwc.config.json +1 -1
- package/package.json +47 -46
- package/src/modules/arch/browserLocale/browserLocale.ts +104 -0
- package/src/modules/arch/localePreference/localePreference.ts +72 -0
- package/src/modules/arch/searchHybrid/searchHybrid.css +113 -0
- package/src/modules/arch/searchHybrid/searchHybrid.html +164 -0
- package/src/modules/arch/searchHybrid/searchHybrid.ts +299 -0
- package/src/modules/arch/searchList/searchList.css +6 -0
- package/src/modules/arch/searchList/searchList.html +18 -3
- package/src/modules/arch/searchList/searchList.ts +188 -41
- package/src/modules/arch/select/select.html +2 -1
- package/src/modules/arch/content/__fixtures__/index.ts +0 -884
- package/src/modules/arch/content/content.css +0 -643
- package/src/modules/arch/content/content.html +0 -65
- package/src/modules/arch/content/content.stories.js +0 -21
- package/src/modules/arch/content/content.ts +0 -169
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
import { LightningElement, api, track } from "lwc";
|
|
2
|
+
import { track as trackEvent } from "arch/instrumentation";
|
|
3
|
+
import { resolveBrowserLocale } from "arch/browserLocale";
|
|
4
|
+
import { getStoredLanguage, setStoredLanguage } from "arch/localePreference";
|
|
5
|
+
import { decodeHtmlEntities, isSortOption } from "docUtils/searchClient";
|
|
6
|
+
import { SearchController } from "docUtils/searchController";
|
|
7
|
+
import type { SearchViewModel, SortOption } from "docUtils/searchController";
|
|
8
|
+
import type { SearchResult, FacetBucket } from "docUtils/searchClient";
|
|
9
|
+
import { parseSupportedLocales } from "docUtils/searchLocale";
|
|
10
|
+
import type { LocaleDeps, LocaleOption } from "docUtils/searchLocale";
|
|
11
|
+
|
|
12
|
+
const DEFAULT_LANGUAGE = "en-us";
|
|
13
|
+
// The architect site value the search backend filters on. arch/searchList
|
|
14
|
+
// pinned no explicit site to the backend (it filtered by language only against
|
|
15
|
+
// Algolia), so we pin the canonical brand id "architect" here (see decisions).
|
|
16
|
+
const ARCHITECT_SITE = "architect";
|
|
17
|
+
|
|
18
|
+
// View-model shape used by the template — augments the controller's results
|
|
19
|
+
// with already-decoded title/description so the template stays logic-free.
|
|
20
|
+
interface RenderResult extends SearchResult {
|
|
21
|
+
// Iteration key for the template for:each. URL alone is NOT unique — two
|
|
22
|
+
// fused results can share a canonical URL, and a duplicate LWC key throws
|
|
23
|
+
// and fails to render the WHOLE list, so we disambiguate with the index.
|
|
24
|
+
key: string;
|
|
25
|
+
decodedTitle: string;
|
|
26
|
+
decodedDescription: string;
|
|
27
|
+
hasDescription: boolean;
|
|
28
|
+
hasContentType: boolean;
|
|
29
|
+
hasDate: boolean;
|
|
30
|
+
displayDate: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface RenderFacet extends FacetBucket {
|
|
34
|
+
active: boolean;
|
|
35
|
+
label: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface SortChoice {
|
|
39
|
+
id: SortOption;
|
|
40
|
+
displayText: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
interface PageButton {
|
|
44
|
+
page: number;
|
|
45
|
+
label: string;
|
|
46
|
+
isCurrent: boolean;
|
|
47
|
+
ariaCurrent: "page" | null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const SORT_CHOICES: SortChoice[] = [
|
|
51
|
+
{ id: "relevance", displayText: "Relevance" },
|
|
52
|
+
{ id: "date", displayText: "Newest" }
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
export default class SearchHybrid extends LightningElement {
|
|
56
|
+
// The catalog of selectable locales, injected from njk as a JSON string
|
|
57
|
+
// (the doc-framework ALL_LANGUAGES global, sourced from languages.yml).
|
|
58
|
+
@api supportedLocales: string = "";
|
|
59
|
+
// The page's server-side locale, used as a fallback default.
|
|
60
|
+
@api defaultLanguage: string = DEFAULT_LANGUAGE;
|
|
61
|
+
|
|
62
|
+
@track private vm: SearchViewModel | null = null;
|
|
63
|
+
|
|
64
|
+
private controller?: SearchController;
|
|
65
|
+
private unsubscribe?: () => void;
|
|
66
|
+
private localeCatalog: LocaleOption[] = [];
|
|
67
|
+
|
|
68
|
+
connectedCallback() {
|
|
69
|
+
this.localeCatalog = parseSupportedLocales(this.supportedLocales);
|
|
70
|
+
const supportedIds = this.localeCatalog.map((option) => option.id);
|
|
71
|
+
|
|
72
|
+
const deps: LocaleDeps = {
|
|
73
|
+
getStored: () => getStoredLanguage(),
|
|
74
|
+
setStored: (value: string) => setStoredLanguage(value),
|
|
75
|
+
resolveBrowser: (langs, ids) => resolveBrowserLocale(langs, ids)
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const host = this.template.host;
|
|
79
|
+
this.controller = new SearchController({
|
|
80
|
+
catalog: this.localeCatalog,
|
|
81
|
+
defaultLanguage: this.defaultLanguage || DEFAULT_LANGUAGE,
|
|
82
|
+
supportedIds,
|
|
83
|
+
site: ARCHITECT_SITE,
|
|
84
|
+
deps,
|
|
85
|
+
instrument: (event, payload) => {
|
|
86
|
+
trackEvent(host, event, payload);
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
this.unsubscribe = this.controller.subscribe((vm) => {
|
|
91
|
+
this.vm = vm;
|
|
92
|
+
});
|
|
93
|
+
this.vm = this.controller.viewModel;
|
|
94
|
+
this.controller.init();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
disconnectedCallback() {
|
|
98
|
+
this.unsubscribe?.();
|
|
99
|
+
this.controller?.dispose();
|
|
100
|
+
this.controller = undefined;
|
|
101
|
+
this.unsubscribe = undefined;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ---- view-model derived getters (template-facing) -------------------
|
|
105
|
+
|
|
106
|
+
get localeOptions(): LocaleOption[] {
|
|
107
|
+
return this.localeCatalog;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
get showLanguageSelector(): boolean {
|
|
111
|
+
return this.localeCatalog.length > 1;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
get language(): string {
|
|
115
|
+
return this.vm?.locale ?? DEFAULT_LANGUAGE;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
get sortChoices(): SortChoice[] {
|
|
119
|
+
return SORT_CHOICES;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
get sort(): SortOption {
|
|
123
|
+
return this.vm?.sort ?? "relevance";
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
get isLoading(): boolean {
|
|
127
|
+
return this.vm?.status === "loading";
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
get isIdle(): boolean {
|
|
131
|
+
return this.vm?.status === "idle";
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
get isEmpty(): boolean {
|
|
135
|
+
return this.vm?.status === "empty";
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
get isError(): boolean {
|
|
139
|
+
return this.vm?.status === "error";
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
get hasResults(): boolean {
|
|
143
|
+
return this.vm?.status === "results" && this.results.length > 0;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
get errorMessage(): string {
|
|
147
|
+
return this.vm?.errorMessage ?? "An error occurred.";
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
get summaryText(): string {
|
|
151
|
+
const vm = this.vm;
|
|
152
|
+
if (!vm) {
|
|
153
|
+
return "";
|
|
154
|
+
}
|
|
155
|
+
const query = vm.query ? ` for "${decodeHtmlEntities(vm.query)}"` : "";
|
|
156
|
+
if (vm.total === 1) {
|
|
157
|
+
return `1 result${query}`;
|
|
158
|
+
}
|
|
159
|
+
return `${vm.total} results${query}`;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
get results(): RenderResult[] {
|
|
163
|
+
const list = this.vm?.results ?? [];
|
|
164
|
+
return list.map((result, index) => {
|
|
165
|
+
const description = result.description;
|
|
166
|
+
// Derive hasDate from the FORMATTED output, not the raw value, so a
|
|
167
|
+
// non-null but unparseable lastmod never yields an empty <time>.
|
|
168
|
+
const displayDate = this.formatDate(result.lastmod);
|
|
169
|
+
return {
|
|
170
|
+
...result,
|
|
171
|
+
// Index-disambiguated so duplicate URLs can't collide (see key).
|
|
172
|
+
key: `${result.url}-${index}`,
|
|
173
|
+
decodedTitle: decodeHtmlEntities(result.title),
|
|
174
|
+
decodedDescription: decodeHtmlEntities(description),
|
|
175
|
+
hasDescription:
|
|
176
|
+
typeof description === "string" && description.length > 0,
|
|
177
|
+
hasContentType:
|
|
178
|
+
typeof result.contentType === "string" &&
|
|
179
|
+
result.contentType.length > 0,
|
|
180
|
+
hasDate: displayDate.length > 0,
|
|
181
|
+
displayDate
|
|
182
|
+
};
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
get contentTypeFacets(): RenderFacet[] {
|
|
187
|
+
const facets = this.vm?.contentTypeFacets ?? [];
|
|
188
|
+
const selected = this.vm?.selectedType ?? null;
|
|
189
|
+
return facets.map((bucket) => ({
|
|
190
|
+
...bucket,
|
|
191
|
+
active: selected === bucket.value,
|
|
192
|
+
// arch has no dxConstants label map; show the raw facet value.
|
|
193
|
+
label: bucket.value
|
|
194
|
+
}));
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
get showFacets(): boolean {
|
|
198
|
+
return this.contentTypeFacets.length > 0;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// ---- pagination (hand-rolled, null-safe) ----------------------------
|
|
202
|
+
|
|
203
|
+
get totalPages(): number {
|
|
204
|
+
return this.vm?.totalPages ?? 1;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
get currentPage(): number {
|
|
208
|
+
return this.vm?.page ?? 1;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
get showPagination(): boolean {
|
|
212
|
+
return this.hasResults && this.totalPages > 1;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
get isFirstPage(): boolean {
|
|
216
|
+
return this.currentPage <= 1;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
get isLastPage(): boolean {
|
|
220
|
+
return this.currentPage >= this.totalPages;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
get pageButtons(): PageButton[] {
|
|
224
|
+
const total = this.totalPages;
|
|
225
|
+
const current = this.currentPage;
|
|
226
|
+
const buttons: PageButton[] = [];
|
|
227
|
+
for (let page = 1; page <= total; page += 1) {
|
|
228
|
+
buttons.push({
|
|
229
|
+
page,
|
|
230
|
+
label: String(page),
|
|
231
|
+
isCurrent: page === current,
|
|
232
|
+
ariaCurrent: page === current ? "page" : null
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
return buttons;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// ---- event handlers --------------------------------------------------
|
|
239
|
+
|
|
240
|
+
handleLanguageChange(event: CustomEvent) {
|
|
241
|
+
const selected = event.detail;
|
|
242
|
+
if (typeof selected !== "string" || selected === this.language) {
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
this.controller?.setLanguage(selected);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
handleSortChange(event: CustomEvent) {
|
|
249
|
+
const selected = event.detail;
|
|
250
|
+
if (typeof selected === "string" && isSortOption(selected)) {
|
|
251
|
+
this.controller?.setSort(selected);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
handleFacetClick(event: Event) {
|
|
256
|
+
const value = (event.currentTarget as HTMLElement)?.dataset.value;
|
|
257
|
+
if (typeof value === "string") {
|
|
258
|
+
// Disjunctive toggle — the controller clears it if re-selected.
|
|
259
|
+
this.controller?.setType(value);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
handlePrevPage() {
|
|
264
|
+
if (!this.isFirstPage) {
|
|
265
|
+
this.controller?.setPage(this.currentPage - 1);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
handleNextPage() {
|
|
270
|
+
if (!this.isLastPage) {
|
|
271
|
+
this.controller?.setPage(this.currentPage + 1);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
handlePageClick(event: Event) {
|
|
276
|
+
const raw = (event.currentTarget as HTMLElement)?.dataset.page;
|
|
277
|
+
const page = Number(raw);
|
|
278
|
+
if (Number.isFinite(page) && page >= 1) {
|
|
279
|
+
this.controller?.setPage(page);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// ---- helpers ---------------------------------------------------------
|
|
284
|
+
|
|
285
|
+
private formatDate(lastmod: string | null): string {
|
|
286
|
+
if (typeof lastmod !== "string" || lastmod.length === 0) {
|
|
287
|
+
return "";
|
|
288
|
+
}
|
|
289
|
+
const date = new Date(lastmod);
|
|
290
|
+
if (Number.isNaN(date.getTime())) {
|
|
291
|
+
return "";
|
|
292
|
+
}
|
|
293
|
+
return date.toLocaleDateString(undefined, {
|
|
294
|
+
year: "numeric",
|
|
295
|
+
month: "short",
|
|
296
|
+
day: "numeric"
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
}
|
|
@@ -1,8 +1,23 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<arch-section-a background="white" title={titleSearchResults}>
|
|
3
|
-
<template if
|
|
3
|
+
<template lwc:if={showLanguageSelector}>
|
|
4
|
+
<div class="search-language-selector">
|
|
5
|
+
<arch-select
|
|
6
|
+
assistive-text="Search language"
|
|
7
|
+
value={language}
|
|
8
|
+
onchange={handleLanguageChange}
|
|
9
|
+
>
|
|
10
|
+
<template for:each={localeOptions} for:item="locale">
|
|
11
|
+
<option key={locale.id} value={locale.id}>
|
|
12
|
+
{locale.displayText}
|
|
13
|
+
</option>
|
|
14
|
+
</template>
|
|
15
|
+
</arch-select>
|
|
16
|
+
</div>
|
|
17
|
+
</template>
|
|
18
|
+
<template lwc:if={results}>
|
|
4
19
|
<template for:each={results} for:item="result">
|
|
5
|
-
<div key={result.
|
|
20
|
+
<div key={result.url}>
|
|
6
21
|
<arch-summary
|
|
7
22
|
description={result.description}
|
|
8
23
|
compact
|
|
@@ -19,7 +34,7 @@
|
|
|
19
34
|
</div>
|
|
20
35
|
</template>
|
|
21
36
|
</template>
|
|
22
|
-
<template if
|
|
37
|
+
<template lwc:if={showSearchSuggestions}>
|
|
23
38
|
<div style="display: block">
|
|
24
39
|
<center>
|
|
25
40
|
<img
|
|
@@ -1,5 +1,15 @@
|
|
|
1
|
-
import { LightningElement, api
|
|
1
|
+
import { LightningElement, api } from "lwc";
|
|
2
2
|
import { track as trackEvent } from "arch/instrumentation";
|
|
3
|
+
import { resolveBrowserLocale } from "arch/browserLocale";
|
|
4
|
+
import { getStoredLanguage, setStoredLanguage } from "arch/localePreference";
|
|
5
|
+
|
|
6
|
+
const DEFAULT_LANGUAGE = "en-us";
|
|
7
|
+
|
|
8
|
+
interface LocaleOption {
|
|
9
|
+
id: string;
|
|
10
|
+
displayText?: string;
|
|
11
|
+
label?: string;
|
|
12
|
+
}
|
|
3
13
|
|
|
4
14
|
function decodeHtmlEntities(value: unknown): string {
|
|
5
15
|
if (typeof value !== "string" || value.length === 0) {
|
|
@@ -23,52 +33,189 @@ export default class SearchList extends LightningElement {
|
|
|
23
33
|
@api algoliaAppId: string = "";
|
|
24
34
|
@api algoliaApiKey: string = "";
|
|
25
35
|
|
|
26
|
-
|
|
36
|
+
// The catalog of selectable locales, injected from njk as a JSON string
|
|
37
|
+
// (the doc-framework ALL_LANGUAGES global, sourced from languages.yml).
|
|
38
|
+
@api supportedLocales: string = "";
|
|
39
|
+
// The page's server-side locale, used as a fallback default.
|
|
40
|
+
@api defaultLanguage: string = DEFAULT_LANGUAGE;
|
|
41
|
+
|
|
42
|
+
// The effective search language. Resolved at runtime; not an @api so the
|
|
43
|
+
// dropdown can change it and re-query. These are reassigned wholesale (not
|
|
44
|
+
// mutated in place), so plain reactive fields suffice — no @track needed.
|
|
45
|
+
language: string = DEFAULT_LANGUAGE;
|
|
46
|
+
localeOptions: LocaleOption[] = [];
|
|
47
|
+
results: any;
|
|
48
|
+
|
|
49
|
+
private keywords: string = "";
|
|
50
|
+
private abortController?: AbortController;
|
|
27
51
|
|
|
28
52
|
connectedCallback() {
|
|
53
|
+
this.localeOptions = this.parseSupportedLocales();
|
|
54
|
+
this.language = this.resolveInitialLanguage();
|
|
55
|
+
|
|
29
56
|
const params = new URLSearchParams(window.location.search);
|
|
30
|
-
if (params
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
`https://${this.algoliaAppId}-dsn.algolia.net/1/indexes/${this.algoliaIndex}?query=${keywords}&hitsPerPage=20`,
|
|
34
|
-
{
|
|
35
|
-
headers: {
|
|
36
|
-
"X-Algolia-Application-Id": this.algoliaAppId,
|
|
37
|
-
"X-Algolia-API-Key": this.algoliaApiKey
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
)
|
|
41
|
-
.then((result) => {
|
|
42
|
-
return result.json();
|
|
43
|
-
})
|
|
44
|
-
.then((json) => {
|
|
45
|
-
trackEvent(this.template.host, "search_executed", {
|
|
46
|
-
searchTerm: keywords,
|
|
47
|
-
resultCount: json.hits?.length || 0
|
|
48
|
-
});
|
|
49
|
-
if (json.hits && json.hits.length) {
|
|
50
|
-
this.titleSearchResults = `Search results for "${decodeURIComponent(
|
|
51
|
-
keywords
|
|
52
|
-
)}"`;
|
|
53
|
-
this.results = json.hits.map((hit: any) => ({
|
|
54
|
-
...hit,
|
|
55
|
-
description: decodeHtmlEntities(hit.description)
|
|
56
|
-
}));
|
|
57
|
-
this.showSearchSuggestions = false;
|
|
58
|
-
} else {
|
|
59
|
-
this.titleSearchResults = `No results for "${decodeURIComponent(
|
|
60
|
-
keywords
|
|
61
|
-
)}"`;
|
|
62
|
-
this.showSearchSuggestions = true;
|
|
63
|
-
}
|
|
64
|
-
})
|
|
65
|
-
.catch(() => {
|
|
66
|
-
this.titleSearchResults = `An error occured`;
|
|
67
|
-
this.showSearchSuggestions = false;
|
|
68
|
-
});
|
|
57
|
+
if (params.get("keywords")) {
|
|
58
|
+
this.keywords = params.get("keywords") || "";
|
|
59
|
+
this.runSearch();
|
|
69
60
|
} else {
|
|
70
61
|
this.titleSearchResults = "No keywords provided";
|
|
71
62
|
this.showSearchSuggestions = false;
|
|
72
63
|
}
|
|
73
64
|
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Parse the injected locale catalog, restricting to entries with an id.
|
|
68
|
+
* Falls back to an empty list (which hides the dropdown) on bad input.
|
|
69
|
+
*/
|
|
70
|
+
private parseSupportedLocales(): LocaleOption[] {
|
|
71
|
+
if (!this.supportedLocales) {
|
|
72
|
+
return [];
|
|
73
|
+
}
|
|
74
|
+
try {
|
|
75
|
+
const parsed = JSON.parse(this.supportedLocales);
|
|
76
|
+
if (!Array.isArray(parsed)) {
|
|
77
|
+
return [];
|
|
78
|
+
}
|
|
79
|
+
return parsed.filter(
|
|
80
|
+
(entry): entry is LocaleOption =>
|
|
81
|
+
!!entry && typeof entry.id === "string"
|
|
82
|
+
);
|
|
83
|
+
} catch (e) {
|
|
84
|
+
return [];
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
private get supportedIds(): string[] {
|
|
89
|
+
return this.localeOptions.map((option) => option.id);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Resolve the language to search in, most-explicit-wins:
|
|
94
|
+
* 1. ?lang= URL param (shared/bookmarked links)
|
|
95
|
+
* 2. stored preference (consented, from a prior visit)
|
|
96
|
+
* 3. browser preferred language mapped to a supported id
|
|
97
|
+
* 4. the page's server locale, else en-us
|
|
98
|
+
* Any candidate must be in the supported catalog (when one was provided).
|
|
99
|
+
*/
|
|
100
|
+
private resolveInitialLanguage(): string {
|
|
101
|
+
const ids = this.supportedIds;
|
|
102
|
+
const isSupported = (value: string | null | undefined): boolean =>
|
|
103
|
+
!!value && (ids.length === 0 || ids.includes(value));
|
|
104
|
+
|
|
105
|
+
const fromUrl = new URLSearchParams(window.location.search).get("lang");
|
|
106
|
+
if (isSupported(fromUrl)) {
|
|
107
|
+
return fromUrl as string;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const stored = getStoredLanguage();
|
|
111
|
+
if (isSupported(stored)) {
|
|
112
|
+
return stored as string;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (ids.length > 0) {
|
|
116
|
+
const detected = resolveBrowserLocale(
|
|
117
|
+
navigator.languages,
|
|
118
|
+
ids
|
|
119
|
+
);
|
|
120
|
+
if (isSupported(detected)) {
|
|
121
|
+
return detected;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return isSupported(this.defaultLanguage)
|
|
126
|
+
? this.defaultLanguage
|
|
127
|
+
: DEFAULT_LANGUAGE;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Run the Algolia query for the current keywords + language. Aborts any
|
|
132
|
+
* in-flight request so a fast language switch can't render stale results.
|
|
133
|
+
*/
|
|
134
|
+
private runSearch() {
|
|
135
|
+
this.abortController?.abort();
|
|
136
|
+
const controller = new AbortController();
|
|
137
|
+
this.abortController = controller;
|
|
138
|
+
|
|
139
|
+
const keywords = this.keywords;
|
|
140
|
+
// Filter to the chosen language so results are isolated by locale
|
|
141
|
+
// (records are tagged with `language` by the indexer; `language` is
|
|
142
|
+
// registered as a filterOnly facet).
|
|
143
|
+
const languageFilter = encodeURIComponent(`language:${this.language}`);
|
|
144
|
+
fetch(
|
|
145
|
+
`https://${this.algoliaAppId}-dsn.algolia.net/1/indexes/${
|
|
146
|
+
this.algoliaIndex
|
|
147
|
+
}?query=${encodeURIComponent(
|
|
148
|
+
keywords
|
|
149
|
+
)}&filters=${languageFilter}&hitsPerPage=20`,
|
|
150
|
+
{
|
|
151
|
+
headers: {
|
|
152
|
+
"X-Algolia-Application-Id": this.algoliaAppId,
|
|
153
|
+
"X-Algolia-API-Key": this.algoliaApiKey
|
|
154
|
+
},
|
|
155
|
+
signal: controller.signal
|
|
156
|
+
}
|
|
157
|
+
)
|
|
158
|
+
.then((result) => result.json())
|
|
159
|
+
.then((json) => {
|
|
160
|
+
trackEvent(this.template.host, "search_executed", {
|
|
161
|
+
searchTerm: keywords,
|
|
162
|
+
language: this.language,
|
|
163
|
+
resultCount: json.hits?.length || 0
|
|
164
|
+
});
|
|
165
|
+
if (json.hits && json.hits.length) {
|
|
166
|
+
// keywords is already decoded by URLSearchParams.get.
|
|
167
|
+
this.titleSearchResults = `Search results for "${keywords}"`;
|
|
168
|
+
this.results = json.hits.map((hit: any) => ({
|
|
169
|
+
...hit,
|
|
170
|
+
description: decodeHtmlEntities(hit.description)
|
|
171
|
+
}));
|
|
172
|
+
this.showSearchSuggestions = false;
|
|
173
|
+
} else {
|
|
174
|
+
this.titleSearchResults = `No results for "${keywords}"`;
|
|
175
|
+
this.showSearchSuggestions = true;
|
|
176
|
+
}
|
|
177
|
+
})
|
|
178
|
+
.catch((error) => {
|
|
179
|
+
// Ignore aborts from a superseding language switch.
|
|
180
|
+
if (error?.name === "AbortError") {
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
// A 400 here usually means the index has not been reindexed
|
|
184
|
+
// with the filterOnly(language) facet yet (see deploy order).
|
|
185
|
+
console.error("Algolia search request failed:", error);
|
|
186
|
+
this.titleSearchResults = `An error occurred`;
|
|
187
|
+
this.showSearchSuggestions = false;
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Dropdown change: switch language, persist (if consented), reflect in the
|
|
193
|
+
* URL for shareability, and re-run the same search.
|
|
194
|
+
*/
|
|
195
|
+
handleLanguageChange(event: CustomEvent) {
|
|
196
|
+
const selected = event.detail;
|
|
197
|
+
if (!selected || selected === this.language) {
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
this.language = selected;
|
|
201
|
+
setStoredLanguage(selected);
|
|
202
|
+
|
|
203
|
+
const params = new URLSearchParams(window.location.search);
|
|
204
|
+
params.set("lang", selected);
|
|
205
|
+
window.history.replaceState(
|
|
206
|
+
{},
|
|
207
|
+
"",
|
|
208
|
+
`${window.location.pathname}?${params.toString()}${
|
|
209
|
+
window.location.hash
|
|
210
|
+
}`
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
if (this.keywords) {
|
|
214
|
+
this.runSearch();
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
get showLanguageSelector(): boolean {
|
|
219
|
+
return this.localeOptions.length > 1;
|
|
220
|
+
}
|
|
74
221
|
}
|