tiptap icon indicating copy to clipboard operation
tiptap copied to clipboard

The cursor auto move to end of editor

Open tiendatleo opened this issue 2 years ago • 4 comments

What’s the bug you are facing?

I am facing an issue about the position of the cursor on production environment.

Which browser was this experienced in? Are any special extensions installed?

OS: macOS 12.3.1 Browser: Chrome Version 103.0.5060.53 (Official Build) (arm64) Dependencies: "@tiptap/core": "^2.0.0-beta.181", "@tiptap/extension-color": "^2.0.0-beta.12", "@tiptap/extension-image": "^2.0.0-beta.30", "@tiptap/extension-link": "^2.0.0-beta.35", "@tiptap/extension-table": "^2.0.0-beta.54", "@tiptap/extension-table-cell": "^2.0.0-beta.23", "@tiptap/extension-table-header": "^2.0.0-beta.25", "@tiptap/extension-table-row": "^2.0.0-beta.22", "@tiptap/extension-text-align": "^2.0.0-beta.31", "@tiptap/extension-text-style": "^2.0.0-beta.26", "@tiptap/extension-underline": "^2.0.0-beta.25", "@tiptap/starter-kit": "^2.0.0-beta.190", "@tiptap/vue-2": "^2.0.0-beta.84",

How can we reproduce the bug on our side?

After I typed a word, the cursor is moved to end of the editor. Video below.

Can you provide a CodeSandbox?

No response

What did you expect to happen?

The position of cursor is not changed

Anything to add? (optional)

https://user-images.githubusercontent.com/25786110/176377309-9f2522f1-9031-4df9-b3a5-48a1ab28dbb4.mov

Did you update your dependencies?

  • [X] Yes, I’ve updated my dependencies to use the latest version of all packages.

Are you sponsoring us?

  • [ ] Yes, I’m a sponsor. 💖

tiendatleo avatar Jun 29 '22 07:06 tiendatleo

Hi @tiendatleo, are you able to create a simplified sandbox? It's hard to tell what's going wrong without more details.

svenadlung avatar Jun 29 '22 15:06 svenadlung

Dependencies installed:

  • "@tiptap/extension-mention": "^2.0.0-beta.102"
  • "@tiptap/starter-kit": "^2.0.0-beta.190"
  • "@tiptap/vue-2": "^2.0.0-beta.84"

That happened to me as well. The bug is really hard to replicate honestly. But I attached the dependencies I've used if it should help.

What fixes the bug for me is to add in this snippet in the watchers: image

Which is the same code mentioned in the documentation as well. But I've removed it as it didn't explain why and it didn't make sense to check same value in the watcher (which watchers should trigger whenever the dependent values' change)


Also, just generally speaking (and completely unrelated to this thread) if you it helps you, I find Tiptap's Vue 2 has weird reactivity issues and I'm doing weird things like stringifying and parsing the editor doc object as a prop.

Jemaz avatar Jul 20 '22 10:07 Jemaz

The codes are very long and I can't bring it to a sandbox. Anyone facing to this issue?

tiendatleo avatar Aug 02 '22 10:08 tiendatleo

Do not use emit and setContent by watch props at the same time. 0@JZL1Q3UXK)ZYWU$WDo not use emit and setContent by watch props at the same time.
3BU setContent makes the cursor move to the end of the editor.

Cookiekira avatar Sep 19 '22 03:09 Cookiekira

Without being able to look at the code, it's difficult to provide help. Now that there were some good advices and the demo on model does not show the same behaviour, I would close the issue for now.

Feel free to let me know if the problem is still reproducible.

svenadlung avatar Dec 22 '22 10:12 svenadlung

I can reproduce this issue with TipTap 2.0.3 on Safari 16.5 when I place the editor inside a custom element with a closed shadow root. Opening the shadow root on all ancestor elements fixes the problem. I don't really have the time to research this further, but it looks like upstream WebKit bug.

jarek-foksa avatar May 25 '23 11:05 jarek-foksa

Do not use emit and setContent by watch props at the same time. 0@JZL1Q3UXK)ZYWU$WDo not use emit and setContent by watch props at the same time. 3BU setContent makes the cursor move to the end of the editor.

wow! so, how to update the editor content if we want to update editor content from parent component?

arbisyarifudin avatar Jul 18 '23 06:07 arbisyarifudin

Do not use emit and setContent by watch props at the same time. 0@JZL1Q3UXK)ZYWU$WDo not use emit and setContent by watch props at the same time. 3BU setContent makes the cursor move to the end of the editor.

wow! so, how to update the editor content if we want to update editor content from parent component?

const setContent = (content: string) => {
  editor.value?.chain().focus().setContent(JSON.parse(content)).run()
}
defineExpose({
  setContent
})

Cookiekira avatar Jul 18 '23 06:07 Cookiekira

Has anyone find a solution for this in React?

SGLara avatar Aug 30 '23 04:08 SGLara

The same is happening to me, this is my code: I'm stuck please help, there is no more info about it other that this issue reported.

NoteContext.jsx

export const NoteContext = createContext()

export const NoteProvider = ({ children }) => {
  const [noteId, setNoteId] = useState('')
  const [noteContent, setNoteContent] = useState('')
  const [debouncedNoteContent] = useDebounce(noteContent, 500)

  const editor = useEditor({
    extensions: [
      StarterKit.configure({
        codeBlock: false,
        code: false,
        hardBreak: false
      }),
      Underline,
      Typography,
      Placeholder.configure({
        placeholder: 'Add note...'
      }),
      TextAlign.configure({
        types: ['heading', 'paragraph'],
        alignments: ['left', 'right', 'center', 'justify']
      }),
      CustomCodeBlock,
      CustomCode,
      CustomHardBreak
    ],
    onUpdate: ({ editor }) => {
      setNoteContent(editor.getHTML())
    }
  })

  return (
    <NoteContext.Provider value={{
      noteId,
      setNoteId,
      editor,
      noteContent,
      setNoteContent,
      debouncedNoteContent
    }}
    >
      {children}
    </NoteContext.Provider>
  )
}

NoteContent.jsx

export default function NoteContent () {
  const [user] = useAuthState(auth)

  const {
    noteId,
    editor,
    noteContent,
    setNoteContent,
    debouncedNoteContent
  } = useContext(NoteContext)

  const navigate = useNavigate()

  const [values] = useListVals(ref(db, `note-it-db/${user.uid}/notes`))

  const existingNote = useMemo(() => {
    return values.find(note => note.id === noteId)
  }, [values, noteId])

  const createNewNote = (noteContent, user) => {
    const newNoteId = uuidv4()

    const newNoteData = {
      id: newNoteId,
      content: noteContent,
      favorite: false,
      lastModified: moment().format('DD-MM-YYYY hh:mm:ss')
    }

    set(ref(db, `note-it-db/${user.uid}/notes/${newNoteId}`), newNoteData)

    return newNoteData
  }

  const updateNote = (existingNote, noteContent, user) => {
    const updatedNoteData = {
      ...existingNote,
      content: noteContent,
      lastModified: moment().format('DD-MM-YYYY hh:mm:ss')
    }
    set(ref(db, `note-it-db/${user.uid}/notes/${existingNote.id}`), updatedNoteData)

    return updatedNoteData
  }

  useEffect(() => {
    if (editor && noteContent) {
      const notePurified = DOMPurify.sanitize(noteContent)
      editor.commands.setContent(notePurified, false, { preserveWhitespace: 'full' })
    }
  }, [editor, noteContent])

  useEffect(() => {
    if (noteId && existingNote) {
      setNoteContent(existingNote.content)
    }
  }, [noteId, existingNote, setNoteContent])

  useEffect(() => {
    if (noteId && debouncedNoteContent.trim() && existingNote) {
      updateNote(existingNote, debouncedNoteContent, user)
    }
  }, [noteId, debouncedNoteContent, existingNote, user])

  useEffect(() => {
    if (!noteId && debouncedNoteContent.trim()) {
      const newNoteId = createNewNote(debouncedNoteContent, user).id

      navigate(`/notes/${newNoteId}`)
    }
  }, [debouncedNoteContent, noteId, navigate, user])

  return (
    <Box style={{
      display: 'flex',
      flexDirection: 'column',
      justifyContent: 'space-between',
      height: '100%',
      gap: '1rem'
    }}
    >
      <EditorContent
        className='note-content'
        editor={editor}
      />
      <NoteTools editor={editor} />
    </Box>
  )
}

SGLara avatar Aug 31 '23 22:08 SGLara

I am also looking for a solution for this in React.

I was able to get something working but I'm not 100% sure it's the best way to go.

  useEffect(() => {
    if (content && editor) {
      // Save cursor position
      const { from, to } = editor.state.selection;

      // Update content
      editor.commands.setContent(content);

      // Restore cursor position
      const newFrom = Math.min(from, editor.state.doc.content.size);
      const newTo = Math.min(to, editor.state.doc.content.size);
      const textSelection = new TextSelection(
        editor.state.doc.resolve(newFrom),
        editor.state.doc.resolve(newTo)
      );
      editor.view.dispatch(editor.state.tr.setSelection(textSelection));
    }
  }, [content, editor]);

templeman15 avatar Sep 14 '23 19:09 templeman15

I faced this problem in React project. For me it was useful to set content only if (editor?.isEmpty). So I'm using setContent inside useEffect only to load initial state. Then I operate with useForm hook to set modified values.

P.S. I'm not logged in on the needed pc, so here is no code example. I'm commenting by phone.

LeonidVE avatar Oct 09 '23 09:10 LeonidVE

@templeman15 Sorry I couldn't reply back to you, I haven't been able to fix this issue, your solution worked for me, I really appreciated it, thanks so much 🙌🏻 Just wondering how you discovered this information, specially the class TextSelection I haven't found anything related to that in the documentation.

Something I can show you is that I found that this also do the trick:

const newFrom = Math.min(from, editor.state.doc.content.size)
const newTo = Math.min(to, editor.state.doc.content.size)

editor.commands.setTextSelection({ from: newFrom, to: newTo })

SGLara avatar Oct 11 '23 04:10 SGLara

@LeonidVE Thank you too, I tried this idea too and it worked, thanks

SGLara avatar Oct 11 '23 04:10 SGLara

@templeman15 Sorry I couldn't reply back to you, I haven't been able to fix this issue, your solution worked for me, I really appreciated it, thanks so much 🙌🏻 Just wondering how you discovered this information, specially the class TextSelection I haven't found anything related to that in the documentation.

@SGLara I looked at the source some and then I asked ChatGPT (4) with some of the source and it pointed it out. 😄

templeman15 avatar Oct 12 '23 15:10 templeman15

Thanks guys!!

This solution worked without changing cursors etc, my example is much simpler tho, but I did have the same issue before ;) edit: value is a prop that is passed to this TipTap component

const editor: Editor | null = useEditor({
    extensions: [StarterKit, Underline],
    content: value,
    editorProps: {
        attributes: {
            class: 'prose prose-sm lg:prose-lg w-full xl:prose-2xl focus:outline-none border bg-white border-radius-lg p-6 ',
        },
    },

    onUpdate: ({ editor: currentEditor }) => {
        onChange(currentEditor.getHTML())
    },
})

useEffect(() => {
    if (editor?.isEmpty) editor.commands.setContent(value)
}, [value, editor])

TephrosisDEV avatar Feb 07 '24 16:02 TephrosisDEV