From 23bd7b1211a80aa3b0dcb60ec4a1c0089ff28dd4 Mon Sep 17 00:00:00 2001 From: silverwind Date: Tue, 16 Nov 2021 09:16:05 +0100 Subject: Add copy button to markdown code blocks (#17638) * Add copy button to markdown code blocks Done mostly in JS because I think it's better not to try getting buttons past the markup sanitizer. * add svg module tests * fix sanitizer regexp * remove outdated comment * vertically center button in issue comments as well * add comment to css * fix undefined on view file line copy * combine animation less files * Update modules/markup/markdown/markdown.go Co-authored-by: wxiaoguang * add test for different sizes * add cloneNode and add tests for it * use deep clone * remove useless optional chaining * remove the svg node cache * unify clipboard copy string and i18n * remove unused var * remove unused localization * minor css tweaks to the button * comment tweak * remove useless attribute Co-authored-by: wxiaoguang --- web_src/js/features/clipboard.js | 34 ++++++++++++----------- web_src/js/features/common-global.js | 2 +- web_src/js/markup/codecopy.js | 16 +++++++++++ web_src/js/markup/content.js | 4 ++- web_src/js/markup/mermaid.js | 5 ++-- web_src/js/svg.js | 2 ++ web_src/js/svg.test.js | 7 +++++ web_src/less/animations.less | 52 +++++++++++++++++++++++++++++++++++ web_src/less/features/animations.less | 34 ----------------------- web_src/less/index.less | 3 +- web_src/less/markup/codecopy.less | 32 +++++++++++++++++++++ 11 files changed, 136 insertions(+), 55 deletions(-) create mode 100644 web_src/js/markup/codecopy.js create mode 100644 web_src/js/svg.test.js create mode 100644 web_src/less/animations.less delete mode 100644 web_src/less/features/animations.less create mode 100644 web_src/less/markup/codecopy.less (limited to 'web_src') diff --git a/web_src/js/features/clipboard.js b/web_src/js/features/clipboard.js index 89aface93a..b0c4134537 100644 --- a/web_src/js/features/clipboard.js +++ b/web_src/js/features/clipboard.js @@ -1,27 +1,25 @@ -// For all DOM elements with [data-clipboard-target] or [data-clipboard-text], this copy-to-clipboard will work for them +const {copy_success, copy_error} = window.config.i18n; -// TODO: replace these with toast-style notifications function onSuccess(btn) { - if (!btn.dataset.content) return; + btn.setAttribute('data-variation', 'inverted tiny'); $(btn).popup('destroy'); - const oldContent = btn.dataset.content; - btn.dataset.content = btn.dataset.success; + const oldContent = btn.getAttribute('data-content'); + btn.setAttribute('data-content', copy_success); $(btn).popup('show'); - btn.dataset.content = oldContent; + btn.setAttribute('data-content', oldContent || ''); } function onError(btn) { - if (!btn.dataset.content) return; - const oldContent = btn.dataset.content; + btn.setAttribute('data-variation', 'inverted tiny'); + const oldContent = btn.getAttribute('data-content'); $(btn).popup('destroy'); - btn.dataset.content = btn.dataset.error; + btn.setAttribute('data-content', copy_error); $(btn).popup('show'); - btn.dataset.content = oldContent; + btn.setAttribute('data-content', oldContent || ''); } -/** - * Fallback to use if navigator.clipboard doesn't exist. - * Achieved via creating a temporary textarea element, selecting the text, and using document.execCommand. - */ + +// Fallback to use if navigator.clipboard doesn't exist. Achieved via creating +// a temporary textarea element, selecting the text, and using document.execCommand function fallbackCopyToClipboard(text) { if (!document.execCommand) return false; @@ -37,7 +35,8 @@ function fallbackCopyToClipboard(text) { tempTextArea.select(); - // if unsecure (not https), there is no navigator.clipboard, but we can still use document.execCommand to copy to clipboard + // if unsecure (not https), there is no navigator.clipboard, but we can still + // use document.execCommand to copy to clipboard const success = document.execCommand('copy'); document.body.removeChild(tempTextArea); @@ -45,10 +44,13 @@ function fallbackCopyToClipboard(text) { return success; } +// For all DOM elements with [data-clipboard-target] or [data-clipboard-text], +// this copy-to-clipboard will work for them export default function initGlobalCopyToClipboardListener() { document.addEventListener('click', (e) => { let target = e.target; - // in case , so we just search up to 3 levels for performance. + // in case , so we just search + // up to 3 levels for performance for (let i = 0; i < 3 && target; i++) { let text; if (target.dataset.clipboardText) { diff --git a/web_src/js/features/common-global.js b/web_src/js/features/common-global.js index da3fb9d1e3..ac9d0cc92d 100644 --- a/web_src/js/features/common-global.js +++ b/web_src/js/features/common-global.js @@ -104,7 +104,7 @@ export function initGlobalCommon() { $('.ui.progress').progress({ showActivity: false }); - $('.poping.up').popup(); + $('.poping.up').attr('data-variation', 'inverted tiny').popup(); $('.top.menu .poping.up').popup({ onShow() { if ($('.top.menu .menu.transition').hasClass('visible')) { diff --git a/web_src/js/markup/codecopy.js b/web_src/js/markup/codecopy.js new file mode 100644 index 0000000000..2aa7070c72 --- /dev/null +++ b/web_src/js/markup/codecopy.js @@ -0,0 +1,16 @@ +import {svg} from '../svg.js'; + +export function renderCodeCopy() { + const els = document.querySelectorAll('.markup .code-block code'); + if (!els.length) return; + + const button = document.createElement('button'); + button.classList.add('code-copy', 'ui', 'button'); + button.innerHTML = svg('octicon-copy'); + + for (const el of els) { + const btn = button.cloneNode(true); + btn.setAttribute('data-clipboard-text', el.textContent); + el.after(btn); + } +} diff --git a/web_src/js/markup/content.js b/web_src/js/markup/content.js index 0564199bbf..ef5067fd66 100644 --- a/web_src/js/markup/content.js +++ b/web_src/js/markup/content.js @@ -1,9 +1,11 @@ import {renderMermaid} from './mermaid.js'; +import {renderCodeCopy} from './codecopy.js'; import {initMarkupTasklist} from './tasklist.js'; // code that runs for all markup content export function initMarkupContent() { - const _promise = renderMermaid(document.querySelectorAll('code.language-mermaid')); + renderMermaid(); + renderCodeCopy(); } // code that only runs for comments diff --git a/web_src/js/markup/mermaid.js b/web_src/js/markup/mermaid.js index f9f069ed1e..7c7ee26c3c 100644 --- a/web_src/js/markup/mermaid.js +++ b/web_src/js/markup/mermaid.js @@ -8,8 +8,9 @@ function displayError(el, err) { el.closest('pre').before(errorNode); } -export async function renderMermaid(els) { - if (!els || !els.length) return; +export async function renderMermaid() { + const els = document.querySelectorAll('.markup code.language-mermaid'); + if (!els.length) return; const {default: mermaid} = await import(/* webpackChunkName: "mermaid" */'mermaid'); diff --git a/web_src/js/svg.js b/web_src/js/svg.js index 11be6b476c..77aa1e7ca7 100644 --- a/web_src/js/svg.js +++ b/web_src/js/svg.js @@ -1,5 +1,6 @@ import octiconChevronDown from '../../public/img/svg/octicon-chevron-down.svg'; import octiconChevronRight from '../../public/img/svg/octicon-chevron-right.svg'; +import octiconCopy from '../../public/img/svg/octicon-copy.svg'; import octiconGitMerge from '../../public/img/svg/octicon-git-merge.svg'; import octiconGitPullRequest from '../../public/img/svg/octicon-git-pull-request.svg'; import octiconIssueClosed from '../../public/img/svg/octicon-issue-closed.svg'; @@ -20,6 +21,7 @@ import Vue from 'vue'; export const svgs = { 'octicon-chevron-down': octiconChevronDown, 'octicon-chevron-right': octiconChevronRight, + 'octicon-copy': octiconCopy, 'octicon-git-merge': octiconGitMerge, 'octicon-git-pull-request': octiconGitPullRequest, 'octicon-issue-closed': octiconIssueClosed, diff --git a/web_src/js/svg.test.js b/web_src/js/svg.test.js new file mode 100644 index 0000000000..f1939c3a46 --- /dev/null +++ b/web_src/js/svg.test.js @@ -0,0 +1,7 @@ +import {svg} from './svg.js'; + +test('svg', () => { + expect(svg('octicon-repo')).toStartWith('