I am creating a copyCode plugin for QuillJs. Everything seems to be working for the plugin, however, when a space is created between the text and the code-block, you get this error:
Failed to execute 'insertBefore' on 'Node'
Here is the code:
const copyContentIntoClipboard = (rawData: string) => {
const encodedContent = encodeURIComponent(rawData);
const filteredEncodedContent = encodedContent.replace(/%EF%BB%BF/g, "");
const targetContent = decodeURIComponent(filteredEncodedContent);
const tmpHolder = document.createElement("textarea");
tmpHolder.value = targetContent;
document.body.appendChild(tmpHolder);
tmpHolder.select();
document.execCommand("copy");
document.body.removeChild(tmpHolder);
};
const CodeBlock = Quill.import("formats/code-block");
class CopyCode extends CodeBlock {
copyBadge: HTMLDivElement | null;
domNode: HTMLElement;
container: HTMLElement | null;
parent: HTMLElement;
copyHandler: EventListener;
_mountContainer() {
const container = document.createElement("div");
container.classList.add("ql-container");
if (this.domNode.nextSibling) {
this.domNode.parentElement?.insertBefore(
container,
this.domNode
);
container.appendChild(this.domNode); // <-- error starts here
this.container = container;
}
}
_dismountContainer() {
if (this.container) {
this.container.parentElement?.insertBefore(
this.domNode,
this.container.nextSibling
);
this.domNode.parentElement?.removeChild(this.container);
}
this.container = null;
}
_mountBadge() {
const copyBadge = document.createElement("div");
copyBadge.contentEditable = "false";
copyBadge.classList.add("ql-badge", "ql-badge-copy");
copyBadge.textContent = "copy";
this.domNode.parentElement?.insertBefore(
copyBadge,
this.domNode.nextSibling
);
const copyHandler = (e: MouseEvent) => {
e.stopPropagation();
e.preventDefault();
const target = e.target as HTMLElement;
const codeArea = target.previousSibling;
const copyCode = codeArea?.textContent?.trim() || '';
if (!codeArea) {
return;
}
copyBadge.textContent = "copied!";
setTimeout(function() {
copyBadge.textContent = "copy";
}, 2000);
copyContentIntoClipboard(copyCode);
};
copyBadge.addEventListener("click", copyHandler, true);
this.copyHandler = copyHandler;
this.copyBadge = copyBadge;
}
_dismountBadge() {
const badgeIsInDom = this.domNode.parentElement?.contains(this.copyBadge);
if (this.copyBadge && badgeIsInDom) {
this.copyBadge.removeEventListener("click", this.copyHandler, true);
this.copyBadge.parentElement?.removeChild(this.copyBadge);
}
this.copyBadge = null;
this.copyHandler = () => {};
}
_mount() {
this._mountContainer();
this._mountBadge();
}
insertInto(...args: any) {
super.insertInto(...args);
const allowCustomMount = !this.copyBadge && !this.container && this.parent;
if (allowCustomMount) {
this._mount();
}
}
remove() {
this._dismountBadge();
this._dismountContainer();
super.remove();
}
}
Here is the StackBlitz: https://stackblitz.com/edit/typescript-ggvuuy?file=index.html
I believe the error is caused by QuillJS believing the code-block should be a pre block instead of a div block containing a pre block. However, I don't know how to fix it...
Any ideas?
Instead of extending formats/code-block
you can use Modules to extend Quill
import hljs from "highlight.js";
import "highlight.js/styles/monokai-sublime.css";
import "./style.css";
import Quill from "quill";
hljs.configure({
languages: ["javascript", "python"]
});
const copyContentIntoClipboard = (rawData: string) => {
const encodedContent = encodeURIComponent(rawData);
const filteredEncodedContent = encodedContent.replace(/%EF%BB%BF/g, "");
const targetContent = decodeURIComponent(filteredEncodedContent);
const tmpHolder = document.createElement("textarea");
tmpHolder.value = targetContent;
document.body.appendChild(tmpHolder);
tmpHolder.select();
document.execCommand("copy");
document.body.removeChild(tmpHolder);
};
class CopyCode {
quill: any;
options: any;
container: HTMLElement;
unusedBadges: HTMLElement[] = [];
reference: { [index: string]: {
parent : HTMLElement | null,
copyBadge : HTMLElement | null
} } = {};
constructor(quill: any, options: any) {
this.quill = quill;
this.options = options;
this.container = this.quill.addContainer('ql-badge-container');
this.quill.root.parentNode.style.position = this.quill.root.parentNode.style.position || 'relative';
this.registerCodeBlock();
this.quill.on('editor-change', () => {
Object.values(this.reference).forEach((item) => {
this.addCopyBadge(item);
this.repositionCopyBadge(item);
})
});
}
registerCodeBlock = () => {
const self = this;
const CodeBlock = Quill.import("formats/code-block");
let counter = 0;
class CopyMode extends CodeBlock {
domNode: HTMLElement;
insertInto(...args: any) {
super.insertInto(...args);
const index = String(counter);
const _node = this.domNode;
_node.setAttribute('data-index', index);
counter++;
self.reference[index] = { parent : _node, copyBadge: null };
}
remove() {
const index = this.domNode.getAttribute("data-index");
if (self.reference[index] && self.reference[index]['copyBadge']) {
const copyBadge = self.reference[index]['copyBadge'];
copyBadge.style.display = 'none';
self.unusedBadges.push(copyBadge);
}
delete self.reference[index];
super.remove();
}
}
Quill.register(CopyMode, true);
}
addCopyBadge = (obj: any) => {
if (obj.copyBadge != null || obj.parent == null) {
return;
}
const index = obj.parent.getAttribute('data-index');
const copyBadge = this.unusedBadges.length ? this.unusedBadges.shift() : document.createElement("span");
copyBadge.style.display = 'block';
copyBadge.contentEditable = "false";
copyBadge.classList.add("ql-badge", "ql-badge-copy");
copyBadge.textContent = "copy";
const copyHandler = (evt: MouseEvent) => {
evt.stopPropagation();
evt.preventDefault();
const codeArea = obj.parent;
const copyText = codeArea?.textContent?.trim() || '';
if (!codeArea) {
return;
}
copyBadge.textContent = "copied!";
setTimeout(function() {
copyBadge.textContent = "copy";
}, 2000);
copyContentIntoClipboard(copyText);
};
copyBadge.addEventListener("click", copyHandler, true);
this.container.appendChild(copyBadge);
this.reference[index]['copyBadge'] = copyBadge;
}
repositionCopyBadge(obj: any) {
const parent: HTMLElement = this.quill.root.parentNode;
const specRect = obj.parent.getBoundingClientRect();
const parentRect = parent.getBoundingClientRect();
Object.assign(obj.copyBadge.style, {
right: `${specRect.left - parentRect.left - 1 + parent.scrollLeft + 4}px`,
top: `${(specRect.top - parentRect.top + parent.scrollTop) + 3}px`,
});
}
}
Quill.register("modules/copy-code", CopyCode);
const quill = new Quill("#editor", {
modules: {
syntax: true,
'copy-code': true,
toolbar: {
container: ["code-block"]
}
},
theme: "snow"
});
Here working example