/ docs / manual / _static / copybutton.js
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)