@@ -30,3 +30,144 @@ document.addEventListener("click", (e) => {
3030 }
3131 }
3232} ) ;
33+
34+ function initCodeCopyButtons ( ) {
35+ const resetTimers = new WeakMap ( ) ;
36+
37+ function codeText ( source ) {
38+ const copy = source . cloneNode ( true ) ;
39+
40+ copy . querySelectorAll ( "[data-copy-ignore]" ) . forEach ( ( element ) => element . remove ( ) ) ;
41+ if ( source . matches ( ".language-shell" ) ) {
42+ copy . querySelectorAll ( 'span[style*="user-select: none"]' ) . forEach ( ( prompt ) => prompt . remove ( ) ) ;
43+ }
44+
45+ return copy . textContent . replace ( / \r ? \n $ / , "" ) ;
46+ }
47+
48+ function fallbackCopy ( text ) {
49+ const textarea = document . createElement ( "textarea" ) ;
50+ const activeElement = document . activeElement ;
51+
52+ textarea . value = text ;
53+ textarea . readOnly = true ;
54+ textarea . setAttribute ( "aria-hidden" , "true" ) ;
55+ textarea . style . position = "fixed" ;
56+ textarea . style . opacity = "0" ;
57+ textarea . style . pointerEvents = "none" ;
58+ document . body . append ( textarea ) ;
59+ textarea . select ( ) ;
60+ textarea . setSelectionRange ( 0 , text . length ) ;
61+
62+ let copied = false ;
63+ try {
64+ copied = document . execCommand ( "copy" ) ;
65+ } finally {
66+ textarea . remove ( ) ;
67+ if ( activeElement instanceof HTMLElement ) {
68+ activeElement . focus ( { preventScroll : true } ) ;
69+ }
70+ }
71+
72+ if ( ! copied ) throw new Error ( "The browser refused the copy command." ) ;
73+ }
74+
75+ async function copyText ( text ) {
76+ if ( globalThis . isSecureContext && navigator . clipboard ?. writeText ) {
77+ try {
78+ await navigator . clipboard . writeText ( text ) ;
79+ return ;
80+ } catch {
81+ // The fallback also works when clipboard permission has been denied.
82+ }
83+ }
84+
85+ fallbackCopy ( text ) ;
86+ }
87+
88+ function makeCopyControl ( getSource ) {
89+ const button = document . createElement ( "button" ) ;
90+ const status = document . createElement ( "span" ) ;
91+
92+ button . type = "button" ;
93+ button . className = "code-copy-button" ;
94+ button . setAttribute ( "aria-label" , "Copy code to clipboard" ) ;
95+ button . dataset . copyState = "idle" ;
96+ button . innerHTML = `
97+ <svg class="code-copy-icon" viewBox="0 0 16 16" aria-hidden="true">
98+ <path class="code-copy-icon-copy" d="M5.5 4.5v-2h8v8h-2m-9-5h8v8h-8z" />
99+ <path class="code-copy-icon-check" d="m3 8.5 3 3 7-7" />
100+ </svg>
101+ <span class="code-copy-label">Copy</span>
102+ ` ;
103+
104+ status . className = "code-copy-status" ;
105+ status . setAttribute ( "role" , "status" ) ;
106+ status . setAttribute ( "aria-live" , "polite" ) ;
107+
108+ button . addEventListener ( "click" , async ( ) => {
109+ const label = button . querySelector ( ".code-copy-label" ) ;
110+ const source = getSource ( ) ;
111+ if ( ! label || ! source ) return ;
112+
113+ const oldTimer = resetTimers . get ( button ) ;
114+ if ( oldTimer ) clearTimeout ( oldTimer ) ;
115+ button . dataset . copyState = "idle" ;
116+ label . textContent = "Copy" ;
117+ status . textContent = "" ;
118+
119+ try {
120+ await copyText ( codeText ( source ) ) ;
121+ button . dataset . copyState = "success" ;
122+ label . textContent = "Copied" ;
123+ status . textContent = "Code copied to clipboard." ;
124+ } catch {
125+ button . dataset . copyState = "error" ;
126+ label . textContent = "Try again" ;
127+ status . textContent = "Could not copy code to the clipboard." ;
128+ }
129+
130+ resetTimers . set (
131+ button ,
132+ setTimeout ( ( ) => {
133+ button . dataset . copyState = "idle" ;
134+ label . textContent = "Copy" ;
135+ status . textContent = "" ;
136+ } , 2000 ) ,
137+ ) ;
138+ } ) ;
139+
140+ return { button, status } ;
141+ }
142+
143+ const codeGroups = new Set ( document . querySelectorAll ( ".content .code-group" ) ) ;
144+ for ( const group of codeGroups ) {
145+ if ( group . classList . contains ( "code-copy-group" ) ) continue ;
146+
147+ const { button, status } = makeCopyControl ( ( ) => {
148+ const activeBlock = [ ...group . querySelectorAll ( ":scope > pre" ) ] . find ( ( pre ) => ! pre . hidden ) ;
149+ return activeBlock ?. querySelector ( ":scope > code" ) || activeBlock ;
150+ } ) ;
151+
152+ group . classList . add ( "code-copy-group" ) ;
153+ group . append ( button , status ) ;
154+ }
155+
156+ for ( const pre of document . querySelectorAll ( ".content pre" ) ) {
157+ if ( pre . closest ( ".code-group" ) || pre . closest ( ".code-copy-wrap" ) ) continue ;
158+
159+ const source = pre . querySelector ( ":scope > code" ) || pre ;
160+ const wrapper = document . createElement ( "div" ) ;
161+ const { button, status } = makeCopyControl ( ( ) => source ) ;
162+
163+ wrapper . className = "code-copy-wrap" ;
164+ pre . before ( wrapper ) ;
165+ wrapper . append ( pre , button , status ) ;
166+ }
167+ }
168+
169+ if ( document . readyState === "loading" ) {
170+ document . addEventListener ( "DOMContentLoaded" , initCodeCopyButtons , { once : true } ) ;
171+ } else {
172+ initCodeCopyButtons ( ) ;
173+ }
0 commit comments