mirror of
https://github.com/laurent22/joplin.git
synced 2024-12-24 10:27:10 +02:00
Mobile: Resolves #2595: Add undo/redo support
This commit is contained in:
parent
341c9ba64b
commit
9a9cfbd130
@ -115,6 +115,7 @@ ReactNativeClient/lib/services/keychain/KeychainServiceDriver.node.js
|
||||
ReactNativeClient/lib/services/keychain/KeychainServiceDriverBase.js
|
||||
ReactNativeClient/lib/services/ResourceEditWatcher.js
|
||||
ReactNativeClient/lib/services/SettingUtils.js
|
||||
ReactNativeClient/lib/services/UndoRedoService.js
|
||||
ReactNativeClient/lib/ShareExtension.js
|
||||
ReactNativeClient/lib/shareHandler.js
|
||||
ReactNativeClient/PluginAssetsLoader.js
|
||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -105,6 +105,7 @@ ReactNativeClient/lib/services/keychain/KeychainServiceDriver.node.js
|
||||
ReactNativeClient/lib/services/keychain/KeychainServiceDriverBase.js
|
||||
ReactNativeClient/lib/services/ResourceEditWatcher.js
|
||||
ReactNativeClient/lib/services/SettingUtils.js
|
||||
ReactNativeClient/lib/services/UndoRedoService.js
|
||||
ReactNativeClient/lib/ShareExtension.js
|
||||
ReactNativeClient/lib/shareHandler.js
|
||||
ReactNativeClient/PluginAssetsLoader.js
|
||||
|
86
CliClient/tests/services_UndoRedoService.js
Normal file
86
CliClient/tests/services_UndoRedoService.js
Normal file
@ -0,0 +1,86 @@
|
||||
// /* eslint-disable no-unused-vars */
|
||||
|
||||
// require('app-module-path').addPath(__dirname);
|
||||
|
||||
// const { asyncTest, fileContentEqual, setupDatabase, checkThrow, revisionService, setupDatabaseAndSynchronizer, db, synchronizer, fileApi, sleep, clearDatabase, switchClient, syncTargetId, objectsEqual, checkThrowAsync } = require('test-utils.js');
|
||||
// const KvStore = require('lib/services/KvStore.js');
|
||||
// const UndoRedoService = require('lib/services/UndoRedoService.js').default;
|
||||
|
||||
// process.on('unhandledRejection', (reason, p) => {
|
||||
// console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
|
||||
// });
|
||||
|
||||
// jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000;
|
||||
|
||||
// describe('services_UndoRedoService', function() {
|
||||
|
||||
// beforeEach(async (done) => {
|
||||
// await setupDatabaseAndSynchronizer(1);
|
||||
// await switchClient(1);
|
||||
// done();
|
||||
// });
|
||||
|
||||
// it('should undo and redo', asyncTest(async () => {
|
||||
// const service = new UndoRedoService();
|
||||
|
||||
// expect(service.canUndo).toBe(false);
|
||||
// expect(service.canRedo).toBe(false);
|
||||
|
||||
// service.push('test');
|
||||
// expect(service.canUndo).toBe(true);
|
||||
// expect(service.canRedo).toBe(false);
|
||||
// service.push('test 2');
|
||||
// service.push('test 3');
|
||||
|
||||
// expect(service.undo()).toBe('test 3');
|
||||
// expect(service.canRedo).toBe(true);
|
||||
// expect(service.undo()).toBe('test 2');
|
||||
// expect(service.undo()).toBe('test');
|
||||
|
||||
// expect(checkThrow(() => service.undo())).toBe(true);
|
||||
|
||||
// expect(service.canUndo).toBe(false);
|
||||
// expect(service.canRedo).toBe(true);
|
||||
|
||||
// expect(service.redo()).toBe('test');
|
||||
// expect(service.canUndo).toBe(true);
|
||||
// expect(service.redo()).toBe('test 2');
|
||||
// expect(service.redo()).toBe('test 3');
|
||||
|
||||
// expect(service.canRedo).toBe(false);
|
||||
|
||||
// expect(checkThrow(() => service.redo())).toBe(true);
|
||||
// }));
|
||||
|
||||
// it('should clear the redo stack when undoing', asyncTest(async () => {
|
||||
// const service = new UndoRedoService();
|
||||
|
||||
// service.push('test');
|
||||
// service.push('test 2');
|
||||
// service.push('test 3');
|
||||
|
||||
// service.undo();
|
||||
// expect(service.canRedo).toBe(true);
|
||||
|
||||
// service.push('test 4');
|
||||
// expect(service.canRedo).toBe(false);
|
||||
|
||||
// expect(service.undo()).toBe('test 4');
|
||||
// expect(service.undo()).toBe('test 2');
|
||||
// }));
|
||||
|
||||
// it('should limit the size of the undo stack', asyncTest(async () => {
|
||||
// const service = new UndoRedoService();
|
||||
|
||||
// for (let i = 0; i < 30; i++) {
|
||||
// service.push(`test${i}`);
|
||||
// }
|
||||
|
||||
// for (let i = 0; i < 20; i++) {
|
||||
// service.undo();
|
||||
// }
|
||||
|
||||
// expect(service.canUndo).toBe(false);
|
||||
// }));
|
||||
|
||||
// });
|
@ -378,6 +378,16 @@ async function checkThrowAsync(asyncFn) {
|
||||
return hasThrown;
|
||||
}
|
||||
|
||||
function checkThrow(fn) {
|
||||
let hasThrown = false;
|
||||
try {
|
||||
fn();
|
||||
} catch (error) {
|
||||
hasThrown = true;
|
||||
}
|
||||
return hasThrown;
|
||||
}
|
||||
|
||||
function fileContentEqual(path1, path2) {
|
||||
const fs = require('fs-extra');
|
||||
const content1 = fs.readFileSync(path1, 'base64');
|
||||
@ -563,4 +573,4 @@ class TestApp extends BaseApplication {
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { kvStore, resourceService, resourceFetcher, tempFilePath, allSyncTargetItemsEncrypted, setupDatabase, revisionService, setupDatabaseAndSynchronizer, db, synchronizer, fileApi, sleep, clearDatabase, switchClient, syncTargetId, objectsEqual, checkThrowAsync, encryptionService, loadEncryptionMasterKey, fileContentEqual, decryptionWorker, asyncTest, currentClientId, id, ids, sortedIds, at, createNTestNotes, createNTestFolders, createNTestTags, TestApp };
|
||||
module.exports = { kvStore, resourceService, resourceFetcher, tempFilePath, allSyncTargetItemsEncrypted, setupDatabase, revisionService, setupDatabaseAndSynchronizer, db, synchronizer, fileApi, sleep, clearDatabase, switchClient, syncTargetId, objectsEqual, checkThrowAsync, checkThrow, encryptionService, loadEncryptionMasterKey, fileContentEqual, decryptionWorker, asyncTest, currentClientId, id, ids, sortedIds, at, createNTestNotes, createNTestFolders, createNTestTags, TestApp };
|
||||
|
@ -7,6 +7,11 @@ export interface QueueItem {
|
||||
context: any,
|
||||
}
|
||||
|
||||
export enum IntervalType {
|
||||
Debounce = 1,
|
||||
Fixed = 2,
|
||||
}
|
||||
|
||||
// The AsyncActionQueue can be used to debounce asynchronous actions, to make sure
|
||||
// they run in the right order, and also to ensure that if multiple actions are emitted
|
||||
// only the last one is executed. This is particularly useful to save data in the background.
|
||||
@ -15,12 +20,14 @@ export default class AsyncActionQueue {
|
||||
|
||||
queue_:QueueItem[] = [];
|
||||
interval_:number;
|
||||
intervalType_:number;
|
||||
scheduleProcessingIID_:any = null;
|
||||
processing_ = false;
|
||||
needProcessing_ = false;
|
||||
|
||||
constructor(interval:number = 100) {
|
||||
constructor(interval:number = 100, intervalType:IntervalType = IntervalType.Debounce) {
|
||||
this.interval_ = interval;
|
||||
this.intervalType_ = intervalType;
|
||||
}
|
||||
|
||||
push(action:QueueItemAction, context:any = null) {
|
||||
@ -39,6 +46,7 @@ export default class AsyncActionQueue {
|
||||
if (interval === null) interval = this.interval_;
|
||||
|
||||
if (this.scheduleProcessingIID_) {
|
||||
if (this.intervalType_ === IntervalType.Fixed) return;
|
||||
clearTimeout(this.scheduleProcessingIID_);
|
||||
}
|
||||
|
||||
@ -67,6 +75,16 @@ export default class AsyncActionQueue {
|
||||
this.processing_ = false;
|
||||
}
|
||||
|
||||
async reset() {
|
||||
if (this.scheduleProcessingIID_) {
|
||||
clearTimeout(this.scheduleProcessingIID_);
|
||||
this.scheduleProcessingIID_ = null;
|
||||
}
|
||||
|
||||
this.queue_ = [];
|
||||
return this.waitForAllDone();
|
||||
}
|
||||
|
||||
// Currently waitForAllDone() already finishes all the actions
|
||||
// as quickly as possible so we can make it an alias.
|
||||
async processAllNow() {
|
||||
|
@ -61,8 +61,8 @@ class ScreenHeaderComponent extends React.PureComponent {
|
||||
iconButton: {
|
||||
flex: 1,
|
||||
backgroundColor: theme.backgroundColor2,
|
||||
paddingLeft: 15,
|
||||
paddingRight: 15,
|
||||
paddingLeft: 10,
|
||||
paddingRight: 10,
|
||||
paddingTop: PADDING_V,
|
||||
paddingBottom: PADDING_V,
|
||||
},
|
||||
@ -248,6 +248,36 @@ class ScreenHeaderComponent extends React.PureComponent {
|
||||
);
|
||||
}
|
||||
|
||||
const renderTopButton = (options) => {
|
||||
if (!options.visible) return null;
|
||||
|
||||
const icon = <Icon name={options.iconName} style={this.styles().topIcon} />;
|
||||
const viewStyle = options.disabled ? this.styles().iconButtonDisabled : this.styles().iconButton;
|
||||
|
||||
return (
|
||||
<TouchableOpacity onPress={options.onPress} style={{ padding: 0 }} disabled={!!options.disabled}>
|
||||
<View style={viewStyle}>{icon}</View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
};
|
||||
|
||||
const renderUndoButton = () => {
|
||||
return renderTopButton({
|
||||
iconName: 'md-undo',
|
||||
onPress: this.props.onUndoButtonPress,
|
||||
visible: this.props.showUndoButton,
|
||||
disabled: this.props.undoButtonDisabled,
|
||||
});
|
||||
};
|
||||
|
||||
const renderRedoButton = () => {
|
||||
return renderTopButton({
|
||||
iconName: 'md-redo',
|
||||
onPress: this.props.onRedoButtonPress,
|
||||
visible: this.props.showRedoButton,
|
||||
});
|
||||
};
|
||||
|
||||
function selectAllButton(styles, onPress) {
|
||||
return (
|
||||
<TouchableOpacity onPress={onPress}>
|
||||
@ -462,6 +492,8 @@ class ScreenHeaderComponent extends React.PureComponent {
|
||||
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
|
||||
{sideMenuComp}
|
||||
{backButtonComp}
|
||||
{renderUndoButton(this.styles())}
|
||||
{renderRedoButton(this.styles())}
|
||||
{saveButton(
|
||||
this.styles(),
|
||||
() => {
|
||||
|
@ -8,6 +8,7 @@ const { uuid } = require('lib/uuid.js');
|
||||
const { MarkdownEditor } = require('../../../MarkdownEditor/index.js');
|
||||
const RNFS = require('react-native-fs');
|
||||
const Note = require('lib/models/Note.js');
|
||||
const UndoRedoService = require('lib/services/UndoRedoService.js').default;
|
||||
const BaseItem = require('lib/models/BaseItem.js');
|
||||
const Setting = require('lib/models/Setting.js');
|
||||
const Resource = require('lib/models/Resource.js');
|
||||
@ -70,9 +71,14 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
// margin. This forces RN to update the text input and to display it. Maybe that hack can be removed once RN is upgraded.
|
||||
// See https://github.com/laurent22/joplin/issues/1057
|
||||
HACK_webviewLoadingState: 0,
|
||||
};
|
||||
|
||||
this.selection = null;
|
||||
undoRedoButtonState: {
|
||||
canUndo: false,
|
||||
canRedo: false,
|
||||
},
|
||||
|
||||
selection: { start: 0, end: 0 },
|
||||
};
|
||||
|
||||
this.saveActionQueues_ = {};
|
||||
|
||||
@ -122,6 +128,8 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
mode: 'view',
|
||||
});
|
||||
|
||||
await this.undoRedoService_.reset();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -216,6 +224,39 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
this.todoCheckbox_change = this.todoCheckbox_change.bind(this);
|
||||
this.titleTextInput_contentSizeChange = this.titleTextInput_contentSizeChange.bind(this);
|
||||
this.title_changeText = this.title_changeText.bind(this);
|
||||
this.undoRedoService_stackChange = this.undoRedoService_stackChange.bind(this);
|
||||
this.screenHeader_undoButtonPress = this.screenHeader_undoButtonPress.bind(this);
|
||||
this.screenHeader_redoButtonPress = this.screenHeader_redoButtonPress.bind(this);
|
||||
this.body_selectionChange = this.body_selectionChange.bind(this);
|
||||
}
|
||||
|
||||
undoRedoService_stackChange() {
|
||||
this.setState({ undoRedoButtonState: {
|
||||
canUndo: this.undoRedoService_.canUndo,
|
||||
canRedo: this.undoRedoService_.canRedo,
|
||||
} });
|
||||
}
|
||||
|
||||
async undoRedo(type) {
|
||||
const undoState = await this.undoRedoService_[type](this.undoState());
|
||||
if (!undoState) return;
|
||||
|
||||
this.setState((state) => {
|
||||
const newNote = Object.assign({}, state.note);
|
||||
newNote.body = undoState.body;
|
||||
return {
|
||||
note: newNote,
|
||||
selection: Object.assign({}, undoState.selection),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
screenHeader_undoButtonPress() {
|
||||
this.undoRedo('undo');
|
||||
}
|
||||
|
||||
screenHeader_redoButtonPress() {
|
||||
this.undoRedo('redo');
|
||||
}
|
||||
|
||||
styles() {
|
||||
@ -307,9 +348,14 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
return shared.isModified(this);
|
||||
}
|
||||
|
||||
async UNSAFE_componentWillMount() {
|
||||
this.selection = null;
|
||||
undoState(noteBody = null) {
|
||||
return {
|
||||
body: noteBody === null ? this.state.note.body : noteBody,
|
||||
selection: Object.assign({}, this.state.selection),
|
||||
};
|
||||
}
|
||||
|
||||
async componentDidMount() {
|
||||
BackButtonService.addHandler(this.backHandler);
|
||||
NavService.addHandler(this.navHandler);
|
||||
|
||||
@ -318,6 +364,9 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
|
||||
await shared.initState(this);
|
||||
|
||||
this.undoRedoService_ = new UndoRedoService();
|
||||
this.undoRedoService_.on('stackChange', this.undoRedoService_stackChange);
|
||||
|
||||
if (this.state.note && this.state.note.body && Setting.value('sync.resourceDownloadMode') === 'auto') {
|
||||
const resourceIds = await Note.linkedResourceIds(this.state.note.body);
|
||||
await ResourceFetcher.instance().markForDownload(resourceIds);
|
||||
@ -353,6 +402,8 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
}
|
||||
|
||||
this.saveActionQueue(this.state.note.id).processAllNow();
|
||||
|
||||
this.undoRedoService_.off('stackChange', this.undoRedoService_stackChange);
|
||||
}
|
||||
|
||||
title_changeText(text) {
|
||||
@ -362,10 +413,19 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
}
|
||||
|
||||
body_changeText(text) {
|
||||
if (!this.undoRedoService_.canUndo) {
|
||||
this.undoRedoService_.push(this.undoState());
|
||||
} else {
|
||||
this.undoRedoService_.schedulePush(this.undoState());
|
||||
}
|
||||
shared.noteComponent_change(this, 'body', text);
|
||||
this.scheduleSave();
|
||||
}
|
||||
|
||||
body_selectionChange(event) {
|
||||
this.setState({ selection: event.nativeEvent.selection });
|
||||
}
|
||||
|
||||
makeSaveAction() {
|
||||
return async () => {
|
||||
return shared.saveNoteButton_press(this);
|
||||
@ -584,9 +644,9 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
|
||||
const newNote = Object.assign({}, this.state.note);
|
||||
|
||||
if (this.state.mode == 'edit' && !Setting.value('editor.beta') && !!this.selection) {
|
||||
const prefix = newNote.body.substring(0, this.selection.start);
|
||||
const suffix = newNote.body.substring(this.selection.end);
|
||||
if (this.state.mode == 'edit' && !Setting.value('editor.beta') && !!this.state.selection) {
|
||||
const prefix = newNote.body.substring(0, this.state.selection.start);
|
||||
const suffix = newNote.body.substring(this.state.selection.end);
|
||||
newNote.body = `${prefix}\n${resourceTag}\n${suffix}`;
|
||||
} else {
|
||||
newNote.body += `\n${resourceTag}`;
|
||||
@ -1022,9 +1082,8 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
multiline={true}
|
||||
value={note.body}
|
||||
onChangeText={(text) => this.body_changeText(text)}
|
||||
onSelectionChange={({ nativeEvent: { selection } }) => {
|
||||
this.selection = selection;
|
||||
}}
|
||||
selection={this.state.selection}
|
||||
onSelectionChange={this.body_selectionChange}
|
||||
blurOnSubmit={false}
|
||||
selectionColor={theme.textSelectionColor}
|
||||
keyboardAppearance={theme.keyboardAppearance}
|
||||
@ -1056,7 +1115,7 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
|
||||
// Save button is not really needed anymore with the improved save logic
|
||||
const showSaveButton = false; // this.state.mode == 'edit' || this.isModified() || this.saveButtonHasBeenShown_;
|
||||
const saveButtonDisabled = !this.isModified();
|
||||
const saveButtonDisabled = true;// !this.isModified();
|
||||
|
||||
if (showSaveButton) this.saveButtonHasBeenShown_ = true;
|
||||
|
||||
@ -1067,7 +1126,20 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
const titleComp = (
|
||||
<View style={titleContainerStyle}>
|
||||
{isTodo && <Checkbox style={this.styles().checkbox} checked={!!Number(note.todo_completed)} onChange={this.todoCheckbox_change} />}
|
||||
<TextInput onContentSizeChange={this.titleTextInput_contentSizeChange} multiline={this.enableMultilineTitle_} ref="titleTextField" underlineColorAndroid="#ffffff00" autoCapitalize="sentences" style={this.styles().titleTextInput} value={note.title} onChangeText={this.title_changeText} selectionColor={theme.textSelectionColor} keyboardAppearance={theme.keyboardAppearance} placeholder={_('Add title')} placeholderTextColor={theme.colorFaded} />
|
||||
<TextInput
|
||||
onContentSizeChange={this.titleTextInput_contentSizeChange}
|
||||
multiline={this.enableMultilineTitle_}
|
||||
ref="titleTextField"
|
||||
underlineColorAndroid="#ffffff00"
|
||||
autoCapitalize="sentences"
|
||||
style={this.styles().titleTextInput}
|
||||
value={note.title}
|
||||
onChangeText={this.title_changeText}
|
||||
selectionColor={theme.textSelectionColor}
|
||||
keyboardAppearance={theme.keyboardAppearance}
|
||||
placeholder={_('Add title')}
|
||||
placeholderTextColor={theme.colorFaded}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
|
||||
@ -1075,7 +1147,20 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
|
||||
return (
|
||||
<View style={this.rootStyle(this.props.theme).root}>
|
||||
<ScreenHeader folderPickerOptions={this.folderPickerOptions()} menuOptions={this.menuOptions()} showSaveButton={showSaveButton} saveButtonDisabled={saveButtonDisabled} onSaveButtonPress={this.saveNoteButton_press} showSideMenuButton={false} showSearchButton={false} />
|
||||
<ScreenHeader
|
||||
folderPickerOptions={this.folderPickerOptions()}
|
||||
menuOptions={this.menuOptions()}
|
||||
showSaveButton={showSaveButton}
|
||||
saveButtonDisabled={saveButtonDisabled}
|
||||
onSaveButtonPress={this.saveNoteButton_press}
|
||||
showSideMenuButton={false}
|
||||
showSearchButton={false}
|
||||
showUndoButton={this.state.undoRedoButtonState.canUndo || this.state.undoRedoButtonState.canRedo}
|
||||
showRedoButton={this.state.undoRedoButtonState.canRedo}
|
||||
undoButtonDisabled={!this.state.undoRedoButtonState.canUndo && this.state.undoRedoButtonState.canRedo}
|
||||
onUndoButtonPress={this.screenHeader_undoButtonPress}
|
||||
onRedoButtonPress={this.screenHeader_redoButtonPress}
|
||||
/>
|
||||
{titleComp}
|
||||
{bodyComponent}
|
||||
{!Setting.value('editor.beta') && actionButtonComp}
|
||||
|
103
ReactNativeClient/lib/services/UndoRedoService.ts
Normal file
103
ReactNativeClient/lib/services/UndoRedoService.ts
Normal file
@ -0,0 +1,103 @@
|
||||
import AsyncActionQueue from '../AsyncActionQueue';
|
||||
const EventEmitter = require('events');
|
||||
|
||||
class UndoQueue {
|
||||
|
||||
private inner_:any[] = [];
|
||||
private size_:number = 20;
|
||||
|
||||
pop() {
|
||||
return this.inner_.pop();
|
||||
}
|
||||
|
||||
push(e:any) {
|
||||
this.inner_.push(e);
|
||||
while (this.length > this.size_) {
|
||||
this.inner_.splice(0,1);
|
||||
}
|
||||
}
|
||||
|
||||
get length():number {
|
||||
return this.inner_.length;
|
||||
}
|
||||
|
||||
at(index:number):any {
|
||||
return this.inner_[index];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default class UndoRedoService {
|
||||
|
||||
private pushAsyncQueue:AsyncActionQueue = new AsyncActionQueue(700);
|
||||
private undoStates:UndoQueue = new UndoQueue();
|
||||
private redoStates:UndoQueue = new UndoQueue();
|
||||
private eventEmitter:any = new EventEmitter();
|
||||
private isUndoing:boolean = false;
|
||||
|
||||
constructor() {
|
||||
this.push = this.push.bind(this);
|
||||
}
|
||||
|
||||
on(eventName:string, callback:Function) {
|
||||
return this.eventEmitter.on(eventName, callback);
|
||||
}
|
||||
|
||||
off(eventName:string, callback:Function) {
|
||||
return this.eventEmitter.removeListener(eventName, callback);
|
||||
}
|
||||
|
||||
push(state:any) {
|
||||
this.undoStates.push(state);
|
||||
this.redoStates = new UndoQueue();
|
||||
this.eventEmitter.emit('stackChange');
|
||||
}
|
||||
|
||||
schedulePush(state:any) {
|
||||
this.pushAsyncQueue.push(async () => {
|
||||
this.push(state);
|
||||
});
|
||||
}
|
||||
|
||||
async undo(redoState:any) {
|
||||
if (this.isUndoing) return;
|
||||
if (!this.canUndo) throw new Error('Nothing to undo');
|
||||
this.isUndoing = true;
|
||||
await this.pushAsyncQueue.processAllNow();
|
||||
const state = this.undoStates.pop();
|
||||
this.redoStates.push(redoState);
|
||||
this.eventEmitter.emit('stackChange');
|
||||
this.isUndoing = false;
|
||||
return state;
|
||||
}
|
||||
|
||||
async redo(undoState:any) {
|
||||
if (this.isUndoing) return;
|
||||
if (!this.canRedo) throw new Error('Nothing to redo');
|
||||
this.isUndoing = true;
|
||||
await this.pushAsyncQueue.processAllNow();
|
||||
const state = this.redoStates.pop();
|
||||
this.undoStates.push(undoState);
|
||||
this.eventEmitter.emit('stackChange');
|
||||
this.isUndoing = false;
|
||||
return state;
|
||||
}
|
||||
|
||||
async reset() {
|
||||
this.undoStates = new UndoQueue();
|
||||
this.redoStates = new UndoQueue();
|
||||
this.isUndoing = false;
|
||||
const output = this.pushAsyncQueue.reset();
|
||||
this.eventEmitter.emit('stackChange');
|
||||
return output;
|
||||
}
|
||||
|
||||
get canUndo():boolean {
|
||||
return !!this.undoStates.length;
|
||||
}
|
||||
|
||||
get canRedo():boolean {
|
||||
return !!this.redoStates.length;
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue
Block a user