forked from libero/editor-client
-
Notifications
You must be signed in to change notification settings - Fork 0
/
update-object-change.ts
118 lines (101 loc) · 4.39 KB
/
update-object-change.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
import * as deepDiff from 'deep-diff';
import { cloneDeepWith, get, set } from 'lodash';
import { EditorState, Transaction } from 'prosemirror-state';
import { Manuscript } from '../../types/manuscript';
import { Change } from './change';
import { cloneManuscript } from '../state.utils';
import { JSONObject } from '../../types/utility.types';
import { ProsemirrorChange } from './prosemirror-change';
import { BatchChange } from './batch-change';
export class UpdateObjectChange<T> extends Change {
public static createFromTwoObjects<T>(path: string, oldObject: T, newObject: T): Change {
const editorStateProps = [];
const pojoDifferences = deepDiff.diff(
oldObject,
newObject,
// if prefilter function returns true diff stops drilling down
(path: Array<string | number>, key: string | number) => {
const propPath = [...path, key].join('.');
if (get(oldObject, propPath) instanceof EditorState) {
editorStateProps.push(propPath);
return true;
}
return false;
}
);
const objectChange = new UpdateObjectChange(path, pojoDifferences);
const prosemirrorChanges = editorStateProps
.map((propPath: string) => {
const transaction = UpdateObjectChange.getEditorStatesDiff(get(oldObject, propPath), get(newObject, propPath));
if (transaction.docChanged) {
return new ProsemirrorChange(`${path}.${propPath}`, transaction);
}
return undefined;
})
.filter(Boolean);
return new BatchChange([objectChange, ...prosemirrorChanges]);
}
public static fromJSON<T>(data: JSONObject): UpdateObjectChange<T> {
const change = new UpdateObjectChange(data.path as string, (data.differences as unknown) as deepDiff.Diff<T, T>[]);
change._timestamp = data.timestamp as number;
return change;
}
private constructor(private path: string, private differences: deepDiff.Diff<T, T>[] | undefined) {
super();
}
get isEmpty(): boolean {
return !Array.isArray(this.differences) || this.differences.length === 0;
}
isPathAffected(pathPattern: RegExp): boolean {
return pathPattern.test(this.path);
}
applyChange(manuscript: Manuscript): Manuscript {
const originalSection = get(manuscript, this.path);
const updatedSection = this.differences.reduce((acc: T, diff) => {
const newAcc = this.cloneWithoutEditorState(acc);
deepDiff.applyChange(newAcc, acc, diff);
return newAcc;
}, originalSection);
return set(cloneManuscript(manuscript), this.path, updatedSection);
}
rollbackChange(manuscript: Manuscript): Manuscript {
const originalSection = get(manuscript, this.path);
const updatedSection = this.differences.reduce((acc: T, diff) => {
const newAcc = this.cloneWithoutEditorState(acc);
deepDiff.revertChange(newAcc, acc, diff);
return newAcc;
}, originalSection);
return set(cloneManuscript(manuscript), this.path, updatedSection);
}
toJSON(): JSONObject {
return {
type: 'update-object',
timestamp: this.timestamp,
path: this.path,
differences: (this.differences as unknown) as JSONObject
};
}
private cloneWithoutEditorState(object: T): T {
const cloneCustomizer = (value): EditorState | undefined => (value instanceof EditorState ? value : undefined);
return cloneDeepWith(object, cloneCustomizer);
}
private static getEditorStatesDiff(prevState: EditorState, nextState: EditorState): Transaction {
const transaction = prevState.tr;
const start = nextState.doc.content.findDiffStart(prevState.doc.content);
if (start !== null) {
let { a: endNext, b: endPrev } = nextState.doc.content.findDiffEnd(get(prevState, 'doc.content'));
// FIXME: Workaround to resolve an issue where using transaction.replace when the new state completely overwrites the old results in an empty transaction. The workaround is to detect when the start and end indices encompose the entire size of the previous state, hence we can just return a new transaction derived from the next state.
if (endPrev - start >= prevState.doc.content.size) {
return nextState.tr;
} else {
const overlap = start - Math.min(endNext, endPrev);
if (overlap > 0) {
endNext += overlap;
endPrev += overlap;
}
transaction.replace(start, endPrev, nextState.doc.slice(start, endNext));
}
}
return transaction;
}
}