@md-plugins/search-ui 0.1.0-rc.9
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.md +21 -0
- package/README.md +97 -0
- package/dist/index.d.mts +133 -0
- package/dist/index.d.ts +133 -0
- package/dist/index.mjs +991 -0
- package/package.json +46 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,991 @@
|
|
|
1
|
+
const whitespaceRE = /\s+/g;
|
|
2
|
+
const htmlTagRE = /<[^>]+>/g;
|
|
3
|
+
const markdownImageRE = /!\[([^\]]*)\]\([^)]*\)/g;
|
|
4
|
+
const markdownLinkRE = /\[([^\]]+)\]\([^)]*\)/g;
|
|
5
|
+
const markdownReferenceLinkRE = /\[([^\]]+)\]\[[^\]]*\]/g;
|
|
6
|
+
const markdownInlineCodeRE = /`([^`]+)`/g;
|
|
7
|
+
const markdownStrongRE = /(\*\*|__)(.*?)\1/g;
|
|
8
|
+
const markdownEmphasisRE = /(\*|_)(.*?)\1/g;
|
|
9
|
+
const markdownStrikeRE = /~~(.*?)~~/g;
|
|
10
|
+
const markdownHeadingMarkerRE = /^#{1,6}\s+/gm;
|
|
11
|
+
const markdownBlockquoteMarkerRE = /^>\s?/gm;
|
|
12
|
+
const markdownListMarkerRE = /^\s*(?:[-*+]|\d+\.)\s+/gm;
|
|
13
|
+
const markdownReferenceDefinitionRE = /^\[[^\]]+\]:\s+\S+.*$/gm;
|
|
14
|
+
const spaceBeforePunctuationRE = /\s+([,.;:!?])/g;
|
|
15
|
+
function stripMarkdownSyntax(value) {
|
|
16
|
+
return value.replace(markdownReferenceDefinitionRE, " ").replace(markdownImageRE, "$1").replace(markdownLinkRE, "$1").replace(markdownReferenceLinkRE, "$1").replace(markdownInlineCodeRE, "$1").replace(markdownStrongRE, "$2").replace(markdownEmphasisRE, "$2").replace(markdownStrikeRE, "$1").replace(markdownHeadingMarkerRE, " ").replace(markdownBlockquoteMarkerRE, " ").replace(markdownListMarkerRE, " ").replace(htmlTagRE, " ").replaceAll("|", " ").replace(spaceBeforePunctuationRE, "$1");
|
|
17
|
+
}
|
|
18
|
+
function normalizeSearchQuery(value) {
|
|
19
|
+
return value.toLocaleLowerCase().replace(whitespaceRE, " ").trim();
|
|
20
|
+
}
|
|
21
|
+
function createSearchTerms(query) {
|
|
22
|
+
return Array.from(new Set(normalizeSearchQuery(query).split(" ").filter(Boolean)));
|
|
23
|
+
}
|
|
24
|
+
function normalizeSearchableText(value) {
|
|
25
|
+
return stripMarkdownSyntax(String(value ?? "")).toLocaleLowerCase().replace(whitespaceRE, " ").trim();
|
|
26
|
+
}
|
|
27
|
+
function createSearchSnippet(content, terms, length = 140) {
|
|
28
|
+
const normalizedContent = stripMarkdownSyntax(content).replace(whitespaceRE, " ").trim();
|
|
29
|
+
if (normalizedContent.length <= length) {
|
|
30
|
+
return normalizedContent;
|
|
31
|
+
}
|
|
32
|
+
const lowerContent = normalizedContent.toLocaleLowerCase();
|
|
33
|
+
const firstMatch = terms.map((term) => lowerContent.indexOf(term)).filter((index) => index >= 0).sort((a, b) => a - b)[0];
|
|
34
|
+
if (firstMatch === void 0) {
|
|
35
|
+
return `${normalizedContent.slice(0, length - 1).trim()}\u2026`;
|
|
36
|
+
}
|
|
37
|
+
const start = Math.max(firstMatch - Math.floor(length / 3), 0);
|
|
38
|
+
const end = Math.min(start + length, normalizedContent.length);
|
|
39
|
+
const prefix = start > 0 ? "\u2026" : "";
|
|
40
|
+
const suffix = end < normalizedContent.length ? "\u2026" : "";
|
|
41
|
+
return `${prefix}${normalizedContent.slice(start, end).trim()}${suffix}`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function normalizeIndexPayload(payload) {
|
|
45
|
+
return Array.isArray(payload) ? payload : payload.records;
|
|
46
|
+
}
|
|
47
|
+
function getSearchFields(record) {
|
|
48
|
+
return {
|
|
49
|
+
title: normalizeSearchableText(record.title),
|
|
50
|
+
section: normalizeSearchableText(record.section),
|
|
51
|
+
hierarchy: normalizeSearchableText(record.hierarchy.join(" ")),
|
|
52
|
+
tags: normalizeSearchableText(record.tags.join(" ")),
|
|
53
|
+
content: normalizeSearchableText(record.content)
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
function scoreRecord(record, terms) {
|
|
57
|
+
const fields = getSearchFields(record);
|
|
58
|
+
let score = 0;
|
|
59
|
+
for (const term of terms) {
|
|
60
|
+
let matched = false;
|
|
61
|
+
if (fields.title.includes(term)) {
|
|
62
|
+
score += fields.title.startsWith(term) ? 36 : 28;
|
|
63
|
+
matched = true;
|
|
64
|
+
}
|
|
65
|
+
if (fields.section.includes(term)) {
|
|
66
|
+
score += fields.section.startsWith(term) ? 24 : 18;
|
|
67
|
+
matched = true;
|
|
68
|
+
}
|
|
69
|
+
if (fields.hierarchy.includes(term)) {
|
|
70
|
+
score += 12;
|
|
71
|
+
matched = true;
|
|
72
|
+
}
|
|
73
|
+
if (fields.tags.includes(term)) {
|
|
74
|
+
score += 10;
|
|
75
|
+
matched = true;
|
|
76
|
+
}
|
|
77
|
+
if (fields.content.includes(term)) {
|
|
78
|
+
score += record.type === "content" ? 8 : 5;
|
|
79
|
+
matched = true;
|
|
80
|
+
}
|
|
81
|
+
if (!matched) {
|
|
82
|
+
return 0;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if (record.type === "page") {
|
|
86
|
+
score += 6;
|
|
87
|
+
} else if (record.type === "heading") {
|
|
88
|
+
score += 4;
|
|
89
|
+
}
|
|
90
|
+
return score;
|
|
91
|
+
}
|
|
92
|
+
function hasUsefulContent(result) {
|
|
93
|
+
const content = normalizeSearchableText(result.content);
|
|
94
|
+
if (content === "") {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
return content !== normalizeSearchableText(result.title) && content !== normalizeSearchableText(result.section);
|
|
98
|
+
}
|
|
99
|
+
function preferDuplicateResult(current, candidate) {
|
|
100
|
+
if (current.type === "content" && candidate.type !== "content" && hasUsefulContent(current)) {
|
|
101
|
+
return current;
|
|
102
|
+
}
|
|
103
|
+
if (candidate.type === "content" && current.type !== "content" && hasUsefulContent(candidate)) {
|
|
104
|
+
return candidate;
|
|
105
|
+
}
|
|
106
|
+
if (candidate.score > current.score) {
|
|
107
|
+
return candidate;
|
|
108
|
+
}
|
|
109
|
+
return current;
|
|
110
|
+
}
|
|
111
|
+
function collapseDuplicateResults(results) {
|
|
112
|
+
const byUrl = /* @__PURE__ */ new Map();
|
|
113
|
+
for (const result of results) {
|
|
114
|
+
const current = byUrl.get(result.url);
|
|
115
|
+
byUrl.set(result.url, current === void 0 ? result : preferDuplicateResult(current, result));
|
|
116
|
+
}
|
|
117
|
+
return Array.from(byUrl.values()).sort(
|
|
118
|
+
(a, b) => b.score - a.score || a.title.localeCompare(b.title)
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
function searchRecords(records, query, options = {}) {
|
|
122
|
+
const terms = createSearchTerms(query);
|
|
123
|
+
if (terms.length === 0) {
|
|
124
|
+
return [];
|
|
125
|
+
}
|
|
126
|
+
const results = records.map((record) => {
|
|
127
|
+
const score = scoreRecord(record, terms);
|
|
128
|
+
if (score === 0) {
|
|
129
|
+
return void 0;
|
|
130
|
+
}
|
|
131
|
+
return {
|
|
132
|
+
id: record.id,
|
|
133
|
+
title: record.title,
|
|
134
|
+
url: record.url,
|
|
135
|
+
content: createSearchSnippet(record.content, terms),
|
|
136
|
+
score,
|
|
137
|
+
type: record.type,
|
|
138
|
+
path: record.path,
|
|
139
|
+
section: record.section,
|
|
140
|
+
hierarchy: record.hierarchy,
|
|
141
|
+
tags: record.tags,
|
|
142
|
+
record
|
|
143
|
+
};
|
|
144
|
+
}).filter((result) => result !== void 0).sort((a, b) => b.score - a.score || a.title.localeCompare(b.title));
|
|
145
|
+
const displayResults = options.collapseDuplicateResults === false ? results : collapseDuplicateResults(results);
|
|
146
|
+
return displayResults.slice(0, options.limit ?? 12);
|
|
147
|
+
}
|
|
148
|
+
function createStaticSearchProvider(records) {
|
|
149
|
+
return {
|
|
150
|
+
search(query, options) {
|
|
151
|
+
return searchRecords(records, query, options);
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
function createJsonSearchProvider(options = {}) {
|
|
156
|
+
let recordsPromise;
|
|
157
|
+
async function loadRecords() {
|
|
158
|
+
if (options.index !== void 0) {
|
|
159
|
+
return normalizeIndexPayload(options.index);
|
|
160
|
+
}
|
|
161
|
+
if (options.src === void 0) {
|
|
162
|
+
throw new Error('createJsonSearchProvider requires either "src" or "index".');
|
|
163
|
+
}
|
|
164
|
+
const src = options.src;
|
|
165
|
+
recordsPromise ??= Promise.resolve().then(async () => {
|
|
166
|
+
const fetcher = options.fetcher ?? globalThis.fetch;
|
|
167
|
+
if (fetcher === void 0) {
|
|
168
|
+
throw new Error("No fetch implementation is available for loading the search index.");
|
|
169
|
+
}
|
|
170
|
+
const response = await fetcher(src);
|
|
171
|
+
if (!response.ok) {
|
|
172
|
+
throw new Error(`Unable to load search index: ${response.status} ${response.statusText}`);
|
|
173
|
+
}
|
|
174
|
+
const responseText = await response.text();
|
|
175
|
+
try {
|
|
176
|
+
return normalizeIndexPayload(JSON.parse(responseText));
|
|
177
|
+
} catch {
|
|
178
|
+
const contentType = response.headers.get("content-type") ?? "unknown content type";
|
|
179
|
+
const preview = responseText.trim().slice(0, 120);
|
|
180
|
+
throw new Error(
|
|
181
|
+
`Unable to parse search index JSON from ${src}. Received ${contentType}${preview ? `: ${preview}` : "."}`
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
return recordsPromise;
|
|
186
|
+
}
|
|
187
|
+
return {
|
|
188
|
+
async search(query, searchOptions) {
|
|
189
|
+
return searchRecords(await loadRecords(), query, searchOptions);
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const templateStyles = (
|
|
195
|
+
/* css */
|
|
196
|
+
`
|
|
197
|
+
:host {
|
|
198
|
+
color-scheme: var(--md-search-color-scheme, light);
|
|
199
|
+
--md-search-z-index: 5000;
|
|
200
|
+
--md-search-font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
201
|
+
--md-search-trigger-bg: color-mix(in srgb, var(--md-search-accent, #2d7ff9) 10%, transparent);
|
|
202
|
+
--md-search-trigger-border: color-mix(in srgb, var(--md-search-accent, #2d7ff9) 34%, transparent);
|
|
203
|
+
--md-search-trigger-color: var(--md-search-text, #172033);
|
|
204
|
+
--md-search-backdrop: rgb(8 13 24 / 54%);
|
|
205
|
+
--md-search-surface: #ffffff;
|
|
206
|
+
--md-search-surface-raised: #f6f8fb;
|
|
207
|
+
--md-search-text: #172033;
|
|
208
|
+
--md-search-muted: #64748b;
|
|
209
|
+
--md-search-border: #d7deea;
|
|
210
|
+
--md-search-accent: #2d7ff9;
|
|
211
|
+
--md-search-highlight: #f97316;
|
|
212
|
+
--md-search-highlight-bg: color-mix(in srgb, var(--md-search-highlight) 14%, transparent);
|
|
213
|
+
--md-search-result-bg: var(--md-search-surface-raised);
|
|
214
|
+
--md-search-result-active-bg: color-mix(in srgb, var(--md-search-accent) 12%, var(--md-search-surface));
|
|
215
|
+
--md-search-result-active-border: color-mix(in srgb, var(--md-search-accent) 74%, var(--md-search-border));
|
|
216
|
+
--md-search-pill-bg: color-mix(in srgb, var(--md-search-accent) 9%, transparent);
|
|
217
|
+
--md-search-pill-border: color-mix(in srgb, var(--md-search-accent) 54%, transparent);
|
|
218
|
+
--md-search-radius: 16px;
|
|
219
|
+
--md-search-shadow: 0 24px 80px rgb(15 23 42 / 28%);
|
|
220
|
+
--md-search-width: min(720px, calc(100vw - 28px));
|
|
221
|
+
--md-search-mobile-trigger-size: 40px;
|
|
222
|
+
display: inline-flex;
|
|
223
|
+
font-family: var(--md-search-font-family);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
:host([theme="dark"]) {
|
|
227
|
+
--md-search-color-scheme: dark;
|
|
228
|
+
--md-search-trigger-bg: color-mix(in srgb, var(--md-search-accent, #72a7ff) 16%, transparent);
|
|
229
|
+
--md-search-trigger-border: color-mix(in srgb, var(--md-search-accent, #72a7ff) 42%, transparent);
|
|
230
|
+
--md-search-trigger-color: var(--md-search-text, #eef5ff);
|
|
231
|
+
--md-search-backdrop: rgb(0 0 0 / 66%);
|
|
232
|
+
--md-search-surface: #111827;
|
|
233
|
+
--md-search-surface-raised: #1f2937;
|
|
234
|
+
--md-search-text: #eef5ff;
|
|
235
|
+
--md-search-muted: #a7b4c8;
|
|
236
|
+
--md-search-border: #334155;
|
|
237
|
+
--md-search-accent: #72a7ff;
|
|
238
|
+
--md-search-highlight: #ff8a3d;
|
|
239
|
+
--md-search-highlight-bg: color-mix(in srgb, var(--md-search-highlight) 18%, transparent);
|
|
240
|
+
--md-search-result-bg: #0b1220;
|
|
241
|
+
--md-search-result-active-bg: color-mix(in srgb, var(--md-search-highlight) 16%, #1b1020);
|
|
242
|
+
--md-search-result-active-border: var(--md-search-highlight);
|
|
243
|
+
--md-search-pill-bg: color-mix(in srgb, var(--md-search-accent) 12%, transparent);
|
|
244
|
+
--md-search-pill-border: color-mix(in srgb, var(--md-search-accent) 74%, transparent);
|
|
245
|
+
--md-search-shadow: 0 24px 90px rgb(0 0 0 / 48%);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
@media (prefers-color-scheme: dark) {
|
|
249
|
+
:host(:not([theme="light"])) {
|
|
250
|
+
--md-search-color-scheme: dark;
|
|
251
|
+
--md-search-trigger-bg: color-mix(in srgb, var(--md-search-accent, #72a7ff) 16%, transparent);
|
|
252
|
+
--md-search-trigger-border: color-mix(in srgb, var(--md-search-accent, #72a7ff) 42%, transparent);
|
|
253
|
+
--md-search-trigger-color: var(--md-search-text, #eef5ff);
|
|
254
|
+
--md-search-backdrop: rgb(0 0 0 / 66%);
|
|
255
|
+
--md-search-surface: #111827;
|
|
256
|
+
--md-search-surface-raised: #1f2937;
|
|
257
|
+
--md-search-text: #eef5ff;
|
|
258
|
+
--md-search-muted: #a7b4c8;
|
|
259
|
+
--md-search-border: #334155;
|
|
260
|
+
--md-search-accent: #72a7ff;
|
|
261
|
+
--md-search-highlight: #ff8a3d;
|
|
262
|
+
--md-search-highlight-bg: color-mix(in srgb, var(--md-search-highlight) 18%, transparent);
|
|
263
|
+
--md-search-result-bg: #0b1220;
|
|
264
|
+
--md-search-result-active-bg: color-mix(in srgb, var(--md-search-highlight) 16%, #1b1020);
|
|
265
|
+
--md-search-result-active-border: var(--md-search-highlight);
|
|
266
|
+
--md-search-pill-bg: color-mix(in srgb, var(--md-search-accent) 12%, transparent);
|
|
267
|
+
--md-search-pill-border: color-mix(in srgb, var(--md-search-accent) 74%, transparent);
|
|
268
|
+
--md-search-shadow: 0 24px 90px rgb(0 0 0 / 48%);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
* {
|
|
273
|
+
box-sizing: border-box;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
button,
|
|
277
|
+
input {
|
|
278
|
+
font: inherit;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
[hidden] {
|
|
282
|
+
display: none !important;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
.trigger {
|
|
286
|
+
display: inline-flex;
|
|
287
|
+
align-items: center;
|
|
288
|
+
gap: 10px;
|
|
289
|
+
min-height: 40px;
|
|
290
|
+
max-width: 100%;
|
|
291
|
+
padding: 0 12px;
|
|
292
|
+
border: 1px solid var(--md-search-trigger-border);
|
|
293
|
+
border-radius: 999px;
|
|
294
|
+
background: var(--md-search-trigger-bg);
|
|
295
|
+
color: var(--md-search-trigger-color);
|
|
296
|
+
cursor: pointer;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
.trigger:hover {
|
|
300
|
+
border-color: var(--md-search-accent);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
.trigger__icon {
|
|
304
|
+
font-size: 18px;
|
|
305
|
+
line-height: 1;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
.kbd {
|
|
309
|
+
display: inline-flex;
|
|
310
|
+
min-width: 34px;
|
|
311
|
+
justify-content: center;
|
|
312
|
+
padding: 3px 6px;
|
|
313
|
+
border: 1px solid var(--md-search-border);
|
|
314
|
+
border-radius: 7px;
|
|
315
|
+
color: var(--md-search-muted);
|
|
316
|
+
font-size: 0.72rem;
|
|
317
|
+
line-height: 1;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
.backdrop {
|
|
321
|
+
position: fixed;
|
|
322
|
+
inset: 0;
|
|
323
|
+
z-index: var(--md-search-z-index);
|
|
324
|
+
display: grid;
|
|
325
|
+
align-items: start;
|
|
326
|
+
justify-items: center;
|
|
327
|
+
padding: min(10vh, 76px) 14px 24px;
|
|
328
|
+
background: var(--md-search-backdrop);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
.dialog {
|
|
332
|
+
width: var(--md-search-width);
|
|
333
|
+
overflow: hidden;
|
|
334
|
+
border: 1px solid var(--md-search-border);
|
|
335
|
+
border-radius: var(--md-search-radius);
|
|
336
|
+
background: var(--md-search-surface);
|
|
337
|
+
color: var(--md-search-text);
|
|
338
|
+
box-shadow: var(--md-search-shadow);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
.header {
|
|
342
|
+
display: grid;
|
|
343
|
+
gap: 12px;
|
|
344
|
+
padding: 16px;
|
|
345
|
+
border-bottom: 1px solid var(--md-search-border);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
.title-row {
|
|
349
|
+
display: flex;
|
|
350
|
+
align-items: center;
|
|
351
|
+
justify-content: space-between;
|
|
352
|
+
gap: 12px;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
.title {
|
|
356
|
+
margin: 0;
|
|
357
|
+
font-size: 0.88rem;
|
|
358
|
+
font-weight: 800;
|
|
359
|
+
letter-spacing: 0.08em;
|
|
360
|
+
text-transform: uppercase;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
.close {
|
|
364
|
+
width: 34px;
|
|
365
|
+
height: 34px;
|
|
366
|
+
border: 0;
|
|
367
|
+
border-radius: 999px;
|
|
368
|
+
background: transparent;
|
|
369
|
+
color: var(--md-search-muted);
|
|
370
|
+
cursor: pointer;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
.close:hover {
|
|
374
|
+
background: var(--md-search-surface-raised);
|
|
375
|
+
color: var(--md-search-text);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
.input {
|
|
379
|
+
width: 100%;
|
|
380
|
+
min-height: 48px;
|
|
381
|
+
border: 1px solid var(--md-search-border);
|
|
382
|
+
border-radius: calc(var(--md-search-radius) - 6px);
|
|
383
|
+
background: var(--md-search-surface-raised);
|
|
384
|
+
color: var(--md-search-text);
|
|
385
|
+
outline: 0;
|
|
386
|
+
padding: 0 14px;
|
|
387
|
+
font-size: 1rem;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
.input:focus {
|
|
391
|
+
border-color: var(--md-search-accent);
|
|
392
|
+
box-shadow: 0 0 0 3px color-mix(in srgb, var(--md-search-accent) 18%, transparent);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
.help {
|
|
396
|
+
display: flex;
|
|
397
|
+
flex-wrap: wrap;
|
|
398
|
+
align-items: center;
|
|
399
|
+
gap: 8px 14px;
|
|
400
|
+
color: var(--md-search-muted);
|
|
401
|
+
font-size: 0.82rem;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
.help__item {
|
|
405
|
+
display: inline-flex;
|
|
406
|
+
align-items: center;
|
|
407
|
+
gap: 5px;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
.help .kbd {
|
|
411
|
+
min-width: 26px;
|
|
412
|
+
padding: 4px 6px;
|
|
413
|
+
border-color: color-mix(in srgb, var(--md-search-highlight) 36%, var(--md-search-border));
|
|
414
|
+
background: color-mix(in srgb, var(--md-search-highlight) 16%, transparent);
|
|
415
|
+
color: var(--md-search-text);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
.status {
|
|
419
|
+
padding: 28px 18px;
|
|
420
|
+
color: var(--md-search-muted);
|
|
421
|
+
text-align: center;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
.list {
|
|
425
|
+
max-height: min(58vh, 560px);
|
|
426
|
+
overflow: auto;
|
|
427
|
+
overscroll-behavior: contain;
|
|
428
|
+
padding: 10px;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
.result {
|
|
432
|
+
display: block;
|
|
433
|
+
width: 100%;
|
|
434
|
+
border: 1px solid var(--md-search-border);
|
|
435
|
+
border-radius: calc(var(--md-search-radius) - 6px);
|
|
436
|
+
background: var(--md-search-result-bg);
|
|
437
|
+
color: inherit;
|
|
438
|
+
cursor: pointer;
|
|
439
|
+
padding: 12px 14px;
|
|
440
|
+
text-align: left;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
.result + .result {
|
|
444
|
+
margin-top: 8px;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
.result:hover,
|
|
448
|
+
.result--active {
|
|
449
|
+
border-color: var(--md-search-result-active-border);
|
|
450
|
+
background: var(--md-search-result-active-bg);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
.result__header {
|
|
454
|
+
display: flex;
|
|
455
|
+
align-items: flex-start;
|
|
456
|
+
justify-content: space-between;
|
|
457
|
+
gap: 12px;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
.result__trail {
|
|
461
|
+
display: inline-flex;
|
|
462
|
+
flex-wrap: wrap;
|
|
463
|
+
align-items: center;
|
|
464
|
+
gap: 5px;
|
|
465
|
+
max-width: 100%;
|
|
466
|
+
padding: 4px 7px;
|
|
467
|
+
border: 1px solid var(--md-search-pill-border);
|
|
468
|
+
border-radius: 6px;
|
|
469
|
+
background: var(--md-search-pill-bg);
|
|
470
|
+
color: var(--md-search-accent);
|
|
471
|
+
font-size: 0.9rem;
|
|
472
|
+
font-weight: 800;
|
|
473
|
+
line-height: 1.15;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
.result__separator {
|
|
477
|
+
color: var(--md-search-muted);
|
|
478
|
+
font-weight: 700;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
.result__type {
|
|
482
|
+
flex: 0 0 auto;
|
|
483
|
+
color: var(--md-search-accent);
|
|
484
|
+
font-size: 0.68rem;
|
|
485
|
+
font-weight: 800;
|
|
486
|
+
letter-spacing: 0.08em;
|
|
487
|
+
line-height: 1.2;
|
|
488
|
+
text-transform: uppercase;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
.result__content {
|
|
492
|
+
margin-top: 10px;
|
|
493
|
+
color: var(--md-search-text);
|
|
494
|
+
font-size: 0.94rem;
|
|
495
|
+
line-height: 1.45;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
.result__path {
|
|
499
|
+
margin-top: 6px;
|
|
500
|
+
color: var(--md-search-muted);
|
|
501
|
+
font-size: 0.76rem;
|
|
502
|
+
line-height: 1.35;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
mark {
|
|
506
|
+
border-radius: 4px;
|
|
507
|
+
background: var(--md-search-highlight-bg);
|
|
508
|
+
color: var(--md-search-highlight);
|
|
509
|
+
font-weight: 900;
|
|
510
|
+
padding: 0 1px;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
@media (max-width: 560px) {
|
|
514
|
+
.trigger__label {
|
|
515
|
+
display: none;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
.trigger {
|
|
519
|
+
width: var(--md-search-mobile-trigger-size);
|
|
520
|
+
min-height: var(--md-search-mobile-trigger-size);
|
|
521
|
+
justify-content: center;
|
|
522
|
+
padding: 0;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
.kbd {
|
|
526
|
+
display: none;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
.help {
|
|
530
|
+
display: none;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
`
|
|
534
|
+
);
|
|
535
|
+
const icon = "\u2315";
|
|
536
|
+
function getShortcutLabel(shortcut) {
|
|
537
|
+
if (shortcut === "none") {
|
|
538
|
+
return "";
|
|
539
|
+
}
|
|
540
|
+
if (shortcut === "mod+k") {
|
|
541
|
+
const isMac = /Mac|iPhone|iPad|iPod/i.test(globalThis.navigator?.platform ?? "");
|
|
542
|
+
return isMac ? "\u2318K" : "Ctrl K";
|
|
543
|
+
}
|
|
544
|
+
return shortcut;
|
|
545
|
+
}
|
|
546
|
+
function escapeHtml(value) {
|
|
547
|
+
return String(value ?? "").replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
548
|
+
}
|
|
549
|
+
function escapeRegExp(value) {
|
|
550
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
551
|
+
}
|
|
552
|
+
function renderHighlightedText(value, terms) {
|
|
553
|
+
const text = String(value ?? "");
|
|
554
|
+
if (text === "" || terms.length === 0) {
|
|
555
|
+
return escapeHtml(text);
|
|
556
|
+
}
|
|
557
|
+
const pattern = new RegExp(
|
|
558
|
+
terms.filter(Boolean).sort((a, b) => b.length - a.length).map(escapeRegExp).join("|"),
|
|
559
|
+
"gi"
|
|
560
|
+
);
|
|
561
|
+
let output = "";
|
|
562
|
+
let lastIndex = 0;
|
|
563
|
+
for (const match of text.matchAll(pattern)) {
|
|
564
|
+
const index = match.index ?? 0;
|
|
565
|
+
output += escapeHtml(text.slice(lastIndex, index));
|
|
566
|
+
output += `<mark>${escapeHtml(match[0])}</mark>`;
|
|
567
|
+
lastIndex = index + match[0].length;
|
|
568
|
+
}
|
|
569
|
+
output += escapeHtml(text.slice(lastIndex));
|
|
570
|
+
return output;
|
|
571
|
+
}
|
|
572
|
+
function formatResultType(type) {
|
|
573
|
+
if (type === "page") {
|
|
574
|
+
return "page";
|
|
575
|
+
}
|
|
576
|
+
if (type === "heading") {
|
|
577
|
+
return "heading";
|
|
578
|
+
}
|
|
579
|
+
return "content";
|
|
580
|
+
}
|
|
581
|
+
function getResultTrail(result) {
|
|
582
|
+
const trail = (result.hierarchy.length > 0 ? result.hierarchy : [result.title]).filter(
|
|
583
|
+
(entry, index, entries) => entry !== "" && entry !== entries[index - 1]
|
|
584
|
+
);
|
|
585
|
+
if (result.section !== void 0 && !trail.includes(result.section)) {
|
|
586
|
+
return [...trail, result.section];
|
|
587
|
+
}
|
|
588
|
+
return trail;
|
|
589
|
+
}
|
|
590
|
+
function renderResultTrail(result, terms) {
|
|
591
|
+
return getResultTrail(result).filter(Boolean).map((entry) => `<span>${renderHighlightedText(entry, terms)}</span>`).join('<span class="result__separator" aria-hidden="true">›</span>');
|
|
592
|
+
}
|
|
593
|
+
const BrowserHTMLElement = globalThis.HTMLElement;
|
|
594
|
+
const HTMLElementBase = BrowserHTMLElement ?? class MdSearchServerElement {
|
|
595
|
+
};
|
|
596
|
+
class MdSearchElement extends HTMLElementBase {
|
|
597
|
+
static observedAttributes = [
|
|
598
|
+
"src",
|
|
599
|
+
"placeholder",
|
|
600
|
+
"trigger-label",
|
|
601
|
+
"panel-title",
|
|
602
|
+
"shortcut",
|
|
603
|
+
"theme",
|
|
604
|
+
"search-label",
|
|
605
|
+
"min-query-length",
|
|
606
|
+
"max-results",
|
|
607
|
+
"show-duplicate-results"
|
|
608
|
+
];
|
|
609
|
+
provider;
|
|
610
|
+
root = this.attachShadow({ mode: "open" });
|
|
611
|
+
elementId = `md-search-${Math.random().toString(36).slice(2)}`;
|
|
612
|
+
results = [];
|
|
613
|
+
query = "";
|
|
614
|
+
opened = false;
|
|
615
|
+
loading = false;
|
|
616
|
+
errorMessage = "";
|
|
617
|
+
activeIndex = 0;
|
|
618
|
+
searchTimer;
|
|
619
|
+
searchRequestId = 0;
|
|
620
|
+
selectionStart = null;
|
|
621
|
+
selectionEnd = null;
|
|
622
|
+
connectedCallback() {
|
|
623
|
+
this.render();
|
|
624
|
+
globalThis.addEventListener?.("keydown", this.onGlobalKeydown);
|
|
625
|
+
}
|
|
626
|
+
disconnectedCallback() {
|
|
627
|
+
globalThis.removeEventListener?.("keydown", this.onGlobalKeydown);
|
|
628
|
+
clearTimeout(this.searchTimer);
|
|
629
|
+
}
|
|
630
|
+
attributeChangedCallback(name) {
|
|
631
|
+
if (name === "src") {
|
|
632
|
+
this.provider = void 0;
|
|
633
|
+
}
|
|
634
|
+
if (name === "show-duplicate-results" && this.query.trim().length >= this.minQueryLength) {
|
|
635
|
+
void this.runSearch();
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
638
|
+
this.render();
|
|
639
|
+
}
|
|
640
|
+
open() {
|
|
641
|
+
this.opened = true;
|
|
642
|
+
this.render();
|
|
643
|
+
this.focusInput();
|
|
644
|
+
}
|
|
645
|
+
close() {
|
|
646
|
+
this.opened = false;
|
|
647
|
+
this.render();
|
|
648
|
+
}
|
|
649
|
+
toggle() {
|
|
650
|
+
if (this.opened) {
|
|
651
|
+
this.close();
|
|
652
|
+
} else {
|
|
653
|
+
this.open();
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
get src() {
|
|
657
|
+
return this.getAttribute("src") ?? void 0;
|
|
658
|
+
}
|
|
659
|
+
get placeholder() {
|
|
660
|
+
return this.getAttribute("placeholder") ?? "Search docs...";
|
|
661
|
+
}
|
|
662
|
+
get triggerLabel() {
|
|
663
|
+
return this.getAttribute("trigger-label") ?? "Search";
|
|
664
|
+
}
|
|
665
|
+
get panelTitle() {
|
|
666
|
+
return this.getAttribute("panel-title") ?? "Search";
|
|
667
|
+
}
|
|
668
|
+
get searchLabel() {
|
|
669
|
+
return this.getAttribute("search-label") ?? this.panelTitle;
|
|
670
|
+
}
|
|
671
|
+
get shortcut() {
|
|
672
|
+
return this.getAttribute("shortcut") ?? "mod+k";
|
|
673
|
+
}
|
|
674
|
+
get minQueryLength() {
|
|
675
|
+
return Number(this.getAttribute("min-query-length") ?? 2);
|
|
676
|
+
}
|
|
677
|
+
get maxResults() {
|
|
678
|
+
return Number(this.getAttribute("max-results") ?? 12);
|
|
679
|
+
}
|
|
680
|
+
get showDuplicateResults() {
|
|
681
|
+
return this.hasAttribute("show-duplicate-results");
|
|
682
|
+
}
|
|
683
|
+
get activeResult() {
|
|
684
|
+
return this.results[this.activeIndex];
|
|
685
|
+
}
|
|
686
|
+
getProvider() {
|
|
687
|
+
this.provider ??= createJsonSearchProvider({ src: this.src });
|
|
688
|
+
return this.provider;
|
|
689
|
+
}
|
|
690
|
+
onGlobalKeydown = (event) => {
|
|
691
|
+
if (this.shortcut !== "mod+k") {
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
694
|
+
if ((event.ctrlKey || event.metaKey) && event.key.toLocaleLowerCase() === "k") {
|
|
695
|
+
event.preventDefault();
|
|
696
|
+
this.open();
|
|
697
|
+
}
|
|
698
|
+
};
|
|
699
|
+
onInput = (event) => {
|
|
700
|
+
const input = event.target;
|
|
701
|
+
this.query = input.value;
|
|
702
|
+
this.selectionStart = input.selectionStart;
|
|
703
|
+
this.selectionEnd = input.selectionEnd;
|
|
704
|
+
this.searchRequestId++;
|
|
705
|
+
clearTimeout(this.searchTimer);
|
|
706
|
+
if (this.query.trim().length < this.minQueryLength) {
|
|
707
|
+
this.results = [];
|
|
708
|
+
this.loading = false;
|
|
709
|
+
this.errorMessage = "";
|
|
710
|
+
this.activeIndex = 0;
|
|
711
|
+
this.renderResultsContainer();
|
|
712
|
+
return;
|
|
713
|
+
}
|
|
714
|
+
this.loading = true;
|
|
715
|
+
this.errorMessage = "";
|
|
716
|
+
this.renderResultsContainer();
|
|
717
|
+
this.searchTimer = setTimeout(() => {
|
|
718
|
+
void this.runSearch();
|
|
719
|
+
}, 120);
|
|
720
|
+
};
|
|
721
|
+
onDialogKeydown = (event) => {
|
|
722
|
+
if (event.key === "Escape") {
|
|
723
|
+
event.preventDefault();
|
|
724
|
+
this.close();
|
|
725
|
+
return;
|
|
726
|
+
}
|
|
727
|
+
if (event.key === "ArrowDown") {
|
|
728
|
+
event.preventDefault();
|
|
729
|
+
this.setActiveIndex(this.activeIndex + 1, {
|
|
730
|
+
scrollIntoView: true
|
|
731
|
+
});
|
|
732
|
+
return;
|
|
733
|
+
}
|
|
734
|
+
if (event.key === "ArrowUp") {
|
|
735
|
+
event.preventDefault();
|
|
736
|
+
this.setActiveIndex(this.activeIndex - 1, {
|
|
737
|
+
scrollIntoView: true
|
|
738
|
+
});
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
741
|
+
if (event.key === "Enter" && this.activeResult !== void 0) {
|
|
742
|
+
event.preventDefault();
|
|
743
|
+
this.selectResult(this.activeResult);
|
|
744
|
+
}
|
|
745
|
+
};
|
|
746
|
+
async runSearch() {
|
|
747
|
+
const requestId = ++this.searchRequestId;
|
|
748
|
+
try {
|
|
749
|
+
const results = await this.getProvider().search(this.query, {
|
|
750
|
+
limit: this.maxResults,
|
|
751
|
+
collapseDuplicateResults: !this.showDuplicateResults
|
|
752
|
+
});
|
|
753
|
+
if (requestId !== this.searchRequestId) {
|
|
754
|
+
return;
|
|
755
|
+
}
|
|
756
|
+
this.results = results;
|
|
757
|
+
this.loading = false;
|
|
758
|
+
this.errorMessage = "";
|
|
759
|
+
this.activeIndex = 0;
|
|
760
|
+
} catch (error) {
|
|
761
|
+
if (requestId !== this.searchRequestId) {
|
|
762
|
+
return;
|
|
763
|
+
}
|
|
764
|
+
this.results = [];
|
|
765
|
+
this.loading = false;
|
|
766
|
+
this.errorMessage = error instanceof Error ? error.message : "Search failed.";
|
|
767
|
+
}
|
|
768
|
+
this.renderResultsContainer();
|
|
769
|
+
}
|
|
770
|
+
focusInput(options = {}) {
|
|
771
|
+
const { selectionStart, selectionEnd } = this;
|
|
772
|
+
requestAnimationFrame(() => {
|
|
773
|
+
const input = this.root.querySelector('[part="input"]');
|
|
774
|
+
input?.focus();
|
|
775
|
+
if (input !== null && options.restoreSelection === true && selectionStart !== null && selectionEnd !== null) {
|
|
776
|
+
input.setSelectionRange(selectionStart, selectionEnd);
|
|
777
|
+
}
|
|
778
|
+
});
|
|
779
|
+
}
|
|
780
|
+
setActiveIndex(index, options = {}) {
|
|
781
|
+
if (this.results.length === 0) {
|
|
782
|
+
return;
|
|
783
|
+
}
|
|
784
|
+
this.activeIndex = Math.min(Math.max(index, 0), this.results.length - 1);
|
|
785
|
+
this.syncActiveResult(options);
|
|
786
|
+
}
|
|
787
|
+
syncInputAria() {
|
|
788
|
+
const input = this.root.querySelector('[part="input"]');
|
|
789
|
+
if (input === null) {
|
|
790
|
+
return;
|
|
791
|
+
}
|
|
792
|
+
input.setAttribute("aria-expanded", this.results.length > 0 ? "true" : "false");
|
|
793
|
+
if (this.results.length === 0) {
|
|
794
|
+
input.removeAttribute("aria-activedescendant");
|
|
795
|
+
return;
|
|
796
|
+
}
|
|
797
|
+
input.setAttribute("aria-activedescendant", `${this.elementId}-result-${this.activeIndex}`);
|
|
798
|
+
}
|
|
799
|
+
syncActiveResult(options = {}) {
|
|
800
|
+
this.syncInputAria();
|
|
801
|
+
this.root.querySelectorAll("[data-result-index]").forEach((element) => {
|
|
802
|
+
const isActive = Number(element.dataset.resultIndex ?? -1) === this.activeIndex;
|
|
803
|
+
element.classList.toggle("result--active", isActive);
|
|
804
|
+
element.setAttribute("aria-selected", isActive ? "true" : "false");
|
|
805
|
+
if (isActive && options.scrollIntoView === true) {
|
|
806
|
+
element.scrollIntoView({
|
|
807
|
+
block: "nearest"
|
|
808
|
+
});
|
|
809
|
+
}
|
|
810
|
+
});
|
|
811
|
+
}
|
|
812
|
+
selectResult(result) {
|
|
813
|
+
const event = new CustomEvent("md-search-select", {
|
|
814
|
+
bubbles: true,
|
|
815
|
+
cancelable: true,
|
|
816
|
+
composed: true,
|
|
817
|
+
detail: { result }
|
|
818
|
+
});
|
|
819
|
+
const shouldNavigate = this.dispatchEvent(event);
|
|
820
|
+
this.close();
|
|
821
|
+
if (shouldNavigate) {
|
|
822
|
+
globalThis.location.assign(result.url);
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
bindResultEvents() {
|
|
826
|
+
this.root.querySelectorAll("[data-result-index]").forEach((element) => {
|
|
827
|
+
element.addEventListener("mouseenter", () => {
|
|
828
|
+
this.setActiveIndex(Number(element.dataset.resultIndex ?? 0));
|
|
829
|
+
});
|
|
830
|
+
element.addEventListener("click", () => {
|
|
831
|
+
const result = this.results[Number(element.dataset.resultIndex ?? 0)];
|
|
832
|
+
if (result !== void 0) {
|
|
833
|
+
this.selectResult(result);
|
|
834
|
+
}
|
|
835
|
+
});
|
|
836
|
+
});
|
|
837
|
+
}
|
|
838
|
+
renderResultsContainer() {
|
|
839
|
+
const container = this.root.querySelector('[part="results"]');
|
|
840
|
+
if (container === null) {
|
|
841
|
+
this.render();
|
|
842
|
+
return;
|
|
843
|
+
}
|
|
844
|
+
container.innerHTML = this.renderResults();
|
|
845
|
+
this.bindResultEvents();
|
|
846
|
+
this.syncInputAria();
|
|
847
|
+
}
|
|
848
|
+
renderResults() {
|
|
849
|
+
if (this.loading) {
|
|
850
|
+
return '<div part="status" class="status" role="status" aria-live="polite">Searching...</div>';
|
|
851
|
+
}
|
|
852
|
+
if (this.errorMessage !== "") {
|
|
853
|
+
return `<div part="status" class="status" role="status" aria-live="assertive">${escapeHtml(this.errorMessage)}</div>`;
|
|
854
|
+
}
|
|
855
|
+
if (this.query.trim().length < this.minQueryLength) {
|
|
856
|
+
return `<div part="status" class="status" role="status" aria-live="polite">Type at least ${this.minQueryLength} characters.</div>`;
|
|
857
|
+
}
|
|
858
|
+
if (this.results.length === 0) {
|
|
859
|
+
return '<div part="status" class="status" role="status" aria-live="polite">No results found.</div>';
|
|
860
|
+
}
|
|
861
|
+
const terms = createSearchTerms(this.query);
|
|
862
|
+
return `
|
|
863
|
+
<div part="list" class="list" id="${this.elementId}-listbox" role="listbox">
|
|
864
|
+
${this.results.map((result, index) => {
|
|
865
|
+
const resultId = `${this.elementId}-result-${index}`;
|
|
866
|
+
return `
|
|
867
|
+
<button
|
|
868
|
+
id="${resultId}"
|
|
869
|
+
type="button"
|
|
870
|
+
part="result"
|
|
871
|
+
class="result ${index === this.activeIndex ? "result--active" : ""}"
|
|
872
|
+
data-result-index="${index}"
|
|
873
|
+
role="option"
|
|
874
|
+
aria-selected="${index === this.activeIndex ? "true" : "false"}"
|
|
875
|
+
>
|
|
876
|
+
<div part="result-title" class="result__header">
|
|
877
|
+
<span part="result-trail" class="result__trail">${renderResultTrail(result, terms)}</span>
|
|
878
|
+
<span part="result-type" class="result__type">${escapeHtml(formatResultType(result.type))}</span>
|
|
879
|
+
</div>
|
|
880
|
+
<div part="result-snippet" class="result__content">${renderHighlightedText(result.content, terms)}</div>
|
|
881
|
+
<div part="result-url" class="result__path">${escapeHtml(result.url)}</div>
|
|
882
|
+
</button>
|
|
883
|
+
`;
|
|
884
|
+
}).join("")}
|
|
885
|
+
</div>
|
|
886
|
+
`;
|
|
887
|
+
}
|
|
888
|
+
render() {
|
|
889
|
+
const shortcutLabel = getShortcutLabel(this.shortcut);
|
|
890
|
+
const dialogId = `${this.elementId}-dialog`;
|
|
891
|
+
const titleId = `${this.elementId}-title`;
|
|
892
|
+
const activeResultId = this.results.length > 0 ? `${this.elementId}-result-${this.activeIndex}` : void 0;
|
|
893
|
+
this.root.innerHTML = `
|
|
894
|
+
<style>${templateStyles}</style>
|
|
895
|
+
<button
|
|
896
|
+
type="button"
|
|
897
|
+
part="trigger"
|
|
898
|
+
class="trigger"
|
|
899
|
+
aria-haspopup="dialog"
|
|
900
|
+
aria-controls="${dialogId}"
|
|
901
|
+
aria-expanded="${this.opened ? "true" : "false"}"
|
|
902
|
+
aria-label="${escapeHtml(this.triggerLabel)}"
|
|
903
|
+
>
|
|
904
|
+
<span part="trigger-icon" class="trigger__icon" aria-hidden="true">${icon}</span>
|
|
905
|
+
<span part="trigger-label" class="trigger__label">${escapeHtml(this.triggerLabel)}</span>
|
|
906
|
+
${shortcutLabel ? `<kbd part="keyboard-shortcut" class="kbd">${escapeHtml(shortcutLabel)}</kbd>` : ""}
|
|
907
|
+
</button>
|
|
908
|
+
<div part="backdrop" class="backdrop" ${this.opened ? "" : "hidden"}>
|
|
909
|
+
<section
|
|
910
|
+
id="${dialogId}"
|
|
911
|
+
part="dialog"
|
|
912
|
+
class="dialog"
|
|
913
|
+
role="dialog"
|
|
914
|
+
aria-modal="true"
|
|
915
|
+
aria-labelledby="${titleId}"
|
|
916
|
+
>
|
|
917
|
+
<div class="header">
|
|
918
|
+
<div class="title-row">
|
|
919
|
+
<h2 id="${titleId}" part="title" class="title">${escapeHtml(this.panelTitle)}</h2>
|
|
920
|
+
<button type="button" part="close-button" class="close" aria-label="Close search">\u2715</button>
|
|
921
|
+
</div>
|
|
922
|
+
<input
|
|
923
|
+
part="input"
|
|
924
|
+
class="input"
|
|
925
|
+
type="search"
|
|
926
|
+
value="${escapeHtml(this.query)}"
|
|
927
|
+
placeholder="${escapeHtml(this.placeholder)}"
|
|
928
|
+
role="combobox"
|
|
929
|
+
aria-label="${escapeHtml(this.searchLabel)}"
|
|
930
|
+
aria-autocomplete="list"
|
|
931
|
+
aria-expanded="${this.results.length > 0 ? "true" : "false"}"
|
|
932
|
+
aria-controls="${this.elementId}-listbox"
|
|
933
|
+
${activeResultId !== void 0 ? `aria-activedescendant="${activeResultId}"` : ""}
|
|
934
|
+
autocomplete="off"
|
|
935
|
+
spellcheck="false"
|
|
936
|
+
/>
|
|
937
|
+
<div part="help" class="help" aria-hidden="true">
|
|
938
|
+
<span class="help__item">Navigate <kbd class="kbd">\u2193</kbd><kbd class="kbd">\u2191</kbd></span>
|
|
939
|
+
<span class="help__item">Select <kbd class="kbd">\u21B5</kbd></span>
|
|
940
|
+
<span class="help__item">Close <kbd class="kbd">Esc</kbd></span>
|
|
941
|
+
</div>
|
|
942
|
+
</div>
|
|
943
|
+
<div part="results">
|
|
944
|
+
${this.renderResults()}
|
|
945
|
+
</div>
|
|
946
|
+
</section>
|
|
947
|
+
</div>
|
|
948
|
+
`;
|
|
949
|
+
this.bindEvents();
|
|
950
|
+
}
|
|
951
|
+
bindEvents() {
|
|
952
|
+
this.root.querySelector('[part="trigger"]')?.addEventListener("click", () => this.open());
|
|
953
|
+
this.root.querySelector('[part="close-button"]')?.addEventListener("click", () => this.close());
|
|
954
|
+
this.root.querySelector('[part="backdrop"]')?.addEventListener("click", (event) => {
|
|
955
|
+
if (event.target === event.currentTarget) {
|
|
956
|
+
this.close();
|
|
957
|
+
}
|
|
958
|
+
});
|
|
959
|
+
this.root.querySelector('[part="dialog"]')?.addEventListener("keydown", this.onDialogKeydown);
|
|
960
|
+
this.root.querySelector('[part="input"]')?.addEventListener("input", this.onInput);
|
|
961
|
+
this.bindResultEvents();
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
function defineMdSearchElement(name = "md-search") {
|
|
965
|
+
if (globalThis.customElements === void 0 || globalThis.customElements.get(name) !== void 0) {
|
|
966
|
+
return;
|
|
967
|
+
}
|
|
968
|
+
globalThis.customElements.define(name, MdSearchElement);
|
|
969
|
+
}
|
|
970
|
+
function createSearch(options) {
|
|
971
|
+
defineMdSearchElement();
|
|
972
|
+
const element = document.createElement("md-search");
|
|
973
|
+
if (options.src !== void 0) element.setAttribute("src", options.src);
|
|
974
|
+
if (options.placeholder !== void 0) element.setAttribute("placeholder", options.placeholder);
|
|
975
|
+
if (options.triggerLabel !== void 0)
|
|
976
|
+
element.setAttribute("trigger-label", options.triggerLabel);
|
|
977
|
+
if (options.panelTitle !== void 0) element.setAttribute("panel-title", options.panelTitle);
|
|
978
|
+
if (options.searchLabel !== void 0) element.setAttribute("search-label", options.searchLabel);
|
|
979
|
+
if (options.theme !== void 0) element.setAttribute("theme", options.theme);
|
|
980
|
+
if (options.shortcut !== void 0) element.setAttribute("shortcut", options.shortcut);
|
|
981
|
+
if (options.minQueryLength !== void 0)
|
|
982
|
+
element.setAttribute("min-query-length", String(options.minQueryLength));
|
|
983
|
+
if (options.maxResults !== void 0)
|
|
984
|
+
element.setAttribute("max-results", String(options.maxResults));
|
|
985
|
+
if (options.showDuplicateResults === true) element.setAttribute("show-duplicate-results", "");
|
|
986
|
+
if (options.provider !== void 0) element.provider = options.provider;
|
|
987
|
+
options.target.append(element);
|
|
988
|
+
return element;
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
export { MdSearchElement, createJsonSearchProvider, createSearch, createSearchSnippet, createSearchTerms, createStaticSearchProvider, defineMdSearchElement, normalizeSearchQuery, normalizeSearchableText, searchRecords };
|