mirror of
https://github.com/hugo-fixit/FixIt.git
synced 2026-08-25 07:48:54 +00:00
14a965f9c1
* refactor: add TypeScript support and refactor utility functions * docs: add CLAUDE.md for Claude Code guidance Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(assets): replace factory-function pattern with service classes and DI container Introduce typed service container, event bus, and service interfaces to replace the shared FixItContext factory-function pattern. Each module is now a standalone class implementing a typed interface, with explicit constructor-injected dependencies. The window.fixit backward-compatibility facade is built via a publicAPI() helper that extracts class methods. Also removes unnecessary setTimeout/afterPaint deferred execution wrappers from init sequences, and improves module header comments. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor: replace private properties with class fields in modules Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(assets): rewrite link page script in TypeScript Rename pages/link.js to link.ts with proper type annotations and update the Hugo template reference accordingly. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs: rewrite CLAUDE.md and copilot-instructions.md in English Update both files to reflect the new service-class architecture with DI container, typed event bus, and ES6 # private fields. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix: update event handling to use CustomEvent for better type safety * refactor: enhance theme switching by adding isChanged flag to event details * refactor(assets): refactor Mermaid module structure and event wiring * refactor(assets): align lib integrations with TypedEventBus and add module overviews * chore(assets): remove vite-specific import ignore comments * refactor(assets): remove deprecated forEach utility and use native forEach Replace custom forEach wrapper with native .forEach() on NodeList, HTMLCollection (via querySelectorAll), and arrays. The utility was already marked @deprecated and all 30+ call sites use synchronous handlers, making the wrapper unnecessary. Also replace getElementsByClassName/getElementsByTagName with querySelectorAll for consistency. - Delete utils/array.ts (no longer needed) - Remove forEach export from utils/index.ts - Refactor 11 module files to use native .forEach() - Switch getElementsByClassName to querySelectorAll in theme.ts and charts.ts * refactor(assets): split ChartsModule into independent lib files Move ECharts, Mapbox GL, and TypeIt integrations from the ServiceContainer-managed ChartsModule into three standalone lib files (echarts.ts, mapbox.ts, typeit.ts). Each lib: - Instantiates its own TypedEventBus - Reads config from window.config.* - Self-initializes on DOMContentLoaded - Handles decrypt/re-init events independently - Uses implicit idempotency via DOM element queries Key improvements: - Reduces ServiceContainer complexity - Makes chart libs independently loadable - Fixes partial decryption handling in echarts (only re-inits decrypted portion) - Maintains consistency with aplayer/file-tree lib pattern * refactor(assets): extract third-party libraries to standalone lib files Extract twemoji, pangu, cookieconsent, watermark, lightgallery, and json-viewer from modules into separate lib files for better separation of concerns. Each lib is loaded conditionally via assets.html and handles its own initialization and event lifecycle independently. Additionally, unify fixit event handling by replacing document.addEventListener calls with TypedEventBus.on() in EncryptionModule and MiscModule for consistent event architecture. Remove unused MiscService dependency from ContentModule. * refactor(assets): unify js.Build pipeline and defaults - extract shared helper partial function/js-build.html for js.Build + minify - make plugin script renderer and Mermaid use the same js.Build logic - default Build.minify to hugo.IsProduction when unset - default Build.sourceMap to cond hugo.IsDevelopment "linked" "none" - remove duplicated sourceMap branching in layout templates - update js targetPath naming to .js (no explicit .min.js suffix) * refactor(assets): extract comment systems to independent lib files Extract comment system initialization logic from CommentModule into 7 independent lib files (artalk, gitalk, valine, waline, utterances, twikoo, giscus). Each lib: - Conditionally loads based on window.config.comment configuration - Handles backend-specific initialization via DOMContentLoaded - Manages its own theme synchronization and event listeners - Is loaded conditionally in assets.html only when enabled Simplify CommentModule to handle only: - Comment section UI display and visibility toggle - Shared lightGallery initialization utility (used by Artalk and Twikoo) - Comment expiration handling This reduces module complexity and enables per-backend lazy loading. * refactor(assets): unify js.Build pipeline and consolidate PWA config Enhance js-build.html to accept Build as bool/dict, auto-derive targetPath from resource name (.ts → .js), and skip js.Build when Build is false/omit. Simplify assets.html by removing explicit targetPath dicts for all .ts lib builds. Consolidate enablePWA and serviceWorkerURL into a PWA config object. * refactor(assets): decouple toc from content module and add JSDoc headers - Remove TocService dependency from ContentModule, toc now self-manages - Rename initContent() → setup(), init() → setup() for toc - Make initFootnotes private (#initFootnotes) - Add multi-line JSDoc headers with Responsibilities to all modules, main.ts, pages/link.ts, and head/color-scheme.ts * refactor(assets): split types.ts into types/ directory module * refactor(assets): simplify JS module architecture and expose typed window.fixit API - Remove DI container (core/container.ts), use direct constructor calls - Export shared eventBus singleton from core/event-bus.ts, remove 16 redundant instances - Merge thin modules: SvgModule/CommentModule/LinkGuardModule into ContentModule/MiscModule - Trim service interfaces to externally-used methods only - Replace publicAPI() reflection with minimal typed window.fixit facade - Expose public APIs: setThemeMode, eventBus, initContent, mask overlay, scroll state - Extract setThemeMode from initSwitchTheme closure as public method - Rename fixit:reset to fixit:re-encrypt, remove dead events - Replace custom.js.example with custom.ts.example - Update CLAUDE.md and copilot-instructions.md * style(i18n): use semantic names for comment dividers instead of file paths Replace file-path-based comment dividers (e.g. partials/base/header.html) with semantic names (e.g. Header) across all 16 i18n files for consistency. * chore(workflow): update cron schedule for library updates --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
419 lines
16 KiB
TypeScript
419 lines
16 KiB
TypeScript
import type { CodeService } from '../core/tokens'
|
|
import { eventBus } from '../core/event-bus'
|
|
import { animateCSS, createCopyText, downloadAsFile, flashCopiedTooltip, getStagingDOM } from '../utils'
|
|
|
|
/**
|
|
* Code module — code block interactions: copy, download, fullscreen, tabs, and line numbers.
|
|
*
|
|
* Responsibilities:
|
|
* - Wrap code blocks with action buttons (copy, download, fullscreen, line numbers).
|
|
* - Initialize code tab groups and sync selection across related blocks.
|
|
* - Copy diagram source (Mermaid, ECharts) from code blocks.
|
|
*/
|
|
const CellTooltip = window.CellTooltip
|
|
const copyText = createCopyText()
|
|
|
|
export class CodeModule implements CodeService {
|
|
#fullscreenAbort: AbortController | undefined
|
|
|
|
constructor() {}
|
|
|
|
/**
|
|
* Attach copy-to-clipboard behaviour to a code block.
|
|
* @param codeBlock - The `.code-block` container element.
|
|
* @param codePreEl - The `<pre>` element containing the code text.
|
|
*/
|
|
initCopyCode(codeBlock: HTMLElement, codePreEl: HTMLElement) {
|
|
const copyBtn = codeBlock.dataset.mode === 'classic'
|
|
? codeBlock.querySelector<HTMLElement>('.code-header .copy-btn')
|
|
: codeBlock.querySelector<HTMLElement>('.copy-icon-btn')
|
|
if (codeBlock.dataset.copyable !== 'true' || !copyBtn)
|
|
return
|
|
copyBtn.addEventListener('click', () => {
|
|
const iswWrap = codeBlock.classList.contains('line-wrapping')
|
|
const highlightLines = codeBlock.querySelectorAll('.hl')
|
|
iswWrap && codeBlock.classList.toggle('line-wrapping')
|
|
highlightLines.forEach(($hl) => {
|
|
$hl.classList.toggle('hl')
|
|
})
|
|
copyText(codePreEl.textContent!.trim()).then(() => {
|
|
animateCSS(codePreEl, 'animate__flash')
|
|
iswWrap && codeBlock.classList.toggle('line-wrapping')
|
|
highlightLines.forEach(($hl) => {
|
|
$hl.classList.toggle('hl')
|
|
})
|
|
flashCopiedTooltip(copyBtn)
|
|
}, () => {
|
|
console.error('Clipboard write failed!', 'Your browser does not support clipboard API!')
|
|
})
|
|
}, false)
|
|
}
|
|
|
|
/**
|
|
* Attach toggle behaviour to the code expand/collapse button.
|
|
* @param codeBlock - The `.code-block` container element.
|
|
*/
|
|
initCodeExpandBtn(codeBlock: HTMLElement) {
|
|
codeBlock.querySelector('.code-expand-btn')?.addEventListener('click', () => {
|
|
codeBlock.classList.toggle('is-expanded')
|
|
}, false)
|
|
}
|
|
|
|
/**
|
|
* Attach download behaviour to a code block's download button.
|
|
* @param codeBlock - The `.code-block` container element.
|
|
* @param codePreEl - The `<pre>` element containing the code text.
|
|
*/
|
|
initDownloadCode(codeBlock: HTMLElement, codePreEl: HTMLElement) {
|
|
const downloadBtn = codeBlock.querySelector<HTMLElement>('.code-header .download-btn')
|
|
if (!downloadBtn)
|
|
return
|
|
downloadBtn.addEventListener('click', () => {
|
|
const $codeHeader = codeBlock.querySelector<HTMLElement>('.code-header')
|
|
const name = codeBlock.dataset.name?.trim()
|
|
const language = Array.from($codeHeader?.classList || []).find(className => className.startsWith('language-'))?.replace('language-', '')
|
|
const ext = language && language !== 'fallback' ? language : 'txt'
|
|
const fallbackName = name
|
|
? (name.includes('.') ? name : `${name}.${ext}`)
|
|
: `code.${ext}`
|
|
const fileName = codeBlock.getAttribute('filename')?.trim()
|
|
downloadAsFile(codePreEl.textContent!, fileName || fallbackName)
|
|
downloadBtn.toggleAttribute('data-downloaded', true)
|
|
downloadBtn.classList.toggle('fa-spin', true)
|
|
setTimeout(() => {
|
|
downloadBtn.toggleAttribute('data-downloaded', false)
|
|
downloadBtn.classList.toggle('fa-spin', false)
|
|
}, 300)
|
|
}, false)
|
|
}
|
|
|
|
/**
|
|
* Get the fullscreen target element (parent `.code-tabs` or the block itself).
|
|
* @param codeBlock - The `.code-block` element.
|
|
* @returns The element to apply fullscreen to.
|
|
*/
|
|
#getCodeFullscreenTarget(codeBlock: HTMLElement): HTMLElement {
|
|
return (codeBlock.closest('.code-tabs') as HTMLElement) || codeBlock
|
|
}
|
|
|
|
/**
|
|
* Toggle fullscreen state and update button tooltips.
|
|
* @param codeBlock - The `.code-block` element.
|
|
* @param show - `true` to enter fullscreen, `false` to exit.
|
|
*/
|
|
#setCodeFullscreenState(codeBlock: HTMLElement, show: boolean) {
|
|
const target = this.#getCodeFullscreenTarget(codeBlock)
|
|
const expandBtn = codeBlock.querySelector<HTMLElement>('.code-expand-btn')
|
|
|
|
if (show && expandBtn) {
|
|
codeBlock.dataset.fullscreenExpanded = codeBlock.classList.contains('is-expanded') ? 'true' : 'false'
|
|
codeBlock.classList.add('is-expanded')
|
|
}
|
|
|
|
if (!show && target.classList.contains('is-fullscreen')) {
|
|
target.classList.add('instant-height')
|
|
window.requestAnimationFrame(() => target.classList.remove('instant-height'))
|
|
|
|
if (expandBtn && codeBlock.dataset.fullscreenExpanded === 'false') {
|
|
codeBlock.classList.remove('is-expanded')
|
|
}
|
|
delete codeBlock.dataset.fullscreenExpanded
|
|
}
|
|
|
|
// update button tooltip
|
|
target.classList.toggle('is-fullscreen', show)
|
|
const btn = target.querySelector<HTMLElement>('.tabs-actions .fullscreen-btn')
|
|
|| codeBlock.querySelector<HTMLElement>('.code-header .fullscreen-btn')
|
|
if (!btn)
|
|
return
|
|
const exitTitle = btn.dataset.exitTitle || btn.getAttribute('data-exit-title') || btn.title
|
|
const originalTitle = btn.dataset.ctOriginalTitle || btn.dataset.ctTitle || btn.title
|
|
btn.dataset.ctOriginalTitle = originalTitle
|
|
btn.dataset.ctTitle = show ? exitTitle : originalTitle
|
|
const instance = CellTooltip.getOrCreateInstance(btn)
|
|
instance.hide()
|
|
}
|
|
|
|
/** Exit fullscreen on the currently active code block. */
|
|
closeCodeFullscreen() {
|
|
const $activeTabs = document.querySelector<HTMLElement>('.code-tabs.is-fullscreen')
|
|
if ($activeTabs) {
|
|
const $activeBlock = $activeTabs.querySelector<HTMLElement>('.code-block.active') || $activeTabs.querySelector<HTMLElement>('.code-block')
|
|
if ($activeBlock)
|
|
this.#setCodeFullscreenState($activeBlock, false)
|
|
return
|
|
}
|
|
const $activeBlock = document.querySelector<HTMLElement>('.code-block.highlight.is-fullscreen')
|
|
if ($activeBlock)
|
|
this.#setCodeFullscreenState($activeBlock, false)
|
|
}
|
|
|
|
/**
|
|
* Attach fullscreen toggle and Escape-key handler to a code block.
|
|
* @param codeBlock - The `.code-block` container element.
|
|
*/
|
|
initFullscreenCode(codeBlock: HTMLElement) {
|
|
const fullscreenBtn = codeBlock.querySelector<HTMLElement>('.code-header .fullscreen-btn')
|
|
if (!fullscreenBtn)
|
|
return
|
|
fullscreenBtn.addEventListener('click', () => {
|
|
const target = this.#getCodeFullscreenTarget(codeBlock)
|
|
const show = !target.classList.contains('is-fullscreen')
|
|
if (show) {
|
|
this.closeCodeFullscreen()
|
|
codeBlock.classList.remove('is-collapsed')
|
|
}
|
|
this.#setCodeFullscreenState(codeBlock, show)
|
|
}, false)
|
|
if (!this.#fullscreenAbort) {
|
|
this.#fullscreenAbort = new AbortController()
|
|
document.addEventListener('keydown', (event) => {
|
|
if (event.key === 'Escape')
|
|
this.closeCodeFullscreen()
|
|
}, { signal: this.#fullscreenAbort.signal })
|
|
}
|
|
}
|
|
|
|
/** Initialize all un-initialized code blocks on the page. */
|
|
initCodeWrapper() {
|
|
const $codeBlocks = document.querySelectorAll<HTMLElement>('.code-block.highlight:not([data-init])')
|
|
$codeBlocks.forEach(($codeBlock) => {
|
|
const $preElements = $codeBlock.querySelectorAll<HTMLPreElement>('pre.chroma')
|
|
if (!$preElements.length)
|
|
return
|
|
const $codePreEl = $preElements[$preElements.length - 1]
|
|
$codeBlock.dataset.init = 'true'
|
|
|
|
this.initCopyCode($codeBlock, $codePreEl)
|
|
this.initCodeExpandBtn($codeBlock)
|
|
|
|
// classic mode code block interactions
|
|
if ($codeBlock.dataset.mode === 'classic') {
|
|
const $codeHeader = $codeBlock.querySelector<HTMLElement>('.code-header')
|
|
if (!$codeHeader)
|
|
return
|
|
this.initDownloadCode($codeBlock, $codePreEl)
|
|
this.initFullscreenCode($codeBlock)
|
|
// code title
|
|
$codeHeader.querySelector<HTMLElement>('.code-title')!.addEventListener('click', () => {
|
|
if ($codeBlock.classList.contains('is-fullscreen'))
|
|
return
|
|
$codeBlock.classList.toggle('is-collapsed')
|
|
}, false)
|
|
// ellipses icon
|
|
$codeHeader.querySelector<HTMLElement>('.ellipses-btn')!.addEventListener('click', () => {
|
|
$codeBlock.classList.remove('is-collapsed')
|
|
}, false)
|
|
// line numbers toggle button
|
|
$codeHeader.querySelector('.line-nos-btn')?.addEventListener('click', () => {
|
|
$codeBlock.classList.toggle('line-nos-hidden')
|
|
}, false)
|
|
// line wrapping toggle button
|
|
$codeHeader.querySelector('.line-wrap-btn')?.addEventListener('click', () => {
|
|
if ($codeBlock.querySelector('[contenteditable="true"]'))
|
|
return
|
|
$codeBlock.classList.toggle('line-wrapping')
|
|
}, false)
|
|
// edit button toggle button
|
|
if ($codeBlock.dataset.editable === 'true') {
|
|
$codeHeader.querySelector('.edit-btn')?.addEventListener('click', () => {
|
|
const isEditable = $codePreEl.getAttribute('contenteditable') === 'true'
|
|
if (isEditable) {
|
|
$codePreEl.setAttribute('contenteditable', 'false')
|
|
$codePreEl.blur()
|
|
}
|
|
else {
|
|
$codeBlock.querySelectorAll('.hl').forEach(($hl: Element) => {
|
|
$hl.classList.remove('hl')
|
|
})
|
|
$codeBlock.classList.add('is-expanded')
|
|
$codeBlock.classList.remove('line-wrapping')
|
|
$codePreEl.setAttribute('contenteditable', 'true')
|
|
$codePreEl.focus()
|
|
}
|
|
}, false)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
/** Group consecutive code blocks into tabbed containers with language sync. */
|
|
initCodeTabs() {
|
|
const $codeBlocks = document.querySelectorAll<HTMLElement>('.code-block[group]:not([data-tab-init])')
|
|
const processed = new Set<HTMLElement>()
|
|
const normalizeTabTitle = (title = '') => title.toLowerCase()
|
|
|
|
$codeBlocks.forEach(($block) => {
|
|
if (processed.has($block))
|
|
return
|
|
|
|
const groupName = $block.getAttribute('group')!
|
|
const $tabs: HTMLElement[] = []
|
|
let $curr: HTMLElement | null = $block
|
|
|
|
// collect consecutive blocks with same group
|
|
while ($curr && $curr.classList?.contains('code-block') && $curr.getAttribute('group') === groupName) {
|
|
$tabs.push($curr)
|
|
processed.add($curr)
|
|
$curr = $curr.nextElementSibling as HTMLElement
|
|
}
|
|
|
|
if ($tabs.length < 2)
|
|
return
|
|
|
|
// create DOM structure
|
|
const $container = document.createElement('div')
|
|
$container.className = 'code-tabs'
|
|
|
|
const $header = document.createElement('div')
|
|
$header.className = 'tabs-header'
|
|
|
|
const $items = document.createElement('div')
|
|
$items.className = 'tabs-items'
|
|
|
|
const $actions = document.createElement('div')
|
|
$actions.className = 'tabs-actions'
|
|
|
|
$header.appendChild($items)
|
|
$header.appendChild($actions)
|
|
|
|
const $content = document.createElement('div')
|
|
$content.className = 'tabs-content'
|
|
|
|
// insert container before the first block
|
|
const $firstBlock = $tabs[0]
|
|
$firstBlock.parentNode!.insertBefore($container, $firstBlock)
|
|
|
|
const activeTabIndex = $tabs.findIndex(tab => tab.classList.contains('active'))
|
|
const langPref = window.localStorage.getItem('config_lang_perf')
|
|
const hasCodeToggle = $tabs.some(tab => tab.dataset.codeToggle === 'true')
|
|
const langPrefIndex = (langPref && hasCodeToggle) ? $tabs.findIndex(tab => tab.dataset.tabTitle!.toLowerCase() === langPref) : -1
|
|
const resolvedIndex = langPrefIndex !== -1 ? langPrefIndex : activeTabIndex
|
|
const beforeTabs = $tabs[0]?.getAttribute('before_tabs')
|
|
if (beforeTabs) {
|
|
const $before = document.createElement('span')
|
|
$before.className = 'before-tabs'
|
|
$before.textContent = beforeTabs
|
|
$items.appendChild($before)
|
|
}
|
|
|
|
const tabButtons: HTMLElement[] = []
|
|
const toggleLangToIndex = new Map<string, number>()
|
|
|
|
const switchToTab = (index: number) => {
|
|
const $nextTab = $tabs[index]
|
|
const $nextBtn = tabButtons[index]
|
|
if (!$nextTab || !$nextBtn)
|
|
return
|
|
|
|
// 1. restore buttons to the currently active tab
|
|
const $activeTab = $tabs.find(t => t.classList.contains('active'))
|
|
if ($activeTab) {
|
|
const $activeHeader = $activeTab.querySelector<HTMLElement>('.code-header')
|
|
if ($activeHeader) {
|
|
Array.from($actions.children).forEach(btn => $activeHeader.appendChild(btn))
|
|
}
|
|
}
|
|
|
|
// 2. switch active tab UI
|
|
tabButtons.forEach(b => b.classList.remove('active'))
|
|
$nextBtn.classList.add('active')
|
|
|
|
// 3. switch content
|
|
$tabs.forEach(b => b.classList.remove('active'))
|
|
$nextTab.classList.add('active')
|
|
|
|
// 4. sync shadow mode data attribute
|
|
const shadowMode = $nextTab?.dataset.shadow
|
|
if (shadowMode) {
|
|
$container.dataset.shadow = shadowMode
|
|
}
|
|
else {
|
|
delete $container.dataset.shadow
|
|
}
|
|
|
|
// 5. move new buttons to actions
|
|
const $codeHeader = $nextTab.querySelector<HTMLElement>('.code-header')
|
|
if ($codeHeader) {
|
|
$codeHeader.querySelectorAll<HTMLElement>('.action-btn').forEach(btn => $actions.appendChild(btn))
|
|
}
|
|
}
|
|
|
|
eventBus.on('fixit:code-tab-sync', ({ detail }) => {
|
|
if (!detail.lang || detail.source === $container)
|
|
return
|
|
const index = toggleLangToIndex.get(detail.lang)
|
|
index !== undefined && switchToTab(index)
|
|
})
|
|
|
|
$tabs.forEach(($tab, index) => {
|
|
const title = $tab.dataset.tabTitle || 'Code'
|
|
const defaultActiveTab = resolvedIndex === -1 && index === 0
|
|
|
|
// tab button
|
|
const $btn = document.createElement('span')
|
|
$btn.className = 'tab-item'
|
|
if (defaultActiveTab)
|
|
$btn.classList.add('active')
|
|
$btn.textContent = title
|
|
$btn.dataset.index = String(index)
|
|
$btn.title = title
|
|
tabButtons.push($btn)
|
|
|
|
const normalizedTitle = normalizeTabTitle(title)
|
|
if (!toggleLangToIndex.has(normalizedTitle)) {
|
|
toggleLangToIndex.set(normalizedTitle, index)
|
|
}
|
|
|
|
$btn.addEventListener('click', () => {
|
|
if ($tab.dataset.codeToggle === 'true') {
|
|
window.localStorage.setItem('config_lang_perf', normalizedTitle)
|
|
eventBus.emit('fixit:code-tab-sync', { lang: normalizedTitle, source: $container })
|
|
}
|
|
switchToTab(index)
|
|
})
|
|
$items.appendChild($btn)
|
|
|
|
// move block to content
|
|
$tab.classList.toggle('active', resolvedIndex === index || defaultActiveTab)
|
|
$tab.classList.remove('is-collapsed')
|
|
$tab.classList.remove('d-none')
|
|
$tab.dataset.tabInit = 'true'
|
|
$content.appendChild($tab)
|
|
})
|
|
|
|
$container.appendChild($header)
|
|
$container.appendChild($content)
|
|
|
|
// initialize actions for the active tab
|
|
if (resolvedIndex !== -1) {
|
|
switchToTab(resolvedIndex)
|
|
}
|
|
else {
|
|
switchToTab(0)
|
|
}
|
|
})
|
|
}
|
|
|
|
/** Attach copy behaviour to diagram container copy buttons. */
|
|
initDiagramCopyBtn() {
|
|
const stagingDOM = getStagingDOM()
|
|
document.querySelectorAll<HTMLElement>('.diagram-container > .copy-icon-btn').forEach(($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 { /* ignore */ }
|
|
copyText(code).then(() => {
|
|
flashCopiedTooltip($btn as HTMLElement)
|
|
}, () => {
|
|
console.error('Clipboard write failed!', 'Your browser does not support clipboard API!')
|
|
})
|
|
}, false)
|
|
})
|
|
stagingDOM.destroy()
|
|
}
|
|
}
|