From d5c33aef8e138760bac4ca435dcec3c7db1e88bd Mon Sep 17 00:00:00 2001 From: Tom Najdek Date: Wed, 8 Apr 2026 00:43:12 +0200 Subject: [PATCH 1/2] Expose undo/redo support --- src/index.zotero.js | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/src/index.zotero.js b/src/index.zotero.js index f588323..bb34b2c 100644 --- a/src/index.zotero.js +++ b/src/index.zotero.js @@ -4,6 +4,7 @@ import { createRoot } from 'react-dom/client'; import { addFTL, getLocalizedString } from './fluent'; import { randomString } from './core/utils'; import { schema } from './core/schema'; +import { undo, redo } from 'prosemirror-history'; import Editor from './ui/editor'; import EditorCore from './core/editor-core'; @@ -87,6 +88,26 @@ class EditorInstance { return this._editorCore.getData(onlyChanged); } + canUndo() { + if (!this._editorCore?.view) return false; + return undo(this._editorCore.view.state); + } + + canRedo() { + if (!this._editorCore?.view) return false; + return redo(this._editorCore.view.state); + } + + doUndo() { + if (!this._editorCore?.view) return; + undo(this._editorCore.view.state, this._editorCore.view.dispatch); + } + + doRedo() { + if (!this._editorCore?.view) return; + redo(this._editorCore.view.state, this._editorCore.view.dispatch); + } + _setFont(font) { let root = document.documentElement; root.style.setProperty('--font-family', font.fontFamily); @@ -619,6 +640,22 @@ window.getDataSync = (onlyChanged) => { return null; }; +window.canUndo = () => { + return currentInstance?.canUndo() ?? false; +}; + +window.canRedo = () => { + return currentInstance?.canRedo() ?? false; +}; + +window.doUndo = () => { + currentInstance?.doUndo(); +}; + +window.doRedo = () => { + currentInstance?.doRedo(); +}; + // Called from Zotero, because file picker can only be opened from user-triggered event or privileged code window.openImageFilePicker = () => { if (currentInstance) { From d15800a9375f5503f6973193a38bffd831c54c3a Mon Sep 17 00:00:00 2001 From: Martynas Bagdonas Date: Tue, 28 Jul 2026 13:26:30 +0300 Subject: [PATCH 2/2] Centralize note editor undo and redo commands --- src/core/history-commands.js | 6 +++ src/core/keymap.js | 16 ++------ src/index.zotero.js | 27 ++++++++------ test/history-commands.js | 71 ++++++++++++++++++++++++++++++++++++ 4 files changed, 97 insertions(+), 23 deletions(-) create mode 100644 src/core/history-commands.js create mode 100644 test/history-commands.js diff --git a/src/core/history-commands.js b/src/core/history-commands.js new file mode 100644 index 0000000..98d89ea --- /dev/null +++ b/src/core/history-commands.js @@ -0,0 +1,6 @@ +import { chainCommands } from 'prosemirror-commands'; +import { undo as historyUndo, redo as historyRedo } from 'prosemirror-history'; +import { undoInputRule } from 'prosemirror-inputrules'; + +export const undoCommand = chainCommands(undoInputRule, historyUndo); +export const redoCommand = historyRedo; diff --git a/src/core/keymap.js b/src/core/keymap.js index 0f064d7..cbeaf53 100644 --- a/src/core/keymap.js +++ b/src/core/keymap.js @@ -3,10 +3,10 @@ import { joinUp, joinDown, lift, newlineInCode, liftEmptyBlock, createParagraphNear } from 'prosemirror-commands'; import { wrapInList, splitListItem } from 'prosemirror-schema-list'; -import { undo, redo } from 'prosemirror-history'; import { undoInputRule } from 'prosemirror-inputrules'; import { schema } from './schema'; import { changeIndent, removeBlockIndent, customSplitBlock } from './commands'; +import { undoCommand, redoCommand } from './history-commands'; import { isMac } from './utils'; export function buildKeymap(options) { @@ -19,11 +19,11 @@ export function buildKeymap(options) { keys[key] = cmd; } - bind('Mod-z', customUndo); - bind('Shift-Mod-z', redo); + bind('Mod-z', undoCommand); + bind('Shift-Mod-z', redoCommand); bind('Backspace', undoInputRule); bind('Backspace', removeBlockIndent()); - if (!isMac()) bind('Ctrl-y', redo); + if (!isMac()) bind('Ctrl-y', redoCommand); bind('Alt-F10', focusToolbar); bind('Alt-ArrowUp', joinUp); @@ -100,14 +100,6 @@ export function buildKeymap(options) { return keys; } -function customUndo(state, dispatch) { - if(undoInputRule(state, dispatch)) { - return true; - } else { - return undo(state, dispatch); - } -} - function focusToolbar() { document.querySelector('.toolbar button').focus(); } diff --git a/src/index.zotero.js b/src/index.zotero.js index bb34b2c..5a91dcb 100644 --- a/src/index.zotero.js +++ b/src/index.zotero.js @@ -4,7 +4,7 @@ import { createRoot } from 'react-dom/client'; import { addFTL, getLocalizedString } from './fluent'; import { randomString } from './core/utils'; import { schema } from './core/schema'; -import { undo, redo } from 'prosemirror-history'; +import { undoCommand, redoCommand } from './core/history-commands'; import Editor from './ui/editor'; import EditorCore from './core/editor-core'; @@ -88,24 +88,29 @@ class EditorInstance { return this._editorCore.getData(onlyChanged); } + _getHistoryView() { + let editorCore = this._editorCore; + return editorCore?.view && !editorCore.readOnly ? editorCore.view : null; + } + canUndo() { - if (!this._editorCore?.view) return false; - return undo(this._editorCore.view.state); + let view = this._getHistoryView(); + return view ? undoCommand(view.state) : false; } canRedo() { - if (!this._editorCore?.view) return false; - return redo(this._editorCore.view.state); + let view = this._getHistoryView(); + return view ? redoCommand(view.state) : false; } doUndo() { - if (!this._editorCore?.view) return; - undo(this._editorCore.view.state, this._editorCore.view.dispatch); + let view = this._getHistoryView(); + return view ? undoCommand(view.state, view.dispatch) : false; } doRedo() { - if (!this._editorCore?.view) return; - redo(this._editorCore.view.state, this._editorCore.view.dispatch); + let view = this._getHistoryView(); + return view ? redoCommand(view.state, view.dispatch) : false; } _setFont(font) { @@ -649,11 +654,11 @@ window.canRedo = () => { }; window.doUndo = () => { - currentInstance?.doUndo(); + return currentInstance?.doUndo() ?? false; }; window.doRedo = () => { - currentInstance?.doRedo(); + return currentInstance?.doRedo() ?? false; }; // Called from Zotero, because file picker can only be opened from user-triggered event or privileged code diff --git a/test/history-commands.js b/test/history-commands.js new file mode 100644 index 0000000..2d147af --- /dev/null +++ b/test/history-commands.js @@ -0,0 +1,71 @@ +/* global describe, it */ + +import { expect } from 'chai'; +import { schema } from 'prosemirror-schema-basic'; +import { history } from 'prosemirror-history'; +import { + inputRules, + textblockTypeInputRule, +} from 'prosemirror-inputrules'; +import { EditorState } from 'prosemirror-state'; + +import { redoCommand, undoCommand } from '../src/core/history-commands.js'; + +function createState() { + let inputRulesPlugin = inputRules({ + rules: [ + textblockTypeInputRule( + /^(#{1,6}) $/, + schema.nodes.heading, + match => ({ level: match[1].length }) + ), + ], + }); + let state = EditorState.create({ + schema, + plugins: [inputRulesPlugin, history()], + }); + let dispatch = (transaction) => { + state = state.apply(transaction); + }; + + return { + get state() { + return state; + }, + dispatch, + inputRulesPlugin, + }; +} + +describe('history commands', function () { + it('should undo and redo document changes', function () { + let editor = createState(); + editor.dispatch(editor.state.tr.insertText('text', 1)); + + expect(undoCommand(editor.state)).to.equal(true); + expect(undoCommand(editor.state, editor.dispatch)).to.equal(true); + expect(editor.state.doc.textContent).to.equal(''); + expect(redoCommand(editor.state)).to.equal(true); + expect(redoCommand(editor.state, editor.dispatch)).to.equal(true); + expect(editor.state.doc.textContent).to.equal('text'); + }); + + it('should undo the latest input rule before document history', function () { + let editor = createState(); + editor.dispatch(editor.state.tr.insertText('#', 1)); + let view = { + get state() { + return editor.state; + }, + dispatch: editor.dispatch, + composing: false, + }; + + expect(editor.inputRulesPlugin.props.handleTextInput(view, 2, 2, ' ')).to.equal(true); + expect(editor.state.doc.firstChild.type).to.equal(schema.nodes.heading); + expect(undoCommand(editor.state, editor.dispatch)).to.equal(true); + expect(editor.state.doc.firstChild.type).to.equal(schema.nodes.paragraph); + expect(editor.state.doc.textContent).to.equal('# '); + }); +});