refactor(utils): migrate utility functions into common module and rewrite method calls

This commit is contained in:
Cell
2026-03-24 11:36:28 +08:00
parent a53ef80035
commit 9132b408c6
4 changed files with 219 additions and 210 deletions
+77 -63
View File
@@ -1,12 +1,26 @@
// TODO use ESLint to check the code style
import Util from './util';
import {
forEach,
getScrollTop,
isMobile,
isTocStatic,
animateCSS,
isValidDate,
scrollIntoView,
getStagingDOM,
createCopyText,
isObjectLiteral,
HTMLEscape,
} from './utils/common';
import FileTree from './lib/file-tree.js'
const copyText = createCopyText();
class FixIt {
constructor() {
this.config = window.config;
this.isDark = document.documentElement.dataset.theme === 'dark';
this.newScrollTop = Util.getScrollTop();
this.newScrollTop = getScrollTop();
this.oldScrollTop = this.newScrollTop;
this.scrollEventSet = new Set();
this.resizeEventSet = new Set();
@@ -29,7 +43,7 @@ class FixIt {
}
initSVGIcon() {
Util.forEach(document.querySelectorAll('[data-svg-src]'), ($icon) => {
forEach(document.querySelectorAll('[data-svg-src]'), ($icon) => {
fetch($icon.dataset.svgSrc)
.then((response) => response.text())
.then((svg) => {
@@ -58,7 +72,7 @@ class FixIt {
}
initMenuDesktop() {
Util.forEach(document.querySelectorAll('.has-children'), ($item) => {
forEach(document.querySelectorAll('.has-children'), ($item) => {
$item.querySelector('.sub-menu').style.minWidth = `${$item.offsetWidth - 8}px`;
});
}
@@ -78,7 +92,7 @@ class FixIt {
});
this.clickMaskEventSet.add(this._menuMobileOnClickMask);
// add nested menu toggler
Util.forEach(document.querySelectorAll('.menu-item>.nested-item'), ($nestedItem) => {
forEach(document.querySelectorAll('.menu-item>.nested-item'), ($nestedItem) => {
$nestedItem.addEventListener('click', function () {
this.parentNode.querySelector('.sub-menu').classList.toggle('open');
this.querySelector('.dropdown-icon').classList.toggle('open');
@@ -87,7 +101,7 @@ class FixIt {
}
initSwitchTheme() {
Util.forEach(document.getElementsByClassName('theme-switch'), ($themeSwitch) => {
forEach(document.getElementsByClassName('theme-switch'), ($themeSwitch) => {
$themeSwitch.addEventListener('click', () => {
document.documentElement.dataset.theme = this.isDark ? 'light' : 'dark';
document.documentElement.style.setProperty('color-scheme', this.isDark ? 'light' : 'dark');
@@ -134,11 +148,11 @@ class FixIt {
initSearch() {
const searchConfig = this.config.search;
const isMobile = Util.isMobile();
const _isMobile = isMobile();
if (
!searchConfig ||
(isMobile && this._searchMobileOnce) ||
(!isMobile && this._searchDesktopOnce)
(_isMobile && this._searchMobileOnce) ||
(!_isMobile && this._searchDesktopOnce)
)
return;
// Initialize default search config
@@ -154,7 +168,7 @@ class FixIt {
const ignoreLocation = searchConfig.ignoreLocation ?? false;
const useExtendedSearch = searchConfig.useExtendedSearch ?? false;
const ignoreFieldNorm = searchConfig.ignoreFieldNorm ?? false;
const suffix = isMobile ? 'mobile' : 'desktop';
const suffix = _isMobile ? 'mobile' : 'desktop';
const $header = document.getElementById(`header-${suffix}`);
const $searchInput = document.getElementById(`search-input-${suffix}`);
const $searchToggle = document.getElementById(`search-toggle-${suffix}`);
@@ -164,7 +178,7 @@ class FixIt {
// goto the PostChat panel rather than search results
if (searchConfig.type === 'post-chat' && window.postChatUser) {
if (isMobile) {
if (_isMobile) {
$searchInput.addEventListener('focus', () => {
window.postChatUser.setSearchInput('');
}, false);
@@ -176,7 +190,7 @@ class FixIt {
return;
}
if (isMobile) {
if (_isMobile) {
this._searchMobileOnce = true;
$searchInput.addEventListener('focus', () => {
this.disableScrollEvent = true;
@@ -341,7 +355,7 @@ class FixIt {
templates: {
suggestion: ({ title, uri, date, context }) =>
`<div><a href="${uri}"><span class="suggestion-title">${title}</span></a><span class="suggestion-date">${date}</span></div><div class="suggestion-context">${context}</div>`,
empty: ({ query }) => `<div class="search-empty">${searchConfig.noResultsFound}: <span class="search-query">"${Util.HTMLEscape(query)}"</span></div>`,
empty: ({ query }) => `<div class="search-empty">${searchConfig.noResultsFound}: <span class="search-query">"${HTMLEscape(query)}"</span></div>`,
footer: ({ }) => {
let searchType, icon, href;
switch (searchConfig.type) {
@@ -376,7 +390,7 @@ class FixIt {
document.getElementById('mask')?.click();
window.location.assign(suggestion.uri);
});
if (isMobile) {
if (_isMobile) {
this._searchMobile = autosearch;
} else {
this._searchDesktop = autosearch;
@@ -394,7 +408,7 @@ class FixIt {
}
initDetails(target = document) {
Util.forEach(target.querySelectorAll('.details:not(.disabled)'), ($details) => {
forEach(target.querySelectorAll('.details:not(.disabled)'), ($details) => {
const $summary = $details.querySelector('.details-summary');
$summary.addEventListener('click', () => {
$details.classList.toggle('open');
@@ -436,11 +450,11 @@ class FixIt {
const iswWrap = codeBlock.classList.contains('line-wrapping');
const highlightLines = codeBlock.querySelectorAll('.hl');
iswWrap && codeBlock.classList.toggle('line-wrapping');
Util.forEach(highlightLines, $hl => $hl.classList.toggle('hl'));
Util.copyText(codePreEl.innerText.trim()).then(() => {
Util.animateCSS(codePreEl, 'animate__flash');
forEach(highlightLines, $hl => $hl.classList.toggle('hl'));
copyText(codePreEl.innerText.trim()).then(() => {
animateCSS(codePreEl, 'animate__flash');
iswWrap && codeBlock.classList.toggle('line-wrapping');
Util.forEach(highlightLines, $hl => $hl.classList.toggle('hl'));
forEach(highlightLines, $hl => $hl.classList.toggle('hl'));
const copiedText = copyBtn.dataset.copiedText;
const originalTitle = copyBtn.dataset.ctOriginalTitle;
copyBtn.toggleAttribute('data-copied', true);
@@ -551,7 +565,7 @@ class FixIt {
*/
initCodeWrapper() {
const $codeBlocks = document.querySelectorAll('.code-block.highlight:not([data-init])');
Util.forEach($codeBlocks, ($codeBlock) => {
forEach($codeBlocks, ($codeBlock) => {
const $preElements = $codeBlock.querySelectorAll('pre.chroma');
if (!$preElements.length) return;
const $codePreEl = $preElements[$preElements.length - 1];
@@ -591,7 +605,7 @@ class FixIt {
$codePreEl.setAttribute('contenteditable', false);
$codePreEl.blur();
} else {
Util.forEach($codeBlock.querySelectorAll('.hl'), ($hl) => {
forEach($codeBlock.querySelectorAll('.hl'), ($hl) => {
$hl.classList.remove('hl');
});
$codeBlock.classList.add('is-expanded');
@@ -611,7 +625,7 @@ class FixIt {
const $codeBlocks = document.querySelectorAll('.code-block[group]:not([data-tab-init])');
const processed = new Set();
Util.forEach($codeBlocks, ($block) => {
forEach($codeBlocks, ($block) => {
if (processed.has($block)) return;
const groupName = $block.getAttribute('group');
@@ -693,7 +707,7 @@ class FixIt {
.tab-item[title="${$tab.dataset.tabTitle.toLowerCase()}"]:not(.active),
.tab-item[title="${$tab.dataset.tabTitle.toUpperCase()}"]:not(.active)`
);
Util.forEach(activeItems, t => t.click());
forEach(activeItems, t => t.click());
}
// 3. switch content
@@ -741,15 +755,15 @@ class FixIt {
* init diagram copy button
*/
initDiagramCopyBtn() {
const stagingDOM = Util.getStagingDOM()
Util.forEach(document.querySelectorAll('.diagram-copy-btn'), ($btn) => {
const stagingDOM = getStagingDOM()
forEach(document.querySelectorAll('.diagram-copy-btn'), ($btn) => {
$btn.addEventListener('click', () => {
stagingDOM.stage($btn.parentElement.querySelector('template').content.cloneNode(true))
let code = stagingDOM.contentAsText();
try {
code = JSON.stringify(JSON.parse(code), null, 2);
} catch { }
Util.copyText(code).then(() => {
copyText(code).then(() => {
$btn.toggleAttribute('data-copied', true);
setTimeout(() => {
$btn.toggleAttribute('data-copied', false);
@@ -773,10 +787,10 @@ class FixIt {
const $tocLiElements = $tocContainer.getElementsByTagName('li');
// Remove all active classes
Util.forEach($tocLinkElements, ($tocLink) => {
forEach($tocLinkElements, ($tocLink) => {
$tocLink.classList.remove('active');
});
Util.forEach($tocLiElements, ($tocLi) => {
forEach($tocLiElements, ($tocLi) => {
$tocLi.classList.remove('has-active');
});
@@ -815,10 +829,10 @@ class FixIt {
// TOC Drawer Button Visibility
const openButton = document.querySelector("#toc-drawer-button");
if (openButton) {
openButton.classList.toggle('d-none', !Util.isTocStatic());
openButton.classList.toggle('d-none', !isTocStatic());
}
// TOC Static and TOC Dialog
if (Util.isTocStatic()) {
if (isTocStatic()) {
const $tocContentStatic = document.getElementById('toc-content-static');
if ($tocCore.parentElement !== $tocContentStatic) {
$tocCore.parentElement.removeChild($tocCore);
@@ -845,7 +859,7 @@ class FixIt {
}
const $toc = document.getElementById('toc-auto');
$toc.style.visibility = 'visible';
Util.animateCSS($toc, ['animate__fadeIn', 'animate__faster'], true);
animateCSS($toc, ['animate__fadeIn', 'animate__faster'], true);
const $postMeta = document.querySelector('.post-meta');
$toc.style.marginTop = `${$postMeta.offsetTop + $postMeta.clientHeight}px`;
@@ -874,7 +888,7 @@ class FixIt {
} else {
$tocContentAuto.classList.remove('animate__fadeIn');
}
Util.animateCSS($tocContentAuto, animation, true, () => {
animateCSS($tocContentAuto, animation, true, () => {
$tocContentAuto.classList.contains('animate__fadeOut') && $tocContentAuto.classList.add('d-none');
});
$toc.classList.toggle('toc-hidden');
@@ -909,7 +923,7 @@ class FixIt {
$tocCore = $newTocCore;
}
// remove APlayer click event listener of the heading mark
Util.forEach(document.querySelectorAll('.heading-mark'), ($headingMark) => {
forEach(document.querySelectorAll('.heading-mark'), ($headingMark) => {
const $newHeadingMark = $headingMark.cloneNode(true);
$headingMark.parentElement.replaceChild($newHeadingMark, $headingMark);
});
@@ -926,8 +940,8 @@ class FixIt {
this._echartsArr[i].dispose();
}
this._echartsArr = [];
const stagingDOM = Util.getStagingDOM()
Util.forEach(document.getElementsByClassName('echarts'), ($echarts) => {
const stagingDOM = getStagingDOM()
forEach(document.getElementsByClassName('echarts'), ($echarts) => {
const $dataEl = $echarts.nextElementSibling;
if ($dataEl.tagName !== 'TEMPLATE') return;
const chart = echarts.init($echarts, this.isDark ? 'dark' : 'light', { renderer: 'svg' });
@@ -959,7 +973,7 @@ class FixIt {
* @returns {Object|Promise} ECharts option or Promise
*/
const _getOption = new Function('fixit', 'chart',
Util.isObjectLiteral(jsCodes) ? `return ${jsCodes}` : jsCodes
isObjectLiteral(jsCodes) ? `return ${jsCodes}` : jsCodes
);
if ($dataEl.dataset.async === 'true') {
return Promise.resolve(_getOption(this, chart)).then(option => {
@@ -993,7 +1007,7 @@ class FixIt {
mapboxgl.setRTLTextPlugin(this.config.mapbox.RTLTextPlugin);
this._mapboxArr = this._mapboxArr || [];
}
Util.forEach(document.querySelectorAll('.mapbox:empty'), ($mapbox) => {
forEach(document.querySelectorAll('.mapbox:empty'), ($mapbox) => {
const { lng, lat, zoom, lightStyle, darkStyle, marked, markers, navigation, geolocate, scale, fullscreen } = JSON.parse($mapbox.dataset.options);
const mapbox = new mapboxgl.Map({
container: $mapbox,
@@ -1042,7 +1056,7 @@ class FixIt {
this._mapboxArr.push(mapbox);
});
this._mapboxOnSwitchTheme = this._mapboxOnSwitchTheme || (() => {
Util.forEach(this._mapboxArr, (mapbox) => {
forEach(this._mapboxArr, (mapbox) => {
const $mapbox = mapbox.getContainer();
const { lightStyle, darkStyle } = JSON.parse($mapbox.dataset.options);
mapbox.setStyle(this.isDark ? darkStyle : lightStyle);
@@ -1069,7 +1083,7 @@ class FixIt {
acc[group].push(ele);
return acc;
}, {});
const stagingDOM = Util.getStagingDOM()
const stagingDOM = getStagingDOM()
Object.values(groupMap).forEach((group) => {
const typeone = (i) => {
@@ -1143,7 +1157,7 @@ class FixIt {
$viewCommentsBtn.classList.remove('d-none');
// view comments button click event
$viewCommentsBtn.addEventListener('click', () => {
Util.scrollIntoView('#comments');
scrollIntoView('#comments');
}, false);
}
this.config.comment.expired && document.querySelector('#comments').remove();
@@ -1260,7 +1274,7 @@ class FixIt {
let now = new Date();
let run = new Date(this.config.siteTime);
let $runTimes = document.querySelector('.run-times');
if (!Util.isValidDate(run) || !$runTimes) {
if (!isValidDate(run) || !$runTimes) {
clearInterval(this.siteTime);
$runTimes && $runTimes.parentNode.remove();
return;
@@ -1338,7 +1352,7 @@ class FixIt {
initJsonViewer() {
if (!window.JsonViewerElement) return;
this._jsonViewerOnSwitchTheme = this._jsonViewerOnSwitchTheme || (() => {
Util.forEach(document.getElementsByTagName('json-viewer'), ($el) => {
forEach(document.getElementsByTagName('json-viewer'), ($el) => {
$el.setAttribute('theme', this.isDark ? 'dark' : 'light');
});
});
@@ -1434,7 +1448,7 @@ class FixIt {
_toggleEncryptedClass(container, show) {
const fromClass = show ? 'encrypted-hidden' : 'decrypted-shown';
const toClass = show ? 'decrypted-shown' : 'encrypted-hidden';
Util.forEach(container.querySelectorAll(`.${fromClass}`), ($element) => {
forEach(container.querySelectorAll(`.${fromClass}`), ($element) => {
$element.classList.replace(fromClass, toClass);
});
}
@@ -1459,7 +1473,7 @@ class FixIt {
initAutoMark() {
if (!this.config.autoBookmark) return;
window.addEventListener('beforeunload', () => {
window.sessionStorage?.setItem(`fixit-bookmark/#${location.pathname}`, Util.getScrollTop());
window.sessionStorage?.setItem(`fixit-bookmark/#${location.pathname}`, getScrollTop());
});
const scrollTop = Number(window.sessionStorage?.getItem(`fixit-bookmark/#${location.pathname}`));
// If the page opens with a specific hash, just jump out
@@ -1475,15 +1489,15 @@ class FixIt {
const $rewards = document.querySelectorAll('.post-reward [data-mode="fixed"]');
if (!$rewards.length) return;
// `fixed` mode only supports desktop
if (Util.isMobile()) {
Util.forEach($rewards, ($reward) => {
if (isMobile()) {
forEach($rewards, ($reward) => {
$reward.removeAttribute('data-mode');
});
return;
}
// Close post reward images exclude special id
const _closeRewardExclude = (id) => {
Util.forEach($rewards, ($reward) => {
forEach($rewards, ($reward) => {
const $rewardInput = $reward.parentElement.querySelector('.reward-input');
if ($rewardInput.id !== id) {
$rewardInput.checked = false;
@@ -1491,7 +1505,7 @@ class FixIt {
});
};
// Add additional click event to reward buttons
Util.forEach($rewards, ($reward) => {
forEach($rewards, ($reward) => {
$reward.previousElementSibling.addEventListener('click', function () {
_closeRewardExclude(this.getAttribute('for'));
}, false)
@@ -1524,7 +1538,7 @@ class FixIt {
$headers.push(document.getElementById('header-mobile'));
}
$backToTop?.addEventListener('click', () => {
Util.scrollIntoView('body');
scrollIntoView('body');
});
window.addEventListener('scroll', (event) => {
if (this.disableScrollEvent) {
@@ -1532,17 +1546,17 @@ class FixIt {
return;
}
const $mask = document.getElementById('mask');
this.newScrollTop = Util.getScrollTop();
this.newScrollTop = getScrollTop();
const scroll = this.newScrollTop - this.oldScrollTop;
// header animation
Util.forEach($headers, ($header) => {
forEach($headers, ($header) => {
if (scroll > ACCURACY) {
$header.classList.remove('animate__fadeInDown');
Util.animateCSS($header, ['animate__fadeOutUp'], true);
animateCSS($header, ['animate__fadeOutUp'], true);
$mask.click();
} else if (scroll < -ACCURACY) {
$header.classList.remove('animate__fadeOutUp');
Util.animateCSS($header, ['animate__fadeInDown'], true);
animateCSS($header, ['animate__fadeInDown'], true);
$mask.click();
}
});
@@ -1555,10 +1569,10 @@ class FixIt {
if ($backToTop) {
if (scrollPercent > 1) {
$backToTop.classList.remove('d-none', 'animate__fadeOut');
Util.animateCSS($backToTop, ['animate__fadeIn'], true);
animateCSS($backToTop, ['animate__fadeIn'], true);
} else {
$backToTop.classList.remove('animate__fadeIn');
Util.animateCSS($backToTop, ['animate__fadeOut'], true, () => {
animateCSS($backToTop, ['animate__fadeOut'], true, () => {
$backToTop.classList.contains('animate__fadeOut') && $backToTop.classList.add('d-none');
});
}
@@ -1578,7 +1592,7 @@ class FixIt {
}
onResize() {
let resizeBefore = Util.isMobile();
let resizeBefore = isMobile();
window.addEventListener('resize', () => {
if (!this._resizeTimeout) {
this._resizeTimeout = window.setTimeout(() => {
@@ -1589,10 +1603,10 @@ class FixIt {
this.initToc();
this.initSearch();
const isMobile = Util.isMobile()
if (isMobile !== resizeBefore) {
const _isMobile = isMobile();
if (_isMobile !== resizeBefore) {
document.getElementById('mask').click();
resizeBefore = isMobile;
resizeBefore = _isMobile;
}
}, 100);
}
@@ -1616,11 +1630,11 @@ class FixIt {
const printConfig = this.config.print || {};
if (printConfig.expandAdmonition) {
Util.forEach($content.querySelectorAll('.admonition'), ($el) => $el.classList.add('open'));
forEach($content.querySelectorAll('.admonition'), ($el) => $el.classList.add('open'));
}
if (printConfig.expandCode) {
// revert code tabs to code blocks for better printing support
Util.forEach($content.querySelectorAll('.code-tabs'), ($codeTabs) => {
forEach($content.querySelectorAll('.code-tabs'), ($codeTabs) => {
// restore action buttons to the active tab's code-header before reverting
const $actions = $codeTabs.querySelector('.tabs-actions');
const $activeBlock = $codeTabs.querySelector('.code-block.active');
@@ -1637,7 +1651,7 @@ class FixIt {
});
$codeTabs.parentElement.removeChild($codeTabs);
});
Util.forEach($content.querySelectorAll('.code-block'), ($el) => {
forEach($content.querySelectorAll('.code-block'), ($el) => {
// line wrapping
$el.classList.add('line-wrapping');
// expand all code blocks
@@ -1649,7 +1663,7 @@ class FixIt {
});
}
if (printConfig.expandDetails) {
Util.forEach($content.querySelectorAll('details'), ($el) => $el.setAttribute('open', ''));
forEach($content.querySelectorAll('details'), ($el) => $el.setAttribute('open', ''));
}
for (let event of this.beforeprintEventSet) {
event();
-146
View File
@@ -1,146 +0,0 @@
export default class Util {
static forEach(elements, handler) {
elements = elements || [];
const promises = [];
for (let i = 0; i < elements.length; i++) {
const result = handler(elements[i], i);
if (result instanceof Promise) {
promises.push(result);
}
}
return Promise.all(promises);
}
static getScrollTop() {
return (document.documentElement ?? document.body).scrollTop;
}
static isMobile() {
return window.matchMedia('only screen and (max-width: 680px)').matches;
}
static isTocStatic() {
return document.getElementById('toc-static').dataset.kept === 'true' || window.matchMedia('only screen and (max-width: 960px)').matches;
}
/**
* add animate to element
* @param {Element} element animate element
* @param {String|Array<String>} animation animation name
* @param {Boolean} reserved reserved animation
* @param {Function} callback remove callback
*/
static animateCSS(element, animation, reserved, callback) {
!Array.isArray(animation) && (animation = [animation]);
element.classList.add('animate__animated', ...animation);
element.addEventListener('animationend', () => {
!reserved && element.classList.remove('animate__animated', ...animation);
typeof callback === 'function' && callback();
}, { once: true });
}
/**
* date validator
* @param {*} date may be date or not
* @returns {Boolean}
*/
static isValidDate(date) {
return date instanceof Date && !isNaN(date.getTime());
}
/**
* scroll some element into view
* @param {String} selector element to scroll
*/
static scrollIntoView(selector) {
const element = selector.startsWith('#')
? document.getElementById(selector.slice(1))
: document.querySelector(selector);
element?.scrollIntoView({
behavior: 'smooth'
});
}
/**
* get a hidden element for temporary use
* @returns {Object} { $el: Element, destroy: Function }
*/
static getStagingDOM() {
const stagingElement = document.createElement('div')
stagingElement.style.display = 'none';
stagingElement.dataset.stagingId = Math.random().toString(36).slice(2);
document.body.appendChild(stagingElement);
return {
$el: stagingElement,
stage(dom) {
stagingElement.innerHTML = '';
stagingElement.appendChild(dom);
},
contentAsHtml() {
return stagingElement.innerHTML;
},
contentAsText() {
return stagingElement.innerText;
},
contentAsJson() {
return JSON.parse(stagingElement.innerHTML);
},
destroy() {
document.body.removeChild(stagingElement);
}
}
}
/**
* copy text to clipboard
* @param {String} text text to copy
* @returns {Promise} promise
*/
static copyText(text) {
if (navigator.clipboard) {
this.copyText = (text) => navigator.clipboard.writeText(text);
return this.copyText(text);
}
this.copyText = (text) => new Promise((resolve, reject) => {
const input = document.createElement('input');
input.value = text;
document.body.appendChild(input);
input.select();
if (document.execCommand('copy')) {
document.body.removeChild(input);
resolve();
} else {
reject();
}
});
return this.copyText(text);
}
/**
* check if a string is a JS object string
* @example isObjectLiteral("{a:1,b:2}") // true
* @param {String} str string to check
* @returns {Boolean} whether the string is a JS object string
*/
static isObjectLiteral(str) {
if (typeof str !== 'string') {
return false;
}
str = str.replace(/\s+/g, ' ').trim().replace(/;$/, '')
if (str.startsWith('{') && str.endsWith('}')) {
return true;
}
return false;
}
static HTMLEscape(str) {
return str.replace(/[&<>"']/g, char => ({
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#39;'
})[char]);
}
}
+141
View File
@@ -0,0 +1,141 @@
export function forEach(elements, handler) {
elements = elements || [];
const promises = [];
for (let i = 0; i < elements.length; i++) {
const result = handler(elements[i], i);
if (result instanceof Promise) {
promises.push(result);
}
}
return Promise.all(promises);
}
export function getScrollTop() {
return (document.documentElement ?? document.body).scrollTop;
}
export function isMobile() {
return window.matchMedia('only screen and (max-width: 680px)').matches;
}
export function isTocStatic() {
return document.getElementById('toc-static').dataset.kept === 'true' || window.matchMedia('only screen and (max-width: 960px)').matches;
}
/**
* add animate to element
* @param {Element} element animate element
* @param {String|Array<String>} animation animation name
* @param {Boolean} reserved reserved animation
* @param {Function} callback remove callback
*/
export function animateCSS(element, animation, reserved, callback) {
!Array.isArray(animation) && (animation = [animation]);
element.classList.add('animate__animated', ...animation);
element.addEventListener('animationend', () => {
!reserved && element.classList.remove('animate__animated', ...animation);
typeof callback === 'function' && callback();
}, { once: true });
}
/**
* date validator
* @param {*} date may be date or not
* @returns {Boolean}
*/
export function isValidDate(date) {
return date instanceof Date && !isNaN(date.getTime());
}
/**
* scroll some element into view
* @param {String} selector element to scroll
*/
export function scrollIntoView(selector) {
const element = selector.startsWith('#')
? document.getElementById(selector.slice(1))
: document.querySelector(selector);
element?.scrollIntoView({
behavior: 'smooth'
});
}
/**
* get a hidden element for temporary use
* @returns {Object} { $el: Element, destroy: Function }
*/
export function getStagingDOM() {
const stagingElement = document.createElement('div')
stagingElement.style.display = 'none';
stagingElement.dataset.stagingId = Math.random().toString(36).slice(2);
document.body.appendChild(stagingElement);
return {
$el: stagingElement,
stage(dom) {
stagingElement.innerHTML = '';
stagingElement.appendChild(dom);
},
contentAsHtml() {
return stagingElement.innerHTML;
},
contentAsText() {
return stagingElement.innerText;
},
contentAsJson() {
return JSON.parse(stagingElement.innerHTML);
},
destroy() {
document.body.removeChild(stagingElement);
}
}
}
/**
* create a copy text function with fallback
* @returns {Function} copy text function
*/
export function createCopyText() {
if (navigator.clipboard) {
return (text) => navigator.clipboard.writeText(text);
}
return (text) => new Promise((resolve, reject) => {
const input = document.createElement('input');
input.value = text;
document.body.appendChild(input);
input.select();
if (document.execCommand('copy')) {
document.body.removeChild(input);
resolve();
} else {
reject();
}
});
}
/**
* check if a string is a JS object string
* @example isObjectLiteral("{a:1,b:2}") // true
* @param {String} str string to check
* @returns {Boolean} whether the string is a JS object string
*/
export function isObjectLiteral(str) {
if (typeof str !== 'string') {
return false;
}
str = str.replace(/\s+/g, ' ').trim().replace(/;$/, '')
if (str.startsWith('{') && str.endsWith('}')) {
return true;
}
return false;
}
export function HTMLEscape(str) {
return str.replace(/[&<>"']/g, char => ({
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#39;'
})[char]);
}