@manuscripts/track-changes-plugin 1.10.1 → 1.10.2-LEAN-4382.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +53 -1
- package/dist/cjs/plugin.js +3 -4
- package/dist/cjs/steps/trackReplaceStep.js +2 -1
- package/dist/cjs/utils/track-utils.js +3 -1
- package/dist/es/plugin.js +3 -4
- package/dist/es/steps/trackReplaceStep.js +2 -1
- package/dist/es/utils/track-utils.js +1 -0
- package/dist/types/utils/track-utils.d.ts +2 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,58 @@
|
|
|
1
1
|
# [@manuscripts/track-changes-plugin](https://github.com/Atypon-OpenSource/manuscripts-quarterback/tree/main/quarterback-packages/track-changes-plugin)
|
|
2
2
|
|
|
3
|
-
ProseMirror plugin to track
|
|
3
|
+
ProseMirror plugin designed to track changes within a document, similar to the track changes functionality found in Google Docs or Microsoft Word. It allows for the tracking of insertions and deletions of nodes, text and node attributes, preserving information about past changes using dataTracked attributes on nodes.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
1. **Tracking of Changes:** Monitors and records insertions and deletions of block nodes, inline nodes, attributes on nodes and text within the ProseMirror editor.
|
|
8
|
+
2. **Changes management:** Allows to reject/accept a single change or a list of changes.
|
|
9
|
+
3. **Command Set:** Provides commands to enable, disable, skip, accept, reject changes. Commands are issues as transactions meta. This is the way to communicate with the plugin.
|
|
10
|
+
4. **Interpretation:** Plugin provides a ChangeSet class that helps to interpret changes in a user-friendly way.
|
|
11
|
+
|
|
12
|
+
## Core Architecture Overview
|
|
13
|
+
|
|
14
|
+
### Main design points
|
|
15
|
+
|
|
16
|
+
- Intercept transactions (insert/delete).
|
|
17
|
+
Transactions are intercepted and reverted using default plugins lifecycle, unless transaction has meta commanding to skip tracking.
|
|
18
|
+
|
|
19
|
+
- Annotate changes with metadata using node attributes and marks.
|
|
20
|
+
Before transaction is reverted the changes in each step of transaction are processed and created metadata that described the changes are stored in the dataTracked attributes. For text changes, dataTracked attributes are added to marks,
|
|
21
|
+
with which the inline change is marked on the document. For node changes the dataTracked metadata are assigned to nodes.
|
|
22
|
+
|
|
23
|
+
- ChangeSet class handles changes interpretation to create a more meaningful representation:
|
|
24
|
+
- Creates a list of top level changes out of a list of nested changed.
|
|
25
|
+
- Groups adjacent inline change of to create a single change
|
|
26
|
+
- Provides utilities to process changes, such as checking the type of a change, checking validity, flattening, etc.
|
|
27
|
+
- Changes acceptance/rejection in the document is executed via commands. Accepting a change means integrating the change into the document and discarding metadata about it.
|
|
28
|
+
Rejecting the change means reverting to the state of nodes or text or attributes before the change and discarding metadata about it.
|
|
29
|
+
- History of edits (who, what, when) is supported only in the boundaries of dataTracked attributes metadata. The plugin doesn't provide Undo/Redo capabilities but perfectly compatible with default prosemirror-history plugin.
|
|
30
|
+
|
|
31
|
+
### How it works under the hood
|
|
32
|
+
|
|
33
|
+
1. Transaction intercepted and decided upon if needs to be tracked or not. Done in appendTransaction method of the plugin. Besides explicit disabling there is a number of internal cases that disables tracking
|
|
34
|
+
2. Each type of prosemirror change step type is processed by differently. **trackTransaction** function invokes a function for each of those, such as trackReplaceStep or trackReplaceAroundStep.
|
|
35
|
+
While all of these step processing functions are a bit different, all of them result in returning an array of **ChangeStep**
|
|
36
|
+
3. **ChangeSteps** have high descriptive value and represent a specific change. This process is required because prosemirror steps are designed to efficiently capture a change in the document structure but are hard to reason about because they don't really correspond to meaningful user actions directly. There also cases when something, what we consider to be a single step of change, is represented by multiple changes. See ChangeStep type for details.
|
|
37
|
+
4. ChangeSteps are then processed by diffChangeSteps function. The function attempts to match inserted content with previously deleted content, so it can detect and consolidate edits rather than treat all changes as new inserts.
|
|
38
|
+
5. Finally, changes produced from ChangeSteps are recreated on the prosemirror document along with appropriate metadata (see **processChangeSteps** function) and a new transaction is issued to apply them. Note that some metadata are created in steps processing function.
|
|
39
|
+
6. Due to the fact that we revert changes from original transaction and then apply new changes with both old state and new state of affected node/text, the current selection on the document may be misplaced. Because of that, in some cases, we repair the selection to match its position as expected by the user.
|
|
40
|
+
|
|
41
|
+
### Basic DataTracked Attributes Model
|
|
42
|
+
|
|
43
|
+
Nodes that change are extended with dataTracked attributes:
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
{ "dataTracked": [ { "id": "uuid", "user": "anonymous", "timestamp": 123456789, "operation": "insert" | "delete" | "set_attrs" | "wrap_with_node" ... } ] }
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Requirements
|
|
50
|
+
|
|
51
|
+
Node schema needs to have { dataTracked: null } attribute declared. Otherwise the node will not be tracked.
|
|
52
|
+
|
|
53
|
+
## Best practices and caveats
|
|
54
|
+
|
|
55
|
+
Storing metadata about changes directly in the document as an attributes provides a lot of advantages (simple data model is one of them) but also has a caveat of treating metadata as data. Unless treated with care, complex changes may result in a loss of metadata during processing. Prosemirror doesn't do deepCloning of attributes between states so it would be a good practice to treat attributes with care and avoiding mutation of attributes to avoid weird behaviour.
|
|
4
56
|
|
|
5
57
|
## How to use
|
|
6
58
|
|
package/dist/cjs/plugin.js
CHANGED
|
@@ -21,6 +21,7 @@ const ChangeSet_1 = require("./ChangeSet");
|
|
|
21
21
|
const trackTransaction_1 = require("./steps/trackTransaction");
|
|
22
22
|
const track_1 = require("./types/track");
|
|
23
23
|
const logger_1 = require("./utils/logger");
|
|
24
|
+
const track_utils_1 = require("./utils/track-utils");
|
|
24
25
|
exports.trackChangesPluginKey = new prosemirror_state_1.PluginKey('track-changes');
|
|
25
26
|
const trackChangesPlugin = (opts = { userID: 'anonymous:Anonymous', initialStatus: track_1.TrackChangesStatus.enabled }) => {
|
|
26
27
|
const { userID, debug, skipTrsWithMetas = [] } = opts;
|
|
@@ -60,9 +61,7 @@ const trackChangesPlugin = (opts = { userID: 'anonymous:Anonymous', initialStatu
|
|
|
60
61
|
return Object.assign(Object.assign({}, pluginState), { changeSet: new ChangeSet_1.ChangeSet() });
|
|
61
62
|
}
|
|
62
63
|
let { changeSet } = pluginState, rest = __rest(pluginState, ["changeSet"]);
|
|
63
|
-
if ((0, actions_1.getAction)(tr, actions_1.TrackChangesAction.refreshChanges) ||
|
|
64
|
-
tr.getMeta('history$') ||
|
|
65
|
-
tr.getMeta('history$1')) {
|
|
64
|
+
if ((0, actions_1.getAction)(tr, actions_1.TrackChangesAction.refreshChanges) || (0, track_utils_1.trFromHistory)(tr)) {
|
|
66
65
|
changeSet = (0, findChanges_1.findChanges)(newState);
|
|
67
66
|
}
|
|
68
67
|
return Object.assign({ changeSet }, rest);
|
|
@@ -99,7 +98,7 @@ const trackChangesPlugin = (opts = { userID: 'anonymous:Anonymous', initialStatu
|
|
|
99
98
|
tr.docChanged &&
|
|
100
99
|
!skipMetaUsed &&
|
|
101
100
|
!skipTrackUsed &&
|
|
102
|
-
!(
|
|
101
|
+
!(0, track_utils_1.trFromHistory)(tr) &&
|
|
103
102
|
!(wasAppended && tr.getMeta('origin') === 'paragraphs')) {
|
|
104
103
|
createdTr = (0, trackTransaction_1.trackTransaction)(tr, oldState, createdTr, userID);
|
|
105
104
|
}
|
|
@@ -58,7 +58,8 @@ function trackReplaceStep(step, oldState, newTr, attrs, stepResult, currentStepD
|
|
|
58
58
|
if (backSpacedText) {
|
|
59
59
|
changeSteps.splice(changeSteps.indexOf(backSpacedText));
|
|
60
60
|
}
|
|
61
|
-
const textWasDeleted = !!changeSteps.length;
|
|
61
|
+
const textWasDeleted = !!changeSteps.length && !(fromA === fromB);
|
|
62
|
+
console.log(textWasDeleted);
|
|
62
63
|
if (!backSpacedText && newSliceContent.size > 0) {
|
|
63
64
|
logger_1.log.info('newSliceContent', newSliceContent);
|
|
64
65
|
let fragment = (0, setFragmentAsInserted_1.setFragmentAsInserted)(newSliceContent, trackUtils.createNewInsertAttrs(attrs), oldState.schema);
|
|
@@ -11,7 +11,7 @@ var __rest = (this && this.__rest) || function (s, e) {
|
|
|
11
11
|
return t;
|
|
12
12
|
};
|
|
13
13
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
|
-
exports.stepIsLift = exports.isLiftStep = exports.isWrapStep = exports.isSplitStep = exports.createNewUpdateAttrs = exports.createNewDeleteAttrs = exports.createNewReferenceAttrs = exports.createNewSplitAttrs = exports.createNewWrapAttrs = exports.createNewInsertAttrs = void 0;
|
|
14
|
+
exports.trFromHistory = exports.stepIsLift = exports.isLiftStep = exports.isWrapStep = exports.isSplitStep = exports.createNewUpdateAttrs = exports.createNewDeleteAttrs = exports.createNewReferenceAttrs = exports.createNewSplitAttrs = exports.createNewWrapAttrs = exports.createNewInsertAttrs = void 0;
|
|
15
15
|
const change_1 = require("../types/change");
|
|
16
16
|
function createNewInsertAttrs(attrs) {
|
|
17
17
|
return Object.assign(Object.assign({}, attrs), { operation: change_1.CHANGE_OPERATION.insert });
|
|
@@ -82,3 +82,5 @@ function stepIsLift(gap, node, to) {
|
|
|
82
82
|
return gap.start < gap.end && gap.insert === 0 && gap.end === to && !node.isText;
|
|
83
83
|
}
|
|
84
84
|
exports.stepIsLift = stepIsLift;
|
|
85
|
+
const trFromHistory = (tr) => Object.keys(tr.meta).find((s) => s.startsWith('history$'));
|
|
86
|
+
exports.trFromHistory = trFromHistory;
|
package/dist/es/plugin.js
CHANGED
|
@@ -18,6 +18,7 @@ import { ChangeSet } from './ChangeSet';
|
|
|
18
18
|
import { trackTransaction } from './steps/trackTransaction';
|
|
19
19
|
import { TrackChangesStatus } from './types/track';
|
|
20
20
|
import { enableDebug, log } from './utils/logger';
|
|
21
|
+
import { trFromHistory } from './utils/track-utils';
|
|
21
22
|
export const trackChangesPluginKey = new PluginKey('track-changes');
|
|
22
23
|
export const trackChangesPlugin = (opts = { userID: 'anonymous:Anonymous', initialStatus: TrackChangesStatus.enabled }) => {
|
|
23
24
|
const { userID, debug, skipTrsWithMetas = [] } = opts;
|
|
@@ -57,9 +58,7 @@ export const trackChangesPlugin = (opts = { userID: 'anonymous:Anonymous', initi
|
|
|
57
58
|
return Object.assign(Object.assign({}, pluginState), { changeSet: new ChangeSet() });
|
|
58
59
|
}
|
|
59
60
|
let { changeSet } = pluginState, rest = __rest(pluginState, ["changeSet"]);
|
|
60
|
-
if (getAction(tr, TrackChangesAction.refreshChanges) ||
|
|
61
|
-
tr.getMeta('history$') ||
|
|
62
|
-
tr.getMeta('history$1')) {
|
|
61
|
+
if (getAction(tr, TrackChangesAction.refreshChanges) || trFromHistory(tr)) {
|
|
63
62
|
changeSet = findChanges(newState);
|
|
64
63
|
}
|
|
65
64
|
return Object.assign({ changeSet }, rest);
|
|
@@ -96,7 +95,7 @@ export const trackChangesPlugin = (opts = { userID: 'anonymous:Anonymous', initi
|
|
|
96
95
|
tr.docChanged &&
|
|
97
96
|
!skipMetaUsed &&
|
|
98
97
|
!skipTrackUsed &&
|
|
99
|
-
!(tr
|
|
98
|
+
!trFromHistory(tr) &&
|
|
100
99
|
!(wasAppended && tr.getMeta('origin') === 'paragraphs')) {
|
|
101
100
|
createdTr = trackTransaction(tr, oldState, createdTr, userID);
|
|
102
101
|
}
|
|
@@ -32,7 +32,8 @@ export function trackReplaceStep(step, oldState, newTr, attrs, stepResult, curre
|
|
|
32
32
|
if (backSpacedText) {
|
|
33
33
|
changeSteps.splice(changeSteps.indexOf(backSpacedText));
|
|
34
34
|
}
|
|
35
|
-
const textWasDeleted = !!changeSteps.length;
|
|
35
|
+
const textWasDeleted = !!changeSteps.length && !(fromA === fromB);
|
|
36
|
+
console.log(textWasDeleted);
|
|
36
37
|
if (!backSpacedText && newSliceContent.size > 0) {
|
|
37
38
|
log.info('newSliceContent', newSliceContent);
|
|
38
39
|
let fragment = setFragmentAsInserted(newSliceContent, trackUtils.createNewInsertAttrs(attrs), oldState.schema);
|
|
@@ -69,3 +69,4 @@ export const isLiftStep = (step) => {
|
|
|
69
69
|
export function stepIsLift(gap, node, to) {
|
|
70
70
|
return gap.start < gap.end && gap.insert === 0 && gap.end === to && !node.isText;
|
|
71
71
|
}
|
|
72
|
+
export const trFromHistory = (tr) => Object.keys(tr.meta).find((s) => s.startsWith('history$'));
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Node as PMNode, Slice } from 'prosemirror-model';
|
|
2
|
-
import { Selection } from 'prosemirror-state';
|
|
2
|
+
import { Selection, Transaction } from 'prosemirror-state';
|
|
3
3
|
import { ReplaceAroundStep, ReplaceStep } from 'prosemirror-transform';
|
|
4
4
|
import { NewDeleteAttrs, NewEmptyAttrs, NewInsertAttrs, NewReferenceAttrs, NewSplitNodeAttrs, NewUpdateAttrs } from '../types/track';
|
|
5
5
|
export declare function createNewInsertAttrs(attrs: NewEmptyAttrs): NewInsertAttrs;
|
|
@@ -17,3 +17,4 @@ export declare function stepIsLift(gap: {
|
|
|
17
17
|
slice: Slice;
|
|
18
18
|
insert: number;
|
|
19
19
|
}, node: PMNode, to: number): boolean;
|
|
20
|
+
export declare const trFromHistory: (tr: Transaction) => string | undefined;
|
package/package.json
CHANGED