diff --git a/CHANGELOG.md b/CHANGELOG.md index aef8e740..0ac12ad2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), - Automatic Markdown image resizing and srcset generation - Performance and Accessibility improvements to achieve perfect Lighthouse scores - Tables of Contents on article pages +- Code copy buttons in article content - Taxonomy and term listings now support Markdown content - Taxonomies on article and list pages - Article pagination direction can be inverted diff --git a/README.md b/README.md index 27440aed..5f817a49 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ Congo is designed to be a powerful, lightweight theme for [Hugo](https://gohugo. - Mathematical notation using KaTeX - SVG icons from FontAwesome 5 - Automatic image resizing using Hugo Pipes -- Heading anchors, Tables of Contents, Buttons, Badges and more +- Heading anchors, Tables of Contents, Code copy, Buttons, Badges and more - HTML and Emoji support in articles 🎉 - SEO friendly with links for sharing to social media - Fathom Analytics and Google Analytics support diff --git a/assets/css/compiled/main.css b/assets/css/compiled/main.css index 652edda5..62d63897 100644 --- a/assets/css/compiled/main.css +++ b/assets/css/compiled/main.css @@ -1022,11 +1022,73 @@ body a, body button { margin-right: 0px; } +/* Code Copy */ + +.highlight-wrapper { + display: block; +} + +.highlight { + position: relative; + z-index: 0; +} + +.highlight:hover > .copy-button { + visibility: visible; +} + +.copy-button { + visibility: hidden; + position: absolute; + top: 0px; + right: 0px; + z-index: 10; + width: 5rem; + cursor: pointer; + white-space: nowrap; + border-bottom-left-radius: 0.375rem; + border-top-right-radius: 0.375rem; + --tw-bg-opacity: 1; + background-color: rgba(var(--color-neutral-200), var(--tw-bg-opacity)); + padding-top: 0.25rem; + padding-bottom: 0.25rem; + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; + font-size: 0.875rem; + line-height: 1.25rem; + --tw-text-opacity: 1; + color: rgba(var(--color-neutral-700), var(--tw-text-opacity)); + opacity: 0.9; +} + +.dark .copy-button { + --tw-bg-opacity: 1; + background-color: rgba(var(--color-neutral-600), var(--tw-bg-opacity)); + --tw-text-opacity: 1; + color: rgba(var(--color-neutral-200), var(--tw-text-opacity)); +} + +.copy-button:hover, .copy-button:focus, .copy-button:active, .copy-button:active:hover { + --tw-bg-opacity: 1; + background-color: rgba(var(--color-primary-100), var(--tw-bg-opacity)); +} + +.dark .copy-button:hover, .dark .copy-button:focus, .dark .copy-button:active, .dark .copy-button:active:hover { + --tw-bg-opacity: 1; + background-color: rgba(var(--color-primary-600), var(--tw-bg-opacity)); +} + +.copy-textarea { + position: absolute; + z-index: -10; + opacity: 0.05; +} + /* -- Chroma Highlight -- */ /* Background */ .prose .chroma { + position: static; border-radius: 0.375rem; --tw-bg-opacity: 1; background-color: rgba(var(--color-neutral-50), var(--tw-bg-opacity)); diff --git a/assets/css/main.css b/assets/css/main.css index 2cc0b9d6..0c4e9314 100644 --- a/assets/css/main.css +++ b/assets/css/main.css @@ -77,10 +77,33 @@ body button { @apply rtl:mr-0; } +/* Code Copy */ +.highlight-wrapper { + @apply block; +} +.highlight { + @apply relative z-0; +} +.highlight:hover > .copy-button { + @apply visible; +} +.copy-button { + @apply absolute top-0 right-0 z-10 invisible w-20 py-1 font-mono text-sm cursor-pointer opacity-90 bg-neutral-200 whitespace-nowrap rounded-bl-md rounded-tr-md text-neutral-700 dark:bg-neutral-600 dark:text-neutral-200; +} +.copy-button:hover, +.copy-button:focus, +.copy-button:active, +.copy-button:active:hover { + @apply bg-primary-100 dark:bg-primary-600; +} +.copy-textarea { + @apply absolute opacity-5 -z-10; +} + /* -- Chroma Highlight -- */ /* Background */ .prose .chroma { - @apply rounded-md text-neutral-700 bg-neutral-50 dark:bg-neutral-700 dark:text-neutral-200; + @apply static rounded-md text-neutral-700 bg-neutral-50 dark:bg-neutral-700 dark:text-neutral-200; } /* LineTableTD */ .chroma .lntd, diff --git a/assets/js/code.js b/assets/js/code.js new file mode 100644 index 00000000..5e88694e --- /dev/null +++ b/assets/js/code.js @@ -0,0 +1,65 @@ +var codeLang = document.getElementById("code-lang"); +var copyText = codeLang ? codeLang.getAttribute("data-copy") : "Copy"; +var copiedText = codeLang ? codeLang.getAttribute("data-copied") : "Copied"; + +function createCopyButton(highlightDiv) { + const button = document.createElement("button"); + button.className = "copy-button"; + button.type = "button"; + button.innerText = copyText; + button.addEventListener("click", () => copyCodeToClipboard(button, highlightDiv)); + addCopyButtonToDom(button, highlightDiv); +} + +async function copyCodeToClipboard(button, highlightDiv) { + const codeToCopy = highlightDiv.querySelector(":last-child > .chroma > code").innerText; + try { + result = await navigator.permissions.query({ name: "clipboard-write" }); + if (result.state == "granted" || result.state == "prompt") { + await navigator.clipboard.writeText(codeToCopy); + } else { + copyCodeBlockExecCommand(codeToCopy, highlightDiv); + } + } catch (_) { + copyCodeBlockExecCommand(codeToCopy, highlightDiv); + } finally { + codeWasCopied(button); + } +} + +function copyCodeBlockExecCommand(codeToCopy, highlightDiv) { + const textArea = document.createElement("textArea"); + textArea.contentEditable = "true"; + textArea.readOnly = "false"; + textArea.className = "copy-textarea"; + textArea.value = codeToCopy; + highlightDiv.insertBefore(textArea, highlightDiv.firstChild); + const range = document.createRange(); + range.selectNodeContents(textArea); + const sel = window.getSelection(); + sel.removeAllRanges(); + sel.addRange(range); + textArea.setSelectionRange(0, 999999); + document.execCommand("copy"); + highlightDiv.removeChild(textArea); +} + +function codeWasCopied(button) { + button.blur(); + button.innerText = copiedText; + setTimeout(function () { + button.innerText = copyText; + }, 2000); +} + +function addCopyButtonToDom(button, highlightDiv) { + highlightDiv.insertBefore(button, highlightDiv.firstChild); + const wrapper = document.createElement("div"); + wrapper.className = "highlight-wrapper"; + highlightDiv.parentNode.insertBefore(wrapper, highlightDiv); + wrapper.appendChild(highlightDiv); +} + +window.addEventListener("DOMContentLoaded", (event) => { + document.querySelectorAll(".highlight").forEach((highlightDiv) => createCopyButton(highlightDiv)); +}); diff --git a/config/_default/params.toml b/config/_default/params.toml index da9dd8e0..c01c0bb5 100644 --- a/config/_default/params.toml +++ b/config/_default/params.toml @@ -7,6 +7,7 @@ colorScheme = "congo" enableSearch = false +enableCodeCopy = false darkMode = "auto" # logo = "img/logo.jpg" # mainSections = ["section1", "section2"] diff --git a/exampleSite/config/_default/params.toml b/exampleSite/config/_default/params.toml index b6c9040a..14666e8f 100644 --- a/exampleSite/config/_default/params.toml +++ b/exampleSite/config/_default/params.toml @@ -7,6 +7,7 @@ colorScheme = "congo" enableSearch = true +enableCodeCopy = true darkMode = "auto" # logo = "img/logo.jpg" mainSections = ["samples"] diff --git a/exampleSite/content/docs/configuration.md b/exampleSite/content/docs/configuration.md index 7a994ed4..499b9c88 100644 --- a/exampleSite/content/docs/configuration.md +++ b/exampleSite/content/docs/configuration.md @@ -100,6 +100,7 @@ Many of the article defaults here can be overridden on a per article basis by sp |---|---|---| |`colorScheme`|`"congo"`|The theme colour scheme to use. Valid values are `congo` (default), `avocado`, `ocean`, `fire` and `slate`. Refer to the [Colour Schemes]({{< ref "getting-started#colour-schemes" >}}) section for more details.| |`enableSearch`|`false`|Whether site search is enabled. Set to `true` to enable search functionality. Note that the search feature depends on the `outputs.home` setting in the [site configuration](#site-configuration) being set correctly.| +|`enableCodeCopy`|`false`|Whether copy buttons are enabled for `` blocks.| |`darkMode`|`"auto"`|The preferred theme appearance for dark mode. Set to `true` to force dark appearance or `false` to force light appearance. Using `"auto"` will defer to the user's operating system preference.| |`logo`|_Not set_|The relative path to the site logo file within the `assets/` folder. The logo file should be provided at 2x resolution and supports any image dimensions.| |`mainSections`|_Not set_|The sections that should be displayed in the recent articles list. If not provided the section with the greatest number of articles is used.| diff --git a/i18n/de.yaml b/i18n/de.yaml index 5a2d23db..e9a2dd53 100644 --- a/i18n/de.yaml +++ b/i18n/de.yaml @@ -16,6 +16,10 @@ article: author: byline_title: "Autor" +# code: +# copy: "Copy" +# copied: "Copied" + error: 404_title: "Seite nicht gefunden :confused:" 404_error: "Fehler 404" diff --git a/i18n/en.yaml b/i18n/en.yaml index 265b298d..a2ec67a1 100644 --- a/i18n/en.yaml +++ b/i18n/en.yaml @@ -16,6 +16,10 @@ article: author: byline_title: "Author" +code: + copy: "Copy" + copied: "Copied" + error: 404_title: "Page Not Found :confused:" 404_error: "Error 404" diff --git a/i18n/es.yaml b/i18n/es.yaml index 74afcf3b..8ff30397 100644 --- a/i18n/es.yaml +++ b/i18n/es.yaml @@ -16,6 +16,10 @@ article: author: byline_title: "Autor" +# code: +# copy: "Copy" +# copied: "Copied" + error: 404_title: "Página no encontrada :confused:" 404_error: "Error 404" diff --git a/i18n/fr.yaml b/i18n/fr.yaml index f9c270ec..4c1e73e8 100644 --- a/i18n/fr.yaml +++ b/i18n/fr.yaml @@ -16,6 +16,10 @@ article: author: byline_title: "Auteur" +# code: +# copy: "Copy" +# copied: "Copied" + error: 404_title: "Cette page n'existe pas :confused:" 404_error: "Erreur 404" diff --git a/i18n/pt-BR.yaml b/i18n/pt-BR.yaml index bd06e2ea..f9cc48b4 100644 --- a/i18n/pt-BR.yaml +++ b/i18n/pt-BR.yaml @@ -16,6 +16,10 @@ article: author: byline_title: "Autor" +# code: +# copy: "Copy" +# copied: "Copied" + error: 404_title: "Página não econtrada :confused:" 404_error: "Erro 404" diff --git a/i18n/tr.yaml b/i18n/tr.yaml index 2b514296..7fa4941f 100644 --- a/i18n/tr.yaml +++ b/i18n/tr.yaml @@ -15,6 +15,10 @@ article: author: byline_title: "Yazar" +# code: +# copy: "Copy" +# copied: "Copied" + error: 404_title: "Sayfa Bulunamadı :confused:" 404_error: "Hata 404" diff --git a/i18n/zh.yaml b/i18n/zh.yaml index 0d1d6fb1..f72e3690 100644 --- a/i18n/zh.yaml +++ b/i18n/zh.yaml @@ -15,6 +15,10 @@ article: author: byline_title: "作者" +# code: +# copy: "Copy" +# copied: "Copied" + error: 404_title: "找不到网页 :confused:" 404_error: "404 错误" diff --git a/layouts/partials/head.html b/layouts/partials/head.html index 88faad10..8cecb591 100644 --- a/layouts/partials/head.html +++ b/layouts/partials/head.html @@ -59,8 +59,14 @@ {{ $jsSearch := resources.Get "js/search.js" }} {{ $assets.Add "js" (slice $jsFuse $jsSearch) }} {{ end }} + {{ if .Site.Params.enableCodeCopy | default false }} + {{ $jsCode := resources.Get "js/code.js" }} + {{ $assets.Add "js" (slice $jsCode) }} + + {{ end }} {{ if $assets.Get "js" }} - {{ $bundleJS := $assets.Get "js" | resources.Concat "js/main.bundle.js" | resources.Minify | resources.Fingerprint "sha512" }} + {{ $bundleJS := $assets.Get "js" | resources.Concat "js/main.bundle.js" | resources.Minify | resources.Fingerprint "sha512" }} + {{ end }} {{/* Icons */}} {{ if templates.Exists "partials/favicons.html" }}