Search code examples
javascripttypescriptdomcopyquill

quilljs copyCode module - Failed to execute 'insertBefore' on 'Node'


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?


Solution

  • 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