Skip to content

Commit 348cbb5

Browse files
committed
add disabled tooltip and fallback image convertion to png
1 parent 307c955 commit 348cbb5

File tree

6 files changed

+64
-6
lines changed

6 files changed

+64
-6
lines changed

.eslintrc.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,7 @@ rules:
199199
newline-per-chained-call: [0]
200200
no-alert: [0]
201201
no-array-constructor: [2]
202-
no-async-promise-executor: [2]
202+
no-async-promise-executor: [0]
203203
no-await-in-loop: [0]
204204
no-bitwise: [0]
205205
no-buffer-constructor: [0]

options/locale/locale_en-US.ini

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ copy_content = Copy content
9595
copy_branch = Copy branch name
9696
copy_success = Copied!
9797
copy_error = Copy failed
98+
copy_unsupported = This file type can not be copied
9899

99100
write = Write
100101
preview = Preview

templates/repo/view_file.tmpl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
{{end}}
3939
</div>
4040
<a download href="{{$.RawFileLink}}"><span class="btn-octicon tooltip" data-content="{{.locale.Tr "repo.download_file"}}" data-position="bottom center">{{svg "octicon-download"}}</span></a>
41-
<a id="copy-content" class="btn-octicon {{if .CanCopyContent}}tooltip{{else}}disabled{{end}}"{{if or .IsImageFile (and .HasSourceRenderedToggle (not .IsDisplayingSource))}} data-link="{{$.RawFileLink}}"{{end}}{{if .CanCopyContent}} data-content="{{.locale.Tr "copy_content"}}" aria-label="{{.locale.Tr "copy_content"}}"{{end}}>{{svg "octicon-copy" 14}}</a>
41+
<a id="copy-content" class="btn-octicon tooltip{{if not .CanCopyContent}} disabled{{end}}"{{if or .IsImageFile (and .HasSourceRenderedToggle (not .IsDisplayingSource))}} data-link="{{$.RawFileLink}}"{{end}} data-content="{{if .CanCopyContent}}{{.locale.Tr "copy_content"}}{{else}}{{.locale.Tr "copy_unsupported"}}{{end}}">{{svg "octicon-copy" 14}}</a>
4242
{{if .Repository.CanEnableEditor}}
4343
{{if .CanEditFile}}
4444
<a href="{{.RepoLink}}/_edit/{{PathEscapeSegments .BranchName}}/{{PathEscapeSegments .TreePath}}"><span class="btn-octicon tooltip" data-content="{{.EditFileTooltip}}" data-position="bottom center">{{svg "octicon-pencil"}}</span></a>

web_src/js/features/copycontent.js

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,20 @@
11
import {copyToClipboard} from './clipboard.js';
22
import {showTemporaryTooltip} from '../modules/tippy.js';
3+
import {imageBlobToPng} from '../utils.js';
34
const {i18n} = window.config;
45

6+
async function doCopy(content, btn) {
7+
const success = await copyToClipboard(content);
8+
showTemporaryTooltip(btn, success ? i18n.copy_success : i18n.copy_error);
9+
}
10+
511
export function initCopyContent() {
612
const btn = document.getElementById('copy-content');
713
if (!btn || btn.classList.contains('disabled')) return;
814

915
btn.addEventListener('click', async () => {
1016
if (btn.classList.contains('is-loading')) return;
11-
let content;
17+
let content, isImage;
1218
const link = btn.getAttribute('data-link');
1319

1420
// when data-link is present, we perform a fetch. this is either because
@@ -21,6 +27,7 @@ export function initCopyContent() {
2127
const contentType = res.headers.get('content-type');
2228

2329
if (contentType.startsWith('image/') && !contentType.startsWith('image/svg')) {
30+
isImage = true;
2431
content = await res.blob();
2532
} else {
2633
content = await res.text();
@@ -36,10 +43,18 @@ export function initCopyContent() {
3643
}
3744

3845
try {
39-
const success = await copyToClipboard(content);
40-
showTemporaryTooltip(btn, success ? i18n.copy_success : i18n.copy_error);
46+
await doCopy(content, btn);
4147
} catch {
42-
showTemporaryTooltip(btn, i18n.copy_error);
48+
if (isImage) {
49+
// convert image to png as last-resort as some browser only support png copy
50+
try {
51+
await doCopy(await imageBlobToPng(content), btn);
52+
} catch {
53+
showTemporaryTooltip(btn, i18n.copy_error);
54+
}
55+
} else {
56+
showTemporaryTooltip(btn, i18n.copy_error);
57+
}
4358
}
4459
});
4560
}

web_src/js/modules/tippy.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ export function createTippy(target, opts = {}) {
2727
export function initTooltip(el, props = {}) {
2828
const content = el.getAttribute('data-content') || props.content;
2929
if (!content) return null;
30+
if (!el.hasAttribute('aria-label')) el.setAttribute('aria-label', content);
3031
return createTippy(el, {
3132
content,
3233
delay: 100,

web_src/js/utils.js

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,3 +85,44 @@ export function translateMonth(month) {
8585
export function translateDay(day) {
8686
return new Date(Date.UTC(2022, 7, day)).toLocaleString(getCurrentLocale(), {weekday: 'short'});
8787
}
88+
89+
// convert a Blob to a DataURI
90+
function blobToDataURI(blob) {
91+
return new Promise((resolve, reject) => {
92+
try {
93+
const reader = new FileReader();
94+
reader.addEventListener('load', (e) => {
95+
resolve(e.target.result);
96+
});
97+
reader.addEventListener('error', () => {
98+
reject(new Error('FileReader failed'));
99+
});
100+
reader.readAsDataURL(blob);
101+
} catch (err) {
102+
reject(err);
103+
}
104+
});
105+
}
106+
107+
// convert a jpg (and possibly other formats) blob to a png blob
108+
export function imageBlobToPng(blob) {
109+
return new Promise(async (resolve, reject) => {
110+
try {
111+
const img = new Image();
112+
const canvas = document.createElement('canvas');
113+
img.addEventListener('load', () => {
114+
canvas.width = img.naturalWidth;
115+
canvas.height = img.naturalHeight;
116+
const context = canvas.getContext('2d');
117+
context.drawImage(img, 0, 0);
118+
canvas.toBlob(resolve, 'image/png');
119+
});
120+
img.addEventListener('error', () => {
121+
reject(new Error('Image convertion failed'));
122+
});
123+
img.src = await blobToDataURI(blob);
124+
} catch (err) {
125+
reject(err);
126+
}
127+
});
128+
}

0 commit comments

Comments
 (0)