mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-31 10:56:10 +01:00 
			
		
		
		
	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:
		| @@ -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 New Issue
	
	Block a user