wip(js): add head.html and assets/js
This commit is contained in:
7
assets/js/categories.js
Normal file
7
assets/js/categories.js
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { basic, initSidebar, initTopbar } from './modules/layouts';
|
||||||
|
import { categoryCollapse } from './modules/components';
|
||||||
|
|
||||||
|
basic();
|
||||||
|
initSidebar();
|
||||||
|
initTopbar();
|
||||||
|
categoryCollapse();
|
5
assets/js/commons.js
Normal file
5
assets/js/commons.js
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import { basic, initSidebar, initTopbar } from './modules/layouts';
|
||||||
|
|
||||||
|
initSidebar();
|
||||||
|
initTopbar();
|
||||||
|
basic();
|
8
assets/js/home.js
Normal file
8
assets/js/home.js
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import { basic, initSidebar, initTopbar } from './modules/layouts';
|
||||||
|
import { initLocaleDatetime, loadImg } from './modules/components';
|
||||||
|
|
||||||
|
loadImg();
|
||||||
|
initLocaleDatetime();
|
||||||
|
initSidebar();
|
||||||
|
initTopbar();
|
||||||
|
basic();
|
7
assets/js/misc.js
Normal file
7
assets/js/misc.js
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { basic, initSidebar, initTopbar } from './modules/layouts';
|
||||||
|
import { initLocaleDatetime } from './modules/components';
|
||||||
|
|
||||||
|
initSidebar();
|
||||||
|
initTopbar();
|
||||||
|
initLocaleDatetime();
|
||||||
|
basic();
|
10
assets/js/modules/components.js
Normal file
10
assets/js/modules/components.js
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
export { categoryCollapse } from './components/category-collapse';
|
||||||
|
export { initClipboard } from './components/clipboard';
|
||||||
|
export { loadImg } from './components/img-loading';
|
||||||
|
export { imgPopup } from './components/img-popup';
|
||||||
|
export { initLocaleDatetime } from './components/locale-datetime';
|
||||||
|
export { initToc } from './components/toc';
|
||||||
|
export { loadMermaid } from './components/mermaid';
|
||||||
|
export { modeWatcher } from './components/mode-toggle';
|
||||||
|
export { back2top } from './components/back-to-top';
|
||||||
|
export { loadTooptip } from './components/tooltip-loader';
|
19
assets/js/modules/components/back-to-top.js
Normal file
19
assets/js/modules/components/back-to-top.js
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
/**
|
||||||
|
* Reference: https://bootsnipp.com/snippets/featured/link-to-top-page
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function back2top() {
|
||||||
|
const btn = document.getElementById('back-to-top');
|
||||||
|
|
||||||
|
window.addEventListener('scroll', () => {
|
||||||
|
if (window.scrollY > 50) {
|
||||||
|
btn.classList.add('show');
|
||||||
|
} else {
|
||||||
|
btn.classList.remove('show');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
btn.addEventListener('click', () => {
|
||||||
|
window.scrollTo({ top: 0 });
|
||||||
|
});
|
||||||
|
}
|
36
assets/js/modules/components/category-collapse.js
Normal file
36
assets/js/modules/components/category-collapse.js
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
/**
|
||||||
|
* Tab 'Categories' expand/close effect.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import 'bootstrap/js/src/collapse.js';
|
||||||
|
|
||||||
|
const childPrefix = 'l_';
|
||||||
|
const parentPrefix = 'h_';
|
||||||
|
const children = document.getElementsByClassName('collapse');
|
||||||
|
|
||||||
|
export function categoryCollapse() {
|
||||||
|
[...children].forEach((elem) => {
|
||||||
|
const id = parentPrefix + elem.id.substring(childPrefix.length);
|
||||||
|
const parent = document.getElementById(id);
|
||||||
|
|
||||||
|
// collapse sub-categories
|
||||||
|
elem.addEventListener('hide.bs.collapse', () => {
|
||||||
|
if (parent) {
|
||||||
|
parent.querySelector('.far.fa-folder-open').className =
|
||||||
|
'far fa-folder fa-fw';
|
||||||
|
parent.querySelector('.fas.fa-angle-down').classList.add('rotate');
|
||||||
|
parent.classList.remove('hide-border-bottom');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// expand sub-categories
|
||||||
|
elem.addEventListener('show.bs.collapse', () => {
|
||||||
|
if (parent) {
|
||||||
|
parent.querySelector('.far.fa-folder').className =
|
||||||
|
'far fa-folder-open fa-fw';
|
||||||
|
parent.querySelector('.fas.fa-angle-down').classList.remove('rotate');
|
||||||
|
parent.classList.add('hide-border-bottom');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
143
assets/js/modules/components/clipboard.js
Normal file
143
assets/js/modules/components/clipboard.js
Normal file
@ -0,0 +1,143 @@
|
|||||||
|
/**
|
||||||
|
* Clipboard functions
|
||||||
|
*
|
||||||
|
* Dependencies:
|
||||||
|
* clipboard.js (https://github.com/zenorocha/clipboard.js)
|
||||||
|
*/
|
||||||
|
|
||||||
|
import Tooltip from 'bootstrap/js/src/tooltip';
|
||||||
|
|
||||||
|
const clipboardSelector = '.code-header>button';
|
||||||
|
|
||||||
|
const ICON_DEFAULT = 'far fa-clipboard';
|
||||||
|
const ICON_SUCCESS = 'fas fa-check';
|
||||||
|
|
||||||
|
const ATTR_TIMEOUT = 'timeout';
|
||||||
|
const ATTR_TITLE_SUCCEED = 'data-title-succeed';
|
||||||
|
const ATTR_TITLE_ORIGIN = 'data-bs-original-title';
|
||||||
|
const TIMEOUT = 2000; // in milliseconds
|
||||||
|
|
||||||
|
function isLocked(node) {
|
||||||
|
if (node.hasAttribute(ATTR_TIMEOUT)) {
|
||||||
|
let timeout = node.getAttribute(ATTR_TIMEOUT);
|
||||||
|
if (Number(timeout) > Date.now()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function lock(node) {
|
||||||
|
node.setAttribute(ATTR_TIMEOUT, Date.now() + TIMEOUT);
|
||||||
|
}
|
||||||
|
|
||||||
|
function unlock(node) {
|
||||||
|
node.removeAttribute(ATTR_TIMEOUT);
|
||||||
|
}
|
||||||
|
|
||||||
|
function showTooltip(btn) {
|
||||||
|
const succeedTitle = btn.getAttribute(ATTR_TITLE_SUCCEED);
|
||||||
|
btn.setAttribute(ATTR_TITLE_ORIGIN, succeedTitle);
|
||||||
|
Tooltip.getInstance(btn).show();
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideTooltip(btn) {
|
||||||
|
Tooltip.getInstance(btn).hide();
|
||||||
|
btn.removeAttribute(ATTR_TITLE_ORIGIN);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setSuccessIcon(btn) {
|
||||||
|
const icon = btn.children[0];
|
||||||
|
icon.setAttribute('class', ICON_SUCCESS);
|
||||||
|
}
|
||||||
|
|
||||||
|
function resumeIcon(btn) {
|
||||||
|
const icon = btn.children[0];
|
||||||
|
icon.setAttribute('class', ICON_DEFAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setCodeClipboard() {
|
||||||
|
const clipboardList = document.querySelectorAll(clipboardSelector);
|
||||||
|
|
||||||
|
if (clipboardList.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initial the clipboard.js object
|
||||||
|
const clipboard = new ClipboardJS(clipboardSelector, {
|
||||||
|
target: (trigger) => {
|
||||||
|
const codeBlock = trigger.parentNode.nextElementSibling;
|
||||||
|
return codeBlock.querySelector('code .rouge-code');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
[...clipboardList].map(
|
||||||
|
(elem) =>
|
||||||
|
new Tooltip(elem, {
|
||||||
|
placement: 'left'
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
clipboard.on('success', (e) => {
|
||||||
|
const trigger = e.trigger;
|
||||||
|
|
||||||
|
e.clearSelection();
|
||||||
|
|
||||||
|
if (isLocked(trigger)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setSuccessIcon(trigger);
|
||||||
|
showTooltip(trigger);
|
||||||
|
lock(trigger);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
hideTooltip(trigger);
|
||||||
|
resumeIcon(trigger);
|
||||||
|
unlock(trigger);
|
||||||
|
}, TIMEOUT);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function setLinkClipboard() {
|
||||||
|
const btnCopyLink = document.getElementById('copy-link');
|
||||||
|
|
||||||
|
if (btnCopyLink === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
btnCopyLink.addEventListener('click', (e) => {
|
||||||
|
const target = e.target;
|
||||||
|
|
||||||
|
if (isLocked(target)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy URL to clipboard
|
||||||
|
navigator.clipboard.writeText(window.location.href).then(() => {
|
||||||
|
const defaultTitle = target.getAttribute(ATTR_TITLE_ORIGIN);
|
||||||
|
const succeedTitle = target.getAttribute(ATTR_TITLE_SUCCEED);
|
||||||
|
|
||||||
|
// Switch tooltip title
|
||||||
|
target.setAttribute(ATTR_TITLE_ORIGIN, succeedTitle);
|
||||||
|
Tooltip.getInstance(target).show();
|
||||||
|
|
||||||
|
lock(target);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
target.setAttribute(ATTR_TITLE_ORIGIN, defaultTitle);
|
||||||
|
unlock(target);
|
||||||
|
}, TIMEOUT);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
btnCopyLink.addEventListener('mouseleave', (e) => {
|
||||||
|
Tooltip.getInstance(e.target).hide();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function initClipboard() {
|
||||||
|
setCodeClipboard();
|
||||||
|
setLinkClipboard();
|
||||||
|
}
|
67
assets/js/modules/components/img-loading.js
Normal file
67
assets/js/modules/components/img-loading.js
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
/**
|
||||||
|
* Setting up image lazy loading and LQIP switching
|
||||||
|
*/
|
||||||
|
|
||||||
|
const ATTR_DATA_SRC = 'data-src';
|
||||||
|
const ATTR_DATA_LQIP = 'data-lqip';
|
||||||
|
|
||||||
|
const cover = {
|
||||||
|
SHIMMER: 'shimmer',
|
||||||
|
BLUR: 'blur'
|
||||||
|
};
|
||||||
|
|
||||||
|
function removeCover(clzss) {
|
||||||
|
this.parentElement.classList.remove(clzss);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleImage() {
|
||||||
|
if (!this.complete) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.hasAttribute(ATTR_DATA_LQIP)) {
|
||||||
|
removeCover.call(this, cover.BLUR);
|
||||||
|
} else {
|
||||||
|
removeCover.call(this, cover.SHIMMER);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Switches the LQIP with the real image URL.
|
||||||
|
*/
|
||||||
|
function switchLQIP() {
|
||||||
|
const src = this.getAttribute(ATTR_DATA_SRC);
|
||||||
|
this.setAttribute('src', encodeURI(src));
|
||||||
|
this.removeAttribute(ATTR_DATA_SRC);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function loadImg() {
|
||||||
|
const images = document.querySelectorAll('article img');
|
||||||
|
|
||||||
|
if (images.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
images.forEach((img) => {
|
||||||
|
img.addEventListener('load', handleImage);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Images loaded from the browser cache do not trigger the 'load' event
|
||||||
|
document.querySelectorAll('article img[loading="lazy"]').forEach((img) => {
|
||||||
|
if (img.complete) {
|
||||||
|
removeCover.call(img, cover.SHIMMER);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// LQIPs set by the data URI or WebP will not trigger the 'load' event,
|
||||||
|
// so manually convert the URI to the URL of a high-resolution image.
|
||||||
|
const lqips = document.querySelectorAll(
|
||||||
|
`article img[${ATTR_DATA_LQIP}="true"]`
|
||||||
|
);
|
||||||
|
|
||||||
|
if (lqips.length) {
|
||||||
|
lqips.forEach((lqip) => {
|
||||||
|
switchLQIP.call(lqip);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
50
assets/js/modules/components/img-popup.js
Normal file
50
assets/js/modules/components/img-popup.js
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
/**
|
||||||
|
* Set up image popup
|
||||||
|
*
|
||||||
|
* Dependencies: https://github.com/biati-digital/glightbox
|
||||||
|
*/
|
||||||
|
|
||||||
|
const lightImages = '.popup:not(.dark)';
|
||||||
|
const darkImages = '.popup:not(.light)';
|
||||||
|
let selector = lightImages;
|
||||||
|
|
||||||
|
function updateImages(current, reverse) {
|
||||||
|
if (selector === lightImages) {
|
||||||
|
selector = darkImages;
|
||||||
|
} else {
|
||||||
|
selector = lightImages;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (reverse === null) {
|
||||||
|
reverse = GLightbox({ selector: `${selector}` });
|
||||||
|
}
|
||||||
|
|
||||||
|
[current, reverse] = [reverse, current];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function imgPopup() {
|
||||||
|
if (document.querySelector('.popup') === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasDualImages = !(
|
||||||
|
document.querySelector('.popup.light') === null &&
|
||||||
|
document.querySelector('.popup.dark') === null
|
||||||
|
);
|
||||||
|
|
||||||
|
if (Theme.visualState === Theme.DARK) {
|
||||||
|
selector = darkImages;
|
||||||
|
}
|
||||||
|
|
||||||
|
let current = GLightbox({ selector: `${selector}` });
|
||||||
|
|
||||||
|
if (hasDualImages && Theme.switchable) {
|
||||||
|
let reverse = null;
|
||||||
|
|
||||||
|
window.addEventListener('message', (event) => {
|
||||||
|
if (event.source === window && event.data && event.data.id === Theme.ID) {
|
||||||
|
updateImages(current, reverse);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
53
assets/js/modules/components/locale-datetime.js
Normal file
53
assets/js/modules/components/locale-datetime.js
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
/**
|
||||||
|
* Update month/day to locale datetime
|
||||||
|
*
|
||||||
|
* Requirement: <https://github.com/iamkun/dayjs>
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* A tool for locale datetime */
|
||||||
|
class LocaleHelper {
|
||||||
|
static get attrTimestamp() {
|
||||||
|
return 'data-ts';
|
||||||
|
}
|
||||||
|
|
||||||
|
static get attrDateFormat() {
|
||||||
|
return 'data-df';
|
||||||
|
}
|
||||||
|
|
||||||
|
static get locale() {
|
||||||
|
return document.documentElement.getAttribute('lang').substring(0, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
static getTimestamp(elem) {
|
||||||
|
return Number(elem.getAttribute(this.attrTimestamp)); // unix timestamp
|
||||||
|
}
|
||||||
|
|
||||||
|
static getDateFormat(elem) {
|
||||||
|
return elem.getAttribute(this.attrDateFormat);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function initLocaleDatetime() {
|
||||||
|
dayjs.locale(LocaleHelper.locale);
|
||||||
|
dayjs.extend(window.dayjs_plugin_localizedFormat);
|
||||||
|
|
||||||
|
document
|
||||||
|
.querySelectorAll(`[${LocaleHelper.attrTimestamp}]`)
|
||||||
|
.forEach((elem) => {
|
||||||
|
const date = dayjs.unix(LocaleHelper.getTimestamp(elem));
|
||||||
|
const text = date.format(LocaleHelper.getDateFormat(elem));
|
||||||
|
elem.textContent = text;
|
||||||
|
elem.removeAttribute(LocaleHelper.attrTimestamp);
|
||||||
|
elem.removeAttribute(LocaleHelper.attrDateFormat);
|
||||||
|
|
||||||
|
// setup tooltips
|
||||||
|
if (
|
||||||
|
elem.hasAttribute('data-bs-toggle') &&
|
||||||
|
elem.getAttribute('data-bs-toggle') === 'tooltip'
|
||||||
|
) {
|
||||||
|
// see: https://day.js.org/docs/en/display/format#list-of-localized-formats
|
||||||
|
const tooltipText = date.format('llll');
|
||||||
|
elem.setAttribute('data-bs-title', tooltipText);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
60
assets/js/modules/components/mermaid.js
Normal file
60
assets/js/modules/components/mermaid.js
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
/**
|
||||||
|
* Mermaid-js loader
|
||||||
|
*/
|
||||||
|
|
||||||
|
const MERMAID = 'mermaid';
|
||||||
|
const themeMapper = Theme.getThemeMapper('default', 'dark');
|
||||||
|
|
||||||
|
function refreshTheme(event) {
|
||||||
|
if (event.source === window && event.data && event.data.id === Theme.ID) {
|
||||||
|
// Re-render the SVG › <https://github.com/mermaid-js/mermaid/issues/311#issuecomment-332557344>
|
||||||
|
const mermaidList = document.getElementsByClassName(MERMAID);
|
||||||
|
|
||||||
|
[...mermaidList].forEach((elem) => {
|
||||||
|
const svgCode = elem.previousSibling.children.item(0).textContent;
|
||||||
|
elem.textContent = svgCode;
|
||||||
|
elem.removeAttribute('data-processed');
|
||||||
|
});
|
||||||
|
|
||||||
|
const newTheme = themeMapper[Theme.visualState];
|
||||||
|
|
||||||
|
mermaid.initialize({ theme: newTheme });
|
||||||
|
mermaid.init(null, `.${MERMAID}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setNode(elem) {
|
||||||
|
const svgCode = elem.textContent;
|
||||||
|
const backup = elem.parentElement;
|
||||||
|
backup.classList.add('d-none');
|
||||||
|
// Create mermaid node
|
||||||
|
const mermaid = document.createElement('pre');
|
||||||
|
mermaid.classList.add(MERMAID);
|
||||||
|
const text = document.createTextNode(svgCode);
|
||||||
|
mermaid.appendChild(text);
|
||||||
|
backup.after(mermaid);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function loadMermaid() {
|
||||||
|
if (
|
||||||
|
typeof mermaid === 'undefined' ||
|
||||||
|
typeof mermaid.initialize !== 'function'
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const initTheme = themeMapper[Theme.visualState];
|
||||||
|
|
||||||
|
let mermaidConf = {
|
||||||
|
theme: initTheme
|
||||||
|
};
|
||||||
|
|
||||||
|
const basicList = document.getElementsByClassName('language-mermaid');
|
||||||
|
[...basicList].forEach(setNode);
|
||||||
|
|
||||||
|
mermaid.initialize(mermaidConf);
|
||||||
|
|
||||||
|
if (Theme.switchable) {
|
||||||
|
window.addEventListener('message', refreshTheme);
|
||||||
|
}
|
||||||
|
}
|
15
assets/js/modules/components/mode-toggle.js
Normal file
15
assets/js/modules/components/mode-toggle.js
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
/**
|
||||||
|
* Add listener for theme mode toggle
|
||||||
|
*/
|
||||||
|
|
||||||
|
const $toggle = document.getElementById('mode-toggle');
|
||||||
|
|
||||||
|
export function modeWatcher() {
|
||||||
|
if (!$toggle) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$toggle.addEventListener('click', () => {
|
||||||
|
Theme.flip();
|
||||||
|
});
|
||||||
|
}
|
110
assets/js/modules/components/search-display.js
Normal file
110
assets/js/modules/components/search-display.js
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
/**
|
||||||
|
* This script make #search-result-wrapper switch to unload or shown automatically.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const btnSbTrigger = document.getElementById('sidebar-trigger');
|
||||||
|
const btnSearchTrigger = document.getElementById('search-trigger');
|
||||||
|
const btnCancel = document.getElementById('search-cancel');
|
||||||
|
const content = document.querySelectorAll('#main-wrapper>.container>.row');
|
||||||
|
const topbarTitle = document.getElementById('topbar-title');
|
||||||
|
const search = document.getElementById('search');
|
||||||
|
const resultWrapper = document.getElementById('search-result-wrapper');
|
||||||
|
const results = document.getElementById('search-results');
|
||||||
|
const input = document.getElementById('search-input');
|
||||||
|
const hints = document.getElementById('search-hints');
|
||||||
|
|
||||||
|
// CSS class names
|
||||||
|
const LOADED = 'd-block';
|
||||||
|
const UNLOADED = 'd-none';
|
||||||
|
const FOCUS = 'input-focus';
|
||||||
|
const FLEX = 'd-flex';
|
||||||
|
|
||||||
|
/* Actions in mobile screens (Sidebar hidden) */
|
||||||
|
class MobileSearchBar {
|
||||||
|
static on() {
|
||||||
|
btnSbTrigger.classList.add(UNLOADED);
|
||||||
|
topbarTitle.classList.add(UNLOADED);
|
||||||
|
btnSearchTrigger.classList.add(UNLOADED);
|
||||||
|
search.classList.add(FLEX);
|
||||||
|
btnCancel.classList.add(LOADED);
|
||||||
|
}
|
||||||
|
|
||||||
|
static off() {
|
||||||
|
btnCancel.classList.remove(LOADED);
|
||||||
|
search.classList.remove(FLEX);
|
||||||
|
btnSbTrigger.classList.remove(UNLOADED);
|
||||||
|
topbarTitle.classList.remove(UNLOADED);
|
||||||
|
btnSearchTrigger.classList.remove(UNLOADED);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ResultSwitch {
|
||||||
|
static resultVisible = false;
|
||||||
|
|
||||||
|
static on() {
|
||||||
|
if (!this.resultVisible) {
|
||||||
|
resultWrapper.classList.remove(UNLOADED);
|
||||||
|
content.forEach((el) => {
|
||||||
|
el.classList.add(UNLOADED);
|
||||||
|
});
|
||||||
|
this.resultVisible = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static off() {
|
||||||
|
if (this.resultVisible) {
|
||||||
|
results.innerHTML = '';
|
||||||
|
|
||||||
|
if (hints.classList.contains(UNLOADED)) {
|
||||||
|
hints.classList.remove(UNLOADED);
|
||||||
|
}
|
||||||
|
|
||||||
|
resultWrapper.classList.add(UNLOADED);
|
||||||
|
content.forEach((el) => {
|
||||||
|
el.classList.remove(UNLOADED);
|
||||||
|
});
|
||||||
|
input.textContent = '';
|
||||||
|
this.resultVisible = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isMobileView() {
|
||||||
|
return btnCancel.classList.contains(LOADED);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function displaySearch() {
|
||||||
|
btnSearchTrigger.addEventListener('click', () => {
|
||||||
|
MobileSearchBar.on();
|
||||||
|
ResultSwitch.on();
|
||||||
|
input.focus();
|
||||||
|
});
|
||||||
|
|
||||||
|
btnCancel.addEventListener('click', () => {
|
||||||
|
MobileSearchBar.off();
|
||||||
|
ResultSwitch.off();
|
||||||
|
});
|
||||||
|
|
||||||
|
input.addEventListener('focus', () => {
|
||||||
|
search.classList.add(FOCUS);
|
||||||
|
});
|
||||||
|
|
||||||
|
input.addEventListener('focusout', () => {
|
||||||
|
search.classList.remove(FOCUS);
|
||||||
|
});
|
||||||
|
|
||||||
|
input.addEventListener('input', () => {
|
||||||
|
if (input.value === '') {
|
||||||
|
if (isMobileView()) {
|
||||||
|
hints.classList.remove(UNLOADED);
|
||||||
|
} else {
|
||||||
|
ResultSwitch.off();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ResultSwitch.on();
|
||||||
|
if (isMobileView()) {
|
||||||
|
hints.classList.add(UNLOADED);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
36
assets/js/modules/components/toc.js
Normal file
36
assets/js/modules/components/toc.js
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import { TocMobile as mobile } from './toc/toc-mobile';
|
||||||
|
import { TocDesktop as desktop } from './toc/toc-desktop';
|
||||||
|
|
||||||
|
const desktopMode = matchMedia('(min-width: 1200px)');
|
||||||
|
|
||||||
|
function refresh(e) {
|
||||||
|
if (e.matches) {
|
||||||
|
if (mobile.popupOpened) {
|
||||||
|
mobile.hidePopup();
|
||||||
|
}
|
||||||
|
|
||||||
|
desktop.refresh();
|
||||||
|
} else {
|
||||||
|
mobile.refresh();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
if (document.querySelector('main>article[data-toc="true"]') === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Avoid create multiple instances of Tocbot. Ref: <https://github.com/tscanlin/tocbot/issues/203>
|
||||||
|
if (desktopMode.matches) {
|
||||||
|
desktop.init();
|
||||||
|
} else {
|
||||||
|
mobile.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
const $tocWrapper = document.getElementById('toc-wrapper');
|
||||||
|
$tocWrapper.classList.remove('invisible');
|
||||||
|
|
||||||
|
desktopMode.onchange = refresh;
|
||||||
|
}
|
||||||
|
|
||||||
|
export { init as initToc };
|
20
assets/js/modules/components/toc/toc-desktop.js
Normal file
20
assets/js/modules/components/toc/toc-desktop.js
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
export class TocDesktop {
|
||||||
|
/* Tocbot options Ref: https://github.com/tscanlin/tocbot#usage */
|
||||||
|
static options = {
|
||||||
|
tocSelector: '#toc',
|
||||||
|
contentSelector: '.content',
|
||||||
|
ignoreSelector: '[data-toc-skip]',
|
||||||
|
headingSelector: 'h2, h3, h4',
|
||||||
|
orderedList: false,
|
||||||
|
scrollSmooth: false,
|
||||||
|
headingsOffset: 16 * 2 // 2rem
|
||||||
|
};
|
||||||
|
|
||||||
|
static refresh() {
|
||||||
|
tocbot.refresh(this.options);
|
||||||
|
}
|
||||||
|
|
||||||
|
static init() {
|
||||||
|
tocbot.init(this.options);
|
||||||
|
}
|
||||||
|
}
|
125
assets/js/modules/components/toc/toc-mobile.js
Normal file
125
assets/js/modules/components/toc/toc-mobile.js
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
/**
|
||||||
|
* TOC button, topbar and popup for mobile devices
|
||||||
|
*/
|
||||||
|
|
||||||
|
const $tocBar = document.getElementById('toc-bar');
|
||||||
|
const $soloTrigger = document.getElementById('toc-solo-trigger');
|
||||||
|
const $triggers = document.getElementsByClassName('toc-trigger');
|
||||||
|
const $popup = document.getElementById('toc-popup');
|
||||||
|
const $btnClose = document.getElementById('toc-popup-close');
|
||||||
|
|
||||||
|
const SCROLL_LOCK = 'overflow-hidden';
|
||||||
|
const CLOSING = 'closing';
|
||||||
|
|
||||||
|
export class TocMobile {
|
||||||
|
static #invisible = true;
|
||||||
|
static #barHeight = 16 * 3; // 3rem
|
||||||
|
|
||||||
|
static options = {
|
||||||
|
tocSelector: '#toc-popup-content',
|
||||||
|
contentSelector: '.content',
|
||||||
|
ignoreSelector: '[data-toc-skip]',
|
||||||
|
headingSelector: 'h2, h3, h4',
|
||||||
|
orderedList: false,
|
||||||
|
scrollSmooth: false,
|
||||||
|
collapseDepth: 4,
|
||||||
|
headingsOffset: this.#barHeight
|
||||||
|
};
|
||||||
|
|
||||||
|
static initBar() {
|
||||||
|
const observer = new IntersectionObserver(
|
||||||
|
(entries) => {
|
||||||
|
entries.forEach((entry) => {
|
||||||
|
$tocBar.classList.toggle('invisible', entry.isIntersecting);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
{ rootMargin: `-${this.#barHeight}px 0px 0px 0px` }
|
||||||
|
);
|
||||||
|
|
||||||
|
observer.observe($soloTrigger);
|
||||||
|
this.#invisible = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static listenAnchors() {
|
||||||
|
const $anchors = document.getElementsByClassName('toc-link');
|
||||||
|
[...$anchors].forEach((anchor) => {
|
||||||
|
anchor.onclick = () => this.hidePopup();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static refresh() {
|
||||||
|
if (this.#invisible) {
|
||||||
|
this.initComponents();
|
||||||
|
}
|
||||||
|
tocbot.refresh(this.options);
|
||||||
|
this.listenAnchors();
|
||||||
|
}
|
||||||
|
|
||||||
|
static get popupOpened() {
|
||||||
|
return $popup.open;
|
||||||
|
}
|
||||||
|
|
||||||
|
static showPopup() {
|
||||||
|
this.lockScroll(true);
|
||||||
|
$popup.showModal();
|
||||||
|
const activeItem = $popup.querySelector('li.is-active-li');
|
||||||
|
activeItem.scrollIntoView({ block: 'center' });
|
||||||
|
}
|
||||||
|
|
||||||
|
static hidePopup() {
|
||||||
|
$popup.toggleAttribute(CLOSING);
|
||||||
|
|
||||||
|
$popup.addEventListener(
|
||||||
|
'animationend',
|
||||||
|
() => {
|
||||||
|
$popup.toggleAttribute(CLOSING);
|
||||||
|
$popup.close();
|
||||||
|
},
|
||||||
|
{ once: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
this.lockScroll(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
static lockScroll(enable) {
|
||||||
|
document.documentElement.classList.toggle(SCROLL_LOCK, enable);
|
||||||
|
document.body.classList.toggle(SCROLL_LOCK, enable);
|
||||||
|
}
|
||||||
|
|
||||||
|
static clickBackdrop(event) {
|
||||||
|
if ($popup.hasAttribute(CLOSING)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rect = event.target.getBoundingClientRect();
|
||||||
|
if (
|
||||||
|
event.clientX < rect.left ||
|
||||||
|
event.clientX > rect.right ||
|
||||||
|
event.clientY < rect.top ||
|
||||||
|
event.clientY > rect.bottom
|
||||||
|
) {
|
||||||
|
this.hidePopup();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static initComponents() {
|
||||||
|
this.initBar();
|
||||||
|
|
||||||
|
[...$triggers].forEach((trigger) => {
|
||||||
|
trigger.onclick = () => this.showPopup();
|
||||||
|
});
|
||||||
|
|
||||||
|
$popup.onclick = (e) => this.clickBackdrop(e);
|
||||||
|
$btnClose.onclick = () => this.hidePopup();
|
||||||
|
$popup.oncancel = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
this.hidePopup();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static init() {
|
||||||
|
tocbot.init(this.options);
|
||||||
|
this.listenAnchors();
|
||||||
|
this.initComponents();
|
||||||
|
}
|
||||||
|
}
|
11
assets/js/modules/components/tooltip-loader.js
Normal file
11
assets/js/modules/components/tooltip-loader.js
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import Tooltip from 'bootstrap/js/src/tooltip';
|
||||||
|
|
||||||
|
export function loadTooptip() {
|
||||||
|
const tooltipTriggerList = document.querySelectorAll(
|
||||||
|
'[data-bs-toggle="tooltip"]'
|
||||||
|
);
|
||||||
|
|
||||||
|
[...tooltipTriggerList].map(
|
||||||
|
(tooltipTriggerEl) => new Tooltip(tooltipTriggerEl)
|
||||||
|
);
|
||||||
|
}
|
3
assets/js/modules/layouts.js
Normal file
3
assets/js/modules/layouts.js
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export { basic } from './layouts/basic';
|
||||||
|
export { initSidebar } from './layouts/sidebar';
|
||||||
|
export { initTopbar } from './layouts/topbar';
|
7
assets/js/modules/layouts/basic.js
Normal file
7
assets/js/modules/layouts/basic.js
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { back2top, loadTooptip, modeWatcher } from '../components';
|
||||||
|
|
||||||
|
export function basic() {
|
||||||
|
modeWatcher();
|
||||||
|
back2top();
|
||||||
|
loadTooptip();
|
||||||
|
}
|
19
assets/js/modules/layouts/sidebar.js
Normal file
19
assets/js/modules/layouts/sidebar.js
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
const ATTR_DISPLAY = 'sidebar-display';
|
||||||
|
const $sidebar = document.getElementById('sidebar');
|
||||||
|
const $trigger = document.getElementById('sidebar-trigger');
|
||||||
|
const $mask = document.getElementById('mask');
|
||||||
|
|
||||||
|
class SidebarUtil {
|
||||||
|
static #isExpanded = false;
|
||||||
|
|
||||||
|
static toggle() {
|
||||||
|
this.#isExpanded = !this.#isExpanded;
|
||||||
|
document.body.toggleAttribute(ATTR_DISPLAY, this.#isExpanded);
|
||||||
|
$sidebar.classList.toggle('z-2', this.#isExpanded);
|
||||||
|
$mask.classList.toggle('d-none', !this.#isExpanded);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function initSidebar() {
|
||||||
|
$trigger.onclick = $mask.onclick = () => SidebarUtil.toggle();
|
||||||
|
}
|
5
assets/js/modules/layouts/topbar.js
Normal file
5
assets/js/modules/layouts/topbar.js
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import { displaySearch } from '../components/search-display';
|
||||||
|
|
||||||
|
export function initTopbar() {
|
||||||
|
displaySearch();
|
||||||
|
}
|
138
assets/js/modules/theme.js
Normal file
138
assets/js/modules/theme.js
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
/**
|
||||||
|
* Theme management class
|
||||||
|
*
|
||||||
|
* To reduce flickering during page load, this script should be loaded synchronously.
|
||||||
|
*/
|
||||||
|
class Theme {
|
||||||
|
static #modeKey = 'mode';
|
||||||
|
static #modeAttr = 'data-mode';
|
||||||
|
static #darkMedia = window.matchMedia('(prefers-color-scheme: dark)');
|
||||||
|
static switchable = !document.documentElement.hasAttribute(this.#modeAttr);
|
||||||
|
|
||||||
|
static get DARK() {
|
||||||
|
return 'dark';
|
||||||
|
}
|
||||||
|
|
||||||
|
static get LIGHT() {
|
||||||
|
return 'light';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {string} Theme mode identifier
|
||||||
|
*/
|
||||||
|
static get ID() {
|
||||||
|
return 'theme-mode';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the current visual state of the theme.
|
||||||
|
*
|
||||||
|
* @returns {string} The current visual state, either the mode if it exists,
|
||||||
|
* or the system dark mode state ('dark' or 'light').
|
||||||
|
*/
|
||||||
|
static get visualState() {
|
||||||
|
if (this.#hasMode) {
|
||||||
|
return this.#mode;
|
||||||
|
} else {
|
||||||
|
return this.#sysDark ? this.DARK : this.LIGHT;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static get #mode() {
|
||||||
|
return (
|
||||||
|
sessionStorage.getItem(this.#modeKey) ||
|
||||||
|
document.documentElement.getAttribute(this.#modeAttr)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static get #isDarkMode() {
|
||||||
|
return this.#mode === this.DARK;
|
||||||
|
}
|
||||||
|
|
||||||
|
static get #hasMode() {
|
||||||
|
return this.#mode !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static get #sysDark() {
|
||||||
|
return this.#darkMedia.matches;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps theme modes to provided values
|
||||||
|
* @param {string} light Value for light mode
|
||||||
|
* @param {string} dark Value for dark mode
|
||||||
|
* @returns {Object} Mapped values
|
||||||
|
*/
|
||||||
|
static getThemeMapper(light, dark) {
|
||||||
|
return {
|
||||||
|
[this.LIGHT]: light,
|
||||||
|
[this.DARK]: dark
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the theme based on system preferences or stored mode
|
||||||
|
*/
|
||||||
|
static init() {
|
||||||
|
if (!this.switchable) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.#darkMedia.addEventListener('change', () => {
|
||||||
|
const lastMode = this.#mode;
|
||||||
|
this.#clearMode();
|
||||||
|
|
||||||
|
if (lastMode !== this.visualState) {
|
||||||
|
this.#notify();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!this.#hasMode) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.#isDarkMode) {
|
||||||
|
this.#setDark();
|
||||||
|
} else {
|
||||||
|
this.#setLight();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flips the current theme mode
|
||||||
|
*/
|
||||||
|
static flip() {
|
||||||
|
if (this.#hasMode) {
|
||||||
|
this.#clearMode();
|
||||||
|
} else {
|
||||||
|
this.#sysDark ? this.#setLight() : this.#setDark();
|
||||||
|
}
|
||||||
|
this.#notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
static #setDark() {
|
||||||
|
document.documentElement.setAttribute(this.#modeAttr, this.DARK);
|
||||||
|
sessionStorage.setItem(this.#modeKey, this.DARK);
|
||||||
|
}
|
||||||
|
|
||||||
|
static #setLight() {
|
||||||
|
document.documentElement.setAttribute(this.#modeAttr, this.LIGHT);
|
||||||
|
sessionStorage.setItem(this.#modeKey, this.LIGHT);
|
||||||
|
}
|
||||||
|
|
||||||
|
static #clearMode() {
|
||||||
|
document.documentElement.removeAttribute(this.#modeAttr);
|
||||||
|
sessionStorage.removeItem(this.#modeKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notifies other plugins that the theme mode has changed
|
||||||
|
*/
|
||||||
|
static #notify() {
|
||||||
|
window.postMessage({ id: this.ID }, '*');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Theme.init();
|
||||||
|
|
||||||
|
export default Theme;
|
15
assets/js/page.js
Normal file
15
assets/js/page.js
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { basic, initSidebar, initTopbar } from './modules/layouts';
|
||||||
|
import {
|
||||||
|
loadImg,
|
||||||
|
imgPopup,
|
||||||
|
initClipboard,
|
||||||
|
loadMermaid
|
||||||
|
} from './modules/components';
|
||||||
|
|
||||||
|
loadImg();
|
||||||
|
imgPopup();
|
||||||
|
initSidebar();
|
||||||
|
initTopbar();
|
||||||
|
initClipboard();
|
||||||
|
loadMermaid();
|
||||||
|
basic();
|
20
assets/js/post.js
Normal file
20
assets/js/post.js
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { basic, initTopbar, initSidebar } from './modules/layouts';
|
||||||
|
|
||||||
|
import {
|
||||||
|
loadImg,
|
||||||
|
imgPopup,
|
||||||
|
initLocaleDatetime,
|
||||||
|
initClipboard,
|
||||||
|
initToc,
|
||||||
|
loadMermaid
|
||||||
|
} from './modules/components';
|
||||||
|
|
||||||
|
loadImg();
|
||||||
|
initToc();
|
||||||
|
imgPopup();
|
||||||
|
initSidebar();
|
||||||
|
initLocaleDatetime();
|
||||||
|
initClipboard();
|
||||||
|
initTopbar();
|
||||||
|
loadMermaid();
|
||||||
|
basic();
|
55
assets/js/pwa/app.js
Normal file
55
assets/js/pwa/app.js
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
import Toast from 'bootstrap/js/src/toast';
|
||||||
|
|
||||||
|
if ('serviceWorker' in navigator) {
|
||||||
|
// Get Jekyll config from URL parameters
|
||||||
|
const src = new URL(document.currentScript.src);
|
||||||
|
const register = src.searchParams.get('register');
|
||||||
|
const baseUrl = src.searchParams.get('baseurl');
|
||||||
|
|
||||||
|
if (register) {
|
||||||
|
const swUrl = `${baseUrl}/sw.min.js`;
|
||||||
|
const notification = document.getElementById('notification');
|
||||||
|
const btnRefresh = notification.querySelector('.toast-body>button');
|
||||||
|
const popupWindow = Toast.getOrCreateInstance(notification);
|
||||||
|
|
||||||
|
navigator.serviceWorker.register(swUrl).then((registration) => {
|
||||||
|
// Restore the update window that was last manually closed by the user
|
||||||
|
if (registration.waiting) {
|
||||||
|
popupWindow.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
registration.addEventListener('updatefound', () => {
|
||||||
|
registration.installing.addEventListener('statechange', () => {
|
||||||
|
if (registration.waiting) {
|
||||||
|
if (navigator.serviceWorker.controller) {
|
||||||
|
popupWindow.show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
btnRefresh.addEventListener('click', () => {
|
||||||
|
if (registration.waiting) {
|
||||||
|
registration.waiting.postMessage('SKIP_WAITING');
|
||||||
|
}
|
||||||
|
popupWindow.hide();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
let refreshing = false;
|
||||||
|
|
||||||
|
// Detect controller change and refresh all the opened tabs
|
||||||
|
navigator.serviceWorker.addEventListener('controllerchange', () => {
|
||||||
|
if (!refreshing) {
|
||||||
|
window.location.reload();
|
||||||
|
refreshing = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
navigator.serviceWorker.getRegistrations().then(function (registrations) {
|
||||||
|
for (let registration of registrations) {
|
||||||
|
registration.unregister();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
92
assets/js/pwa/sw.js
Normal file
92
assets/js/pwa/sw.js
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
importScripts('./assets/js/data/swconf.js');
|
||||||
|
|
||||||
|
const purge = swconf.purge;
|
||||||
|
const interceptor = swconf.interceptor;
|
||||||
|
|
||||||
|
function verifyUrl(url) {
|
||||||
|
const requestUrl = new URL(url);
|
||||||
|
const requestPath = requestUrl.pathname;
|
||||||
|
|
||||||
|
if (!requestUrl.protocol.startsWith('http')) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const prefix of interceptor.urlPrefixes) {
|
||||||
|
if (requestUrl.href.startsWith(prefix)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const path of interceptor.paths) {
|
||||||
|
if (requestPath.startsWith(path)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.addEventListener('install', (event) => {
|
||||||
|
if (purge) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
event.waitUntil(
|
||||||
|
caches.open(swconf.cacheName).then((cache) => {
|
||||||
|
return cache.addAll(swconf.resources);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
self.addEventListener('activate', (event) => {
|
||||||
|
event.waitUntil(
|
||||||
|
caches.keys().then((keyList) => {
|
||||||
|
return Promise.all(
|
||||||
|
keyList.map((key) => {
|
||||||
|
if (purge) {
|
||||||
|
return caches.delete(key);
|
||||||
|
} else {
|
||||||
|
if (key !== swconf.cacheName) {
|
||||||
|
return caches.delete(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
self.addEventListener('message', (event) => {
|
||||||
|
if (event.data === 'SKIP_WAITING') {
|
||||||
|
self.skipWaiting();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
self.addEventListener('fetch', (event) => {
|
||||||
|
if (event.request.headers.has('range')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
event.respondWith(
|
||||||
|
caches.match(event.request).then((response) => {
|
||||||
|
if (response) {
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
return fetch(event.request).then((response) => {
|
||||||
|
const url = event.request.url;
|
||||||
|
|
||||||
|
if (purge || event.request.method !== 'GET' || !verifyUrl(url)) {
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
// See: <https://developers.google.com/web/fundamentals/primers/service-workers#cache_and_return_requests>
|
||||||
|
let responseToCache = response.clone();
|
||||||
|
|
||||||
|
caches.open(swconf.cacheName).then((cache) => {
|
||||||
|
cache.put(event.request, responseToCache);
|
||||||
|
});
|
||||||
|
return response;
|
||||||
|
});
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
@ -8,6 +8,9 @@
|
|||||||
{{ $sass := resources.Get "scss/main.bundle.scss" }}
|
{{ $sass := resources.Get "scss/main.bundle.scss" }}
|
||||||
{{ $style := $sass | toCSS $opts | minify | fingerprint }}
|
{{ $style := $sass | toCSS $opts | minify | fingerprint }}
|
||||||
<link rel="stylesheet" href="{{ $style.Permalink }}">
|
<link rel="stylesheet" href="{{ $style.Permalink }}">
|
||||||
|
{{ $jsFiles := resources.Match "js/**/*.js" }}
|
||||||
|
{{ $jsBundle := $jsFiles | resources.Concat "js/bundle.js" | minify | fingerprint }}
|
||||||
|
<script src="{{ $jsBundle.RelPermalink }}" integrity="{{ $jsBundle.Data.Integrity }}" type="module" defer></script>
|
||||||
</head>
|
</head>
|
||||||
<html>
|
<html>
|
||||||
<body>
|
<body>
|
||||||
|
104
layouts/partials/head.html
Normal file
104
layouts/partials/head.html
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
<head>
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||||
|
<meta name="theme-color" media="(prefers-color-scheme: light)" content="#f7f7f7">
|
||||||
|
<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#1b1b1e">
|
||||||
|
<meta name="mobile-web-app-capable" content="yes">
|
||||||
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||||||
|
<meta
|
||||||
|
name="viewport"
|
||||||
|
content="width=device-width, user-scalable=no initial-scale=1, shrink-to-fit=no, viewport-fit=cover"
|
||||||
|
>
|
||||||
|
|
||||||
|
{{ template "_internal/opengraph.html" }}
|
||||||
|
{{ template "_internal/twitter_cards.html" }}
|
||||||
|
{{ template "_internal/schema.html" }}
|
||||||
|
|
||||||
|
<!-- Setup Open Graph image -->
|
||||||
|
{{ if .Params.image }}
|
||||||
|
{{ $src := .Params.image }}
|
||||||
|
{{ $imgUrl := "" }}
|
||||||
|
|
||||||
|
{{ if not (findRE "://" $src) }}
|
||||||
|
{{ $imgUrl = absURL $src }}
|
||||||
|
{{ $oldUrl := $src | absURL }}
|
||||||
|
{{ $newUrl := $imgUrl }}
|
||||||
|
<!-- Hugo doesn't have direct string replacement in templates,
|
||||||
|
so we'd need to handle this differently or use a custom function -->
|
||||||
|
{{ end }}
|
||||||
|
{{ else if site.Params.social_preview_image }}
|
||||||
|
{{ $imgUrl := site.Params.social_preview_image | absURL }}
|
||||||
|
<meta property="og:image" content="{{ $imgUrl }}" />
|
||||||
|
<meta name="twitter:card" content="summary_large_image" />
|
||||||
|
<meta property="twitter:image" content="{{ $imgUrl }}" />
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
<title>
|
||||||
|
{{ if ne .Kind "home" }}
|
||||||
|
{{ .Title | safeHTML }} |
|
||||||
|
{{ end }}
|
||||||
|
{{ site.Title }}
|
||||||
|
</title>
|
||||||
|
|
||||||
|
{{ partial "favicons.html" . }}
|
||||||
|
|
||||||
|
<!-- Resource Hints -->
|
||||||
|
{{ if not site.Params.assets.self_host.enabled }}
|
||||||
|
{{ range site.Data.origin.cors.resource_hints }}
|
||||||
|
{{ range .links }}
|
||||||
|
<link rel="{{ .rel }}" href="{{ .url }}" {{ delimit .opts " " | safeHTMLAttr }}>
|
||||||
|
{{ end }}
|
||||||
|
{{ end }}
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
<!-- Bootstrap -->
|
||||||
|
{{ if not hugo.IsProduction }}
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css">
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
<!-- Theme style -->
|
||||||
|
<link rel="stylesheet" href="{{ (printf "/assets/css/%s.css" site.Params.theme) | relURL }}">
|
||||||
|
|
||||||
|
<!-- Web Font -->
|
||||||
|
<link rel="stylesheet" href="{{ site.Data.origin.webfonts | relURL }}">
|
||||||
|
|
||||||
|
<!-- Font Awesome Icons -->
|
||||||
|
<link rel="stylesheet" href="{{ site.Data.origin.fontawesome.css | relURL }}">
|
||||||
|
|
||||||
|
<!-- 3rd-party Dependencies -->
|
||||||
|
{{ if and site.Params.toc .Params.toc }}
|
||||||
|
<link rel="stylesheet" href="{{ site.Data.origin.toc.css | relURL }}">
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
{{ if or (eq .Type "post") (eq .Type "page") (eq .Kind "home") }}
|
||||||
|
<link rel="stylesheet" href="{{ site.Data.origin.lazy_polyfill.css | relURL }}">
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
{{ if or (eq .Type "page") (eq .Type "post") }}
|
||||||
|
<!-- Image Popup -->
|
||||||
|
<link rel="stylesheet" href="{{ site.Data.origin.glightbox.css | relURL }}">
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
<!-- Scripts -->
|
||||||
|
<script src="{{ "/assets/js/dist/theme.min.js" | relURL }}"></script>
|
||||||
|
|
||||||
|
{{ partial "js-selector.html" . }}
|
||||||
|
|
||||||
|
{{ if hugo.IsProduction }}
|
||||||
|
<!-- PWA -->
|
||||||
|
{{ if site.Params.pwa.enabled }}
|
||||||
|
<script
|
||||||
|
defer
|
||||||
|
src="{{ "/app.min.js" | relURL }}?baseurl={{ site.BaseURL }}®ister={{ site.Params.pwa.cache.enabled }}"
|
||||||
|
></script>
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
<!-- Web Analytics -->
|
||||||
|
{{ range $platform, $config := site.Params.analytics }}
|
||||||
|
{{ if and $config.id (ne $config.id "") }}
|
||||||
|
{{ partial (printf "analytics/%s.html" $platform) . }}
|
||||||
|
{{ end }}
|
||||||
|
{{ end }}
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
{{ partial "metadata-hook.html" . }}
|
||||||
|
</head>
|
Reference in New Issue
Block a user