ueberdosis / tiptap

The headless rich text editor framework for web artisans.
https://tiptap.dev
MIT License
26.74k stars 2.23k forks source link

[Documentation]: Display figcaption without HTML tags #4020

Open GodsonAddy opened 1 year ago

GodsonAddy commented 1 year ago

What’s the URL to the page you’re sending feedback for?

https://tiptap.dev/experiments/figure

What part of the documentation needs improvement?

I have gone through the documentation of the Figure and I want when a user adds an image , that image gets a default figcaption. This figcaption contains links, bold, italics, etc.

What is helpful about that part?

It's really important for the kind of project I'm building

What is hard to understand, missing or misleading?

Because I want to add a default figcaption that already has links, italics, etc, I need to add HTML tags. But adding this tags, I see the tags in the frontend which shouldn't be so. eg: <p>Figcaption from <a href="https://tiptap.dev"> @tiptap editor </a> </p> . The end result should be Figcaption from @tiptap editor

Anything to add? (optional)

This a link to the codesandbox: https://codesandbox.io/p/sandbox/polished-wildflower-98rju0

GodsonAddy commented 10 months ago

So this is how I was able to solve it

import {
  findChildrenInRange,
  mergeAttributes,
  Node as TiptapNode,
  nodeInputRule,
  Tracker,
} from "@tiptap/core";
import styles from "../../../../styles/stories.module.css";

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

export const Figure = TiptapNode.create({
  name: "figure",

  addOptions() {
    return {
      HTMLAttributes: {},
    };
  },

  group: "block",

  content: "inline*",

  draggable: true,

  isolating: false,

  selectable: true,

  addAttributes() {
    return {
      src: {
        default: null,
        parseHTML: (element) =>
          element.querySelector("img")?.getAttribute("src"),
      },

      alt: {
        default: null,
        parseHTML: (element) =>
          element.querySelector("img")?.getAttribute("alt"),
      },

      title: {
        default: null,
        parseHTML: (element) =>
          element.querySelector("img")?.getAttribute("title"),
      },
    };
  },

  parseHTML() {
    return [
      {
        tag: "figure",
        contentElement: "figcaption",
      },
    ];
  },

  renderHTML({ HTMLAttributes }) {
    return [
      "figure",
      {
        ...this.options.HTMLAttributes,
      },

      [
        "img",
        mergeAttributes(HTMLAttributes, {
          draggable: false,
          contenteditable: false,
          class: styles.my_image,
        }),
      ],
      [
        "figcaption",
        {
          "data-placeholder": "Your image caption goes here",
        },
        0,
      ],
    ];
  },
  addCommands() {
    return {
      setFigure:
        ({ caption, ...attrs }) =>
        ({ chain }) => {
          const childNodes = Array.from(caption).map((child, index) => {
            if (child.nodeType === Node.TEXT_NODE) {
              return {
                type: "text",
                text: child.textContent,
                marks: [],
              };
            } else if (child.nodeType === Node.ELEMENT_NODE) {
              const marks = [];

              if (child.href) {
                marks.push({
                  type: "link",
                  attrs: {
                    class: child.className,
                    href: child.href,
                    target: child.target,
                    rel: child.rel,
                  },
                });
              }

              return {
                type: "text",
                text: child.textContent,
                marks,
              };
            }
          });

          return (
            chain()
              .insertContent({
                type: this.name,
                attrs,
                content: caption ? childNodes : [],
              })

              .command(({ tr, commands }) => {
                const { doc, selection } = tr;
                const position = doc
                  .resolve(Math.max(selection.to - 2, 0))
                  .end();

                return commands.insertContentAt(position, {
                  type: "paragraph",
                });
              })
              .run()
          );
        },

      imageToFigure:
        () =>
        ({ tr, commands }) => {
          const { doc, selection } = tr;
          const { from, to } = selection;
          const images = findChildrenInRange(
            doc,
            { from, to },
            (node) => node.type.name === "image"
          );

          if (!images.length) {
            return false;
          }

          const tracker = new Tracker(tr);

          return commands.forEach(images, ({ node, pos }) => {
            const mapResult = tracker.map(pos);

            if (mapResult.deleted) {
              return false;
            }

            const range = {
              from: mapResult.position,
              to: mapResult.position + node.nodeSize,
            };

            return commands.insertContentAt(range, {
              type: this.name,
              attrs: {
                src: node.attrs.src,
              },
            });
          });
        },

      figureToImage:
        () =>
        ({ tr, commands }) => {
          const { doc, selection } = tr;
          const { from, to } = selection;
          const figures = findChildrenInRange(
            doc,
            { from, to },
            (node) => node.type.name === this.name
          );

          if (!figures.length) {
            return false;
          }

          const tracker = new Tracker(tr);

          return commands.forEach(figures, ({ node, pos }) => {
            const mapResult = tracker.map(pos);

            if (mapResult.deleted) {
              return false;
            }

            const range = {
              from: mapResult.position,
              to: mapResult.position + node.nodeSize,
            };

            return commands.insertContentAt(range, {
              type: "image",
              attrs: {
                src: node.attrs.src,
              },
            });
          });
        },
    };
  },

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

          return { src, alt, title };
        },
      }),
    ];
  },
});
  const InsertImage = async (url, newAlt) => {
    if (url) {
   const parsedCaption = `Photo by <a target="_blank" rel="noopener noreferrer nofollow" class=" " href="https://unsplash.com">name</a> on <a target="_blank" rel="noopener noreferrer nofollow" class="" href="https://unsplash.com">Unsplash</a>`;

     const parser = new DOMParser();
     const captionNodes = parser.parseFromString(parsedCaption, "text/html" );
      const childNodes = captionNodes.body.childNodes;
      editor
        .chain()
        .focus()
        .setFigure({ src: url, alt: newAlt, caption: childNodes })
        .run();
    }
  };