aboutsummaryrefslogtreecommitdiffstats
path: root/web_src
diff options
context:
space:
mode:
authorsilverwind <me@silverwind.io>2021-11-16 09:16:05 +0100
committerGitHub <noreply@github.com>2021-11-16 16:16:05 +0800
commit23bd7b1211a80aa3b0dcb60ec4a1c0089ff28dd4 (patch)
tree38fafd772b6341daa87cb256ee10dfdcf5a87a48 /web_src
parentd789670894d09d7db96f4cd2dc3d57d2424eb753 (diff)
downloadgitea-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.js34
-rw-r--r--web_src/js/features/common-global.js2
-rw-r--r--web_src/js/markup/codecopy.js16
-rw-r--r--web_src/js/markup/content.js4
-rw-r--r--web_src/js/markup/mermaid.js5
-rw-r--r--web_src/js/svg.js2
-rw-r--r--web_src/js/svg.test.js7
-rw-r--r--web_src/less/animations.less (renamed from web_src/less/features/animations.less)18
-rw-r--r--web_src/less/index.less3
-rw-r--r--web_src/less/markup/codecopy.less32
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;
+}