Rewrite half the theme to make Mozilla observatory more happy about strict CSP
This commit is contained in:
349
static/comments.js
Normal file
349
static/comments.js
Normal file
@ -0,0 +1,349 @@
|
||||
// Taken from https://carlschwan.eu/2020/12/29/adding-comments-to-your-static-blog-with-mastodon/
|
||||
// Attachment code taken from https://github.com/cassidyjames/cassidyjames.github.io/blob/99782788a7e3ba3cc52d6803010873abd1b02b9e/_includes/comments.html#L251-L296
|
||||
|
||||
let relAttributes = document.getElementById("rel-attributes").textContent;
|
||||
let host = document.getElementById("host").textContent;
|
||||
let user = document.getElementById("user").textContent;
|
||||
let id = document.getElementById("id").textContent;
|
||||
let dateLocale = document.getElementById("date-locale").textContent;
|
||||
let loadingText = document.getElementById("loading-text").textContent;
|
||||
let reloadText = document.getElementById("reload-text").textContent;
|
||||
let viewProfileText = document.getElementById("view-profile-text").textContent;
|
||||
let viewCommentText = document.getElementById("view-comment-text").textContent;
|
||||
let boostsFromText = document.getElementById("boosts-from-text").textContent;
|
||||
let favesFromText = document.getElementById("faves-from-text").textContent;
|
||||
let blogPostAuthorText = document.getElementById("blog-post-author-text").textContent;
|
||||
let noCommentsText = document.getElementById("no-comments-text").textContent;
|
||||
|
||||
document.getElementById("load-comments").addEventListener("click", loadComments);
|
||||
|
||||
function escapeHtml(unsafe) {
|
||||
return unsafe
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
}
|
||||
function emojify(input, emojis) {
|
||||
let output = input;
|
||||
|
||||
emojis.forEach((emoji) => {
|
||||
let picture = document.createElement("picture");
|
||||
|
||||
let source = document.createElement("source");
|
||||
source.setAttribute("srcset", escapeHtml(emoji.url));
|
||||
source.setAttribute("media", "(prefers-reduced-motion: no-preference)");
|
||||
|
||||
let img = document.createElement("img");
|
||||
img.className = "emoji";
|
||||
img.setAttribute("src", escapeHtml(emoji.static_url));
|
||||
img.setAttribute("alt", `:${emoji.shortcode}:`);
|
||||
img.setAttribute("title", `:${emoji.shortcode}:`);
|
||||
// {% - if config.markdown.lazy_async_image -%}
|
||||
// img.setAttribute("decoding", "async");
|
||||
// img.setAttribute("loading", "lazy");
|
||||
// {% - endif -%}
|
||||
|
||||
picture.appendChild(source);
|
||||
picture.appendChild(img);
|
||||
|
||||
output = output.replace(`:${emoji.shortcode}:`, picture.outerHTML);
|
||||
});
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
function loadComments() {
|
||||
let commentsWrapper = document.getElementById("comments-wrapper");
|
||||
commentsWrapper.innerHTML = "";
|
||||
|
||||
let loadCommentsButton = document.getElementById("load-comments");
|
||||
loadCommentsButton.innerHTML = loadingText;
|
||||
loadCommentsButton.disabled = true;
|
||||
|
||||
fetch(`https://${host}/api/v1/statuses/${id}/context`)
|
||||
.then(function (response) {
|
||||
return response.json();
|
||||
})
|
||||
.then(function (data) {
|
||||
let descendants = data["descendants"];
|
||||
if (
|
||||
descendants &&
|
||||
Array.isArray(descendants) &&
|
||||
descendants.length > 0
|
||||
) {
|
||||
commentsWrapper.innerHTML = "";
|
||||
|
||||
descendants.forEach(function (status) {
|
||||
console.log(descendants);
|
||||
if (status.account.display_name.length > 0) {
|
||||
status.account.display_name = escapeHtml(
|
||||
status.account.display_name
|
||||
);
|
||||
status.account.display_name = emojify(
|
||||
status.account.display_name,
|
||||
status.account.emojis
|
||||
);
|
||||
} else {
|
||||
status.account.display_name = status.account.username;
|
||||
}
|
||||
|
||||
let instance = "";
|
||||
if (status.account.acct.includes("@")) {
|
||||
instance = status.account.acct.split("@")[1];
|
||||
} else {
|
||||
instance = host;
|
||||
}
|
||||
|
||||
const isReply = status.in_reply_to_id !== id;
|
||||
|
||||
let op = false;
|
||||
if (status.account.acct == user) {
|
||||
op = true;
|
||||
}
|
||||
|
||||
status.content = emojify(status.content, status.emojis);
|
||||
|
||||
let avatarSource = document.createElement("source");
|
||||
avatarSource.setAttribute(
|
||||
"srcset",
|
||||
escapeHtml(status.account.avatar)
|
||||
);
|
||||
avatarSource.setAttribute(
|
||||
"media",
|
||||
"(prefers-reduced-motion: no-preference)"
|
||||
);
|
||||
|
||||
let avatarImg = document.createElement("img");
|
||||
avatarImg.className = "avatar";
|
||||
avatarImg.setAttribute(
|
||||
"src",
|
||||
escapeHtml(status.account.avatar_static)
|
||||
);
|
||||
avatarImg.setAttribute(
|
||||
"alt",
|
||||
`@${status.account.username}@${instance} avatar`
|
||||
);
|
||||
// {% - if config.markdown.lazy_async_image -%}
|
||||
// avatarImg.setAttribute("decoding", "async");
|
||||
// avatarImg.setAttribute("loading", "lazy");
|
||||
// {% - endif -%}
|
||||
|
||||
let avatarPicture = document.createElement("picture");
|
||||
avatarPicture.appendChild(avatarSource);
|
||||
avatarPicture.appendChild(avatarImg);
|
||||
|
||||
let avatar = document.createElement("a");
|
||||
avatar.className = "avatar-link";
|
||||
avatar.setAttribute("href", status.account.url);
|
||||
avatar.setAttribute("rel", relAttributes);
|
||||
avatar.setAttribute(
|
||||
"title",
|
||||
`${viewProfileText} @${status.account.username}@${instance}`
|
||||
);
|
||||
avatar.appendChild(avatarPicture);
|
||||
|
||||
let instanceBadge = document.createElement("a");
|
||||
instanceBadge.className = "instance";
|
||||
instanceBadge.setAttribute("href", status.account.url);
|
||||
instanceBadge.setAttribute(
|
||||
"title",
|
||||
`@${status.account.username}@${instance}`
|
||||
);
|
||||
instanceBadge.setAttribute("rel", relAttributes);
|
||||
instanceBadge.textContent = instance;
|
||||
|
||||
let display = document.createElement("span");
|
||||
display.className = "display";
|
||||
display.setAttribute("itemprop", "author");
|
||||
display.setAttribute("itemtype", "http://schema.org/Person");
|
||||
display.innerHTML = status.account.display_name;
|
||||
|
||||
let header = document.createElement("header");
|
||||
header.className = "author";
|
||||
header.appendChild(display);
|
||||
header.appendChild(instanceBadge);
|
||||
|
||||
let permalink = document.createElement("a");
|
||||
permalink.setAttribute("href", status.url);
|
||||
permalink.setAttribute("itemprop", "url");
|
||||
permalink.setAttribute("title", `${viewCommentText} ${instance}`);
|
||||
permalink.setAttribute("rel", relAttributes);
|
||||
permalink.textContent = new Date(
|
||||
status.created_at
|
||||
).toLocaleString(dateLocale, {
|
||||
dateStyle: "long",
|
||||
timeStyle: "short",
|
||||
});
|
||||
|
||||
let timestamp = document.createElement("time");
|
||||
timestamp.setAttribute("datetime", status.created_at);
|
||||
timestamp.appendChild(permalink);
|
||||
permalink.classList.add("external");
|
||||
|
||||
let main = document.createElement("main");
|
||||
main.setAttribute("itemprop", "text");
|
||||
main.innerHTML = status.content;
|
||||
|
||||
let attachments = status.media_attachments;
|
||||
let SUPPORTED_MEDIA = ["image", "video", "gifv", "audio"];
|
||||
let media = document.createElement("div");
|
||||
media.className = "attachments";
|
||||
if (
|
||||
attachments &&
|
||||
Array.isArray(attachments) &&
|
||||
attachments.length > 0
|
||||
) {
|
||||
attachments.forEach((attachment) => {
|
||||
if (SUPPORTED_MEDIA.includes(attachment.type)) {
|
||||
|
||||
let mediaElement;
|
||||
switch (attachment.type) {
|
||||
case "image":
|
||||
mediaElement = document.createElement("img");
|
||||
mediaElement.setAttribute("src", attachment.preview_url);
|
||||
|
||||
if (attachment.description != null) {
|
||||
mediaElement.setAttribute("alt", attachment.description);
|
||||
mediaElement.setAttribute("title", attachment.description);
|
||||
}
|
||||
|
||||
// {% - if config.markdown.lazy_async_image -%}
|
||||
// mediaElement.setAttribute("decoding", "async");
|
||||
// mediaElement.setAttribute("loading", "lazy");
|
||||
// {% - endif -%}
|
||||
|
||||
media.appendChild(mediaElement);
|
||||
break;
|
||||
|
||||
case "video":
|
||||
mediaElement = document.createElement("video");
|
||||
mediaElement.setAttribute("src", attachment.url);
|
||||
mediaElement.setAttribute("controls", "");
|
||||
|
||||
if (attachment.description != null) {
|
||||
mediaElement.setAttribute("aria-title", attachment.description);
|
||||
mediaElement.setAttribute("title", attachment.description);
|
||||
}
|
||||
|
||||
media.appendChild(mediaElement);
|
||||
break;
|
||||
|
||||
case "gifv":
|
||||
mediaElement = document.createElement("video");
|
||||
mediaElement.setAttribute("src", attachment.url);
|
||||
mediaElement.setAttribute("autoplay", "");
|
||||
mediaElement.setAttribute("playsinline", "");
|
||||
mediaElement.setAttribute("loop", "");
|
||||
|
||||
if (attachment.description != null) {
|
||||
mediaElement.setAttribute("aria-title", attachment.description);
|
||||
mediaElement.setAttribute("title", attachment.description);
|
||||
}
|
||||
|
||||
media.appendChild(mediaElement);
|
||||
break;
|
||||
|
||||
case "audio":
|
||||
mediaElement = document.createElement("audio");
|
||||
mediaElement.setAttribute("src", attachment.url);
|
||||
mediaElement.setAttribute("controls", "");
|
||||
|
||||
if (attachment.description != null) {
|
||||
mediaElement.setAttribute("aria-title", attachment.description);
|
||||
mediaElement.setAttribute("title", attachment.description);
|
||||
}
|
||||
|
||||
media.appendChild(mediaElement);
|
||||
break;
|
||||
}
|
||||
|
||||
let mediaLink = document.createElement("a");
|
||||
mediaLink.setAttribute("href", attachment.url);
|
||||
mediaLink.setAttribute("rel", relAttributes);
|
||||
mediaLink.appendChild(mediaElement);
|
||||
|
||||
media.appendChild(mediaLink);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let interactions = document.createElement("footer");
|
||||
|
||||
let boosts = document.createElement("a");
|
||||
boosts.className = "boosts";
|
||||
boosts.setAttribute("href", `${status.url}/reblogs`);
|
||||
boosts.setAttribute("title", `${boostsFromText}`.replace("$INSTANCE", instance));
|
||||
|
||||
let boostsIcon = document.createElement("i");
|
||||
boostsIcon.className = "icon";
|
||||
boosts.appendChild(boostsIcon);
|
||||
boosts.insertAdjacentHTML('beforeend', ` ${status.reblogs_count}`);
|
||||
interactions.appendChild(boosts);
|
||||
|
||||
let faves = document.createElement("a");
|
||||
faves.className = "faves";
|
||||
faves.setAttribute("href", `${status.url}/favourites`);
|
||||
faves.setAttribute("title", `${favesFromText}`.replace("$INSTANCE", instance));
|
||||
|
||||
let favesIcon = document.createElement("i");
|
||||
favesIcon.className = "icon";
|
||||
faves.appendChild(favesIcon);
|
||||
faves.insertAdjacentHTML('beforeend', ` ${status.favourites_count}`);
|
||||
interactions.appendChild(faves);
|
||||
|
||||
let comment = document.createElement("article");
|
||||
comment.id = `comment-${status.id}`;
|
||||
comment.className = isReply ? "comment comment-reply" : "comment";
|
||||
comment.setAttribute("itemprop", "comment");
|
||||
comment.setAttribute("itemtype", "http://schema.org/Comment");
|
||||
comment.appendChild(avatar);
|
||||
comment.appendChild(header);
|
||||
comment.appendChild(timestamp);
|
||||
comment.appendChild(main);
|
||||
if (
|
||||
attachments &&
|
||||
Array.isArray(attachments) &&
|
||||
attachments.length > 0
|
||||
) {
|
||||
comment.appendChild(media);
|
||||
}
|
||||
comment.appendChild(interactions);
|
||||
|
||||
if (op === true) {
|
||||
comment.classList.add("op");
|
||||
|
||||
avatar.classList.add("op");
|
||||
avatar.setAttribute(
|
||||
"title",
|
||||
`${blogPostAuthorText}: ` + avatar.getAttribute("title")
|
||||
);
|
||||
|
||||
instanceBadge.classList.add("op");
|
||||
instanceBadge.setAttribute(
|
||||
"title",
|
||||
`${blogPostAuthorText}: ` + instanceBadge.getAttribute("title")
|
||||
);
|
||||
}
|
||||
|
||||
commentsWrapper.innerHTML += comment.outerHTML;
|
||||
});
|
||||
}
|
||||
|
||||
else {
|
||||
var statusText = document.createElement("p");
|
||||
statusText.innerHTML = noCommentsText;
|
||||
statusText.setAttribute("id", "comments-status");
|
||||
commentsWrapper.appendChild(statusText);
|
||||
}
|
||||
|
||||
loadCommentsButton.innerHTML = reloadText;
|
||||
})
|
||||
.catch(function (error) {
|
||||
console.error('Error loading comments:', error);
|
||||
})
|
||||
.finally(function () {
|
||||
loadCommentsButton.disabled = false;
|
||||
});
|
||||
}
|
57
static/copy-button.js
Normal file
57
static/copy-button.js
Normal file
@ -0,0 +1,57 @@
|
||||
// Based on https://www.roboleary.net/2022/01/13/copy-code-to-clipboard-blog.html
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
let blocks = document.querySelectorAll("pre[class^='language-']");
|
||||
|
||||
blocks.forEach((block) => {
|
||||
if (navigator.clipboard) {
|
||||
// Code block header title
|
||||
let title = document.createElement("span");
|
||||
let lang = block.getAttribute("data-lang");
|
||||
title.innerHTML = lang;
|
||||
|
||||
// Copy button icon
|
||||
let icon = document.createElement("i");
|
||||
icon.classList.add("icon");
|
||||
|
||||
// Copy button
|
||||
let button = document.createElement("button");
|
||||
let copyCodeText = document.getElementById("copy-code-text").textContent;
|
||||
button.setAttribute("title", copyCodeText)
|
||||
button.appendChild(icon);
|
||||
|
||||
// Code block header
|
||||
let header = document.createElement("div");
|
||||
header.classList.add("header");
|
||||
header.appendChild(title);
|
||||
header.appendChild(button);
|
||||
|
||||
// Container that holds header and the code block itself
|
||||
let container = document.createElement("div");
|
||||
container.classList.add("pre-container");
|
||||
container.appendChild(header);
|
||||
|
||||
// Move code block into the container
|
||||
block.parentNode.insertBefore(container, block);
|
||||
container.appendChild(block);
|
||||
|
||||
button.addEventListener("click", async () => {
|
||||
await copyCode(block, header, button); // Pass the button here
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
async function copyCode(block, header, button) {
|
||||
let code = block.querySelector("code");
|
||||
let text = code.innerText;
|
||||
|
||||
await navigator.clipboard.writeText(text);
|
||||
|
||||
header.classList.add("active");
|
||||
button.setAttribute("disabled", true);
|
||||
|
||||
header.addEventListener("animationend", () => {
|
||||
header.classList.remove("active");
|
||||
button.removeAttribute("disabled");
|
||||
}, { once: true });
|
||||
}
|
||||
});
|
209
static/search-elasticlunr.js
Normal file
209
static/search-elasticlunr.js
Normal file
@ -0,0 +1,209 @@
|
||||
// Based on https://github.com/getzola/zola/blob/1ac1231de1e342bbaf4d7a51a8a9a40ea152e246/docs/static/search.js
|
||||
function debounce(func, wait) {
|
||||
var timeout;
|
||||
|
||||
return function () {
|
||||
var context = this;
|
||||
var args = arguments;
|
||||
clearTimeout(timeout);
|
||||
|
||||
timeout = setTimeout(function () {
|
||||
timeout = null;
|
||||
func.apply(context, args);
|
||||
}, wait);
|
||||
};
|
||||
}
|
||||
|
||||
// Taken from mdbook
|
||||
// The strategy is as follows:
|
||||
// First, assign a value to each word in the document:
|
||||
// Words that correspond to search terms (stemmer aware): 40
|
||||
// Normal words: 2
|
||||
// First word in a sentence: 8
|
||||
// Then use a sliding window with a constant number of words and count the
|
||||
// sum of the values of the words within the window. Then use the window that got the
|
||||
// maximum sum. If there are multiple maximas, then get the last one.
|
||||
// Enclose the terms in <b>.
|
||||
function makeTeaser(body, terms) {
|
||||
var TERM_WEIGHT = 40;
|
||||
var NORMAL_WORD_WEIGHT = 2;
|
||||
var FIRST_WORD_WEIGHT = 8;
|
||||
var TEASER_MAX_WORDS = 30;
|
||||
|
||||
var stemmedTerms = terms.map(function (w) {
|
||||
return elasticlunr.stemmer(w.toLowerCase());
|
||||
});
|
||||
var termFound = false;
|
||||
var index = 0;
|
||||
var weighted = []; // contains elements of ["word", weight, index_in_document]
|
||||
|
||||
// split in sentences, then words
|
||||
var sentences = body.toLowerCase().split(". ");
|
||||
|
||||
for (var i in sentences) {
|
||||
var words = sentences[i].split(" ");
|
||||
var value = FIRST_WORD_WEIGHT;
|
||||
|
||||
for (var j in words) {
|
||||
var word = words[j];
|
||||
|
||||
if (word.length > 0) {
|
||||
for (var k in stemmedTerms) {
|
||||
if (elasticlunr.stemmer(word).startsWith(stemmedTerms[k])) {
|
||||
value = TERM_WEIGHT;
|
||||
termFound = true;
|
||||
}
|
||||
}
|
||||
weighted.push([word, value, index]);
|
||||
value = NORMAL_WORD_WEIGHT;
|
||||
}
|
||||
|
||||
index += word.length;
|
||||
index += 1; // ' ' or '.' if last word in sentence
|
||||
}
|
||||
|
||||
index += 1; // because we split at a two-char boundary '. '
|
||||
}
|
||||
|
||||
if (weighted.length === 0) {
|
||||
return body;
|
||||
}
|
||||
|
||||
var windowWeights = [];
|
||||
var windowSize = Math.min(weighted.length, TEASER_MAX_WORDS);
|
||||
// We add a window with all the weights first
|
||||
var curSum = 0;
|
||||
for (var i = 0; i < windowSize; i++) {
|
||||
curSum += weighted[i][1];
|
||||
}
|
||||
windowWeights.push(curSum);
|
||||
|
||||
for (var i = 0; i < weighted.length - windowSize; i++) {
|
||||
curSum -= weighted[i][1];
|
||||
curSum += weighted[i + windowSize][1];
|
||||
windowWeights.push(curSum);
|
||||
}
|
||||
|
||||
// If we didn't find the term, just pick the first window
|
||||
var maxSumIndex = 0;
|
||||
if (termFound) {
|
||||
var maxFound = 0;
|
||||
// backwards
|
||||
for (var i = windowWeights.length - 1; i >= 0; i--) {
|
||||
if (windowWeights[i] > maxFound) {
|
||||
maxFound = windowWeights[i];
|
||||
maxSumIndex = i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var teaser = [];
|
||||
var startIndex = weighted[maxSumIndex][2];
|
||||
for (var i = maxSumIndex; i < maxSumIndex + windowSize; i++) {
|
||||
var word = weighted[i];
|
||||
if (startIndex < word[2]) {
|
||||
// missing text from index to start of `word`
|
||||
teaser.push(body.substring(startIndex, word[2]));
|
||||
startIndex = word[2];
|
||||
}
|
||||
|
||||
// add <strong> around search terms
|
||||
if (word[1] === TERM_WEIGHT) {
|
||||
teaser.push("<strong>");
|
||||
}
|
||||
startIndex = word[2] + word[0].length;
|
||||
teaser.push(body.substring(word[2], startIndex));
|
||||
|
||||
if (word[1] === TERM_WEIGHT) {
|
||||
teaser.push("</strong>");
|
||||
}
|
||||
}
|
||||
teaser.push("…");
|
||||
return teaser.join("");
|
||||
}
|
||||
|
||||
function formatSearchResultItem(item, terms) {
|
||||
return '<div class="item">'
|
||||
+ `<a href="${item.ref}">${item.doc.title}</a>`
|
||||
+ `<span>${makeTeaser(item.doc.body, terms)}</span>`
|
||||
+ '</div>';
|
||||
}
|
||||
|
||||
function initSearch() {
|
||||
var searchBar = document.getElementById("search-bar");
|
||||
var searchContainer = document.getElementById("search-container");
|
||||
var searchResults = document.getElementById("search-results");
|
||||
var MAX_ITEMS = 10;
|
||||
|
||||
var options = {
|
||||
bool: "AND",
|
||||
fields: {
|
||||
title: { boost: 2 },
|
||||
body: { boost: 1 },
|
||||
}
|
||||
};
|
||||
var currentTerm = "";
|
||||
var index;
|
||||
|
||||
var initIndex = async function () {
|
||||
if (index === undefined) {
|
||||
let searchIndex = document.getElementById("search-index").textContent;
|
||||
index = fetch(searchIndex)
|
||||
.then(
|
||||
async function (response) {
|
||||
return await elasticlunr.Index.load(await response.json());
|
||||
}
|
||||
);
|
||||
}
|
||||
let res = await index;
|
||||
return res;
|
||||
}
|
||||
|
||||
searchBar.addEventListener("keyup", debounce(async function () {
|
||||
var term = searchBar.value.trim();
|
||||
if (term === currentTerm) {
|
||||
return;
|
||||
}
|
||||
searchResults.style.display = term === "" ? "none" : "flex";
|
||||
searchResults.innerHTML = "";
|
||||
currentTerm = term;
|
||||
if (term === "") {
|
||||
return;
|
||||
}
|
||||
|
||||
var results = (await initIndex()).search(term, options);
|
||||
if (results.length === 0) {
|
||||
searchResults.style.display = "none";
|
||||
return;
|
||||
}
|
||||
|
||||
for (var i = 0; i < Math.min(results.length, MAX_ITEMS); i++) {
|
||||
searchResults.innerHTML += formatSearchResultItem(results[i], term.split(" "));
|
||||
}
|
||||
}, 150));
|
||||
|
||||
document.addEventListener("keydown", function (event) {
|
||||
if (event.key === "/") {
|
||||
event.preventDefault();
|
||||
toggleSearch();
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById("search-toggle").addEventListener("click", toggleSearch);
|
||||
}
|
||||
|
||||
function toggleSearch() {
|
||||
var searchContainer = document.getElementById("search-container");
|
||||
var searchBar = document.getElementById("search-bar");
|
||||
searchContainer.classList.toggle("active");
|
||||
searchBar.toggleAttribute("disabled");
|
||||
searchBar.focus();
|
||||
}
|
||||
|
||||
if (document.readyState === "complete" ||
|
||||
(document.readyState !== "loading" && !document.documentElement.doScroll)
|
||||
) {
|
||||
initSearch();
|
||||
} else {
|
||||
document.addEventListener("DOMContentLoaded", initSearch);
|
||||
}
|
127
static/search-fuse.js
Normal file
127
static/search-fuse.js
Normal file
@ -0,0 +1,127 @@
|
||||
// Based on https://codeberg.org/daudix/duckquill/issues/101#issuecomment-2377169
|
||||
let searchSetup = false;
|
||||
let fuse;
|
||||
|
||||
async function initIndex() {
|
||||
if (searchSetup) return;
|
||||
|
||||
const url = document.getElementById("search-index").textContent;
|
||||
const response = await fetch(url);
|
||||
|
||||
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
|
||||
|
||||
const options = {
|
||||
includeScore: false,
|
||||
includeMatches: true,
|
||||
ignoreLocation: true,
|
||||
threshold: 0.15,
|
||||
keys: [
|
||||
{ name: "title", weight: 3 },
|
||||
{ name: "description", weight: 2 },
|
||||
{ name: "body", weight: 1 }
|
||||
]
|
||||
};
|
||||
|
||||
fuse = new Fuse(await response.json(), options);
|
||||
searchSetup = true;
|
||||
|
||||
console.log("Search index initialized successfully");
|
||||
}
|
||||
|
||||
function toggleSearch() {
|
||||
initIndex();
|
||||
const searchBar = document.getElementById("search-bar");
|
||||
const searchContainer = document.getElementById("search-container");
|
||||
const searchResults = document.getElementById("search-results");
|
||||
searchContainer.classList.toggle("active");
|
||||
searchBar.toggleAttribute("disabled");
|
||||
searchBar.focus();
|
||||
}
|
||||
|
||||
function debounce(actual_fn, wait) {
|
||||
let timeoutId;
|
||||
|
||||
return (...args) => {
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
timeoutId = setTimeout(() => {
|
||||
actual_fn(...args);
|
||||
}, wait);
|
||||
};
|
||||
};
|
||||
|
||||
function initSearch() {
|
||||
const searchBar = document.getElementById("search-bar");
|
||||
const searchResults = document.getElementById("search-results");
|
||||
const searchContainer = document.getElementById("search-container");
|
||||
const MAX_ITEMS = 10;
|
||||
const MAX_RESULTS = 4;
|
||||
|
||||
let currentTerm = "";
|
||||
|
||||
searchBar.addEventListener("keyup", (e) => {
|
||||
const searchVal = searchBar.value.trim();
|
||||
const results = fuse.search(searchVal, { limit: MAX_ITEMS });
|
||||
|
||||
let html = "";
|
||||
for (const result of results) {
|
||||
html += makeTeaser(result, searchVal);
|
||||
}
|
||||
searchResults.innerHTML = html;
|
||||
|
||||
if (html) {
|
||||
searchResults.style.display = "flex";
|
||||
} else {
|
||||
searchResults.style.display = "none";
|
||||
}
|
||||
});
|
||||
|
||||
function makeTeaser(result, searchVal) {
|
||||
const TEASER_SIZE = 20;
|
||||
let output = `<div class="search-result item"><a class="result-title" href=${result.item.url}>${result.item.title}</a>`;
|
||||
|
||||
for (const match of result.matches) {
|
||||
if (match.key === "title") continue;
|
||||
|
||||
const indices = match.indices.sort((a, b) => Math.abs(a[1] - a[0] - searchVal.length) - Math.abs(b[1] - b[0] - searchVal.length)).slice(0, MAX_RESULTS);
|
||||
const value = match.value;
|
||||
|
||||
for (const ind of indices) {
|
||||
const start = Math.max(0, ind[0] - TEASER_SIZE);
|
||||
const end = Math.min(value.length - 1, ind[1] + TEASER_SIZE);
|
||||
output += "<span>"
|
||||
+ value.substring(start, ind[0])
|
||||
+ `<strong>${value.substring(ind[0], ind[1] + 1)}</strong>`
|
||||
+ value.substring(ind[1] + 1, end)
|
||||
+ "</span>";
|
||||
}
|
||||
|
||||
if (match.indices.length > 4) {
|
||||
const moreMatchesText = document.getElementById("more-matches-text").textContent;
|
||||
output += `<span class="more-matches">${moreMatchesText}</span>`.replace("$MATCHES", `+${match.indices.length - MAX_RESULTS}`);
|
||||
}
|
||||
}
|
||||
return output + "</div>";
|
||||
}
|
||||
|
||||
/*window.addEventListener("click", function (event) {
|
||||
if (searchSetup && searchBar.getAttribute("disabled") === null && !searchContainer.contains(event.target)) {
|
||||
toggleSearch();
|
||||
}
|
||||
}, { passive: true });*/
|
||||
|
||||
document.addEventListener("keydown", function(event) {
|
||||
if (event.key === "/") {
|
||||
event.preventDefault();
|
||||
toggleSearch();
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById("search-toggle").addEventListener("click", toggleSearch);
|
||||
}
|
||||
|
||||
if (document.readyState === "complete" ||
|
||||
(document.readyState !== "loading" && !document.documentElement.doScroll))
|
||||
initSearch();
|
||||
else
|
||||
document.addEventListener("DOMContentLoaded", initSearch);
|
@ -85,6 +85,16 @@ function updateActiveButton(theme) {
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById("theme-light").addEventListener("click", function () {
|
||||
switchTheme("light");
|
||||
});
|
||||
document.getElementById("theme-dark").addEventListener("click", function () {
|
||||
switchTheme("dark");
|
||||
});
|
||||
document.getElementById("theme-system").addEventListener("click", function () {
|
||||
switchTheme("system");
|
||||
});
|
||||
|
||||
// Update icon class on page load based on current theme
|
||||
const currentTheme = localStorage.getItem("theme") || window.defaultTheme || "system";
|
||||
updateIconClass(currentTheme);
|
||||
|
Reference in New Issue
Block a user