Skip to content

Commit

Permalink
feat: add button to copy code to clipboard
Browse files Browse the repository at this point in the history
  • Loading branch information
kaique-soares committed Nov 8, 2023
1 parent 88dbacc commit 01d55ed
Show file tree
Hide file tree
Showing 6 changed files with 190 additions and 0 deletions.
17 changes: 17 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"@bytemd/plugin-math": "1.21.0",
"@bytemd/plugin-mermaid": "1.21.0",
"@bytemd/react": "1.21.0",
"@primer/octicons": "19.8.0",
"@primer/octicons-react": "18.3.0",
"@primer/react": "35.25.1",
"@resvg/resvg-js": "2.4.1",
Expand Down
4 changes: 4 additions & 0 deletions pages/interface/components/Markdown/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ import 'katex/dist/katex.css';
import { useEffect, useMemo, useRef, useState } from 'react';

import { Box, EditorColors, EditorStyles, useTheme } from '@/TabNewsUI';
import { createCopyButton } from 'pages/interface/utils/copy-button';

import { copyCodeToClipboardPlugin } from './plugins/copy-code-to-clipboard';

const bytemdPluginBaseList = [
gfmPlugin({ locale: gfmLocale }),
Expand All @@ -23,6 +26,7 @@ const bytemdPluginBaseList = [
}),
breaksPlugin(),
gemojiPlugin(),
copyCodeToClipboardPlugin(createCopyButton),
];

function usePlugins() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/**
* @returns {import('@bytemd/react').ViewerProps['plugins'][0]}
*/

export function copyCodeToClipboardPlugin(createCopyButton) {
return {
viewerEffect({ markdownBody }) {
if (!navigator.clipboard) return;

const preElements = markdownBody.querySelectorAll('pre');

preElements.forEach((pre) => {
const codeToCopy = pre.querySelector('code')?.innerText;
if (!codeToCopy) return;

const externalDivElement = document.createElement('div');
externalDivElement.classList.add('copy-button-external-container');
pre.appendChild(externalDivElement);

const internalDivElement = document.createElement('div');
internalDivElement.classList.add('copy-button-internal-container');
externalDivElement.appendChild(internalDivElement);

createCopyButton(internalDivElement, codeToCopy, {
beforeCopy: {
title: 'Copiar código',
ariaLabel: 'Copiar código',
},
afterCopy: {
title: 'Código copiado',
ariaLabel: 'Código copiado',
},
});
});
},
};
}
44 changes: 44 additions & 0 deletions pages/interface/components/Markdown/styles/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -1803,6 +1803,9 @@ export function ViewerStyles() {
line-height: 1.45;
background-color: ${colors.canvas.subtle};
border-radius: 6px;
display: flex;
justify-content: space-between;
gap: 4px;
}
.markdown-body .math {
overflow: auto;
Expand Down Expand Up @@ -1913,6 +1916,47 @@ export function ViewerStyles() {
.markdown-body ::-webkit-calendar-picker-indicator {
filter: invert(50%);
}
.copy-button-external-container {
--parent-padding-discount: -6px;
position: relative;
top: var(--parent-padding-discount);
right: var(--parent-padding-discount);
min-width: 32px;
}
.copy-button-internal-container {
position: absolute;
border: 1px solid ${colors.btn.border};
border-radius: 6px;
color: ${colors.fg.muted};
transition: 0.2s ease-in-out;
transition-property: color, background-color, border-color;
}
.copy-button-internal-container > button {
width: 32px;
height: 32px;
display: grid;
place-content: center;
border: 0;
background-color: transparent;
color: inherit;
cursor: pointer;
}
.copy-button-internal-container:has(button:hover) {
border-color: ${colors.btn.fg};
background-color: ${colors.btn.hoverBg};
}
.copy-button-internal-container:has(button[title='Código copiado']) {
border-color: ${colors.success.fg};
background-color: ${colors.btn.hoverBg};
color: ${colors.success.fg};
pointer-events: none;
}
`}
</style>
);
Expand Down
87 changes: 87 additions & 0 deletions pages/interface/utils/copy-button.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { check, copy } from '@primer/octicons';

const defaultOptions = {
beforeCopy: {
title: 'Copiar',
ariaLabel: 'Copiar',
icon: copy.toSVG(),
},
afterCopy: {
title: 'Copiado',
ariaLabel: 'Copiado',
icon: check.toSVG(),
},
};

export function createCopyButton(parentElement, contentToCopy, options = defaultOptions) {
const validatedOptions = validateOptions(options);
const buttonElement = createButtonElement(validatedOptions);

buttonElement.onclick = async () => {
await navigator.clipboard.writeText(contentToCopy);
setStateAfterCopy(buttonElement, validatedOptions);
setTimeout(() => setStateBeforeCopy(buttonElement, validatedOptions), 2000);
};

return parentElement.appendChild(buttonElement);

function validateOptions(options) {
const { beforeCopy, afterCopy } = options;

const allowedOptions = Object.keys(defaultOptions.beforeCopy);
Object.entries(beforeCopy).forEach(([key, value]) => {
if (!allowedOptions.includes(key)) {
throw new Error(`
"beforeCopy.${key}" is invalid, please use one of the following: [${allowedOptions.join(', ')}]
`);
}
if (typeof value !== 'string') {
throw new TypeError(`"beforeCopy.${key}" must receive a string as a value`);
}
});
Object.entries(afterCopy).forEach(([key, value]) => {
if (!allowedOptions.includes(key)) {
throw new Error(`
"afterCopy.${key}" is invalid, please use one of the following: [${allowedOptions.join(', ')}]
`);
}
if (typeof value !== 'string') {
throw new TypeError(`"afterCopy.${key}" must receive a string as a value`);
}
});

return Object.freeze({
beforeCopy: Object.assign({}, defaultOptions.beforeCopy, beforeCopy),
afterCopy: Object.assign({}, defaultOptions.afterCopy, afterCopy),
});
}

function createButtonElement(options) {
const buttonElement = document.createElement('button');
buttonElement.setAttribute('type', 'button');
setStateBeforeCopy(buttonElement, options);
return buttonElement;
}

function setStateBeforeCopy(buttonElement, options) {
setAttributes(buttonElement, {
title: options.beforeCopy.title,
'aria-label': options.beforeCopy.ariaLabel,
});
buttonElement.innerHTML = options.beforeCopy.icon;
}

function setStateAfterCopy(buttonElement, options) {
setAttributes(buttonElement, {
title: options.afterCopy.title,
'aria-label': options.afterCopy.ariaLabel,
});
buttonElement.innerHTML = options.afterCopy.icon;
}

function setAttributes(buttonElement, attributes) {
Object.entries(attributes).forEach(([key, value]) => {
buttonElement.setAttribute(key, value);
});
}
}

0 comments on commit 01d55ed

Please sign in to comment.