remove git modules

This commit is contained in:
FlintyLemming
2022-10-11 17:34:41 +08:00
parent dca907d5ab
commit ed850ed725
187 changed files with 9690 additions and 4 deletions

View File

@@ -0,0 +1,63 @@
interface colorScheme {
hash: string, /// Regenerate color scheme when the image hash is changed
DarkMuted: {
hex: string,
rgb: Number[],
bodyTextColor: string
},
Vibrant: {
hex: string,
rgb: Number[],
bodyTextColor: string
}
}
let colorsCache: { [key: string]: colorScheme } = {};
if (localStorage.hasOwnProperty('StackColorsCache')) {
try {
colorsCache = JSON.parse(localStorage.getItem('StackColorsCache'));
}
catch (e) {
colorsCache = {};
}
}
async function getColor(key: string, hash: string, imageURL: string) {
if (!key) {
/**
* If no key is provided, do not cache the result
*/
return await Vibrant.from(imageURL).getPalette();
}
if (!colorsCache.hasOwnProperty(key) || colorsCache[key].hash !== hash) {
/**
* If key is provided, but not found in cache, or the hash mismatches => Regenerate color scheme
*/
const palette = await Vibrant.from(imageURL).getPalette();
colorsCache[key] = {
hash: hash,
Vibrant: {
hex: palette.Vibrant.hex,
rgb: palette.Vibrant.rgb,
bodyTextColor: palette.Vibrant.bodyTextColor
},
DarkMuted: {
hex: palette.DarkMuted.hex,
rgb: palette.DarkMuted.rgb,
bodyTextColor: palette.DarkMuted.bodyTextColor
}
}
/* Save the result in localStorage */
localStorage.setItem('StackColorsCache', JSON.stringify(colorsCache));
}
return colorsCache[key];
}
export {
getColor
}

View File

@@ -0,0 +1,88 @@
type colorScheme = 'light' | 'dark' | 'auto';
class StackColorScheme {
private localStorageKey = 'StackColorScheme';
private currentScheme: colorScheme;
private systemPreferScheme: colorScheme;
constructor(toggleEl: HTMLElement) {
this.bindMatchMedia();
this.currentScheme = this.getSavedScheme();
this.dispatchEvent(document.documentElement.dataset.scheme as colorScheme);
if (toggleEl)
this.bindClick(toggleEl);
if (document.body.style.transition == '')
document.body.style.setProperty('transition', 'background-color .3s ease');
}
private saveScheme() {
localStorage.setItem(this.localStorageKey, this.currentScheme);
}
private bindClick(toggleEl: HTMLElement) {
toggleEl.addEventListener('click', (e) => {
if (this.isDark()) {
/// Disable dark mode
this.currentScheme = 'light';
}
else {
this.currentScheme = 'dark';
}
this.setBodyClass();
if (this.currentScheme == this.systemPreferScheme) {
/// Set to auto
this.currentScheme = 'auto';
}
this.saveScheme();
})
}
private isDark() {
return (this.currentScheme == 'dark' || this.currentScheme == 'auto' && this.systemPreferScheme == 'dark');
}
private dispatchEvent(colorScheme: colorScheme) {
const event = new CustomEvent('onColorSchemeChange', {
detail: colorScheme
});
window.dispatchEvent(event);
}
private setBodyClass() {
if (this.isDark()) {
document.documentElement.dataset.scheme = 'dark';
}
else {
document.documentElement.dataset.scheme = 'light';
}
this.dispatchEvent(document.documentElement.dataset.scheme as colorScheme);
}
private getSavedScheme(): colorScheme {
const savedScheme = localStorage.getItem(this.localStorageKey);
if (savedScheme == 'light' || savedScheme == 'dark' || savedScheme == 'auto') return savedScheme;
else return 'auto';
}
private bindMatchMedia() {
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {
if (e.matches) {
this.systemPreferScheme = 'dark';
}
else {
this.systemPreferScheme = 'light';
}
this.setBodyClass();
});
}
}
export default StackColorScheme;

View File

@@ -0,0 +1,34 @@
/**
* createElement
* Edited from:
* @link https://stackoverflow.com/a/42405694
*/
function createElement(tag, attrs, children) {
var element = document.createElement(tag);
for (let name in attrs) {
if (name && attrs.hasOwnProperty(name)) {
let value = attrs[name];
if (name == "dangerouslySetInnerHTML") {
element.innerHTML = value.__html;
}
else if (value === true) {
element.setAttribute(name, name);
} else if (value !== false && value != null) {
element.setAttribute(name, value.toString());
}
}
}
for (let i = 2; i < arguments.length; i++) {
let child = arguments[i];
if (child) {
element.appendChild(
child.nodeType == null ?
document.createTextNode(child.toString()) : child);
}
}
return element;
}
export default createElement;

View File

@@ -0,0 +1,186 @@
declare global {
interface Window {
PhotoSwipe: any;
PhotoSwipeUI_Default: any
}
}
interface PhotoSwipeItem {
w: number;
h: number;
src: string;
msrc: string;
title?: string;
el: HTMLElement;
}
class StackGallery {
private galleryUID: number;
private items: PhotoSwipeItem[] = [];
constructor(container: HTMLElement, galleryUID = 1) {
if (window.PhotoSwipe == undefined || window.PhotoSwipeUI_Default == undefined) {
console.error("PhotoSwipe lib not loaded.");
return;
}
this.galleryUID = galleryUID;
StackGallery.createGallery(container);
this.loadItems(container);
this.bindClick();
}
private loadItems(container: HTMLElement) {
this.items = [];
const figures = container.querySelectorAll('figure.gallery-image');
for (const el of figures) {
const figcaption = el.querySelector('figcaption'),
img = el.querySelector('img');
let aux: PhotoSwipeItem = {
w: parseInt(img.getAttribute('width')),
h: parseInt(img.getAttribute('height')),
src: img.src,
msrc: img.getAttribute('data-thumb') || img.src,
el: el
}
if (figcaption) {
aux.title = figcaption.innerHTML;
}
this.items.push(aux);
}
}
public static createGallery(container: HTMLElement) {
/// The process of wrapping image with figure tag is done using JavaScript instead of only Hugo markdown render hook
/// because it can not detect whether image is being wrapped by a link or not
/// and it lead to a invalid HTML construction (<a><figure><img></figure></a>)
const images = container.querySelectorAll('img.gallery-image');
for (const img of Array.from(images)) {
/// Images are wrapped with figure tag if the paragraph has only images without texts
/// This is done to allow inline images within paragraphs
const paragraph = img.closest('p');
if (!paragraph || !container.contains(paragraph)) continue;
if (paragraph.textContent.trim() == '') {
/// Once we insert figcaption, this check no longer works
/// So we add a class to paragraph to mark it
paragraph.classList.add('no-text');
}
let isNewLineImage = paragraph.classList.contains('no-text');
if (!isNewLineImage) continue;
const hasLink = img.parentElement.tagName == 'A';
let el: HTMLElement = img;
/// Wrap image with figure tag, with flex-grow and flex-basis values extracted from img's data attributes
const figure = document.createElement('figure');
figure.style.setProperty('flex-grow', img.getAttribute('data-flex-grow') || '1');
figure.style.setProperty('flex-basis', img.getAttribute('data-flex-basis') || '0');
if (hasLink) {
/// Wrap <a> if it exists
el = img.parentElement;
}
el.parentElement.insertBefore(figure, el);
figure.appendChild(el);
/// Add figcaption if it exists
if (img.hasAttribute('alt')) {
const figcaption = document.createElement('figcaption');
figcaption.innerText = img.getAttribute('alt');
figure.appendChild(figcaption);
}
/// Wrap img tag with <a> tag if image was not wrapped by <a> tag
if (!hasLink) {
figure.className = 'gallery-image';
const a = document.createElement('a');
a.href = img.src;
a.setAttribute('target', '_blank');
img.parentNode.insertBefore(a, img);
a.appendChild(img);
}
}
const figuresEl = container.querySelectorAll('figure.gallery-image');
let currentGallery = [];
for (const figure of figuresEl) {
if (!currentGallery.length) {
/// First iteration
currentGallery = [figure];
}
else if (figure.previousElementSibling === currentGallery[currentGallery.length - 1]) {
/// Adjacent figures
currentGallery.push(figure);
}
else if (currentGallery.length) {
/// End gallery
StackGallery.wrap(currentGallery);
currentGallery = [figure];
}
}
if (currentGallery.length > 0) {
StackGallery.wrap(currentGallery);
}
}
/**
* Wrap adjacent figure tags with div.gallery
* @param figures
*/
public static wrap(figures: HTMLElement[]) {
const galleryContainer = document.createElement('div');
galleryContainer.className = 'gallery';
const parentNode = figures[0].parentNode,
first = figures[0];
parentNode.insertBefore(galleryContainer, first)
for (const figure of figures) {
galleryContainer.appendChild(figure);
}
}
public open(index: number) {
const pswp = document.querySelector('.pswp') as HTMLDivElement;
const ps = new window.PhotoSwipe(pswp, window.PhotoSwipeUI_Default, this.items, {
index: index,
galleryUID: this.galleryUID,
getThumbBoundsFn: (index) => {
const thumbnail = this.items[index].el.getElementsByTagName('img')[0],
pageYScroll = window.pageYOffset || document.documentElement.scrollTop,
rect = thumbnail.getBoundingClientRect();
return { x: rect.left, y: rect.top + pageYScroll, w: rect.width };
}
});
ps.init();
}
private bindClick() {
for (const [index, item] of this.items.entries()) {
const a = item.el.querySelector('a');
a.addEventListener('click', (e) => {
e.preventDefault();
this.open(index);
})
}
}
}
export default StackGallery;

View File

@@ -0,0 +1,112 @@
/*!
* Hugo Theme Stack
*
* @author: Jimmy Cai
* @website: https://jimmycai.com
* @link: https://github.com/CaiJimmy/hugo-theme-stack
*/
import StackGallery from "ts/gallery";
import { getColor } from 'ts/color';
import menu from 'ts/menu';
import createElement from 'ts/createElement';
import StackColorScheme from 'ts/colorScheme';
import { setupScrollspy } from 'ts/scrollspy';
import { setupSmoothAnchors } from "ts/smoothAnchors";
let Stack = {
init: () => {
/**
* Bind menu event
*/
menu();
const articleContent = document.querySelector('.article-content') as HTMLElement;
if (articleContent) {
new StackGallery(articleContent);
setupSmoothAnchors();
setupScrollspy();
}
/**
* Add linear gradient background to tile style article
*/
const articleTile = document.querySelector('.article-list--tile');
if (articleTile) {
let observer = new IntersectionObserver(async (entries, observer) => {
entries.forEach(entry => {
if (!entry.isIntersecting) return;
observer.unobserve(entry.target);
const articles = entry.target.querySelectorAll('article.has-image');
articles.forEach(async articles => {
const image = articles.querySelector('img'),
imageURL = image.src,
key = image.getAttribute('data-key'),
hash = image.getAttribute('data-hash'),
articleDetails: HTMLDivElement = articles.querySelector('.article-details');
const colors = await getColor(key, hash, imageURL);
articleDetails.style.background = `
linear-gradient(0deg,
rgba(${colors.DarkMuted.rgb[0]}, ${colors.DarkMuted.rgb[1]}, ${colors.DarkMuted.rgb[2]}, 0.5) 0%,
rgba(${colors.Vibrant.rgb[0]}, ${colors.Vibrant.rgb[1]}, ${colors.Vibrant.rgb[2]}, 0.75) 100%)`;
})
})
});
observer.observe(articleTile)
}
/**
* Add copy button to code block
*/
const highlights = document.querySelectorAll('.article-content div.highlight');
const copyText = `Copy`,
copiedText = `Copied!`;
highlights.forEach(highlight => {
const copyButton = document.createElement('button');
copyButton.innerHTML = copyText;
copyButton.classList.add('copyCodeButton');
highlight.appendChild(copyButton);
const codeBlock = highlight.querySelector('code[data-lang]');
if (!codeBlock) return;
copyButton.addEventListener('click', () => {
navigator.clipboard.writeText(codeBlock.textContent)
.then(() => {
copyButton.textContent = copiedText;
setTimeout(() => {
copyButton.textContent = copyText;
}, 1000);
})
.catch(err => {
alert(err)
console.log('Something went wrong', err);
});
});
});
new StackColorScheme(document.getElementById('dark-mode-toggle'));
}
}
window.addEventListener('load', () => {
setTimeout(function () {
Stack.init();
}, 0);
})
declare global {
interface Window {
createElement: any;
Stack: any
}
}
window.Stack = Stack;
window.createElement = createElement;

View File

@@ -0,0 +1,83 @@
/**
* Slide up/down
* Code from https://dev.to/bmsvieira/vanilla-js-slidedown-up-4dkn
* @param target
* @param duration
*/
let slideUp = (target: HTMLElement, duration = 500) => {
target.classList.add('transiting');
target.style.transitionProperty = 'height, margin, padding';
target.style.transitionDuration = duration + 'ms';
///target.style.boxSizing = 'border-box';
target.style.height = target.offsetHeight + 'px';
target.offsetHeight;
target.style.overflow = 'hidden';
target.style.height = "0";
target.style.paddingTop = "0";
target.style.paddingBottom = "0";
target.style.marginTop = "0";
target.style.marginBottom = "0";
window.setTimeout(() => {
target.classList.remove('show')
target.style.removeProperty('height');
target.style.removeProperty('padding-top');
target.style.removeProperty('padding-bottom');
target.style.removeProperty('margin-top');
target.style.removeProperty('margin-bottom');
target.style.removeProperty('overflow');
target.style.removeProperty('transition-duration');
target.style.removeProperty('transition-property');
target.classList.remove('transiting');
}, duration);
}
let slideDown = (target: HTMLElement, duration = 500) => {
target.classList.add('transiting');
target.style.removeProperty('display');
target.classList.add('show');
let height = target.offsetHeight;
target.style.overflow = 'hidden';
target.style.height = "0";
target.style.paddingTop = "0";
target.style.paddingBottom = "0";
target.style.marginTop = "0";
target.style.marginBottom = "0";
target.offsetHeight;
///target.style.boxSizing = 'border-box';
target.style.transitionProperty = "height, margin, padding";
target.style.transitionDuration = duration + 'ms';
target.style.height = height + 'px';
target.style.removeProperty('padding-top');
target.style.removeProperty('padding-bottom');
target.style.removeProperty('margin-top');
target.style.removeProperty('margin-bottom');
window.setTimeout(() => {
target.style.removeProperty('height');
target.style.removeProperty('overflow');
target.style.removeProperty('transition-duration');
target.style.removeProperty('transition-property');
target.classList.remove('transiting');
}, duration);
}
let slideToggle = (target, duration = 500) => {
if (window.getComputedStyle(target).display === 'none') {
return slideDown(target, duration);
} else {
return slideUp(target, duration);
}
}
export default function () {
const toggleMenu = document.getElementById('toggle-menu');
if (toggleMenu) {
toggleMenu.addEventListener('click', () => {
if (document.getElementById('main-menu').classList.contains('transiting')) return;
document.body.classList.toggle('show-menu');
slideToggle(document.getElementById('main-menu'), 300);
toggleMenu.classList.toggle('is-active');
});
}
}

View File

@@ -0,0 +1,131 @@
// Implements a scroll spy system for the ToC, displaying the current section with an indicator and scrolling to it when needed.
// Inspired from https://gomakethings.com/debouncing-your-javascript-events/
function debounced(func: Function) {
let timeout;
return () => {
if (timeout) {
window.cancelAnimationFrame(timeout);
}
timeout = window.requestAnimationFrame(() => func());
}
}
const headersQuery = ".article-content h1[id], .article-content h2[id], .article-content h3[id], .article-content h4[id], .article-content h5[id], .article-content h6[id]";
const tocQuery = "#TableOfContents";
const navigationQuery = "#TableOfContents li";
const activeClass = "active-class";
function scrollToTocElement(tocElement: HTMLElement, scrollableNavigation: HTMLElement) {
let textHeight = tocElement.querySelector("a").offsetHeight;
let scrollTop = tocElement.offsetTop - scrollableNavigation.offsetHeight / 2 + textHeight / 2 - scrollableNavigation.offsetTop;
if (scrollTop < 0) {
scrollTop = 0;
}
scrollableNavigation.scrollTo({ top: scrollTop, behavior: "smooth" });
}
type IdToElementMap = { [key: string]: HTMLElement };
function buildIdToNavigationElementMap(navigation: NodeListOf<Element>): IdToElementMap {
const sectionLinkRef: IdToElementMap = {};
navigation.forEach((navigationElement: HTMLElement) => {
const link = navigationElement.querySelector("a");
const href = link.getAttribute("href");
if (href.startsWith("#")) {
sectionLinkRef[href.slice(1)] = navigationElement;
}
});
return sectionLinkRef;
}
function computeOffsets(headers: NodeListOf<Element>) {
let sectionsOffsets = [];
headers.forEach((header: HTMLElement) => { sectionsOffsets.push({ id: header.id, offset: header.offsetTop }) });
sectionsOffsets.sort((a, b) => a.offset - b.offset);
return sectionsOffsets;
}
function setupScrollspy() {
let headers = document.querySelectorAll(headersQuery);
if (!headers) {
console.warn("No header matched query", headers);
return;
}
let scrollableNavigation = document.querySelector(tocQuery) as HTMLElement | undefined;
if (!scrollableNavigation) {
console.warn("No toc matched query", tocQuery);
return;
}
let navigation = document.querySelectorAll(navigationQuery);
if (!navigation) {
console.warn("No navigation matched query", navigationQuery);
return;
}
let sectionsOffsets = computeOffsets(headers);
// We need to avoid scrolling when the user is actively interacting with the ToC. Otherwise, if the user clicks on a link in the ToC,
// we would scroll their view, which is not optimal usability-wise.
let tocHovered: boolean = false;
scrollableNavigation.addEventListener("mouseenter", debounced(() => tocHovered = true));
scrollableNavigation.addEventListener("mouseleave", debounced(() => tocHovered = false));
let activeSectionLink: Element;
let idToNavigationElement: IdToElementMap = buildIdToNavigationElementMap(navigation);
function scrollHandler() {
let scrollPosition = document.documentElement.scrollTop || document.body.scrollTop;
let newActiveSection: HTMLElement | undefined;
// Find the section that is currently active.
// It is possible for no section to be active, so newActiveSection may be undefined.
sectionsOffsets.forEach((section) => {
if (scrollPosition >= section.offset - 20) {
newActiveSection = document.getElementById(section.id);
}
});
// Find the link for the active section. Once again, there are a few edge cases:
// - No active section = no link => undefined
// - No active section but the link does not exist in toc (e.g. because it is outside of the applicable ToC levels) => undefined
let newActiveSectionLink: HTMLElement | undefined
if (newActiveSection) {
newActiveSectionLink = idToNavigationElement[newActiveSection.id];
}
if (newActiveSection && !newActiveSectionLink) {
// The active section does not have a link in the ToC, so we can't scroll to it.
console.debug("No link found for section", newActiveSection);
} else if (newActiveSectionLink !== activeSectionLink) {
if (activeSectionLink)
activeSectionLink.classList.remove(activeClass);
if (newActiveSectionLink) {
newActiveSectionLink.classList.add(activeClass);
if (!tocHovered) {
// Scroll so that newActiveSectionLink is in the middle of scrollableNavigation, except when it's from a manual click (hence the tocHovered check)
scrollToTocElement(newActiveSectionLink, scrollableNavigation);
}
}
activeSectionLink = newActiveSectionLink;
}
}
window.addEventListener("scroll", debounced(scrollHandler));
// Resizing may cause the offset values to change: recompute them.
function resizeHandler() {
sectionsOffsets = computeOffsets(headers);
scrollHandler();
}
window.addEventListener("resize", debounced(resizeHandler));
}
export { setupScrollspy };

View File

@@ -0,0 +1,325 @@
interface pageData {
title: string,
date: string,
permalink: string,
content: string,
image?: string,
preview: string,
matchCount: number
}
interface match {
start: number,
end: number
}
/**
* Escape HTML tags as HTML entities
* Edited from:
* @link https://stackoverflow.com/a/5499821
*/
const tagsToReplace = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
'…': '&hellip;'
};
function replaceTag(tag) {
return tagsToReplace[tag] || tag;
}
function replaceHTMLEnt(str) {
return str.replace(/[&<>"]/g, replaceTag);
}
function escapeRegExp(string) {
return string.replace(/[.*+\-?^${}()|[\]\\]/g, '\\$&');
}
class Search {
private data: pageData[];
private form: HTMLFormElement;
private input: HTMLInputElement;
private list: HTMLDivElement;
private resultTitle: HTMLHeadElement;
private resultTitleTemplate: string;
constructor({ form, input, list, resultTitle, resultTitleTemplate }) {
this.form = form;
this.input = input;
this.list = list;
this.resultTitle = resultTitle;
this.resultTitleTemplate = resultTitleTemplate;
this.handleQueryString();
this.bindQueryStringChange();
this.bindSearchForm();
}
/**
* Processes search matches
* @param str original text
* @param matches array of matches
* @param ellipsis whether to add ellipsis to the end of each match
* @param charLimit max length of preview string
* @param offset how many characters before and after the match to include in preview
* @returns preview string
*/
private static processMatches(str: string, matches: match[], ellipsis: boolean = true, charLimit = 140, offset = 20): string {
matches.sort((a, b) => {
return a.start - b.start;
});
let i = 0,
lastIndex = 0,
charCount = 0;
const resultArray: string[] = [];
while (i < matches.length) {
const item = matches[i];
/// item.start >= lastIndex (equal only for the first iteration)
/// because of the while loop that comes after, iterating over variable j
if (ellipsis && item.start - offset > lastIndex) {
resultArray.push(`${replaceHTMLEnt(str.substring(lastIndex, lastIndex + offset))} [...] `);
resultArray.push(`${replaceHTMLEnt(str.substring(item.start - offset, item.start))}`);
charCount += offset * 2;
}
else {
/// If the match is too close to the end of last match, don't add ellipsis
resultArray.push(replaceHTMLEnt(str.substring(lastIndex, item.start)));
charCount += item.start - lastIndex;
}
let j = i + 1,
end = item.end;
/// Include as many matches as possible
/// [item.start, end] is the range of the match
while (j < matches.length && matches[j].start <= end) {
end = Math.max(matches[j].end, end);
++j;
}
resultArray.push(`<mark>${replaceHTMLEnt(str.substring(item.start, end))}</mark>`);
charCount += end - item.start;
i = j;
lastIndex = end;
if (ellipsis && charCount > charLimit) break;
}
/// Add the rest of the string
if (lastIndex < str.length) {
let end = str.length;
if (ellipsis) end = Math.min(end, lastIndex + offset);
resultArray.push(`${replaceHTMLEnt(str.substring(lastIndex, end))}`);
if (ellipsis && end != str.length) {
resultArray.push(` [...]`);
}
}
return resultArray.join('');
}
private async searchKeywords(keywords: string[]) {
const rawData = await this.getData();
const results: pageData[] = [];
const regex = new RegExp(keywords.filter((v, index, arr) => {
arr[index] = escapeRegExp(v);
return v.trim() !== '';
}).join('|'), 'gi');
for (const item of rawData) {
const titleMatches: match[] = [],
contentMatches: match[] = [];
let result = {
...item,
preview: '',
matchCount: 0
}
const contentMatchAll = item.content.matchAll(regex);
for (const match of Array.from(contentMatchAll)) {
contentMatches.push({
start: match.index,
end: match.index + match[0].length
});
}
const titleMatchAll = item.title.matchAll(regex);
for (const match of Array.from(titleMatchAll)) {
titleMatches.push({
start: match.index,
end: match.index + match[0].length
});
}
if (titleMatches.length > 0) result.title = Search.processMatches(result.title, titleMatches, false);
if (contentMatches.length > 0) {
result.preview = Search.processMatches(result.content, contentMatches);
}
else {
/// If there are no matches in the content, use the first 140 characters as preview
result.preview = replaceHTMLEnt(result.content.substring(0, 140));
}
result.matchCount = titleMatches.length + contentMatches.length;
if (result.matchCount > 0) results.push(result);
}
/// Result with more matches appears first
return results.sort((a, b) => {
return b.matchCount - a.matchCount;
});
}
private async doSearch(keywords: string[]) {
const startTime = performance.now();
const results = await this.searchKeywords(keywords);
this.clear();
for (const item of results) {
this.list.append(Search.render(item));
}
const endTime = performance.now();
this.resultTitle.innerText = this.generateResultTitle(results.length, ((endTime - startTime) / 1000).toPrecision(1));
}
private generateResultTitle(resultLen, time) {
return this.resultTitleTemplate.replace("#PAGES_COUNT", resultLen).replace("#TIME_SECONDS", time);
}
public async getData() {
if (!this.data) {
/// Not fetched yet
const jsonURL = this.form.dataset.json;
this.data = await fetch(jsonURL).then(res => res.json());
const parser = new DOMParser();
for (const item of this.data) {
item.content = parser.parseFromString(item.content, 'text/html').body.innerText;
}
}
return this.data;
}
private bindSearchForm() {
let lastSearch = '';
const eventHandler = (e) => {
e.preventDefault();
const keywords = this.input.value.trim();
Search.updateQueryString(keywords, true);
if (keywords === '') {
return this.clear();
}
if (lastSearch === keywords) return;
lastSearch = keywords;
this.doSearch(keywords.split(' '));
}
this.input.addEventListener('input', eventHandler);
this.input.addEventListener('compositionend', eventHandler);
}
private clear() {
this.list.innerHTML = '';
this.resultTitle.innerText = '';
}
private bindQueryStringChange() {
window.addEventListener('popstate', (e) => {
this.handleQueryString()
})
}
private handleQueryString() {
const pageURL = new URL(window.location.toString());
const keywords = pageURL.searchParams.get('keyword');
this.input.value = keywords;
if (keywords) {
this.doSearch(keywords.split(' '));
}
else {
this.clear()
}
}
private static updateQueryString(keywords: string, replaceState = false) {
const pageURL = new URL(window.location.toString());
if (keywords === '') {
pageURL.searchParams.delete('keyword')
}
else {
pageURL.searchParams.set('keyword', keywords);
}
if (replaceState) {
window.history.replaceState('', '', pageURL.toString());
}
else {
window.history.pushState('', '', pageURL.toString());
}
}
public static render(item: pageData) {
return <article>
<a href={item.permalink}>
<div class="article-details">
<h2 class="article-title" dangerouslySetInnerHTML={{ __html: item.title }}></h2>
<section class="article-preview" dangerouslySetInnerHTML={{ __html: item.preview }}></section>
</div>
{item.image &&
<div class="article-image">
<img src={item.image} loading="lazy" />
</div>
}
</a>
</article>;
}
}
declare global {
interface Window {
searchResultTitleTemplate: string;
}
}
window.addEventListener('load', () => {
setTimeout(function () {
const searchForm = document.querySelector('.search-form') as HTMLFormElement,
searchInput = searchForm.querySelector('input') as HTMLInputElement,
searchResultList = document.querySelector('.search-result--list') as HTMLDivElement,
searchResultTitle = document.querySelector('.search-result--title') as HTMLHeadingElement;
new Search({
form: searchForm,
input: searchInput,
list: searchResultList,
resultTitle: searchResultTitle,
resultTitleTemplate: window.searchResultTitleTemplate
});
}, 0);
})
export default Search;

View File

@@ -0,0 +1,37 @@
// Implements smooth scrolling when clicking on an anchor link.
// This is required instead of using modern CSS because Chromium does not currently support scrolling
// one element with scrollTo while another element is scrolled because of a click on a link. This would
// thus not work with the ToC scrollspy and e.g. footnotes.
// Here are additional links about this issue:
// - https://stackoverflow.com/questions/49318497/google-chrome-simultaneously-smooth-scrollintoview-with-more-elements-doesn
// - https://stackoverflow.com/questions/57214373/scrollintoview-using-smooth-function-on-multiple-elements-in-chrome
// - https://bugs.chromium.org/p/chromium/issues/detail?id=833617
// - https://bugs.chromium.org/p/chromium/issues/detail?id=1043933
// - https://bugs.chromium.org/p/chromium/issues/detail?id=1121151
const anchorLinksQuery = "a[href]";
function setupSmoothAnchors() {
document.querySelectorAll(anchorLinksQuery).forEach(aElement => {
let href = aElement.getAttribute("href");
if (!href.startsWith("#")) {
return;
}
aElement.addEventListener("click", clickEvent => {
clickEvent.preventDefault();
const targetId = aElement.getAttribute("href").substring(1),
target = document.getElementById(targetId) as HTMLElement,
offset = target.getBoundingClientRect().top - document.documentElement.getBoundingClientRect().top;
window.history.pushState({}, "", aElement.getAttribute("href"));
scrollTo({
top: offset,
behavior: "smooth"
});
});
});
}
export { setupSmoothAnchors };