diff --git a/assets/js/click-to-copy.js b/assets/js/click-to-copy.js new file mode 100644 index 00000000000..e43149caf3d --- /dev/null +++ b/assets/js/click-to-copy.js @@ -0,0 +1,89 @@ +const codeListings = document.querySelectorAll('.highlight > pre'); +for (let index = 0; index < codeListings.length; index++) { + const codeSample = codeListings[index].querySelector('code'); + + const copyButton = document.createElement('button'); + const buttonAttributes = { + type: 'button', + title: 'Copy to clipboard', + 'data-bs-toggle': 'tooltip', + 'data-bs-placement': 'top', + 'data-bs-container': 'body', + }; + Object.keys(buttonAttributes).forEach((key) => { + copyButton.setAttribute(key, buttonAttributes[key]); + }); + copyButton.classList.add( + 'fas', + 'fa-copy', + 'btn', + 'btn-sm', + 'td-click-to-copy' + ); + const tooltip = new bootstrap.Tooltip(copyButton); + let revertTimeout; + copyButton.onclick = () => { + copyCode(codeSample) + .then(() => { + copyButton.setAttribute('data-bs-original-title', 'Copied!'); + tooltip.show(); + copyButton.classList.remove('fa-copy'); + copyButton.classList.add('fa-check', 'td-click-to-copy--copied'); + clearTimeout(revertTimeout); + revertTimeout = setTimeout(() => { + copyButton.classList.remove('fa-check', 'td-click-to-copy--copied'); + copyButton.classList.add('fa-copy'); + copyButton.setAttribute('data-bs-original-title', 'Copy to clipboard'); + tooltip.hide(); + }, 2500); + }) + .catch((err) => { + console.warn('Failed to copy code to clipboard:', err); + copyButton.setAttribute('data-bs-original-title', 'Failed to copy'); + tooltip.show(); + clearTimeout(revertTimeout); + revertTimeout = setTimeout(() => { + copyButton.setAttribute('data-bs-original-title', 'Copy to clipboard'); + tooltip.hide(); + }, 2500); + }); +}; + const buttonDiv = document.createElement('div'); + buttonDiv.classList.add('click-to-copy'); + buttonDiv.append(copyButton); + codeListings[index].insertBefore(buttonDiv, codeSample); +} + +const copyCode = (codeSample) => { + const isConsoleBlock = codeSample.matches( + "code[data-lang='console'], code.language-console" + ); + let text; + if (isConsoleBlock) { + const clone = codeSample.cloneNode(true); + pruneUnselectableElements(codeSample, clone); + text = clone.textContent; + text = text.replace(/^ /gm, ''); + } else { + text = codeSample.textContent; + } + text = text ? text.trim() : ''; + return navigator.clipboard.writeText(text + '\n'); + }; + +const pruneUnselectableElements = (sourceNode, cloneNode) => { + const sourceChildren = sourceNode.children; + const cloneChildren = cloneNode.children; + for (let i = sourceChildren.length - 1; i >= 0; i--) { + const sourceChild = sourceChildren[i]; + const cloneChild = cloneChildren[i]; + const style = window.getComputedStyle(sourceChild); + const unselectable = + style.userSelect === 'none' || style.webkitUserSelect === 'none'; + if (unselectable) { + cloneChild.remove(); + continue; + } + pruneUnselectableElements(sourceChild, cloneChild); + } +}; \ No newline at end of file diff --git a/assets/scss/_styles_project.scss b/assets/scss/_styles_project.scss index 5bb83cc425a..d4462321c33 100644 --- a/assets/scss/_styles_project.scss +++ b/assets/scss/_styles_project.scss @@ -705,6 +705,14 @@ a:not([href]):not([class]):hover { &:hover { color: $body-color !important; } + + &:active { + transform: none !important; + } + + &.td-click-to-copy--copied { + color: $success !important; + } } .tooltip .tooltip-inner {