// TODO use ESLint to check the code style import Util from './util'; class FixIt { constructor() { this.config = window.config; this.isDark = document.documentElement.dataset.theme === 'dark'; this.util = new Util(); this.newScrollTop = this.util.getScrollTop(); this.oldScrollTop = this.newScrollTop; this.scrollEventSet = new Set(); this.resizeEventSet = new Set(); this.switchThemeEventSet = new Set(); this.clickMaskEventSet = new Set(); this.beforeprintEventSet = new Set(); this.disableScrollEvent = false; window.objectFitImages && objectFitImages(); } initThemeColor() { const $meta = document.querySelector('[name="theme-color"]'); if (!$meta) { return; } this._themeColorOnSwitchTheme = this._themeColorOnSwitchTheme || (() => { $meta.content = this.isDark ? $meta.dataset.dark : $meta.dataset.light; }); this.switchThemeEventSet.add(this._themeColorOnSwitchTheme); this._themeColorOnSwitchTheme(); } initSVGIcon() { this.util.forEach(document.querySelectorAll('[data-svg-src]'), ($icon) => { fetch($icon.dataset.svgSrc) .then((response) => response.text()) .then((svg) => { const $temp = document.createElement('div'); $temp.insertAdjacentHTML('afterbegin', svg); const $svg = $temp.firstChild; $svg.dataset.svgSrc = $icon.dataset.svgSrc $svg.classList.add('icon'); const $titleElements = $svg.getElementsByTagName('title'); $titleElements.length && $svg.removeChild($titleElements[0]); $icon.parentElement.replaceChild($svg, $icon); }) .catch((err) => { console.error(err); }); }); } initTwemoji(target = document.body) { this.config.twemoji && twemoji.parse(target); } initMenu() { this.initMenuDesktop(); this.initMenuMobile(); } initMenuDesktop() { this.util.forEach(document.querySelectorAll('.has-children'), ($item) => { $item.querySelector('.sub-menu').style.minWidth = `${$item.offsetWidth - 8}px`; }); } initMenuMobile() { const $menuToggleMobile = document.getElementById('menu-toggle-mobile'); const $menuMobile = document.getElementById('menu-mobile'); $menuToggleMobile.addEventListener('click', (event) => { document.body.classList.toggle('blur'); $menuToggleMobile.classList.toggle('active'); $menuMobile.classList.toggle('active'); this.disableScrollEvent = document.body.classList.contains('blur'); }, false); this._menuMobileOnClickMask = this._menuMobileOnClickMask || (() => { $menuToggleMobile.classList.remove('active'); $menuMobile.classList.remove('active'); }); this.clickMaskEventSet.add(this._menuMobileOnClickMask); // add nested menu toggler this.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'); }); }); } initSwitchTheme() { this.util.forEach(document.getElementsByClassName('theme-switch'), ($themeSwitch) => { $themeSwitch.addEventListener('click', () => { document.documentElement.dataset.theme = this.isDark ? 'light' : 'dark'; this.isDark = !this.isDark; window.localStorage?.setItem('theme', this.isDark ? 'dark' : 'light'); for (let event of this.switchThemeEventSet) { event(this.isDark); } }, false); }); } initSearch() { const searchConfig = this.config.search; const isMobile = this.util.isMobile(); if (!searchConfig || (isMobile && this._searchMobileOnce) || (!isMobile && this._searchDesktopOnce)) { return; } // Initialize default search config const maxResultLength = searchConfig.maxResultLength ?? 10; const snippetLength = searchConfig.snippetLength ?? 50; const highlightTag = searchConfig.highlightTag ?? 'em'; const isCaseSensitive = searchConfig.isCaseSensitive ?? false; const minMatchCharLength = searchConfig.minMatchCharLength ?? 1; const findAllMatches = searchConfig.findAllMatches ?? false; const location = searchConfig.location ?? 0; const threshold = searchConfig.threshold ?? 0.3; const distance = searchConfig.distance ?? 100; const ignoreLocation = searchConfig.ignoreLocation ?? false; const useExtendedSearch = searchConfig.useExtendedSearch ?? false; const ignoreFieldNorm = searchConfig.ignoreFieldNorm ?? false; 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}`); const $searchLoading = document.getElementById(`search-loading-${suffix}`); const $searchClear = document.getElementById(`search-clear-${suffix}`); const $searchCancel = document.getElementById('search-cancel-mobile'); // goto the PostChat panel rather than search results if (searchConfig.type === 'post-chat' && window.postChatUser) { if (isMobile) { $searchInput.addEventListener('focus', () => { window.postChatUser.setSearchInput(''); }, false); } else { $searchToggle.addEventListener('click', () => { window.postChatUser.setSearchInput(''); }, false); } return; } if (isMobile) { this._searchMobileOnce = true; $searchInput.addEventListener('focus', () => { this.disableScrollEvent = true; document.body.classList.add('blur'); $header.classList.add('open'); }, false); $searchCancel.addEventListener('click', () => { this.disableScrollEvent = false; $header.classList.remove('open'); document.body.classList.remove('blur'); document.getElementById('menu-toggle-mobile').classList.remove('active'); document.getElementById('menu-mobile').classList.remove('active'); $searchLoading.style.display = 'none'; $searchClear.style.display = 'none'; this._searchMobile && this._searchMobile.autocomplete.setVal(''); }, false); $searchClear.addEventListener('click', () => { $searchClear.style.display = 'none'; this._searchMobile && this._searchMobile.autocomplete.setVal(''); }, false); this._searchMobileOnClickMask = this._searchMobileOnClickMask || (() => { $header.classList.remove('open'); $searchLoading.style.display = 'none'; $searchClear.style.display = 'none'; this._searchMobile && this._searchMobile.autocomplete.setVal(''); }); this.clickMaskEventSet.add(this._searchMobileOnClickMask); } else { this._searchDesktopOnce = true; $searchToggle.addEventListener('click', () => { document.body.classList.add('blur'); $header.classList.add('open'); $searchInput.focus(); this.disableScrollEvent = true; }, false); $searchClear.addEventListener('click', () => { this.disableScrollEvent = false; $searchClear.style.display = 'none'; this._searchDesktop && this._searchDesktop.autocomplete.setVal(''); }, false); this._searchDesktopOnClickMask = this._searchDesktopOnClickMask ||(() => { $header.classList.remove('open'); $searchLoading.style.display = 'none'; $searchClear.style.display = 'none'; this._searchDesktop && this._searchDesktop.autocomplete.setVal(''); }); this.clickMaskEventSet.add(this._searchDesktopOnClickMask); } $searchInput.addEventListener('input', () => { if ($searchInput.value === '') $searchClear.style.display = 'none'; else $searchClear.style.display = 'inline'; }, false); const initAutosearch = () => { const autosearch = autocomplete(`#search-input-${suffix}`, { hint: false, autoselect: true, dropdownMenuContainer: `#search-dropdown-${suffix}`, clearOnSelected: true, cssClasses: { noPrefix: true }, debug: true }, { name: 'search', source: (query, callback) => { $searchLoading.style.display = 'inline'; $searchClear.style.display = 'none'; const finish = (results) => { $searchLoading.style.display = 'none'; $searchClear.style.display = 'inline'; callback(results); }; if (searchConfig.type === 'algolia') { this._algoliaIndex = this._algoliaIndex || algoliasearch(searchConfig.algoliaAppID, searchConfig.algoliaSearchKey).initIndex(searchConfig.algoliaIndex); this._algoliaIndex .search(query, { offset: 0, length: maxResultLength * 8, attributesToHighlight: ['title'], attributesToSnippet: [`content:${snippetLength}`], highlightPreTag: `<${highlightTag}>`, highlightPostTag: `` }) .then(({ hits }) => { const results = {}; hits.forEach(({ uri, date, _highlightResult: { title }, _snippetResult: { content } }) => { if (results[uri] && results[uri].context.length > content.value) { return; } results[uri] = { uri: uri, title: title.value, date: date, context: content.value }; }); finish(Object.values(results).slice(0, maxResultLength)); }) .catch((err) => { console.error(err); finish([]); }); } else if (searchConfig.type === 'fuse') { const search = () => { const results = {}; window._index.search(query).forEach(({ item, refIndex, matches }) => { let title = item.title; let content = item.content.replace(/&/g, '&').replace(//g, '>') matches.forEach(({ indices, value, key }) => { if (key === 'content') { let offset = 0; for (let i = 0; i < indices.length; i++) { const substr = content.substring(indices[i][0] + offset, indices[i][1] + 1 + offset); const tag = `<${highlightTag}>` + substr + ``; content = content.substring(0, indices[i][0] + offset) + tag + content.substring(indices[i][1] + 1 + offset, content.length); offset += highlightTag.length * 2 + 5; } } else if (key === 'title') { let offset = 0; for (let i = 0; i < indices.length; i++) { const substr = title.substring(indices[i][0] + offset, indices[i][1] + 1 + offset); const tag = `<${highlightTag}>` + substr + ``; title = title.substring(0, indices[i][0] + offset) + tag + title.substring(indices[i][1] + 1 + offset, content.length); offset += highlightTag.length * 2 + 5; } } }); results[item.uri] = { uri: item.uri, title: title, date: item.date, context: content }; }); return Object.values(results).slice(0, maxResultLength); }; if (!window._index) { fetch(searchConfig.fuseIndexURL) .then((response) => response.json()) .then((data) => { const options = { isCaseSensitive: isCaseSensitive, findAllMatches: findAllMatches, minMatchCharLength: minMatchCharLength, location: location, threshold: threshold, distance: distance, ignoreLocation: ignoreLocation, useExtendedSearch: useExtendedSearch, ignoreFieldNorm: ignoreFieldNorm, includeScore: false, shouldSort: true, includeMatches: true, keys: ['content', 'title'] }; window._index = new Fuse(data, options); finish(search()); }) .catch((err) => { console.error(err); finish([]); }); } else finish(search()); } else if (searchConfig.type === 'cse') { const cseConfig = this.config.cse; if (cseConfig.engine === 'google' && cseConfig.cx) { finish([{ uri: `${cseConfig.resultsPage}#gsc.tab=0&gsc.q=${encodeURIComponent(query)}`, title: cseConfig.searchIn, date: '', context: cseConfig.gotoResultsPage }]); } } else { finish([]); } }, templates: { suggestion: ({ title, date, context }) => `
${title}${date}
${context}
`, empty: ({ query }) => `
${searchConfig.noResultsFound}: "${query}"
`, footer: ({}) => { let searchType, icon, href; switch (searchConfig.type) { case 'algolia': searchType = 'algolia'; icon = ''; href = 'https://www.algolia.com/'; break; case 'fuse': searchType = 'Fuse.js'; icon = ''; href = 'https://fusejs.io/'; break; case 'cse': if (this.config.cse.engine === 'google') { searchType = 'Google CSE'; icon = ''; href = 'https://programmablesearchengine.google.com/'; } break; default: searchType = ''; icon = ''; href = ''; } return ``; } } } ); autosearch.on('autocomplete:selected', (_event, suggestion, _dataset, _context) => { window.location.assign(suggestion.uri); }); if (isMobile) { this._searchMobile = autosearch; } else { this._searchDesktop = autosearch; } }; initAutosearch(); } initBreadcrumb() { const $breadcrumbContainer = document.querySelector('.breadcrumb-container.sticky') this.breadcrumbHeight = $breadcrumbContainer?.clientHeight ?? 0; if (this.breadcrumbHeight) { document.querySelector('main.fi-container')?.style.setProperty('--fi-breadcrumb-height', `${this.breadcrumbHeight}px`); } } initDetails(target = document) { this.util.forEach(target.querySelectorAll('.details:not(.disabled)'), ($details) => { const $summary = $details.querySelector('.details-summary'); $summary.addEventListener('click', () => { $details.classList.toggle('open'); }, false); }); } initLightGallery() { if (this.config.lightgallery) { this.lg && this.lg.destroy(true); this.lg = lightGallery(document.getElementById('content'), { plugins: [lgThumbnail, lgZoom], selector: '.lightgallery', speed: 400, hideBarsDelay: 2000, allowMediaOverlap: true, exThumbImage: 'data-thumbnail', toggleThumb: true, thumbWidth: 80, thumbHeight: '60px', actualSize: false, showZoomInOutIcons: true, licenseKey: 'none' }); } } /** * init code wrapper */ initCodeWrapper() { if (!this.config.code) { this.initCopyCode(); return } // if markup.highlight.lineNumbersInTable set to false this.util.forEach(document.querySelectorAll('.highlight > pre.chroma'), ($preChroma) => { const $chroma = document.createElement('div'); $chroma.className = $preChroma.className; const $table = document.createElement('table'); $chroma.appendChild($table); const $tbody = document.createElement('tbody'); $table.appendChild($tbody); const $tr = document.createElement('tr'); $tbody.appendChild($tr); const $td = document.createElement('td'); $tr.appendChild($td); $preChroma.parentElement.replaceChild($chroma, $preChroma); $td.appendChild($preChroma); }); // render code header this.util.forEach(document.querySelectorAll('.highlight > .chroma:not([data-init])'), ($chroma) => { $chroma.dataset.init = 'true'; if ($chroma.parentElement.classList.contains('no-header')) { this.initCopyCode($chroma); return; } const $codeElements = $chroma.querySelectorAll('pre.chroma > code'); if ($codeElements.length) { const $code = $codeElements[$codeElements.length - 1]; const $header = document.createElement('div'); $header.className = 'code-header ' + $code.className.toLowerCase(); // code title const $title = document.createElement('span'); $title.classList.add('code-title'); // insert code title inner $title.insertAdjacentHTML( 'afterbegin', $chroma.parentNode.title ? `${$chroma.parentNode.title}` : '' ); $title.addEventListener('click', () => { $chroma.classList.toggle('open'); }, false); $header.appendChild($title); // ellipses icon const $ellipses = document.createElement('span'); $ellipses.insertAdjacentHTML('afterbegin', ''); $ellipses.classList.add('ellipses'); $ellipses.addEventListener('click', () => { $chroma.classList.add('open'); }, false); $header.appendChild($ellipses); // edit button if (this.config.code.editable) { const $edit = document.createElement('span'); $edit.classList.add('edit'); $edit.insertAdjacentHTML('afterbegin', ``); $edit.addEventListener('click', () => { const $iconKey = $edit.querySelector('.fa-pen-to-square'); const $iconLock = $edit.querySelector('.fa-lock'); const $preChromas = $edit.parentElement.parentElement.querySelectorAll('pre.chroma'); const $preChroma = $preChromas.length === 2 ? $preChromas[1] : $preChromas[0]; if ($iconKey) { $iconKey.classList.add('fa-lock'); $iconKey.classList.remove('fa-pen-to-square'); $iconKey.title = this.config.code.editLockTitle; $preChroma.setAttribute('contenteditable', true); $preChroma.focus(); } else { $iconLock.classList.add('fa-pen-to-square'); $iconLock.classList.remove('fa-lock'); $iconLock.title = this.config.code.editUnLockTitle; $preChroma.setAttribute('contenteditable', false); $preChroma.blur(); } }, false); $header.appendChild($edit); } // copy button if (this.config.code.copyTitle) { const $copy = document.createElement('span'); $copy.insertAdjacentHTML('afterbegin', ''); $copy.classList.add('copy'); // remove the leading and trailing whitespace of the code string let code = $code.innerText.trim(); // in the details element, the code string cannot be gotten directly. if ($chroma.closest('details') !== null) { const _tempEl = document.createElement('div'); _tempEl.appendChild($code.cloneNode(true)); code = _tempEl.innerText.trim(); } const forceOpen = $chroma.parentElement.dataset.open ? JSON.parse($chroma.parentElement.dataset.open) : void 0; if (forceOpen ?? (this.config.code.maxShownLines < 0 || code.split('\n').length < this.config.code.maxShownLines + 2)) { $chroma.classList.add('open'); } $copy.title = this.config.code.copyTitle; $copy.addEventListener('click', () => { this.util.copyText(code).then(() => { this.util.animateCSS($code, 'animate__flash'); }, () => { console.error('Clipboard write failed!', 'Your browser does not support clipboard API!'); }); }, false); $header.appendChild($copy); } $chroma.insertBefore($header, $chroma.firstChild); } }); } /** * init diagram copy button */ initDiagramCopyBtn() { this.util.forEach(document.querySelectorAll('.diagram-copy-btn'), ($btn) => { $btn.addEventListener('click', () => { const _tempEl = document.createElement('div'); _tempEl.appendChild($btn.parentElement.querySelector('template').content.cloneNode(true)); const code = _tempEl.innerText.trim(); this.util.copyText(code).then(() => { $btn.toggleAttribute('data-copied', true); setTimeout(() => { $btn.toggleAttribute('data-copied', false); }, 2000); }, () => { console.error('Clipboard write failed!', 'Your browser does not support clipboard API!'); }); }, false); }); } initTable(target = document) { this.util.forEach(target.querySelectorAll('.content table'), ($table) => { const $wrapper = document.createElement('div'); $wrapper.className = 'table-wrapper'; $table.parentElement.replaceChild($wrapper, $table); $wrapper.appendChild($table); }); } /** * init simple copy code when there is no code header * https://github.com/github/clipboard-copy-element * @param {ELement} singleCode single code block */ initCopyCode(singleCode) { // TODO } /** * init table of contents */ initToc() { const $tocCore = document.getElementById('TableOfContents'); if ($tocCore === null) { return; } if (document.getElementById('toc-static').dataset.kept === 'true' || this.util.isTocStatic()) { const $tocContentStatic = document.getElementById('toc-content-static'); if ($tocCore.parentElement !== $tocContentStatic) { $tocCore.parentElement.removeChild($tocCore); $tocContentStatic.appendChild($tocCore); } this._tocOnScroll && this.scrollEventSet.delete(this._tocOnScroll); } else { const $tocContentAuto = document.getElementById('toc-content-auto'); if ($tocCore.parentElement !== $tocContentAuto) { $tocCore.parentElement.removeChild($tocCore); $tocContentAuto.appendChild($tocCore); } const $toc = document.getElementById('toc-auto'); $toc.style.visibility = 'visible'; this.util.animateCSS($toc, ['animate__fadeIn', 'animate__faster'], true); const $postMeta = document.querySelector('.post-meta'); $toc.style.marginTop = `${$postMeta.offsetTop + $postMeta.clientHeight}px`; const $tocLinkElements = $tocCore.querySelectorAll('a:first-child'); const $tocLiElements = $tocCore.getElementsByTagName('li'); const $headingElements = document.getElementsByClassName('heading-element'); const headerHeight = document.getElementById('header-desktop').offsetHeight; document.querySelector('.fi-container').addEventListener('resize', () => { $toc.style.marginBottom = `${document.querySelector('.fi-container').clientHeight - document.querySelector('.post-footer').offsetTop}px`; }); this._tocOnScroll = this._tocOnScroll || (() => { $toc.style.marginBottom = `${document.querySelector('.fi-container').clientHeight - document.querySelector('.post-footer').offsetTop}px`; this.util.forEach($tocLinkElements, ($tocLink) => { $tocLink.classList.remove('active'); }); this.util.forEach($tocLiElements, ($tocLi) => { $tocLi.classList.remove('has-active'); }); const INDEX_SPACING = 20 + (document.body.dataset.headerDesktop !== 'normal' ? headerHeight : 0) + this.breadcrumbHeight; let activeTocIndex = $headingElements.length - 1; for (let i = 0; i < $headingElements.length - 1; i++) { const thisTop = $headingElements[i].getBoundingClientRect().top; const nextTop = $headingElements[i + 1].getBoundingClientRect().top; if ((i == 0 && thisTop > INDEX_SPACING) || (thisTop <= INDEX_SPACING && nextTop > INDEX_SPACING)) { activeTocIndex = i; break; } } if (activeTocIndex !== -1 && $tocLinkElements[activeTocIndex]) { $tocLinkElements[activeTocIndex].classList.add('active'); let $parent = $tocLinkElements[activeTocIndex].parentElement; while ($parent !== $tocCore) { $parent.classList.add('has-active'); $parent = $parent.parentElement.parentElement; } } }); this._tocOnScroll(); this.scrollEventSet.add(this._tocOnScroll); } } initTocListener() { const $toc = document.getElementById('toc-auto'); const $tocContentAuto = document.getElementById('toc-content-auto'); document.querySelector('#toc-auto>.toc-title')?.addEventListener('click', () => { const animation = ['animate__faster']; const tocHidden = $toc.classList.contains('toc-hidden'); animation.push(tocHidden ? 'animate__fadeIn' : 'animate__fadeOut'); $tocContentAuto.classList.remove(tocHidden ? 'animate__fadeOut' : 'animate__fadeIn'); this.util.animateCSS($tocContentAuto, animation, true); $toc.classList.toggle('toc-hidden'); }, false); } /** * It's a dirty hack to fix the bug of APlayer and smoothScroll. * see https://github.com/hugo-fixit/FixIt/issues/292 */ fixTocScroll() { if (typeof APlayer === 'function') { // remove APlayer click event listener of the toc link let $tocCore = document.getElementById('TableOfContents'); if ($tocCore) { const $newTocCore = $tocCore.cloneNode(true); $tocCore.parentElement.replaceChild($newTocCore, $tocCore); $tocCore = $newTocCore; } // remove APlayer click event listener of the heading mark this.util.forEach(document.querySelectorAll('.heading-mark'), ($headingMark) => { const $newHeadingMark = $headingMark.cloneNode(true); $headingMark.parentElement.replaceChild($newHeadingMark, $headingMark); }); } } initMermaid() { if (!this.config.mermaid) return; const themes = this.config.mermaid.themes ?? ['default', 'dark']; let processing = false; let delayTask = null; const loadMermaid = async () => { processing = true; // https://mermaid.js.org/config/schema-docs/config.html window.mermaid.initialize({ startOnLoad: false, darkMode: this.isDark, theme: this.isDark ? themes[1] : themes[0], securityLevel: this.config.mermaid.securityLevel, look: this.config.mermaid.look, fontFamily: this.config.mermaid.fontFamily, altFontFamily: this.config.mermaid.fontFamily }); await window.mermaid.run({ querySelector: '.mermaid', suppressErrors: true, }); processing = false; if (delayTask && typeof delayTask === 'function') { delayTask(); delayTask = null; // console.log('Delayed task executed'); } }; const reloadMermaid = async () => { await this.util.forEach(document.querySelectorAll('.mermaid[data-processed]'), (el) => { el.removeAttribute('data-processed'); el.parentElement.replaceChild(el.nextElementSibling.content.cloneNode(true), el); }); await loadMermaid(); }; this.switchThemeEventSet.add(() => { if (processing) { console.warn('Mermaid is still processing, delaying the reload.'); delayTask = reloadMermaid; return; } // console.log('reload immediately'); reloadMermaid().catch(console.error); }); this.beforeprintEventSet.add(() => { // Optionally set theme to 'neutral' for printing if required }); loadMermaid(); } initEcharts() { if (!this.config.echarts) { return; } echarts.registerTheme('light', this.config.echarts.lightTheme); echarts.registerTheme('dark', this.config.echarts.darkTheme); this._echartsOnSwitchTheme = this._echartsOnSwitchTheme || (() => { this._echartsArr = this._echartsArr || []; for (let i = 0; i < this._echartsArr.length; i++) { this._echartsArr[i].dispose(); } this._echartsArr = []; const stagingDOM = this.util.getStagingDOM() this.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' }); chart.showLoading(); stagingDOM.stage($dataEl.content.cloneNode(true)); const _setOption = (option) => { if (!option) { chart.hideLoading(); console.warn('ECharts option is missing or invalid. Chart disposed.', { element: $echarts, option: $dataEl, }); chart.dispose(); $echarts.removeAttribute('style'); return; } chart.hideLoading(); chart.setOption(option); this._echartsArr.push(chart); }; // support JS object literal or JS code if ($dataEl.dataset.fmt === 'js') { try { const jsCodes = stagingDOM.contentAsText(); /** * Get ECharts option * @param {Object} fixit FixIt instance * @param {Object} chart ECharts instance * @returns {Object|Promise} ECharts option or Promise */ const _getOption = new Function('fixit', 'chart', this.util.isObjectLiteral(jsCodes) ? `return ${jsCodes}` : jsCodes ); if ($dataEl.dataset.async === 'true') { return Promise.resolve(_getOption(this, chart)).then(option => { _setOption(option); }); } return _setOption(_getOption(this, chart)); } catch (err) { return console.error(err); } } // support JSON _setOption(stagingDOM.contentAsJson()); }); stagingDOM.destroy(); }); this.switchThemeEventSet.add(this._echartsOnSwitchTheme); this._echartsOnSwitchTheme(); this._echartsOnResize = this._echartsOnResize || (() => { for (let i = 0; i < this._echartsArr.length; i++) { this._echartsArr[i].resize(); } }); this.resizeEventSet.add(this._echartsOnResize); } initMapbox() { if (this.config.mapbox) { if (!mapboxgl.accessToken) { mapboxgl.accessToken = this.config.mapbox.accessToken; mapboxgl.setRTLTextPlugin(this.config.mapbox.RTLTextPlugin); this._mapboxArr = this._mapboxArr || []; } this.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, center: [lng, lat], zoom: zoom, minZoom: 0.2, style: this.isDark ? darkStyle : lightStyle, attributionControl: false }); if (marked) { new mapboxgl.Marker().setLngLat([lng, lat]).addTo(mapbox); } const markerArray = typeof markers === 'string' ? JSON.parse(markers) : markers; if (Array.isArray(markerArray) && markerArray.length > 0) { markerArray.forEach(marker => { const { lng: markerLng, lat: markerLat, description } = marker; const popup = new mapboxgl.Popup({ offset: 25 }).setText(description); new mapboxgl.Marker() .setLngLat([markerLng, markerLat]) .setPopup(popup) .addTo(mapbox); }); } if (navigation) { mapbox.addControl(new mapboxgl.NavigationControl(), 'bottom-right'); } if (geolocate) { mapbox.addControl( new mapboxgl.GeolocateControl({ positionOptions: { enableHighAccuracy: true }, showUserLocation: true, trackUserLocation: true }), 'bottom-right' ); } if (scale) { mapbox.addControl(new mapboxgl.ScaleControl()); } if (fullscreen) { mapbox.addControl(new mapboxgl.FullscreenControl()); } mapbox.addControl(new MapboxLanguage()); this._mapboxArr.push(mapbox); }); this._mapboxOnSwitchTheme = this._mapboxOnSwitchTheme || (() => { this.util.forEach(this._mapboxArr, (mapbox) => { const $mapbox = mapbox.getContainer(); const { lightStyle, darkStyle } = JSON.parse($mapbox.dataset.options); mapbox.setStyle(this.isDark ? darkStyle : lightStyle); mapbox.addControl(new MapboxLanguage()); }); }); this.switchThemeEventSet.add(this._mapboxOnSwitchTheme); } } initTypeit(target = document) { if (this.config.typeit) { const typeitConfig = this.config.typeit; const speed = typeitConfig.speed || 100; const cursorSpeed = typeitConfig.cursorSpeed || 1000; const cursorChar = typeitConfig.cursorChar || '|'; const loop = typeitConfig.loop ?? false; // divide them into different groups according to the data-group attribute value of the element // results in an object like {group1: [ele1, ele2], group2: [ele3, ele4]} const typeitElements = target.querySelectorAll('.typeit') const groupMap = Array.from(typeitElements).reduce((acc, ele) => { const group = ele.dataset.group || ele.id || Math.random().toString(36).substring(2); acc[group] = acc[group] || []; acc[group].push(ele); return acc; }, {}); const stagingDOM = this.util.getStagingDOM() Object.values(groupMap).forEach((group) => { const typeone = (i) => { const typeitElement = group[i]; const singleData = typeitElement.dataset; stagingDOM.stage(typeitElement.querySelector('template').content.cloneNode(true)); // for shortcodes usage let targetEle = typeitElement.firstElementChild // for system elements usage if (typeitElement.firstElementChild.tagName === 'TEMPLATE') { typeitElement.innerHTML = ''; targetEle = typeitElement } // create a new instance of TypeIt for each element const instance = new TypeIt(targetEle, { strings: stagingDOM.$el.querySelector('pre')?.innerHTML || stagingDOM.contentAsHtml(), speed: Number(singleData.speed) >= 0 ? Number(singleData.speed) : speed, lifeLike: true, cursorSpeed: Number(singleData.cursorSpeed) >= 0 ? Number(singleData.cursorSpeed) : cursorSpeed, cursorChar: singleData.cursorChar || cursorChar, waitUntilVisible: true, loop: singleData.loop ? singleData.loop === 'true' : loop, afterComplete: () => { const duration = Number(singleData.duration ?? typeitConfig.duration); if (i === group.length - 1) { if (duration >= 0) { window.setTimeout(() => { instance.destroy(); }, duration); } return; } instance.destroy(); typeone(i + 1); } }).go(); }; typeone(0); }); stagingDOM.destroy(); } } initCommentLightGallery(comments, images) { document.querySelectorAll(comments).forEach(($content) => { const $imgs = $content.querySelectorAll(images + ':not([lightgallery-loaded])'); $imgs.forEach(($img) => { $img.setAttribute('lightgallery-loaded', ''); const $link = document.createElement('a'); $link.setAttribute('class', 'comment-lightgallery'); $link.setAttribute('href', $img.src); $link.append($img.cloneNode()); $img.replaceWith($link); }); if ($imgs.length) { lightGallery($content, { selector: '.comment-lightgallery', actualSize: false, hideBarsDelay: 2000, speed: 400 }); } }); } initComment() { if (!this.config.comment?.enable) { return; } // whether to show the view comments button if (document.querySelector('#comments')) { const $viewCommentsBtn = document.querySelector('.view-comments'); $viewCommentsBtn.classList.remove('d-none'); // view comments button click event $viewCommentsBtn.addEventListener('click', () => { this.util.scrollIntoView('#comments'); }, false); } this.config.comment.expired && document.querySelector('#comments').remove(); if (this.config.comment.artalk) { if (this.config.comment.expired) { return Artalk.LoadCountWidget({ server: this.config.comment.artalk.server, site: this.config.comment.artalk.site, pvEl: this.config.comment.artalk.pvEl, countEl: this.config.comment.artalk.countEl }) } const artalk = Artalk.init(this.config.comment.artalk); artalk.setDarkMode(this.isDark); this.switchThemeEventSet.add(() => { artalk.setDarkMode(this.isDark); }); artalk.on('comments-loaded', () => { this.config.comment.artalk.lightgallery && this.initCommentLightGallery('.atk-comment .atk-content', 'img:not([atk-emoticon])'); }); return artalk; } if (this.config.comment.gitalk) { this.config.comment.gitalk.body = decodeURI(window.location.href); const gitalk = new Gitalk(this.config.comment.gitalk); gitalk.render('gitalk'); return gitalk; } if (this.config.comment.valine) { return new Valine(this.config.comment.valine); } if (this.config.comment.waline) { if (this.config.comment.expired) { this.config.comment.waline.pageview && Waline.pageviewCount({ serverURL: this.config.comment.waline.serverURL, path: window.location.pathname }); return; } return Waline.init(this.config.comment.waline); } if (this.config.comment.utterances) { const utterancesConfig = this.config.comment.utterances; const script = document.createElement('script'); script.src = 'https://utteranc.es/client.js'; script.setAttribute('repo', utterancesConfig.repo); script.setAttribute('issue-term', utterancesConfig.issueTerm); if (utterancesConfig.label) script.setAttribute('label', utterancesConfig.label); script.setAttribute('theme', this.isDark ? utterancesConfig.darkTheme : utterancesConfig.lightTheme); script.crossOrigin = 'anonymous'; script.async = true; document.getElementById('utterances').appendChild(script); this._utterancesOnSwitchTheme = this._utterancesOnSwitchTheme || (() => { const message = { type: 'set-theme', theme: this.isDark ? utterancesConfig.darkTheme : utterancesConfig.lightTheme }; document.querySelector('.utterances-frame')?.contentWindow.postMessage(message, 'https://utteranc.es'); }); this.switchThemeEventSet.add(this._utterancesOnSwitchTheme); return; } if (this.config.comment.twikoo) { const twikooConfig = this.config.comment.twikoo; if (twikooConfig.lightgallery) { twikooConfig.onCommentLoaded = () => { this.initCommentLightGallery('.tk-comments .tk-content', 'img:not(.tk-owo-emotion)'); }; } twikoo.init(twikooConfig); if (twikooConfig.commentCount) { // https://twikoo.js.org/api.html#get-comments-count twikoo .getCommentsCount({ envId: twikooConfig.envId, region: twikooConfig.region, urls: [window.location.pathname], includeReply: false }) .then(function (response) { const twikooCommentCount = document.getElementById('twikoo-comment-count'); if (twikooCommentCount) twikooCommentCount.innerHTML = response[0].count; }); } return; } if (this.config.comment.giscus) { const giscusConfig = this.config.comment.giscus; this._giscusOnSwitchTheme = this._giscusOnSwitchTheme || (() => { const message = { setConfig: { theme: this.isDark ? giscusConfig.darkTheme : giscusConfig.lightTheme }}; document.querySelector('.giscus-frame')?.contentWindow.postMessage({ giscus: message }, giscusConfig.origin); }); this.switchThemeEventSet.add(this._giscusOnSwitchTheme); // gicuss to parent message this._messageListener = (event) => { if (event.origin !== giscusConfig.origin) return; const $script = document.querySelector('#giscus>script'); if ($script){ $script.parentElement.removeChild($script); } this._giscusOnSwitchTheme() window.removeEventListener('message', this._messageListener, false); }; window.addEventListener('message', this._messageListener, false); return; } } initCookieconsent() { this.config.cookieconsent && window.cookieconsent?.initialise(this.config.cookieconsent); } getSiteTime = () => { let now = new Date(); let run = new Date(this.config.siteTime); let $runTimes = document.querySelector('.run-times'); if (!this.util.isValidDate(run) || !$runTimes) { clearInterval(this.siteTime); $runTimes && $runTimes.parentNode.remove(); return; } let runTime = (now - run) / 1000, days = Math.floor(runTime / 60 / 60 / 24), hours = Math.floor(runTime / 60 / 60 - 24 * days), minutes = Math.floor(runTime / 60 - 24 * 60 * days - 60 * hours), seconds = Math.floor((now - run) / 1000 - 24 * 60 * 60 * days - 60 * 60 * hours - 60 * minutes); $runTimes.innerHTML = `${days}, ${String(hours).padStart(2, 0)}:${String(minutes).padStart(2, 0)}:${String(seconds).padStart(2, 0)}`; document.querySelector('.site-time .d-none')?.classList.remove('d-none'); }; initSiteTime() { if (this.config.siteTime) { this.siteTime = setInterval(this.getSiteTime, 500); document.addEventListener('visibilitychange', () => { if (document.hidden) { return clearInterval(this.siteTime); } this.siteTime = setInterval(this.getSiteTime, 500); }, false); } } initServiceWorker() { if (this.config.enablePWA && 'serviceWorker' in navigator) { navigator.serviceWorker .register('/service-worker.min.js', { scope: '/' }) .then(function (registration) { // console.log('Service Worker Registered'); }) .catch(function (error) { console.error('error: ', error); }); navigator.serviceWorker .ready .then(function (registration) { // console.log('Service Worker Ready'); }); } } initWatermark() { this.config.watermark?.enable && new Watermark({ content: this.config.watermark.content || `${document.querySelector('footer .fixit-icon')?.outerHTML ?? ''} FixIt Theme`, appendTo: '.widgets', opacity: this.config.watermark.opacity, width: this.config.watermark.width, height: this.config.watermark.height, rowSpacing: this.config.watermark.rowspacing, colSpacing: this.config.watermark.colspacing, rotate: this.config.watermark.rotate, fontSize: this.config.watermark.fontsize, fontFamily: this.config.watermark.fontfamily }); } initPangu() { if (!this.config.pangu?.enable) { return; } const selector = this.config.pangu.selector; if (selector) { // to avoid extra spaces for extended Markdown syntax fraction in Chinese pangu.ignoredTags = /^(script|code|pre|textarea|sup|sub)$/i; if (selector.startsWith('#')) { pangu.spacingElementById(selector.slice(1)); } else if (selector.startsWith('.')) { pangu.spacingElementByClassName(selector.slice(1)); } else { pangu.spacingElementByTagName(selector) } return; } pangu.autoSpacingPage(); } initMathJax() { window.MathJax?.typeset && window.MathJax.typeset(); } initFixItDecryptor() { this.decryptor = new FixItDecryptor({ decrypted: () => { this.initTwemoji(); this.initDetails(); this.initLightGallery(); this.initCodeWrapper(); this.initDiagramCopyBtn(); this.initTable(); this.initMermaid(); this.initEcharts(); this.initTypeit(); this.initMapbox(); this.fixTocScroll(); this.initToc(); this.initTocListener(); this.initPangu(); this.initMathJax(); this.util.forEach(document.querySelectorAll('.encrypted-hidden'), ($element) => { $element.classList.replace('encrypted-hidden', 'decrypted-shown'); }); }, partialDecrypted: ($content) => { this.initTwemoji($content); this.initDetails($content); this.initLightGallery(); this.initCodeWrapper(); this.initDiagramCopyBtn(); this.initTable($content); this.initMermaid(); this.initEcharts(); this.initTypeit($content); this.initMapbox(); this.initPangu(); this.initMathJax(); this.util.forEach($content.querySelectorAll('.encrypted-hidden'), ($element) => { $element.classList.replace('encrypted-hidden', 'decrypted-shown'); }); }, reset: () => { this.util.forEach(document.querySelectorAll('.decrypted-shown'), ($element) => { $element.classList.replace('decrypted-shown', 'encrypted-hidden'); }); } }); this.decryptor.init(this.config.encryption); } initAutoMark() { if (!this.config.autoBookmark) { return; } window.addEventListener('beforeunload', () => { window.sessionStorage?.setItem(`fixit-bookmark/#${location.pathname}`, this.util.getScrollTop()); }); const scrollTop = Number(window.sessionStorage?.getItem(`fixit-bookmark/#${location.pathname}`)); // If the page opens with a specific hash, just jump out if (scrollTop && location.hash === '') { window.scrollTo({ top: scrollTop, behavior: 'smooth' }); } } initReward() { 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) => { $reward.removeAttribute('data-mode'); }); return; } // Close post reward images exclude special id const _closeRewardExclude = (id) => { this.util.forEach($rewards, ($reward) => { const $rewardInput = $reward.parentElement.querySelector('.reward-input'); if ($rewardInput.id !== id) { $rewardInput.checked = false; } }); }; // Add additional click event to reward buttons this.util.forEach($rewards, ($reward) => { $reward.previousElementSibling.addEventListener('click', function () { _closeRewardExclude(this.getAttribute('for')); }, false) }); this.scrollEventSet.add(_closeRewardExclude); } initPostChatUser() { if (!window.postChatUser || !postChatConfig || postChatConfig.userMode === 'magic') { return; } postChat_theme = this.isDark ? 'dark' : 'light'; this.switchThemeEventSet.add((isDark) => { const targetFrame = document.getElementById("postChat_iframeContainer") if (targetFrame) { window.postChatUser.setPostChatTheme(isDark ? 'dark' : 'light'); } else { postChat_theme = isDark ? 'dark' : 'light'; } }); } onScroll() { const $headers = []; const ACCURACY = 20; const $fixedButtons = document.querySelector('.fixed-buttons'); const $backToTop = document.querySelector('.back-to-top'); const $readingProgressBar = document.querySelector('.reading-progress-bar'); if (document.body.dataset.headerDesktop === 'auto') { $headers.push(document.getElementById('header-desktop')); } if (document.body.dataset.headerMobile === 'auto') { $headers.push(document.getElementById('header-mobile')); } // b2t button click event $backToTop?.addEventListener('click', () => { this.util.scrollIntoView('body'); }); window.addEventListener('scroll', (event) => { if (this.disableScrollEvent) { event.preventDefault(); return; } const $mask = document.getElementById('mask'); this.newScrollTop = this.util.getScrollTop(); const scroll = this.newScrollTop - this.oldScrollTop; // header animation this.util.forEach($headers, ($header) => { if (scroll > ACCURACY) { $header.classList.remove('animate__fadeInDown'); this.util.animateCSS($header, ['animate__fadeOutUp'], true); $mask.click(); } else if (scroll < -ACCURACY) { $header.classList.remove('animate__fadeOutUp'); this.util.animateCSS($header, ['animate__fadeInDown'], true); $mask.click(); } }); const contentHeight = document.body.scrollHeight - window.innerHeight; const scrollPercent = Math.max(Math.min(100 * Math.max(this.newScrollTop, 0) / contentHeight, 100), 0); if ($readingProgressBar) { $readingProgressBar.style.setProperty('--progress', `${scrollPercent.toFixed(2)}%`); } // whether to show fixed buttons if ($fixedButtons) { if (scrollPercent > 1) { $fixedButtons.classList.remove('d-none', 'animate__fadeOut'); this.util.animateCSS($fixedButtons, ['animate__fadeIn'], true); } else { $fixedButtons.classList.remove('animate__fadeIn'); this.util.animateCSS($fixedButtons, ['animate__fadeOut'], true, () => { $fixedButtons.classList.contains('animate__fadeOut') && $fixedButtons.classList.add('d-none'); }); } if ($backToTop) { $backToTop.querySelector('span').innerText = `${Math.round(scrollPercent)}%`; } } for (let event of this.scrollEventSet) { event(); } this.oldScrollTop = this.newScrollTop; }, false); } onResize() { let resizeBefore = this.util.isMobile(); window.addEventListener('resize', () => { if (!this._resizeTimeout) { this._resizeTimeout = window.setTimeout(() => { this._resizeTimeout = null; for (let event of this.resizeEventSet) { event(); } this.initToc(); this.initSearch(); const isMobile = this.util.isMobile() if (isMobile !== resizeBefore) { document.getElementById('mask').click(); resizeBefore = isMobile; } }, 100); } }, false); } onClickMask() { document.getElementById('mask').addEventListener('click', () => { if (!document.body.classList.contains('blur')) { return; } for (let event of this.clickMaskEventSet) { event(); } this.disableScrollEvent = false; document.body.classList.remove('blur'); }, false); } beforeprint() { window.addEventListener('beforeprint', () => { this.util.forEach(document.querySelectorAll('.chroma'), ($el) => { $el.classList.toggle('open', true) }); for (let event of this.beforeprintEventSet) { event(); } }, false); } init() { try { if (this.config.encryption) { this.initFixItDecryptor(); } if (!this.config.encryption?.all) { this.initTwemoji(); this.initDetails(); this.initLightGallery(); this.initCodeWrapper(); this.initDiagramCopyBtn(); this.initTable(); this.initMermaid(); this.initEcharts(); this.initTypeit(); this.initMapbox(); this.initPangu(); } this.initThemeColor(); this.initSVGIcon(); this.initMenu(); this.initSwitchTheme(); this.initSearch(); this.initBreadcrumb(); this.initCookieconsent(); this.initSiteTime(); this.initServiceWorker(); this.initWatermark(); this.initAutoMark(); this.initReward(); this.initPostChatUser(); window.setTimeout(() => { this.initComment(); if (!this.config.encryption?.all) { this.fixTocScroll(); this.initToc(); this.initTocListener(); } this.onScroll(); this.onResize(); this.onClickMask(); this.beforeprint(); }, 100); } catch (err) { console.error(err); } console.log( `%c FixIt ${this.config.version} %c https://github.com/hugo-fixit %c`, `background: #FF735A;border:1px solid #FF735A; padding: 1px; border-radius: 2px 0 0 2px; color: #fff;`, `border:1px solid #FF735A; padding: 1px; border-radius: 0 2px 2px 0; color: #FF735A;`, 'background:transparent;' ); } } const themeInit = () => { window.fixit = new FixIt(); window.fixit.init(); }; if (document.readyState !== 'loading') { themeInit(); } else { document.addEventListener('DOMContentLoaded', themeInit, false); }