From adc2aa327740f94e67002d2139c69b9989a7ee88 Mon Sep 17 00:00:00 2001 From: Tanya Fomina Date: Tue, 13 Sep 2022 11:57:18 +0300 Subject: [PATCH] Add ability to copy header link --- 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 | 88 ++++++++++++++++------ src/frontend/styles/vars.pcss | 4 + src/frontend/svg/copy.svg | 4 + 6 files changed, 166 insertions(+), 22 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..4522986 100644 --- a/src/backend/views/pages/blocks/header.twig +++ b/src/backend/views/pages/blocks/header.twig @@ -2,5 +2,10 @@ {{ 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 d100dae..b1ec837 100644 --- a/src/frontend/styles/components/page.pcss +++ b/src/frontend/styles/components/page.pcss @@ -98,6 +98,8 @@ .block-header { @apply --text-header; + position: relative; + &--2 { @apply --text-header-2; } @@ -114,32 +116,74 @@ &--anchor { cursor: pointer; + } - &::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"); - opacity: 0; - transform: translateX(5px); - will-change: opacity, transform; - transition: all 100ms ease; - - @media (--mobile){ - display: none !important; - } + &--link-copied { + .block-header__copy-link { + display: none; } + .block-header__copy-link-success { + display: flex; + } + } + + &__copy-link-success { + position: absolute; + width: 64px; + height: 64px; + left: -63px; + top: -15px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 100%; + background: var(--color-success-light); + color: white; + display: none; + + &-inner { + width: 34px; + height: 34px; + background-color: var(--color-success); + display: flex; + align-items: center; + justify-content: center; + border-radius: 100%; + } + + svg { + width: 24px; + height: 24px; + } + } + + &__copy-link { + position: absolute; + width: 34px; + height: 34px; + left: -48px; + top: 0; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + border-radius: 100%; + background: var(--color-link-hover); + color: var(--color-text-second); + opacity: 0; + transition: opacity; + transition-delay: 0.5s; + transition-duration: 200ms; + } + + @media (--can-hover) { &:hover { - color: #4c4c58; - } - - &:hover::before{ - opacity: 1; - transform: none; + .block-header__copy-link { + opacity: 1; + transition-delay: 0s; + transition-duration: 100ms; + } } } diff --git a/src/frontend/styles/vars.pcss b/src/frontend/styles/vars.pcss index a74e081..ad12713 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,9 @@ --color-button-warning-hover: #D65151; --color-button-warning-active: #BD4848; + --color-success: #00e08f; + --color-success-light: #dbfbef; + /** * 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 @@ + + + +