From 99722c58fa1455702cff7815a1295f3e2887f422 Mon Sep 17 00:00:00 2001
From: XhmikosR <xhmikosr@gmail.com>
Date: Sun, 27 Nov 2022 17:34:33 +0200
Subject: [PATCH] Add copy support

---
 package-lock.json                  | 39 +++++++++++++++++++++++
 package.json                       |  1 +
 src/assets/js/application.js       | 51 ++++++++++++++++++++++++++++++
 src/assets/scss/_clipboard-js.scss | 35 ++++++++++++++++++++
 src/assets/scss/style.scss         |  1 +
 src/layouts/partials/icons.html    |  9 +++++-
 src/layouts/partials/scripts.html  | 11 +++++--
 7 files changed, 144 insertions(+), 3 deletions(-)
 create mode 100644 src/assets/js/application.js
 create mode 100644 src/assets/scss/_clipboard-js.scss

diff --git a/package-lock.json b/package-lock.json
index 1395da5da..e548ac747 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -11,6 +11,7 @@
       "devDependencies": {
         "autoprefixer": "^10.4.13",
         "bootstrap": "5.2.3",
+        "clipboard": "^2.0.11",
         "cross-env": "^7.0.3",
         "find-unused-sass-variables": "^4.0.5",
         "hugo-bin": "^0.95.0",
@@ -840,6 +841,17 @@
         "fsevents": "~2.3.2"
       }
     },
+    "node_modules/clipboard": {
+      "version": "2.0.11",
+      "resolved": "https://registry.npmjs.org/clipboard/-/clipboard-2.0.11.tgz",
+      "integrity": "sha512-C+0bbOqkezLIsmWSvlsXS0Q0bmkugu7jcfMIACB+RDEntIzQIkdr148we28AfSloQLRdZlYL/QYyrq05j/3Faw==",
+      "dev": true,
+      "dependencies": {
+        "good-listener": "^1.2.2",
+        "select": "^1.1.2",
+        "tiny-emitter": "^2.0.0"
+      }
+    },
     "node_modules/cliui": {
       "version": "7.0.4",
       "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz",
@@ -1238,6 +1250,12 @@
         "url": "https://github.com/sponsors/ljharb"
       }
     },
+    "node_modules/delegate": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/delegate/-/delegate-3.2.0.tgz",
+      "integrity": "sha512-IofjkYBZaZivn0V8nnsMJGBr4jVLxHDheKSW88PyxS5QC4Vo9ZbZVvhzlSxY87fVq3STR6r+4cGepyHkcWOQSw==",
+      "dev": true
+    },
     "node_modules/dependency-graph": {
       "version": "0.11.0",
       "resolved": "https://registry.npmjs.org/dependency-graph/-/dependency-graph-0.11.0.tgz",
@@ -2085,6 +2103,15 @@
       "integrity": "sha512-xYfnw62CKG8nLkZBfWbhWwDw02CHty86jfPcc2cr3ZfeuK9ysoVPPEUxf21bAD/rWAgk52SuBrLJlefNy8mvFg==",
       "dev": true
     },
+    "node_modules/good-listener": {
+      "version": "1.2.2",
+      "resolved": "https://registry.npmjs.org/good-listener/-/good-listener-1.2.2.tgz",
+      "integrity": "sha512-goW1b+d9q/HIwbVYZzZ6SsTr4IgE+WA44A0GmPIQstuOrgsFcT7VEJ48nmr9GaRtNu0XTKacFLGnBPAM6Afouw==",
+      "dev": true,
+      "dependencies": {
+        "delegate": "^3.1.2"
+      }
+    },
     "node_modules/got": {
       "version": "8.3.2",
       "resolved": "https://registry.npmjs.org/got/-/got-8.3.2.tgz",
@@ -4624,6 +4651,12 @@
       "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
       "dev": true
     },
+    "node_modules/select": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/select/-/select-1.1.2.tgz",
+      "integrity": "sha512-OwpTSOfy6xSs1+pwcNrv0RBMOzI39Lp3qQKUTPVVPRjCdNa5JH/oPRiqsesIskK8TVgmRiHwO4KXlV2Li9dANA==",
+      "dev": true
+    },
     "node_modules/semver": {
       "version": "5.7.1",
       "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
@@ -5623,6 +5656,12 @@
         "node": ">=0.10.0"
       }
     },
+    "node_modules/tiny-emitter": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz",
+      "integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==",
+      "dev": true
+    },
     "node_modules/to-buffer": {
       "version": "1.1.1",
       "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.1.1.tgz",
diff --git a/package.json b/package.json
index 8770bfea7..b92915060 100644
--- a/package.json
+++ b/package.json
@@ -35,6 +35,7 @@
   "devDependencies": {
     "autoprefixer": "^10.4.13",
     "bootstrap": "5.2.3",
+    "clipboard": "^2.0.11",
     "cross-env": "^7.0.3",
     "find-unused-sass-variables": "^4.0.5",
     "hugo-bin": "^0.95.0",
diff --git a/src/assets/js/application.js b/src/assets/js/application.js
new file mode 100644
index 000000000..031a07e28
--- /dev/null
+++ b/src/assets/js/application.js
@@ -0,0 +1,51 @@
+/* eslint-env browser */
+
+/* global ClipboardJS:false */
+
+import ClipboardJS from 'clipboard';
+
+const btnHtml = `<div class="bd-clipboard">
+  <button type="button" class="btn-clipboard" title="Copy to clipboard">
+    <svg class="bi" width="1em" height="1em" fill="currentColor">
+      <use xlink:href="#clipboard"/>
+    </svg>
+  </button>
+</div>`;
+
+document.querySelectorAll('div.highlight')
+  .forEach((element) => {
+    element.insertAdjacentHTML('beforebegin', btnHtml)
+  })
+
+const clipboard = new ClipboardJS('.btn-clipboard', {
+  target: trigger => trigger.parentNode.nextElementSibling
+})
+
+clipboard.on('success', (event) => {
+  const iconFirstChild = event.trigger.querySelector('.bi').firstElementChild
+  const namespace = 'http://www.w3.org/1999/xlink'
+  const originalXhref = iconFirstChild.getAttributeNS(namespace, 'href')
+  const originalTitle = event.trigger.title
+
+  event.clearSelection()
+  iconFirstChild.setAttributeNS(namespace, 'href', originalXhref.replace('clipboard', 'check2'))
+  event.trigger.title = 'Copied!'
+
+  setTimeout(() => {
+    iconFirstChild.setAttributeNS(namespace, 'href', originalXhref)
+    event.trigger.title = originalTitle
+  }, 2000)
+})
+
+/*clipboard.on('error', () => {
+  const modifierKey = /mac/i.test(navigator.userAgent) ? '\u2318' : 'Ctrl-'
+  const fallbackMsg = 'Press ' + modifierKey + 'C to copy'
+  const errorElement = document.getElementById('copy-error-callout')
+
+  if (!errorElement) {
+    return
+  }
+
+  errorElement.classList.remove('d-none')
+  errorElement.insertAdjacentHTML('afterbegin', fallbackMsg)
+})*/
diff --git a/src/assets/scss/_clipboard-js.scss b/src/assets/scss/_clipboard-js.scss
new file mode 100644
index 000000000..5d4cbba44
--- /dev/null
+++ b/src/assets/scss/_clipboard-js.scss
@@ -0,0 +1,35 @@
+// clipboard.js
+//
+// JS-based `Copy` buttons for code snippets.
+
+.bd-clipboard {
+  position: relative;
+  display: none;
+  float: right;
+
+  + .highlight {
+    margin-top: 0;
+  }
+
+  @media (min-width: 768px) {
+    display: block;
+  }
+}
+
+.btn-clipboard {
+  position: absolute;
+  top: .75em;
+  right: .5em;
+  z-index: 10;
+  display: block;
+  padding: .5em .75em .625em;
+  line-height: 1;
+  color: var(--bs-gray-900);
+  background-color: var(--bs-gray-100);
+  border: 0;
+  border-radius: .25rem;
+
+  &:hover {
+    color: var(--bs-primary);
+  }
+}
diff --git a/src/assets/scss/style.scss b/src/assets/scss/style.scss
index f25ea4633..8e6cc6dd1 100644
--- a/src/assets/scss/style.scss
+++ b/src/assets/scss/style.scss
@@ -46,6 +46,7 @@
 @import "footer";
 @import "theme";
 @import "ads";
+@import "clipboard-js";
 
 .f0 { font-size: 3rem; }
 .f2 { font-size: 2rem; }
diff --git a/src/layouts/partials/icons.html b/src/layouts/partials/icons.html
index 722a8ab8b..46b6753ab 100644
--- a/src/layouts/partials/icons.html
+++ b/src/layouts/partials/icons.html
@@ -1,7 +1,14 @@
-<svg xmlns="http://www.w3.org/2000/svg" style="display: none;">
+<svg xmlns="http://www.w3.org/2000/svg" style="display: none">
   <symbol id="archive" viewBox="0 0 16 16">
     <path d="M0 2a1 1 0 0 1 1-1h14a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1v7.5a2.5 2.5 0 0 1-2.5 2.5h-9A2.5 2.5 0 0 1 1 12.5V5a1 1 0 0 1-1-1V2zm2 3v7.5A1.5 1.5 0 0 0 3.5 14h9a1.5 1.5 0 0 0 1.5-1.5V5H2zm13-3H1v2h14V2zM5 7.5a.5.5 0 0 1 .5-.5h5a.5.5 0 0 1 0 1h-5a.5.5 0 0 1-.5-.5z"/>
   </symbol>
+  <symbol id="check2" viewBox="0 0 16 16">
+    <path d="M13.854 3.646a.5.5 0 0 1 0 .708l-7 7a.5.5 0 0 1-.708 0l-3.5-3.5a.5.5 0 1 1 .708-.708L6.5 10.293l6.646-6.647a.5.5 0 0 1 .708 0z"/>
+  </symbol>
+  <symbol id="clipboard" viewBox="0 0 16 16">
+    <path d="M4 1.5H3a2 2 0 0 0-2 2V14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V3.5a2 2 0 0 0-2-2h-1v1h1a1 1 0 0 1 1 1V14a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V3.5a1 1 0 0 1 1-1h1v-1z"/>
+    <path d="M9.5 1a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-3a.5.5 0 0 1-.5-.5v-1a.5.5 0 0 1 .5-.5h3zm-3-1A1.5 1.5 0 0 0 5 1.5v1A1.5 1.5 0 0 0 6.5 4h3A1.5 1.5 0 0 0 11 2.5v-1A1.5 1.5 0 0 0 9.5 0h-3z"/>
+  </symbol>
   <symbol id="file-earmark-richtext" viewBox="0 0 16 16">
     <path d="M14 4.5V14a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2h5.5L14 4.5zm-3 0A1.5 1.5 0 0 1 9.5 3V1H4a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V4.5h-2z"/>
     <path d="M4.5 12.5A.5.5 0 0 1 5 12h3a.5.5 0 0 1 0 1H5a.5.5 0 0 1-.5-.5zm0-2A.5.5 0 0 1 5 10h6a.5.5 0 0 1 0 1H5a.5.5 0 0 1-.5-.5zm1.639-3.708 1.33.886 1.854-1.855a.25.25 0 0 1 .289-.047l1.888.974V8.5a.5.5 0 0 1-.5.5H5a.5.5 0 0 1-.5-.5V8s1.54-1.274 1.639-1.208zM6.25 6a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5z"/>
diff --git a/src/layouts/partials/scripts.html b/src/layouts/partials/scripts.html
index d4c42dcce..fe1ac08aa 100644
--- a/src/layouts/partials/scripts.html
+++ b/src/layouts/partials/scripts.html
@@ -1,11 +1,18 @@
 {{- $bootstrapJs := resources.Get "/js/bootstrap.min.js" | resources.Copy "/assets/js/vendor/bootstrap.min.js" -}}
 <script async src="{{ $bootstrapJs.Permalink | relURL }}"></script>
 
-{{- $esbuildOptions := dict "targetPath" "/assets/js/lazyload.js" "target" "es2019" -}}
+{{- $esbuildOptions := dict "target" "es2019" -}}
+{{- $lazyloadOptions := dict "targetPath" "/assets/js/lazyload.js" -}}
+{{- $appOptions := dict "targetPath" "/assets/js/application.js" -}}
 
 {{- if eq hugo.Environment "production" -}}
   {{- $esbuildOptions = merge $esbuildOptions (dict "minify" "true") -}}
 {{- end -}}
 
-{{- $lazyload := resources.Get "js/lazyload.js" | js.Build $esbuildOptions }}
+{{- $lazyloadOptions = merge $esbuildOptions $lazyloadOptions -}}
+{{- $appOptions = merge $esbuildOptions $appOptions -}}
+{{- $lazyload := resources.Get "js/lazyload.js" | js.Build $lazyloadOptions -}}
+{{- $application := resources.Get "js/application.js" | js.Build $appOptions -}}
+
 <script async src="{{ $lazyload.RelPermalink | relURL }}"></script>
+<script async src="{{ $application.RelPermalink | relURL }}"></script>