From ce7a6ba3e4e967921937b3c091783a153b92ceac Mon Sep 17 00:00:00 2001 From: Jason Fairchild Date: Wed, 19 Feb 2025 23:48:15 +1000 Subject: [PATCH 1/4] feat(#202): Add code copy button layout renderer --- src/parser/copycode.ts | 19 +++++++++++++++++++ src/parser/markdown.ts | 2 ++ static/markdown.css | 29 +++++++++++++++++++++++++++++ 3 files changed, 50 insertions(+) create mode 100644 src/parser/copycode.ts diff --git a/src/parser/copycode.ts b/src/parser/copycode.ts new file mode 100644 index 0000000..2fedd6c --- /dev/null +++ b/src/parser/copycode.ts @@ -0,0 +1,19 @@ +import MarkdownIt from 'markdown-it'; +import octicons from '@primer/octicons'; + +const copyIcon = octicons['copy'].toSVG({ class: 'icon-copy' }); + +export default function copycode(md: MarkdownIt) { + const defaultRender = md.renderer.rules.fence!; + md.renderer.rules.fence = (tokens, idx, options, env, self) => { + const renderedPreBlock = defaultRender(tokens, idx, options, env, self); + return ` +
+ ${renderedPreBlock} +
+ +
+
+`; + }; +} diff --git a/src/parser/markdown.ts b/src/parser/markdown.ts index 9a0fe9d..ffdadeb 100644 --- a/src/parser/markdown.ts +++ b/src/parser/markdown.ts @@ -1,5 +1,6 @@ import MarkdownIt from 'markdown-it'; import anchor from 'markdown-it-anchor'; +import copycode from './copycode.js'; import frontMatter from 'markdown-it-front-matter'; import highlight from './highlight.js'; import graphviz from './dot.js'; @@ -68,6 +69,7 @@ mdit.use(anchor, { placement: 'before', }), }); +mdit.use(copycode); mdit.use(graphviz); mdit.use(githubAlerts); mdit.use(mermaid); diff --git a/static/markdown.css b/static/markdown.css index e15c326..ecf17c7 100644 --- a/static/markdown.css +++ b/static/markdown.css @@ -180,6 +180,35 @@ blockquote { color: var(--alert-caution); } +/* -------------------------------------------------------------------------- + * COPY-CODE-BUTTON --------------------------------------------------------- */ + +.copy-wrapper { + position: absolute; + top: 0.5rem; + right: 0.5rem; +} +.copy-button { + padding: 0.4rem; + border: 0; + border-radius: 0.4rem; + background: none; + opacity: 0; + outline: none; + cursor: pointer; + transition: opacity 0.2s, background 0.2s; +} +pre:hover + .copy-wrapper .copy-button { + opacity: 0.3; +} +.copy-button:hover { + opacity: 1; + background-color: var(--border-regular); +} +.copy-button { + fill: var(--text-primary); +} + /* -------------------------------------------------------------------------- * MISCELLANEOUS ------------------------------------------------------------ */ From 789a4dedf0e2d59264fcae694f92b7f0c25f3a6c Mon Sep 17 00:00:00 2001 From: Jason Fairchild Date: Thu, 20 Feb 2025 10:27:05 +1000 Subject: [PATCH 2/4] feat(#202): Add clipboard library --- src/parser/copycode.ts | 2 +- src/routes/viewer.ts | 2 ++ static/copycode/client.js | 5 +++++ static/copycode/clipboard.min.js | 7 +++++++ 4 files changed, 15 insertions(+), 1 deletion(-) create mode 100644 static/copycode/client.js create mode 100644 static/copycode/clipboard.min.js diff --git a/src/parser/copycode.ts b/src/parser/copycode.ts index 2fedd6c..92a939b 100644 --- a/src/parser/copycode.ts +++ b/src/parser/copycode.ts @@ -11,7 +11,7 @@ export default function copycode(md: MarkdownIt) {
${renderedPreBlock}
- +
`; diff --git a/src/routes/viewer.ts b/src/routes/viewer.ts index b53e80a..ec569c4 100644 --- a/src/routes/viewer.ts +++ b/src/routes/viewer.ts @@ -88,6 +88,8 @@ router.get(/.*/, async (req: Request, res: Response) => { ${config.scripts ? `` : ''} + + `); }); diff --git a/static/copycode/client.js b/static/copycode/client.js new file mode 100644 index 0000000..ed5e111 --- /dev/null +++ b/static/copycode/client.js @@ -0,0 +1,5 @@ +var clipboard = new ClipboardJS('.copy-button'); + +//TODO: Implement success callback + +//TODO: Implement failure callback diff --git a/static/copycode/clipboard.min.js b/static/copycode/clipboard.min.js new file mode 100644 index 0000000..1103f81 --- /dev/null +++ b/static/copycode/clipboard.min.js @@ -0,0 +1,7 @@ +/*! + * clipboard.js v2.0.11 + * https://clipboardjs.com/ + * + * Licensed MIT © Zeno Rocha + */ +!function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports.ClipboardJS=e():t.ClipboardJS=e()}(this,function(){return n={686:function(t,e,n){"use strict";n.d(e,{default:function(){return b}});var e=n(279),i=n.n(e),e=n(370),u=n.n(e),e=n(817),r=n.n(e);function c(t){try{return document.execCommand(t)}catch(t){return}}var a=function(t){t=r()(t);return c("cut"),t};function o(t,e){var n,o,t=(n=t,o="rtl"===document.documentElement.getAttribute("dir"),(t=document.createElement("textarea")).style.fontSize="12pt",t.style.border="0",t.style.padding="0",t.style.margin="0",t.style.position="absolute",t.style[o?"right":"left"]="-9999px",o=window.pageYOffset||document.documentElement.scrollTop,t.style.top="".concat(o,"px"),t.setAttribute("readonly",""),t.value=n,t);return e.container.appendChild(t),e=r()(t),c("copy"),t.remove(),e}var f=function(t){var e=1 Date: Thu, 20 Feb 2025 11:59:33 +1000 Subject: [PATCH 3/4] feat(#202): Implement copy success / failure popup notifications --- src/parser/copycode.ts | 8 ++++- static/colors.css | 20 ++++++++++++ static/copycode/client.js | 69 ++++++++++++++++++++++++++++++++++++++- static/markdown.css | 6 ++-- 4 files changed, 97 insertions(+), 6 deletions(-) diff --git a/src/parser/copycode.ts b/src/parser/copycode.ts index 92a939b..9bdfd3c 100644 --- a/src/parser/copycode.ts +++ b/src/parser/copycode.ts @@ -2,6 +2,8 @@ import MarkdownIt from 'markdown-it'; import octicons from '@primer/octicons'; const copyIcon = octicons['copy'].toSVG({ class: 'icon-copy' }); +const checkIcon = octicons['check'].toSVG({ class: 'icon-check' }); +const xIcon = octicons['x'].toSVG({ class: 'icon-x' }); export default function copycode(md: MarkdownIt) { const defaultRender = md.renderer.rules.fence!; @@ -11,7 +13,11 @@ export default function copycode(md: MarkdownIt) {
${renderedPreBlock}
- +
`; diff --git a/static/colors.css b/static/colors.css index d3bf14c..846f6ad 100644 --- a/static/colors.css +++ b/static/colors.css @@ -19,6 +19,16 @@ --icon-file: var(--text-secondary); --icon-back: var(--text-secondary); + /* Code Copy Icons */ + --icon-copy-fg: #aaa; + --icon-copy-bg: #333; + --icon-check-fg: #ddffcc; + --icon-check-bg: #5c993d; + --icon-x-fg: #ffcccc; + --icon-x-bg: #993d3d; + --copy-button-fg: var(--icon-copy-fg) + --copy-button-bg: var(--icon-copy-bg) + --syntax-text: var(--text-primary); --syntax-keyword: #aed7ff; --syntax-entity: #aeafff; @@ -69,6 +79,16 @@ --icon-file: #636c76; --icon-back: var(--text-secondary); + /* Code Copy Icons */ + --icon-copy-fg: #1f2328; + --icon-copy-bg: #e7e9eb; + --icon-check-fg: #154000; + --icon-check-bg: #c7e6b8; + --icon-x-fg: #400000; + --icon-x-bg: #e6b8b8; + --copy-button-fg: var(--icon-copy-fg) + --copy-button-bg: var(--icon-copy-bg) + /* source for light mode syntax theme: * https://github.com/highlightjs/highlight.js/blob/main/src/styles/github.css * */ diff --git a/static/copycode/client.js b/static/copycode/client.js index ed5e111..3bc27f9 100644 --- a/static/copycode/client.js +++ b/static/copycode/client.js @@ -1,5 +1,72 @@ -var clipboard = new ClipboardJS('.copy-button'); +let clipboard = new ClipboardJS('.copy-button'); + +// Setup copy button icons +const Icons = { + Copy: { + Class: 'icon-copy', + ForegroundColor: '', + BackgroundColor: '', + }, + Success: { + Class: 'icon-check', + ForegroundColor: '', + BackgroundColor: '', + }, + Fail: { + Class: 'icon-x', + ForegroundColor: '', + BackgroundColor: '', + }, +}; +resetButtons(); +window.matchMedia('(prefers-color-scheme: light)').addEventListener('change', resetButtons); + +function resetButtons() { + getIconColors(); + document.querySelectorAll('.copy-button').forEach((btn) => { + setIcon(btn, Icons.Copy); + }); +} + +// Get icon color values from css +function getIconColors() { + console.log('getting colors'); + let element = window.getComputedStyle(document.documentElement); + Object.entries(Icons).forEach(([, icon]) => { + icon.ForegroundColor = element.getPropertyValue(`--${icon.Class}-fg`); + icon.BackgroundColor = element.getPropertyValue(`--${icon.Class}-bg`); + }); +} + +function setIcon(btn, icon) { + for (let child of btn.children) { + console.log(child.classList.contains(icon.Class)); + if (child.classList.contains(icon.Class)) { + child.style.display = 'inline-block'; + btn.style.setProperty('--copy-button-fg', icon.ForegroundColor); + btn.style.setProperty('--copy-button-bg', icon.BackgroundColor); + } else { + child.style.display = 'none'; + } + } +} //TODO: Implement success callback +clipboard.on('success', function (e) { + console.info('Trigger:', e.trigger); + const btn = e.trigger; + + console.info(Icons); + + setIcon(btn, Icons.Success); + + setTimeout(() => { + setIcon(btn, Icons.Copy); + }, 2000); +}); //TODO: Implement failure callback +clipboard.on('error', function (e) { + console.log('Copy failed!'); + console.error('Trigger:', e.trigger); +}); diff --git a/static/markdown.css b/static/markdown.css index ecf17c7..0ad7cca 100644 --- a/static/markdown.css +++ b/static/markdown.css @@ -193,6 +193,7 @@ blockquote { border: 0; border-radius: 0.4rem; background: none; + fill: var(--copy-button-fg); opacity: 0; outline: none; cursor: pointer; @@ -203,10 +204,7 @@ pre:hover + .copy-wrapper .copy-button { } .copy-button:hover { opacity: 1; - background-color: var(--border-regular); -} -.copy-button { - fill: var(--text-primary); + background-color: var(--copy-button-bg); } /* -------------------------------------------------------------------------- From 750ae8c861841e391ff428a582cd7757612e63f6 Mon Sep 17 00:00:00 2001 From: Jason Fairchild Date: Sat, 22 Feb 2025 02:01:11 +1000 Subject: [PATCH 4/4] feat(#202): Implement copy content and simplify copy popup notifications --- src/parser/copycode.ts | 9 ++++- static/colors.css | 30 ++++++--------- static/copycode/client.js | 80 +++++++++------------------------------ static/markdown.css | 25 ++++++++++-- 4 files changed, 57 insertions(+), 87 deletions(-) diff --git a/src/parser/copycode.ts b/src/parser/copycode.ts index 9bdfd3c..cb611b9 100644 --- a/src/parser/copycode.ts +++ b/src/parser/copycode.ts @@ -9,15 +9,20 @@ export default function copycode(md: MarkdownIt) { const defaultRender = md.renderer.rules.fence!; md.renderer.rules.fence = (tokens, idx, options, env, self) => { const renderedPreBlock = defaultRender(tokens, idx, options, env, self); + const content = tokens[idx].content; return `
${renderedPreBlock}
- +
${checkIcon} +
+
${xIcon} - +
`; diff --git a/static/colors.css b/static/colors.css index 846f6ad..a101441 100644 --- a/static/colors.css +++ b/static/colors.css @@ -19,15 +19,12 @@ --icon-file: var(--text-secondary); --icon-back: var(--text-secondary); - /* Code Copy Icons */ - --icon-copy-fg: #aaa; - --icon-copy-bg: #333; - --icon-check-fg: #ddffcc; - --icon-check-bg: #5c993d; - --icon-x-fg: #ffcccc; - --icon-x-bg: #993d3d; - --copy-button-fg: var(--icon-copy-fg) - --copy-button-bg: var(--icon-copy-bg) + --copy-button-fg: #aaa; + --copy-button-bg: #333; + --copy-success-fg: #ddffcc; + --copy-success-bg: #5c993d; + --copy-fail-fg: #ffcccc; + --copy-fail-bg: #993d3d; --syntax-text: var(--text-primary); --syntax-keyword: #aed7ff; @@ -79,15 +76,12 @@ --icon-file: #636c76; --icon-back: var(--text-secondary); - /* Code Copy Icons */ - --icon-copy-fg: #1f2328; - --icon-copy-bg: #e7e9eb; - --icon-check-fg: #154000; - --icon-check-bg: #c7e6b8; - --icon-x-fg: #400000; - --icon-x-bg: #e6b8b8; - --copy-button-fg: var(--icon-copy-fg) - --copy-button-bg: var(--icon-copy-bg) + --copy-button-fg: #1f2328; + --copy-button-bg: #e7e9eb; + --copy-success-fg: #154000; + --copy-success-bg: #c7e6b8; + --copy-fail-fg: #400000; + --copy-fail-bg: #e6b8b8; /* source for light mode syntax theme: * https://github.com/highlightjs/highlight.js/blob/main/src/styles/github.css diff --git a/static/copycode/client.js b/static/copycode/client.js index 3bc27f9..bb0080f 100644 --- a/static/copycode/client.js +++ b/static/copycode/client.js @@ -1,72 +1,26 @@ -let clipboard = new ClipboardJS('.copy-button'); +document.querySelectorAll('.copy-success, .copy-fail').forEach((element) => { + element.style.display = 'none'; +}); -// Setup copy button icons -const Icons = { - Copy: { - Class: 'icon-copy', - ForegroundColor: '', - BackgroundColor: '', - }, - Success: { - Class: 'icon-check', - ForegroundColor: '', - BackgroundColor: '', - }, - Fail: { - Class: 'icon-x', - ForegroundColor: '', - BackgroundColor: '', - }, +const Notify = { + Success: '.copy-success', + Fail: '.copy-fail', }; -resetButtons(); -window.matchMedia('(prefers-color-scheme: light)').addEventListener('change', resetButtons); -function resetButtons() { - getIconColors(); - document.querySelectorAll('.copy-button').forEach((btn) => { - setIcon(btn, Icons.Copy); - }); -} - -// Get icon color values from css -function getIconColors() { - console.log('getting colors'); - let element = window.getComputedStyle(document.documentElement); - Object.entries(Icons).forEach(([, icon]) => { - icon.ForegroundColor = element.getPropertyValue(`--${icon.Class}-fg`); - icon.BackgroundColor = element.getPropertyValue(`--${icon.Class}-bg`); - }); -} - -function setIcon(btn, icon) { - for (let child of btn.children) { - console.log(child.classList.contains(icon.Class)); - if (child.classList.contains(icon.Class)) { - child.style.display = 'inline-block'; - btn.style.setProperty('--copy-button-fg', icon.ForegroundColor); - btn.style.setProperty('--copy-button-bg', icon.BackgroundColor); - } else { - child.style.display = 'none'; - } - } -} +let clipboard = new ClipboardJS('.copy-button'); -//TODO: Implement success callback clipboard.on('success', function (e) { - console.info('Trigger:', e.trigger); - const btn = e.trigger; - - console.info(Icons); - - setIcon(btn, Icons.Success); - - setTimeout(() => { - setIcon(btn, Icons.Copy); - }, 2000); + showNotification(e.trigger, Notify.Success); }); -//TODO: Implement failure callback clipboard.on('error', function (e) { - console.log('Copy failed!'); - console.error('Trigger:', e.trigger); + showNotification(e.trigger, Notify.Fail); }); + +function showNotification(btn, notify) { + const notificationElement = btn.parentElement.querySelector(notify); + notificationElement.style.display = 'flex'; + setTimeout(() => { + notificationElement.style.display = 'none'; + }, 2000); +} diff --git a/static/markdown.css b/static/markdown.css index 0ad7cca..0587506 100644 --- a/static/markdown.css +++ b/static/markdown.css @@ -188,24 +188,41 @@ blockquote { top: 0.5rem; right: 0.5rem; } -.copy-button { +.copy-button, .copy-success, .copy-fail { + position: absolute; + top: 0; + right: 0; + display: flex; + align-items: center; + justify-content: center; + height: 2rem; + width: 2rem; padding: 0.4rem; border: 0; border-radius: 0.4rem; background: none; fill: var(--copy-button-fg); - opacity: 0; - outline: none; cursor: pointer; transition: opacity 0.2s, background 0.2s; } -pre:hover + .copy-wrapper .copy-button { +.copy-button { + opacity: 0; +} +pre:hover + .copy-wrapper > .copy-button { opacity: 0.3; } .copy-button:hover { opacity: 1; background-color: var(--copy-button-bg); } +.copy-success { + background: var(--copy-success-bg); + fill: var(--copy-success-fg); +} +.copy-fail { + background: var(--copy-fail-bg); + fill: var(--copy-fail-fg); +} /* -------------------------------------------------------------------------- * MISCELLANEOUS ------------------------------------------------------------ */