@md-plugins/search-ui 0.1.0-rc.9 → 1.0.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/README.md +12 -1
- package/dist/index.d.mts +95 -100
- package/dist/index.d.ts +95 -100
- package/dist/index.mjs +414 -520
- package/package.json +2 -2
package/dist/index.mjs
CHANGED
|
@@ -13,187 +13,147 @@ const markdownListMarkerRE = /^\s*(?:[-*+]|\d+\.)\s+/gm;
|
|
|
13
13
|
const markdownReferenceDefinitionRE = /^\[[^\]]+\]:\s+\S+.*$/gm;
|
|
14
14
|
const spaceBeforePunctuationRE = /\s+([,.;:!?])/g;
|
|
15
15
|
function stripMarkdownSyntax(value) {
|
|
16
|
-
|
|
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
17
|
}
|
|
18
18
|
function normalizeSearchQuery(value) {
|
|
19
|
-
|
|
19
|
+
return value.toLocaleLowerCase().replace(whitespaceRE, " ").trim();
|
|
20
20
|
}
|
|
21
21
|
function createSearchTerms(query) {
|
|
22
|
-
|
|
22
|
+
return Array.from(new Set(normalizeSearchQuery(query).split(" ").filter(Boolean)));
|
|
23
23
|
}
|
|
24
24
|
function normalizeSearchableText(value) {
|
|
25
|
-
|
|
25
|
+
return stripMarkdownSyntax(String(value ?? "")).toLocaleLowerCase().replace(whitespaceRE, " ").trim();
|
|
26
26
|
}
|
|
27
27
|
function createSearchSnippet(content, terms, length = 140) {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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}`;
|
|
28
|
+
const normalizedContent = stripMarkdownSyntax(content).replace(whitespaceRE, " ").trim();
|
|
29
|
+
if (normalizedContent.length <= length) return normalizedContent;
|
|
30
|
+
const lowerContent = normalizedContent.toLocaleLowerCase();
|
|
31
|
+
const firstMatch = terms.map((term) => lowerContent.indexOf(term)).filter((index) => index >= 0).sort((a, b) => a - b)[0];
|
|
32
|
+
if (firstMatch === void 0) return `${normalizedContent.slice(0, length - 1).trim()}…`;
|
|
33
|
+
const start = Math.max(firstMatch - Math.floor(length / 3), 0);
|
|
34
|
+
const end = Math.min(start + length, normalizedContent.length);
|
|
35
|
+
const prefix = start > 0 ? "…" : "";
|
|
36
|
+
const suffix = end < normalizedContent.length ? "…" : "";
|
|
37
|
+
return `${prefix}${normalizedContent.slice(start, end).trim()}${suffix}`;
|
|
42
38
|
}
|
|
43
39
|
|
|
44
40
|
function normalizeIndexPayload(payload) {
|
|
45
|
-
|
|
41
|
+
return Array.isArray(payload) ? payload : payload.records;
|
|
46
42
|
}
|
|
47
43
|
function getSearchFields(record) {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
44
|
+
return {
|
|
45
|
+
title: normalizeSearchableText(record.title),
|
|
46
|
+
section: normalizeSearchableText(record.section),
|
|
47
|
+
hierarchy: normalizeSearchableText(record.hierarchy.join(" ")),
|
|
48
|
+
tags: normalizeSearchableText(record.tags.join(" ")),
|
|
49
|
+
content: normalizeSearchableText(record.content)
|
|
50
|
+
};
|
|
55
51
|
}
|
|
56
52
|
function scoreRecord(record, terms) {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
score += 6;
|
|
87
|
-
} else if (record.type === "heading") {
|
|
88
|
-
score += 4;
|
|
89
|
-
}
|
|
90
|
-
return score;
|
|
53
|
+
const fields = getSearchFields(record);
|
|
54
|
+
let score = 0;
|
|
55
|
+
for (const term of terms) {
|
|
56
|
+
let matched = false;
|
|
57
|
+
if (fields.title.includes(term)) {
|
|
58
|
+
score += fields.title.startsWith(term) ? 36 : 28;
|
|
59
|
+
matched = true;
|
|
60
|
+
}
|
|
61
|
+
if (fields.section.includes(term)) {
|
|
62
|
+
score += fields.section.startsWith(term) ? 24 : 18;
|
|
63
|
+
matched = true;
|
|
64
|
+
}
|
|
65
|
+
if (fields.hierarchy.includes(term)) {
|
|
66
|
+
score += 12;
|
|
67
|
+
matched = true;
|
|
68
|
+
}
|
|
69
|
+
if (fields.tags.includes(term)) {
|
|
70
|
+
score += 10;
|
|
71
|
+
matched = true;
|
|
72
|
+
}
|
|
73
|
+
if (fields.content.includes(term)) {
|
|
74
|
+
score += record.type === "content" ? 8 : 5;
|
|
75
|
+
matched = true;
|
|
76
|
+
}
|
|
77
|
+
if (!matched) return 0;
|
|
78
|
+
}
|
|
79
|
+
if (record.type === "page") score += 6;
|
|
80
|
+
else if (record.type === "heading") score += 4;
|
|
81
|
+
return score;
|
|
91
82
|
}
|
|
92
83
|
function hasUsefulContent(result) {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
}
|
|
97
|
-
return content !== normalizeSearchableText(result.title) && content !== normalizeSearchableText(result.section);
|
|
84
|
+
const content = normalizeSearchableText(result.content);
|
|
85
|
+
if (content === "") return false;
|
|
86
|
+
return content !== normalizeSearchableText(result.title) && content !== normalizeSearchableText(result.section);
|
|
98
87
|
}
|
|
99
88
|
function preferDuplicateResult(current, candidate) {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
return candidate;
|
|
105
|
-
}
|
|
106
|
-
if (candidate.score > current.score) {
|
|
107
|
-
return candidate;
|
|
108
|
-
}
|
|
109
|
-
return current;
|
|
89
|
+
if (current.type === "content" && candidate.type !== "content" && hasUsefulContent(current)) return current;
|
|
90
|
+
if (candidate.type === "content" && current.type !== "content" && hasUsefulContent(candidate)) return candidate;
|
|
91
|
+
if (candidate.score > current.score) return candidate;
|
|
92
|
+
return current;
|
|
110
93
|
}
|
|
111
94
|
function collapseDuplicateResults(results) {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
(a, b) => b.score - a.score || a.title.localeCompare(b.title)
|
|
119
|
-
);
|
|
95
|
+
const byUrl = /* @__PURE__ */ new Map();
|
|
96
|
+
for (const result of results) {
|
|
97
|
+
const current = byUrl.get(result.url);
|
|
98
|
+
byUrl.set(result.url, current === void 0 ? result : preferDuplicateResult(current, result));
|
|
99
|
+
}
|
|
100
|
+
return Array.from(byUrl.values()).sort((a, b) => b.score - a.score || a.title.localeCompare(b.title));
|
|
120
101
|
}
|
|
121
102
|
function searchRecords(records, query, options = {}) {
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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);
|
|
103
|
+
const terms = createSearchTerms(query);
|
|
104
|
+
if (terms.length === 0) return [];
|
|
105
|
+
const results = records.map((record) => {
|
|
106
|
+
const score = scoreRecord(record, terms);
|
|
107
|
+
if (score === 0) return;
|
|
108
|
+
return {
|
|
109
|
+
id: record.id,
|
|
110
|
+
title: record.title,
|
|
111
|
+
url: record.url,
|
|
112
|
+
content: createSearchSnippet(record.content, terms),
|
|
113
|
+
score,
|
|
114
|
+
type: record.type,
|
|
115
|
+
path: record.path,
|
|
116
|
+
section: record.section,
|
|
117
|
+
hierarchy: record.hierarchy,
|
|
118
|
+
tags: record.tags,
|
|
119
|
+
record
|
|
120
|
+
};
|
|
121
|
+
}).filter((result) => result !== void 0).sort((a, b) => b.score - a.score || a.title.localeCompare(b.title));
|
|
122
|
+
return (options.collapseDuplicateResults === false ? results : collapseDuplicateResults(results)).slice(0, options.limit ?? 12);
|
|
147
123
|
}
|
|
148
124
|
function createStaticSearchProvider(records) {
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
}
|
|
153
|
-
};
|
|
125
|
+
return { search(query, options) {
|
|
126
|
+
return searchRecords(records, query, options);
|
|
127
|
+
} };
|
|
154
128
|
}
|
|
155
129
|
function createJsonSearchProvider(options = {}) {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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
|
-
};
|
|
130
|
+
let recordsPromise;
|
|
131
|
+
async function loadRecords() {
|
|
132
|
+
if (options.index !== void 0) return normalizeIndexPayload(options.index);
|
|
133
|
+
if (options.src === void 0) throw new Error("createJsonSearchProvider requires either \"src\" or \"index\".");
|
|
134
|
+
const src = options.src;
|
|
135
|
+
recordsPromise ??= Promise.resolve().then(async () => {
|
|
136
|
+
const fetcher = options.fetcher ?? globalThis.fetch;
|
|
137
|
+
if (fetcher === void 0) throw new Error("No fetch implementation is available for loading the search index.");
|
|
138
|
+
const response = await fetcher(src);
|
|
139
|
+
if (!response.ok) throw new Error(`Unable to load search index: ${response.status} ${response.statusText}`);
|
|
140
|
+
const responseText = await response.text();
|
|
141
|
+
try {
|
|
142
|
+
return normalizeIndexPayload(JSON.parse(responseText));
|
|
143
|
+
} catch {
|
|
144
|
+
const contentType = response.headers.get("content-type") ?? "unknown content type";
|
|
145
|
+
const preview = responseText.trim().slice(0, 120);
|
|
146
|
+
throw new Error(`Unable to parse search index JSON from ${src}. Received ${contentType}${preview ? `: ${preview}` : "."}`);
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
return recordsPromise;
|
|
150
|
+
}
|
|
151
|
+
return { async search(query, searchOptions) {
|
|
152
|
+
return searchRecords(await loadRecords(), query, searchOptions);
|
|
153
|
+
} };
|
|
192
154
|
}
|
|
193
155
|
|
|
194
|
-
const templateStyles =
|
|
195
|
-
/* css */
|
|
196
|
-
`
|
|
156
|
+
const templateStyles = `
|
|
197
157
|
:host {
|
|
198
158
|
color-scheme: var(--md-search-color-scheme, light);
|
|
199
159
|
--md-search-z-index: 5000;
|
|
@@ -530,342 +490,283 @@ const templateStyles = (
|
|
|
530
490
|
display: none;
|
|
531
491
|
}
|
|
532
492
|
}
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
const icon = "\u2315";
|
|
493
|
+
`;
|
|
494
|
+
const icon = "⌕";
|
|
536
495
|
function getShortcutLabel(shortcut) {
|
|
537
|
-
|
|
538
|
-
|
|
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;
|
|
496
|
+
if (shortcut === "none") return "";
|
|
497
|
+
if (shortcut === "mod+k") return /Mac|iPhone|iPad|iPod/i.test(globalThis.navigator?.platform ?? "") ? "⌘K" : "Ctrl K";
|
|
498
|
+
return shortcut;
|
|
545
499
|
}
|
|
546
500
|
function escapeHtml(value) {
|
|
547
|
-
|
|
501
|
+
return String(value ?? "").replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll("\"", """).replaceAll("'", "'");
|
|
548
502
|
}
|
|
549
503
|
function escapeRegExp(value) {
|
|
550
|
-
|
|
504
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
551
505
|
}
|
|
552
506
|
function renderHighlightedText(value, terms) {
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
output += `<mark>${escapeHtml(match[0])}</mark>`;
|
|
567
|
-
lastIndex = index + match[0].length;
|
|
568
|
-
}
|
|
569
|
-
output += escapeHtml(text.slice(lastIndex));
|
|
570
|
-
return output;
|
|
507
|
+
const text = String(value ?? "");
|
|
508
|
+
if (text === "" || terms.length === 0) return escapeHtml(text);
|
|
509
|
+
const pattern = new RegExp(terms.filter(Boolean).sort((a, b) => b.length - a.length).map(escapeRegExp).join("|"), "gi");
|
|
510
|
+
let output = "";
|
|
511
|
+
let lastIndex = 0;
|
|
512
|
+
for (const match of text.matchAll(pattern)) {
|
|
513
|
+
const index = match.index ?? 0;
|
|
514
|
+
output += escapeHtml(text.slice(lastIndex, index));
|
|
515
|
+
output += `<mark>${escapeHtml(match[0])}</mark>`;
|
|
516
|
+
lastIndex = index + match[0].length;
|
|
517
|
+
}
|
|
518
|
+
output += escapeHtml(text.slice(lastIndex));
|
|
519
|
+
return output;
|
|
571
520
|
}
|
|
572
521
|
function formatResultType(type) {
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
if (type === "heading") {
|
|
577
|
-
return "heading";
|
|
578
|
-
}
|
|
579
|
-
return "content";
|
|
522
|
+
if (type === "page") return "page";
|
|
523
|
+
if (type === "heading") return "heading";
|
|
524
|
+
return "content";
|
|
580
525
|
}
|
|
581
526
|
function getResultTrail(result) {
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
if (result.section !== void 0 && !trail.includes(result.section)) {
|
|
586
|
-
return [...trail, result.section];
|
|
587
|
-
}
|
|
588
|
-
return trail;
|
|
527
|
+
const trail = (result.hierarchy.length > 0 ? result.hierarchy : [result.title]).filter((entry, index, entries) => entry !== "" && entry !== entries[index - 1]);
|
|
528
|
+
if (result.section !== void 0 && !trail.includes(result.section)) return [...trail, result.section];
|
|
529
|
+
return trail;
|
|
589
530
|
}
|
|
590
531
|
function renderResultTrail(result, terms) {
|
|
591
|
-
|
|
532
|
+
return getResultTrail(result).filter(Boolean).map((entry) => `<span>${renderHighlightedText(entry, terms)}</span>`).join("<span class=\"result__separator\" aria-hidden=\"true\">›</span>");
|
|
592
533
|
}
|
|
593
|
-
const
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
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 `
|
|
534
|
+
const HTMLElementBase = globalThis.HTMLElement ?? class MdSearchServerElement {};
|
|
535
|
+
var MdSearchElement = class extends HTMLElementBase {
|
|
536
|
+
static observedAttributes = [
|
|
537
|
+
"src",
|
|
538
|
+
"placeholder",
|
|
539
|
+
"trigger-label",
|
|
540
|
+
"panel-title",
|
|
541
|
+
"shortcut",
|
|
542
|
+
"theme",
|
|
543
|
+
"search-label",
|
|
544
|
+
"min-query-length",
|
|
545
|
+
"max-results",
|
|
546
|
+
"show-duplicate-results"
|
|
547
|
+
];
|
|
548
|
+
provider;
|
|
549
|
+
root = this.attachShadow({ mode: "open" });
|
|
550
|
+
elementId = `md-search-${Math.random().toString(36).slice(2)}`;
|
|
551
|
+
results = [];
|
|
552
|
+
query = "";
|
|
553
|
+
opened = false;
|
|
554
|
+
loading = false;
|
|
555
|
+
errorMessage = "";
|
|
556
|
+
activeIndex = 0;
|
|
557
|
+
searchTimer;
|
|
558
|
+
searchRequestId = 0;
|
|
559
|
+
selectionStart = null;
|
|
560
|
+
selectionEnd = null;
|
|
561
|
+
connectedCallback() {
|
|
562
|
+
this.render();
|
|
563
|
+
globalThis.addEventListener?.("keydown", this.onGlobalKeydown);
|
|
564
|
+
}
|
|
565
|
+
disconnectedCallback() {
|
|
566
|
+
globalThis.removeEventListener?.("keydown", this.onGlobalKeydown);
|
|
567
|
+
clearTimeout(this.searchTimer);
|
|
568
|
+
}
|
|
569
|
+
attributeChangedCallback(name) {
|
|
570
|
+
if (name === "src") this.provider = void 0;
|
|
571
|
+
if (name === "show-duplicate-results" && this.query.trim().length >= this.minQueryLength) {
|
|
572
|
+
this.runSearch();
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
575
|
+
this.render();
|
|
576
|
+
}
|
|
577
|
+
open() {
|
|
578
|
+
this.opened = true;
|
|
579
|
+
this.render();
|
|
580
|
+
this.focusInput();
|
|
581
|
+
}
|
|
582
|
+
close() {
|
|
583
|
+
this.opened = false;
|
|
584
|
+
this.render();
|
|
585
|
+
}
|
|
586
|
+
toggle() {
|
|
587
|
+
if (this.opened) this.close();
|
|
588
|
+
else this.open();
|
|
589
|
+
}
|
|
590
|
+
get src() {
|
|
591
|
+
return this.getAttribute("src") ?? void 0;
|
|
592
|
+
}
|
|
593
|
+
get placeholder() {
|
|
594
|
+
return this.getAttribute("placeholder") ?? "Search docs...";
|
|
595
|
+
}
|
|
596
|
+
get triggerLabel() {
|
|
597
|
+
return this.getAttribute("trigger-label") ?? "Search";
|
|
598
|
+
}
|
|
599
|
+
get panelTitle() {
|
|
600
|
+
return this.getAttribute("panel-title") ?? "Search";
|
|
601
|
+
}
|
|
602
|
+
get searchLabel() {
|
|
603
|
+
return this.getAttribute("search-label") ?? this.panelTitle;
|
|
604
|
+
}
|
|
605
|
+
get shortcut() {
|
|
606
|
+
return this.getAttribute("shortcut") ?? "mod+k";
|
|
607
|
+
}
|
|
608
|
+
get minQueryLength() {
|
|
609
|
+
return Number(this.getAttribute("min-query-length") ?? 2);
|
|
610
|
+
}
|
|
611
|
+
get maxResults() {
|
|
612
|
+
return Number(this.getAttribute("max-results") ?? 12);
|
|
613
|
+
}
|
|
614
|
+
get showDuplicateResults() {
|
|
615
|
+
return this.hasAttribute("show-duplicate-results");
|
|
616
|
+
}
|
|
617
|
+
get activeResult() {
|
|
618
|
+
return this.results[this.activeIndex];
|
|
619
|
+
}
|
|
620
|
+
getProvider() {
|
|
621
|
+
this.provider ??= createJsonSearchProvider({ src: this.src });
|
|
622
|
+
return this.provider;
|
|
623
|
+
}
|
|
624
|
+
onGlobalKeydown = (event) => {
|
|
625
|
+
if (this.shortcut !== "mod+k") return;
|
|
626
|
+
if ((event.ctrlKey || event.metaKey) && event.key.toLocaleLowerCase() === "k") {
|
|
627
|
+
event.preventDefault();
|
|
628
|
+
this.open();
|
|
629
|
+
}
|
|
630
|
+
};
|
|
631
|
+
onInput = (event) => {
|
|
632
|
+
const input = event.target;
|
|
633
|
+
this.query = input.value;
|
|
634
|
+
this.selectionStart = input.selectionStart;
|
|
635
|
+
this.selectionEnd = input.selectionEnd;
|
|
636
|
+
this.searchRequestId++;
|
|
637
|
+
clearTimeout(this.searchTimer);
|
|
638
|
+
if (this.query.trim().length < this.minQueryLength) {
|
|
639
|
+
this.results = [];
|
|
640
|
+
this.loading = false;
|
|
641
|
+
this.errorMessage = "";
|
|
642
|
+
this.activeIndex = 0;
|
|
643
|
+
this.renderResultsContainer();
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
646
|
+
this.loading = true;
|
|
647
|
+
this.errorMessage = "";
|
|
648
|
+
this.renderResultsContainer();
|
|
649
|
+
this.searchTimer = setTimeout(() => {
|
|
650
|
+
this.runSearch();
|
|
651
|
+
}, 120);
|
|
652
|
+
};
|
|
653
|
+
onDialogKeydown = (event) => {
|
|
654
|
+
if (event.key === "Escape") {
|
|
655
|
+
event.preventDefault();
|
|
656
|
+
this.close();
|
|
657
|
+
return;
|
|
658
|
+
}
|
|
659
|
+
if (event.key === "ArrowDown") {
|
|
660
|
+
event.preventDefault();
|
|
661
|
+
this.setActiveIndex(this.activeIndex + 1, { scrollIntoView: true });
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
664
|
+
if (event.key === "ArrowUp") {
|
|
665
|
+
event.preventDefault();
|
|
666
|
+
this.setActiveIndex(this.activeIndex - 1, { scrollIntoView: true });
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
669
|
+
if (event.key === "Enter" && this.activeResult !== void 0) {
|
|
670
|
+
event.preventDefault();
|
|
671
|
+
this.selectResult(this.activeResult);
|
|
672
|
+
}
|
|
673
|
+
};
|
|
674
|
+
async runSearch() {
|
|
675
|
+
const requestId = ++this.searchRequestId;
|
|
676
|
+
try {
|
|
677
|
+
const results = await this.getProvider().search(this.query, {
|
|
678
|
+
limit: this.maxResults,
|
|
679
|
+
collapseDuplicateResults: !this.showDuplicateResults
|
|
680
|
+
});
|
|
681
|
+
if (requestId !== this.searchRequestId) return;
|
|
682
|
+
this.results = results;
|
|
683
|
+
this.loading = false;
|
|
684
|
+
this.errorMessage = "";
|
|
685
|
+
this.activeIndex = 0;
|
|
686
|
+
} catch (error) {
|
|
687
|
+
if (requestId !== this.searchRequestId) return;
|
|
688
|
+
this.results = [];
|
|
689
|
+
this.loading = false;
|
|
690
|
+
this.errorMessage = error instanceof Error ? error.message : "Search failed.";
|
|
691
|
+
}
|
|
692
|
+
this.renderResultsContainer();
|
|
693
|
+
}
|
|
694
|
+
focusInput(options = {}) {
|
|
695
|
+
const { selectionStart, selectionEnd } = this;
|
|
696
|
+
requestAnimationFrame(() => {
|
|
697
|
+
const input = this.root.querySelector("[part=\"input\"]");
|
|
698
|
+
input?.focus();
|
|
699
|
+
if (input !== null && options.restoreSelection === true && selectionStart !== null && selectionEnd !== null) input.setSelectionRange(selectionStart, selectionEnd);
|
|
700
|
+
});
|
|
701
|
+
}
|
|
702
|
+
setActiveIndex(index, options = {}) {
|
|
703
|
+
if (this.results.length === 0) return;
|
|
704
|
+
this.activeIndex = Math.min(Math.max(index, 0), this.results.length - 1);
|
|
705
|
+
this.syncActiveResult(options);
|
|
706
|
+
}
|
|
707
|
+
syncInputAria() {
|
|
708
|
+
const input = this.root.querySelector("[part=\"input\"]");
|
|
709
|
+
if (input === null) return;
|
|
710
|
+
input.setAttribute("aria-expanded", this.results.length > 0 ? "true" : "false");
|
|
711
|
+
if (this.results.length === 0) {
|
|
712
|
+
input.removeAttribute("aria-activedescendant");
|
|
713
|
+
return;
|
|
714
|
+
}
|
|
715
|
+
input.setAttribute("aria-activedescendant", `${this.elementId}-result-${this.activeIndex}`);
|
|
716
|
+
}
|
|
717
|
+
syncActiveResult(options = {}) {
|
|
718
|
+
this.syncInputAria();
|
|
719
|
+
this.root.querySelectorAll("[data-result-index]").forEach((element) => {
|
|
720
|
+
const isActive = Number(element.dataset.resultIndex ?? -1) === this.activeIndex;
|
|
721
|
+
element.classList.toggle("result--active", isActive);
|
|
722
|
+
element.setAttribute("aria-selected", isActive ? "true" : "false");
|
|
723
|
+
if (isActive && options.scrollIntoView === true) element.scrollIntoView({ block: "nearest" });
|
|
724
|
+
});
|
|
725
|
+
}
|
|
726
|
+
selectResult(result) {
|
|
727
|
+
const event = new CustomEvent("md-search-select", {
|
|
728
|
+
bubbles: true,
|
|
729
|
+
cancelable: true,
|
|
730
|
+
composed: true,
|
|
731
|
+
detail: { result }
|
|
732
|
+
});
|
|
733
|
+
const shouldNavigate = this.dispatchEvent(event);
|
|
734
|
+
this.close();
|
|
735
|
+
if (shouldNavigate) globalThis.location.assign(result.url);
|
|
736
|
+
}
|
|
737
|
+
bindResultEvents() {
|
|
738
|
+
this.root.querySelectorAll("[data-result-index]").forEach((element) => {
|
|
739
|
+
element.addEventListener("mouseenter", () => {
|
|
740
|
+
this.setActiveIndex(Number(element.dataset.resultIndex ?? 0));
|
|
741
|
+
});
|
|
742
|
+
element.addEventListener("click", () => {
|
|
743
|
+
const result = this.results[Number(element.dataset.resultIndex ?? 0)];
|
|
744
|
+
if (result !== void 0) this.selectResult(result);
|
|
745
|
+
});
|
|
746
|
+
});
|
|
747
|
+
}
|
|
748
|
+
renderResultsContainer() {
|
|
749
|
+
const container = this.root.querySelector("[part=\"results\"]");
|
|
750
|
+
if (container === null) {
|
|
751
|
+
this.render();
|
|
752
|
+
return;
|
|
753
|
+
}
|
|
754
|
+
container.innerHTML = this.renderResults();
|
|
755
|
+
this.bindResultEvents();
|
|
756
|
+
this.syncInputAria();
|
|
757
|
+
}
|
|
758
|
+
renderResults() {
|
|
759
|
+
if (this.loading) return "<div part=\"status\" class=\"status\" role=\"status\" aria-live=\"polite\">Searching...</div>";
|
|
760
|
+
if (this.errorMessage !== "") return `<div part="status" class="status" role="status" aria-live="assertive">${escapeHtml(this.errorMessage)}</div>`;
|
|
761
|
+
if (this.query.trim().length < this.minQueryLength) return `<div part="status" class="status" role="status" aria-live="polite">Type at least ${this.minQueryLength} characters.</div>`;
|
|
762
|
+
if (this.results.length === 0) return "<div part=\"status\" class=\"status\" role=\"status\" aria-live=\"polite\">No results found.</div>";
|
|
763
|
+
const terms = createSearchTerms(this.query);
|
|
764
|
+
return `
|
|
863
765
|
<div part="list" class="list" id="${this.elementId}-listbox" role="listbox">
|
|
864
766
|
${this.results.map((result, index) => {
|
|
865
|
-
|
|
866
|
-
return `
|
|
767
|
+
return `
|
|
867
768
|
<button
|
|
868
|
-
id="${
|
|
769
|
+
id="${`${this.elementId}-result-${index}`}"
|
|
869
770
|
type="button"
|
|
870
771
|
part="result"
|
|
871
772
|
class="result ${index === this.activeIndex ? "result--active" : ""}"
|
|
@@ -881,16 +782,16 @@ class MdSearchElement extends HTMLElementBase {
|
|
|
881
782
|
<div part="result-url" class="result__path">${escapeHtml(result.url)}</div>
|
|
882
783
|
</button>
|
|
883
784
|
`;
|
|
884
|
-
|
|
785
|
+
}).join("")}
|
|
885
786
|
</div>
|
|
886
787
|
`;
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
788
|
+
}
|
|
789
|
+
render() {
|
|
790
|
+
const shortcutLabel = getShortcutLabel(this.shortcut);
|
|
791
|
+
const dialogId = `${this.elementId}-dialog`;
|
|
792
|
+
const titleId = `${this.elementId}-title`;
|
|
793
|
+
const activeResultId = this.results.length > 0 ? `${this.elementId}-result-${this.activeIndex}` : void 0;
|
|
794
|
+
this.root.innerHTML = `
|
|
894
795
|
<style>${templateStyles}</style>
|
|
895
796
|
<button
|
|
896
797
|
type="button"
|
|
@@ -917,7 +818,7 @@ class MdSearchElement extends HTMLElementBase {
|
|
|
917
818
|
<div class="header">
|
|
918
819
|
<div class="title-row">
|
|
919
820
|
<h2 id="${titleId}" part="title" class="title">${escapeHtml(this.panelTitle)}</h2>
|
|
920
|
-
<button type="button" part="close-button" class="close" aria-label="Close search"
|
|
821
|
+
<button type="button" part="close-button" class="close" aria-label="Close search">✕</button>
|
|
921
822
|
</div>
|
|
922
823
|
<input
|
|
923
824
|
part="input"
|
|
@@ -935,8 +836,8 @@ class MdSearchElement extends HTMLElementBase {
|
|
|
935
836
|
spellcheck="false"
|
|
936
837
|
/>
|
|
937
838
|
<div part="help" class="help" aria-hidden="true">
|
|
938
|
-
<span class="help__item">Navigate <kbd class="kbd"
|
|
939
|
-
<span class="help__item">Select <kbd class="kbd"
|
|
839
|
+
<span class="help__item">Navigate <kbd class="kbd">↓</kbd><kbd class="kbd">↑</kbd></span>
|
|
840
|
+
<span class="help__item">Select <kbd class="kbd">↵</kbd></span>
|
|
940
841
|
<span class="help__item">Close <kbd class="kbd">Esc</kbd></span>
|
|
941
842
|
</div>
|
|
942
843
|
</div>
|
|
@@ -946,46 +847,39 @@ class MdSearchElement extends HTMLElementBase {
|
|
|
946
847
|
</section>
|
|
947
848
|
</div>
|
|
948
849
|
`;
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
}
|
|
963
|
-
}
|
|
850
|
+
this.bindEvents();
|
|
851
|
+
}
|
|
852
|
+
bindEvents() {
|
|
853
|
+
this.root.querySelector("[part=\"trigger\"]")?.addEventListener("click", () => this.open());
|
|
854
|
+
this.root.querySelector("[part=\"close-button\"]")?.addEventListener("click", () => this.close());
|
|
855
|
+
this.root.querySelector("[part=\"backdrop\"]")?.addEventListener("click", (event) => {
|
|
856
|
+
if (event.target === event.currentTarget) this.close();
|
|
857
|
+
});
|
|
858
|
+
this.root.querySelector("[part=\"dialog\"]")?.addEventListener("keydown", this.onDialogKeydown);
|
|
859
|
+
this.root.querySelector("[part=\"input\"]")?.addEventListener("input", this.onInput);
|
|
860
|
+
this.bindResultEvents();
|
|
861
|
+
}
|
|
862
|
+
};
|
|
964
863
|
function defineMdSearchElement(name = "md-search") {
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
}
|
|
968
|
-
globalThis.customElements.define(name, MdSearchElement);
|
|
864
|
+
if (globalThis.customElements === void 0 || globalThis.customElements.get(name) !== void 0) return;
|
|
865
|
+
globalThis.customElements.define(name, MdSearchElement);
|
|
969
866
|
}
|
|
970
867
|
function createSearch(options) {
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
if (options.provider !== void 0) element.provider = options.provider;
|
|
987
|
-
options.target.append(element);
|
|
988
|
-
return element;
|
|
868
|
+
defineMdSearchElement();
|
|
869
|
+
const element = document.createElement("md-search");
|
|
870
|
+
if (options.src !== void 0) element.setAttribute("src", options.src);
|
|
871
|
+
if (options.placeholder !== void 0) element.setAttribute("placeholder", options.placeholder);
|
|
872
|
+
if (options.triggerLabel !== void 0) element.setAttribute("trigger-label", options.triggerLabel);
|
|
873
|
+
if (options.panelTitle !== void 0) element.setAttribute("panel-title", options.panelTitle);
|
|
874
|
+
if (options.searchLabel !== void 0) element.setAttribute("search-label", options.searchLabel);
|
|
875
|
+
if (options.theme !== void 0) element.setAttribute("theme", options.theme);
|
|
876
|
+
if (options.shortcut !== void 0) element.setAttribute("shortcut", options.shortcut);
|
|
877
|
+
if (options.minQueryLength !== void 0) element.setAttribute("min-query-length", String(options.minQueryLength));
|
|
878
|
+
if (options.maxResults !== void 0) element.setAttribute("max-results", String(options.maxResults));
|
|
879
|
+
if (options.showDuplicateResults === true) element.setAttribute("show-duplicate-results", "");
|
|
880
|
+
if (options.provider !== void 0) element.provider = options.provider;
|
|
881
|
+
options.target.append(element);
|
|
882
|
+
return element;
|
|
989
883
|
}
|
|
990
884
|
|
|
991
|
-
export { MdSearchElement, createJsonSearchProvider, createSearch, createSearchSnippet, createSearchTerms, createStaticSearchProvider, defineMdSearchElement, normalizeSearchQuery, normalizeSearchableText, searchRecords };
|
|
885
|
+
export { MdSearchElement, createJsonSearchProvider, createSearch, createSearchSnippet, createSearchTerms, createStaticSearchProvider, defineMdSearchElement, normalizeSearchQuery, normalizeSearchableText, searchRecords };
|