site updated

This commit is contained in:
Wilson Freitas
2024-02-17 09:39:55 -03:00
parent 7173ed3708
commit 8f4b1dcdd0
31 changed files with 2167 additions and 317 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+211 -48
View File
@@ -43,9 +43,9 @@ window.document.addEventListener("DOMContentLoaded", function (_event) {
const mainEl = window.document.querySelector("main");
// highlight matches on the page
if (query !== null && mainEl) {
if (query && mainEl) {
// perform any highlighting
highlight(query, mainEl);
highlight(escapeRegExp(query), mainEl);
// fix up the URL to remove the q query param
const replacementUrl = new URL(window.location);
@@ -57,7 +57,7 @@ window.document.addEventListener("DOMContentLoaded", function (_event) {
// (e.g. if the user edits the query or clears it)
let highlighting = true;
const resetHighlighting = (searchTerm) => {
if (mainEl && highlighting && query !== null && searchTerm !== query) {
if (mainEl && highlighting && query && searchTerm !== query) {
clearHighlight(query, mainEl);
highlighting = false;
}
@@ -80,27 +80,25 @@ window.document.addEventListener("DOMContentLoaded", function (_event) {
// the media query since we generate different HTML for sidebar overlays than we do
// for sidebar input UI)
const detachedMediaQuery =
quartoSearchOptions.type === "overlay"
? "all"
: quartoSearchOptions.location === "navbar"
? "(max-width: 991px)"
: "none";
quartoSearchOptions.type === "overlay" ? "all" : "(max-width: 991px)";
// If configured, include the analytics client to send insights
const plugins = configurePlugins(quartoSearchOptions);
let lastState = null;
const { setIsOpen } = autocomplete({
const { setIsOpen, setQuery, setCollections } = autocomplete({
container: searchEl,
detachedMediaQuery: detachedMediaQuery,
defaultActiveItemId: 0,
panelContainer: "#quarto-search-results",
panelPlacement: quartoSearchOptions["panel-placement"],
debug: false,
openOnFocus: true,
plugins,
classNames: {
form: "d-flex",
},
placeholder: language["search-text-placeholder"],
translations: {
clearButtonTitle: language["search-clear-button-title"],
detachedCancelButtonText: language["search-detached-cancel-button-title"],
@@ -113,6 +111,8 @@ window.document.addEventListener("DOMContentLoaded", function (_event) {
return item.href;
},
onStateChange({ state }) {
// If this is a file URL, note that
// Perhaps reset highlighting
resetHighlighting(state.query);
@@ -280,6 +280,10 @@ window.document.addEventListener("DOMContentLoaded", function (_event) {
}
},
getItems({ query }) {
if (query === null || query === "") {
return [];
}
const limit = quartoSearchOptions.limit;
if (quartoSearchOptions.algolia) {
return algoliaSearch(query, limit, quartoSearchOptions.algolia);
@@ -299,9 +303,15 @@ window.document.addEventListener("DOMContentLoaded", function (_event) {
},
templates: {
noResults({ createElement }) {
const hasQuery = lastState.query;
return createElement(
"div",
{ class: "quarto-search-no-results" },
{
class: `quarto-search-no-results${
hasQuery ? "" : " no-query"
}`,
},
language["search-no-results-text"]
);
},
@@ -352,7 +362,8 @@ window.document.addEventListener("DOMContentLoaded", function (_event) {
state,
setActiveItemId,
setContext,
refresh
refresh,
quartoSearchOptions
);
},
},
@@ -361,6 +372,38 @@ window.document.addEventListener("DOMContentLoaded", function (_event) {
},
});
window.quartoOpenSearch = () => {
setIsOpen(false);
setIsOpen(true);
focusSearchInput();
};
document.addEventListener("keyup", (event) => {
const { key } = event;
const kbds = quartoSearchOptions["keyboard-shortcut"];
const focusedEl = document.activeElement;
const isFormElFocused = [
"input",
"select",
"textarea",
"button",
"option",
].find((tag) => {
return focusedEl.tagName.toLowerCase() === tag;
});
if (
kbds &&
kbds.includes(key) &&
!isFormElFocused &&
!document.activeElement.isContentEditable
) {
event.preventDefault();
window.quartoOpenSearch();
}
});
// Remove the labeleledby attribute since it is pointing
// to a non-existent label
if (quartoSearchOptions.type === "overlay") {
@@ -372,11 +415,30 @@ window.document.addEventListener("DOMContentLoaded", function (_event) {
}
}
function throttle(func, wait) {
let waiting = false;
return function () {
if (!waiting) {
func.apply(this, arguments);
waiting = true;
setTimeout(function () {
waiting = false;
}, wait);
}
};
}
// If the main document scrolls dismiss the search results
// (otherwise, since they're floating in the document they can scroll with the document)
window.document.body.onscroll = () => {
setIsOpen(false);
};
window.document.body.onscroll = throttle(() => {
// Only do this if we're not detached
// Bug #7117
// This will happen when the keyboard is shown on ios (resulting in a scroll)
// which then closed the search UI
if (!window.matchMedia(detachedMediaQuery).matches) {
setIsOpen(false);
}
}, 50);
if (showSearchResults) {
setIsOpen(true);
@@ -416,15 +478,27 @@ function configurePlugins(quartoSearchOptions) {
const algoliaInsightsPlugin = createAlgoliaInsightsPlugin({
insightsClient: window.aa,
onItemsChange({ insights, insightsEvents }) {
const events = insightsEvents.map((event) => {
const maxEvents = event.objectIDs.slice(0, 20);
return {
...event,
objectIDs: maxEvents,
};
const events = insightsEvents.flatMap((event) => {
// This API limits the number of items per event to 20
const chunkSize = 20;
const itemChunks = [];
const eventItems = event.items;
for (let i = 0; i < eventItems.length; i += chunkSize) {
itemChunks.push(eventItems.slice(i, i + chunkSize));
}
// Split the items into multiple events that can be sent
const events = itemChunks.map((items) => {
return {
...event,
items,
};
});
return events;
});
insights.viewedObjectIDs(...events);
for (const event of events) {
insights.viewedObjectIDs(event);
}
},
});
return algoliaInsightsPlugin;
@@ -600,20 +674,30 @@ function showCopyLink(query, options) {
/* Search Index Handling */
// create the index
var fuseIndex = undefined;
var shownWarning = false;
// fuse index options
const kFuseIndexOptions = {
keys: [
{ name: "title", weight: 20 },
{ name: "section", weight: 20 },
{ name: "text", weight: 10 },
],
ignoreLocation: true,
threshold: 0.1,
};
async function readSearchData() {
// Initialize the search index on demand
if (fuseIndex === undefined) {
// create fuse index
const options = {
keys: [
{ name: "title", weight: 20 },
{ name: "section", weight: 20 },
{ name: "text", weight: 10 },
],
ignoreLocation: true,
threshold: 0.1,
};
const fuse = new window.Fuse([], options);
if (window.location.protocol === "file:" && !shownWarning) {
window.alert(
"Search requires JavaScript features disabled when running in file://... URLs. In order to use search, please run this document in a web server."
);
shownWarning = true;
return;
}
const fuse = new window.Fuse([], kFuseIndexOptions);
// fetch the main search.json
const response = await fetch(offsetURL("search.json"));
@@ -633,6 +717,7 @@ async function readSearchData() {
);
}
}
return fuseIndex;
}
@@ -661,7 +746,8 @@ function renderItem(
state,
setActiveItemId,
setContext,
refresh
refresh,
quartoSearchOptions
) {
switch (item.type) {
case kItemTypeDoc:
@@ -671,7 +757,9 @@ function renderItem(
item.title,
item.section,
item.text,
item.href
item.href,
item.crumbs,
quartoSearchOptions
);
case kItemTypeMore:
return createMoreCard(
@@ -696,15 +784,46 @@ function renderItem(
}
}
function createDocumentCard(createElement, icon, title, section, text, href) {
function createDocumentCard(
createElement,
icon,
title,
section,
text,
href,
crumbs,
quartoSearchOptions
) {
const iconEl = createElement("i", {
class: `bi bi-${icon} search-result-icon`,
});
const titleEl = createElement("p", { class: "search-result-title" }, title);
const titleContents = [iconEl, titleEl];
const showParent = quartoSearchOptions["show-item-context"];
if (crumbs && showParent) {
let crumbsOut = undefined;
const crumbClz = ["search-result-crumbs"];
if (showParent === "root") {
crumbsOut = crumbs.length > 1 ? crumbs[0] : undefined;
} else if (showParent === "parent") {
crumbsOut = crumbs.length > 1 ? crumbs[crumbs.length - 2] : undefined;
} else {
crumbsOut = crumbs.length > 1 ? crumbs.join(" > ") : undefined;
crumbClz.push("search-result-crumbs-wrap");
}
const crumbEl = createElement(
"p",
{ class: crumbClz.join(" ") },
crumbsOut
);
titleContents.push(crumbEl);
}
const titleContainerEl = createElement(
"div",
{ class: "search-result-title-container" },
[iconEl, titleEl]
titleContents
);
const textEls = [];
@@ -976,6 +1095,10 @@ function clearHighlight(searchterm, el) {
}
}
function escapeRegExp(string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); // $& means the whole matched string
}
// highlight matches
function highlight(term, el) {
const termRegex = new RegExp(term, "ig");
@@ -1082,17 +1205,19 @@ function algoliaSearch(query, limit, algoliaOptions) {
const remappedHits = response.hits.map((hit) => {
return hit.map((item) => {
const newItem = { ...item };
["href", "section", "title", "text"].forEach((keyName) => {
const mappedName = indexFields[keyName];
if (
mappedName &&
item[mappedName] !== undefined &&
mappedName !== keyName
) {
newItem[keyName] = item[mappedName];
delete newItem[mappedName];
["href", "section", "title", "text", "crumbs"].forEach(
(keyName) => {
const mappedName = indexFields[keyName];
if (
mappedName &&
item[mappedName] !== undefined &&
mappedName !== keyName
) {
newItem[keyName] = item[mappedName];
delete newItem[mappedName];
}
}
});
);
newItem.text = highlightMatch(query, newItem.text);
return newItem;
});
@@ -1103,8 +1228,34 @@ function algoliaSearch(query, limit, algoliaOptions) {
});
}
function fuseSearch(query, fuse, fuseOptions) {
return fuse.search(query, fuseOptions).map((result) => {
let subSearchTerm = undefined;
let subSearchFuse = undefined;
const kFuseMaxWait = 125;
async function fuseSearch(query, fuse, fuseOptions) {
let index = fuse;
// Fuse.js using the Bitap algorithm for text matching which runs in
// O(nm) time (no matter the structure of the text). In our case this
// means that long search terms mixed with large index gets very slow
//
// This injects a subIndex that will be used once the terms get long enough
// Usually making this subindex is cheap since there will typically be
// a subset of results matching the existing query
if (subSearchFuse !== undefined && query.startsWith(subSearchTerm)) {
// Use the existing subSearchFuse
index = subSearchFuse;
} else if (subSearchFuse !== undefined) {
// The term changed, discard the existing fuse
subSearchFuse = undefined;
subSearchTerm = undefined;
}
// Search using the active fuse
const then = performance.now();
const resultsRaw = await index.search(query, fuseOptions);
const now = performance.now();
const results = resultsRaw.map((result) => {
const addParam = (url, name, value) => {
const anchorParts = url.split("#");
const baseUrl = anchorParts[0];
@@ -1118,6 +1269,18 @@ function fuseSearch(query, fuse, fuseOptions) {
section: result.item.section,
href: addParam(result.item.href, kQueryArg, query),
text: highlightMatch(query, result.item.text),
crumbs: result.item.crumbs,
};
});
// If we don't have a subfuse and the query is long enough, go ahead
// and create a subfuse to use for subsequent queries
if (now - then > kFuseMaxWait && subSearchFuse === undefined) {
subSearchTerm = query;
subSearchFuse = new window.Fuse([], kFuseIndexOptions);
resultsRaw.forEach((rr) => {
subSearchFuse.add(rr.item);
});
}
return results;
}