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 @@ - + + + + + + + + diff --git a/src/layouts/partials/scripts.html b/src/layouts/partials/scripts.html index 6cb54836c..cce276a89 100644 --- a/src/layouts/partials/scripts.html +++ b/src/layouts/partials/scripts.html @@ -1,11 +1,18 @@ {{- $bootstrapJs := resources.Get "/js/bootstrap.bundle.min.js" | resources.Copy "/assets/js/vendor/bootstrap.bundle.min.js" -}} -{{- $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 -}} + +