Confluence: copy link buttons

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

安裝腳本
  1. // ==UserScript==
  2. // @name Confluence: copy link buttons
  3. // @namespace https://github.com/rybak
  4. // @version 5
  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.
  6. // @author Andrei Rybak
  7. // @license MIT
  8. // @homepageURL https://github.com/rybak/atlassian-tweaks
  9. // @include https://confluence*
  10. // @match https://confluence.example.com
  11. // @icon https://seeklogo.com/images/C/confluence-logo-D9B07137C2-seeklogo.com.png
  12. // @grant none
  13. // ==/UserScript==
  14.  
  15. /*
  16. * Copyright (c) 2023-2024 Andrei Rybak
  17. *
  18. * Permission is hereby granted, free of charge, to any person obtaining a copy
  19. * of this software and associated documentation files (the "Software"), to deal
  20. * in the Software without restriction, including without limitation the rights
  21. * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  22. * copies of the Software, and to permit persons to whom the Software is
  23. * furnished to do so, subject to the following conditions:
  24. *
  25. * The above copyright notice and this permission notice shall be included in all
  26. * copies or substantial portions of the Software.
  27. *
  28. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  29. * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  30. * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  31. * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  32. * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  33. * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  34. * SOFTWARE.
  35. */
  36.  
  37. (function() {
  38. 'use strict';
  39.  
  40. const LOG_PREFIX = '[Confluence copy link buttons]:';
  41.  
  42. function log(...toLog) {
  43. console.log(LOG_PREFIX, ...toLog);
  44. }
  45.  
  46. function error(...toLog) {
  47. console.error(LOG_PREFIX, ...toLog);
  48. }
  49.  
  50. function cloudCopyIcon() {
  51. // icon similar to the achnor "Copy link" under the button "Share"
  52. 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>';
  53. }
  54.  
  55. /*
  56. * Calls one of the parameters, based on the version of Confluence running.
  57. * This is needed to account for the differences in HTML and CSS.
  58. *
  59. * Tested on versions:
  60. * - Confluence Server 7.13.*
  61. * - Confluence Server 7.19.*
  62. * - Confluence Cloud 1000.0.0-22300355ddad (a free version on https://atlassian.net as of 2023-06-19)
  63. */
  64. function onVersion(selfHostedFn, cloudFn) {
  65. if (document.querySelector('meta[name=ajs-cloud-id]')) {
  66. // It would seem that all Cloud instances of Confluece have this <meta> tag.
  67. return cloudFn();
  68. }
  69. /*
  70. * Try to parse version number hidden in the <meta> tags.
  71. * Assume Confluence Cloud, if can't parse.
  72. */
  73. const maybeVersionElem = document.querySelector('meta[name=ajs-version-number]');
  74. if (maybeVersionElem) {
  75. const majorVersion = parseInt(maybeVersionElem.content);
  76. if (isNaN(majorVersion)) {
  77. log("Cannot parse major version", maybeVersionElem.content);
  78. return cloudFn();
  79. }
  80. if (majorVersion >= 1000) {
  81. return cloudFn();
  82. } else {
  83. return selfHostedFn();
  84. }
  85. } else {
  86. log("Couldn't find meta tag with version");
  87. return cloudFn();
  88. }
  89. }
  90.  
  91. function addLinkToClipboard(event, plainText, html) {
  92. event.stopPropagation();
  93. event.preventDefault();
  94.  
  95. let clipboardData = event.clipboardData || window.clipboardData;
  96. clipboardData.setData('text/plain', plainText);
  97. clipboardData.setData('text/html', html);
  98. }
  99.  
  100. function copyClickAction(event, plainTextFn) {
  101. event.preventDefault();
  102. try {
  103. let pageTitle = null;
  104. try {
  105. pageTitle = document.querySelector('meta[name="ajs-page-title"]').content;
  106. } catch (ignored) {
  107. }
  108. if (!pageTitle) {
  109. try {
  110. // `AJS` is defined in Confluence's own JS
  111. pageTitle = AJS.Data.get('page-title');
  112. } catch (e) {
  113. error('Could not get the page title. Aborting.', e);
  114. return;
  115. }
  116. }
  117. const url = document.location.href;
  118. /*
  119. * Using both plain text and HTML ("rich text") means that the copied links
  120. * can be inserted both in plain text inputs (Jira syntax – for Jira, Markdown
  121. * syntax – for Bitbucket, GitHub, etc) and in rich text inputs, such as
  122. * Microsoft Word, Slack, etc.
  123. */
  124. const plainText = plainTextFn(url, pageTitle);
  125. const html = htmlSyntaxLink(url, pageTitle);
  126.  
  127. const handleCopyEvent = e => {
  128. addLinkToClipboard(e, plainText, html);
  129. };
  130. document.addEventListener('copy', handleCopyEvent);
  131. document.execCommand('copy');
  132. document.removeEventListener('copy', handleCopyEvent);
  133. } catch (e) {
  134. error('Could not do the copying', e);
  135. }
  136. }
  137.  
  138. // adapted from https://stackoverflow.com/a/35385518/1083697 by Mark Amery
  139. function htmlToElement(html) {
  140. const template = document.createElement('template');
  141. template.innerHTML = html.trim();
  142. return template.content.firstChild;
  143. }
  144.  
  145. function selfHostedButtonHtml(text, title) {
  146. const icon = '<span class="aui-icon aui-icon-small aui-iconfont-copy"></span>';
  147. return `<a href="#" class="aui-button aui-button-subtle" title="${title}"><span>${icon}${text}</span></a>`;
  148. }
  149.  
  150. function cloudButtonHtml(text, title) {
  151. const icon = cloudCopyIcon();
  152. // Custom CSS is needed to make the ${text} readable.
  153. const customCss = 'font-size: 16px; line-height: 26px;';
  154. // HTML & CSS classes from the "Watch this page" button
  155. const watchThisPageButton = document.querySelector('[data-id="page-watch-button"]');
  156. const buttonClasses = watchThisPageButton.className;
  157. const innerSpanClasses = watchThisPageButton.children[0].className;
  158. const innerInnerSpanClasses = watchThisPageButton.children[0].children[0].className;
  159. return htmlToElement(
  160. `<button class="${buttonClasses}" type="button">
  161. <span class="${innerSpanClasses}" title="${title}">
  162. <span class="${innerInnerSpanClasses}" role="img" style="--icon-primary-color: currentColor; --icon-secondary-color: var(--ds-surface, #FFFFFF); ${customCss}">
  163. ${icon}${text}
  164. </span>
  165. </span>
  166. </button>`
  167. );
  168. }
  169.  
  170. function copyButton(text, title, plainTextFn) {
  171. const onclick = (event) => copyClickAction(event, plainTextFn);
  172.  
  173. return onVersion(
  174. () => {
  175. const copyButtonAnchor = htmlToElement(selfHostedButtonHtml(text, title));
  176. copyButtonAnchor.onclick = onclick;
  177. const copyButtonListItem = htmlToElement('<li class="ajs-button normal"></li>');
  178. copyButtonListItem.appendChild(copyButtonAnchor);
  179. return copyButtonListItem;
  180. },
  181. () => {
  182. const button = cloudButtonHtml(text, title);
  183. button.onclick = onclick;
  184. return button;
  185. }
  186. );
  187. }
  188.  
  189. function htmlSyntaxLink(url, pageTitle) {
  190. const html = `<a href="${url}">${pageTitle}</a>`;
  191. return html;
  192. }
  193.  
  194. function markdownSyntaxLink(url, pageTitle) {
  195. return `[${pageTitle}](${url})`;
  196. }
  197.  
  198. function jiraSyntaxLink(url, pageTitle) {
  199. return `[${pageTitle}|${url}]`;
  200. }
  201.  
  202. function insertBefore(newElem, oldElem) {
  203. oldElem.parentNode.insertBefore(newElem, oldElem);
  204. }
  205.  
  206. // from https://stackoverflow.com/a/61511955/1083697 by Yong Wang
  207. function waitForElement(selector) {
  208. return new Promise(resolve => {
  209. if (document.querySelector(selector)) {
  210. return resolve(document.querySelector(selector));
  211. }
  212. const observer = new MutationObserver(mutations => {
  213. if (document.querySelector(selector)) {
  214. resolve(document.querySelector(selector));
  215. observer.disconnect();
  216. }
  217. });
  218.  
  219. observer.observe(document.body, {
  220. childList: true,
  221. subtree: true
  222. });
  223. });
  224. }
  225.  
  226. function createButtons() {
  227. onVersion(
  228. () => waitForElement('#action-menu-link'),
  229. () => waitForElement('button[aria-label="Share"]').then(shareButton => {
  230. // HTML of Cloud version is weird, lots of nesting and wrapping
  231. return shareButton.parentNode.parentNode.parentNode.parentNode;
  232. })
  233. ).then(target => {
  234. /*
  235. * Buttons are added to the left of the `target` element.
  236. */
  237. log('target', target);
  238. const markdownListItem = copyButton("[]()", "Copy Markdown link", markdownSyntaxLink);
  239. const jiraListItem = copyButton("[&#124;]", "Copy Jira syntax link", jiraSyntaxLink);
  240. insertBefore(markdownListItem, target);
  241. insertBefore(jiraListItem, target);
  242. log('Created buttons');
  243. });
  244. }
  245.  
  246. try {
  247. createButtons();
  248. } catch (e) {
  249. error('Could not create buttons', e);
  250. }
  251. })();