enh: proper undo/redo note editor support

This commit is contained in:
Timothy Jaeryang Baek 2025-07-11 12:00:41 +04:00
parent 3b9d86de0b
commit 3bc5485867
4 changed files with 85 additions and 9 deletions

7
package-lock.json generated
View file

@ -24,6 +24,7 @@
"@tiptap/extension-code-block-lowlight": "^2.11.9", "@tiptap/extension-code-block-lowlight": "^2.11.9",
"@tiptap/extension-floating-menu": "^2.25.0", "@tiptap/extension-floating-menu": "^2.25.0",
"@tiptap/extension-highlight": "^2.10.0", "@tiptap/extension-highlight": "^2.10.0",
"@tiptap/extension-history": "^2.25.1",
"@tiptap/extension-link": "^2.25.0", "@tiptap/extension-link": "^2.25.0",
"@tiptap/extension-placeholder": "^2.10.0", "@tiptap/extension-placeholder": "^2.10.0",
"@tiptap/extension-table": "^2.12.0", "@tiptap/extension-table": "^2.12.0",
@ -3330,9 +3331,9 @@
} }
}, },
"node_modules/@tiptap/extension-history": { "node_modules/@tiptap/extension-history": {
"version": "2.10.0", "version": "2.25.1",
"resolved": "https://registry.npmjs.org/@tiptap/extension-history/-/extension-history-2.10.0.tgz", "resolved": "https://registry.npmjs.org/@tiptap/extension-history/-/extension-history-2.25.1.tgz",
"integrity": "sha512-5aYOmxqaCnw7e7wmWqFZmkpYCxxDjEzFbgVI6WknqNwqeOizR4+YJf3aAt/lTbksLJe47XF+NBX51gOm/ZBCiw==", "integrity": "sha512-ZoxxOAObk1U8H3d+XEG0MjccJN0ViGIKEZqnLUSswmVweYPdkJG2WF2pEif9hpwJONslvLTKa+f8jwK5LEnJLQ==",
"license": "MIT", "license": "MIT",
"funding": { "funding": {
"type": "github", "type": "github",

View file

@ -68,6 +68,7 @@
"@tiptap/extension-code-block-lowlight": "^2.11.9", "@tiptap/extension-code-block-lowlight": "^2.11.9",
"@tiptap/extension-floating-menu": "^2.25.0", "@tiptap/extension-floating-menu": "^2.25.0",
"@tiptap/extension-highlight": "^2.10.0", "@tiptap/extension-highlight": "^2.10.0",
"@tiptap/extension-history": "^2.25.1",
"@tiptap/extension-link": "^2.25.0", "@tiptap/extension-link": "^2.25.0",
"@tiptap/extension-placeholder": "^2.10.0", "@tiptap/extension-placeholder": "^2.10.0",
"@tiptap/extension-table": "^2.12.0", "@tiptap/extension-table": "^2.12.0",

View file

@ -50,16 +50,20 @@
import { onMount, onDestroy, tick, getContext } from 'svelte'; import { onMount, onDestroy, tick, getContext } from 'svelte';
import { createEventDispatcher } from 'svelte'; import { createEventDispatcher } from 'svelte';
import { socket } from '$lib/stores';
const i18n = getContext('i18n'); const i18n = getContext('i18n');
const eventDispatch = createEventDispatcher(); const eventDispatch = createEventDispatcher();
import { Fragment, DOMParser } from 'prosemirror-model'; import { Fragment, DOMParser } from 'prosemirror-model';
import { EditorState, Plugin, PluginKey, TextSelection, Selection } from 'prosemirror-state'; import { EditorState, Plugin, PluginKey, TextSelection, Selection } from 'prosemirror-state';
import { receiveTransaction, sendableSteps, getVersion } from 'prosemirror-collab';
import { Step } from 'prosemirror-transform';
import { Decoration, DecorationSet } from 'prosemirror-view'; import { Decoration, DecorationSet } from 'prosemirror-view';
import { Editor } from '@tiptap/core'; import { Editor } from '@tiptap/core';
import { AIAutocompletion } from './RichTextInput/AutoCompletion.js'; import { AIAutocompletion } from './RichTextInput/AutoCompletion.js';
import History from '@tiptap/extension-history';
import Table from '@tiptap/extension-table'; import Table from '@tiptap/extension-table';
import TableRow from '@tiptap/extension-table-row'; import TableRow from '@tiptap/extension-table-row';
import TableHeader from '@tiptap/extension-table-header'; import TableHeader from '@tiptap/extension-table-header';
@ -118,6 +122,75 @@
export let largeTextAsFile = false; export let largeTextAsFile = false;
export let insertPromptAsRichText = false; export let insertPromptAsRichText = false;
let isConnected = false;
let collaborators = new Map();
let version = 0;
// Custom collaboration plugin
const collaborationPlugin = () => {
return new Plugin({
key: new PluginKey('collaboration'),
state: {
init: () => ({ version: 0 }),
apply: (tr, pluginState) => {
const newState = { ...pluginState };
if (tr.getMeta('collaboration')) {
newState.version = tr.getMeta('collaboration').version;
}
return newState;
}
},
view: () => ({
update: (view, prevState) => {
const sendable = sendableSteps(view.state);
if (sendable) {
$socket.emit('document_steps', {
document_id: documentId,
user_id: userId,
version: sendable.version,
steps: sendable.steps.map((step) => step.toJSON()),
clientID: sendable.clientID
});
}
}
})
});
};
function handleDocumentSteps(data) {
if (data.user_id !== userId && editor) {
const steps = data.steps.map((stepJSON) => Step.fromJSON(editor.schema, stepJSON));
const tr = receiveTransaction(editor.state, steps, data.clientID);
if (tr) {
editor.view.dispatch(tr);
}
}
}
function handleDocumentState(data) {
version = data.version;
if (data.content && editor) {
editor.commands.setContent(data.content);
}
isConnected = true;
}
function handleUserJoined(data) {
collaborators.set(data.user_id, {
name: data.user_name,
color: data.user_color
});
collaborators = collaborators;
}
function handleUserLeft(data) {
collaborators.delete(data.user_id);
collaborators = collaborators;
}
let floatingMenuElement = null; let floatingMenuElement = null;
let bubbleMenuElement = null; let bubbleMenuElement = null;
let element; let element;

View file

@ -772,16 +772,16 @@ Provide the enhanced notes in markdown format. Use markdown syntax for headings,
/> />
<div class="flex items-center gap-0.5 translate-x-1"> <div class="flex items-center gap-0.5 translate-x-1">
{#if note.data?.versions?.length > 0} {#if editor}
<div> <div>
<div class="flex items-center gap-0.5 self-center min-w-fit" dir="ltr"> <div class="flex items-center gap-0.5 self-center min-w-fit" dir="ltr">
<button <button
class="self-center p-1 hover:enabled:bg-black/5 dark:hover:enabled:bg-white/5 dark:hover:enabled:text-white hover:enabled:text-black rounded-md transition disabled:cursor-not-allowed disabled:text-gray-500 disabled:hover:text-gray-500" class="self-center p-1 hover:enabled:bg-black/5 dark:hover:enabled:bg-white/5 dark:hover:enabled:text-white hover:enabled:text-black rounded-md transition disabled:cursor-not-allowed disabled:text-gray-500 disabled:hover:text-gray-500"
on:click={() => { on:click={() => {
versionNavigateHandler('prev'); editor.chain().focus().undo().run();
// versionNavigateHandler('prev');
}} }}
disabled={(versionIdx === null && note.data.versions.length === 0) || disabled={!editor.can().undo()}
versionIdx === 0}
> >
<ArrowUturnLeft className="size-4" /> <ArrowUturnLeft className="size-4" />
</button> </button>
@ -789,9 +789,10 @@ Provide the enhanced notes in markdown format. Use markdown syntax for headings,
<button <button
class="self-center p-1 hover:enabled:bg-black/5 dark:hover:enabled:bg-white/5 dark:hover:enabled:text-white hover:enabled:text-black rounded-md transition disabled:cursor-not-allowed disabled:text-gray-500 disabled:hover:text-gray-500" class="self-center p-1 hover:enabled:bg-black/5 dark:hover:enabled:bg-white/5 dark:hover:enabled:text-white hover:enabled:text-black rounded-md transition disabled:cursor-not-allowed disabled:text-gray-500 disabled:hover:text-gray-500"
on:click={() => { on:click={() => {
versionNavigateHandler('next'); editor.chain().focus().redo().run();
// versionNavigateHandler('next');
}} }}
disabled={versionIdx >= note.data.versions.length || versionIdx === null} disabled={!editor.can().redo()}
> >
<ArrowUturnRight className="size-4" /> <ArrowUturnRight className="size-4" />
</button> </button>