Greasy Fork is available in English.
Adds buttons to copy a link to the current page directly into clipboard. Two buttons are supported: Markdown and Jira syntax. Both buttons support HTML for rich text editors.
Вам также может понравится Confluence: space avatar as tab icon.
// ==UserScript== // @name Confluence: copy link buttons // @namespace https://github.com/rybak // @version 5 // @description Adds buttons to copy a link to the current page directly into clipboard. Two buttons are supported: Markdown and Jira syntax. Both buttons support HTML for rich text editors. // @author Andrei Rybak // @license MIT // @homepageURL https://github.com/rybak/atlassian-tweaks // @include https://confluence* // @match https://confluence.example.com // @icon https://seeklogo.com/images/C/confluence-logo-D9B07137C2-seeklogo.com.png // @grant none // ==/UserScript== /* * Copyright (c) 2023-2024 Andrei Rybak * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ (function() { 'use strict'; const LOG_PREFIX = '[Confluence copy link buttons]:'; function log(...toLog) { console.log(LOG_PREFIX, ...toLog); } function error(...toLog) { console.error(LOG_PREFIX, ...toLog); } function cloudCopyIcon() { // icon similar to the achnor "Copy link" under the button "Share" return '<svg width="24" height="24" viewBox="0 0 24 24" role="presentation"><path d="M12.654 8.764a.858.858 0 01-1.213-1.213l1.214-1.214a3.717 3.717 0 015.257 0 3.714 3.714 0 01.001 5.258l-1.214 1.214-.804.804a3.72 3.72 0 01-5.263.005.858.858 0 011.214-1.214c.781.782 2.05.78 2.836-.005l.804-.803 1.214-1.214a1.998 1.998 0 00-.001-2.831 2 2 0 00-2.83 0l-1.215 1.213zm-.808 6.472a.858.858 0 011.213 1.213l-1.214 1.214a3.717 3.717 0 01-5.257 0 3.714 3.714 0 01-.001-5.258l1.214-1.214.804-.804a3.72 3.72 0 015.263-.005.858.858 0 01-1.214 1.214 2.005 2.005 0 00-2.836.005l-.804.803L7.8 13.618a1.998 1.998 0 00.001 2.831 2 2 0 002.83 0l1.215-1.213z" fill="currentColor"></path></svg>'; } /* * Calls one of the parameters, based on the version of Confluence running. * This is needed to account for the differences in HTML and CSS. * * Tested on versions: * - Confluence Server 7.13.* * - Confluence Server 7.19.* * - Confluence Cloud 1000.0.0-22300355ddad (a free version on https://atlassian.net as of 2023-06-19) */ function onVersion(selfHostedFn, cloudFn) { if (document.querySelector('meta[name=ajs-cloud-id]')) { // It would seem that all Cloud instances of Confluece have this <meta> tag. return cloudFn(); } /* * Try to parse version number hidden in the <meta> tags. * Assume Confluence Cloud, if can't parse. */ const maybeVersionElem = document.querySelector('meta[name=ajs-version-number]'); if (maybeVersionElem) { const majorVersion = parseInt(maybeVersionElem.content); if (isNaN(majorVersion)) { log("Cannot parse major version", maybeVersionElem.content); return cloudFn(); } if (majorVersion >= 1000) { return cloudFn(); } else { return selfHostedFn(); } } else { log("Couldn't find meta tag with version"); return cloudFn(); } } function addLinkToClipboard(event, plainText, html) { event.stopPropagation(); event.preventDefault(); let clipboardData = event.clipboardData || window.clipboardData; clipboardData.setData('text/plain', plainText); clipboardData.setData('text/html', html); } function copyClickAction(event, plainTextFn) { event.preventDefault(); try { let pageTitle = null; try { pageTitle = document.querySelector('meta[name="ajs-page-title"]').content; } catch (ignored) { } if (!pageTitle) { try { // `AJS` is defined in Confluence's own JS pageTitle = AJS.Data.get('page-title'); } catch (e) { error('Could not get the page title. Aborting.', e); return; } } const url = document.location.href; /* * Using both plain text and HTML ("rich text") means that the copied links * can be inserted both in plain text inputs (Jira syntax – for Jira, Markdown * syntax – for Bitbucket, GitHub, etc) and in rich text inputs, such as * Microsoft Word, Slack, etc. */ const plainText = plainTextFn(url, pageTitle); const html = htmlSyntaxLink(url, pageTitle); const handleCopyEvent = e => { addLinkToClipboard(e, plainText, html); }; document.addEventListener('copy', handleCopyEvent); document.execCommand('copy'); document.removeEventListener('copy', handleCopyEvent); } catch (e) { error('Could not do the copying', e); } } // adapted from https://stackoverflow.com/a/35385518/1083697 by Mark Amery function htmlToElement(html) { const template = document.createElement('template'); template.innerHTML = html.trim(); return template.content.firstChild; } function selfHostedButtonHtml(text, title) { const icon = '<span class="aui-icon aui-icon-small aui-iconfont-copy"></span>'; return `<a href="#" class="aui-button aui-button-subtle" title="${title}"><span>${icon}${text}</span></a>`; } function cloudButtonHtml(text, title) { const icon = cloudCopyIcon(); // Custom CSS is needed to make the ${text} readable. const customCss = 'font-size: 16px; line-height: 26px;'; // HTML & CSS classes from the "Watch this page" button const watchThisPageButton = document.querySelector('[data-id="page-watch-button"]'); const buttonClasses = watchThisPageButton.className; const innerSpanClasses = watchThisPageButton.children[0].className; const innerInnerSpanClasses = watchThisPageButton.children[0].children[0].className; return htmlToElement( `<button class="${buttonClasses}" type="button"> <span class="${innerSpanClasses}" title="${title}"> <span class="${innerInnerSpanClasses}" role="img" style="--icon-primary-color: currentColor; --icon-secondary-color: var(--ds-surface, #FFFFFF); ${customCss}"> ${icon}${text} </span> </span> </button>` ); } function copyButton(text, title, plainTextFn) { const onclick = (event) => copyClickAction(event, plainTextFn); return onVersion( () => { const copyButtonAnchor = htmlToElement(selfHostedButtonHtml(text, title)); copyButtonAnchor.onclick = onclick; const copyButtonListItem = htmlToElement('<li class="ajs-button normal"></li>'); copyButtonListItem.appendChild(copyButtonAnchor); return copyButtonListItem; }, () => { const button = cloudButtonHtml(text, title); button.onclick = onclick; return button; } ); } function htmlSyntaxLink(url, pageTitle) { const html = `<a href="${url}">${pageTitle}</a>`; return html; } function markdownSyntaxLink(url, pageTitle) { return `[${pageTitle}](${url})`; } function jiraSyntaxLink(url, pageTitle) { return `[${pageTitle}|${url}]`; } function insertBefore(newElem, oldElem) { oldElem.parentNode.insertBefore(newElem, oldElem); } // from https://stackoverflow.com/a/61511955/1083697 by Yong Wang function waitForElement(selector) { return new Promise(resolve => { if (document.querySelector(selector)) { return resolve(document.querySelector(selector)); } const observer = new MutationObserver(mutations => { if (document.querySelector(selector)) { resolve(document.querySelector(selector)); observer.disconnect(); } }); observer.observe(document.body, { childList: true, subtree: true }); }); } function createButtons() { onVersion( () => waitForElement('#action-menu-link'), () => waitForElement('button[aria-label="Share"]').then(shareButton => { // HTML of Cloud version is weird, lots of nesting and wrapping return shareButton.parentNode.parentNode.parentNode.parentNode; }) ).then(target => { /* * Buttons are added to the left of the `target` element. */ log('target', target); const markdownListItem = copyButton("[]()", "Copy Markdown link", markdownSyntaxLink); const jiraListItem = copyButton("[|]", "Copy Jira syntax link", jiraSyntaxLink); insertBefore(markdownListItem, target); insertBefore(jiraListItem, target); log('Created buttons'); }); } try { createButtons(); } catch (e) { error('Could not create buttons', e); } })();