mirror of
https://github.com/codex-team/codex.docs.git
synced 2025-07-19 05:09:41 +02:00
Add ability to copy header link (#256)
* Add ability to copy header link * Update copy button styles * Update splash border radius * Remove cursor pointer from header * Fix for different header sizes * Update animation * Update src/frontend/styles/components/page.pcss Co-authored-by: Peter Savchenko <specc.dev@gmail.com> Co-authored-by: Peter Savchenko <specc.dev@gmail.com>
This commit is contained in:
parent
51fb6655e2
commit
3e5b6a8ba0
6 changed files with 196 additions and 21 deletions
|
@ -1,4 +1,9 @@
|
|||
<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>
|
||||
<a href="#{{ text | urlify }}">
|
||||
{{ text }}
|
||||
</a>
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import copyToClipboard from '../utils/copyToClipboard';
|
||||
|
||||
/**
|
||||
* @class Page
|
||||
* @classdesc Class for page module
|
||||
|
@ -11,12 +13,33 @@ export default class Page {
|
|||
this.tableOfContent = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* CSS classes used in the codes
|
||||
*
|
||||
* @returns {Record<string, string>}
|
||||
*/
|
||||
static get CSS() {
|
||||
return {
|
||||
page: 'page',
|
||||
copyLinkBtn: 'block-header__copy-link',
|
||||
header: 'block-header--anchor',
|
||||
headerLinkCopied: 'block-header--link-copied',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by ModuleDispatcher to initialize module from DOM
|
||||
*/
|
||||
init() {
|
||||
this.codeStyler = this.createCodeStyling();
|
||||
this.tableOfContent = this.createTableOfContent();
|
||||
|
||||
/**
|
||||
* Add click event listener to capture copy link button clicks
|
||||
*/
|
||||
const page = document.querySelector(`.${Page.CSS.page}`);
|
||||
|
||||
page.addEventListener('click', this.copyAnchorLinkIfNeeded);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -56,4 +79,29 @@ export default class Page {
|
|||
console.error(error); // @todo send to Hawk
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if 'copy link' button was clicked and copies the link to clipboard
|
||||
*
|
||||
* @param e - click event
|
||||
*/
|
||||
copyAnchorLinkIfNeeded = async (e) => {
|
||||
const copyLinkButtonClicked = e.target.closest(`.${Page.CSS.copyLinkBtn}`);
|
||||
|
||||
if (!copyLinkButtonClicked) {
|
||||
return;
|
||||
}
|
||||
|
||||
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 });
|
||||
}
|
||||
}
|
||||
|
|
39
src/frontend/js/utils/copyToClipboard.js
Normal file
39
src/frontend/js/utils/copyToClipboard.js
Normal file
|
@ -0,0 +1,39 @@
|
|||
const ERROR_MESSAGE = 'Unable to copy to clipboard';
|
||||
|
||||
/**
|
||||
* Copies specified text to clipboard
|
||||
*
|
||||
* @param {string} text - text to be copied to clipboard
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export default function (text) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (navigator.clipboard) {
|
||||
navigator.clipboard.writeText(text)
|
||||
.then(() => resolve())
|
||||
.catch(() => reject(new Error(ERROR_MESSAGE)));
|
||||
} else {
|
||||
const tmpElement = document.createElement('input');
|
||||
|
||||
Object.assign(tmpElement.style, {
|
||||
position: 'fixed',
|
||||
top: '0',
|
||||
left: '0',
|
||||
opacity: '0',
|
||||
});
|
||||
|
||||
tmpElement.value = text;
|
||||
document.body.appendChild(tmpElement);
|
||||
tmpElement.select();
|
||||
|
||||
try {
|
||||
document.execCommand('copy');
|
||||
resolve();
|
||||
} catch (e) {
|
||||
reject(new Error(ERROR_MESSAGE));
|
||||
} finally {
|
||||
document.body.removeChild(tmpElement);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
|
@ -99,6 +99,10 @@
|
|||
.block-header {
|
||||
@apply --text-header;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
transform: translateX(-36px);
|
||||
cursor: text;
|
||||
&--2 {
|
||||
@apply --text-header-2;
|
||||
}
|
||||
|
@ -111,42 +115,114 @@
|
|||
text-decoration: none !important;
|
||||
border: 0;
|
||||
color: inherit !important;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
&--anchor {
|
||||
cursor: pointer;
|
||||
&--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;
|
||||
}
|
||||
|
||||
&::before {
|
||||
position: absolute;
|
||||
content: '';
|
||||
margin-left: -30px;
|
||||
width: 14px;
|
||||
height: 19px;
|
||||
margin-top: 0.35em;
|
||||
background-image: url("data:image/svg+xml,%3Csvg width='14' height='19' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='%237B7E89' fill-rule='nonzero'%3E%3Cpath d='M8.58 11.997L2.283 5.701a1.762 1.762 0 0 1 0-2.49l.933-.935a1.762 1.762 0 0 1 2.49 0l6.298 6.297a3.508 3.508 0 0 0-.901-3.39L6.952 1.031a3.522 3.522 0 0 0-4.982 0l-.933.933a3.522 3.522 0 0 0 0 4.982l4.151 4.151c.92.92 2.218 1.208 3.392.9z'/%3E%3Cpath d='M12.958 11.628l-4.151-4.15a3.507 3.507 0 0 0-3.391-.899l6.296 6.296a1.76 1.76 0 0 1 0 2.49l-.933.936a1.764 1.764 0 0 1-2.49 0l-6.296-6.298a3.507 3.507 0 0 0 .899 3.39l4.151 4.15a3.522 3.522 0 0 0 4.982 0l.933-.935a3.52 3.52 0 0 0 0-4.98z'/%3E%3C/g%3E%3C/svg%3E");
|
||||
.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;
|
||||
transform: translateX(5px);
|
||||
will-change: opacity, transform;
|
||||
transition: all 100ms ease;
|
||||
visibility: visible;
|
||||
transform: scale(3.5);
|
||||
}
|
||||
}
|
||||
|
||||
@media (--mobile){
|
||||
display: none !important;
|
||||
&__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;
|
||||
color: var(--color-text-second);
|
||||
opacity: 0;
|
||||
margin-right: 8px;
|
||||
|
||||
@media (--can-hover) {
|
||||
&:hover {
|
||||
background: var(--color-link-hover);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (--can-hover) {
|
||||
&:hover {
|
||||
color: #4c4c58;
|
||||
}
|
||||
|
||||
&:hover::before{
|
||||
opacity: 1;
|
||||
transform: none;
|
||||
.block-header__copy-link {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -22,6 +22,7 @@
|
|||
--color-code-number: #ff6262;
|
||||
--color-code-comment: #6c7f93;
|
||||
|
||||
/* Button component styles */
|
||||
--color-button-primary: #3389FF;
|
||||
--color-button-primary-hover: #2E7AE6;
|
||||
--color-button-primary-active: #296DCC;
|
||||
|
@ -34,6 +35,8 @@
|
|||
--color-button-warning-hover: #D65151;
|
||||
--color-button-warning-active: #BD4848;
|
||||
|
||||
--color-success: #00e08f;
|
||||
|
||||
|
||||
/**
|
||||
* Site layout sizes
|
||||
|
|
4
src/frontend/svg/copy.svg
Normal file
4
src/frontend/svg/copy.svg
Normal file
|
@ -0,0 +1,4 @@
|
|||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M16.9231 11H12.0769C11.4822 11 11 11.4822 11 12.0769V16.9231C11 17.5178 11.4822 18 12.0769 18H16.9231C17.5178 18 18 17.5178 18 16.9231V12.0769C18 11.4822 17.5178 11 16.9231 11Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" fill="transparent"/>
|
||||
<path d="M8.61538 14H8.07692C7.79131 14 7.51739 13.8865 7.31542 13.6846C7.11346 13.4826 7 13.2087 7 12.9231V8.07692C7 7.79131 7.11346 7.51739 7.31542 7.31542C7.51739 7.11346 7.79131 7 8.07692 7H12.9231C13.2087 7 13.4826 7.11346 13.6846 7.31542C13.8865 7.51739 14 7.79131 14 8.07692V8.61538" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" fill="transparent"/>
|
||||
</svg>
|
After Width: | Height: | Size: 803 B |
Loading…
Add table
Add a link
Reference in a new issue