tiptap
tiptap copied to clipboard
The cursor auto move to end of editor
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. 💖
Hi @tiendatleo, are you able to create a simplified sandbox? It's hard to tell what's going wrong without more details.
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:
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.
The codes are very long and I can't bring it to a sandbox. Anyone facing to this issue?
Do not use emit and setContent by watch props at the same time.
setContent makes the cursor move to the end of the editor.
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.
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.
Do not use emit and setContent by watch props at the same time.
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?
Do not use emit and setContent by watch props at the same time.
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
})
Has anyone find a solution for this in React?
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>
)
}
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]);
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.
@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 })
@LeonidVE Thank you too, I tried this idea too and it worked, thanks
@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. 😄
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])