mirror of
https://github.com/hacks-guide/minimal-mistakes.git
synced 2024-11-26 18:30:28 +00:00
Add "copy to clipboard" button for code blocks (#2812)
* Add copy-to-clipboard button and JS * Ignore line numbers if present * Rewrite heading permalink code to use vanilla JS * README: Add credits to zenorocha/clipboard.js (MIT License) @iBug really wants a place here in the Credits section :P * Add .no-copy for hiding the button, update docs * Add td.rouge-code to selectors * Fix navigator.clipboard branch * Add screenreader text for copy button * Restore focus to the button after copying * Add site-wide enable switch
This commit is contained in:
parent
c76813f6f5
commit
0527e17354
@ -204,6 +204,7 @@ To test the theme, run `bundle exec rake preview` and open your browser at `http
|
|||||||
- [Gumshoe](https://github.com/cferdinandi/gumshoe)
|
- [Gumshoe](https://github.com/cferdinandi/gumshoe)
|
||||||
- [jQuery throttle / debounce](http://benalman.com/projects/jquery-throttle-debounce-plugin/)
|
- [jQuery throttle / debounce](http://benalman.com/projects/jquery-throttle-debounce-plugin/)
|
||||||
- [Lunr](http://lunrjs.com)
|
- [Lunr](http://lunrjs.com)
|
||||||
|
- [Clipboard.js](https://clipboardjs.com)
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
@ -282,3 +283,7 @@ Pure Liquid Jekyll Table of Contents is distributed under the terms of the [MIT
|
|||||||
Minimal Mistakes incorporates [Lunr](http://lunrjs.com),
|
Minimal Mistakes incorporates [Lunr](http://lunrjs.com),
|
||||||
Copyright (c) 2018 Oliver Nightingale.
|
Copyright (c) 2018 Oliver Nightingale.
|
||||||
Lunr is distributed under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
Lunr is distributed under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
||||||
|
|
||||||
|
Minimal Mistakes incorporates [clipboard.js](https://clipboardjs.com/),
|
||||||
|
Copyright (c) 2021 Zeno Rocha.
|
||||||
|
Clipboard.js is distributed under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
||||||
|
@ -29,6 +29,7 @@ logo : # path of logo image to display in the masthead, e.g.
|
|||||||
masthead_title : # overrides the website title displayed in the masthead, use " " for no title
|
masthead_title : # overrides the website title displayed in the masthead, use " " for no title
|
||||||
# breadcrumbs : false # true, false (default)
|
# breadcrumbs : false # true, false (default)
|
||||||
words_per_minute : 200
|
words_per_minute : 200
|
||||||
|
enable_copy_code_button : # true, false (default)
|
||||||
copyright : # "copyright" name, defaults to site.title
|
copyright : # "copyright" name, defaults to site.title
|
||||||
copyright_url : # "copyright" URL, defaults to site.url
|
copyright_url : # "copyright" URL, defaults to site.url
|
||||||
comments:
|
comments:
|
||||||
|
@ -9,8 +9,11 @@
|
|||||||
{%- comment %} https://docs.google.com/presentation/d/1rmxwWa9P6_xHqonmh5ONXRS-jPc5XKbnv99Rjkhe04s/present {% endcomment -%}
|
{%- comment %} https://docs.google.com/presentation/d/1rmxwWa9P6_xHqonmh5ONXRS-jPc5XKbnv99Rjkhe04s/present {% endcomment -%}
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
|
||||||
<script>
|
<script type="text/javascript">
|
||||||
document.documentElement.className = document.documentElement.className.replace(/\bno-js\b/g, '') + ' js ';
|
document.documentElement.className = document.documentElement.className.replace(/\bno-js\b/g, '') + ' js ';
|
||||||
|
{% if site.enable_copy_code_button -%}
|
||||||
|
window.enable_copy_code_button = true;
|
||||||
|
{%- endif %}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- For all browsers -->
|
<!-- For all browsers -->
|
||||||
|
@ -591,3 +591,60 @@ a.reversefootnote {
|
|||||||
position: static;
|
position: static;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Copy <pre> block to clipboard
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
// a <textarea> to hold text for document.execCommand("copy")
|
||||||
|
.clipboard-helper {
|
||||||
|
// Prevent zooming on iOS
|
||||||
|
font-size: 12pt !important;
|
||||||
|
border: 0 !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
outline: none !important;
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre {
|
||||||
|
.clipboard-copy-button {
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
top: 0.6em;
|
||||||
|
right: 0.5em;
|
||||||
|
z-index: 1;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
border-radius: 0.1em;
|
||||||
|
padding: 0.2em 0.5em;
|
||||||
|
color: white;
|
||||||
|
opacity: 0.4;
|
||||||
|
transition: color 0.25s linear -0.25s, opacity 0.25s linear;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: #ffffca;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
@at-root {
|
||||||
|
.no-copy & {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover .clipboard-copy-button {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -2,18 +2,18 @@
|
|||||||
jQuery plugin settings and other scripts
|
jQuery plugin settings and other scripts
|
||||||
========================================================================== */
|
========================================================================== */
|
||||||
|
|
||||||
$(function() {
|
$(document).ready(function () {
|
||||||
// FitVids init
|
// FitVids init
|
||||||
$("#main").fitVids();
|
$("#main").fitVids();
|
||||||
|
|
||||||
// Follow menu drop down
|
// Follow menu drop down
|
||||||
$(".author__urls-wrapper").find("button").on("click", function() {
|
$(".author__urls-wrapper button").on("click", function () {
|
||||||
$(".author__urls").toggleClass("is--visible");
|
$(".author__urls").toggleClass("is--visible");
|
||||||
$(".author__urls-wrapper").find("button").toggleClass("open");
|
$(".author__urls-wrapper").find("button").toggleClass("open");
|
||||||
});
|
});
|
||||||
|
|
||||||
// Close search screen with Esc key
|
// Close search screen with Esc key
|
||||||
$(document).keyup(function(e) {
|
$(document).keyup(function (e) {
|
||||||
if (e.keyCode === 27) {
|
if (e.keyCode === 27) {
|
||||||
if ($(".initial-content").hasClass("is--hidden")) {
|
if ($(".initial-content").hasClass("is--hidden")) {
|
||||||
$(".search-content").toggleClass("is--visible");
|
$(".search-content").toggleClass("is--visible");
|
||||||
@ -23,12 +23,12 @@ $(function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Search toggle
|
// Search toggle
|
||||||
$(".search__toggle").on("click", function() {
|
$(".search__toggle").on("click", function () {
|
||||||
$(".search-content").toggleClass("is--visible");
|
$(".search-content").toggleClass("is--visible");
|
||||||
$(".initial-content").toggleClass("is--hidden");
|
$(".initial-content").toggleClass("is--hidden");
|
||||||
// set focus on input
|
// set focus on input
|
||||||
setTimeout(function() {
|
setTimeout(function () {
|
||||||
$(".search-content").find("input").focus();
|
$(".search-content input").focus();
|
||||||
}, 400);
|
}, 400);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -37,11 +37,11 @@ $(function() {
|
|||||||
offset: 20,
|
offset: 20,
|
||||||
speed: 400,
|
speed: 400,
|
||||||
speedAsDuration: true,
|
speedAsDuration: true,
|
||||||
durationMax: 500
|
durationMax: 500,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Gumshoe scroll spy init
|
// Gumshoe scroll spy init
|
||||||
if($("nav.toc").length > 0) {
|
if ($("nav.toc").length > 0) {
|
||||||
var spy = new Gumshoe("nav.toc a", {
|
var spy = new Gumshoe("nav.toc a", {
|
||||||
// Active classes
|
// Active classes
|
||||||
navClass: "active", // applied to the nav list item
|
navClass: "active", // applied to the nav list item
|
||||||
@ -56,7 +56,7 @@ $(function() {
|
|||||||
reflow: true, // if true, listen for reflows
|
reflow: true, // if true, listen for reflows
|
||||||
|
|
||||||
// Event support
|
// Event support
|
||||||
events: true // if true, emit custom events
|
events: true, // if true, emit custom events
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -95,38 +95,120 @@ $(function() {
|
|||||||
gallery: {
|
gallery: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
navigateByImgClick: true,
|
navigateByImgClick: true,
|
||||||
preload: [0, 1] // Will preload 0 - before current, and 1 after the current image
|
preload: [0, 1], // Will preload 0 - before current, and 1 after the current image
|
||||||
},
|
},
|
||||||
image: {
|
image: {
|
||||||
tError: '<a href="%url%">Image #%curr%</a> could not be loaded.'
|
tError: '<a href="%url%">Image #%curr%</a> could not be loaded.',
|
||||||
},
|
},
|
||||||
removalDelay: 500, // Delay in milliseconds before popup is removed
|
removalDelay: 500, // Delay in milliseconds before popup is removed
|
||||||
// Class that is added to body when popup is open.
|
// Class that is added to body when popup is open.
|
||||||
// make it unique to apply your CSS animations just to this exact popup
|
// make it unique to apply your CSS animations just to this exact popup
|
||||||
mainClass: "mfp-zoom-in",
|
mainClass: "mfp-zoom-in",
|
||||||
callbacks: {
|
callbacks: {
|
||||||
beforeOpen: function() {
|
beforeOpen: function () {
|
||||||
// just a hack that adds mfp-anim class to markup
|
// just a hack that adds mfp-anim class to markup
|
||||||
this.st.image.markup = this.st.image.markup.replace(
|
this.st.image.markup = this.st.image.markup.replace(
|
||||||
"mfp-figure",
|
"mfp-figure",
|
||||||
"mfp-figure mfp-with-anim"
|
"mfp-figure mfp-with-anim"
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
closeOnContentClick: true,
|
closeOnContentClick: true,
|
||||||
midClick: true // allow opening popup on middle mouse click. Always set it to true if you don't provide alternative source.
|
midClick: true, // allow opening popup on middle mouse click. Always set it to true if you don't provide alternative source.
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add anchors for headings
|
// Add anchors for headings
|
||||||
$('.page__content').find('h1, h2, h3, h4, h5, h6').each(function() {
|
document
|
||||||
var id = $(this).attr('id');
|
.querySelector(".page__content")
|
||||||
if (id) {
|
.querySelectorAll("h1, h2, h3, h4, h5, h6")
|
||||||
var anchor = document.createElement("a");
|
.forEach(function (element) {
|
||||||
anchor.className = 'header-link';
|
var id = element.getAttribute("id");
|
||||||
anchor.href = '#' + id;
|
if (id) {
|
||||||
anchor.innerHTML = '<span class=\"sr-only\">Permalink</span><i class=\"fas fa-link\"></i>';
|
var anchor = document.createElement("a");
|
||||||
anchor.title = "Permalink";
|
anchor.className = "header-link";
|
||||||
$(this).append(anchor);
|
anchor.href = "#" + id;
|
||||||
|
anchor.innerHTML =
|
||||||
|
'<span class="sr-only">Permalink</span><i class="fas fa-link"></i>';
|
||||||
|
anchor.title = "Permalink";
|
||||||
|
element.appendChild(anchor);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add copy button for <pre> blocks
|
||||||
|
var copyText = function (text) {
|
||||||
|
if (document.queryCommandEnabled("copy") && navigator.clipboard) {
|
||||||
|
navigator.clipboard.writeText(text).then(
|
||||||
|
() => true,
|
||||||
|
() => console.error("Failed to copy text to clipboard: " + text)
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
var isRTL = document.documentElement.getAttribute("dir") === "rtl";
|
||||||
|
|
||||||
|
var textarea = document.createElement("textarea");
|
||||||
|
textarea.className = "clipboard-helper";
|
||||||
|
textarea.style[isRTL ? "right" : "left"] = "-9999px";
|
||||||
|
// Move element to the same position vertically
|
||||||
|
var yPosition = window.pageYOffset || document.documentElement.scrollTop;
|
||||||
|
textarea.style.top = yPosition + "px";
|
||||||
|
|
||||||
|
textarea.setAttribute("readonly", "");
|
||||||
|
textarea.value = text;
|
||||||
|
document.body.appendChild(textarea);
|
||||||
|
|
||||||
|
var success = true;
|
||||||
|
try {
|
||||||
|
textarea.select();
|
||||||
|
success = document.execCommand("copy");
|
||||||
|
} catch (e) {
|
||||||
|
success = false;
|
||||||
|
}
|
||||||
|
textarea.parentNode.removeChild(textarea);
|
||||||
|
return success;
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
|
|
||||||
|
var copyButtonEventListener = function (event) {
|
||||||
|
var thisButton = event.target;
|
||||||
|
|
||||||
|
// Locate the <code> element
|
||||||
|
var codeBlock = thisButton.nextElementSibling;
|
||||||
|
while (codeBlock && codeBlock.tagName.toLowerCase() !== "code") {
|
||||||
|
codeBlock = codeBlock.nextElementSibling;
|
||||||
|
}
|
||||||
|
if (!codeBlock) {
|
||||||
|
// No <code> found - wtf?
|
||||||
|
console.warn(thisButton);
|
||||||
|
throw new Error("No code block found for this button.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip line numbers if present (i.e. {% highlight lineno %})
|
||||||
|
var realCodeBlock = codeBlock.querySelector("td.code, td.rouge-code");
|
||||||
|
if (realCodeBlock) {
|
||||||
|
codeBlock = realCodeBlock;
|
||||||
|
}
|
||||||
|
var result = copyText(codeBlock.innerText);
|
||||||
|
// Restore the focus to the button
|
||||||
|
thisButton.focus();
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (window.enable_copy_code_button) {
|
||||||
|
document
|
||||||
|
.querySelectorAll(".page__content pre > code")
|
||||||
|
.forEach(function (element, index, parentList) {
|
||||||
|
// Locate the <pre> element
|
||||||
|
var container = element.parentElement;
|
||||||
|
// Sanity check - don't add an extra button if there's already one
|
||||||
|
if (container.firstElementChild.tagName.toLowerCase() !== "code") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var copyButton = document.createElement("button");
|
||||||
|
copyButton.title = "Copy to clipboard";
|
||||||
|
copyButton.className = "clipboard-copy-button";
|
||||||
|
copyButton.innerHTML = '<span class="sr-only">Copy code</span><i class="far fa-copy"></i>';
|
||||||
|
copyButton.addEventListener("click", copyButtonEventListener);
|
||||||
|
container.prepend(copyButton);
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
2
assets/js/main.min.js
vendored
2
assets/js/main.min.js
vendored
File diff suppressed because one or more lines are too long
@ -24,6 +24,7 @@ logo : # path of logo image to display in the masthead, e.g.
|
|||||||
masthead_title : # overrides the website title displayed in the masthead, use " " for no title
|
masthead_title : # overrides the website title displayed in the masthead, use " " for no title
|
||||||
# breadcrumbs : false # true, false (default)
|
# breadcrumbs : false # true, false (default)
|
||||||
words_per_minute : 200
|
words_per_minute : 200
|
||||||
|
enable_copy_code_button : true
|
||||||
comments:
|
comments:
|
||||||
provider : "false" # false (default), "disqus", "discourse", "facebook", "staticman_v2", "staticman", "utterances", "giscus", "custom"
|
provider : "false" # false (default), "disqus", "discourse", "facebook", "staticman_v2", "staticman", "utterances", "giscus", "custom"
|
||||||
disqus:
|
disqus:
|
||||||
|
@ -296,6 +296,30 @@ For example,
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Code block copy button
|
||||||
|
|
||||||
|
To enable a copy button on code blocks, add the following to `_config.yml`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
enable_copy_code_button: true
|
||||||
|
```
|
||||||
|
|
||||||
|
When enabled site-wide, the button can be disabled on individual code blocks by adding `no-copy` to the code block's class list.
|
||||||
|
|
||||||
|
````markdown
|
||||||
|
```
|
||||||
|
Hey, I have a "copy to clipboard" button!
|
||||||
|
```
|
||||||
|
````
|
||||||
|
|
||||||
|
````markdown
|
||||||
|
```
|
||||||
|
But I don't have one.
|
||||||
|
```
|
||||||
|
{: .no-copy}
|
||||||
|
````
|
||||||
|
{: .no-copy}
|
||||||
|
|
||||||
### Comments
|
### Comments
|
||||||
|
|
||||||
[**Disqus**](https://disqus.com/), [**Discourse**](https://www.discourse.org/), [**Facebook**](https://developers.facebook.com/docs/plugins/comments), [**utterances**](https://utteranc.es/), [**giscus**](https://giscus.app/) and static-based commenting via [**Staticman**](https://staticman.net/) are built into the theme. First set the comment provider you'd like to use:
|
[**Disqus**](https://disqus.com/), [**Discourse**](https://www.discourse.org/), [**Facebook**](https://developers.facebook.com/docs/plugins/comments), [**utterances**](https://utteranc.es/), [**giscus**](https://giscus.app/) and static-based commenting via [**Staticman**](https://staticman.net/) are built into the theme. First set the comment provider you'd like to use:
|
||||||
|
Loading…
Reference in New Issue
Block a user