From cf9eac62620f1e38eef044607a93f9dd26d91c3a Mon Sep 17 00:00:00 2001 From: William Laugesen Date: Thu, 6 Aug 2026 08:58:54 +1200 Subject: [PATCH] Extract the clipboard write shared by the copy features copy-markdown.js carries a secure-context check plus an execCommand fallback for browsers where navigator.clipboard is unavailable. The heading copy button needs the same logic, so it moves to its own module rather than becoming a second copy. Behavior is unchanged: the same two code paths in the same order, with the warning prefix generalized from [copy-md] to [clipboard]. code-blocks.js still has its own simpler inline version. It is bound up in a larger icon-swapping flow, so it is left alone. Co-Authored-By: Claude Opus 5 (1M context) --- src/scripts/modules/clipboard.js | 57 ++++++++++++++++++++++++++++ src/scripts/modules/copy-markdown.js | 46 +--------------------- 2 files changed, 59 insertions(+), 44 deletions(-) create mode 100644 src/scripts/modules/clipboard.js diff --git a/src/scripts/modules/clipboard.js b/src/scripts/modules/clipboard.js new file mode 100644 index 0000000000..e4ae21c82c --- /dev/null +++ b/src/scripts/modules/clipboard.js @@ -0,0 +1,57 @@ +// @ts-check + +/** + * Copies text to the clipboard, reporting whether it worked. + * + * navigator.clipboard needs a secure context, so this falls back to the + * deprecated execCommand path for HTTP and older browsers. + * + * @param {string} text + * @returns {Promise} + */ +async function writeToClipboard(text) { + if ( + typeof navigator !== 'undefined' && + navigator.clipboard && + typeof navigator.clipboard.writeText === 'function' && + (typeof window === 'undefined' || window.isSecureContext !== false) + ) { + try { + await navigator.clipboard.writeText(text); + return true; + } catch (err) { + console.warn('[clipboard] navigator.clipboard failed, falling back', err); + } + } + + return execCommandCopyFallback(text); +} + +/** + * @param {string} text + * @returns {boolean} + */ +function execCommandCopyFallback(text) { + if (typeof document === 'undefined') return false; + const ta = document.createElement('textarea'); + ta.value = text; + ta.setAttribute('readonly', ''); + ta.style.position = 'fixed'; + ta.style.top = '0'; + ta.style.left = '0'; + ta.style.opacity = '0'; + ta.style.pointerEvents = 'none'; + document.body.appendChild(ta); + ta.select(); + let ok = false; + try { + ok = document.execCommand('copy'); + } catch (err) { + console.warn('[clipboard] execCommand fallback threw', err); + ok = false; + } + document.body.removeChild(ta); + return ok; +} + +export { writeToClipboard }; diff --git a/src/scripts/modules/copy-markdown.js b/src/scripts/modules/copy-markdown.js index 271ed70573..3428c35b95 100644 --- a/src/scripts/modules/copy-markdown.js +++ b/src/scripts/modules/copy-markdown.js @@ -1,5 +1,6 @@ // @ts-check import { qs, qsa } from './query.js'; +import { writeToClipboard } from './clipboard.js'; class CopyMarkdown { constructor(menu) { @@ -29,49 +30,6 @@ class CopyMarkdown { } } - // navigator.clipboard requires a secure context; falls back to - // execCommand for HTTP and older browsers. - async writeToClipboard(text) { - if ( - typeof navigator !== 'undefined' && - navigator.clipboard && - typeof navigator.clipboard.writeText === 'function' && - (typeof window === 'undefined' || window.isSecureContext !== false) - ) { - try { - await navigator.clipboard.writeText(text); - return true; - } catch (err) { - console.warn('[copy-md] navigator.clipboard failed, falling back', err); - } - } - - return this.execCommandCopyFallback(text); - } - - execCommandCopyFallback(text) { - if (typeof document === 'undefined') return false; - const ta = document.createElement('textarea'); - ta.value = text; - ta.setAttribute('readonly', ''); - ta.style.position = 'fixed'; - ta.style.top = '0'; - ta.style.left = '0'; - ta.style.opacity = '0'; - ta.style.pointerEvents = 'none'; - document.body.appendChild(ta); - ta.select(); - let ok = false; - try { - ok = document.execCommand('copy'); - } catch (err) { - console.warn('[copy-md] execCommand fallback threw', err); - ok = false; - } - document.body.removeChild(ta); - return ok; - } - async handleCopy(btn) { const url = btn.dataset.copyMdUrl; const success = btn.dataset.copyMdSuccess ?? ''; @@ -81,7 +39,7 @@ class CopyMarkdown { const res = await fetch(url); if (!res.ok) throw new Error('HTTP ' + res.status); const text = await res.text(); - const ok = await this.writeToClipboard(text); + const ok = await writeToClipboard(text); if (!ok) throw new Error('clipboard-write-failed'); this.announce(btn, success); } catch (err) {