From 3e5b6a8ba0134bbac31030ad260f928750d01519 Mon Sep 17 00:00:00 2001 From: Tanya Date: Wed, 14 Sep 2022 15:31:16 +0300 Subject: [PATCH] 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 Co-authored-by: Peter Savchenko --- src/backend/views/pages/blocks/header.twig | 5 + src/frontend/js/modules/page.js | 48 +++++++++ src/frontend/js/utils/copyToClipboard.js | 39 +++++++ src/frontend/styles/components/page.pcss | 118 +++++++++++++++++---- src/frontend/styles/vars.pcss | 3 + src/frontend/svg/copy.svg | 4 + 6 files changed, 196 insertions(+), 21 deletions(-) create mode 100644 src/frontend/js/utils/copyToClipboard.js create mode 100644 src/frontend/svg/copy.svg diff --git a/src/backend/views/pages/blocks/header.twig b/src/backend/views/pages/blocks/header.twig index 90b6b1b..f069652 100644 --- a/src/backend/views/pages/blocks/header.twig +++ b/src/backend/views/pages/blocks/header.twig @@ -1,4 +1,9 @@ + + {{ text }} diff --git a/src/frontend/js/modules/page.js b/src/frontend/js/modules/page.js index 104928e..00bbaf9 100644 --- a/src/frontend/js/modules/page.js +++ b/src/frontend/js/modules/page.js @@ -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} + */ + 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 }); + } } diff --git a/src/frontend/js/utils/copyToClipboard.js b/src/frontend/js/utils/copyToClipboard.js new file mode 100644 index 0000000..dd368b0 --- /dev/null +++ b/src/frontend/js/utils/copyToClipboard.js @@ -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} + */ +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); + } + } + }); +} diff --git a/src/frontend/styles/components/page.pcss b/src/frontend/styles/components/page.pcss index dd6d9f2..e47ee84 100644 --- a/src/frontend/styles/components/page.pcss +++ b/src/frontend/styles/components/page.pcss @@ -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; + } + } } /** diff --git a/src/frontend/styles/vars.pcss b/src/frontend/styles/vars.pcss index ac39fb5..91c6d1d 100644 --- a/src/frontend/styles/vars.pcss +++ b/src/frontend/styles/vars.pcss @@ -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 diff --git a/src/frontend/svg/copy.svg b/src/frontend/svg/copy.svg new file mode 100644 index 0000000..82ab1ce --- /dev/null +++ b/src/frontend/svg/copy.svg @@ -0,0 +1,4 @@ + + + +