diff --git a/assets/js/theme.js b/assets/js/theme.js index c9b55bf5..34312f1f 100644 --- a/assets/js/theme.js +++ b/assets/js/theme.js @@ -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 }) => `
${title}${date}
${context}
`, - empty: ({ query }) => `
${searchConfig.noResultsFound}: "${Util.HTMLEscape(query)}"
`, + empty: ({ query }) => `
${searchConfig.noResultsFound}: "${HTMLEscape(query)}"
`, 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(); diff --git a/assets/js/util.js b/assets/js/util.js deleted file mode 100644 index b0049e71..00000000 --- a/assets/js/util.js +++ /dev/null @@ -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} 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]); - } -} diff --git a/assets/js/utils/common.js b/assets/js/utils/common.js new file mode 100644 index 00000000..8b6310e7 --- /dev/null +++ b/assets/js/utils/common.js @@ -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} 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]); +} diff --git a/layouts/_partials/init/index.html b/layouts/_partials/init/index.html index 828ee1be..1301345e 100644 --- a/layouts/_partials/init/index.html +++ b/layouts/_partials/init/index.html @@ -1,4 +1,4 @@ -{{- hugo.Store.Set "version" "v0.4.5-20260320183805-d5aa0131" -}} +{{- hugo.Store.Set "version" "v0.4.5-20260324033631-a53ef800" -}} {{- .Store.Set "this" dict -}} {{- partial "init/detection-env.html" . -}}