FEATURE: Add copy button to codeblocks (#9451)
This commit is contained in:
parent
776caa24c9
commit
6559ad0d80
|
@ -0,0 +1,157 @@
|
||||||
|
import { withPluginApi } from "discourse/lib/plugin-api";
|
||||||
|
import { cancel, later } from "@ember/runloop";
|
||||||
|
import { Promise } from "rsvp";
|
||||||
|
import { iconHTML } from "discourse-common/lib/icon-library";
|
||||||
|
|
||||||
|
// http://github.com/feross/clipboard-copy
|
||||||
|
function clipboardCopy(text) {
|
||||||
|
// Use the Async Clipboard API when available. Requires a secure browsing
|
||||||
|
// context (i.e. HTTPS)
|
||||||
|
if (navigator.clipboard) {
|
||||||
|
return navigator.clipboard.writeText(text).catch(function(err) {
|
||||||
|
throw err !== undefined
|
||||||
|
? err
|
||||||
|
: new DOMException("The request is not allowed", "NotAllowedError");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ...Otherwise, use document.execCommand() fallback
|
||||||
|
|
||||||
|
// Put the text to copy into a <span>
|
||||||
|
const span = document.createElement("span");
|
||||||
|
span.textContent = text;
|
||||||
|
|
||||||
|
// Preserve consecutive spaces and newlines
|
||||||
|
span.style.whiteSpace = "pre";
|
||||||
|
|
||||||
|
// Add the <span> to the page
|
||||||
|
document.body.appendChild(span);
|
||||||
|
|
||||||
|
// Make a selection object representing the range of text selected by the user
|
||||||
|
const selection = window.getSelection();
|
||||||
|
const range = window.document.createRange();
|
||||||
|
selection.removeAllRanges();
|
||||||
|
range.selectNode(span);
|
||||||
|
selection.addRange(range);
|
||||||
|
|
||||||
|
// Copy text to the clipboard
|
||||||
|
let success = false;
|
||||||
|
try {
|
||||||
|
success = window.document.execCommand("copy");
|
||||||
|
} catch (err) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log("error", err);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
selection.removeAllRanges();
|
||||||
|
window.document.body.removeChild(span);
|
||||||
|
|
||||||
|
return success
|
||||||
|
? Promise.resolve()
|
||||||
|
: Promise.reject(
|
||||||
|
new DOMException("The request is not allowed", "NotAllowedError")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let _copyCodeblocksClickHandlers = {};
|
||||||
|
let _fadeCopyCodeblocksRunners = {};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "copy-codeblocks",
|
||||||
|
|
||||||
|
initialize(container) {
|
||||||
|
withPluginApi("0.8.7", api => {
|
||||||
|
function _cleanUp() {
|
||||||
|
Object.values(_copyCodeblocksClickHandlers || {}).forEach(handler =>
|
||||||
|
handler.removeEventListener("click", _handleClick)
|
||||||
|
);
|
||||||
|
|
||||||
|
Object.values(_fadeCopyCodeblocksRunners || {}).forEach(runner =>
|
||||||
|
cancel(runner)
|
||||||
|
);
|
||||||
|
|
||||||
|
_copyCodeblocksClickHandlers = {};
|
||||||
|
_fadeCopyCodeblocksRunners = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
function _handleClick(event) {
|
||||||
|
if (!event.target.classList.contains("copy-cmd")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const button = event.target;
|
||||||
|
const code = button.nextSibling;
|
||||||
|
|
||||||
|
if (code) {
|
||||||
|
clipboardCopy(code.innerText.trim()).then(() => {
|
||||||
|
button.classList.add("copied");
|
||||||
|
|
||||||
|
const commandId = Ember.guidFor(button);
|
||||||
|
|
||||||
|
if (_fadeCopyCodeblocksRunners[commandId]) {
|
||||||
|
cancel(_fadeCopyCodeblocksRunners[commandId]);
|
||||||
|
delete _fadeCopyCodeblocksRunners[commandId];
|
||||||
|
}
|
||||||
|
|
||||||
|
_fadeCopyCodeblocksRunners[commandId] = later(() => {
|
||||||
|
button.classList.remove("copied");
|
||||||
|
delete _fadeCopyCodeblocksRunners[commandId];
|
||||||
|
}, 3000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function _attachCommands(postElements, helper) {
|
||||||
|
if (!helper) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const siteSettings = container.lookup("site-settings:main");
|
||||||
|
const { isIE11 } = container.lookup("capabilities:main");
|
||||||
|
if (!siteSettings.show_copy_button_on_codeblocks || isIE11) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const commands = postElements[0].querySelectorAll(
|
||||||
|
":scope > pre > code"
|
||||||
|
);
|
||||||
|
|
||||||
|
const post = helper.getModel();
|
||||||
|
|
||||||
|
if (!commands.length || !post) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const postElement = postElements[0];
|
||||||
|
|
||||||
|
commands.forEach(command => {
|
||||||
|
const button = document.createElement("button");
|
||||||
|
button.classList.add("btn", "nohighlight", "copy-cmd");
|
||||||
|
button.innerHTML = iconHTML("copy");
|
||||||
|
command.before(button);
|
||||||
|
command.parentElement.classList.add("copy-codeblocks");
|
||||||
|
});
|
||||||
|
|
||||||
|
if (_copyCodeblocksClickHandlers[post.id]) {
|
||||||
|
_copyCodeblocksClickHandlers[post.id].removeEventListener(
|
||||||
|
"click",
|
||||||
|
_handleClick
|
||||||
|
);
|
||||||
|
|
||||||
|
delete _copyCodeblocksClickHandlers[post.id];
|
||||||
|
}
|
||||||
|
|
||||||
|
_copyCodeblocksClickHandlers[post.id] = postElement;
|
||||||
|
postElement.addEventListener("click", _handleClick, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
api.decorateCooked(_attachCommands, {
|
||||||
|
onlyStream: true,
|
||||||
|
id: "copy-codeblocks"
|
||||||
|
});
|
||||||
|
|
||||||
|
api.cleanupStream(_cleanUp);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
|
@ -609,6 +609,34 @@ pre {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.copy-codeblocks {
|
||||||
|
display: block;
|
||||||
|
position: relative;
|
||||||
|
overflow: visible;
|
||||||
|
|
||||||
|
.copy-cmd {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
min-height: 0;
|
||||||
|
font-size: $font-down-2;
|
||||||
|
min-height: 0;
|
||||||
|
font-size: $font-down-2;
|
||||||
|
user-select: none;
|
||||||
|
|
||||||
|
&.copied {
|
||||||
|
.d-icon {
|
||||||
|
color: $tertiary;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.d-icon {
|
||||||
|
pointer-events: none;
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
kbd {
|
kbd {
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
box-shadow: shadow("kbd");
|
box-shadow: shadow("kbd");
|
||||||
|
|
|
@ -247,6 +247,17 @@ nav.post-controls {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pre.copy-codeblocks .copy-cmd:not(.copied) {
|
||||||
|
opacity: 0;
|
||||||
|
transition: 0.2s;
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre.copy-codeblocks:hover .copy-cmd {
|
||||||
|
opacity: 1;
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
|
||||||
.embedded-posts {
|
.embedded-posts {
|
||||||
h1,
|
h1,
|
||||||
h2,
|
h2,
|
||||||
|
|
|
@ -397,6 +397,10 @@ blockquote {
|
||||||
margin-right: 0;
|
margin-right: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pre.copy-codeblocks code {
|
||||||
|
padding-right: 2.75em;
|
||||||
|
}
|
||||||
|
|
||||||
.gap {
|
.gap {
|
||||||
padding: 0.25em 0;
|
padding: 0.25em 0;
|
||||||
}
|
}
|
||||||
|
|
|
@ -2067,6 +2067,7 @@ en:
|
||||||
warn_reviving_old_topic_age: "When someone starts replying to a topic where the last reply is older than this many days, a warning will be displayed. Disable by setting to 0."
|
warn_reviving_old_topic_age: "When someone starts replying to a topic where the last reply is older than this many days, a warning will be displayed. Disable by setting to 0."
|
||||||
autohighlight_all_code: "Force apply code highlighting to all preformatted code blocks even when they didn't explicitly specify the language."
|
autohighlight_all_code: "Force apply code highlighting to all preformatted code blocks even when they didn't explicitly specify the language."
|
||||||
highlighted_languages: "Included syntax highlighting rules. (Warning: including too many languages may impact performance) see: <a href='https://highlightjs.org/static/demo/' target='_blank'>https://highlightjs.org/static/demo</a> for a demo"
|
highlighted_languages: "Included syntax highlighting rules. (Warning: including too many languages may impact performance) see: <a href='https://highlightjs.org/static/demo/' target='_blank'>https://highlightjs.org/static/demo</a> for a demo"
|
||||||
|
show_copy_button_on_codeblocks: "Add a button to codeblocks to copy the block contents to the user's clipboard. This feature is not supported on Internet Explorer."
|
||||||
|
|
||||||
embed_any_origin: "Allow embeddable content regardless of origin. This is required for mobile apps with static HTML."
|
embed_any_origin: "Allow embeddable content regardless of origin. This is required for mobile apps with static HTML."
|
||||||
embed_topics_list: "Support HTML embedding of topics lists"
|
embed_topics_list: "Support HTML embedding of topics lists"
|
||||||
|
|
|
@ -850,6 +850,9 @@ posting:
|
||||||
type: list
|
type: list
|
||||||
client: true
|
client: true
|
||||||
list_type: compact
|
list_type: compact
|
||||||
|
show_copy_button_on_codeblocks:
|
||||||
|
client: true
|
||||||
|
default: false
|
||||||
delete_old_hidden_posts: true
|
delete_old_hidden_posts: true
|
||||||
enable_emoji:
|
enable_emoji:
|
||||||
default: true
|
default: true
|
||||||
|
|
Loading…
Reference in New Issue