diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 7cde58bf..838665a0 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -120,7 +120,7 @@ FixIt/ ```javascript // 使用 ES6 类 export default class Util { - copyText(text) { + static copyText(text) { // ... } } @@ -129,7 +129,7 @@ export default class Util { class FixIt { constructor() { this.config = window.config - this.util = new Util() + this.scrollTop = Util.getScrollTop() } init() { diff --git a/assets/js/theme.js b/assets/js/theme.js index 76f6b1f1..3f26dbb8 100644 --- a/assets/js/theme.js +++ b/assets/js/theme.js @@ -1,5 +1,4 @@ // TODO use ESLint to check the code style -// 按需加载主题内部库和功能模块 import Util from './util'; import FileTree from './lib/file-tree.js' @@ -7,9 +6,8 @@ class FixIt { constructor() { this.config = window.config; this.isDark = document.documentElement.dataset.theme === 'dark'; - this.util = new Util(); this.fileTree = new FileTree(); - this.newScrollTop = this.util.getScrollTop(); + this.newScrollTop = Util.getScrollTop(); this.oldScrollTop = this.newScrollTop; this.scrollEventSet = new Set(); this.resizeEventSet = new Set(); @@ -31,7 +29,7 @@ class FixIt { } initSVGIcon() { - this.util.forEach(document.querySelectorAll('[data-svg-src]'), ($icon) => { + Util.forEach(document.querySelectorAll('[data-svg-src]'), ($icon) => { fetch($icon.dataset.svgSrc) .then((response) => response.text()) .then((svg) => { @@ -60,7 +58,7 @@ class FixIt { } initMenuDesktop() { - this.util.forEach(document.querySelectorAll('.has-children'), ($item) => { + Util.forEach(document.querySelectorAll('.has-children'), ($item) => { $item.querySelector('.sub-menu').style.minWidth = `${$item.offsetWidth - 8}px`; }); } @@ -80,7 +78,7 @@ class FixIt { }); this.clickMaskEventSet.add(this._menuMobileOnClickMask); // add nested menu toggler - this.util.forEach(document.querySelectorAll('.menu-item>.nested-item'), ($nestedItem) => { + Util.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'); @@ -89,7 +87,7 @@ class FixIt { } initSwitchTheme() { - this.util.forEach(document.getElementsByClassName('theme-switch'), ($themeSwitch) => { + Util.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'); @@ -136,7 +134,7 @@ class FixIt { initSearch() { const searchConfig = this.config.search; - const isMobile = this.util.isMobile(); + const isMobile = Util.isMobile(); if ( !searchConfig || (isMobile && this._searchMobileOnce) || @@ -343,7 +341,7 @@ class FixIt { templates: { suggestion: ({ title, uri, date, context }) => `
${title}${date}
${context}
`, - empty: ({ query }) => `
${searchConfig.noResultsFound}: "${this.util.HTMLEscape(query)}"
`, + empty: ({ query }) => `
${searchConfig.noResultsFound}: "${Util.HTMLEscape(query)}"
`, footer: ({ }) => { let searchType, icon, href; switch (searchConfig.type) { @@ -396,7 +394,7 @@ class FixIt { } initDetails(target = document) { - this.util.forEach(target.querySelectorAll('.details:not(.disabled)'), ($details) => { + Util.forEach(target.querySelectorAll('.details:not(.disabled)'), ($details) => { const $summary = $details.querySelector('.details-summary'); $summary.addEventListener('click', () => { $details.classList.toggle('open'); @@ -438,11 +436,11 @@ class FixIt { const iswWrap = codeBlock.classList.contains('line-wrapping'); const highlightLines = codeBlock.querySelectorAll('.hl'); iswWrap && codeBlock.classList.toggle('line-wrapping'); - this.util.forEach(highlightLines, $hl => $hl.classList.toggle('hl')); - this.util.copyText(codePreEl.innerText.trim()).then(() => { - this.util.animateCSS(codePreEl, 'animate__flash'); + Util.forEach(highlightLines, $hl => $hl.classList.toggle('hl')); + Util.copyText(codePreEl.innerText.trim()).then(() => { + Util.animateCSS(codePreEl, 'animate__flash'); iswWrap && codeBlock.classList.toggle('line-wrapping'); - this.util.forEach(highlightLines, $hl => $hl.classList.toggle('hl')); + Util.forEach(highlightLines, $hl => $hl.classList.toggle('hl')); copyBtn.toggleAttribute('data-copied', true); setTimeout(() => { copyBtn.toggleAttribute('data-copied', false); @@ -543,7 +541,7 @@ class FixIt { */ initCodeWrapper() { const $codeBlocks = document.querySelectorAll('.code-block.highlight:not([data-init])'); - this.util.forEach($codeBlocks, ($codeBlock) => { + Util.forEach($codeBlocks, ($codeBlock) => { const $preElements = $codeBlock.querySelectorAll('pre.chroma'); if (!$preElements.length) return; const $codePreEl = $preElements[$preElements.length - 1]; @@ -583,7 +581,7 @@ class FixIt { $codePreEl.setAttribute('contenteditable', false); $codePreEl.blur(); } else { - this.util.forEach($codeBlock.querySelectorAll('.hl'), ($hl) => { + Util.forEach($codeBlock.querySelectorAll('.hl'), ($hl) => { $hl.classList.remove('hl'); }); $codeBlock.classList.add('is-expanded'); @@ -600,15 +598,15 @@ class FixIt { * init diagram copy button */ initDiagramCopyBtn() { - const stagingDOM = this.util.getStagingDOM() - this.util.forEach(document.querySelectorAll('.diagram-copy-btn'), ($btn) => { + const stagingDOM = Util.getStagingDOM() + Util.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 { } - this.util.copyText(code).then(() => { + Util.copyText(code).then(() => { $btn.toggleAttribute('data-copied', true); setTimeout(() => { $btn.toggleAttribute('data-copied', false); @@ -632,10 +630,10 @@ class FixIt { const $tocLiElements = $tocContainer.getElementsByTagName('li'); // Remove all active classes - this.util.forEach($tocLinkElements, ($tocLink) => { + Util.forEach($tocLinkElements, ($tocLink) => { $tocLink.classList.remove('active'); }); - this.util.forEach($tocLiElements, ($tocLi) => { + Util.forEach($tocLiElements, ($tocLi) => { $tocLi.classList.remove('has-active'); }); @@ -674,10 +672,10 @@ class FixIt { // TOC Drawer Button Visibility const openButton = document.querySelector("#toc-drawer-button"); if (openButton) { - openButton.classList.toggle('d-none', !this.util.isTocStatic()); + openButton.classList.toggle('d-none', !Util.isTocStatic()); } // TOC Static and TOC Dialog - if (this.util.isTocStatic()) { + if (Util.isTocStatic()) { const $tocContentStatic = document.getElementById('toc-content-static'); if ($tocCore.parentElement !== $tocContentStatic) { $tocCore.parentElement.removeChild($tocCore); @@ -704,7 +702,7 @@ class FixIt { } const $toc = document.getElementById('toc-auto'); $toc.style.visibility = 'visible'; - this.util.animateCSS($toc, ['animate__fadeIn', 'animate__faster'], true); + Util.animateCSS($toc, ['animate__fadeIn', 'animate__faster'], true); const $postMeta = document.querySelector('.post-meta'); $toc.style.marginTop = `${$postMeta.offsetTop + $postMeta.clientHeight}px`; @@ -733,7 +731,7 @@ class FixIt { } else { $tocContentAuto.classList.remove('animate__fadeIn'); } - this.util.animateCSS($tocContentAuto, animation, true, () => { + Util.animateCSS($tocContentAuto, animation, true, () => { $tocContentAuto.classList.contains('animate__fadeOut') && $tocContentAuto.classList.add('d-none'); }); $toc.classList.toggle('toc-hidden'); @@ -768,7 +766,7 @@ class FixIt { $tocCore = $newTocCore; } // remove APlayer click event listener of the heading mark - this.util.forEach(document.querySelectorAll('.heading-mark'), ($headingMark) => { + Util.forEach(document.querySelectorAll('.heading-mark'), ($headingMark) => { const $newHeadingMark = $headingMark.cloneNode(true); $headingMark.parentElement.replaceChild($newHeadingMark, $headingMark); }); @@ -785,8 +783,8 @@ class FixIt { this._echartsArr[i].dispose(); } this._echartsArr = []; - const stagingDOM = this.util.getStagingDOM() - this.util.forEach(document.getElementsByClassName('echarts'), ($echarts) => { + const stagingDOM = Util.getStagingDOM() + Util.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' }); @@ -818,7 +816,7 @@ class FixIt { * @returns {Object|Promise} ECharts option or Promise */ const _getOption = new Function('fixit', 'chart', - this.util.isObjectLiteral(jsCodes) ? `return ${jsCodes}` : jsCodes + Util.isObjectLiteral(jsCodes) ? `return ${jsCodes}` : jsCodes ); if ($dataEl.dataset.async === 'true') { return Promise.resolve(_getOption(this, chart)).then(option => { @@ -852,7 +850,7 @@ class FixIt { mapboxgl.setRTLTextPlugin(this.config.mapbox.RTLTextPlugin); this._mapboxArr = this._mapboxArr || []; } - this.util.forEach(document.querySelectorAll('.mapbox:empty'), ($mapbox) => { + Util.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, @@ -901,7 +899,7 @@ class FixIt { this._mapboxArr.push(mapbox); }); this._mapboxOnSwitchTheme = this._mapboxOnSwitchTheme || (() => { - this.util.forEach(this._mapboxArr, (mapbox) => { + Util.forEach(this._mapboxArr, (mapbox) => { const $mapbox = mapbox.getContainer(); const { lightStyle, darkStyle } = JSON.parse($mapbox.dataset.options); mapbox.setStyle(this.isDark ? darkStyle : lightStyle); @@ -928,7 +926,7 @@ class FixIt { acc[group].push(ele); return acc; }, {}); - const stagingDOM = this.util.getStagingDOM() + const stagingDOM = Util.getStagingDOM() Object.values(groupMap).forEach((group) => { const typeone = (i) => { @@ -1002,7 +1000,7 @@ class FixIt { $viewCommentsBtn.classList.remove('d-none'); // view comments button click event $viewCommentsBtn.addEventListener('click', () => { - this.util.scrollIntoView('#comments'); + Util.scrollIntoView('#comments'); }, false); } this.config.comment.expired && document.querySelector('#comments').remove(); @@ -1119,7 +1117,7 @@ class FixIt { let now = new Date(); let run = new Date(this.config.siteTime); let $runTimes = document.querySelector('.run-times'); - if (!this.util.isValidDate(run) || !$runTimes) { + if (!Util.isValidDate(run) || !$runTimes) { clearInterval(this.siteTime); $runTimes && $runTimes.parentNode.remove(); return; @@ -1197,7 +1195,7 @@ class FixIt { initJsonViewer() { if (!window.JsonViewerElement) return; this._jsonViewerOnSwitchTheme = this._jsonViewerOnSwitchTheme || (() => { - this.util.forEach(document.getElementsByTagName('json-viewer'), ($el) => { + Util.forEach(document.getElementsByTagName('json-viewer'), ($el) => { $el.setAttribute('theme', this.isDark ? 'dark' : 'light'); }); }); @@ -1249,7 +1247,7 @@ class FixIt { _toggleEncryptedClass(container, show) { const fromClass = show ? 'encrypted-hidden' : 'decrypted-shown'; const toClass = show ? 'decrypted-shown' : 'encrypted-hidden'; - this.util.forEach(container.querySelectorAll(`.${fromClass}`), ($element) => { + Util.forEach(container.querySelectorAll(`.${fromClass}`), ($element) => { $element.classList.replace(fromClass, toClass); }); } @@ -1274,7 +1272,7 @@ class FixIt { initAutoMark() { if (!this.config.autoBookmark) return; window.addEventListener('beforeunload', () => { - window.sessionStorage?.setItem(`fixit-bookmark/#${location.pathname}`, this.util.getScrollTop()); + window.sessionStorage?.setItem(`fixit-bookmark/#${location.pathname}`, Util.getScrollTop()); }); const scrollTop = Number(window.sessionStorage?.getItem(`fixit-bookmark/#${location.pathname}`)); // If the page opens with a specific hash, just jump out @@ -1290,15 +1288,15 @@ class FixIt { const $rewards = document.querySelectorAll('.post-reward [data-mode="fixed"]'); if (!$rewards.length) return; // `fixed` mode only supports desktop - if (this.util.isMobile()) { - this.util.forEach($rewards, ($reward) => { + if (Util.isMobile()) { + Util.forEach($rewards, ($reward) => { $reward.removeAttribute('data-mode'); }); return; } // Close post reward images exclude special id const _closeRewardExclude = (id) => { - this.util.forEach($rewards, ($reward) => { + Util.forEach($rewards, ($reward) => { const $rewardInput = $reward.parentElement.querySelector('.reward-input'); if ($rewardInput.id !== id) { $rewardInput.checked = false; @@ -1306,7 +1304,7 @@ class FixIt { }); }; // Add additional click event to reward buttons - this.util.forEach($rewards, ($reward) => { + Util.forEach($rewards, ($reward) => { $reward.previousElementSibling.addEventListener('click', function () { _closeRewardExclude(this.getAttribute('for')); }, false) @@ -1339,7 +1337,7 @@ class FixIt { $headers.push(document.getElementById('header-mobile')); } $backToTop?.addEventListener('click', () => { - this.util.scrollIntoView('body'); + Util.scrollIntoView('body'); }); window.addEventListener('scroll', (event) => { if (this.disableScrollEvent) { @@ -1347,17 +1345,17 @@ class FixIt { return; } const $mask = document.getElementById('mask'); - this.newScrollTop = this.util.getScrollTop(); + this.newScrollTop = Util.getScrollTop(); const scroll = this.newScrollTop - this.oldScrollTop; // header animation - this.util.forEach($headers, ($header) => { + Util.forEach($headers, ($header) => { if (scroll > ACCURACY) { $header.classList.remove('animate__fadeInDown'); - this.util.animateCSS($header, ['animate__fadeOutUp'], true); + Util.animateCSS($header, ['animate__fadeOutUp'], true); $mask.click(); } else if (scroll < -ACCURACY) { $header.classList.remove('animate__fadeOutUp'); - this.util.animateCSS($header, ['animate__fadeInDown'], true); + Util.animateCSS($header, ['animate__fadeInDown'], true); $mask.click(); } }); @@ -1370,10 +1368,10 @@ class FixIt { if ($backToTop) { if (scrollPercent > 1) { $backToTop.classList.remove('d-none', 'animate__fadeOut'); - this.util.animateCSS($backToTop, ['animate__fadeIn'], true); + Util.animateCSS($backToTop, ['animate__fadeIn'], true); } else { $backToTop.classList.remove('animate__fadeIn'); - this.util.animateCSS($backToTop, ['animate__fadeOut'], true, () => { + Util.animateCSS($backToTop, ['animate__fadeOut'], true, () => { $backToTop.classList.contains('animate__fadeOut') && $backToTop.classList.add('d-none'); }); } @@ -1393,7 +1391,7 @@ class FixIt { } onResize() { - let resizeBefore = this.util.isMobile(); + let resizeBefore = Util.isMobile(); window.addEventListener('resize', () => { if (!this._resizeTimeout) { this._resizeTimeout = window.setTimeout(() => { @@ -1404,7 +1402,7 @@ class FixIt { this.initToc(); this.initSearch(); - const isMobile = this.util.isMobile() + const isMobile = Util.isMobile() if (isMobile !== resizeBefore) { document.getElementById('mask').click(); resizeBefore = isMobile; @@ -1427,7 +1425,7 @@ class FixIt { beforeprint() { window.addEventListener('beforeprint', () => { - this.util.forEach(document.querySelectorAll('.chroma'), ($el) => { + Util.forEach(document.querySelectorAll('.chroma'), ($el) => { $el.classList.toggle('open', true) }); for (let event of this.beforeprintEventSet) { diff --git a/assets/js/util.js b/assets/js/util.js index 9c24cf03..b0049e71 100644 --- a/assets/js/util.js +++ b/assets/js/util.js @@ -1,5 +1,5 @@ export default class Util { - forEach(elements, handler) { + static forEach(elements, handler) { elements = elements || []; const promises = []; for (let i = 0; i < elements.length; i++) { @@ -11,15 +11,15 @@ export default class Util { return Promise.all(promises); } - getScrollTop() { + static getScrollTop() { return (document.documentElement ?? document.body).scrollTop; } - isMobile() { + static isMobile() { return window.matchMedia('only screen and (max-width: 680px)').matches; } - isTocStatic() { + static isTocStatic() { return document.getElementById('toc-static').dataset.kept === 'true' || window.matchMedia('only screen and (max-width: 960px)').matches; } @@ -30,7 +30,7 @@ export default class Util { * @param {Boolean} reserved reserved animation * @param {Function} callback remove callback */ - animateCSS(element, animation, reserved, callback) { + static animateCSS(element, animation, reserved, callback) { !Array.isArray(animation) && (animation = [animation]); element.classList.add('animate__animated', ...animation); element.addEventListener('animationend', () => { @@ -44,7 +44,7 @@ export default class Util { * @param {*} date may be date or not * @returns {Boolean} */ - isValidDate(date) { + static isValidDate(date) { return date instanceof Date && !isNaN(date.getTime()); } @@ -52,7 +52,7 @@ export default class Util { * scroll some element into view * @param {String} selector element to scroll */ - scrollIntoView(selector) { + static scrollIntoView(selector) { const element = selector.startsWith('#') ? document.getElementById(selector.slice(1)) : document.querySelector(selector); @@ -65,7 +65,7 @@ export default class Util { * get a hidden element for temporary use * @returns {Object} { $el: Element, destroy: Function } */ - getStagingDOM() { + static getStagingDOM() { const stagingElement = document.createElement('div') stagingElement.style.display = 'none'; stagingElement.dataset.stagingId = Math.random().toString(36).slice(2); @@ -97,7 +97,7 @@ export default class Util { * @param {String} text text to copy * @returns {Promise} promise */ - copyText(text) { + static copyText(text) { if (navigator.clipboard) { this.copyText = (text) => navigator.clipboard.writeText(text); return this.copyText(text); @@ -123,7 +123,7 @@ export default class Util { * @param {String} str string to check * @returns {Boolean} whether the string is a JS object string */ - isObjectLiteral(str) { + static isObjectLiteral(str) { if (typeof str !== 'string') { return false; } @@ -134,7 +134,7 @@ export default class Util { return false; } - HTMLEscape(str) { + static HTMLEscape(str) { return str.replace(/[&<>"']/g, char => ({ '&': '&', '<': '<', diff --git a/layouts/_partials/init/index.html b/layouts/_partials/init/index.html index ce22f3e1..28c8c717 100644 --- a/layouts/_partials/init/index.html +++ b/layouts/_partials/init/index.html @@ -1,4 +1,4 @@ -{{- hugo.Store.Set "version" "v0.4.4-20260304080211-da297a71" -}} +{{- hugo.Store.Set "version" "v0.4.4-20260306024648-423a7ddb" -}} {{- .Store.Set "this" dict -}} {{- partial "init/detection-env.html" . -}}