From 914399bc759848fe5cccad1e59aa62908b63a63f Mon Sep 17 00:00:00 2001 From: silverwind Date: Thu, 17 Nov 2022 00:44:46 +0100 Subject: [PATCH 01/18] Improvements for Content Copy It now supports copying Markdown, SVG and Images (not in Firefox currently because of lacking ClipboardItem support). It will fetch the data if in a rendered view or when it's an image. --- options/locale/locale_en-US.ini | 1 - routers/web/repo/view.go | 9 +++++- templates/repo/view_file.tmpl | 6 +--- web_src/js/features/clipboard.js | 15 ++++++---- web_src/js/features/copycontent.js | 44 ++++++++++++++++++++++++++++++ web_src/js/features/repo-code.js | 16 +---------- web_src/js/index.js | 2 ++ web_src/less/animations.less | 6 ++++ 8 files changed, 72 insertions(+), 27 deletions(-) create mode 100644 web_src/js/features/copycontent.js diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index ce93e92d3455..2d6f174e223e 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -1096,7 +1096,6 @@ editor.cannot_edit_non_text_files = Binary files cannot be edited in the web int editor.edit_this_file = Edit File editor.this_file_locked = File is locked editor.must_be_on_a_branch = You must be on a branch to make or propose changes to this file. -editor.only_copy_raw = You may only copy raw text files. editor.fork_before_edit = You must fork this repository to make or propose changes to this file. editor.delete_this_file = Delete File editor.must_have_write_access = You must have write access to make or propose changes to this file. diff --git a/routers/web/repo/view.go b/routers/web/repo/view.go index 7a9e44ff5e39..1d1ba250646c 100644 --- a/routers/web/repo/view.go +++ b/routers/web/repo/view.go @@ -443,7 +443,12 @@ func renderFile(ctx *context.Context, entry *git.TreeEntry, treeLink, rawLink st ctx.Data["IsRepresentableAsText"] = isRepresentableAsText ctx.Data["IsDisplayingSource"] = isDisplayingSource ctx.Data["IsDisplayingRendered"] = isDisplayingRendered - ctx.Data["IsTextSource"] = isTextFile || isDisplayingSource + + isTextSource := isTextFile || isDisplayingSource + ctx.Data["IsTextSource"] = isTextSource + if isTextSource { + ctx.Data["CanCopyContent"] = true + } // Check LFS Lock lfsLock, err := git_model.GetTreePathLock(ctx.Repo.Repository.ID, ctx.Repo.TreePath) @@ -474,6 +479,7 @@ func renderFile(ctx *context.Context, entry *git.TreeEntry, treeLink, rawLink st case isRepresentableAsText: if st.IsSvgImage() { ctx.Data["IsImageFile"] = true + ctx.Data["CanCopyContent"] = true ctx.Data["HasSourceRenderedToggle"] = true } @@ -608,6 +614,7 @@ func renderFile(ctx *context.Context, entry *git.TreeEntry, treeLink, rawLink st ctx.Data["IsAudioFile"] = true case st.IsImage() && (setting.UI.SVG.Enabled || !st.IsSvgImage()): ctx.Data["IsImageFile"] = true + ctx.Data["CanCopyContent"] = true default: if fileSize >= setting.UI.MaxDisplayFileSize { ctx.Data["IsFileTooLarge"] = true diff --git a/templates/repo/view_file.tmpl b/templates/repo/view_file.tmpl index 321600a9975f..e346f1b1de45 100644 --- a/templates/repo/view_file.tmpl +++ b/templates/repo/view_file.tmpl @@ -38,11 +38,7 @@ {{end}} {{svg "octicon-download"}} - {{if or .IsMarkup .IsRenderedHTML (not .IsTextSource)}} - {{svg "octicon-copy" 14}} - {{else}} - {{svg "octicon-copy" 14}} - {{end}} + {{svg "octicon-copy" 14}} {{if .Repository.CanEnableEditor}} {{if .CanEditFile}} {{svg "octicon-pencil"}} diff --git a/web_src/js/features/clipboard.js b/web_src/js/features/clipboard.js index 85324303e347..6bcb484b9425 100644 --- a/web_src/js/features/clipboard.js +++ b/web_src/js/features/clipboard.js @@ -2,11 +2,16 @@ import {showTemporaryTooltip} from '../modules/tippy.js'; const {copy_success, copy_error} = window.config.i18n; -export async function copyToClipboard(text) { - try { - await navigator.clipboard.writeText(text); - } catch { - return fallbackCopyToClipboard(text); +export async function copyToClipboard(content) { + if (typeof content === 'string') { + try { + await navigator.clipboard.writeText(content); + } catch { + return fallbackCopyToClipboard(content); + } + } else { // blob, this may throw in unsupporting browsers + const item = new window.ClipboardItem({[content.type]: content}); + await navigator.clipboard.write([item]); } return true; } diff --git a/web_src/js/features/copycontent.js b/web_src/js/features/copycontent.js new file mode 100644 index 000000000000..4d59922eca0d --- /dev/null +++ b/web_src/js/features/copycontent.js @@ -0,0 +1,44 @@ +import {copyToClipboard} from './clipboard.js'; +import {showTemporaryTooltip} from '../modules/tippy.js'; +const {i18n} = window.config; + +export function initCopyContent() { + const btn = document.getElementById('copy-content'); + if (!btn || btn.classList.contains('disabled') || btn.classList.contains('is-loading')) return; + + btn.addEventListener('click', async () => { + let content; + const link = btn.getAttribute('data-link'); + + // when data-link is present, we perform a fetch. this is either because + // the text to copy is not in the DOM or it is an image which should be + // fetched to copy in full resolution + if (link) { + btn.classList.add('is-loading'); + try { + const res = await fetch(link, {credentials: 'include', redirect: 'follow'}); + const contentType = res.headers.get('content-type'); + + if (contentType.startsWith('image/') && !contentType.startsWith('image/svg')) { + content = await res.blob(); + } else { + content = await res.text(); + } + } catch { + return showTemporaryTooltip(btn, i18n.copy_error); + } finally { + btn.classList.remove('is-loading'); + } + } else { // text, copy from DOM + const lineEls = document.querySelectorAll('.file-view .lines-code'); + content = Array.from(lineEls).map((el) => el.textContent).join(''); + } + + try { + const success = await copyToClipboard(content); + showTemporaryTooltip(btn, success ? i18n.copy_success : i18n.copy_error); + } catch { + showTemporaryTooltip(btn, i18n.copy_error); + } + }); +} diff --git a/web_src/js/features/repo-code.js b/web_src/js/features/repo-code.js index ef6b61196b73..083a17bf216f 100644 --- a/web_src/js/features/repo-code.js +++ b/web_src/js/features/repo-code.js @@ -1,10 +1,9 @@ import $ from 'jquery'; import {svg} from '../svg.js'; import {invertFileFolding} from './file-fold.js'; -import {createTippy, showTemporaryTooltip} from '../modules/tippy.js'; +import {createTippy} from '../modules/tippy.js'; import {copyToClipboard} from './clipboard.js'; -const {i18n} = window.config; export const singleAnchorRegex = /^#(L|n)([1-9][0-9]*)$/; export const rangeAnchorRegex = /^#(L[1-9][0-9]*)-(L[1-9][0-9]*)$/; @@ -114,18 +113,6 @@ function showLineButton() { }); } -function initCopyFileContent() { - // get raw text for copy content button, at the moment, only one button (and one related file content) is supported. - const copyFileContent = document.querySelector('#copy-file-content'); - if (!copyFileContent) return; - - copyFileContent.addEventListener('click', async () => { - const text = Array.from(document.querySelectorAll('.file-view .lines-code')).map((el) => el.textContent).join(''); - const success = await copyToClipboard(text); - showTemporaryTooltip(copyFileContent, success ? i18n.copy_success : i18n.copy_error); - }); -} - export function initRepoCodeView() { if ($('.code-view .lines-num').length > 0) { $(document).on('click', '.lines-num span', function (e) { @@ -205,5 +192,4 @@ export function initRepoCodeView() { if (!success) return; document.querySelector('.code-line-button')?._tippy?.hide(); }); - initCopyFileContent(); } diff --git a/web_src/js/index.js b/web_src/js/index.js index a829deaf116e..f4638a60e09a 100644 --- a/web_src/js/index.js +++ b/web_src/js/index.js @@ -89,6 +89,7 @@ import {initRepoWikiForm} from './features/repo-wiki.js'; import {initRepoCommentForm, initRepository} from './features/repo-legacy.js'; import {initFormattingReplacements} from './features/formatting.js'; import {initMcaptcha} from './features/mcaptcha.js'; +import {initCopyContent} from './features/copycontent.js'; // Run time-critical code as soon as possible. This is safe to do because this // script appears at the end of and rendered HTML is accessible at that point. @@ -136,6 +137,7 @@ $(document).ready(() => { initStopwatch(); initTableSort(); initFindFileInRepo(); + initCopyContent(); initAdminCommon(); initAdminEmails(); diff --git a/web_src/less/animations.less b/web_src/less/animations.less index 6d32625704d7..689898da2ad0 100644 --- a/web_src/less/animations.less +++ b/web_src/less/animations.less @@ -33,6 +33,12 @@ height: var(--height-loading); } +.btn-octicon.is-loading::after { + border-width: 2px; + height: 1.25rem; + width: 1.25rem; +} + code.language-math.is-loading::after { padding: 0; border-width: 2px; From 5eeabcfe61e02e4bee856be777a354a00c9ab2cc Mon Sep 17 00:00:00 2001 From: silverwind Date: Thu, 17 Nov 2022 01:27:44 +0100 Subject: [PATCH 02/18] check for blob --- web_src/js/features/clipboard.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/web_src/js/features/clipboard.js b/web_src/js/features/clipboard.js index 6bcb484b9425..746d2e5551dd 100644 --- a/web_src/js/features/clipboard.js +++ b/web_src/js/features/clipboard.js @@ -3,15 +3,15 @@ import {showTemporaryTooltip} from '../modules/tippy.js'; const {copy_success, copy_error} = window.config.i18n; export async function copyToClipboard(content) { - if (typeof content === 'string') { + if (content instanceof Blob) { // this may throw in unsupporting browsers + const item = new window.ClipboardItem({[content.type]: content}); + await navigator.clipboard.write([item]); + } else { // text try { await navigator.clipboard.writeText(content); } catch { return fallbackCopyToClipboard(content); } - } else { // blob, this may throw in unsupporting browsers - const item = new window.ClipboardItem({[content.type]: content}); - await navigator.clipboard.write([item]); } return true; } From bd438207a463977e0006d5d3f5c6964b58fcc7ad Mon Sep 17 00:00:00 2001 From: silverwind Date: Thu, 17 Nov 2022 01:42:14 +0100 Subject: [PATCH 03/18] move comment --- web_src/js/features/clipboard.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web_src/js/features/clipboard.js b/web_src/js/features/clipboard.js index 746d2e5551dd..344f727a17e9 100644 --- a/web_src/js/features/clipboard.js +++ b/web_src/js/features/clipboard.js @@ -3,8 +3,8 @@ import {showTemporaryTooltip} from '../modules/tippy.js'; const {copy_success, copy_error} = window.config.i18n; export async function copyToClipboard(content) { - if (content instanceof Blob) { // this may throw in unsupporting browsers - const item = new window.ClipboardItem({[content.type]: content}); + if (content instanceof Blob) { + const item = new window.ClipboardItem({[content.type]: content}); // may throw await navigator.clipboard.write([item]); } else { // text try { From a6bf4ff864ba2ac561fa006b7849279e790eb66b Mon Sep 17 00:00:00 2001 From: silverwind Date: Thu, 17 Nov 2022 02:03:56 +0100 Subject: [PATCH 04/18] Update templates/repo/view_file.tmpl --- templates/repo/view_file.tmpl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/repo/view_file.tmpl b/templates/repo/view_file.tmpl index e346f1b1de45..fc8519b4fecd 100644 --- a/templates/repo/view_file.tmpl +++ b/templates/repo/view_file.tmpl @@ -38,7 +38,7 @@ {{end}} {{svg "octicon-download"}} - {{svg "octicon-copy" 14}} + {{svg "octicon-copy" 14}} {{if .Repository.CanEnableEditor}} {{if .CanEditFile}} {{svg "octicon-pencil"}} From a16485bad308cc5d41f00d4201118cce2af774f0 Mon Sep 17 00:00:00 2001 From: silverwind Date: Thu, 17 Nov 2022 02:06:17 +0100 Subject: [PATCH 05/18] fix is-loading check --- web_src/js/features/copycontent.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/web_src/js/features/copycontent.js b/web_src/js/features/copycontent.js index 4d59922eca0d..bf31880c8889 100644 --- a/web_src/js/features/copycontent.js +++ b/web_src/js/features/copycontent.js @@ -4,9 +4,10 @@ const {i18n} = window.config; export function initCopyContent() { const btn = document.getElementById('copy-content'); - if (!btn || btn.classList.contains('disabled') || btn.classList.contains('is-loading')) return; + if (!btn || btn.classList.contains('disabled')) return; btn.addEventListener('click', async () => { + if (btn.classList.contains('is-loading')) return; let content; const link = btn.getAttribute('data-link'); From 6ff315e60b518ab281717b349b953cfebc257b19 Mon Sep 17 00:00:00 2001 From: silverwind Date: Thu, 17 Nov 2022 02:14:15 +0100 Subject: [PATCH 06/18] Update web_src/js/features/copycontent.js --- web_src/js/features/copycontent.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web_src/js/features/copycontent.js b/web_src/js/features/copycontent.js index bf31880c8889..eeb1b7ef1052 100644 --- a/web_src/js/features/copycontent.js +++ b/web_src/js/features/copycontent.js @@ -30,7 +30,7 @@ export function initCopyContent() { } finally { btn.classList.remove('is-loading'); } - } else { // text, copy from DOM + } else { // text, read from DOM const lineEls = document.querySelectorAll('.file-view .lines-code'); content = Array.from(lineEls).map((el) => el.textContent).join(''); } From 76992e990a75ce09c2600e59fbc59d13c0d8e50f Mon Sep 17 00:00:00 2001 From: silverwind Date: Thu, 17 Nov 2022 12:27:55 +0100 Subject: [PATCH 07/18] Update indirect eslint dependencies to allow ClipboardItem global Ref: https://github.com/sindresorhus/globals/pull/194 --- package-lock.json | 117 ++++++++++++++++--------------- web_src/js/features/clipboard.js | 2 +- 2 files changed, 61 insertions(+), 58 deletions(-) diff --git a/package-lock.json b/package-lock.json index d46d3a15cae5..23448e403056 100644 --- a/package-lock.json +++ b/package-lock.json @@ -384,14 +384,14 @@ "dev": true }, "node_modules/@humanwhocodes/config-array": { - "version": "0.11.6", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.6.tgz", - "integrity": "sha512-jJr+hPTJYKyDILJfhNSHsjiwXYf26Flsz8DvNndOsHs5pwSnpGUEy8yzF0JYhCEvTDdV2vuOK5tt8BVhwO5/hg==", + "version": "0.11.7", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.7.tgz", + "integrity": "sha512-kBbPWzN8oVMLb0hOUYXhmxggL/1cJE6ydvjDIGi9EnAGUyA7cLVKQg+d/Dsm+KZwx2czGHrCmMVLiyg8s5JPKw==", "dev": true, "dependencies": { "@humanwhocodes/object-schema": "^1.2.1", "debug": "^4.1.1", - "minimatch": "^3.0.4" + "minimatch": "^3.0.5" }, "engines": { "node": ">=10.10.0" @@ -1872,15 +1872,15 @@ } }, "node_modules/array-includes": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.5.tgz", - "integrity": "sha512-iSDYZMMyTPkiFasVqfuAQnWAYcvO/SeBSCGKePoEthjp4LEMTe4uLc7b025o4jAZpHhihh8xPo99TNWUWWkGDQ==", + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.6.tgz", + "integrity": "sha512-sgTbLvL6cNnw24FnbaDyjmvddQ2ML8arZsgaJhoABMoplz/4QRhtrYS+alr1BUM1Bwp6dhx8vVCBSLG+StwOFw==", "dev": true, "dependencies": { "call-bind": "^1.0.2", "define-properties": "^1.1.4", - "es-abstract": "^1.19.5", - "get-intrinsic": "^1.1.1", + "es-abstract": "^1.20.4", + "get-intrinsic": "^1.1.3", "is-string": "^1.0.7" }, "engines": { @@ -1900,14 +1900,14 @@ } }, "node_modules/array.prototype.flat": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.0.tgz", - "integrity": "sha512-12IUEkHsAhA4DY5s0FPgNXIdc8VRSqD9Zp78a5au9abH/SOBrsp082JOWFNTjkMozh8mqcdiKuaLGhPeYztxSw==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.1.tgz", + "integrity": "sha512-roTU0KWIOmJ4DRLmwKd19Otg0/mT3qPNt0Qb3GWW8iObuZXxrjB/pzn0R3hqpRSWg4HCwqx+0vwOnWnvlOyeIA==", "dev": true, "dependencies": { "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.19.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4", "es-shim-unscopables": "^1.0.0" }, "engines": { @@ -2240,10 +2240,13 @@ } }, "node_modules/ci-info": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.5.0.tgz", - "integrity": "sha512-yH4RezKOGlOhxkmhbeNuC4eYZKAUsEaGtBuBzDDP1eFUKiccDWzBABxBfOx31IDwDIXMTxWuwAxUGModvkbuVw==", - "dev": true + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.6.1.tgz", + "integrity": "sha512-up5ggbaDqOqJ4UqLKZ2naVkyqSJQgJi5lwD6b6mM748ysrghDBX0bx/qJTUHzw7zu6Mq4gycviSF5hJnwceD8w==", + "dev": true, + "engines": { + "node": ">=8" + } }, "node_modules/citeproc": { "version": "2.4.62", @@ -4585,9 +4588,9 @@ "dev": true }, "node_modules/espree": { - "version": "9.4.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.4.0.tgz", - "integrity": "sha512-DQmnRpLj7f6TgN/NYb0MTzJXL+vJF9h3pHy4JhCIs3zwcgez8xmGg3sXHcEO97BrmO2OSvCwMdfdlyl+E9KjOw==", + "version": "9.4.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.4.1.tgz", + "integrity": "sha512-XwctdmTO6SIvCzd9810yyNzIrOrqNYV9Koizx4C/mRhf9uq0o4yHoCEU/670pOxOL/MSraektvSAji79kX90Vg==", "dev": true, "dependencies": { "acorn": "^8.8.0", @@ -5098,9 +5101,9 @@ } }, "node_modules/globals": { - "version": "13.17.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.17.0.tgz", - "integrity": "sha512-1C+6nQRb1GwGMKm2dH/E7enFAMxGTmGI7/dEdhy/DNelv85w9B72t3uc5frtMNXIbzrarJJ/lTCjcaZwbLJmyw==", + "version": "13.18.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.18.0.tgz", + "integrity": "sha512-/mR4KI8Ps2spmoc0Ulu9L7agOF0du1CZNQ3dke8yItYlyKNmGrkONemBbd6V8UTc1Wgcqn21t3WYB7dbRmh6/A==", "dev": true, "dependencies": { "type-fest": "^0.20.2" @@ -6958,14 +6961,14 @@ } }, "node_modules/object.values": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.5.tgz", - "integrity": "sha512-QUZRW0ilQ3PnPpbNtgdNV1PDbEqLIiSFB3l+EnGtBQ/8SUTLj1PZwtQHABZtLgwpJZTSZhuGLOGk57Drx2IvYg==", + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.6.tgz", + "integrity": "sha512-FVVTkD1vENCsAcwNs9k6jea2uHC/X0+JcjG8YA60FN5CMaJmG95wT9jek/xX9nornqGRrBkKtzuAu2wuHpKqvw==", "dev": true, "dependencies": { "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.19.1" + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4" }, "engines": { "node": ">= 0.4" @@ -10126,14 +10129,14 @@ } }, "@humanwhocodes/config-array": { - "version": "0.11.6", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.6.tgz", - "integrity": "sha512-jJr+hPTJYKyDILJfhNSHsjiwXYf26Flsz8DvNndOsHs5pwSnpGUEy8yzF0JYhCEvTDdV2vuOK5tt8BVhwO5/hg==", + "version": "0.11.7", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.7.tgz", + "integrity": "sha512-kBbPWzN8oVMLb0hOUYXhmxggL/1cJE6ydvjDIGi9EnAGUyA7cLVKQg+d/Dsm+KZwx2czGHrCmMVLiyg8s5JPKw==", "dev": true, "requires": { "@humanwhocodes/object-schema": "^1.2.1", "debug": "^4.1.1", - "minimatch": "^3.0.4" + "minimatch": "^3.0.5" } }, "@humanwhocodes/module-importer": { @@ -11335,15 +11338,15 @@ "integrity": "sha512-M1HQyIXcBGtVywBt8WVdim+lrNaK7VHp99Qt5pSNziXznKHViIBbXWtfRTpEFpF/c4FdfxNAsCCwPp5phBYJtw==" }, "array-includes": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.5.tgz", - "integrity": "sha512-iSDYZMMyTPkiFasVqfuAQnWAYcvO/SeBSCGKePoEthjp4LEMTe4uLc7b025o4jAZpHhihh8xPo99TNWUWWkGDQ==", + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.6.tgz", + "integrity": "sha512-sgTbLvL6cNnw24FnbaDyjmvddQ2ML8arZsgaJhoABMoplz/4QRhtrYS+alr1BUM1Bwp6dhx8vVCBSLG+StwOFw==", "dev": true, "requires": { "call-bind": "^1.0.2", "define-properties": "^1.1.4", - "es-abstract": "^1.19.5", - "get-intrinsic": "^1.1.1", + "es-abstract": "^1.20.4", + "get-intrinsic": "^1.1.3", "is-string": "^1.0.7" } }, @@ -11354,14 +11357,14 @@ "dev": true }, "array.prototype.flat": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.0.tgz", - "integrity": "sha512-12IUEkHsAhA4DY5s0FPgNXIdc8VRSqD9Zp78a5au9abH/SOBrsp082JOWFNTjkMozh8mqcdiKuaLGhPeYztxSw==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.1.tgz", + "integrity": "sha512-roTU0KWIOmJ4DRLmwKd19Otg0/mT3qPNt0Qb3GWW8iObuZXxrjB/pzn0R3hqpRSWg4HCwqx+0vwOnWnvlOyeIA==", "dev": true, "requires": { "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.19.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4", "es-shim-unscopables": "^1.0.0" } }, @@ -11574,9 +11577,9 @@ "integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==" }, "ci-info": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.5.0.tgz", - "integrity": "sha512-yH4RezKOGlOhxkmhbeNuC4eYZKAUsEaGtBuBzDDP1eFUKiccDWzBABxBfOx31IDwDIXMTxWuwAxUGModvkbuVw==", + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.6.1.tgz", + "integrity": "sha512-up5ggbaDqOqJ4UqLKZ2naVkyqSJQgJi5lwD6b6mM748ysrghDBX0bx/qJTUHzw7zu6Mq4gycviSF5hJnwceD8w==", "dev": true }, "citeproc": { @@ -13305,9 +13308,9 @@ "dev": true }, "espree": { - "version": "9.4.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.4.0.tgz", - "integrity": "sha512-DQmnRpLj7f6TgN/NYb0MTzJXL+vJF9h3pHy4JhCIs3zwcgez8xmGg3sXHcEO97BrmO2OSvCwMdfdlyl+E9KjOw==", + "version": "9.4.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.4.1.tgz", + "integrity": "sha512-XwctdmTO6SIvCzd9810yyNzIrOrqNYV9Koizx4C/mRhf9uq0o4yHoCEU/670pOxOL/MSraektvSAji79kX90Vg==", "dev": true, "requires": { "acorn": "^8.8.0", @@ -13695,9 +13698,9 @@ } }, "globals": { - "version": "13.17.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.17.0.tgz", - "integrity": "sha512-1C+6nQRb1GwGMKm2dH/E7enFAMxGTmGI7/dEdhy/DNelv85w9B72t3uc5frtMNXIbzrarJJ/lTCjcaZwbLJmyw==", + "version": "13.18.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.18.0.tgz", + "integrity": "sha512-/mR4KI8Ps2spmoc0Ulu9L7agOF0du1CZNQ3dke8yItYlyKNmGrkONemBbd6V8UTc1Wgcqn21t3WYB7dbRmh6/A==", "dev": true, "requires": { "type-fest": "^0.20.2" @@ -15068,14 +15071,14 @@ } }, "object.values": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.5.tgz", - "integrity": "sha512-QUZRW0ilQ3PnPpbNtgdNV1PDbEqLIiSFB3l+EnGtBQ/8SUTLj1PZwtQHABZtLgwpJZTSZhuGLOGk57Drx2IvYg==", + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.6.tgz", + "integrity": "sha512-FVVTkD1vENCsAcwNs9k6jea2uHC/X0+JcjG8YA60FN5CMaJmG95wT9jek/xX9nornqGRrBkKtzuAu2wuHpKqvw==", "dev": true, "requires": { "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.19.1" + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4" } }, "once": { diff --git a/web_src/js/features/clipboard.js b/web_src/js/features/clipboard.js index 344f727a17e9..3fc3f997313a 100644 --- a/web_src/js/features/clipboard.js +++ b/web_src/js/features/clipboard.js @@ -4,7 +4,7 @@ const {copy_success, copy_error} = window.config.i18n; export async function copyToClipboard(content) { if (content instanceof Blob) { - const item = new window.ClipboardItem({[content.type]: content}); // may throw + const item = new ClipboardItem({[content.type]: content}); // may throw on unsupporting browsers await navigator.clipboard.write([item]); } else { // text try { From 307c9555930eb38f3e0a87c6ac138b34b722f6d2 Mon Sep 17 00:00:00 2001 From: silverwind Date: Thu, 17 Nov 2022 12:37:09 +0100 Subject: [PATCH 08/18] Revert "Update indirect eslint dependencies to allow ClipboardItem global" This reverts commit 476519e119c8a575e9325632f64aec1ac9cc8f17. --- package-lock.json | 117 +++++++++++++++---------------- web_src/js/features/clipboard.js | 2 +- 2 files changed, 58 insertions(+), 61 deletions(-) diff --git a/package-lock.json b/package-lock.json index 23448e403056..d46d3a15cae5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -384,14 +384,14 @@ "dev": true }, "node_modules/@humanwhocodes/config-array": { - "version": "0.11.7", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.7.tgz", - "integrity": "sha512-kBbPWzN8oVMLb0hOUYXhmxggL/1cJE6ydvjDIGi9EnAGUyA7cLVKQg+d/Dsm+KZwx2czGHrCmMVLiyg8s5JPKw==", + "version": "0.11.6", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.6.tgz", + "integrity": "sha512-jJr+hPTJYKyDILJfhNSHsjiwXYf26Flsz8DvNndOsHs5pwSnpGUEy8yzF0JYhCEvTDdV2vuOK5tt8BVhwO5/hg==", "dev": true, "dependencies": { "@humanwhocodes/object-schema": "^1.2.1", "debug": "^4.1.1", - "minimatch": "^3.0.5" + "minimatch": "^3.0.4" }, "engines": { "node": ">=10.10.0" @@ -1872,15 +1872,15 @@ } }, "node_modules/array-includes": { - "version": "3.1.6", - "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.6.tgz", - "integrity": "sha512-sgTbLvL6cNnw24FnbaDyjmvddQ2ML8arZsgaJhoABMoplz/4QRhtrYS+alr1BUM1Bwp6dhx8vVCBSLG+StwOFw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.5.tgz", + "integrity": "sha512-iSDYZMMyTPkiFasVqfuAQnWAYcvO/SeBSCGKePoEthjp4LEMTe4uLc7b025o4jAZpHhihh8xPo99TNWUWWkGDQ==", "dev": true, "dependencies": { "call-bind": "^1.0.2", "define-properties": "^1.1.4", - "es-abstract": "^1.20.4", - "get-intrinsic": "^1.1.3", + "es-abstract": "^1.19.5", + "get-intrinsic": "^1.1.1", "is-string": "^1.0.7" }, "engines": { @@ -1900,14 +1900,14 @@ } }, "node_modules/array.prototype.flat": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.1.tgz", - "integrity": "sha512-roTU0KWIOmJ4DRLmwKd19Otg0/mT3qPNt0Qb3GWW8iObuZXxrjB/pzn0R3hqpRSWg4HCwqx+0vwOnWnvlOyeIA==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.0.tgz", + "integrity": "sha512-12IUEkHsAhA4DY5s0FPgNXIdc8VRSqD9Zp78a5au9abH/SOBrsp082JOWFNTjkMozh8mqcdiKuaLGhPeYztxSw==", "dev": true, "dependencies": { "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.2", "es-shim-unscopables": "^1.0.0" }, "engines": { @@ -2240,13 +2240,10 @@ } }, "node_modules/ci-info": { - "version": "3.6.1", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.6.1.tgz", - "integrity": "sha512-up5ggbaDqOqJ4UqLKZ2naVkyqSJQgJi5lwD6b6mM748ysrghDBX0bx/qJTUHzw7zu6Mq4gycviSF5hJnwceD8w==", - "dev": true, - "engines": { - "node": ">=8" - } + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.5.0.tgz", + "integrity": "sha512-yH4RezKOGlOhxkmhbeNuC4eYZKAUsEaGtBuBzDDP1eFUKiccDWzBABxBfOx31IDwDIXMTxWuwAxUGModvkbuVw==", + "dev": true }, "node_modules/citeproc": { "version": "2.4.62", @@ -4588,9 +4585,9 @@ "dev": true }, "node_modules/espree": { - "version": "9.4.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.4.1.tgz", - "integrity": "sha512-XwctdmTO6SIvCzd9810yyNzIrOrqNYV9Koizx4C/mRhf9uq0o4yHoCEU/670pOxOL/MSraektvSAji79kX90Vg==", + "version": "9.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.4.0.tgz", + "integrity": "sha512-DQmnRpLj7f6TgN/NYb0MTzJXL+vJF9h3pHy4JhCIs3zwcgez8xmGg3sXHcEO97BrmO2OSvCwMdfdlyl+E9KjOw==", "dev": true, "dependencies": { "acorn": "^8.8.0", @@ -5101,9 +5098,9 @@ } }, "node_modules/globals": { - "version": "13.18.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.18.0.tgz", - "integrity": "sha512-/mR4KI8Ps2spmoc0Ulu9L7agOF0du1CZNQ3dke8yItYlyKNmGrkONemBbd6V8UTc1Wgcqn21t3WYB7dbRmh6/A==", + "version": "13.17.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.17.0.tgz", + "integrity": "sha512-1C+6nQRb1GwGMKm2dH/E7enFAMxGTmGI7/dEdhy/DNelv85w9B72t3uc5frtMNXIbzrarJJ/lTCjcaZwbLJmyw==", "dev": true, "dependencies": { "type-fest": "^0.20.2" @@ -6961,14 +6958,14 @@ } }, "node_modules/object.values": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.6.tgz", - "integrity": "sha512-FVVTkD1vENCsAcwNs9k6jea2uHC/X0+JcjG8YA60FN5CMaJmG95wT9jek/xX9nornqGRrBkKtzuAu2wuHpKqvw==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.5.tgz", + "integrity": "sha512-QUZRW0ilQ3PnPpbNtgdNV1PDbEqLIiSFB3l+EnGtBQ/8SUTLj1PZwtQHABZtLgwpJZTSZhuGLOGk57Drx2IvYg==", "dev": true, "dependencies": { "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" + "define-properties": "^1.1.3", + "es-abstract": "^1.19.1" }, "engines": { "node": ">= 0.4" @@ -10129,14 +10126,14 @@ } }, "@humanwhocodes/config-array": { - "version": "0.11.7", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.7.tgz", - "integrity": "sha512-kBbPWzN8oVMLb0hOUYXhmxggL/1cJE6ydvjDIGi9EnAGUyA7cLVKQg+d/Dsm+KZwx2czGHrCmMVLiyg8s5JPKw==", + "version": "0.11.6", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.6.tgz", + "integrity": "sha512-jJr+hPTJYKyDILJfhNSHsjiwXYf26Flsz8DvNndOsHs5pwSnpGUEy8yzF0JYhCEvTDdV2vuOK5tt8BVhwO5/hg==", "dev": true, "requires": { "@humanwhocodes/object-schema": "^1.2.1", "debug": "^4.1.1", - "minimatch": "^3.0.5" + "minimatch": "^3.0.4" } }, "@humanwhocodes/module-importer": { @@ -11338,15 +11335,15 @@ "integrity": "sha512-M1HQyIXcBGtVywBt8WVdim+lrNaK7VHp99Qt5pSNziXznKHViIBbXWtfRTpEFpF/c4FdfxNAsCCwPp5phBYJtw==" }, "array-includes": { - "version": "3.1.6", - "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.6.tgz", - "integrity": "sha512-sgTbLvL6cNnw24FnbaDyjmvddQ2ML8arZsgaJhoABMoplz/4QRhtrYS+alr1BUM1Bwp6dhx8vVCBSLG+StwOFw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.5.tgz", + "integrity": "sha512-iSDYZMMyTPkiFasVqfuAQnWAYcvO/SeBSCGKePoEthjp4LEMTe4uLc7b025o4jAZpHhihh8xPo99TNWUWWkGDQ==", "dev": true, "requires": { "call-bind": "^1.0.2", "define-properties": "^1.1.4", - "es-abstract": "^1.20.4", - "get-intrinsic": "^1.1.3", + "es-abstract": "^1.19.5", + "get-intrinsic": "^1.1.1", "is-string": "^1.0.7" } }, @@ -11357,14 +11354,14 @@ "dev": true }, "array.prototype.flat": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.1.tgz", - "integrity": "sha512-roTU0KWIOmJ4DRLmwKd19Otg0/mT3qPNt0Qb3GWW8iObuZXxrjB/pzn0R3hqpRSWg4HCwqx+0vwOnWnvlOyeIA==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.0.tgz", + "integrity": "sha512-12IUEkHsAhA4DY5s0FPgNXIdc8VRSqD9Zp78a5au9abH/SOBrsp082JOWFNTjkMozh8mqcdiKuaLGhPeYztxSw==", "dev": true, "requires": { "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.2", "es-shim-unscopables": "^1.0.0" } }, @@ -11577,9 +11574,9 @@ "integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==" }, "ci-info": { - "version": "3.6.1", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.6.1.tgz", - "integrity": "sha512-up5ggbaDqOqJ4UqLKZ2naVkyqSJQgJi5lwD6b6mM748ysrghDBX0bx/qJTUHzw7zu6Mq4gycviSF5hJnwceD8w==", + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.5.0.tgz", + "integrity": "sha512-yH4RezKOGlOhxkmhbeNuC4eYZKAUsEaGtBuBzDDP1eFUKiccDWzBABxBfOx31IDwDIXMTxWuwAxUGModvkbuVw==", "dev": true }, "citeproc": { @@ -13308,9 +13305,9 @@ "dev": true }, "espree": { - "version": "9.4.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.4.1.tgz", - "integrity": "sha512-XwctdmTO6SIvCzd9810yyNzIrOrqNYV9Koizx4C/mRhf9uq0o4yHoCEU/670pOxOL/MSraektvSAji79kX90Vg==", + "version": "9.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.4.0.tgz", + "integrity": "sha512-DQmnRpLj7f6TgN/NYb0MTzJXL+vJF9h3pHy4JhCIs3zwcgez8xmGg3sXHcEO97BrmO2OSvCwMdfdlyl+E9KjOw==", "dev": true, "requires": { "acorn": "^8.8.0", @@ -13698,9 +13695,9 @@ } }, "globals": { - "version": "13.18.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.18.0.tgz", - "integrity": "sha512-/mR4KI8Ps2spmoc0Ulu9L7agOF0du1CZNQ3dke8yItYlyKNmGrkONemBbd6V8UTc1Wgcqn21t3WYB7dbRmh6/A==", + "version": "13.17.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.17.0.tgz", + "integrity": "sha512-1C+6nQRb1GwGMKm2dH/E7enFAMxGTmGI7/dEdhy/DNelv85w9B72t3uc5frtMNXIbzrarJJ/lTCjcaZwbLJmyw==", "dev": true, "requires": { "type-fest": "^0.20.2" @@ -15071,14 +15068,14 @@ } }, "object.values": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.6.tgz", - "integrity": "sha512-FVVTkD1vENCsAcwNs9k6jea2uHC/X0+JcjG8YA60FN5CMaJmG95wT9jek/xX9nornqGRrBkKtzuAu2wuHpKqvw==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.5.tgz", + "integrity": "sha512-QUZRW0ilQ3PnPpbNtgdNV1PDbEqLIiSFB3l+EnGtBQ/8SUTLj1PZwtQHABZtLgwpJZTSZhuGLOGk57Drx2IvYg==", "dev": true, "requires": { "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" + "define-properties": "^1.1.3", + "es-abstract": "^1.19.1" } }, "once": { diff --git a/web_src/js/features/clipboard.js b/web_src/js/features/clipboard.js index 3fc3f997313a..e70c81c6cb4e 100644 --- a/web_src/js/features/clipboard.js +++ b/web_src/js/features/clipboard.js @@ -4,7 +4,7 @@ const {copy_success, copy_error} = window.config.i18n; export async function copyToClipboard(content) { if (content instanceof Blob) { - const item = new ClipboardItem({[content.type]: content}); // may throw on unsupporting browsers + const item = new window.ClipboardItem({[content.type]: content}); // may throw on unsupporting browsers await navigator.clipboard.write([item]); } else { // text try { From 348cbb55f42ddb270897f7d87e7312a7f9cbe039 Mon Sep 17 00:00:00 2001 From: silverwind Date: Fri, 18 Nov 2022 19:53:40 +0100 Subject: [PATCH 09/18] add disabled tooltip and fallback image convertion to png --- .eslintrc.yaml | 2 +- options/locale/locale_en-US.ini | 1 + templates/repo/view_file.tmpl | 2 +- web_src/js/features/copycontent.js | 23 ++++++++++++++--- web_src/js/modules/tippy.js | 1 + web_src/js/utils.js | 41 ++++++++++++++++++++++++++++++ 6 files changed, 64 insertions(+), 6 deletions(-) diff --git a/.eslintrc.yaml b/.eslintrc.yaml index cd86b680ee41..2f213db37d58 100644 --- a/.eslintrc.yaml +++ b/.eslintrc.yaml @@ -199,7 +199,7 @@ rules: newline-per-chained-call: [0] no-alert: [0] no-array-constructor: [2] - no-async-promise-executor: [2] + no-async-promise-executor: [0] no-await-in-loop: [0] no-bitwise: [0] no-buffer-constructor: [0] diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 2d6f174e223e..b4934afedcde 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -95,6 +95,7 @@ copy_content = Copy content copy_branch = Copy branch name copy_success = Copied! copy_error = Copy failed +copy_unsupported = This file type can not be copied write = Write preview = Preview diff --git a/templates/repo/view_file.tmpl b/templates/repo/view_file.tmpl index fc8519b4fecd..aa712bcc3b67 100644 --- a/templates/repo/view_file.tmpl +++ b/templates/repo/view_file.tmpl @@ -38,7 +38,7 @@ {{end}} {{svg "octicon-download"}} - {{svg "octicon-copy" 14}} + {{svg "octicon-copy" 14}} {{if .Repository.CanEnableEditor}} {{if .CanEditFile}} {{svg "octicon-pencil"}} diff --git a/web_src/js/features/copycontent.js b/web_src/js/features/copycontent.js index eeb1b7ef1052..88ab832ab8f6 100644 --- a/web_src/js/features/copycontent.js +++ b/web_src/js/features/copycontent.js @@ -1,14 +1,20 @@ import {copyToClipboard} from './clipboard.js'; import {showTemporaryTooltip} from '../modules/tippy.js'; +import {imageBlobToPng} from '../utils.js'; const {i18n} = window.config; +async function doCopy(content, btn) { + const success = await copyToClipboard(content); + showTemporaryTooltip(btn, success ? i18n.copy_success : i18n.copy_error); +} + export function initCopyContent() { const btn = document.getElementById('copy-content'); if (!btn || btn.classList.contains('disabled')) return; btn.addEventListener('click', async () => { if (btn.classList.contains('is-loading')) return; - let content; + let content, isImage; const link = btn.getAttribute('data-link'); // when data-link is present, we perform a fetch. this is either because @@ -21,6 +27,7 @@ export function initCopyContent() { const contentType = res.headers.get('content-type'); if (contentType.startsWith('image/') && !contentType.startsWith('image/svg')) { + isImage = true; content = await res.blob(); } else { content = await res.text(); @@ -36,10 +43,18 @@ export function initCopyContent() { } try { - const success = await copyToClipboard(content); - showTemporaryTooltip(btn, success ? i18n.copy_success : i18n.copy_error); + await doCopy(content, btn); } catch { - showTemporaryTooltip(btn, i18n.copy_error); + if (isImage) { + // convert image to png as last-resort as some browser only support png copy + try { + await doCopy(await imageBlobToPng(content), btn); + } catch { + showTemporaryTooltip(btn, i18n.copy_error); + } + } else { + showTemporaryTooltip(btn, i18n.copy_error); + } } }); } diff --git a/web_src/js/modules/tippy.js b/web_src/js/modules/tippy.js index 045df6f0a023..6a89151691a9 100644 --- a/web_src/js/modules/tippy.js +++ b/web_src/js/modules/tippy.js @@ -27,6 +27,7 @@ export function createTippy(target, opts = {}) { export function initTooltip(el, props = {}) { const content = el.getAttribute('data-content') || props.content; if (!content) return null; + if (!el.hasAttribute('aria-label')) el.setAttribute('aria-label', content); return createTippy(el, { content, delay: 100, diff --git a/web_src/js/utils.js b/web_src/js/utils.js index 9b8bf925a9ee..411156828363 100644 --- a/web_src/js/utils.js +++ b/web_src/js/utils.js @@ -85,3 +85,44 @@ export function translateMonth(month) { export function translateDay(day) { return new Date(Date.UTC(2022, 7, day)).toLocaleString(getCurrentLocale(), {weekday: 'short'}); } + +// convert a Blob to a DataURI +function blobToDataURI(blob) { + return new Promise((resolve, reject) => { + try { + const reader = new FileReader(); + reader.addEventListener('load', (e) => { + resolve(e.target.result); + }); + reader.addEventListener('error', () => { + reject(new Error('FileReader failed')); + }); + reader.readAsDataURL(blob); + } catch (err) { + reject(err); + } + }); +} + +// convert a jpg (and possibly other formats) blob to a png blob +export function imageBlobToPng(blob) { + return new Promise(async (resolve, reject) => { + try { + const img = new Image(); + const canvas = document.createElement('canvas'); + img.addEventListener('load', () => { + canvas.width = img.naturalWidth; + canvas.height = img.naturalHeight; + const context = canvas.getContext('2d'); + context.drawImage(img, 0, 0); + canvas.toBlob(resolve, 'image/png'); + }); + img.addEventListener('error', () => { + reject(new Error('Image convertion failed')); + }); + img.src = await blobToDataURI(blob); + } catch (err) { + reject(err); + } + }); +} From df1d1500101fd8dad0d6b26eda4aafca1f7f3b7d Mon Sep 17 00:00:00 2001 From: silverwind Date: Fri, 18 Nov 2022 19:57:49 +0100 Subject: [PATCH 10/18] Fix typo --- web_src/js/utils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web_src/js/utils.js b/web_src/js/utils.js index 411156828363..c60e4691c2f3 100644 --- a/web_src/js/utils.js +++ b/web_src/js/utils.js @@ -118,7 +118,7 @@ export function imageBlobToPng(blob) { canvas.toBlob(resolve, 'image/png'); }); img.addEventListener('error', () => { - reject(new Error('Image convertion failed')); + reject(new Error('Image conversion failed')); }); img.src = await blobToDataURI(blob); } catch (err) { From babd642e2fe57d6060a9ca18b84a1fbba1c53f15 Mon Sep 17 00:00:00 2001 From: silverwind Date: Fri, 18 Nov 2022 19:59:53 +0100 Subject: [PATCH 11/18] tweak comment --- web_src/js/utils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web_src/js/utils.js b/web_src/js/utils.js index c60e4691c2f3..e97bac1d4f05 100644 --- a/web_src/js/utils.js +++ b/web_src/js/utils.js @@ -104,7 +104,7 @@ function blobToDataURI(blob) { }); } -// convert a jpg (and possibly other formats) blob to a png blob +// convert any image Blob to a png Blob export function imageBlobToPng(blob) { return new Promise(async (resolve, reject) => { try { From 617988c49cb5297dd2be28c2faf43d5bedc43864 Mon Sep 17 00:00:00 2001 From: silverwind Date: Fri, 18 Nov 2022 20:03:56 +0100 Subject: [PATCH 12/18] add test --- web_src/js/utils.js | 2 +- web_src/js/utils.test.js | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/web_src/js/utils.js b/web_src/js/utils.js index e97bac1d4f05..73e0a7226e67 100644 --- a/web_src/js/utils.js +++ b/web_src/js/utils.js @@ -87,7 +87,7 @@ export function translateDay(day) { } // convert a Blob to a DataURI -function blobToDataURI(blob) { +export function blobToDataURI(blob) { return new Promise((resolve, reject) => { try { const reader = new FileReader(); diff --git a/web_src/js/utils.test.js b/web_src/js/utils.test.js index 0567a5c64af9..1df0caa21103 100644 --- a/web_src/js/utils.test.js +++ b/web_src/js/utils.test.js @@ -1,7 +1,7 @@ import {expect, test} from 'vitest'; import { basename, extname, isObject, uniq, stripTags, joinPaths, parseIssueHref, - prettyNumber, parseUrl, translateMonth, translateDay + prettyNumber, parseUrl, translateMonth, translateDay, blobToDataURI, } from './utils.js'; test('basename', () => { @@ -131,3 +131,8 @@ test('translateDay', () => { expect(translateDay(5)).toEqual('pt.'); document.documentElement.lang = originalLang; }); + +test('blobToDataURI', async () => { + const blob = new Blob([JSON.stringify({test: true})], {type: 'application/json'}); + expect(await blobToDataURI(blob)).toEqual('data:application/json;base64,eyJ0ZXN0Ijp0cnVlfQ=='); +}); From a4c2ab8e3dcceef90a3f6261bcfd6c5912f1d50f Mon Sep 17 00:00:00 2001 From: silverwind Date: Fri, 18 Nov 2022 20:07:00 +0100 Subject: [PATCH 13/18] Update web_src/js/features/clipboard.js --- web_src/js/features/clipboard.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web_src/js/features/clipboard.js b/web_src/js/features/clipboard.js index e70c81c6cb4e..f266d4f64d11 100644 --- a/web_src/js/features/clipboard.js +++ b/web_src/js/features/clipboard.js @@ -4,7 +4,7 @@ const {copy_success, copy_error} = window.config.i18n; export async function copyToClipboard(content) { if (content instanceof Blob) { - const item = new window.ClipboardItem({[content.type]: content}); // may throw on unsupporting browsers + const item = new window.ClipboardItem({[content.type]: content}); await navigator.clipboard.write([item]); } else { // text try { From cbc80cb88531a0129bb571bd1d3e2ec0359965e6 Mon Sep 17 00:00:00 2001 From: silverwind Date: Fri, 18 Nov 2022 20:08:24 +0100 Subject: [PATCH 14/18] Update web_src/js/features/copycontent.js --- web_src/js/features/copycontent.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/web_src/js/features/copycontent.js b/web_src/js/features/copycontent.js index 88ab832ab8f6..874ad727612e 100644 --- a/web_src/js/features/copycontent.js +++ b/web_src/js/features/copycontent.js @@ -45,8 +45,7 @@ export function initCopyContent() { try { await doCopy(content, btn); } catch { - if (isImage) { - // convert image to png as last-resort as some browser only support png copy + if (isImage) { // convert image to png as last-resort as some browser only support png copy try { await doCopy(await imageBlobToPng(content), btn); } catch { From e43a837c0d2b99c0d8f2bfdea7f1df12ed2fd204 Mon Sep 17 00:00:00 2001 From: silverwind Date: Fri, 18 Nov 2022 20:13:15 +0100 Subject: [PATCH 15/18] handle canvas.toBlob failure --- web_src/js/utils.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/web_src/js/utils.js b/web_src/js/utils.js index 73e0a7226e67..42b89d38f2b3 100644 --- a/web_src/js/utils.js +++ b/web_src/js/utils.js @@ -115,10 +115,13 @@ export function imageBlobToPng(blob) { canvas.height = img.naturalHeight; const context = canvas.getContext('2d'); context.drawImage(img, 0, 0); - canvas.toBlob(resolve, 'image/png'); + canvas.toBlob((blob) => { + if (!(blob instanceof Blob)) return reject(new Error('imageBlobToPng failed')); + resolve(blob); + }, 'image/png'); }); img.addEventListener('error', () => { - reject(new Error('Image conversion failed')); + reject(new Error('imageBlobToPng failed')); }); img.src = await blobToDataURI(blob); } catch (err) { From ccb0820bcfeb98fe5f1a4ecb71b401b5ea565df2 Mon Sep 17 00:00:00 2001 From: silverwind Date: Fri, 18 Nov 2022 23:52:06 +0100 Subject: [PATCH 16/18] rename translation key --- options/locale/locale_en-US.ini | 2 +- templates/repo/view_file.tmpl | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index b4934afedcde..02598dc3dc4d 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -95,7 +95,7 @@ copy_content = Copy content copy_branch = Copy branch name copy_success = Copied! copy_error = Copy failed -copy_unsupported = This file type can not be copied +copy_type_unsupported = This file type can not be copied write = Write preview = Preview diff --git a/templates/repo/view_file.tmpl b/templates/repo/view_file.tmpl index aa712bcc3b67..0fe0a131985d 100644 --- a/templates/repo/view_file.tmpl +++ b/templates/repo/view_file.tmpl @@ -38,7 +38,7 @@ {{end}} {{svg "octicon-download"}} - {{svg "octicon-copy" 14}} + {{svg "octicon-copy" 14}} {{if .Repository.CanEnableEditor}} {{if .CanEditFile}} {{svg "octicon-pencil"}} From cf710ebc948fa2eb6b757cf69ca3313c3f918a51 Mon Sep 17 00:00:00 2001 From: silverwind Date: Sat, 19 Nov 2022 13:42:35 +0100 Subject: [PATCH 17/18] make convertImage more flexible --- web_src/js/features/copycontent.js | 4 ++-- web_src/js/utils.js | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/web_src/js/features/copycontent.js b/web_src/js/features/copycontent.js index 874ad727612e..9b791bedba56 100644 --- a/web_src/js/features/copycontent.js +++ b/web_src/js/features/copycontent.js @@ -1,6 +1,6 @@ import {copyToClipboard} from './clipboard.js'; import {showTemporaryTooltip} from '../modules/tippy.js'; -import {imageBlobToPng} from '../utils.js'; +import {convertImage} from '../utils.js'; const {i18n} = window.config; async function doCopy(content, btn) { @@ -47,7 +47,7 @@ export function initCopyContent() { } catch { if (isImage) { // convert image to png as last-resort as some browser only support png copy try { - await doCopy(await imageBlobToPng(content), btn); + await doCopy(await convertImage(content, 'image/png'), btn); } catch { showTemporaryTooltip(btn, i18n.copy_error); } diff --git a/web_src/js/utils.js b/web_src/js/utils.js index 42b89d38f2b3..d08b8e6101c1 100644 --- a/web_src/js/utils.js +++ b/web_src/js/utils.js @@ -104,8 +104,8 @@ export function blobToDataURI(blob) { }); } -// convert any image Blob to a png Blob -export function imageBlobToPng(blob) { +// convert image Blob to another mime-type format. +export function convertImage(blob, mime) { return new Promise(async (resolve, reject) => { try { const img = new Image(); @@ -118,7 +118,7 @@ export function imageBlobToPng(blob) { canvas.toBlob((blob) => { if (!(blob instanceof Blob)) return reject(new Error('imageBlobToPng failed')); resolve(blob); - }, 'image/png'); + }, mime); }); img.addEventListener('error', () => { reject(new Error('imageBlobToPng failed')); From ff8cd672e679d875850148948c242f7cd2e1425d Mon Sep 17 00:00:00 2001 From: silverwind Date: Mon, 21 Nov 2022 10:06:00 +0100 Subject: [PATCH 18/18] add another safeguard catch handler --- web_src/js/utils.js | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/web_src/js/utils.js b/web_src/js/utils.js index d08b8e6101c1..62ee11c2eb78 100644 --- a/web_src/js/utils.js +++ b/web_src/js/utils.js @@ -111,14 +111,18 @@ export function convertImage(blob, mime) { const img = new Image(); const canvas = document.createElement('canvas'); img.addEventListener('load', () => { - canvas.width = img.naturalWidth; - canvas.height = img.naturalHeight; - const context = canvas.getContext('2d'); - context.drawImage(img, 0, 0); - canvas.toBlob((blob) => { - if (!(blob instanceof Blob)) return reject(new Error('imageBlobToPng failed')); - resolve(blob); - }, mime); + try { + canvas.width = img.naturalWidth; + canvas.height = img.naturalHeight; + const context = canvas.getContext('2d'); + context.drawImage(img, 0, 0); + canvas.toBlob((blob) => { + if (!(blob instanceof Blob)) return reject(new Error('imageBlobToPng failed')); + resolve(blob); + }, mime); + } catch (err) { + reject(err); + } }); img.addEventListener('error', () => { reject(new Error('imageBlobToPng failed'));