fix: update layout and import Chirpy template files

This commit is contained in:
Rubens Jean Simon
2024-08-13 04:48:24 +00:00
parent b0c003ba2e
commit b9caf8eedb
128 changed files with 8636 additions and 34 deletions

View 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 });
});
}

View 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');
}
});
});
}

View 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();
}

View 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);
});
}
}

View File

@ -0,0 +1,48 @@
/**
* Set up image popup
*
* Dependencies: https://github.com/biati-digital/glightbox
*/
const html = document.documentElement;
const lightImages = '.popup:not(.dark)';
const darkImages = '.popup:not(.light)';
let selector = lightImages;
if (
(html.hasAttribute('data-mode') &&
html.getAttribute('data-mode') === 'dark') ||
(!html.hasAttribute('data-mode') &&
window.matchMedia('(prefers-color-scheme: dark)').matches)
) {
selector = darkImages;
}
let lightbox = GLightbox({ selector: `${selector}` });
function updateImages(event) {
if (
event.source === window &&
event.data &&
event.data.direction === ModeToggle.ID
) {
if (selector === lightImages) {
selector = darkImages;
} else {
selector = lightImages;
}
}
lightbox.destroy();
lightbox = GLightbox({ selector: `${selector}` });
}
export function imgPopup() {
if (document.querySelector(`${selector}`) === null) {
return;
}
if (document.getElementById('mode-toggle')) {
window.addEventListener('message', updateImages);
}
}

View 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);
}
});
}

View File

@ -0,0 +1,14 @@
/**
* Add listener for theme mode toggle
*/
const toggle = document.getElementById('mode-toggle');
export function modeWatcher() {
if (!toggle) {
return;
}
toggle.addEventListener('click', () => {
modeToggle.flipMode();
});
}

View 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);
}
}
});
}

View File

@ -0,0 +1,27 @@
/**
* Expand or close the sidebar in mobile screens.
*/
const ATTR_DISPLAY = 'sidebar-display';
class SidebarUtil {
static isExpanded = false;
static toggle() {
if (SidebarUtil.isExpanded === false) {
document.body.setAttribute(ATTR_DISPLAY, '');
} else {
document.body.removeAttribute(ATTR_DISPLAY);
}
SidebarUtil.isExpanded = !SidebarUtil.isExpanded;
}
}
export function sidebarExpand() {
document
.getElementById('sidebar-trigger')
.addEventListener('click', SidebarUtil.toggle);
document.getElementById('mask').addEventListener('click', SidebarUtil.toggle);
}

View File

@ -0,0 +1,15 @@
export function toc() {
if (document.querySelector('main h2, main h3')) {
// see: https://github.com/tscanlin/tocbot#usage
tocbot.init({
tocSelector: '#toc',
contentSelector: '.content',
ignoreSelector: '[data-toc-skip]',
headingSelector: 'h2, h3, h4',
orderedList: false,
scrollSmooth: false
});
document.getElementById('toc-wrapper').classList.remove('d-none');
}
}

View 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)
);
}