(function () { if (typeof Prism === 'undefined' || typeof document === 'undefined') { return; } if (!Prism.plugins.toolbar) { console.warn('Copy to Clipboard plugin loaded before Toolbar plugin.'); return; } /** * When the given elements is clicked by the user, the given text will be copied to clipboard. * * @param {HTMLElement} element * @param {CopyInfo} copyInfo * * @typedef CopyInfo * @property {() => string} getText * @property {() => void} success * @property {(reason: unknown) => void} error */ function registerClipboard(element, copyInfo) { element.addEventListener('click', function () { copyTextToClipboard(copyInfo); }); } // https://stackoverflow.com/a/30810322/7595472 /** @param {CopyInfo} copyInfo */ function fallbackCopyTextToClipboard(copyInfo) { var textArea = document.createElement('textarea'); textArea.value = copyInfo.getText(); // Avoid scrolling to bottom textArea.style.top = '0'; textArea.style.left = '0'; textArea.style.position = 'fixed'; document.body.appendChild(textArea); textArea.focus(); textArea.select(); try { var successful = document.execCommand('copy'); setTimeout(function () { if (successful) { copyInfo.success(); } else { copyInfo.error(); } }, 1); } catch (err) { setTimeout(function () { copyInfo.error(err); }, 1); } document.body.removeChild(textArea); } /** @param {CopyInfo} copyInfo */ function copyTextToClipboard(copyInfo) { if (navigator.clipboard) { navigator.clipboard.writeText(copyInfo.getText()).then(copyInfo.success, function () { // try the fallback in case `writeText` didn't work fallbackCopyTextToClipboard(copyInfo); }); } else { fallbackCopyTextToClipboard(copyInfo); } } /** * Selects the text content of the given element. * * @param {Element} element */ function selectElementText(element) { // https://stackoverflow.com/a/20079910/7595472 window.getSelection().selectAllChildren(element); } /** * Traverses up the DOM tree to find data attributes that override the default plugin settings. * * @param {Element} startElement An element to start from. * @returns {Settings} The plugin settings. * @typedef {Record<"copy" | "copy-error" | "copy-success" | "copy-timeout", string | number>} Settings */ function getSettings(startElement) { /** @type {Settings} */ var settings = { 'copy': 'Copy', 'copy-error': 'Press Ctrl+C to copy', 'copy-success': 'Copied!', 'copy-timeout': 5000 }; var prefix = 'data-prismjs-'; for (var key in settings) { var attr = prefix + key; var element = startElement; while (element && !element.hasAttribute(attr)) { element = element.parentElement; } if (element) { settings[key] = element.getAttribute(attr); } } return settings; } Prism.plugins.toolbar.registerButton('copy-to-clipboard', function (env) { var element = env.element; var settings = getSettings(element); var linkCopy = document.createElement('button'); linkCopy.className = 'copy-to-clipboard-button'; linkCopy.setAttribute('type', 'button'); var linkSpan = document.createElement('span'); linkCopy.appendChild(linkSpan); setState('copy'); registerClipboard(linkCopy, { getText: function () { return element.textContent; }, success: function () { setState('copy-success'); resetText(); }, error: function () { setState('copy-error'); setTimeout(function () { selectElementText(element); }, 1); resetText(); } }); return linkCopy; function resetText() { setTimeout(function () { setState('copy'); }, settings['copy-timeout']); } /** @param {"copy" | "copy-error" | "copy-success"} state */ function setState(state) { linkSpan.textContent = settings[state]; linkCopy.setAttribute('data-copy-state', state); } }); }());