mirror of
https://github.com/hugo-fixit/FixIt.git
synced 2026-08-24 15:28:57 +00:00
refactor(utils): migrate utility functions into common module and rewrite method calls
This commit is contained in:
+77
-63
@@ -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();
|
||||
|
||||
@@ -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 => ({
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'"': '"',
|
||||
"'": '''
|
||||
})[char]);
|
||||
}
|
||||
}
|
||||
@@ -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 => ({
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'"': '"',
|
||||
"'": '''
|
||||
})[char]);
|
||||
}
|
||||
Reference in New Issue
Block a user