htmlreactjseditortiptapprose-mirror

insertContentAt does not work when adding a html video tag


We have the following extension for Tiptap that handles a video node:

import { mergeAttributes, Node, nodeInputRule } from "@tiptap/core";

export interface VideoOptions {
  url: string;
  width: number;
  height: number;
  HTMLAttributes: Record<string, any>;
}

declare module "@tiptap/core" {
  interface Commands<ReturnType> {
    video: {
      /**
       * Set a video node
       * @param options.updateSelection set to true will select the newly inserted content
       */
      setVideo: (
        id: string,
        src: string,
        width?: number,
        height?: number,
        controls?: boolean,
        controlslist?: string,
        options?: { updateSelection: boolean }
      ) => ReturnType;
      /**
       * Toggle a video
       */
      toggleVideo: (src: string) => ReturnType;
    };
  }
}

const VIDEO_INPUT_REGEX = /!\[(.+|:?)]\((\S+)(?:(?:\s+)["'](\S+)["'])?\)/;

export const Video = Node.create({
  name: "video",
  group: "block",
  draggable: true,
  selectable: true,

  addAttributes() {
    return {
      src: {
        default: null,
        parseHTML: (el: any) => (el as HTMLSpanElement).getAttribute("src"),
        renderHTML: (attrs: any) => ({ src: attrs.src }),
      },
      controls: {
        default: true,
        renderHTML: (attrs: any) => ({ controls: attrs.controls }),
        parseHTML: (el: any) => {
          if ((el as HTMLSpanElement).getAttribute("controls")) {
            return (el as HTMLSpanElement).getAttribute("controls");
          } else if ((el as HTMLSpanElement).hasAttribute("controls")) {
            return true;
          } else {
            return false;
          }
        },
      },
      controlslist: {
        default: "",
        renderHTML: (attributes: any) => {
          return { controlslist: attributes.controlslist };
        },
        parseHTML: (element: any) => element.getAttribute("controlslist"),
      },
      documentId: {
        default: "",
        renderHTML: (attributes: any) => {
          return { "data-document-id": attributes.documentId };
        },
        parseHTML: (element: any) => element.getAttribute("data-document-id"),
      },
      videoResolution: {
        default: "404x720",
        renderHTML: (attributes: any) => {
          return { "data-video-resolution": attributes.videoResolution };
        },
        parseHTML: (element: any) =>
          element.getAttribute("data-video-resolution"),
      },
      width: {
        renderHTML: (attributes: any) => {
          return {
            width: parseInt(attributes.width),
          };
        },
        parseHTML: (element) => element.getAttribute("width"),
      },
      height: {
        renderHTML: (attributes: any) => {
          return {
            height: parseInt(attributes.height),
          };
        },
        parseHTML: (element) => element.getAttribute("height"),
      },
    };
  },

  parseHTML() {
    return [
      {
        tag: "div.video-wrapper>video,video",
        getAttrs: (el: any) => ({
          src: (el as HTMLVideoElement).getAttribute("src"),
        }),
      },
    ];
  },

  renderHTML({ HTMLAttributes }) {
    return [
      "div",
      { class: "video-wrapper" },
      ["video", mergeAttributes(HTMLAttributes)],
    ];
  },

  addCommands() {
    return {
      setVideo:
        (
          id,
          src,
          width = 640,
          height = 480,
          controls = true,
          controlslist = "nodownload",
          options
        ) =>
        ({ commands, state }) => {
          return commands.insertContentAt(
            state.selection,
            `<video 
              controls="${controls}" 
              controlslist="${controlslist}"
              src="${src}" 
              width="${width}"
              height="${height}"
              data-document-id="${id}" 
              data-video-resolution="${width}x${height}" />`,
            options
          );
        },

      toggleVideo:
        () =>
        ({ commands }) =>
          commands.toggleNode(this.name, "paragraph"),
    };
  },

  addInputRules() {
    return [
      nodeInputRule({
        find: VIDEO_INPUT_REGEX,
        type: this.type,
        getAttributes: (match) => {
          const [, , src] = match;

          return { src };
        },
      }),
    ];
  },
});

We want to insert a new video with the give entity:

if (asset.mime.includes("video")) {
          const { id, src, width, height } = getUpdatedVideo(asset);

          if (!forceInsert)
            editor.chain().focus().setVideo(String(id), src, width, height);
          else editor.commands.setVideo(String(id), src, width, height);
        }

The setCommand is executed, we checked it by congole.log, but the node its never inserted on the document, thus, the document is never updated.

We added an audio extension with a similar structure, and we execute it in the same way,

Why is it failing?


Solution

  • We were calling the function, so we always set the log, but we were not running it:

    editor
                  .chain()
                  .focus()
                  .setVideo(String(id), src, width, height)
                  .run();
    

    The run() is the instruction that executes the action.