1
0
Fork 0
mirror of https://github.com/codex-team/codex.docs.git synced 2025-07-18 20:59:42 +02:00

Merge pull request #283 from alsolovyev/feat/copy-button

Add reusable copy button component
This commit is contained in:
Nikita Melnikov 2022-11-30 15:36:44 +04:00 committed by GitHub
commit 6ff8b7e40b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 202 additions and 131 deletions

View file

@ -0,0 +1,25 @@
{#
Reusable copy button component.
Available props:
- ariaLabel: label for better accessibility
- class: additional class for the button
- textToCopy: text to be copied to the clipboard (use '#' for anchor links)
Usage examples:
{% include 'components/copy-button.twig' with { textToCopy: 'Lorem ipsum dolor' } %}
{% include 'components/copy-button.twig' with { textToCopy: '#anchor-link-dolor' } %}
#}
{% set attrNameForTextToCopy = 'data-text-to-copy' %}
{% set ariaLabel = ariaLabel ?? 'Copy to the Clipboard' %}
{% set mainTag = 'button' %}
{% set mainClass = 'copy-button' %}
<{{ mainTag }} class="{{ mainClass }} {{ class ?? '' }}" aria-label="{{ ariaLabel }}" {{ attrNameForTextToCopy }}="{{ textToCopy }}">
<div class="{{ mainClass }}__inner">
<div class="{{ mainClass }}__icon--initial">{{ svg('copy') }}</div>
<div class="{{ mainClass }}__icon--success">{{ svg('check') }}</div>
</div>
</{{ mainTag }}>

View file

@ -1,4 +1,12 @@
<div class="block-code">
<div class="block-code__content">{{ code|escape }}</div>
<div class="block-code__wrapper">
<div class="block-code__content">{{ code | escape }}</div>
</div>
{%
include 'components/copy-button.twig' with {
ariaLabel: 'Copy Code to Clipboard',
class: 'block-code__copy-button',
textToCopy: code | escape,
}
%}
</div>

View file

@ -1,11 +1,12 @@
<h{{ level }} id="{{ text | urlify }}" class="block-header block-header--{{ level }} block-header--anchor">
<div class="block-header__copy-link-splash"></div>
<div class="block-header__copy-link">
<div class="block-header__copy-link-icon--initial">{{ svg('copy') }}</div>
<div class="block-header__copy-link-icon--success">{{ svg('check') }}</div>
</div>
<h{{ level }} id="{{ text | urlify }}" class="block-header block-header--{{ level }}">
{%
include 'components/copy-button.twig' with {
ariaLabel: 'Copy Link to the ' ~ text,
class: 'block-header__copy-button',
textToCopy: '#' ~ text | urlify,
}
%}
<a href="#{{ text | urlify }}">
{{ text }}
</a>
</h{{ level }}>

View file

@ -193,7 +193,7 @@ export default class TableOfContent {
const linkWrapper = $.make('li', this.CSS.tocElementItem);
const linkBlock = $.make('a', null, {
innerText: tag.innerText,
innerText: tag.innerText.trim(),
href: `${linkTarget}`,
});

View file

@ -20,10 +20,9 @@ export default class Page {
*/
static get CSS() {
return {
copyButton: 'copy-button',
copyButtonCopied: 'copy-button__copied',
page: 'page',
copyLinkBtn: 'block-header__copy-link',
header: 'block-header--anchor',
headerLinkCopied: 'block-header--link-copied',
};
}
@ -35,11 +34,15 @@ export default class Page {
this.tableOfContent = this.createTableOfContent();
/**
* Add click event listener to capture copy link button clicks
* Add click event listener
*/
const page = document.querySelector(`.${Page.CSS.page}`);
page.addEventListener('click', this.copyAnchorLinkIfNeeded);
page.addEventListener('click', (event) => {
if (event.target.classList.contains(Page.CSS.copyButton)) {
this.handleCopyButtonClickEvent(event);
}
});
}
/**
@ -69,10 +72,7 @@ export default class Page {
try {
// eslint-disable-next-line no-new
new TableOfContent({
tagSelector:
'h2.block-header--anchor,' +
'h3.block-header--anchor,' +
'h4.block-header--anchor',
tagSelector: '.block-header',
appendTo: document.getElementById('layout-sidebar-right'),
});
} catch (error) {
@ -81,27 +81,31 @@ export default class Page {
}
/**
* Checks if 'copy link' button was clicked and copies the link to clipboard
* Handles copy button click events
*
* @param e - click event
* @param {Event} e - Event Object.
* @returns {Promise<void>}
*/
copyAnchorLinkIfNeeded = async (e) => {
const copyLinkButtonClicked = e.target.closest(`.${Page.CSS.copyLinkBtn}`);
async handleCopyButtonClickEvent({ target }) {
if (target.classList.contains(Page.CSS.copyButtonCopied)) return;
if (!copyLinkButtonClicked) {
return;
let textToCopy = target.getAttribute('data-text-to-copy');
if (!textToCopy) return;
// Check if text to copy is an anchor link
if (/^#\S*$/.test(textToCopy))
textToCopy = window.location.origin + window.location.pathname + textToCopy;
try {
await copyToClipboard(textToCopy);
target.classList.add(Page.CSS.copyButtonCopied);
target.addEventListener('mouseleave', () => {
setTimeout(() => target.classList.remove(Page.CSS.copyButtonCopied), 5e2);
}, { once: true });
} catch (error) {
console.error(error); // @todo send to Hawk
}
const header = e.target.closest(`.${Page.CSS.header}`);
const link = header.querySelector('a').href;
await copyToClipboard(link);
header.classList.add(Page.CSS.headerLinkCopied);
header.addEventListener('mouseleave', () => {
setTimeout(() => {
header.classList.remove(Page.CSS.headerLinkCopied);
}, 500);
}, { once: true });
}
}

View file

@ -0,0 +1,103 @@
.copy-button {
position: relative;
width: 28px;
height: 28px;
padding: 0;
border: none;
background: none;
cursor: pointer;
transition: opacity 200ms;
@media (--can-hover) {
&:hover .copy-button__inner {
background: var(--color-link-hover);
}
}
&::before {
content: '';
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
border-radius: 100%;
background-color: var(--color-success);
visibility: hidden;
pointer-events: none;
transform: scale(1);
transition: transform 400ms ease-out, opacity 400ms;
}
&__inner {
@apply --squircle;
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
background: white;
pointer-events: none;
}
&__icon--initial {
display: flex;
transform: translateZ(0);
}
&__icon--success {
display: none;
width: 24px;
height: 24px;
color: white;
}
&__copied {
&::before {
opacity: 0;
visibility: visible;
transform: scale(3.5);
}
.copy-button__inner,
.copy-button__inner:hover {
background: var(--color-success) !important;
animation: check-square-in 250ms ease-in;
}
.copy-button__icon--initial {
display: none;
}
.copy-button__icon--success {
display: flex;
animation: check-sign-in 350ms ease-in forwards;
}
}
@keyframes check-sign-in {
from {
transform: scale(.7);
}
80% {
transform: scale(1.1);
}
to {
transform: none;
}
}
@keyframes check-square-in {
from {
transform: scale(1.05);
}
80% {
transform: scale(.96);
}
to {
transform: none;
}
}
}

View file

@ -125,79 +125,15 @@
pointer-events: none;
}
&--link-copied {
.block-header__copy-link,
.block-header__copy-link:hover {
background: var(--color-success);
opacity: 1;
animation: check-square-in 250ms ease-in;
pointer-events: none;
}
.block-header__copy-link-icon--initial {
display: none;
}
.block-header__copy-link-icon--success {
display: flex;
animation: check-sign-in 350ms ease-in forwards;
}
.block-header__copy-link-splash {
opacity: 0;
visibility: visible;
transform: scale(3.5);
}
}
&__copy-link-splash {
position: absolute;
left: 0;
width: 28px;
height: 28px;
background-color: var(--color-success);
transform: scale(1);
border-radius: 100%;
transition: transform 400ms ease-out, opacity 400ms;
visibility: hidden;
}
&__copy-link-icon--success {
width: 24px;
height: 24px;
display: none;
color: white;
}
&__copy-link-icon--initial {
display: flex;
justify-content: center;
align-items: center;
}
&__copy-link {
width: 28px;
height: 28px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
border-radius: 8px;
@apply --squircle;
&__copy-button {
margin-right: 8px;
color: var(--color-text-second);
opacity: 0;
margin-right: 8px;
@media (--can-hover) {
&:hover {
background: var(--color-link-hover);
}
}
}
@media (--can-hover) {
&:hover {
.block-header__copy-link {
.block-header__copy-button {
opacity: 1;
}
}
@ -206,30 +142,6 @@
.inline-code {
line-height: inherit;
}
@keyframes check-sign-in {
from {
transform: scale(.7);
}
80% {
transform: scale(1.1);
}
to {
transform: none;
}
}
@keyframes check-square-in {
from {
transform: scale(1.05);
}
80% {
transform: scale(.96);
}
to {
transform: none;
}
}
}
/**
@ -237,8 +149,18 @@
* ==================
*/
.block-code {
@apply --text-code-block;
@apply --squircle;
position: relative;
&:hover {
.block-code__copy-button {
opacity: 1;
}
}
&__wrapper {
@apply --text-code-block;
@apply --squircle;
}
&__content {
display: inline-block !important;
@ -257,6 +179,13 @@
}
}
&__copy-button {
position: absolute;
top: 10px;
right: 10px;
opacity: 0;
}
.hljs-params {
color: var(--color-code-params);
}
@ -580,4 +509,3 @@
}
}
}

View file

@ -8,6 +8,7 @@
.docs {
min-height: calc(100vh - var(--layout-height-header));
overflow-x: hidden;
@media (--desktop) {
display: flex;

View file

@ -5,6 +5,7 @@
@import './carbon.pcss';
@import './components/header.pcss';
@import './components/writing.pcss';
@import './components/copy-button.pcss';
@import './components/page.pcss';
@import './components/greeting.pcss';
@import './components/auth.pcss';