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>
This commit is contained in:
parent
d789670894
commit
23bd7b1211
19 changed files with 140 additions and 44 deletions
|
@ -4,7 +4,9 @@ export default {
|
||||||
testEnvironment: 'jsdom',
|
testEnvironment: 'jsdom',
|
||||||
testMatch: ['<rootDir>/**/*.test.js'],
|
testMatch: ['<rootDir>/**/*.test.js'],
|
||||||
testTimeout: 20000,
|
testTimeout: 20000,
|
||||||
transform: {},
|
transform: {
|
||||||
|
'\\.svg$': 'jest-raw-loader',
|
||||||
|
},
|
||||||
verbose: false,
|
verbose: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -107,25 +107,18 @@ func actualRender(ctx *markup.RenderContext, input io.Reader, output io.Writer)
|
||||||
|
|
||||||
languageStr := string(language)
|
languageStr := string(language)
|
||||||
|
|
||||||
preClasses := []string{}
|
preClasses := []string{"code-block"}
|
||||||
if languageStr == "mermaid" {
|
if languageStr == "mermaid" {
|
||||||
preClasses = append(preClasses, "is-loading")
|
preClasses = append(preClasses, "is-loading")
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(preClasses) > 0 {
|
_, err := w.WriteString(`<pre class="` + strings.Join(preClasses, " ") + `">`)
|
||||||
_, err := w.WriteString(`<pre class="` + strings.Join(preClasses, " ") + `">`)
|
if err != nil {
|
||||||
if err != nil {
|
return
|
||||||
return
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
_, err := w.WriteString(`<pre>`)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// include language-x class as part of commonmark spec
|
// include language-x class as part of commonmark spec
|
||||||
_, err := w.WriteString(`<code class="chroma language-` + string(language) + `">`)
|
_, err = w.WriteString(`<code class="chroma language-` + string(language) + `">`)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -52,8 +52,11 @@ func InitializeSanitizer() {
|
||||||
|
|
||||||
func createDefaultPolicy() *bluemonday.Policy {
|
func createDefaultPolicy() *bluemonday.Policy {
|
||||||
policy := bluemonday.UGCPolicy()
|
policy := bluemonday.UGCPolicy()
|
||||||
|
|
||||||
|
// For JS code copy and Mermaid loading state
|
||||||
|
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^code-block( is-loading)?$`)).OnElements("pre")
|
||||||
|
|
||||||
// For Chroma markdown plugin
|
// For Chroma markdown plugin
|
||||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^is-loading$`)).OnElements("pre")
|
|
||||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^(chroma )?language-[\w-]+$`)).OnElements("code")
|
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^(chroma )?language-[\w-]+$`)).OnElements("code")
|
||||||
|
|
||||||
// Checkboxes
|
// Checkboxes
|
||||||
|
|
|
@ -85,6 +85,12 @@ remove = Remove
|
||||||
remove_all = Remove All
|
remove_all = Remove All
|
||||||
edit = Edit
|
edit = Edit
|
||||||
|
|
||||||
|
copy = Copy
|
||||||
|
copy_url = Copy URL
|
||||||
|
copy_branch = Copy branch name
|
||||||
|
copy_success = Copied!
|
||||||
|
copy_error = Copy failed
|
||||||
|
|
||||||
write = Write
|
write = Write
|
||||||
preview = Preview
|
preview = Preview
|
||||||
loading = Loading…
|
loading = Loading…
|
||||||
|
@ -927,13 +933,6 @@ fork_from_self = You cannot fork a repository you own.
|
||||||
fork_guest_user = Sign in to fork this repository.
|
fork_guest_user = Sign in to fork this repository.
|
||||||
watch_guest_user = Sign in to watch this repository.
|
watch_guest_user = Sign in to watch this repository.
|
||||||
star_guest_user = Sign in to star this repository.
|
star_guest_user = Sign in to star this repository.
|
||||||
copy_link = Copy
|
|
||||||
copy_link_success = Link has been copied
|
|
||||||
copy_link_error = Use ⌘C or Ctrl-C to copy
|
|
||||||
copy_branch = Copy
|
|
||||||
copy_branch_success = Branch name has been copied
|
|
||||||
copy_branch_error = Use ⌘C or Ctrl-C to copy
|
|
||||||
copied = Copied OK
|
|
||||||
unwatch = Unwatch
|
unwatch = Unwatch
|
||||||
watch = Watch
|
watch = Watch
|
||||||
unstar = Unstar
|
unstar = Unstar
|
||||||
|
|
13
package-lock.json
generated
13
package-lock.json
generated
|
@ -51,6 +51,7 @@
|
||||||
"eslint-plugin-vue": "8.0.3",
|
"eslint-plugin-vue": "8.0.3",
|
||||||
"jest": "27.3.1",
|
"jest": "27.3.1",
|
||||||
"jest-extended": "1.1.0",
|
"jest-extended": "1.1.0",
|
||||||
|
"jest-raw-loader": "1.0.1",
|
||||||
"postcss-less": "5.0.0",
|
"postcss-less": "5.0.0",
|
||||||
"stylelint": "14.0.1",
|
"stylelint": "14.0.1",
|
||||||
"stylelint-config-standard": "23.0.0",
|
"stylelint-config-standard": "23.0.0",
|
||||||
|
@ -6221,6 +6222,12 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/jest-raw-loader": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/jest-raw-loader/-/jest-raw-loader-1.0.1.tgz",
|
||||||
|
"integrity": "sha1-zp9W1UZQ8VfEp9FtIkul1hO81iY=",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"node_modules/jest-regex-util": {
|
"node_modules/jest-regex-util": {
|
||||||
"version": "27.0.6",
|
"version": "27.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-27.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-27.0.6.tgz",
|
||||||
|
@ -14693,6 +14700,12 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {}
|
"requires": {}
|
||||||
},
|
},
|
||||||
|
"jest-raw-loader": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/jest-raw-loader/-/jest-raw-loader-1.0.1.tgz",
|
||||||
|
"integrity": "sha1-zp9W1UZQ8VfEp9FtIkul1hO81iY=",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"jest-regex-util": {
|
"jest-regex-util": {
|
||||||
"version": "27.0.6",
|
"version": "27.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-27.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-27.0.6.tgz",
|
||||||
|
|
|
@ -51,6 +51,7 @@
|
||||||
"eslint-plugin-vue": "8.0.3",
|
"eslint-plugin-vue": "8.0.3",
|
||||||
"jest": "27.3.1",
|
"jest": "27.3.1",
|
||||||
"jest-extended": "1.1.0",
|
"jest-extended": "1.1.0",
|
||||||
|
"jest-raw-loader": "1.0.1",
|
||||||
"postcss-less": "5.0.0",
|
"postcss-less": "5.0.0",
|
||||||
"stylelint": "14.0.1",
|
"stylelint": "14.0.1",
|
||||||
"stylelint-config-standard": "23.0.0",
|
"stylelint-config-standard": "23.0.0",
|
||||||
|
|
|
@ -46,6 +46,10 @@
|
||||||
]).values()),
|
]).values()),
|
||||||
{{end}}
|
{{end}}
|
||||||
mermaidMaxSourceCharacters: {{MermaidMaxSourceCharacters}},
|
mermaidMaxSourceCharacters: {{MermaidMaxSourceCharacters}},
|
||||||
|
i18n: {
|
||||||
|
copy_success: '{{.i18n.Tr "copy_success"}}',
|
||||||
|
copy_error: '{{.i18n.Tr "copy_error"}}',
|
||||||
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
<link rel="icon" href="{{AssetUrlPrefix}}/img/logo.svg" type="image/svg+xml">
|
<link rel="icon" href="{{AssetUrlPrefix}}/img/logo.svg" type="image/svg+xml">
|
||||||
|
|
|
@ -14,7 +14,7 @@
|
||||||
<input id="repo-clone-url" value="{{if $.PageIsWiki}}{{$.WikiCloneLink.SSH}}{{else}}{{$.CloneLink.SSH}}{{end}}" readonly>
|
<input id="repo-clone-url" value="{{if $.PageIsWiki}}{{$.WikiCloneLink.SSH}}{{else}}{{$.CloneLink.SSH}}{{end}}" readonly>
|
||||||
{{end}}
|
{{end}}
|
||||||
{{if or (not $.DisableHTTP) (and (not $.DisableSSH) (or $.IsSigned $.ExposeAnonSSH))}}
|
{{if or (not $.DisableHTTP) (and (not $.DisableSSH) (or $.IsSigned $.ExposeAnonSSH))}}
|
||||||
<button class="ui basic icon button poping up" id="clipboard-btn" data-success="{{.i18n.Tr "repo.copy_link_success"}}" data-error="{{.i18n.Tr "repo.copy_link_error"}}" data-content="{{.i18n.Tr "repo.copy_link"}}" data-variation="inverted tiny" data-clipboard-target="#repo-clone-url">
|
<button class="ui basic icon button poping up" id="clipboard-btn" data-content="{{.i18n.Tr "copy_url"}}" data-clipboard-target="#repo-clone-url">
|
||||||
{{svg "octicon-paste"}}
|
{{svg "octicon-paste"}}
|
||||||
</button>
|
</button>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
|
@ -34,7 +34,7 @@
|
||||||
{{if .HeadBranchHTMLURL}}
|
{{if .HeadBranchHTMLURL}}
|
||||||
{{$headHref = printf "<a href=\"%s\">%s</a>" (.HeadBranchHTMLURL | Escape) $headHref}}
|
{{$headHref = printf "<a href=\"%s\">%s</a>" (.HeadBranchHTMLURL | Escape) $headHref}}
|
||||||
{{end}}
|
{{end}}
|
||||||
{{$headHref = printf "%s <a class=\"poping up\" data-content=\"%s\" data-success=\"%s\" data-error=\"%s\" data-clipboard-text=\"%s\" data-variation=\"inverted tiny\">%s</a>" $headHref (.i18n.Tr "repo.copy_branch") (.i18n.Tr "repo.copy_branch_success") (.i18n.Tr "repo.copy_branch_error") (.HeadTarget | Escape) (svg "octicon-copy" 14)}}
|
{{$headHref = printf "%s <a class=\"poping up\" data-content=\"%s\" data-clipboard-text=\"%s\">%s</a>" $headHref (.i18n.Tr "copy_branch") (.HeadTarget | Escape) (svg "octicon-copy" 14)}}
|
||||||
{{$baseHref := .BaseTarget|Escape}}
|
{{$baseHref := .BaseTarget|Escape}}
|
||||||
{{if .BaseBranchHTMLURL}}
|
{{if .BaseBranchHTMLURL}}
|
||||||
{{$baseHref = printf "<a href=\"%s\">%s</a>" (.BaseBranchHTMLURL | Escape) $baseHref}}
|
{{$baseHref = printf "<a href=\"%s\">%s</a>" (.BaseBranchHTMLURL | Escape) $baseHref}}
|
||||||
|
|
|
@ -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) {
|
function onSuccess(btn) {
|
||||||
if (!btn.dataset.content) return;
|
btn.setAttribute('data-variation', 'inverted tiny');
|
||||||
$(btn).popup('destroy');
|
$(btn).popup('destroy');
|
||||||
const oldContent = btn.dataset.content;
|
const oldContent = btn.getAttribute('data-content');
|
||||||
btn.dataset.content = btn.dataset.success;
|
btn.setAttribute('data-content', copy_success);
|
||||||
$(btn).popup('show');
|
$(btn).popup('show');
|
||||||
btn.dataset.content = oldContent;
|
btn.setAttribute('data-content', oldContent || '');
|
||||||
}
|
}
|
||||||
function onError(btn) {
|
function onError(btn) {
|
||||||
if (!btn.dataset.content) return;
|
btn.setAttribute('data-variation', 'inverted tiny');
|
||||||
const oldContent = btn.dataset.content;
|
const oldContent = btn.getAttribute('data-content');
|
||||||
$(btn).popup('destroy');
|
$(btn).popup('destroy');
|
||||||
btn.dataset.content = btn.dataset.error;
|
btn.setAttribute('data-content', copy_error);
|
||||||
$(btn).popup('show');
|
$(btn).popup('show');
|
||||||
btn.dataset.content = oldContent;
|
btn.setAttribute('data-content', oldContent || '');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Fallback to use if navigator.clipboard doesn't exist.
|
// Fallback to use if navigator.clipboard doesn't exist. Achieved via creating
|
||||||
* Achieved via creating a temporary textarea element, selecting the text, and using document.execCommand.
|
// a temporary textarea element, selecting the text, and using document.execCommand
|
||||||
*/
|
|
||||||
function fallbackCopyToClipboard(text) {
|
function fallbackCopyToClipboard(text) {
|
||||||
if (!document.execCommand) return false;
|
if (!document.execCommand) return false;
|
||||||
|
|
||||||
|
@ -37,7 +35,8 @@ function fallbackCopyToClipboard(text) {
|
||||||
|
|
||||||
tempTextArea.select();
|
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');
|
const success = document.execCommand('copy');
|
||||||
|
|
||||||
document.body.removeChild(tempTextArea);
|
document.body.removeChild(tempTextArea);
|
||||||
|
@ -45,10 +44,13 @@ function fallbackCopyToClipboard(text) {
|
||||||
return success;
|
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() {
|
export default function initGlobalCopyToClipboardListener() {
|
||||||
document.addEventListener('click', (e) => {
|
document.addEventListener('click', (e) => {
|
||||||
let target = e.target;
|
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++) {
|
for (let i = 0; i < 3 && target; i++) {
|
||||||
let text;
|
let text;
|
||||||
if (target.dataset.clipboardText) {
|
if (target.dataset.clipboardText) {
|
||||||
|
|
|
@ -104,7 +104,7 @@ export function initGlobalCommon() {
|
||||||
$('.ui.progress').progress({
|
$('.ui.progress').progress({
|
||||||
showActivity: false
|
showActivity: false
|
||||||
});
|
});
|
||||||
$('.poping.up').popup();
|
$('.poping.up').attr('data-variation', 'inverted tiny').popup();
|
||||||
$('.top.menu .poping.up').popup({
|
$('.top.menu .poping.up').popup({
|
||||||
onShow() {
|
onShow() {
|
||||||
if ($('.top.menu .menu.transition').hasClass('visible')) {
|
if ($('.top.menu .menu.transition').hasClass('visible')) {
|
||||||
|
|
16
web_src/js/markup/codecopy.js
Normal file
16
web_src/js/markup/codecopy.js
Normal file
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,9 +1,11 @@
|
||||||
import {renderMermaid} from './mermaid.js';
|
import {renderMermaid} from './mermaid.js';
|
||||||
|
import {renderCodeCopy} from './codecopy.js';
|
||||||
import {initMarkupTasklist} from './tasklist.js';
|
import {initMarkupTasklist} from './tasklist.js';
|
||||||
|
|
||||||
// code that runs for all markup content
|
// code that runs for all markup content
|
||||||
export function initMarkupContent() {
|
export function initMarkupContent() {
|
||||||
const _promise = renderMermaid(document.querySelectorAll('code.language-mermaid'));
|
renderMermaid();
|
||||||
|
renderCodeCopy();
|
||||||
}
|
}
|
||||||
|
|
||||||
// code that only runs for comments
|
// code that only runs for comments
|
||||||
|
|
|
@ -8,8 +8,9 @@ function displayError(el, err) {
|
||||||
el.closest('pre').before(errorNode);
|
el.closest('pre').before(errorNode);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function renderMermaid(els) {
|
export async function renderMermaid() {
|
||||||
if (!els || !els.length) return;
|
const els = document.querySelectorAll('.markup code.language-mermaid');
|
||||||
|
if (!els.length) return;
|
||||||
|
|
||||||
const {default: mermaid} = await import(/* webpackChunkName: "mermaid" */'mermaid');
|
const {default: mermaid} = await import(/* webpackChunkName: "mermaid" */'mermaid');
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import octiconChevronDown from '../../public/img/svg/octicon-chevron-down.svg';
|
import octiconChevronDown from '../../public/img/svg/octicon-chevron-down.svg';
|
||||||
import octiconChevronRight from '../../public/img/svg/octicon-chevron-right.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 octiconGitMerge from '../../public/img/svg/octicon-git-merge.svg';
|
||||||
import octiconGitPullRequest from '../../public/img/svg/octicon-git-pull-request.svg';
|
import octiconGitPullRequest from '../../public/img/svg/octicon-git-pull-request.svg';
|
||||||
import octiconIssueClosed from '../../public/img/svg/octicon-issue-closed.svg';
|
import octiconIssueClosed from '../../public/img/svg/octicon-issue-closed.svg';
|
||||||
|
@ -20,6 +21,7 @@ import Vue from 'vue';
|
||||||
export const svgs = {
|
export const svgs = {
|
||||||
'octicon-chevron-down': octiconChevronDown,
|
'octicon-chevron-down': octiconChevronDown,
|
||||||
'octicon-chevron-right': octiconChevronRight,
|
'octicon-chevron-right': octiconChevronRight,
|
||||||
|
'octicon-copy': octiconCopy,
|
||||||
'octicon-git-merge': octiconGitMerge,
|
'octicon-git-merge': octiconGitMerge,
|
||||||
'octicon-git-pull-request': octiconGitPullRequest,
|
'octicon-git-pull-request': octiconGitPullRequest,
|
||||||
'octicon-issue-closed': octiconIssueClosed,
|
'octicon-issue-closed': octiconIssueClosed,
|
||||||
|
|
7
web_src/js/svg.test.js
Normal file
7
web_src/js/svg.test.js
Normal file
|
@ -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"');
|
||||||
|
});
|
|
@ -32,3 +32,21 @@
|
||||||
.editor-loading.is-loading {
|
.editor-loading.is-loading {
|
||||||
height: 12rem;
|
height: 12rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@keyframes fadein {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeout {
|
||||||
|
0% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,8 +1,8 @@
|
||||||
@import "font-awesome/css/font-awesome.css";
|
@import "font-awesome/css/font-awesome.css";
|
||||||
|
|
||||||
@import "./variables.less";
|
@import "./variables.less";
|
||||||
|
@import "./animations.less";
|
||||||
@import "./shared/issuelist.less";
|
@import "./shared/issuelist.less";
|
||||||
@import "./features/animations.less";
|
|
||||||
@import "./features/dropzone.less";
|
@import "./features/dropzone.less";
|
||||||
@import "./features/gitgraph.less";
|
@import "./features/gitgraph.less";
|
||||||
@import "./features/heatmap.less";
|
@import "./features/heatmap.less";
|
||||||
|
@ -11,6 +11,7 @@
|
||||||
@import "./features/projects.less";
|
@import "./features/projects.less";
|
||||||
@import "./markup/content.less";
|
@import "./markup/content.less";
|
||||||
@import "./markup/mermaid.less";
|
@import "./markup/mermaid.less";
|
||||||
|
@import "./markup/codecopy.less";
|
||||||
@import "./code/linebutton.less";
|
@import "./code/linebutton.less";
|
||||||
|
|
||||||
@import "./chroma/base.less";
|
@import "./chroma/base.less";
|
||||||
|
|
32
web_src/less/markup/codecopy.less
Normal file
32
web_src/less/markup/codecopy.less
Normal file
|
@ -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;
|
||||||
|
}
|
Reference in a new issue