ueberdosis / tiptap

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

How to clear history? #491

Closed xy2z closed 4 years ago

xy2z commented 5 years ago

Is it possible to clear the history?

The problem is that I'm changing the content in the editor dynamically when user selects a new "file", but then the history from the last file will still be available. Basically, when a user changes file, and hit undo, the content from the last file will be put into the editor. So I want to clear history every time the user changes files.

I've tried recreating the component using the "hack" :key="componentKey" - but it just gave an error TypeError: Cannot read property 'matchesNode' of null

Anyone know how?

RuneHanssens commented 4 years ago

Did you find out how to clear the history?

xy2z commented 4 years ago

I ended up doing my custom history. Not ideal but I couldn't find any other way.

RuneHanssens commented 4 years ago

Just destroy your editor and create a new one seems to do the trick.

  private editor = new Editor(this.editorOptions);

  private get editorOptions() {
    return {
       ...
    };
  }

  clearHistory() {
    this.editor.destroy();
    this.editor = new Editor(this.editorOptions);
  }

And I didn't notice any stuttering by recreating the editor (yet)

philippkuehn commented 4 years ago

There is not such a feature in prosemirror-history. Related: https://discuss.prosemirror.net/t/reset-history-plugin-state/1883

Feel free to create a PR for prosemirror.

OrkhanAlikhanov commented 4 years ago

Sent a simple PR for this https://github.com/ProseMirror/prosemirror-history/pull/5

marijnh commented 4 years ago

When loading a new file, you should create a fresh ProseMirror state. I don't intend to merge that PR until someone comes up with a more compelling use case.

OrkhanAlikhanov commented 4 years ago

@marijnh Thanks for the quick response. That makes sense. Actually, I added it according to your suggestion here https://discuss.prosemirror.net/t/reset-history-plugin-state/1883

marijnh commented 4 years ago

Ah right, seems I did suggest that last year. But there's other weird effects you'll get when trying to start a new document without starting a new state (decorations might stick around, other plugins might still drag around irrelevant state), so unless someone has a more solid reason for needing this, I think providing it just steers people in the wrong direction.

OrkhanAlikhanov commented 4 years ago

As per the suggestion of the author we should set brand new state when loading new document. I am doing it in this way if anyone needs:

this.editor.options.content = jsonDocOrHTML
this.editor.view.updateState(this.editor.createState())
brechtm commented 4 years ago

It would be good to encapsulate this functionality, preferably also handling the collaboration plugin state (#691). How about Editor.loadDocument(doc, version)?

mattersj commented 4 years ago
this.editor.options.content = jsonDocOrHTML
this.editor.view.updateState(this.editor.createState())

So updating current editor state is not the best solution in my case. It breaks menu positioning so menu.left and menu.bottom always return 0. I believe that I found the right-way solution:

const { doc, tr } = this.editor.state;
const document = this.editor.createDocument(yourHTMLOrJSONContentHere);
const selection = TextSelection.create(doc, 0, doc.content.size);
const transaction = tr
  .setSelection(selection)
  .replaceSelectionWith(document, false)
  .setMeta('preventUpdate', true) // true by default but you can set it to false
  .setMeta('addToHistory', false); // Finally we prevent pushing content to the history

this.editor.view.dispatch(transaction);

TextSelection should be imported from 'tiptap' as well.

Note: this code only prevents pushing new content to the history stack but DOES NOT clear the entire history. It can be useful when you need to replace content after fetching it from API.

longlongago2 commented 2 years ago
this.editor.options.content = jsonDocOrHTML
this.editor.view.updateState(this.editor.createState())

So updating current editor state is not the best solution in my case. It breaks menu positioning so menu.left and menu.bottom always return 0. I believe that I found the right-way solution:

const { doc, tr } = this.editor.state;
const document = this.editor.createDocument(yourHTMLOrJSONContentHere);
const selection = TextSelection.create(doc, 0, doc.content.size);
const transaction = tr
  .setSelection(selection)
  .replaceSelectionWith(document, false)
  .setMeta('preventUpdate', true) // true by default but you can set it to false
  .setMeta('addToHistory', false); // Finally we prevent pushing content to the history

this.editor.view.dispatch(transaction);

TextSelection should be imported from 'tiptap' as well.

Note: this code only prevents pushing new content to the history stack but DOES NOT clear the entire history. It can be useful when you need to replace content after fetching it from API.

We can create an extension like this:

import { Extension } from '@tiptap/core';

export default Extension.create({
  name: 'loaddoc',
  addCommands() {
    return {
      loadContent:
        (content) => ({ tr, dispatch, commands }) => {
          commands.setContent(content, false, { preserveWhitespace: 'full' });
          if (dispatch) {
            tr.setMeta('addToHistory', false); // Finally we prevent pushing content to the history
          }
          return true;
        },
    };
  },
});
puopg commented 2 years ago

Is it possible to do this like this in the current version of tiptap?:

@marijnh Curious if you see any issues with this?

reset = (editor: Editor) => {
    // Create a new document using this method, since it's what createDocument does, but that utility is not accessible.
    const doc = editor.schema.nodeFromJSON({
      type: "doc",
      content: [{ type: "paragraph" }],
    });

    // Create a new editor state, and pass the existing plugins on the current editor into the new state
    const newEditorState = EditorState.create({
      doc,
      selection: undefined,
      plugins: editor.extensionManager.plugins,
    });

   editor.view.updateState(newEditorState);
  };
Jettonn commented 2 years ago

I don't know if this is the best approach but after fetching content from api and using editor.setContent({ type: 'doc', content }) to set content to editor. I cleared the history using this trick: editor.state.history$.prevRanges = null; editor.state.history$.done.eventCount = 0

bassem-mf commented 1 year ago

The previous answers did not work for me because of missing types and/or members. The following code worked for me and I could not see any side effects in my application.

import { Editor } from "@tiptap/core";
import { EditorState } from 'prosemirror-state';

function resetEditorContent(editor: Editor, newContent: string) {
    editor.commands.setContent(newContent);

    // The following code clears the history. Hopefully without side effects.
    const newEditorState = EditorState.create({
        doc: editor.state.doc,
        plugins: editor.state.plugins,
        schema: editor.state.schema
    });
    editor.view.updateState(newEditorState);
}
samyarkd commented 1 year ago

The previous answers did not work for me because of missing types and/or members. The following code worked for me and I could not see any side effects in my application.

import { Editor } from "@tiptap/core";
import { EditorState } from 'prosemirror-state';

function resetEditorContent(editor: Editor, newContent: string) {
    editor.commands.setContent(newContent);

    // The following code clears the history. Hopefully without side effects.
    const newEditorState = EditorState.create({
        doc: editor.state.doc,
        plugins: editor.state.plugins,
        schema: editor.state.schema
    });
    editor.view.updateState(newEditorState);
}

Thank you this one worked great and clean.

floydnant commented 1 year ago

Previous two answers work great, though I'm setting the document directly in the state (saves one transaction I guess):

import { Editor, createDocument } from '@tiptap/core'
import { EditorState } from 'prosemirror-state'

function resetEditorState(editor: Editor, content: string) {
    const newState = EditorState.create({
        doc: createDocument(content, editor.schema),
        schema: editor.schema,
        plugins: editor.state.plugins,
    })
    editor.view.updateState(newState)
}
stevobm commented 1 year ago

The previous answers did not work for me because of missing types and/or members. The following code worked for me and I could not see any side effects in my application.

import { Editor } from "@tiptap/core";
import { EditorState } from 'prosemirror-state';

function resetEditorContent(editor: Editor, newContent: string) {
    editor.commands.setContent(newContent);

    // The following code clears the history. Hopefully without side effects.
    const newEditorState = EditorState.create({
        doc: editor.state.doc,
        plugins: editor.state.plugins,
        schema: editor.state.schema
    });
    editor.view.updateState(newEditorState);
}

I tried this, but when I clicked the content of editor, the cursor sometimes would jump to the beginning of the line... A little problem though.

milimyname commented 1 year ago

The previous answers did not work for me because of missing types and/or members. The following code worked for me and I could not see any side effects in my application.

import { Editor } from "@tiptap/core";
import { EditorState } from 'prosemirror-state';

function resetEditorContent(editor: Editor, newContent: string) {
    editor.commands.setContent(newContent);

    // The following code clears the history. Hopefully without side effects.
    const newEditorState = EditorState.create({
        doc: editor.state.doc,
        plugins: editor.state.plugins,
        schema: editor.state.schema
    });
    editor.view.updateState(newEditorState);
}

I tried this, but when I clicked the content of editor, the cursor sometimes would jump to the beginning of the line... A little problem though.

Here is the solution. I hope it helps for future seekers.


    function resetEditorContent() {
        // Capture the current selection
        const currentSelection = editor.state.selection;

        // Reset the content
        editor.commands.setContent(editor.getJSON());

        // Create a new editor state while preserving the old selection
        const newEditorState = EditorStatePrsomirror.create({
            doc: editor.state.doc,
            plugins: editor.state.plugins,
            selection: currentSelection
        });

        // Update the editor state
        editor.view.updateState(newEditorState);
    }
Amar-Gill commented 7 months ago

The previous answers did not work for me because of missing types and/or members. The following code worked for me and I could not see any side effects in my application.

import { Editor } from "@tiptap/core";
import { EditorState } from 'prosemirror-state';

function resetEditorContent(editor: Editor, newContent: string) {
    editor.commands.setContent(newContent);

    // The following code clears the history. Hopefully without side effects.
    const newEditorState = EditorState.create({
        doc: editor.state.doc,
        plugins: editor.state.plugins,
        schema: editor.state.schema
    });
    editor.view.updateState(newEditorState);
}

I tried this, but when I clicked the content of editor, the cursor sometimes would jump to the beginning of the line... A little problem though.

Here is the solution. I hope it helps for future seekers.


  function resetEditorContent() {
      // Capture the current selection
      const currentSelection = editor.state.selection;

      // Reset the content
      editor.commands.setContent(editor.getJSON());

      // Create a new editor state while preserving the old selection
      const newEditorState = EditorStatePrsomirror.create({
          doc: editor.state.doc,
          plugins: editor.state.plugins,
          selection: currentSelection
      });

      // Update the editor state
      editor.view.updateState(newEditorState);
  }

This solution unfortunately did not work for me, and I am unable to figure out why. I am loading new content into existing editor instance after fetching the content from backend. I am using the vue-3 package although not sure if that matters.

But I did find an alternative solution that works for me. It's involves unregistering the history plugin, and re-registering it with a new instance of the plugin:

import { history } from '@tiptap/pm/history';

const editor = useEditor({ ...});

await useFetch('/content', {
  onResponse(ctx) {
    if (ctx.response.ok) {
      editor.value.commands.setContent(ctx.response._data.content, false); // <-- data from backend

      // unregister and re-register history plugin to clear data when fetching new content
      editor.value?.unregisterPlugin('history');
      editor.value?.registerPlugin(history());

      editor.value.commands.focus('end');
    }
  },
});
rajatkulkarni95 commented 2 months ago

The previous answers did not work for me because of missing types and/or members. The following code worked for me and I could not see any side effects in my application.

import { Editor } from "@tiptap/core";
import { EditorState } from 'prosemirror-state';

function resetEditorContent(editor: Editor, newContent: string) {
    editor.commands.setContent(newContent);

    // The following code clears the history. Hopefully without side effects.
    const newEditorState = EditorState.create({
        doc: editor.state.doc,
        plugins: editor.state.plugins,
        schema: editor.state.schema
    });
    editor.view.updateState(newEditorState);
}

I tried this, but when I clicked the content of editor, the cursor sometimes would jump to the beginning of the line... A little problem though.

Here is the solution. I hope it helps for future seekers.


    function resetEditorContent() {
        // Capture the current selection
        const currentSelection = editor.state.selection;

        // Reset the content
        editor.commands.setContent(editor.getJSON());

        // Create a new editor state while preserving the old selection
        const newEditorState = EditorStatePrsomirror.create({
            doc: editor.state.doc,
            plugins: editor.state.plugins,
            selection: currentSelection
        });

        // Update the editor state
        editor.view.updateState(newEditorState);
    }

This solution unfortunately did not work for me, and I am unable to figure out why. I am loading new content into existing editor instance after fetching the content from backend. I am using the vue-3 package although not sure if that matters.

But I did find an alternative solution that works for me. It's involves unregistering the history plugin, and re-registering it with a new instance of the plugin:

import { history } from '@tiptap/pm/history';

const editor = useEditor({ ...});

await useFetch('/content', {
  onResponse(ctx) {
    if (ctx.response.ok) {
      editor.value.commands.setContent(ctx.response._data.content, false); // <-- data from backend

      // unregister and re-register history plugin to clear data when fetching new content
      editor.value?.unregisterPlugin('history');
      editor.value?.registerPlugin(history());

      editor.value.commands.focus('end');
    }
  },
});

I do this --

  editor.commands.setContent(newContent, false);

  editor?.unregisterPlugin("history");
  editor?.registerPlugin(history());
};

but I get a RangeError: Adding different instances of a keyed plugin (history$) intermittently. With the older new EditorState I get a TypeError: null is not an object (evaluating 'this.docView.matchesNode')

Any idea?

chrisdellisantirn commented 1 day ago

As stated by mattersj you can set an "addToHistory" metadata property of false on a transaction to prevent it from being rolled back by undo like so:

editor.chain().setMeta('addToHistory', false).insertContent(text).run();

Do note that setContent() replaces the entire content of the editor and resets the history stack. This means all previous undo/redo history is lost.

ProseMirror History TipTap setMeta() TipTap Chain Commands TipTap Content Commands