outline / rich-markdown-editor

The open source React and Prosemirror based markdown editor that powers Outline. Want to try it out? Create an account:
https://www.getoutline.com
BSD 3-Clause "New" or "Revised" License
2.87k stars 589 forks source link

fix: always copy latest value of node in CodeFence #409

Closed aulneau closed 3 years ago

aulneau commented 3 years ago

When playing around with the CodeFence node, I noticed there was a copy action, but it was always copying the initial value of the code block node from when the editor first loaded. This change copies the logic of the language toggle such that it gets the position of the node, and then finds the node in the editor view and gets the latest text content.

tommoor commented 3 years ago

lgtm, please run yarn lint --fix to pass CI

aulneau commented 3 years ago

lgtm, please run yarn lint --fix to pass CI

Done :)

BrianHung commented 3 years ago

Sorry if I'm drive-by commenting, but the duplicated logic between handleCopyToClipboard and handleLanguageChange to find the codeblock node could be removed by allowing for custom nodeviews written with vanilla javascript (in addition to React ).

That's because with custom node views, you can easily get access to the current node and its position in the constructor and in the update method.

To do that, this library would just need to relax constraints in createNodeViews and move ComponentView into the extension.component method, so individual nodes could choose to use React or plain JS.

  createNodeViews() {
    return this.extensions.extensions
      .filter((extension: Node) => extension.component)
      .reduce((nodeViews, extension: Node) => {
        const nodeView = (node, view, getPos, decorations) => {
          return extension.component({
            editor: this,
            extension,
            node,
            view,
            getPos,
            decorations,
          });
        };

        return {
          ...nodeViews,
          [extension.name]: nodeView,
        };
      }, {});
  }

Then for the codefence, you could have something like this:

export default class CodeFence extends Node {

  get schema() {
    return {
      attrs: {
        language: {
          default: "javascript",
        },
      },
      content: "text*",
      marks: "",
      group: "block",
      code: true,
      defining: true,
      draggable: false,
      parseDOM: [
        { tag: "pre", preserveWhitespace: "full" },
        {
          tag: ".code-block",
          preserveWhitespace: "full",
          contentElement: "code",
          getAttrs: (dom: HTMLDivElement) => {
            return {
              language: dom.dataset.language,
            };
          },
        },
      ],
      toDOM: node => {
        return [
          "div",
          { class: "code-block", "data-language": node.attrs.language },
          ["pre", ["code", { spellCheck: false }, 0]],
        ];
      },
    };
  }

  component(props) {
    return new CodeFenceNodeView(props);
  }

}

class CodeFenceNodeView {

  constructor({ editor, extension, node, view, getPos, decorations}) {
    this.extension = extension;
    this.node = node;
    this.view = view;
    this.getPos = getPos; 

    this.dom = document.createElement("div");

    const div = this.dom.appendChild(document.createElement("div"));

    const button = div.appendChild(document.createElement("button"));
    button.innerText = "Copy";
    button.type = "button";
    button.addEventListener("click", this.handleCopyToClipboard);

    const select = div.appendChild(document.createElement("select"));
    select.addEventListener("change", this.handleLanguageChange);

    let pre = this.dom.appendChild(document.createElement("pre"));
    this.contentDOM = pre.appendChild(document.createElement("code"));
  }

  handleCopyToClipboard = event => {
    copy(this.node.textContent);
    if (this.extension.options.onShowToast) {
      this.extension.options.onShowToast(
        this.extension.options.dictionary.codeCopied,
        ToastType.Info
      );
     }
  };

  handleLanguageChange = event => {
    const element = event.target;
    this.view.view.dispatch(this.view.state.tr.setNodeMarkup(this.getPos(), undefined, {
        language: element.value,
      }););
   }
  };

  update (node, decorations) {
    if (node.type !== this.node.type) { return false; }
    this.node = node;
    return true;
  }

}