diff --git a/package-lock.json b/package-lock.json
index 039a5f659..4eaacfccc 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -11,6 +11,7 @@
"devDependencies": {
"autoprefixer": "^10.4.14",
"bootstrap": "5.3.0-alpha1",
+ "clipboard": "^2.0.11",
"cross-env": "^7.0.3",
"eslint": "^8.36.0",
"find-unused-sass-variables": "^4.0.5",
@@ -1173,6 +1174,17 @@
"node": ">= 6"
}
},
+ "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",
@@ -1573,6 +1585,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",
@@ -2624,6 +2642,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/gopd": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
@@ -5440,6 +5467,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": "7.3.8",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz",
@@ -6525,6 +6558,12 @@
"integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==",
"dev": true
},
+ "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 9fc633348..fae692e1d 100644
--- a/package.json
+++ b/package.json
@@ -36,6 +36,7 @@
"devDependencies": {
"autoprefixer": "^10.4.14",
"bootstrap": "5.3.0-alpha1",
+ "clipboard": "^2.0.11",
"cross-env": "^7.0.3",
"eslint": "^8.36.0",
"find-unused-sass-variables": "^4.0.5",
diff --git a/src/assets/js/application.js b/src/assets/js/application.js
new file mode 100644
index 000000000..f1aa74612
--- /dev/null
+++ b/src/assets/js/application.js
@@ -0,0 +1,59 @@
+/* global bootstrap:false */
+
+import ClipboardJS from 'clipboard'
+
+const btnTitle = 'Copy to clipboard'
+
+const btnHtml = [
+'
',
+ `',
+'
'].join('')
+
+document.querySelectorAll('div.highlight')
+ .forEach((element) => {
+ element.insertAdjacentHTML('beforebegin', btnHtml)
+ })
+
+window.addEventListener('load', () => {
+ document.querySelectorAll('.btn-clipboard').forEach(btn => {
+ bootstrap.Tooltip.getOrCreateInstance(btn, { btnTitle })
+ })
+})
+
+const clipboard = new ClipboardJS('.btn-clipboard', {
+ target: trigger => trigger.parentNode.nextElementSibling,
+ text: trigger => trigger.parentNode.nextElementSibling.textContent.trimEnd()
+})
+
+clipboard.on('success', (event) => {
+ const iconFirstChild = event.trigger.querySelector('.bi').firstElementChild
+ const tooltipBtn = bootstrap.Tooltip.getInstance(event.trigger)
+ const namespace = 'http://www.w3.org/1999/xlink'
+ const originalXhref = iconFirstChild.getAttributeNS(namespace, 'href')
+ const originalTitle = event.trigger.title
+
+ tooltipBtn.setContent({ '.tooltip-inner': 'Copied!' })
+ event.trigger.addEventListener('hidden.bs.tooltip', () => {
+ tooltipBtn.setContent({ '.tooltip-inner': btnTitle })
+ }, { once: true })
+ event.clearSelection()
+ iconFirstChild.setAttributeNS(namespace, 'href', originalXhref.replace('clipboard', 'check2'))
+
+ setTimeout(() => {
+ iconFirstChild.setAttributeNS(namespace, 'href', originalXhref)
+ event.trigger.title = originalTitle
+ }, 2000)
+})
+
+clipboard.on('error', event => {
+ const modifierKey = /mac/i.test(navigator.userAgent) ? '\u2318' : 'Ctrl-'
+ const fallbackMsg = `Press ${modifierKey}C to copy`
+ const tooltipBtn = bootstrap.Tooltip.getInstance(event.trigger)
+
+ tooltipBtn.setContent({ '.tooltip-inner': fallbackMsg })
+ event.trigger.addEventListener('hidden.bs.tooltip', () => {
+ tooltipBtn.setContent({ '.tooltip-inner': btnTitle })
+ }, { once: true })
+})
diff --git a/src/assets/scss/_bootstrap.scss b/src/assets/scss/_bootstrap.scss
index 013626f1f..415cafc19 100644
--- a/src/assets/scss/_bootstrap.scss
+++ b/src/assets/scss/_bootstrap.scss
@@ -31,7 +31,7 @@
@import "bootstrap/close";
// @import "bootstrap/toasts";
// @import "bootstrap/modal";
-// @import "bootstrap/tooltip";
+@import "bootstrap/tooltip";
// @import "bootstrap/popover";
// @import "bootstrap/carousel";
// @import "bootstrap/spinners";
diff --git a/src/assets/scss/_clipboard-js.scss b/src/assets/scss/_clipboard-js.scss
new file mode 100644
index 000000000..2ae8fa5b6
--- /dev/null
+++ b/src/assets/scss/_clipboard-js.scss
@@ -0,0 +1,36 @@
+// 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-body-color);
+ background-color: var(--bd-pre-bg);
+
+ border: 0;
+ border-radius: .25rem;
+
+ &:hover {
+ color: var(--bs-link-hover-color);
+ }
+}
diff --git a/src/assets/scss/style.scss b/src/assets/scss/style.scss
index 6afe46dcb..23972b65b 100644
--- a/src/assets/scss/style.scss
+++ b/src/assets/scss/style.scss
@@ -4,6 +4,7 @@
@import "variables";
@import "ads";
@import "buttons";
+@import "clipboard-js";
@import "footer";
@import "navbar";
@import "pagination";
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 @@
-