krackgen
krackgen

Reputation: 37

How to maintain line breaks for the pasted text in TipTap editor?

Even a TipTap editor playground does that - when you paste a text with line breaks, it will remove line breaks and will display text without them.

How can I make editor to retain the line breaks when pasting a text?

Edit: this is how it's done

const editor = {
  editorProps: {
    handlePaste: (view, event, slice) => {
      // Prevent the default paste behavior
      event.preventDefault();

      // Get plain text from clipboard
      const text = event.clipboardData?.getData("text/plain");

      if (text) {
        // Split text by newlines
        const lines = text.split(/\n/);

        // Insert each line with proper paragraph formatting
        const tr = view.state.tr;
        lines.forEach((line, index) => {
          // Insert line
          tr.insertText(line);

          // Add newline between paragraphs (except for last line)
          if (index < lines.length - 1) {
            tr.split(tr.selection.from);
          }
        });

        view.dispatch(tr);
      }

      return true;
    },
  },
}

Upvotes: 0

Views: 313

Answers (1)

Nex
Nex

Reputation: 137

Here is what I did to achieve consistent line breaks from copy-paste events without the tradeoffs of:

  1. Also destroying selected HTML elements such as lists
  2. Losing the ability to apply global styles

This goes in place of your solution:

const editor = {
  editorProps: {
    handlePaste: (_view, event) => {
      // Prevent the default paste behaviour
      event.preventDefault();

      // Get html text from clipboard
      const htmlContents = event.clipboardData?.getData("text/html");

      // Let the editor handle things instead
      if (!htmlContents) return false;

      const cleanedCliboardData = getCleanedHtmlClipboardValue(
                                     htmlContent,                  
                                     initalValueOfGlobalStlyeFontFamily,
                                  );

      // Let the editor handle things instead
      if (!cleanedCliboardData) return false;

      // Inserting content needs a wrapper component... might as well apply initial global styles
      const cleanedHtmlWithWrapper = `<span style="font-family: 
      ${selectedFontFamily};>${cleanedCliboardData}<span>`;

      editor?.commands.insertContent(cleanedHtmlWithWrapper);
      return true;
     },
  },
}

The actual cleanup method:

const getCleanedHtmlClipboardValue = (htmlContent, initalValueOfGlobalStlyeFontFamily) => {
    const textNodes = ["p", "span"];
    // This extends the list of allowed elements that won't be removed
    const allowedTags = [...textNodes, "br", "ul", "ol", "li"];

    try {
        const parser = new DOMParser();
        const doc = parser.parseFromString(htmlContent, "text/html");
        // ! VERY IMPORTANT !
        // This is a NodeListOf<Element>, which is a list of all elements in body that keeps track of parent-child relations
        // This means there is no need to travel the DOM via recursions
        const documentBodyElements = doc.body.querySelectorAll("*");

        documentBodyElements.forEach((element: Element) => {
            const elementTagName = element.tagName.toLowerCase();
            const isAllowedTag = allowedTags.includes(elementTagName);
            const isTextNode = textNodes.includes(elementTagName);

            // START OVERRIDE STYLES BELOW - that also removes all unwanted style
            if (isAllowedTag && isTextNode) {
                // Keep text alignment
                if (elementTagName=== "p") {
                    const paragraphTextAlignment = el.style.textAlign;
                el.setAttribute("style", `text-align: ${paragraphTextAlignment}`);
                }

                // Apply global font-family
                if (elementTagName === "span") {
                    el.setAttribute("style", `font-family: ${initalValueOfGlobalStlyeFontFamily};`);
                }
            } else if (!isAllowedTag) {
                // "Unbox elements" to remove element that are not allowed leaving their content behind
                element.replaceWith(...element.childNodes);
            }
        });
        
        return doc.body.innerHTML;
    } catch {
        console.error("Clipboard data is invalid text/html");
        return null;
    }
  }
}
  • The example above lets you to extend allowedTags so those won't be removed. For example you might want to leave links in. Then add a. But don't forget to override styles for it too.
  • The example also adds initial global styles to p and span which then can be changed interactively from a UI component by the user if needed.

Upvotes: 0

Related Questions