Skip to content

Commit 6ff8b7e

Browse files
authored
Merge pull request #283 from alsolovyev/feat/copy-button
Add reusable copy button component
2 parents de30ddf + 36ba3a3 commit 6ff8b7e

9 files changed

Lines changed: 200 additions & 129 deletions

File tree

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
{#
2+
Reusable copy button component.
3+
Available props:
4+
- ariaLabel: label for better accessibility
5+
- class: additional class for the button
6+
- textToCopy: text to be copied to the clipboard (use '#' for anchor links)
7+
8+
Usage examples:
9+
{% include 'components/copy-button.twig' with { textToCopy: 'Lorem ipsum dolor' } %}
10+
{% include 'components/copy-button.twig' with { textToCopy: '#anchor-link-dolor' } %}
11+
#}
12+
13+
{% set attrNameForTextToCopy = 'data-text-to-copy' %}
14+
15+
{% set ariaLabel = ariaLabel ?? 'Copy to the Clipboard' %}
16+
17+
{% set mainTag = 'button' %}
18+
{% set mainClass = 'copy-button' %}
19+
20+
<{{ mainTag }} class="{{ mainClass }} {{ class ?? '' }}" aria-label="{{ ariaLabel }}" {{ attrNameForTextToCopy }}="{{ textToCopy }}">
21+
<div class="{{ mainClass }}__inner">
22+
<div class="{{ mainClass }}__icon--initial">{{ svg('copy') }}</div>
23+
<div class="{{ mainClass }}__icon--success">{{ svg('check') }}</div>
24+
</div>
25+
</{{ mainTag }}>

src/backend/views/pages/blocks/code.twig

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,12 @@
11
<div class="block-code">
2-
<div class="block-code__content">{{ code|escape }}</div>
2+
<div class="block-code__wrapper">
3+
<div class="block-code__content">{{ code | escape }}</div>
4+
</div>
5+
{%
6+
include 'components/copy-button.twig' with {
7+
ariaLabel: 'Copy Code to Clipboard',
8+
class: 'block-code__copy-button',
9+
textToCopy: code | escape,
10+
}
11+
%}
312
</div>
4-

src/backend/views/pages/blocks/header.twig

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1-
<h{{ level }} id="{{ text | urlify }}" class="block-header block-header--{{ level }} block-header--anchor">
2-
<div class="block-header__copy-link-splash"></div>
3-
<div class="block-header__copy-link">
4-
<div class="block-header__copy-link-icon--initial">{{ svg('copy') }}</div>
5-
<div class="block-header__copy-link-icon--success">{{ svg('check') }}</div>
6-
</div>
1+
<h{{ level }} id="{{ text | urlify }}" class="block-header block-header--{{ level }}">
2+
{%
3+
include 'components/copy-button.twig' with {
4+
ariaLabel: 'Copy Link to the ' ~ text,
5+
class: 'block-header__copy-button',
6+
textToCopy: '#' ~ text | urlify,
7+
}
8+
%}
79
<a href="#{{ text | urlify }}">
810
{{ text }}
911
</a>
1012
</h{{ level }}>
11-

src/frontend/js/classes/table-of-content.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,7 @@ export default class TableOfContent {
193193

194194
const linkWrapper = $.make('li', this.CSS.tocElementItem);
195195
const linkBlock = $.make('a', null, {
196-
innerText: tag.innerText,
196+
innerText: tag.innerText.trim(),
197197
href: `${linkTarget}`,
198198
});
199199

src/frontend/js/modules/page.js

Lines changed: 29 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,9 @@ export default class Page {
2020
*/
2121
static get CSS() {
2222
return {
23+
copyButton: 'copy-button',
24+
copyButtonCopied: 'copy-button__copied',
2325
page: 'page',
24-
copyLinkBtn: 'block-header__copy-link',
25-
header: 'block-header--anchor',
26-
headerLinkCopied: 'block-header--link-copied',
2726
};
2827
}
2928

@@ -35,11 +34,15 @@ export default class Page {
3534
this.tableOfContent = this.createTableOfContent();
3635

3736
/**
38-
* Add click event listener to capture copy link button clicks
37+
* Add click event listener
3938
*/
4039
const page = document.querySelector(`.${Page.CSS.page}`);
4140

42-
page.addEventListener('click', this.copyAnchorLinkIfNeeded);
41+
page.addEventListener('click', (event) => {
42+
if (event.target.classList.contains(Page.CSS.copyButton)) {
43+
this.handleCopyButtonClickEvent(event);
44+
}
45+
});
4346
}
4447

4548
/**
@@ -69,10 +72,7 @@ export default class Page {
6972
try {
7073
// eslint-disable-next-line no-new
7174
new TableOfContent({
72-
tagSelector:
73-
'h2.block-header--anchor,' +
74-
'h3.block-header--anchor,' +
75-
'h4.block-header--anchor',
75+
tagSelector: '.block-header',
7676
appendTo: document.getElementById('layout-sidebar-right'),
7777
});
7878
} catch (error) {
@@ -81,27 +81,31 @@ export default class Page {
8181
}
8282

8383
/**
84-
* Checks if 'copy link' button was clicked and copies the link to clipboard
84+
* Handles copy button click events
8585
*
86-
* @param e - click event
86+
* @param {Event} e - Event Object.
87+
* @returns {Promise<void>}
8788
*/
88-
copyAnchorLinkIfNeeded = async (e) => {
89-
const copyLinkButtonClicked = e.target.closest(`.${Page.CSS.copyLinkBtn}`);
89+
async handleCopyButtonClickEvent({ target }) {
90+
if (target.classList.contains(Page.CSS.copyButtonCopied)) return;
9091

91-
if (!copyLinkButtonClicked) {
92-
return;
93-
}
92+
let textToCopy = target.getAttribute('data-text-to-copy');
93+
if (!textToCopy) return;
94+
95+
// Check if text to copy is an anchor link
96+
if (/^#\S*$/.test(textToCopy))
97+
textToCopy = window.location.origin + window.location.pathname + textToCopy;
9498

95-
const header = e.target.closest(`.${Page.CSS.header}`);
96-
const link = header.querySelector('a').href;
99+
try {
100+
await copyToClipboard(textToCopy);
97101

98-
await copyToClipboard(link);
99-
header.classList.add(Page.CSS.headerLinkCopied);
102+
target.classList.add(Page.CSS.copyButtonCopied);
103+
target.addEventListener('mouseleave', () => {
104+
setTimeout(() => target.classList.remove(Page.CSS.copyButtonCopied), 5e2);
105+
}, { once: true });
100106

101-
header.addEventListener('mouseleave', () => {
102-
setTimeout(() => {
103-
header.classList.remove(Page.CSS.headerLinkCopied);
104-
}, 500);
105-
}, { once: true });
107+
} catch (error) {
108+
console.error(error); // @todo send to Hawk
109+
}
106110
}
107111
}
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
.copy-button {
2+
position: relative;
3+
width: 28px;
4+
height: 28px;
5+
padding: 0;
6+
border: none;
7+
background: none;
8+
cursor: pointer;
9+
transition: opacity 200ms;
10+
11+
@media (--can-hover) {
12+
&:hover .copy-button__inner {
13+
background: var(--color-link-hover);
14+
}
15+
}
16+
17+
&::before {
18+
content: '';
19+
position: absolute;
20+
top: 0;
21+
right: 0;
22+
bottom: 0;
23+
left: 0;
24+
border-radius: 100%;
25+
background-color: var(--color-success);
26+
visibility: hidden;
27+
pointer-events: none;
28+
transform: scale(1);
29+
transition: transform 400ms ease-out, opacity 400ms;
30+
}
31+
32+
&__inner {
33+
@apply --squircle;
34+
35+
display: flex;
36+
justify-content: center;
37+
align-items: center;
38+
width: 100%;
39+
height: 100%;
40+
background: white;
41+
pointer-events: none;
42+
}
43+
44+
&__icon--initial {
45+
display: flex;
46+
transform: translateZ(0);
47+
}
48+
49+
&__icon--success {
50+
display: none;
51+
width: 24px;
52+
height: 24px;
53+
color: white;
54+
}
55+
56+
&__copied {
57+
58+
&::before {
59+
opacity: 0;
60+
visibility: visible;
61+
transform: scale(3.5);
62+
}
63+
64+
.copy-button__inner,
65+
.copy-button__inner:hover {
66+
background: var(--color-success) !important;
67+
animation: check-square-in 250ms ease-in;
68+
}
69+
70+
.copy-button__icon--initial {
71+
display: none;
72+
}
73+
74+
.copy-button__icon--success {
75+
display: flex;
76+
animation: check-sign-in 350ms ease-in forwards;
77+
}
78+
}
79+
80+
@keyframes check-sign-in {
81+
from {
82+
transform: scale(.7);
83+
}
84+
80% {
85+
transform: scale(1.1);
86+
}
87+
to {
88+
transform: none;
89+
}
90+
}
91+
92+
@keyframes check-square-in {
93+
from {
94+
transform: scale(1.05);
95+
}
96+
80% {
97+
transform: scale(.96);
98+
}
99+
to {
100+
transform: none;
101+
}
102+
}
103+
}

0 commit comments

Comments
 (0)