testEnvironment: 'jsdom',
testMatch: ['<rootDir>/**/*.test.js'],
testTimeout: 20000,
- transform: {},
+ transform: {
+ '\\.svg$': 'jest-raw-loader',
+ },
verbose: false,
};
languageStr := string(language)
- preClasses := []string{}
+ preClasses := []string{"code-block"}
if languageStr == "mermaid" {
preClasses = append(preClasses, "is-loading")
}
- if len(preClasses) > 0 {
- _, err := w.WriteString(`<pre class="` + strings.Join(preClasses, " ") + `">`)
- if err != nil {
- return
- }
- } else {
- _, err := w.WriteString(`<pre>`)
- if err != nil {
- return
- }
+ _, err := w.WriteString(`<pre class="` + strings.Join(preClasses, " ") + `">`)
+ if err != nil {
+ return
}
// 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 {
return
}
func createDefaultPolicy() *bluemonday.Policy {
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
- policy.AllowAttrs("class").Matching(regexp.MustCompile(`^is-loading$`)).OnElements("pre")
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^(chroma )?language-[\w-]+$`)).OnElements("code")
// Checkboxes
remove_all = Remove All
edit = Edit
+copy = Copy
+copy_url = Copy URL
+copy_branch = Copy branch name
+copy_success = Copied!
+copy_error = Copy failed
+
write = Write
preview = Preview
loading = Loading…
fork_guest_user = Sign in to fork this repository.
watch_guest_user = Sign in to watch 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
watch = Watch
unstar = Unstar
"eslint-plugin-vue": "8.0.3",
"jest": "27.3.1",
"jest-extended": "1.1.0",
+ "jest-raw-loader": "1.0.1",
"postcss-less": "5.0.0",
"stylelint": "14.0.1",
"stylelint-config-standard": "23.0.0",
}
}
},
+ "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": {
"version": "27.0.6",
"resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-27.0.6.tgz",
"dev": true,
"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": {
"version": "27.0.6",
"resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-27.0.6.tgz",
"eslint-plugin-vue": "8.0.3",
"jest": "27.3.1",
"jest-extended": "1.1.0",
+ "jest-raw-loader": "1.0.1",
"postcss-less": "5.0.0",
"stylelint": "14.0.1",
"stylelint-config-standard": "23.0.0",
]).values()),
{{end}}
mermaidMaxSourceCharacters: {{MermaidMaxSourceCharacters}},
+ i18n: {
+ copy_success: '{{.i18n.Tr "copy_success"}}',
+ copy_error: '{{.i18n.Tr "copy_error"}}',
+ }
};
</script>
<link rel="icon" href="{{AssetUrlPrefix}}/img/logo.svg" type="image/svg+xml">
<input id="repo-clone-url" value="{{if $.PageIsWiki}}{{$.WikiCloneLink.SSH}}{{else}}{{$.CloneLink.SSH}}{{end}}" readonly>
{{end}}
{{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"}}
</button>
{{end}}
{{if .HeadBranchHTMLURL}}
{{$headHref = printf "<a href=\"%s\">%s</a>" (.HeadBranchHTMLURL | Escape) $headHref}}
{{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}}
{{if .BaseBranchHTMLURL}}
{{$baseHref = printf "<a href=\"%s\">%s</a>" (.BaseBranchHTMLURL | Escape) $baseHref}}
-// 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;
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);
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) {
$('.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')) {
--- /dev/null
+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);
+ }
+}
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
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');
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';
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,
--- /dev/null
+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"');
+});
--- /dev/null
+@keyframes isloadingspin {
+ 0% { transform: translate(-50%, -50%) rotate(0deg); }
+ 100% { transform: translate(-50%, -50%) rotate(360deg); }
+}
+
+.is-loading {
+ background: transparent !important;
+ color: transparent !important;
+ border: transparent !important;
+ pointer-events: none !important;
+ position: relative !important;
+ overflow: hidden !important;
+}
+
+.is-loading::after {
+ content: "";
+ position: absolute;
+ display: block;
+ width: 4rem;
+ height: 4rem;
+ left: 50%;
+ top: 50%;
+ transform: translate(-50%, -50%);
+ animation: isloadingspin 500ms infinite linear;
+ border-width: 4px;
+ border-style: solid;
+ border-color: #ececec #ececec #666 #666;
+ border-radius: 100%;
+}
+
+.markup pre.is-loading,
+.editor-loading.is-loading {
+ height: 12rem;
+}
+
+@keyframes fadein {
+ 0% {
+ opacity: 0;
+ }
+ 100% {
+ opacity: 1;
+ }
+}
+
+@keyframes fadeout {
+ 0% {
+ opacity: 1;
+ }
+ 100% {
+ opacity: 0;
+ }
+}
+++ /dev/null
-@keyframes isloadingspin {
- 0% { transform: translate(-50%, -50%) rotate(0deg); }
- 100% { transform: translate(-50%, -50%) rotate(360deg); }
-}
-
-.is-loading {
- background: transparent !important;
- color: transparent !important;
- border: transparent !important;
- pointer-events: none !important;
- position: relative !important;
- overflow: hidden !important;
-}
-
-.is-loading::after {
- content: "";
- position: absolute;
- display: block;
- width: 4rem;
- height: 4rem;
- left: 50%;
- top: 50%;
- transform: translate(-50%, -50%);
- animation: isloadingspin 500ms infinite linear;
- border-width: 4px;
- border-style: solid;
- border-color: #ececec #ececec #666 #666;
- border-radius: 100%;
-}
-
-.markup pre.is-loading,
-.editor-loading.is-loading {
- height: 12rem;
-}
@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";
@import "./features/projects.less";
@import "./markup/content.less";
@import "./markup/mermaid.less";
+@import "./markup/codecopy.less";
@import "./code/linebutton.less";
@import "./chroma/base.less";
--- /dev/null
+.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;
+}