diff options
author | Alexander Neonxp Kiryukhin <i@neonxp.ru> | 2024-11-16 19:32:18 +0300 |
---|---|---|
committer | Alexander Neonxp Kiryukhin <i@neonxp.ru> | 2024-11-17 01:28:57 +0300 |
commit | 239d68f94c6250276850fbe95eaa6cdd5c38fb26 (patch) | |
tree | c576da169afc442f51eae6213ad0ff749ed86589 /themes/hugo-theme-stack/assets/ts | |
parent | 8e79098193fd0a8b65305dd8054cf7c424c60bc5 (diff) |
Своя тема, полностью всё переделал
Diffstat (limited to 'themes/hugo-theme-stack/assets/ts')
-rw-r--r-- | themes/hugo-theme-stack/assets/ts/color.ts | 63 | ||||
-rw-r--r-- | themes/hugo-theme-stack/assets/ts/colorScheme.ts | 92 | ||||
-rw-r--r-- | themes/hugo-theme-stack/assets/ts/createElement.ts | 34 | ||||
-rw-r--r-- | themes/hugo-theme-stack/assets/ts/gallery.ts | 186 | ||||
-rw-r--r-- | themes/hugo-theme-stack/assets/ts/main.ts | 112 | ||||
-rw-r--r-- | themes/hugo-theme-stack/assets/ts/menu.ts | 83 | ||||
-rw-r--r-- | themes/hugo-theme-stack/assets/ts/scrollspy.ts | 131 | ||||
-rw-r--r-- | themes/hugo-theme-stack/assets/ts/search.tsx | 333 | ||||
-rw-r--r-- | themes/hugo-theme-stack/assets/ts/smoothAnchors.ts | 37 |
9 files changed, 0 insertions, 1071 deletions
diff --git a/themes/hugo-theme-stack/assets/ts/color.ts b/themes/hugo-theme-stack/assets/ts/color.ts deleted file mode 100644 index 50581d1..0000000 --- a/themes/hugo-theme-stack/assets/ts/color.ts +++ /dev/null @@ -1,63 +0,0 @@ -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 -}
\ No newline at end of file diff --git a/themes/hugo-theme-stack/assets/ts/colorScheme.ts b/themes/hugo-theme-stack/assets/ts/colorScheme.ts deleted file mode 100644 index 978e98e..0000000 --- a/themes/hugo-theme-stack/assets/ts/colorScheme.ts +++ /dev/null @@ -1,92 +0,0 @@ -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(); - if (window.matchMedia('(prefers-color-scheme: dark)').matches === true) - this.systemPreferScheme = 'dark' - else - this.systemPreferScheme = 'light'; - - 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; diff --git a/themes/hugo-theme-stack/assets/ts/createElement.ts b/themes/hugo-theme-stack/assets/ts/createElement.ts deleted file mode 100644 index 3a1e85e..0000000 --- a/themes/hugo-theme-stack/assets/ts/createElement.ts +++ /dev/null @@ -1,34 +0,0 @@ -/** - * 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;
\ No newline at end of file diff --git a/themes/hugo-theme-stack/assets/ts/gallery.ts b/themes/hugo-theme-stack/assets/ts/gallery.ts deleted file mode 100644 index 9840f1e..0000000 --- a/themes/hugo-theme-stack/assets/ts/gallery.ts +++ /dev/null @@ -1,186 +0,0 @@ -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;
\ No newline at end of file diff --git a/themes/hugo-theme-stack/assets/ts/main.ts b/themes/hugo-theme-stack/assets/ts/main.ts deleted file mode 100644 index f3160ae..0000000 --- a/themes/hugo-theme-stack/assets/ts/main.ts +++ /dev/null @@ -1,112 +0,0 @@ -/*! -* 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;
\ No newline at end of file diff --git a/themes/hugo-theme-stack/assets/ts/menu.ts b/themes/hugo-theme-stack/assets/ts/menu.ts deleted file mode 100644 index 34615ba..0000000 --- a/themes/hugo-theme-stack/assets/ts/menu.ts +++ /dev/null @@ -1,83 +0,0 @@ -/** - * 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'); - }); - } -}
\ No newline at end of file diff --git a/themes/hugo-theme-stack/assets/ts/scrollspy.ts b/themes/hugo-theme-stack/assets/ts/scrollspy.ts deleted file mode 100644 index 8a14085..0000000 --- a/themes/hugo-theme-stack/assets/ts/scrollspy.ts +++ /dev/null @@ -1,131 +0,0 @@ -// 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 };
\ No newline at end of file diff --git a/themes/hugo-theme-stack/assets/ts/search.tsx b/themes/hugo-theme-stack/assets/ts/search.tsx deleted file mode 100644 index 1c81dd1..0000000 --- a/themes/hugo-theme-stack/assets/ts/search.tsx +++ /dev/null @@ -1,333 +0,0 @@ -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 = { - '&': '&', - '<': '<', - '>': '>', - '"': '"', - '…': '…' -}; - -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; - - /// Check if there's already value in the search input - if (this.input.value.trim() !== '') { - this.doSearch(this.input.value.split(' ')); - } - else { - 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 === '') { - lastSearch = ''; - 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;
\ No newline at end of file diff --git a/themes/hugo-theme-stack/assets/ts/smoothAnchors.ts b/themes/hugo-theme-stack/assets/ts/smoothAnchors.ts deleted file mode 100644 index 16ab6a3..0000000 --- a/themes/hugo-theme-stack/assets/ts/smoothAnchors.ts +++ /dev/null @@ -1,37 +0,0 @@ -// 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 = decodeURI(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 };
\ No newline at end of file |