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 @@
+