ueberdosis / tiptap

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

[Bug]: Editing content removes any `\n` . #5696

Closed masudhossain closed 1 month ago

masudhossain commented 1 month ago

Affected Packages

@tiptap/react

Version(s)

^2.8.0

Bug Description

If you have content like hello \n world and you type in the editor, it will remove the \n. Including the code below will make it appear the first time in the editor properly, but after that it will remove them.

parseOptions: {
      preserveWhitespace: 'full',
    },

Browser Used

Chrome

Code Example URL

No response

Expected Behavior

To prever the \n in the content state.

Additional Context (Optional)

Here's the entire block of code:

import React , { useState, useContext, useEffect } from 'react';
import { Route, Switch, withRouter, NavLink, useHistory, useRouteMatch } from 'react-router-dom'
import axios from "axios"; 
import {UserContext} from "./UserContext.js";
import Mention from '@tiptap/extension-mention'
import { EditorContent, useEditor, ReactRenderer, BubbleMenu } from '@tiptap/react'
import FileHandler from '@tiptap-pro/extension-file-handler'
import StarterKit from '@tiptap/starter-kit'
import Link from '@tiptap/extension-link'
import Placeholder from '@tiptap/extension-placeholder'
import Image from '@tiptap/extension-image'
import TaskItem from '@tiptap/extension-task-item'
import TaskList from '@tiptap/extension-task-list'
import Underline from '@tiptap/extension-underline'
import Commands from './TipTap/commands.js'
import getSuggestionItems from "./TipTap/items.js";
import renderItems from "./TipTap/renderItems.js";
import TipTapBubbleMenu from "./TipTapBubbleMenu.js";
import Highlight from '@tiptap/extension-highlight'
import Youtube from '@tiptap/extension-youtube'
import MentionList from '../Daas/Portal/Projects/TaskComments/MentionList.jsx'
import tippy from 'tippy.js'
import Code from '@tiptap/extension-code'
import Table from '@tiptap/extension-table'
import TableCell from '@tiptap/extension-table-cell'
import TableHeader from '@tiptap/extension-table-header'
import TableRow from '@tiptap/extension-table-row'
import Gapcursor from '@tiptap/extension-gapcursor'
import Document from '@tiptap/extension-document'

const TipTap = ({comment, setComment, users, classNames}) => {
  const currentUser = useContext(UserContext);
  const history = useHistory();
  const match = useRouteMatch();
  const [isEditable, setIsEditable] = useState(currentUser != null);

  const editor = useEditor({
    onUpdate({ editor }) {
      setComment(editor.getHTML())
    },
    parseOptions: {
      preserveWhitespace: 'full',
    },
    extensions: [
      StarterKit,
      Image,
      TaskList,
      Underline,
      Code,
      Document,
      Gapcursor,
      Table.configure({
        resizable: true,
      }),
      TableRow,
      TableHeader,
      TableCell,
      Highlight.configure({ multicolor: true }),
      TaskItem.configure({
        nested: true,
      }),
      Youtube.configure({
        inline: false,
        width: 480,
        height: 320,
      }),
      Link.configure({
        openOnClick: false,
        autolink: true,
      }),
      FileHandler.configure({
        allowedMimeTypes: ['image/png', 'image/jpeg', 'image/gif', 'image/webp'],
        onDrop: (currentEditor, files, pos) => {
          files.forEach(file => {
            const fileReader = new FileReader()

            fileReader.readAsDataURL(file)
            fileReader.onload = () => {
              currentEditor.chain().insertContentAt(pos, {
                type: 'image',
                attrs: {
                  src: fileReader.result,
                },
              }).focus().run()

            }
          })
        },
        onPaste: (currentEditor, files, htmlContent) => {
          files.forEach(file => {
            if (htmlContent) {
              return false
            }

            const fileReader = new FileReader()

            fileReader.readAsDataURL(file)
            fileReader.onload = () => {
              notice("Pasting image...")
              axios.post(`/api/upload_to_digitalocean`, {
                base64: fileReader.result
              })
              .then(function(response){
                if(response.data.success){
                  currentEditor.chain().insertContentAt(currentEditor.state.selection.anchor, {
                    type: 'image',
                    attrs: {
                      src: response.data.image_link,
                      class: 'your-custom-class',
                    },
                  }).focus().run()
                } else {
                  response.data.errors.forEach((error) => {
                    notice(error);
                  });
                }
              })
              .catch(function(error){
                console.log(error)
                notice("An error occured");
                reportError(`File: Status.js.requestUrl: ${error.config.url}. StackTrace: ${error.stack}.`);
              })
              .then(function () {

              });
            }
          })
        },
      }),
      Placeholder.configure({
        placeholder: '/slash command...',
      }),
      Mention.configure({
        HTMLAttributes: {
          class: 'mention border-all background-3',
        },
        suggestion: {
          items: ({ query }) => {
            return users
              .filter((item) =>
                item.toLowerCase().startsWith(query.toLowerCase())
              )
              .slice(0, 5);
          },
          render: () => {
            let reactRenderer;
            let popup;

            return {
              onStart: (props) => {
                reactRenderer = new ReactRenderer(MentionList, {
                  props,
                  editor: props.editor
                });

                popup = tippy("body", {
                  getReferenceClientRect: props.clientRect,
                  appendTo: () => document.body,
                  content: reactRenderer.element,
                  showOnCreate: true,
                  interactive: true,
                  trigger: "manual",
                  placement: "bottom-start"
                });
              },
              onUpdate(props) {
                reactRenderer.updateProps(props);

                popup[0].setProps({
                  getReferenceClientRect: props.clientRect
                });
              },
              onKeyDown(props) {
                if (props.event.key === "Escape") {
                  popup[0].hide();

                  return true;
                }

                return reactRenderer?.ref?.onKeyDown(props);
              },
              onExit() {
                popup[0].destroy();
                reactRenderer.destroy();
              }
            };
          }
        }
      }),
      Commands.configure({
        suggestion: {
          items: getSuggestionItems,
          render: renderItems
        }
      })
    ],
    content: comment,
  });

  // Watch for changes in the comment prop and update the editor's content
  useEffect(() => {
    if (editor) {
      // editor.commands.setContent(comment || '');
      if(comment === null || comment === ""){
      // Remove all content, and trigger the `update` event
      editor.commands.clearContent(true)
      }
    }
  }, [comment]);

  useEffect(() => {
    if(editor){
      if(currentUser != null){
        editor.setEditable(true)
      } else {
        editor.setEditable(false)
      }
    }
  }, [currentUser, editor]);

  return(
    <React.Fragment>
      <EditorContent editor={editor} className={classNames || `border-all border-radius`}/>
      {editor && 
        <TipTapBubbleMenu editor={editor}/>
      }
    </React.Fragment>
  )
}

export default TipTap;

Dependency Updates

nperez0111 commented 1 month ago

I'm pretty sure what is happening here is that you need to extend the base paragraph extension and say whitespace: 'pre' to actually preserve the whitespace, otherwise, normal HTML rules apply and whitespace is removed.

In general you should be using hardbreaks, not relying on newline behaviour: https://tiptap.dev/docs/editor/extensions/nodes/hard-break

kubanzz commented 5 days ago

Have you solved the problem?