diff options
author | silverwind <me@silverwind.io> | 2021-11-16 09:16:05 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-11-16 16:16:05 +0800 |
commit | 23bd7b1211a80aa3b0dcb60ec4a1c0089ff28dd4 (patch) | |
tree | 38fafd772b6341daa87cb256ee10dfdcf5a87a48 /web_src | |
parent | d789670894d09d7db96f4cd2dc3d57d2424eb753 (diff) | |
download | gitea-23bd7b1211a80aa3b0dcb60ec4a1c0089ff28dd4.tar.gz gitea-23bd7b1211a80aa3b0dcb60ec4a1c0089ff28dd4.zip |
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 <wxiaoguang@gmail.com>
* 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 <wxiaoguang@gmail.com>
Diffstat (limited to 'web_src')
-rw-r--r-- | web_src/js/features/clipboard.js | 34 | ||||
-rw-r--r-- | web_src/js/features/common-global.js | 2 | ||||
-rw-r--r-- | web_src/js/markup/codecopy.js | 16 | ||||
-rw-r--r-- | web_src/js/markup/content.js | 4 | ||||
-rw-r--r-- | web_src/js/markup/mermaid.js | 5 | ||||
-rw-r--r-- | web_src/js/svg.js | 2 | ||||
-rw-r--r-- | web_src/js/svg.test.js | 7 | ||||
-rw-r--r-- | web_src/less/animations.less (renamed from web_src/less/features/animations.less) | 18 | ||||
-rw-r--r-- | web_src/less/index.less | 3 | ||||
-rw-r--r-- | web_src/less/markup/codecopy.less | 32 |
10 files changed, 102 insertions, 21 deletions
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 <button data-clipboard-text><svg></button>, so we just search up to 3 levels for performance. + // in case <button data-clipboard-text><svg></button>, 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('<svg'); + expect(svg('octicon-repo', 16)).toInclude('width="16"'); + expect(svg('octicon-repo', 32)).toInclude('width="32"'); +}); diff --git a/web_src/less/features/animations.less b/web_src/less/animations.less index f3491155cd..cdb10236fb 100644 --- a/web_src/less/features/animations.less +++ b/web_src/less/animations.less @@ -32,3 +32,21 @@ .editor-loading.is-loading { height: 12rem; } + +@keyframes fadein { + 0% { + opacity: 0; + } + 100% { + opacity: 1; + } +} + +@keyframes fadeout { + 0% { + opacity: 1; + } + 100% { + opacity: 0; + } +} diff --git a/web_src/less/index.less b/web_src/less/index.less index d96fe3df82..0aa4a2f8f8 100644 --- a/web_src/less/index.less +++ b/web_src/less/index.less @@ -1,8 +1,8 @@ @import "font-awesome/css/font-awesome.css"; @import "./variables.less"; +@import "./animations.less"; @import "./shared/issuelist.less"; -@import "./features/animations.less"; @import "./features/dropzone.less"; @import "./features/gitgraph.less"; @import "./features/heatmap.less"; @@ -11,6 +11,7 @@ @import "./features/projects.less"; @import "./markup/content.less"; @import "./markup/mermaid.less"; +@import "./markup/codecopy.less"; @import "./code/linebutton.less"; @import "./chroma/base.less"; diff --git a/web_src/less/markup/codecopy.less b/web_src/less/markup/codecopy.less new file mode 100644 index 0000000000..b2ce77aaa1 --- /dev/null +++ b/web_src/less/markup/codecopy.less @@ -0,0 +1,32 @@ +.markup .code-block { + position: relative; +} + +.markup .code-copy { + position: absolute; + top: 8px; + right: 6px; + padding: 9px; + visibility: hidden; + animation: fadeout .2s both; +} + +/* adjustments for comment content having only 14px font size */ +.repository.view.issue .comment-list .comment .markup .code-copy { + right: 5px; + padding: 8px; +} + +/* can not use regular transparent button colors for hover and active states because + we need opaque colors here as code can appear behind the button */ +.markup .code-copy:hover { + background: var(--color-secondary) !important; +} +.markup .code-copy:active { + background: var(--color-secondary-dark-1) !important; +} + +.markup .code-block:hover .code-copy { + visibility: visible; + animation: fadein .2s both; +} |