copybutton.js 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220
  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': 'Copié 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. let locale = 'en'
  47. if( document.documentElement.lang !== undefined
  48. && messages[document.documentElement.lang] !== undefined ) {
  49. locale = document.documentElement.lang
  50. }
  51. let doc_url_root = DOCUMENTATION_OPTIONS.URL_ROOT;
  52. if (doc_url_root == '#') {
  53. doc_url_root = '';
  54. }
  55. /**
  56. * SVG files for our copy buttons
  57. */
  58. 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">
  59. <title>${messages[locale]['copy_success']}</title>
  60. <path stroke="none" d="M0 0h24v24H0z" fill="none"/>
  61. <path d="M5 12l5 5l10 -10" />
  62. </svg>`
  63. // If the user specified their own SVG use that, otherwise use the default
  64. let iconCopy = ``;
  65. if (!iconCopy) {
  66. 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">
  67. <title>${messages[locale]['copy_to_clipboard']}</title>
  68. <path stroke="none" d="M0 0h24v24H0z" fill="none"/>
  69. <rect x="8" y="8" width="12" height="12" rx="2" />
  70. <path d="M16 8v-2a2 2 0 0 0 -2 -2h-8a2 2 0 0 0 -2 2v8a2 2 0 0 0 2 2h2" />
  71. </svg>`
  72. }
  73. /**
  74. * Set up copy/paste for code blocks
  75. */
  76. const runWhenDOMLoaded = cb => {
  77. if (document.readyState != 'loading') {
  78. cb()
  79. } else if (document.addEventListener) {
  80. document.addEventListener('DOMContentLoaded', cb)
  81. } else {
  82. document.attachEvent('onreadystatechange', function() {
  83. if (document.readyState == 'complete') cb()
  84. })
  85. }
  86. }
  87. const codeCellId = index => `codecell${index}`
  88. // Clears selected text since ClipboardJS will select the text when copying
  89. const clearSelection = () => {
  90. if (window.getSelection) {
  91. window.getSelection().removeAllRanges()
  92. } else if (document.selection) {
  93. document.selection.empty()
  94. }
  95. }
  96. // Changes tooltip text for two seconds, then changes it back
  97. const temporarilyChangeTooltip = (el, oldText, newText) => {
  98. el.setAttribute('data-tooltip', newText)
  99. el.classList.add('success')
  100. setTimeout(() => el.setAttribute('data-tooltip', oldText), 2000)
  101. setTimeout(() => el.classList.remove('success'), 2000)
  102. }
  103. // Changes the copy button icon for two seconds, then changes it back
  104. const temporarilyChangeIcon = (el) => {
  105. el.innerHTML = iconCheck;
  106. setTimeout(() => {el.innerHTML = iconCopy}, 2000)
  107. }
  108. const addCopyButtonToCodeCells = () => {
  109. // If ClipboardJS hasn't loaded, wait a bit and try again. This
  110. // happens because we load ClipboardJS asynchronously.
  111. if (window.ClipboardJS === undefined) {
  112. setTimeout(addCopyButtonToCodeCells, 250)
  113. return
  114. }
  115. // Add copybuttons to all of our code cells
  116. const codeCells = document.querySelectorAll('div.highlight pre')
  117. codeCells.forEach((codeCell, index) => {
  118. const id = codeCellId(index)
  119. codeCell.setAttribute('id', id)
  120. const clipboardButton = id =>
  121. `<button class="copybtn o-tooltip--left" data-tooltip="${messages[locale]['copy']}" data-clipboard-target="#${id}">
  122. ${iconCopy}
  123. </button>`
  124. codeCell.insertAdjacentHTML('afterend', clipboardButton(id))
  125. })
  126. function escapeRegExp(string) {
  127. return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
  128. }
  129. // Callback when a copy button is clicked. Will be passed the node that was clicked
  130. // should then grab the text and replace pieces of text that shouldn't be used in output
  131. function formatCopyText(textContent, copybuttonPromptText, isRegexp = false, onlyCopyPromptLines = true, removePrompts = true, copyEmptyLines = true, lineContinuationChar = "", hereDocDelim = "") {
  132. var regexp;
  133. var match;
  134. // Do we check for line continuation characters and "HERE-documents"?
  135. var useLineCont = !!lineContinuationChar
  136. var useHereDoc = !!hereDocDelim
  137. // create regexp to capture prompt and remaining line
  138. if (isRegexp) {
  139. regexp = new RegExp('^(' + copybuttonPromptText + ')(.*)')
  140. } else {
  141. regexp = new RegExp('^(' + escapeRegExp(copybuttonPromptText) + ')(.*)')
  142. }
  143. const outputLines = [];
  144. var promptFound = false;
  145. var gotLineCont = false;
  146. var gotHereDoc = false;
  147. const lineGotPrompt = [];
  148. for (const line of textContent.split('\n')) {
  149. match = line.match(regexp)
  150. if (match || gotLineCont || gotHereDoc) {
  151. promptFound = regexp.test(line)
  152. lineGotPrompt.push(promptFound)
  153. if (removePrompts && promptFound) {
  154. outputLines.push(match[2])
  155. } else {
  156. outputLines.push(line)
  157. }
  158. gotLineCont = line.endsWith(lineContinuationChar) & useLineCont
  159. if (line.includes(hereDocDelim) & useHereDoc)
  160. gotHereDoc = !gotHereDoc
  161. } else if (!onlyCopyPromptLines) {
  162. outputLines.push(line)
  163. } else if (copyEmptyLines && line.trim() === '') {
  164. outputLines.push(line)
  165. }
  166. }
  167. // If no lines with the prompt were found then just use original lines
  168. if (lineGotPrompt.some(v => v === true)) {
  169. textContent = outputLines.join('\n');
  170. }
  171. // Remove a trailing newline to avoid auto-running when pasting
  172. if (textContent.endsWith("\n")) {
  173. textContent = textContent.slice(0, -1)
  174. }
  175. return textContent
  176. }
  177. var copyTargetText = (trigger) => {
  178. var target = document.querySelector(trigger.attributes['data-clipboard-target'].value);
  179. return formatCopyText(target.innerText, '', false, true, true, true, '', '')
  180. }
  181. // Initialize with a callback so we can modify the text before copy
  182. const clipboard = new ClipboardJS('.copybtn', {text: copyTargetText})
  183. // Update UI with error/success messages
  184. clipboard.on('success', event => {
  185. clearSelection()
  186. temporarilyChangeTooltip(event.trigger, messages[locale]['copy'], messages[locale]['copy_success'])
  187. temporarilyChangeIcon(event.trigger)
  188. })
  189. clipboard.on('error', event => {
  190. temporarilyChangeTooltip(event.trigger, messages[locale]['copy'], messages[locale]['copy_failure'])
  191. })
  192. }
  193. runWhenDOMLoaded(addCopyButtonToCodeCells)