Add copy to clipboard button to code blocks (#735)
parent
1012282cd4
commit
aaad3dbf0a
|
@ -0,0 +1,125 @@
|
|||
import { Client } from "../client.ts";
|
||||
import {
|
||||
Decoration,
|
||||
DecorationSet,
|
||||
EditorView,
|
||||
Range,
|
||||
syntaxTree,
|
||||
ViewPlugin,
|
||||
ViewUpdate,
|
||||
WidgetType,
|
||||
} from "../deps.ts";
|
||||
|
||||
const ICON_SVG =
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-copy"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg>';
|
||||
|
||||
const EXCLUDE_LANGUAGES = ["template", "query", "embed"];
|
||||
|
||||
class CodeCopyWidget extends WidgetType {
|
||||
constructor(readonly value: string, readonly client: Client) {
|
||||
super();
|
||||
}
|
||||
|
||||
eq(other: CodeCopyWidget) {
|
||||
return other.value == this.value;
|
||||
}
|
||||
|
||||
toDOM() {
|
||||
const wrap = document.createElement("span");
|
||||
wrap.setAttribute("aria-hidden", "true");
|
||||
wrap.className = "sb-actions";
|
||||
|
||||
const button = wrap.appendChild(document.createElement("button"));
|
||||
button.type = "button";
|
||||
button.title = "Copy to clipboard";
|
||||
button.className = "sb-code-copy-button";
|
||||
button.innerHTML = ICON_SVG;
|
||||
button.title = "Copy";
|
||||
button.onclick = (e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
navigator.clipboard.writeText(this.value)
|
||||
.catch((err) => {
|
||||
this.client.flashNotification(
|
||||
`Error copying to clipboard: ${err}`,
|
||||
"error",
|
||||
);
|
||||
})
|
||||
.then(() => {
|
||||
this.client.flashNotification("Copied to cliboard", "info");
|
||||
});
|
||||
};
|
||||
|
||||
return wrap;
|
||||
}
|
||||
|
||||
ignoreEvent() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
function codeCopyDecoration(
|
||||
view: EditorView,
|
||||
client: Client,
|
||||
) {
|
||||
const widgets: Range<Decoration>[] = [];
|
||||
for (const { from, to } of view.visibleRanges) {
|
||||
syntaxTree(view.state).iterate({
|
||||
from,
|
||||
to,
|
||||
enter: (node) => {
|
||||
if (node.name == "FencedCode") {
|
||||
const textNode = node.node.getChild("CodeText");
|
||||
const infoNode = node.node.getChild("CodeInfo");
|
||||
|
||||
if (!textNode) {
|
||||
return;
|
||||
}
|
||||
|
||||
const language = infoNode
|
||||
? view.state.doc.sliceString(
|
||||
infoNode.from,
|
||||
infoNode.to,
|
||||
)
|
||||
: undefined;
|
||||
|
||||
if (language && EXCLUDE_LANGUAGES.includes(language)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const text = view.state.doc.sliceString(textNode.from, textNode.to);
|
||||
const deco = Decoration.widget({
|
||||
widget: new CodeCopyWidget(text, client),
|
||||
side: 0,
|
||||
});
|
||||
widgets.push(deco.range(node.from));
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
return Decoration.set(widgets);
|
||||
}
|
||||
|
||||
export const codeCopyPlugin = (client: Client) => {
|
||||
return ViewPlugin.fromClass(
|
||||
class {
|
||||
decorations: DecorationSet;
|
||||
|
||||
constructor(view: EditorView) {
|
||||
this.decorations = codeCopyDecoration(view, client);
|
||||
}
|
||||
|
||||
update(update: ViewUpdate) {
|
||||
if (
|
||||
update.docChanged || update.viewportChanged ||
|
||||
syntaxTree(update.startState) != syntaxTree(update.state)
|
||||
) {
|
||||
this.decorations = codeCopyDecoration(update.view, client);
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
decorations: (v) => v.decorations,
|
||||
},
|
||||
);
|
||||
};
|
|
@ -44,6 +44,7 @@ import { Compartment, Extension } from "@codemirror/state";
|
|||
import { extendedMarkdownLanguage } from "$common/markdown_parser/parser.ts";
|
||||
import { parseCommand } from "$common/command.ts";
|
||||
import { safeRun } from "$lib/async.ts";
|
||||
import { codeCopyPlugin } from "./cm_plugins/code_copy.ts";
|
||||
|
||||
export function createEditorState(
|
||||
client: Client,
|
||||
|
@ -105,6 +106,7 @@ export function createEditorState(
|
|||
],
|
||||
}),
|
||||
inlineImagesPlugin(client),
|
||||
codeCopyPlugin(client),
|
||||
highlightSpecialChars(),
|
||||
history(),
|
||||
drawSelection(),
|
||||
|
|
|
@ -395,6 +395,17 @@
|
|||
padding-right: 7px;
|
||||
}
|
||||
|
||||
.sb-code-copy-button {
|
||||
float: right;
|
||||
cursor: pointer;
|
||||
margin: 0 3px;
|
||||
}
|
||||
|
||||
.sb-code-copy-button > svg {
|
||||
height: 1rem;
|
||||
width: 1rem;
|
||||
}
|
||||
|
||||
.sb-line-fenced-code .sb-code {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue