@salesforcedevs/dx-components 1.32.0-alpha.9 → 1.32.0-docs-content-type-1
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 +2 -1
- package/package.json +47 -47
- 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 +253 -0
- package/src/modules/dx/button/button.css +5 -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/searchHybrid/searchHybrid.css +165 -0
- package/src/modules/dx/searchHybrid/searchHybrid.html +136 -0
- package/src/modules/dx/searchHybrid/searchHybrid.ts +306 -0
- 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/sidebarOld/sidebarOld.html +0 -1
- 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/searchLocalePrefs/searchLocalePrefs.ts +193 -0
- package/LICENSE +0 -12
- package/src/modules/dxUtils/data360Search/data360Search.ts +0 -168
|
@@ -1,31 +1,59 @@
|
|
|
1
1
|
import kebabCase from "lodash.kebabcase";
|
|
2
2
|
import { LightningElement, api } from "lwc";
|
|
3
3
|
|
|
4
|
-
const defaultDomain = "
|
|
4
|
+
const defaultDomain = ""; // empty domain means the header settings will be loaded from the same domain as the current page
|
|
5
5
|
const defaultLocale = "en-us";
|
|
6
6
|
const defaultHeaderSettingsBasePath = "/c/public/header-settings";
|
|
7
|
-
const
|
|
7
|
+
const defaultPropertyTitle = "Developers";
|
|
8
|
+
const headerSettingsJsonKeys = [
|
|
9
|
+
"regionSelectorOverride",
|
|
10
|
+
"contactLinksOverride"
|
|
11
|
+
];
|
|
8
12
|
|
|
9
13
|
export default class DevExNavigation extends LightningElement {
|
|
14
|
+
static renderMode = "light"; // Light DOM currently required due to other elements reading from this element's DOM; we should fix that and use existing nav CSS variables in those other elements (TODO)
|
|
15
|
+
|
|
10
16
|
@api locale: string = defaultLocale;
|
|
11
17
|
@api path: string = defaultHeaderSettingsBasePath;
|
|
12
18
|
@api domain: string = defaultDomain;
|
|
19
|
+
@api propertyTitle: string = defaultPropertyTitle; // used only as a fallback in the "barebones" nav
|
|
20
|
+
|
|
21
|
+
// Fallback basic nav config used when the header settings are not available
|
|
22
|
+
private get barebonesNavConfig() {
|
|
23
|
+
return {
|
|
24
|
+
headerSettings: {
|
|
25
|
+
origin: "",
|
|
26
|
+
contextNavEnabled: "true"
|
|
27
|
+
},
|
|
28
|
+
navItems: {
|
|
29
|
+
variation: "static",
|
|
30
|
+
propertyTitle: {
|
|
31
|
+
label: this.propertyTitle,
|
|
32
|
+
url: "/"
|
|
33
|
+
},
|
|
34
|
+
menuGroup: {}
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
}
|
|
13
38
|
|
|
14
39
|
async connectedCallback(): Promise<void> {
|
|
40
|
+
let navConfig = null;
|
|
15
41
|
try {
|
|
16
|
-
const headerSettingsResponse = await fetch(
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
42
|
+
const headerSettingsResponse = await fetch(
|
|
43
|
+
`${this.domain}${this.path}/${this.locale}.json`,
|
|
44
|
+
{
|
|
45
|
+
headers: {
|
|
46
|
+
"Content-Type": "application/json"
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
);
|
|
21
50
|
if (headerSettingsResponse.ok) {
|
|
22
|
-
|
|
23
|
-
} else {
|
|
24
|
-
this.createBarebonesNav();
|
|
51
|
+
navConfig = await headerSettingsResponse.json();
|
|
25
52
|
}
|
|
26
53
|
} catch (ex) {
|
|
27
|
-
console.error(`
|
|
28
|
-
|
|
54
|
+
console.error(`Header settings error: ${ex}`);
|
|
55
|
+
} finally {
|
|
56
|
+
this.createFullNav(navConfig || this.barebonesNavConfig);
|
|
29
57
|
}
|
|
30
58
|
}
|
|
31
59
|
|
|
@@ -55,22 +83,4 @@ export default class DevExNavigation extends LightningElement {
|
|
|
55
83
|
containerEl.appendChild(hgfNav);
|
|
56
84
|
containerEl.appendChild(hgfNavContext);
|
|
57
85
|
}
|
|
58
|
-
|
|
59
|
-
private createBarebonesNav(): void {
|
|
60
|
-
const hgfNav = this.createGlobalNav({
|
|
61
|
-
origin: "",
|
|
62
|
-
contextNavEnabled: "true",
|
|
63
|
-
});
|
|
64
|
-
const hgfNavContext = this.createContextNav({
|
|
65
|
-
variation: "static",
|
|
66
|
-
propertyTitle: {
|
|
67
|
-
label: "Developers",
|
|
68
|
-
url: "/",
|
|
69
|
-
},
|
|
70
|
-
menuGroup: {},
|
|
71
|
-
});
|
|
72
|
-
const containerEl = this.refs.globalNavContainer as Element;
|
|
73
|
-
containerEl.appendChild(hgfNav);
|
|
74
|
-
containerEl.appendChild(hgfNavContext);
|
|
75
|
-
}
|
|
76
86
|
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
:host {
|
|
2
|
+
display: block;
|
|
3
|
+
font-family: var(--dx-g-font-sans);
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
.dx-search-hybrid {
|
|
7
|
+
background-color: #fafaf9;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/* Header --------------------------------------------------------------- */
|
|
11
|
+
.dx-search-header {
|
|
12
|
+
padding: var(--dx-g-spacing-xl);
|
|
13
|
+
background: linear-gradient(
|
|
14
|
+
77deg,
|
|
15
|
+
var(--dx-g-indigo-vibrant-30) 40.1%,
|
|
16
|
+
var(--dx-g-indigo-vibrant-40) 99.07%
|
|
17
|
+
);
|
|
18
|
+
color: white;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.dx-search-header-container {
|
|
22
|
+
margin: 0 auto;
|
|
23
|
+
max-width: 1200px;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.dx-search-header-title {
|
|
27
|
+
font-size: var(--dx-g-text-lg);
|
|
28
|
+
line-height: var(--dx-g-text-lg);
|
|
29
|
+
font-weight: var(--dx-g-font-normal);
|
|
30
|
+
margin-bottom: var(--dx-g-spacing-md);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.dx-search-header-title-results {
|
|
34
|
+
font-weight: var(--dx-g-font-bold);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.dx-search-header-query {
|
|
38
|
+
font-family: var(--dx-g-font-display);
|
|
39
|
+
font-size: var(--dx-g-text-2xl);
|
|
40
|
+
line-height: var(--dx-g-text-2xl);
|
|
41
|
+
font-weight: var(--dx-g-font-demi);
|
|
42
|
+
margin-top: var(--dx-g-spacing-md);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/* Body + toolbar ------------------------------------------------------- */
|
|
46
|
+
.dx-search-body {
|
|
47
|
+
margin: 0 auto;
|
|
48
|
+
max-width: 1200px;
|
|
49
|
+
padding: var(--dx-g-spacing-xl);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.dx-search-toolbar {
|
|
53
|
+
display: flex;
|
|
54
|
+
flex-wrap: wrap;
|
|
55
|
+
align-items: flex-end;
|
|
56
|
+
justify-content: space-between;
|
|
57
|
+
gap: var(--dx-g-spacing-md);
|
|
58
|
+
margin-bottom: var(--dx-g-spacing-lg);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.dx-facet-row {
|
|
62
|
+
display: flex;
|
|
63
|
+
flex-wrap: wrap;
|
|
64
|
+
gap: var(--dx-g-spacing-sm);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.dx-facet-chip {
|
|
68
|
+
background-color: white;
|
|
69
|
+
color: var(--dx-g-blue-vibrant-20);
|
|
70
|
+
border: 1px solid var(--dx-g-blue-vibrant-50);
|
|
71
|
+
border-radius: 500px;
|
|
72
|
+
padding: 6px 14px;
|
|
73
|
+
font-size: var(--dx-g-text-sm);
|
|
74
|
+
cursor: pointer;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.dx-facet-chip:hover {
|
|
78
|
+
background-color: var(--dx-g-blue-vibrant-95);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.dx-facet-chip_active {
|
|
82
|
+
background-color: var(--dx-g-blue-vibrant-50);
|
|
83
|
+
color: white;
|
|
84
|
+
font-weight: var(--dx-g-font-demi);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.dx-facet-chip-count {
|
|
88
|
+
margin-left: 6px;
|
|
89
|
+
opacity: 0.75;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.dx-search-controls {
|
|
93
|
+
display: flex;
|
|
94
|
+
gap: var(--dx-g-spacing-md);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/* Status states -------------------------------------------------------- */
|
|
98
|
+
.dx-search-status {
|
|
99
|
+
display: flex;
|
|
100
|
+
justify-content: center;
|
|
101
|
+
padding: var(--dx-g-spacing-2xl) 0;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.dx-search-error {
|
|
105
|
+
color: var(--dx-g-red-vibrant-40, #ba0517);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/* Result cards (ported from dx/searchResults visual style) ------------- */
|
|
109
|
+
.dx-result-list {
|
|
110
|
+
list-style: none;
|
|
111
|
+
margin: 0;
|
|
112
|
+
padding: 0;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
.dx-result {
|
|
116
|
+
background: white;
|
|
117
|
+
border-radius: 16px;
|
|
118
|
+
border: 1px solid rgb(24 24 24 / 4%);
|
|
119
|
+
box-shadow: 0 0 2px 0 rgb(24 24 24 / 8%), 0 2px 4px 1px rgb(24 24 24 / 16%);
|
|
120
|
+
padding: 20px;
|
|
121
|
+
margin-bottom: var(--dx-g-spacing-lg);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
.dx-result-info {
|
|
125
|
+
display: flex;
|
|
126
|
+
gap: 12px;
|
|
127
|
+
margin-bottom: var(--dx-g-spacing-smd);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
.dx-badge {
|
|
131
|
+
display: inline-flex;
|
|
132
|
+
align-items: center;
|
|
133
|
+
border-radius: 4px;
|
|
134
|
+
padding: 2px 8px;
|
|
135
|
+
font-size: var(--dx-g-text-xs);
|
|
136
|
+
background-color: var(--dx-g-blue-vibrant-95);
|
|
137
|
+
color: var(--dx-g-blue-vibrant-20);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
.dx-result-title {
|
|
141
|
+
color: var(--dx-g-blue-vibrant-20);
|
|
142
|
+
display: block;
|
|
143
|
+
font-family: var(--dx-g-font-display);
|
|
144
|
+
font-size: var(--dx-g-text-lg);
|
|
145
|
+
margin: 0 0 var(--dx-g-spacing-sm);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
.dx-result-date {
|
|
149
|
+
color: #555;
|
|
150
|
+
font-size: var(--dx-g-text-xs);
|
|
151
|
+
margin: 0 0 var(--dx-g-spacing-xs);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
.dx-result-excerpt {
|
|
155
|
+
color: var(--dx-g-gray-10);
|
|
156
|
+
font-size: 14px;
|
|
157
|
+
line-height: 20px;
|
|
158
|
+
margin: 0;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/* Pagination ----------------------------------------------------------- */
|
|
162
|
+
.dx-pagination-container {
|
|
163
|
+
margin: 0 auto;
|
|
164
|
+
width: fit-content;
|
|
165
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="dx-search-hybrid">
|
|
3
|
+
<!-- Header: result count + query (?q-driven; no input box per D15.3) -->
|
|
4
|
+
<div if:true={hasQuery} class="dx-search-header">
|
|
5
|
+
<div class="dx-search-header-container">
|
|
6
|
+
<p class="dx-search-header-title">
|
|
7
|
+
<span class="dx-search-header-title-results">{total}</span>
|
|
8
|
+
results for
|
|
9
|
+
</p>
|
|
10
|
+
<p class="dx-search-header-query">“{query}”</p>
|
|
11
|
+
</div>
|
|
12
|
+
</div>
|
|
13
|
+
|
|
14
|
+
<div class="dx-search-body">
|
|
15
|
+
<!-- Toolbar: content-type facets + sort + locale selector -->
|
|
16
|
+
<div class="dx-search-toolbar">
|
|
17
|
+
<div
|
|
18
|
+
if:true={hasFacets}
|
|
19
|
+
class="dx-facet-row"
|
|
20
|
+
role="group"
|
|
21
|
+
aria-label="Filter by content type"
|
|
22
|
+
>
|
|
23
|
+
<template for:each={facetChips} for:item="chip">
|
|
24
|
+
<button
|
|
25
|
+
key={chip.value}
|
|
26
|
+
type="button"
|
|
27
|
+
class={chip.cssClass}
|
|
28
|
+
data-value={chip.value}
|
|
29
|
+
aria-pressed={chip.active}
|
|
30
|
+
onclick={handleFacetClick}
|
|
31
|
+
>
|
|
32
|
+
{chip.label}
|
|
33
|
+
<span class="dx-facet-chip-count">
|
|
34
|
+
{chip.count}
|
|
35
|
+
</span>
|
|
36
|
+
</button>
|
|
37
|
+
</template>
|
|
38
|
+
</div>
|
|
39
|
+
|
|
40
|
+
<div class="dx-search-controls">
|
|
41
|
+
<div if:true={showLocaleSelector} class="dx-search-locale">
|
|
42
|
+
<dx-select
|
|
43
|
+
label="Language"
|
|
44
|
+
options={localeSelectOptions}
|
|
45
|
+
value={localeValue}
|
|
46
|
+
onchange={handleLocaleChange}
|
|
47
|
+
></dx-select>
|
|
48
|
+
</div>
|
|
49
|
+
<div class="dx-search-sort">
|
|
50
|
+
<dx-select
|
|
51
|
+
label="Sort by"
|
|
52
|
+
options={sortOptions}
|
|
53
|
+
value={sortValue}
|
|
54
|
+
onchange={handleSortChange}
|
|
55
|
+
></dx-select>
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
</div>
|
|
59
|
+
|
|
60
|
+
<!-- Loading -->
|
|
61
|
+
<template if:true={isLoading}>
|
|
62
|
+
<div class="dx-search-status" aria-live="polite">
|
|
63
|
+
<dx-spinner size="medium" variant="base"></dx-spinner>
|
|
64
|
+
</div>
|
|
65
|
+
</template>
|
|
66
|
+
|
|
67
|
+
<!-- Idle prompt (no query yet) -->
|
|
68
|
+
<template if:true={isIdle}>
|
|
69
|
+
<div class="dx-search-status" aria-live="polite">
|
|
70
|
+
<dx-empty-state
|
|
71
|
+
header="Search the developer site"
|
|
72
|
+
subtitle="Enter a search term to get started."
|
|
73
|
+
variant="alt"
|
|
74
|
+
></dx-empty-state>
|
|
75
|
+
</div>
|
|
76
|
+
</template>
|
|
77
|
+
|
|
78
|
+
<!-- Empty (query ran, no results) -->
|
|
79
|
+
<template if:true={isEmpty}>
|
|
80
|
+
<div class="dx-search-status" aria-live="polite">
|
|
81
|
+
<dx-empty-state
|
|
82
|
+
header="No results found"
|
|
83
|
+
subtitle="Try different keywords or check your spelling."
|
|
84
|
+
variant="alt"
|
|
85
|
+
></dx-empty-state>
|
|
86
|
+
</div>
|
|
87
|
+
</template>
|
|
88
|
+
|
|
89
|
+
<!-- Error -->
|
|
90
|
+
<template if:true={isError}>
|
|
91
|
+
<div class="dx-search-status dx-search-error" role="alert">
|
|
92
|
+
<p>{errorMessage}</p>
|
|
93
|
+
</div>
|
|
94
|
+
</template>
|
|
95
|
+
|
|
96
|
+
<!-- Results -->
|
|
97
|
+
<template if:true={hasResults}>
|
|
98
|
+
<ul class="dx-result-list">
|
|
99
|
+
<template for:each={displayResults} for:item="result">
|
|
100
|
+
<li key={result.key} class="dx-result">
|
|
101
|
+
<div
|
|
102
|
+
if:true={result.hasBadge}
|
|
103
|
+
class="dx-result-info"
|
|
104
|
+
>
|
|
105
|
+
<span class="dx-badge">
|
|
106
|
+
{result.contentTypeLabel}
|
|
107
|
+
</span>
|
|
108
|
+
</div>
|
|
109
|
+
<a href={result.url} class="dx-result-title">
|
|
110
|
+
{result.title}
|
|
111
|
+
</a>
|
|
112
|
+
<p if:true={result.hasDate} class="dx-result-date">
|
|
113
|
+
{result.displayDate}
|
|
114
|
+
</p>
|
|
115
|
+
<p
|
|
116
|
+
if:true={result.hasDescription}
|
|
117
|
+
class="dx-result-excerpt"
|
|
118
|
+
>
|
|
119
|
+
{result.description}
|
|
120
|
+
</p>
|
|
121
|
+
</li>
|
|
122
|
+
</template>
|
|
123
|
+
</ul>
|
|
124
|
+
|
|
125
|
+
<div if:true={showPagination} class="dx-pagination-container">
|
|
126
|
+
<dx-pagination
|
|
127
|
+
current-page={currentPage}
|
|
128
|
+
total-pages={totalPages}
|
|
129
|
+
pages-to-show="5"
|
|
130
|
+
onpagechange={handlePageChange}
|
|
131
|
+
></dx-pagination>
|
|
132
|
+
</div>
|
|
133
|
+
</template>
|
|
134
|
+
</div>
|
|
135
|
+
</div>
|
|
136
|
+
</template>
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
import { LightningElement, api, track } from "lwc";
|
|
2
|
+
import {
|
|
3
|
+
SearchController,
|
|
4
|
+
type SearchViewModel
|
|
5
|
+
} from "docUtils/searchController";
|
|
6
|
+
import type { LocaleOption } from "docUtils/searchLocale";
|
|
7
|
+
import {
|
|
8
|
+
decodeHtmlEntities,
|
|
9
|
+
isSortOption,
|
|
10
|
+
SORT_OPTIONS,
|
|
11
|
+
type SearchResult,
|
|
12
|
+
type SortOption
|
|
13
|
+
} from "docUtils/searchClient";
|
|
14
|
+
import { searchLocalePrefs } from "dxUtils/searchLocalePrefs";
|
|
15
|
+
import { track as trackGTM } from "dxUtils/analytics";
|
|
16
|
+
import { CONTENT_TYPE_LABELS } from "dxConstants/contentTypes";
|
|
17
|
+
import { parseSupportedLocales } from "docUtils/searchLocale";
|
|
18
|
+
|
|
19
|
+
// The developer site pins itself as the "developer" site in the unified
|
|
20
|
+
// search corpus (matches the dx search call; the SSG/WordPress developer docs
|
|
21
|
+
// are ingested under this site value).
|
|
22
|
+
const DX_SITE = "developer";
|
|
23
|
+
|
|
24
|
+
const SORT_LABELS: Record<SortOption, string> = {
|
|
25
|
+
relevance: "Relevance",
|
|
26
|
+
date: "Most recent"
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
interface DisplayResult {
|
|
30
|
+
key: string;
|
|
31
|
+
url: string;
|
|
32
|
+
title: string;
|
|
33
|
+
description: string | null;
|
|
34
|
+
hasDescription: boolean;
|
|
35
|
+
contentTypeLabel: string | null;
|
|
36
|
+
hasBadge: boolean;
|
|
37
|
+
displayDate: string;
|
|
38
|
+
hasDate: boolean;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
interface FacetChip {
|
|
42
|
+
value: string;
|
|
43
|
+
label: string;
|
|
44
|
+
count: number;
|
|
45
|
+
active: boolean;
|
|
46
|
+
cssClass: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
interface SortSelectOption {
|
|
50
|
+
value: string;
|
|
51
|
+
label: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export default class SearchHybrid extends LightningElement {
|
|
55
|
+
/**
|
|
56
|
+
* JSON string of the supported locale catalog (LocaleOption[]). Injected by
|
|
57
|
+
* the host page. Drives both the controller catalog and the locale selector.
|
|
58
|
+
*/
|
|
59
|
+
@api locales?: string;
|
|
60
|
+
|
|
61
|
+
/** The page's server locale, used as the default search language. */
|
|
62
|
+
@api defaultLanguage: string = "en-us";
|
|
63
|
+
|
|
64
|
+
/** Results per page. */
|
|
65
|
+
@api limit?: number;
|
|
66
|
+
|
|
67
|
+
/** Override the /api/search base path (mostly for tests / storybook). */
|
|
68
|
+
@api basePath?: string;
|
|
69
|
+
|
|
70
|
+
@track private vm: SearchViewModel | null = null;
|
|
71
|
+
|
|
72
|
+
private controller: SearchController | null = null;
|
|
73
|
+
private unsubscribe: (() => void) | null = null;
|
|
74
|
+
|
|
75
|
+
connectedCallback(): void {
|
|
76
|
+
const catalog = parseSupportedLocales(this.locales ?? "");
|
|
77
|
+
const supportedIds = catalog.map((l: LocaleOption) => l.id);
|
|
78
|
+
|
|
79
|
+
this.controller = new SearchController({
|
|
80
|
+
catalog,
|
|
81
|
+
defaultLanguage: this.defaultLanguage,
|
|
82
|
+
supportedIds,
|
|
83
|
+
site: DX_SITE,
|
|
84
|
+
limit: this.limit,
|
|
85
|
+
basePath: this.basePath,
|
|
86
|
+
deps: searchLocalePrefs,
|
|
87
|
+
instrument: (event, payload) => {
|
|
88
|
+
// dxUtils/analytics.track requires an EventTarget; route through
|
|
89
|
+
// the host element so the instrumentation listener can capture it.
|
|
90
|
+
trackGTM(this as unknown as EventTarget, event, payload);
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
this.unsubscribe = this.controller.subscribe((vm) => {
|
|
95
|
+
this.vm = vm;
|
|
96
|
+
});
|
|
97
|
+
this.vm = this.controller.viewModel;
|
|
98
|
+
this.controller.init();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
disconnectedCallback(): void {
|
|
102
|
+
this.unsubscribe?.();
|
|
103
|
+
this.unsubscribe = null;
|
|
104
|
+
this.controller?.dispose();
|
|
105
|
+
this.controller = null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ---- status flags -----------------------------------------------------
|
|
109
|
+
|
|
110
|
+
get isIdle(): boolean {
|
|
111
|
+
return this.vm?.status === "idle";
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
get isLoading(): boolean {
|
|
115
|
+
return this.vm?.status === "loading";
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
get isError(): boolean {
|
|
119
|
+
return this.vm?.status === "error";
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
get isEmpty(): boolean {
|
|
123
|
+
return this.vm?.status === "empty";
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
get hasResults(): boolean {
|
|
127
|
+
return this.vm?.status === "results";
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
get errorMessage(): string {
|
|
131
|
+
return this.vm?.errorMessage || "Something went wrong. Try again.";
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
get query(): string {
|
|
135
|
+
return this.vm?.query ?? "";
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
get hasQuery(): boolean {
|
|
139
|
+
return this.query !== "";
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
get total(): number {
|
|
143
|
+
return this.vm?.total ?? 0;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// ---- results ----------------------------------------------------------
|
|
147
|
+
|
|
148
|
+
get displayResults(): DisplayResult[] {
|
|
149
|
+
const results = this.vm?.results ?? [];
|
|
150
|
+
return results.map((r: SearchResult, i: number) => {
|
|
151
|
+
const description =
|
|
152
|
+
r.description != null && r.description !== ""
|
|
153
|
+
? decodeHtmlEntities(r.description)
|
|
154
|
+
: null;
|
|
155
|
+
const label = this.contentTypeLabel(r.contentType);
|
|
156
|
+
const displayDate = this.formatDate(r.lastmod);
|
|
157
|
+
return {
|
|
158
|
+
key: `${r.url}-${i}`,
|
|
159
|
+
url: r.url,
|
|
160
|
+
title: decodeHtmlEntities(r.title),
|
|
161
|
+
description,
|
|
162
|
+
hasDescription: description !== null,
|
|
163
|
+
contentTypeLabel: label,
|
|
164
|
+
hasBadge: label !== null,
|
|
165
|
+
displayDate,
|
|
166
|
+
// Tie hasDate to the FORMATTED output so a non-null but
|
|
167
|
+
// unparseable lastmod renders no date element (no raw ISO).
|
|
168
|
+
hasDate: displayDate.length > 0
|
|
169
|
+
};
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Null/NaN-safe date formatter (mirrors arch/searchHybrid). lastmod is null
|
|
175
|
+
* for the entire SSG corpus (the common case → ""); the WordPress portion
|
|
176
|
+
* of the developer corpus emits an ISO timestamp, which we render as a
|
|
177
|
+
* human date rather than the raw machine string.
|
|
178
|
+
*/
|
|
179
|
+
private formatDate(lastmod: string | null): string {
|
|
180
|
+
if (typeof lastmod !== "string" || lastmod.length === 0) {
|
|
181
|
+
return "";
|
|
182
|
+
}
|
|
183
|
+
const date = new Date(lastmod);
|
|
184
|
+
if (Number.isNaN(date.getTime())) {
|
|
185
|
+
return "";
|
|
186
|
+
}
|
|
187
|
+
return date.toLocaleDateString(undefined, {
|
|
188
|
+
year: "numeric",
|
|
189
|
+
month: "short",
|
|
190
|
+
day: "numeric"
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Null-safe content-type label lookup. contentType is null for the entire
|
|
196
|
+
* SSG corpus and is the common case; unknown values fall back to the raw
|
|
197
|
+
* value so we never render an empty badge or crash.
|
|
198
|
+
*/
|
|
199
|
+
private contentTypeLabel(contentType: string | null): string | null {
|
|
200
|
+
if (contentType == null || contentType === "") {
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
const labels = CONTENT_TYPE_LABELS as Record<string, string>;
|
|
204
|
+
return labels[contentType] ?? contentType;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// ---- content-type facet chips (disjunctive) ---------------------------
|
|
208
|
+
|
|
209
|
+
get facetChips(): FacetChip[] {
|
|
210
|
+
const facets = this.vm?.contentTypeFacets ?? [];
|
|
211
|
+
const selected = this.vm?.selectedType ?? null;
|
|
212
|
+
return facets
|
|
213
|
+
.filter((b) => b.value != null && b.value !== "")
|
|
214
|
+
.map((b) => {
|
|
215
|
+
const active = selected === b.value;
|
|
216
|
+
return {
|
|
217
|
+
value: b.value,
|
|
218
|
+
label: this.contentTypeLabel(b.value) ?? b.value,
|
|
219
|
+
count: b.count,
|
|
220
|
+
active,
|
|
221
|
+
cssClass: active
|
|
222
|
+
? "dx-facet-chip dx-facet-chip_active"
|
|
223
|
+
: "dx-facet-chip"
|
|
224
|
+
};
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
get hasFacets(): boolean {
|
|
229
|
+
return this.facetChips.length > 0;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// ---- sort + locale select options -------------------------------------
|
|
233
|
+
|
|
234
|
+
get sortOptions(): SortSelectOption[] {
|
|
235
|
+
return SORT_OPTIONS.map((value) => ({
|
|
236
|
+
value,
|
|
237
|
+
label: SORT_LABELS[value]
|
|
238
|
+
}));
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
get sortValue(): string {
|
|
242
|
+
return this.vm?.sort ?? "relevance";
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
get showLocaleSelector(): boolean {
|
|
246
|
+
return this.controller?.showLocaleSelector ?? false;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
get localeSelectOptions(): SortSelectOption[] {
|
|
250
|
+
const options = this.controller?.localeOptions ?? [];
|
|
251
|
+
return options.map((l: LocaleOption) => ({
|
|
252
|
+
value: l.id,
|
|
253
|
+
label: l.displayText ?? l.label ?? l.id
|
|
254
|
+
}));
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
get localeValue(): string {
|
|
258
|
+
return this.vm?.locale ?? this.defaultLanguage;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// ---- pagination -------------------------------------------------------
|
|
262
|
+
|
|
263
|
+
get currentPage(): string {
|
|
264
|
+
return String(this.vm?.page ?? 1);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
get totalPages(): string {
|
|
268
|
+
return String(this.vm?.totalPages ?? 1);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
get showPagination(): boolean {
|
|
272
|
+
return (this.vm?.totalPages ?? 1) > 1;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// ---- event handlers ---------------------------------------------------
|
|
276
|
+
|
|
277
|
+
handleSortChange(event: CustomEvent<{ value: string }>): void {
|
|
278
|
+
const value = event.detail?.value;
|
|
279
|
+
if (isSortOption(value)) {
|
|
280
|
+
this.controller?.setSort(value);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
handleLocaleChange(event: CustomEvent<{ value: string }>): void {
|
|
285
|
+
const value = event.detail?.value;
|
|
286
|
+
if (value) {
|
|
287
|
+
this.controller?.setLanguage(value);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
handleFacetClick(event: Event): void {
|
|
292
|
+
const value = (event.currentTarget as HTMLElement)?.dataset?.value;
|
|
293
|
+
if (value) {
|
|
294
|
+
// Disjunctive toggle: the controller clears it if it is already
|
|
295
|
+
// active.
|
|
296
|
+
this.controller?.setType(value);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
handlePageChange(event: CustomEvent<number>): void {
|
|
301
|
+
const page = event.detail;
|
|
302
|
+
if (typeof page === "number" && page >= 1) {
|
|
303
|
+
this.controller?.setPage(page);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|