From f5593d08dc6d615e650fe5e954b300d1895212b7 Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Sun, 2 Apr 2023 11:25:36 +0200
Subject: [PATCH] Use clippie module to copy to clipboard (#23801)

Externalize clipboard copying to the
[clippie](https://github.com/silverwind/clippie) module which I feel I
can maintain outside this repo for shared benefit with my other
projects.

The module is feature-equivalent to the previous code and has one
improvement where it sets `aria-hidden` on the fallback textarea,
preventing screen readers from picking it up. Also it support `Array` of
`content` as well to copy multiple items at once, in case it's ever
needed.
---
 package-lock.json                  |  6 +++++
 package.json                       |  1 +
 web_src/js/features/clipboard.js   | 43 ++----------------------------
 web_src/js/features/copycontent.js |  4 +--
 web_src/js/features/repo-code.js   |  4 +--
 5 files changed, 13 insertions(+), 45 deletions(-)

diff --git a/package-lock.json b/package-lock.json
index 313fa515ae..7bbe13abc3 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -19,6 +19,7 @@
         "add-asset-webpack-plugin": "2.0.1",
         "ansi-to-html": "0.7.2",
         "asciinema-player": "3.2.0",
+        "clippie": "3.1.4",
         "css-loader": "6.7.3",
         "dropzone": "6.0.0-beta.2",
         "easymde": "2.18.0",
@@ -2762,6 +2763,11 @@
         "url": "https://github.com/chalk/strip-ansi?sponsor=1"
       }
     },
+    "node_modules/clippie": {
+      "version": "3.1.4",
+      "resolved": "https://registry.npmjs.org/clippie/-/clippie-3.1.4.tgz",
+      "integrity": "sha512-jrW6sG1zcTEQr5MtCXJzszNmHWV9Fkaco8sAqFeuOApNFP/lRFcUi4JABMmxBJwFZLIvbw2BY3G5E+BjBqZMdQ=="
+    },
     "node_modules/cliui": {
       "version": "7.0.4",
       "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz",
diff --git a/package.json b/package.json
index dc781c6a32..640b4fa20f 100644
--- a/package.json
+++ b/package.json
@@ -19,6 +19,7 @@
     "add-asset-webpack-plugin": "2.0.1",
     "ansi-to-html": "0.7.2",
     "asciinema-player": "3.2.0",
+    "clippie": "3.1.4",
     "css-loader": "6.7.3",
     "dropzone": "6.0.0-beta.2",
     "easymde": "2.18.0",
diff --git a/web_src/js/features/clipboard.js b/web_src/js/features/clipboard.js
index 07c439504e..bcbdae2704 100644
--- a/web_src/js/features/clipboard.js
+++ b/web_src/js/features/clipboard.js
@@ -1,48 +1,9 @@
 import {showTemporaryTooltip} from '../modules/tippy.js';
 import {toAbsoluteUrl} from '../utils.js';
+import {clippie} from 'clippie';
 
 const {copy_success, copy_error} = window.config.i18n;
 
-export async function copyToClipboard(content) {
-  if (content instanceof Blob) {
-    const item = new ClipboardItem({[content.type]: content});
-    await navigator.clipboard.write([item]);
-  } else { // text
-    try {
-      await navigator.clipboard.writeText(content);
-    } catch {
-      return fallbackCopyToClipboard(content);
-    }
-  }
-  return true;
-}
-
-// 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;
-
-  const tempTextArea = document.createElement('textarea');
-  tempTextArea.value = text;
-
-  // avoid scrolling
-  tempTextArea.style.top = 0;
-  tempTextArea.style.left = 0;
-  tempTextArea.style.position = 'fixed';
-
-  document.body.appendChild(tempTextArea);
-
-  tempTextArea.select();
-
-  // 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 function initGlobalCopyToClipboardListener() {
@@ -61,7 +22,7 @@ export function initGlobalCopyToClipboardListener() {
         e.preventDefault();
 
         (async() => {
-          const success = await copyToClipboard(text);
+          const success = await clippie(text);
           showTemporaryTooltip(target, success ? copy_success : copy_error);
         })();
 
diff --git a/web_src/js/features/copycontent.js b/web_src/js/features/copycontent.js
index 5a4b99ae9b..e51953625d 100644
--- a/web_src/js/features/copycontent.js
+++ b/web_src/js/features/copycontent.js
@@ -1,11 +1,11 @@
-import {copyToClipboard} from './clipboard.js';
+import {clippie} from 'clippie';
 import {showTemporaryTooltip} from '../modules/tippy.js';
 import {convertImage} from '../utils.js';
 
 const {i18n} = window.config;
 
 async function doCopy(content, btn) {
-  const success = await copyToClipboard(content);
+  const success = await clippie(content);
   showTemporaryTooltip(btn, success ? i18n.copy_success : i18n.copy_error);
 }
 
diff --git a/web_src/js/features/repo-code.js b/web_src/js/features/repo-code.js
index 1c59913132..c0dced44eb 100644
--- a/web_src/js/features/repo-code.js
+++ b/web_src/js/features/repo-code.js
@@ -2,7 +2,7 @@ import $ from 'jquery';
 import {svg} from '../svg.js';
 import {invertFileFolding} from './file-fold.js';
 import {createTippy} from '../modules/tippy.js';
-import {copyToClipboard} from './clipboard.js';
+import {clippie} from 'clippie';
 import {toAbsoluteUrl} from '../utils.js';
 
 export const singleAnchorRegex = /^#(L|n)([1-9][0-9]*)$/;
@@ -190,7 +190,7 @@ export function initRepoCodeView() {
     currentTarget.closest('tr').outerHTML = blob;
   });
   $(document).on('click', '.copy-line-permalink', async (e) => {
-    const success = await copyToClipboard(toAbsoluteUrl(e.currentTarget.getAttribute('data-url')));
+    const success = await clippie(toAbsoluteUrl(e.currentTarget.getAttribute('data-url')));
     if (!success) return;
     document.querySelector('.code-line-button')?._tippy?.hide();
   });