aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/client/util/UndoManager.ts121
-rw-r--r--src/client/views/Main.tsx24
-rw-r--r--src/client/views/collections/CollectionFreeFormView.tsx2
-rw-r--r--src/fields/BasicField.ts11
-rw-r--r--src/fields/Document.ts10
-rw-r--r--src/fields/ListField.ts16
6 files changed, 179 insertions, 5 deletions
diff --git a/src/client/util/UndoManager.ts b/src/client/util/UndoManager.ts
new file mode 100644
index 000000000..34910bac3
--- /dev/null
+++ b/src/client/util/UndoManager.ts
@@ -0,0 +1,121 @@
+import { observable, action } from "mobx";
+import { Opt } from "../../fields/Field";
+
+export function undoBatch(target: any, key: string | symbol, descriptor?: TypedPropertyDescriptor<any>): any {
+ let fn: (...args: any[]) => any;
+ let patchedFn: Opt<(...args: any[]) => any>;
+
+ if (descriptor) {
+ fn = descriptor.value;
+ }
+
+ return {
+ configurable: true,
+ enumerable: false,
+ get() {
+ if (!patchedFn) {
+ patchedFn = (...args: any[]) => {
+ try {
+ UndoManager.StartBatch()
+ return fn.call(this, ...args)
+ } finally {
+ UndoManager.EndBatch()
+ }
+ };
+ }
+ return patchedFn;
+ },
+ set(newFn: any) {
+ patchedFn = undefined;
+ fn = newFn;
+ }
+ }
+}
+export namespace UndoManager {
+ export interface UndoEvent {
+ undo: () => void;
+ redo: () => void;
+ }
+ type UndoBatch = UndoEvent[];
+
+ let undoStack: UndoBatch[] = observable([]);
+ let redoStack: UndoBatch[] = observable([]);
+ let currentBatch: UndoBatch | undefined;
+ let batchCounter = 0;
+ let undoing = false;
+
+ export function AddEvent(event: UndoEvent): void {
+ if (currentBatch && batchCounter && !undoing) {
+ currentBatch.push(event);
+ }
+ }
+
+ export function CanUndo(): boolean {
+ return undoStack.length > 0;
+ }
+
+ export function CanRedo(): boolean {
+ return redoStack.length > 0;
+ }
+
+ export function StartBatch(): void {
+ batchCounter++;
+ if (batchCounter > 0) {
+ currentBatch = [];
+ }
+ }
+
+ export const EndBatch = action(() => {
+ batchCounter--;
+ if (batchCounter === 0 && currentBatch && currentBatch.length) {
+ undoStack.push(currentBatch);
+ redoStack.length = 0;
+ currentBatch = undefined;
+ }
+ })
+
+ export function RunInBatch(fn: () => void) {
+ StartBatch();
+ fn();
+ EndBatch();
+ }
+
+ export const Undo = action(() => {
+ if (undoStack.length === 0) {
+ return;
+ }
+
+ let commands = undoStack.pop();
+ if (!commands) {
+ return;
+ }
+
+ undoing = true;
+ for (let i = commands.length - 1; i >= 0; i--) {
+ commands[i].undo();
+ }
+ undoing = false;
+
+ redoStack.push(commands);
+ })
+
+ export const Redo = action(() => {
+ if (redoStack.length === 0) {
+ return;
+ }
+
+ let commands = redoStack.pop();
+ if (!commands) {
+ return;
+ }
+
+ undoing = true;
+ for (let i = 0; i < commands.length; i++) {
+ commands[i].redo();
+ }
+ undoing = false;
+
+ undoStack.push(commands);
+ })
+
+} \ No newline at end of file
diff --git a/src/client/views/Main.tsx b/src/client/views/Main.tsx
index 0e770cd11..2f8337d8e 100644
--- a/src/client/views/Main.tsx
+++ b/src/client/views/Main.tsx
@@ -16,6 +16,7 @@ import { MessageStore, DocumentTransfer } from '../../server/Message';
import { Transform } from '../util/Transform';
import { CollectionDockingView } from './collections/CollectionDockingView';
import { FieldWaiting } from '../../fields/Field';
+import { UndoManager } from '../util/UndoManager';
configure({
@@ -94,6 +95,11 @@ Documents.initProtos(() => {
x: 0, y: 300, width: 200, height: 200, title: "added note"
}));
})
+ let addSchemaNode = action(() => {
+ mainfreeform.GetList<Document>(KeyStore.Data, []).push(Documents.SchemaDocument([Documents.TextDocument()], {
+ x: 0, y: 300, width: 200, height: 200, title: "added note"
+ }));
+ })
let clearDatabase = action(() => {
Utils.Emit(Server.Socket, MessageStore.DeleteAll, {});
@@ -129,10 +135,28 @@ Documents.initProtos(() => {
}} onClick={addColNode}>Add Collection</button>
<button style={{
position: 'absolute',
+ bottom: '100',
+ left: '0px',
+ width: '150px'
+ }} onClick={addSchemaNode}>Add Schema</button>
+ <button style={{
+ position: 'absolute',
bottom: '75px',
left: '0px',
width: '150px'
}} onClick={clearDatabase}>Clear Database</button>
+ <button style={{
+ position: 'absolute',
+ bottom: '25',
+ right: '0px',
+ width: '150px'
+ }} onClick={() => UndoManager.Undo()}>Undo</button>
+ <button style={{
+ position: 'absolute',
+ bottom: '0',
+ right: '0px',
+ width: '150px'
+ }} onClick={() => UndoManager.Redo()}>Redo</button>
</div>),
document.getElementById('root'));
})
diff --git a/src/client/views/collections/CollectionFreeFormView.tsx b/src/client/views/collections/CollectionFreeFormView.tsx
index 412a4d03d..3a66ebb75 100644
--- a/src/client/views/collections/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/CollectionFreeFormView.tsx
@@ -13,6 +13,7 @@ import { Documents } from "../../documents/Documents";
import { FieldWaiting } from "../../../fields/Field";
import { Transform } from "../../util/Transform";
import { DocumentView } from "../nodes/DocumentView";
+import { undoBatch } from "../../util/UndoManager";
@observer
export class CollectionFreeFormView extends CollectionViewBase {
@@ -37,6 +38,7 @@ export class CollectionFreeFormView extends CollectionViewBase {
@computed
get resizeScaling() { return this.isAnnotationOverlay ? this.props.Document.GetNumber(KeyStore.Width, 0) / this.nativeWidth : 1; }
+ @undoBatch
@action
drop = (e: Event, de: DragManager.DropEvent) => {
const doc: DocumentView = de.data["document"];
diff --git a/src/fields/BasicField.ts b/src/fields/BasicField.ts
index 8728b7145..91977b243 100644
--- a/src/fields/BasicField.ts
+++ b/src/fields/BasicField.ts
@@ -1,6 +1,7 @@
import { Field, FieldId } from "./Field"
import { observable, computed, action } from "mobx";
import { Server } from "../client/Server";
+import { UndoManager } from "../client/util/UndoManager";
export abstract class BasicField<T> extends Field {
constructor(data: T, save: boolean, id?: FieldId) {
@@ -27,9 +28,15 @@ export abstract class BasicField<T> extends Field {
}
set Data(value: T) {
- if (this.data != value) {
- this.data = value;
+ if (this.data === value) {
+ return;
}
+ let oldValue = this.data;
+ this.data = value;
+ UndoManager.AddEvent({
+ undo: () => this.Data = oldValue,
+ redo: () => this.Data = value
+ })
Server.UpdateField(this);
}
diff --git a/src/fields/Document.ts b/src/fields/Document.ts
index 0c2ad0fdb..d8522fb5b 100644
--- a/src/fields/Document.ts
+++ b/src/fields/Document.ts
@@ -7,6 +7,7 @@ import { TextField } from "./TextField";
import { ListField } from "./ListField";
import { Server } from "../client/Server";
import { Types } from "../server/Message";
+import { UndoManager } from "../client/util/UndoManager";
export class Document extends Field {
public fields: ObservableMap<string, { key: Key, field: Field }> = new ObservableMap();
@@ -127,7 +128,8 @@ export class Document extends Field {
@action
Set(key: Key, field: Field | undefined): void {
- console.log("Assign: " + key.Name + " = " + (field ? field.GetValue() : "<undefined>") + " (" + (field ? field.Id : "<undefined>") + ")");
+ let old = this.fields.get(key.Id);
+ let oldField = old ? old.field : undefined;
if (field) {
this.fields.set(key.Id, { key, field });
this._proxies.set(key.Id, field.Id)
@@ -137,6 +139,12 @@ export class Document extends Field {
this._proxies.delete(key.Id)
// Server.DeleteDocumentField(this, key);
}
+ if (oldField || field) {
+ UndoManager.AddEvent({
+ undo: () => this.Set(key, oldField),
+ redo: () => this.Set(key, field)
+ })
+ }
Server.UpdateField(this);
}
diff --git a/src/fields/ListField.ts b/src/fields/ListField.ts
index 2e192bf90..ad5374dc9 100644
--- a/src/fields/ListField.ts
+++ b/src/fields/ListField.ts
@@ -1,9 +1,10 @@
import { Field, FieldId, FieldValue, Opt } from "./Field";
import { BasicField } from "./BasicField";
import { Types } from "../server/Message";
-import { observe, action } from "mobx";
+import { observe, action, IArrayChange, IArraySplice, IObservableArray } from "mobx";
import { Server } from "../client/Server";
import { ServerUtils } from "../server/ServerUtil";
+import { UndoManager } from "../client/util/UndoManager";
export class ListField<T extends Field> extends BasicField<T[]> {
private _proxies: string[] = []
@@ -13,8 +14,19 @@ export class ListField<T extends Field> extends BasicField<T[]> {
if (save) {
Server.UpdateField(this);
}
- observe(this.Data, () => {
+ observe(this.Data as IObservableArray<T>, (change: IArrayChange<T> | IArraySplice<T>) => {
this.updateProxies()
+ if (change.type == "splice") {
+ UndoManager.AddEvent({
+ undo: () => this.Data.splice(change.index, change.addedCount, ...change.removed),
+ redo: () => this.Data.splice(change.index, change.removedCount, ...change.added)
+ })
+ } else {
+ UndoManager.AddEvent({
+ undo: () => this.Data[change.index] = change.oldValue,
+ redo: () => this.Data[change.index] = change.newValue
+ })
+ }
Server.UpdateField(this);
})
}