copybutton.js
1 // Localization support 2 const messages = { 3 'en': { 4 'copy': 'Copy', 5 'copy_to_clipboard': 'Copy to clipboard', 6 'copy_success': 'Copied!', 7 'copy_failure': 'Failed to copy', 8 }, 9 'es' : { 10 'copy': 'Copiar', 11 'copy_to_clipboard': 'Copiar al portapapeles', 12 'copy_success': '¡Copiado!', 13 'copy_failure': 'Error al copiar', 14 }, 15 'de' : { 16 'copy': 'Kopieren', 17 'copy_to_clipboard': 'In die Zwischenablage kopieren', 18 'copy_success': 'Kopiert!', 19 'copy_failure': 'Fehler beim Kopieren', 20 }, 21 'fr' : { 22 'copy': 'Copier', 23 'copy_to_clipboard': 'Copier dans le presse-papier', 24 'copy_success': 'Copié !', 25 'copy_failure': 'Échec de la copie', 26 }, 27 'ru': { 28 'copy': 'Скопировать', 29 'copy_to_clipboard': 'Скопировать в буфер', 30 'copy_success': 'Скопировано!', 31 'copy_failure': 'Не удалось скопировать', 32 }, 33 'zh-CN': { 34 'copy': '复制', 35 'copy_to_clipboard': '复制到剪贴板', 36 'copy_success': '复制成功!', 37 'copy_failure': '复制失败', 38 }, 39 'it' : { 40 'copy': 'Copiare', 41 'copy_to_clipboard': 'Copiato negli appunti', 42 'copy_success': 'Copiato!', 43 'copy_failure': 'Errore durante la copia', 44 } 45 } 46 47 let locale = 'en' 48 if( document.documentElement.lang !== undefined 49 && messages[document.documentElement.lang] !== undefined ) { 50 locale = document.documentElement.lang 51 } 52 53 let doc_url_root = DOCUMENTATION_OPTIONS.URL_ROOT; 54 if (doc_url_root == '#') { 55 doc_url_root = ''; 56 } 57 58 /** 59 * SVG files for our copy buttons 60 */ 61 let iconCheck = `<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-check" width="44" height="44" viewBox="0 0 24 24" stroke-width="2" stroke="#22863a" fill="none" stroke-linecap="round" stroke-linejoin="round"> 62 <title>${messages[locale]['copy_success']}</title> 63 <path stroke="none" d="M0 0h24v24H0z" fill="none"/> 64 <path d="M5 12l5 5l10 -10" /> 65 </svg>` 66 67 // If the user specified their own SVG use that, otherwise use the default 68 let iconCopy = ``; 69 if (!iconCopy) { 70 iconCopy = `<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-copy" width="44" height="44" viewBox="0 0 24 24" stroke-width="1.5" stroke="#000000" fill="none" stroke-linecap="round" stroke-linejoin="round"> 71 <title>${messages[locale]['copy_to_clipboard']}</title> 72 <path stroke="none" d="M0 0h24v24H0z" fill="none"/> 73 <rect x="8" y="8" width="12" height="12" rx="2" /> 74 <path d="M16 8v-2a2 2 0 0 0 -2 -2h-8a2 2 0 0 0 -2 2v8a2 2 0 0 0 2 2h2" /> 75 </svg>` 76 } 77 78 /** 79 * Set up copy/paste for code blocks 80 */ 81 82 const runWhenDOMLoaded = cb => { 83 if (document.readyState != 'loading') { 84 cb() 85 } else if (document.addEventListener) { 86 document.addEventListener('DOMContentLoaded', cb) 87 } else { 88 document.attachEvent('onreadystatechange', function() { 89 if (document.readyState == 'complete') cb() 90 }) 91 } 92 } 93 94 const codeCellId = index => `codecell${index}` 95 96 // Clears selected text since ClipboardJS will select the text when copying 97 const clearSelection = () => { 98 if (window.getSelection) { 99 window.getSelection().removeAllRanges() 100 } else if (document.selection) { 101 document.selection.empty() 102 } 103 } 104 105 // Changes tooltip text for a moment, then changes it back 106 // We want the timeout of our `success` class to be a bit shorter than the 107 // tooltip and icon change, so that we can hide the icon before changing back. 108 var timeoutIcon = 2000; 109 var timeoutSuccessClass = 1500; 110 111 const temporarilyChangeTooltip = (el, oldText, newText) => { 112 el.setAttribute('data-tooltip', newText) 113 el.classList.add('success') 114 // Remove success a little bit sooner than we change the tooltip 115 // So that we can use CSS to hide the copybutton first 116 setTimeout(() => el.classList.remove('success'), timeoutSuccessClass) 117 setTimeout(() => el.setAttribute('data-tooltip', oldText), timeoutIcon) 118 } 119 120 // Changes the copy button icon for two seconds, then changes it back 121 const temporarilyChangeIcon = (el) => { 122 el.innerHTML = iconCheck; 123 setTimeout(() => {el.innerHTML = iconCopy}, timeoutIcon) 124 } 125 126 const addCopyButtonToCodeCells = () => { 127 // If ClipboardJS hasn't loaded, wait a bit and try again. This 128 // happens because we load ClipboardJS asynchronously. 129 if (window.ClipboardJS === undefined) { 130 setTimeout(addCopyButtonToCodeCells, 250) 131 return 132 } 133 134 // Add copybuttons to all of our code cells 135 const COPYBUTTON_SELECTOR = 'div.highlight pre'; 136 const codeCells = document.querySelectorAll(COPYBUTTON_SELECTOR) 137 codeCells.forEach((codeCell, index) => { 138 const id = codeCellId(index) 139 codeCell.setAttribute('id', id) 140 141 const clipboardButton = id => 142 `<button class="copybtn o-tooltip--left" data-tooltip="${messages[locale]['copy']}" data-clipboard-target="#${id}"> 143 ${iconCopy} 144 </button>` 145 codeCell.insertAdjacentHTML('afterend', clipboardButton(id)) 146 }) 147 148 function escapeRegExp(string) { 149 return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string 150 } 151 152 /** 153 * Removes excluded text from a Node. 154 * 155 * @param {Node} target Node to filter. 156 * @param {string} exclude CSS selector of nodes to exclude. 157 * @returns {DOMString} Text from `target` with text removed. 158 */ 159 function filterText(target, exclude) { 160 const clone = target.cloneNode(true); // clone as to not modify the live DOM 161 if (exclude) { 162 // remove excluded nodes 163 clone.querySelectorAll(exclude).forEach(node => node.remove()); 164 } 165 return clone.innerText; 166 } 167 168 // Callback when a copy button is clicked. Will be passed the node that was clicked 169 // should then grab the text and replace pieces of text that shouldn't be used in output 170 function formatCopyText(textContent, copybuttonPromptText, isRegexp = false, onlyCopyPromptLines = true, removePrompts = true, copyEmptyLines = true, lineContinuationChar = "", hereDocDelim = "") { 171 var regexp; 172 var match; 173 174 // Do we check for line continuation characters and "HERE-documents"? 175 var useLineCont = !!lineContinuationChar 176 var useHereDoc = !!hereDocDelim 177 178 // create regexp to capture prompt and remaining line 179 if (isRegexp) { 180 regexp = new RegExp('^(' + copybuttonPromptText + ')(.*)') 181 } else { 182 regexp = new RegExp('^(' + escapeRegExp(copybuttonPromptText) + ')(.*)') 183 } 184 185 const outputLines = []; 186 var promptFound = false; 187 var gotLineCont = false; 188 var gotHereDoc = false; 189 const lineGotPrompt = []; 190 for (const line of textContent.split('\n')) { 191 match = line.match(regexp) 192 if (match || gotLineCont || gotHereDoc) { 193 promptFound = regexp.test(line) 194 lineGotPrompt.push(promptFound) 195 if (removePrompts && promptFound) { 196 outputLines.push(match[2]) 197 } else { 198 outputLines.push(line) 199 } 200 gotLineCont = line.endsWith(lineContinuationChar) & useLineCont 201 if (line.includes(hereDocDelim) & useHereDoc) 202 gotHereDoc = !gotHereDoc 203 } else if (!onlyCopyPromptLines) { 204 outputLines.push(line) 205 } else if (copyEmptyLines && line.trim() === '') { 206 outputLines.push(line) 207 } 208 } 209 210 // If no lines with the prompt were found then just use original lines 211 if (lineGotPrompt.some(v => v === true)) { 212 textContent = outputLines.join('\n'); 213 } 214 215 // Remove a trailing newline to avoid auto-running when pasting 216 if (textContent.endsWith("\n")) { 217 textContent = textContent.slice(0, -1) 218 } 219 return textContent 220 } 221 222 223 var copyTargetText = (trigger) => { 224 var target = document.querySelector(trigger.attributes['data-clipboard-target'].value); 225 226 // get filtered text 227 let exclude = '.linenos'; 228 229 let text = filterText(target, exclude); 230 return formatCopyText(text, '', false, true, true, true, '', '') 231 } 232 233 // Initialize with a callback so we can modify the text before copy 234 const clipboard = new ClipboardJS('.copybtn', {text: copyTargetText}) 235 236 // Update UI with error/success messages 237 clipboard.on('success', event => { 238 clearSelection() 239 temporarilyChangeTooltip(event.trigger, messages[locale]['copy'], messages[locale]['copy_success']) 240 temporarilyChangeIcon(event.trigger) 241 }) 242 243 clipboard.on('error', event => { 244 temporarilyChangeTooltip(event.trigger, messages[locale]['copy'], messages[locale]['copy_failure']) 245 }) 246 } 247 248 runWhenDOMLoaded(addCopyButtonToCodeCells)