jekyll-client-search 0.1.0 → 0.3.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +58 -0
- data/README.developer.md +272 -10
- data/README.md +224 -4
- data/assets/client-search-base.js +53 -1
- data/assets/client-search-dropdown.js +434 -0
- data/assets/client-search-related.js +10 -0
- data/lib/jekyll/client_search/configuration.rb +40 -13
- data/lib/jekyll/client_search/configuration_accessors.rb +12 -4
- data/lib/jekyll/client_search/document_builder.rb +23 -5
- data/lib/jekyll/client_search/dropdown_configuration.rb +92 -0
- data/lib/jekyll/client_search/dropdown_tag.rb +115 -0
- data/lib/jekyll/client_search/generator.rb +6 -2
- data/lib/jekyll/client_search/related_configuration.rb +1 -1
- data/lib/jekyll/client_search/related_tag.rb +30 -8
- data/lib/jekyll/client_search/runtime_config_page.rb +5 -1
- data/lib/jekyll/client_search/search_tag.rb +23 -6
- data/lib/jekyll/client_search/version.rb +1 -1
- data/lib/jekyll/client_search.rb +2 -0
- metadata +4 -1
|
@@ -58,6 +58,12 @@
|
|
|
58
58
|
return;
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
var coreFields = [
|
|
62
|
+
"id", "title", "url", "excerpt", "content",
|
|
63
|
+
"categories", "tags", "categoriesText", "tagsText",
|
|
64
|
+
"date", "date_timestamp", "embedding"
|
|
65
|
+
];
|
|
66
|
+
|
|
61
67
|
function normalize(entry) {
|
|
62
68
|
var id = entry.id || entry.url;
|
|
63
69
|
if (!id) {
|
|
@@ -78,9 +84,20 @@
|
|
|
78
84
|
categoriesText: categories.join(" "),
|
|
79
85
|
tagsText: tags.join(" ")
|
|
80
86
|
};
|
|
87
|
+
if (entry.source) {
|
|
88
|
+
normalized.source = entry.source;
|
|
89
|
+
}
|
|
81
90
|
if (Array.isArray(entry.embedding) && entry.embedding.length > 0) {
|
|
82
91
|
normalized.embedding = entry.embedding;
|
|
83
92
|
}
|
|
93
|
+
Object.keys(entry).forEach(function (key) {
|
|
94
|
+
if (coreFields.indexOf(key) === -1 && key !== "source" && !(key in normalized)) {
|
|
95
|
+
var value = entry[key];
|
|
96
|
+
if (value !== null && value !== undefined && value !== "") {
|
|
97
|
+
normalized[key] = value;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
});
|
|
84
101
|
return normalized;
|
|
85
102
|
}
|
|
86
103
|
|
|
@@ -143,17 +160,52 @@
|
|
|
143
160
|
var url = safeUrl(entry.url);
|
|
144
161
|
|
|
145
162
|
article.className = "box client-search-result";
|
|
163
|
+
if (entry.source) {
|
|
164
|
+
article.dataset.source = entry.source;
|
|
165
|
+
}
|
|
166
|
+
if (entry.categories.length) {
|
|
167
|
+
article.dataset.categories = entry.categories.join(" ");
|
|
168
|
+
}
|
|
169
|
+
if (entry.tags.length) {
|
|
170
|
+
article.dataset.tags = entry.tags.join(" ");
|
|
171
|
+
}
|
|
172
|
+
Object.keys(entry).forEach(function (key) {
|
|
173
|
+
if (coreFields.indexOf(key) !== -1 || key === "source") {
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
var value = entry[key];
|
|
177
|
+
if (typeof value === "string" || typeof value === "number") {
|
|
178
|
+
article.dataset[toCamelCase(key)] = String(value);
|
|
179
|
+
}
|
|
180
|
+
});
|
|
146
181
|
heading.className = "title is-4";
|
|
147
182
|
titleLink.href = url;
|
|
148
183
|
titleLink.textContent = entry.title;
|
|
184
|
+
heading.appendChild(titleLink);
|
|
185
|
+
var iconField = options.iconField;
|
|
186
|
+
if (iconField && entry[iconField]) {
|
|
187
|
+
var icon = document.createElement("img");
|
|
188
|
+
icon.className = "client-search-result-icon";
|
|
189
|
+
icon.src = safeUrl(entry[iconField]);
|
|
190
|
+
icon.alt = entry.file_type || entry.source || "";
|
|
191
|
+
icon.loading = "lazy";
|
|
192
|
+
icon.style.width = "1em";
|
|
193
|
+
icon.style.height = "1em";
|
|
194
|
+
icon.style.verticalAlign = "middle";
|
|
195
|
+
icon.style.marginRight = "0.3em";
|
|
196
|
+
heading.insertBefore(icon, titleLink);
|
|
197
|
+
}
|
|
149
198
|
excerpt.textContent = entry.excerpt;
|
|
150
199
|
readMore.href = url;
|
|
151
200
|
readMore.textContent = "Read more";
|
|
152
|
-
heading.appendChild(titleLink);
|
|
153
201
|
article.append(heading, excerpt, readMore);
|
|
154
202
|
return article;
|
|
155
203
|
}
|
|
156
204
|
|
|
205
|
+
function toCamelCase(key) {
|
|
206
|
+
return key.replace(/_([a-z])/g, function (_, char) { return char.toUpperCase(); });
|
|
207
|
+
}
|
|
208
|
+
|
|
157
209
|
function sortMatches(matches) {
|
|
158
210
|
var sortControl = document.querySelector(options.sortControl);
|
|
159
211
|
var sortOrder = sortControl ? sortControl.value : options.sort;
|
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
(function () {
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* ClientSearch dropdown runtime — compact live-search dropdown for
|
|
6
|
+
* navbars and headers. Framework-agnostic: emits semantic HTML with
|
|
7
|
+
* data attributes, no CSS classes from any framework.
|
|
8
|
+
*
|
|
9
|
+
* Features:
|
|
10
|
+
* - Lazy index loading (fetches search-index.json on first keystroke)
|
|
11
|
+
* - Two-stage search (AND first, fuzzy OR fallback) via engine adapter
|
|
12
|
+
* - Compact <li><a> items with optional icon rendering
|
|
13
|
+
* - Keyboard navigation (Arrow Up/Down, Enter, Escape)
|
|
14
|
+
* - Enter with no selection → redirect to redirect_url (?q=...)
|
|
15
|
+
* - Click outside or Escape → close dropdown
|
|
16
|
+
* - Multi-instance via [data-client-search-dropdown] attributes
|
|
17
|
+
* - Shared index cache with the base runtime when present
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
var sharedIndex = null;
|
|
21
|
+
var sharedDocuments = null;
|
|
22
|
+
var indexLoadPromise = null;
|
|
23
|
+
|
|
24
|
+
function safeUrl(value) {
|
|
25
|
+
if (value === "#") {
|
|
26
|
+
return "#";
|
|
27
|
+
}
|
|
28
|
+
try {
|
|
29
|
+
var url = new URL(value, window.location.origin);
|
|
30
|
+
if (url.origin === window.location.origin &&
|
|
31
|
+
["http:", "https:"].includes(url.protocol)) {
|
|
32
|
+
return url.pathname + url.search + url.hash;
|
|
33
|
+
}
|
|
34
|
+
} catch (_error) {
|
|
35
|
+
return "#";
|
|
36
|
+
}
|
|
37
|
+
return "#";
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function toCamelCase(key) {
|
|
41
|
+
return key.replace(/_([a-z])/g, function (_, char) { return char.toUpperCase(); });
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function normalize(entry) {
|
|
45
|
+
var id = entry.id || entry.url;
|
|
46
|
+
if (!id) {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
var categories = Array.isArray(entry.categories) ? entry.categories : [];
|
|
50
|
+
var tags = Array.isArray(entry.tags) ? entry.tags : [];
|
|
51
|
+
var normalized = {
|
|
52
|
+
id: id,
|
|
53
|
+
title: entry.title || "Untitled",
|
|
54
|
+
url: entry.url || "#",
|
|
55
|
+
excerpt: entry.excerpt || "",
|
|
56
|
+
content: entry.content || "",
|
|
57
|
+
date: entry.date || "",
|
|
58
|
+
date_timestamp: Number(entry.date_timestamp) || 0,
|
|
59
|
+
categories: categories,
|
|
60
|
+
tags: tags,
|
|
61
|
+
categoriesText: categories.join(" "),
|
|
62
|
+
tagsText: tags.join(" ")
|
|
63
|
+
};
|
|
64
|
+
if (entry.source) {
|
|
65
|
+
normalized.source = entry.source;
|
|
66
|
+
}
|
|
67
|
+
Object.keys(entry).forEach(function (key) {
|
|
68
|
+
if (!(key in normalized) && key !== "embedding") {
|
|
69
|
+
var value = entry[key];
|
|
70
|
+
if (value !== null && value !== undefined && value !== "") {
|
|
71
|
+
normalized[key] = value;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
return normalized;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function loadIndex(config) {
|
|
79
|
+
if (sharedIndex && sharedDocuments) {
|
|
80
|
+
return Promise.resolve({ index: sharedIndex, documents: sharedDocuments });
|
|
81
|
+
}
|
|
82
|
+
if (indexLoadPromise) {
|
|
83
|
+
return indexLoadPromise;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
var indexUrl = config.indexUrl;
|
|
87
|
+
if (!indexUrl) {
|
|
88
|
+
return Promise.reject(new Error("No index URL configured"));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
indexLoadPromise = fetch(indexUrl, { headers: { Accept: "application/json" } })
|
|
92
|
+
.then(function (response) {
|
|
93
|
+
if (!response.ok) {
|
|
94
|
+
throw new Error("Unable to load search index");
|
|
95
|
+
}
|
|
96
|
+
return response.json();
|
|
97
|
+
})
|
|
98
|
+
.then(function (data) {
|
|
99
|
+
if (!Array.isArray(data)) {
|
|
100
|
+
throw new TypeError("Search index must be an array");
|
|
101
|
+
}
|
|
102
|
+
var documents = data.map(normalize).filter(Boolean);
|
|
103
|
+
var adapter = window.ClientSearchAdapters &&
|
|
104
|
+
(window.ClientSearchAdapters[config.engine] ||
|
|
105
|
+
window.ClientSearchAdapters.minisearch);
|
|
106
|
+
if (!adapter) {
|
|
107
|
+
throw new Error("Search adapter not loaded");
|
|
108
|
+
}
|
|
109
|
+
sharedDocuments = new Map(documents.map(function (entry) {
|
|
110
|
+
return [entry.id, entry];
|
|
111
|
+
}));
|
|
112
|
+
sharedIndex = adapter.buildIndex(documents);
|
|
113
|
+
return { index: sharedIndex, documents: sharedDocuments };
|
|
114
|
+
})
|
|
115
|
+
.catch(function (error) {
|
|
116
|
+
indexLoadPromise = null;
|
|
117
|
+
throw error;
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
return indexLoadPromise;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function search(index, query, adapter) {
|
|
124
|
+
var exact = adapter.search(index, query, {
|
|
125
|
+
combineWith: "AND",
|
|
126
|
+
fuzzy: false,
|
|
127
|
+
prefix: true
|
|
128
|
+
});
|
|
129
|
+
if (exact && typeof exact.then === "function") {
|
|
130
|
+
return exact.then(function (results) {
|
|
131
|
+
if (results && results.length > 0) {
|
|
132
|
+
return results;
|
|
133
|
+
}
|
|
134
|
+
return adapter.search(index, query, {
|
|
135
|
+
combineWith: "OR",
|
|
136
|
+
fuzzy: true,
|
|
137
|
+
prefix: true
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
if (exact && exact.length > 0) {
|
|
142
|
+
return exact;
|
|
143
|
+
}
|
|
144
|
+
return adapter.search(index, query, {
|
|
145
|
+
combineWith: "OR",
|
|
146
|
+
fuzzy: true,
|
|
147
|
+
prefix: true
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function createResultItem(match, documents, iconField) {
|
|
152
|
+
var entry = documents.get(match.ref);
|
|
153
|
+
if (!entry) {
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
var li = document.createElement("li");
|
|
158
|
+
li.setAttribute("role", "option");
|
|
159
|
+
li.className = "client-search-dropdown-item";
|
|
160
|
+
li.dataset.url = safeUrl(entry.url);
|
|
161
|
+
|
|
162
|
+
if (entry.source) {
|
|
163
|
+
li.dataset.source = entry.source;
|
|
164
|
+
}
|
|
165
|
+
if (entry.categories && entry.categories.length) {
|
|
166
|
+
li.dataset.categories = entry.categories.join(" ");
|
|
167
|
+
}
|
|
168
|
+
if (entry.tags && entry.tags.length) {
|
|
169
|
+
li.dataset.tags = entry.tags.join(" ");
|
|
170
|
+
}
|
|
171
|
+
Object.keys(entry).forEach(function (key) {
|
|
172
|
+
var skip = ["id", "title", "url", "excerpt", "content",
|
|
173
|
+
"categories", "tags", "source", "embedding",
|
|
174
|
+
"date_timestamp"];
|
|
175
|
+
if (skip.indexOf(key) !== -1) {
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
var value = entry[key];
|
|
179
|
+
if (typeof value === "string" || typeof value === "number") {
|
|
180
|
+
li.dataset[toCamelCase(key)] = String(value);
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
if (iconField && entry[iconField]) {
|
|
185
|
+
var icon = document.createElement("img");
|
|
186
|
+
icon.className = "client-search-dropdown-icon";
|
|
187
|
+
icon.src = safeUrl(entry[iconField]);
|
|
188
|
+
icon.alt = entry.file_type || entry.source || "";
|
|
189
|
+
icon.loading = "lazy";
|
|
190
|
+
icon.style.width = "1em";
|
|
191
|
+
icon.style.height = "1em";
|
|
192
|
+
icon.style.verticalAlign = "middle";
|
|
193
|
+
icon.style.marginRight = "0.3em";
|
|
194
|
+
li.appendChild(icon);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
var link = document.createElement("a");
|
|
198
|
+
link.href = safeUrl(entry.url);
|
|
199
|
+
link.textContent = entry.title;
|
|
200
|
+
link.className = "client-search-dropdown-link";
|
|
201
|
+
li.appendChild(link);
|
|
202
|
+
|
|
203
|
+
return li;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function DropdownInstance(root, config) {
|
|
207
|
+
this.root = root;
|
|
208
|
+
this.form = root.querySelector("[data-cs-dropdown-form]");
|
|
209
|
+
this.input = root.querySelector("[data-cs-dropdown-input]");
|
|
210
|
+
this.results = root.querySelector("[data-cs-dropdown-results]");
|
|
211
|
+
this.config = config;
|
|
212
|
+
this.maxItems = parseInt(this.results.dataset.maxItems, 10) || config.maxItems || 5;
|
|
213
|
+
this.selectedIndex = -1;
|
|
214
|
+
this.currentItems = [];
|
|
215
|
+
this.debounceTimer = null;
|
|
216
|
+
this.renderVersion = 0;
|
|
217
|
+
this.adapter = null;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
DropdownInstance.prototype.init = function () {
|
|
221
|
+
if (!this.form || !this.input || !this.results) {
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
var self = this;
|
|
226
|
+
|
|
227
|
+
this.input.addEventListener("input", function () {
|
|
228
|
+
self.onInput();
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
this.input.addEventListener("keydown", function (event) {
|
|
232
|
+
self.onKeydown(event);
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
this.form.addEventListener("submit", function (event) {
|
|
236
|
+
event.preventDefault();
|
|
237
|
+
self.onSubmit();
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
document.addEventListener("click", function (event) {
|
|
241
|
+
if (!self.root.contains(event.target)) {
|
|
242
|
+
self.hide();
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
this.results.addEventListener("click", function (event) {
|
|
247
|
+
var li = event.target.closest("li");
|
|
248
|
+
if (li && li.dataset.url && li.dataset.url !== "#") {
|
|
249
|
+
window.location.href = li.dataset.url;
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
DropdownInstance.prototype.onInput = function () {
|
|
255
|
+
var self = this;
|
|
256
|
+
this.renderVersion += 1;
|
|
257
|
+
clearTimeout(this.debounceTimer);
|
|
258
|
+
|
|
259
|
+
var query = this.input.value.trim();
|
|
260
|
+
if (!query || query.length < this.config.minChars) {
|
|
261
|
+
this.hide();
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
this.debounceTimer = setTimeout(function () {
|
|
266
|
+
self.performSearch(query);
|
|
267
|
+
}, this.config.debounceMs);
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
DropdownInstance.prototype.onKeydown = function (event) {
|
|
271
|
+
if (this.results.getAttribute("aria-hidden") === "true") {
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
var visible = this.results.children.length > 0;
|
|
275
|
+
if (!visible) {
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
switch (event.key) {
|
|
280
|
+
case "ArrowDown":
|
|
281
|
+
event.preventDefault();
|
|
282
|
+
this.selectItem(Math.min(this.selectedIndex + 1, this.currentItems.length - 1));
|
|
283
|
+
break;
|
|
284
|
+
case "ArrowUp":
|
|
285
|
+
event.preventDefault();
|
|
286
|
+
this.selectItem(Math.max(this.selectedIndex - 1, 0));
|
|
287
|
+
break;
|
|
288
|
+
case "Enter":
|
|
289
|
+
if (this.selectedIndex >= 0 && this.currentItems[this.selectedIndex]) {
|
|
290
|
+
event.preventDefault();
|
|
291
|
+
var url = this.currentItems[this.selectedIndex].dataset.url;
|
|
292
|
+
if (url && url !== "#") {
|
|
293
|
+
window.location.href = url;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
break;
|
|
297
|
+
case "Escape":
|
|
298
|
+
this.hide();
|
|
299
|
+
this.input.blur();
|
|
300
|
+
break;
|
|
301
|
+
case "Tab":
|
|
302
|
+
this.hide();
|
|
303
|
+
break;
|
|
304
|
+
}
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
DropdownInstance.prototype.onSubmit = function () {
|
|
308
|
+
var query = this.input.value.trim();
|
|
309
|
+
if (!query) {
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
var redirectUrl = this.config.redirectUrl || "/search/";
|
|
313
|
+
var separator = redirectUrl.indexOf("?") !== -1 ? "&" : "?";
|
|
314
|
+
window.location.href = redirectUrl + separator + "q=" + encodeURIComponent(query);
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
DropdownInstance.prototype.performSearch = function (query) {
|
|
318
|
+
var self = this;
|
|
319
|
+
var version = this.renderVersion;
|
|
320
|
+
|
|
321
|
+
var config = this.config;
|
|
322
|
+
var engineName = config.engine || "minisearch";
|
|
323
|
+
|
|
324
|
+
loadIndex(config).then(function (loaded) {
|
|
325
|
+
if (version !== self.renderVersion) {
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
var adapter = window.ClientSearchAdapters &&
|
|
330
|
+
(window.ClientSearchAdapters[engineName] ||
|
|
331
|
+
window.ClientSearchAdapters.minisearch);
|
|
332
|
+
if (!adapter || !adapter.available()) {
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
var results = search(loaded.index, query, adapter);
|
|
337
|
+
return Promise.resolve(results).then(function (matches) {
|
|
338
|
+
if (version === self.renderVersion) {
|
|
339
|
+
self.renderResults(matches, loaded.documents);
|
|
340
|
+
}
|
|
341
|
+
});
|
|
342
|
+
}).catch(function () {
|
|
343
|
+
if (version === self.renderVersion) {
|
|
344
|
+
self.hide();
|
|
345
|
+
}
|
|
346
|
+
});
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
DropdownInstance.prototype.renderResults = function (matches, documents) {
|
|
350
|
+
this.results.replaceChildren();
|
|
351
|
+
this.currentItems = [];
|
|
352
|
+
this.selectedIndex = -1;
|
|
353
|
+
|
|
354
|
+
if (!matches || matches.length === 0) {
|
|
355
|
+
this.hide();
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
var iconField = this.config.iconField || null;
|
|
360
|
+
var count = Math.min(matches.length, this.maxItems);
|
|
361
|
+
|
|
362
|
+
for (var i = 0; i < count; i++) {
|
|
363
|
+
var item = createResultItem(matches[i], documents, iconField);
|
|
364
|
+
if (item) {
|
|
365
|
+
this.results.appendChild(item);
|
|
366
|
+
this.currentItems.push(item);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
if (this.currentItems.length === 0) {
|
|
371
|
+
this.hide();
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
this.show();
|
|
376
|
+
};
|
|
377
|
+
|
|
378
|
+
DropdownInstance.prototype.selectItem = function (index) {
|
|
379
|
+
this.selectedIndex = index;
|
|
380
|
+
for (var i = 0; i < this.currentItems.length; i++) {
|
|
381
|
+
if (i === index) {
|
|
382
|
+
this.currentItems[i].setAttribute("aria-selected", "true");
|
|
383
|
+
} else {
|
|
384
|
+
this.currentItems[i].removeAttribute("aria-selected");
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
if (this.currentItems[index]) {
|
|
388
|
+
this.currentItems[index].scrollIntoView({ block: "nearest" });
|
|
389
|
+
}
|
|
390
|
+
};
|
|
391
|
+
|
|
392
|
+
DropdownInstance.prototype.show = function () {
|
|
393
|
+
this.results.setAttribute("aria-hidden", "false");
|
|
394
|
+
this.input.setAttribute("aria-expanded", "true");
|
|
395
|
+
};
|
|
396
|
+
|
|
397
|
+
DropdownInstance.prototype.hide = function () {
|
|
398
|
+
this.results.setAttribute("aria-hidden", "true");
|
|
399
|
+
this.input.setAttribute("aria-expanded", "false");
|
|
400
|
+
this.results.replaceChildren();
|
|
401
|
+
this.currentItems = [];
|
|
402
|
+
this.selectedIndex = -1;
|
|
403
|
+
};
|
|
404
|
+
|
|
405
|
+
function initAll() {
|
|
406
|
+
var config = window.clientSearchConfig || {};
|
|
407
|
+
var dropdownConfig = config.dropdown || {};
|
|
408
|
+
if (dropdownConfig.enabled === false) {
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
var mergedConfig = {
|
|
413
|
+
indexUrl: config.indexUrl,
|
|
414
|
+
engine: config.engine || "minisearch",
|
|
415
|
+
iconField: config.iconField || null,
|
|
416
|
+
minChars: typeof dropdownConfig.minChars === "number" ? dropdownConfig.minChars : 2,
|
|
417
|
+
debounceMs: typeof dropdownConfig.debounceMs === "number" ? dropdownConfig.debounceMs : 150,
|
|
418
|
+
maxItems: typeof dropdownConfig.maxItems === "number" ? dropdownConfig.maxItems : 5,
|
|
419
|
+
redirectUrl: dropdownConfig.redirectUrl || "/search/"
|
|
420
|
+
};
|
|
421
|
+
|
|
422
|
+
var roots = document.querySelectorAll("[data-client-search-dropdown]");
|
|
423
|
+
roots.forEach(function (root) {
|
|
424
|
+
var instance = new DropdownInstance(root, mergedConfig);
|
|
425
|
+
instance.init();
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
if (document.readyState === "loading") {
|
|
430
|
+
document.addEventListener("DOMContentLoaded", initAll);
|
|
431
|
+
} else {
|
|
432
|
+
initAll();
|
|
433
|
+
}
|
|
434
|
+
}());
|
|
@@ -100,6 +100,11 @@
|
|
|
100
100
|
}
|
|
101
101
|
sortItems(items, sortOrder);
|
|
102
102
|
|
|
103
|
+
var maxItems = options.maxItems || 0;
|
|
104
|
+
if (maxItems && maxItems > 0) {
|
|
105
|
+
items = items.slice(0, maxItems);
|
|
106
|
+
}
|
|
107
|
+
|
|
103
108
|
container.replaceChildren();
|
|
104
109
|
if (items.length === 0) {
|
|
105
110
|
return;
|
|
@@ -138,6 +143,7 @@
|
|
|
138
143
|
relationsUrl: window.clientSearchConfig && window.clientSearchConfig.relatedUrl,
|
|
139
144
|
currentUrl: window.location.pathname,
|
|
140
145
|
sort: (window.clientSearchConfig && window.clientSearchConfig.relatedSort) || "relevance",
|
|
146
|
+
maxItems: 0,
|
|
141
147
|
renderItem: null,
|
|
142
148
|
filter: null
|
|
143
149
|
}, options || {});
|
|
@@ -146,6 +152,10 @@
|
|
|
146
152
|
return Promise.resolve();
|
|
147
153
|
}
|
|
148
154
|
|
|
155
|
+
if (container.dataset.relatedMax) {
|
|
156
|
+
options.maxItems = parseInt(container.dataset.relatedMax, 10) || 0;
|
|
157
|
+
}
|
|
158
|
+
|
|
149
159
|
return fetch(options.relationsUrl, { headers: { Accept: "application/json" } })
|
|
150
160
|
.then(function (response) {
|
|
151
161
|
if (!response.ok) {
|
|
@@ -23,6 +23,8 @@ module Jekyll
|
|
|
23
23
|
"collections" => ["posts"],
|
|
24
24
|
"include_pages" => false,
|
|
25
25
|
"copy_runtime" => true,
|
|
26
|
+
"passthrough_fields" => [],
|
|
27
|
+
"icon_field" => "icon_url",
|
|
26
28
|
"embedding" => {
|
|
27
29
|
"enabled" => false,
|
|
28
30
|
"model" => "embeddinggemma:300m",
|
|
@@ -37,17 +39,11 @@ module Jekyll
|
|
|
37
39
|
}.freeze
|
|
38
40
|
|
|
39
41
|
def initialize(site)
|
|
40
|
-
configured = site.config["client_search"]
|
|
41
|
-
configured = { "enabled" => false } if configured == false
|
|
42
|
-
configured ||= {}
|
|
43
|
-
unless configured.is_a?(Hash)
|
|
44
|
-
raise Jekyll::Errors::FatalException,
|
|
45
|
-
"client_search configuration must be a mapping or false"
|
|
46
|
-
end
|
|
47
|
-
|
|
42
|
+
configured = normalize_configured(site.config["client_search"])
|
|
48
43
|
@values = DEFAULTS.merge(configured)
|
|
49
44
|
@live_search = LiveSearchConfiguration.new(configured["live_search"] || {}, engine: engine)
|
|
50
45
|
@related = RelatedConfiguration.new(configured["related"] || {})
|
|
46
|
+
@dropdown = DropdownConfiguration.new(configured["dropdown"] || {})
|
|
51
47
|
merge_embedding_config(configured["embedding"])
|
|
52
48
|
validate_engine!
|
|
53
49
|
return unless embedding_enabled?
|
|
@@ -58,6 +54,15 @@ module Jekyll
|
|
|
58
54
|
|
|
59
55
|
private
|
|
60
56
|
|
|
57
|
+
def normalize_configured(value)
|
|
58
|
+
return { "enabled" => false } if value == false
|
|
59
|
+
return value if value.is_a?(Hash)
|
|
60
|
+
return {} if value.nil?
|
|
61
|
+
|
|
62
|
+
raise Jekyll::Errors::FatalException,
|
|
63
|
+
"client_search configuration must be a mapping or false"
|
|
64
|
+
end
|
|
65
|
+
|
|
61
66
|
def merge_embedding_config(configured_embedding)
|
|
62
67
|
embedding = configured_embedding || {}
|
|
63
68
|
unless embedding.is_a?(Hash)
|
|
@@ -103,6 +108,7 @@ module Jekyll
|
|
|
103
108
|
assets = ["assets/client-search-base.js", "assets/adapters/#{engine}.js"]
|
|
104
109
|
assets.concat(query_embedder_assets) if engine == "semantic" && embedding_enabled?
|
|
105
110
|
assets << "assets/client-search-related.js" if related_enabled?
|
|
111
|
+
assets << "assets/client-search-dropdown.js" if dropdown_enabled?
|
|
106
112
|
assets
|
|
107
113
|
end
|
|
108
114
|
|
|
@@ -111,11 +117,7 @@ module Jekyll
|
|
|
111
117
|
end
|
|
112
118
|
|
|
113
119
|
def collections
|
|
114
|
-
Array(@values["collections"])
|
|
115
|
-
.compact
|
|
116
|
-
.map(&:to_s)
|
|
117
|
-
.reject(&:empty?)
|
|
118
|
-
.uniq
|
|
120
|
+
Array(@values["collections"]).compact.map(&:to_s).reject(&:empty?).uniq
|
|
119
121
|
end
|
|
120
122
|
|
|
121
123
|
def include_pages?
|
|
@@ -126,6 +128,31 @@ module Jekyll
|
|
|
126
128
|
@values["copy_runtime"] != false
|
|
127
129
|
end
|
|
128
130
|
|
|
131
|
+
def passthrough_fields
|
|
132
|
+
Array(@values["passthrough_fields"]).compact.each_with_object([]) do |e, f|
|
|
133
|
+
e.is_a?(Hash) ? add_hash_fields(e, f) : add_string_field(e, f)
|
|
134
|
+
end.uniq
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def add_hash_fields(hash, fields)
|
|
138
|
+
hash.each do |s, t|
|
|
139
|
+
fields << [s.to_s, t.to_s] unless s.to_s.empty? || t.to_s.empty?
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def add_string_field(entry, fields)
|
|
144
|
+
fields << [entry.to_s, entry.to_s] unless entry.to_s.empty?
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def icon_field
|
|
148
|
+
v = @values["icon_field"]
|
|
149
|
+
v.nil? || v == false ? nil : v.to_s
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def runtime_icon_field
|
|
153
|
+
icon_field if passthrough_fields.map(&:last).include?(icon_field)
|
|
154
|
+
end
|
|
155
|
+
|
|
129
156
|
private
|
|
130
157
|
|
|
131
158
|
def validate_query_embedder!
|
|
@@ -28,10 +28,6 @@ module Jekyll
|
|
|
28
28
|
@query_embedder.model
|
|
29
29
|
end
|
|
30
30
|
|
|
31
|
-
def query_embedder_api_url
|
|
32
|
-
@query_embedder.api_url
|
|
33
|
-
end
|
|
34
|
-
|
|
35
31
|
def query_embedder_asset
|
|
36
32
|
@query_embedder.asset
|
|
37
33
|
end
|
|
@@ -43,6 +39,18 @@ module Jekyll
|
|
|
43
39
|
def query_embedder_config_json
|
|
44
40
|
@query_embedder.to_json
|
|
45
41
|
end
|
|
42
|
+
|
|
43
|
+
def dropdown_enabled?
|
|
44
|
+
@dropdown.enabled?
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def dropdown_max_items
|
|
48
|
+
@dropdown.max_items
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def dropdown_config
|
|
52
|
+
@dropdown.to_h
|
|
53
|
+
end
|
|
46
54
|
end
|
|
47
55
|
end
|
|
48
56
|
end
|