gdpr audit implemented, email log, vollmachten, pdf delete cancel data privacy and vollmachten, removed message no id card in engergy car, and other contracts that are not telecom contracts, added insert counter for engery
This commit is contained in:
+8
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"libs": ["browser"],
|
||||
"plugins": {
|
||||
"node": {},
|
||||
"complete_strings": {},
|
||||
"es_modules": {}
|
||||
}
|
||||
}
|
||||
+170
@@ -0,0 +1,170 @@
|
||||
## 1.5.0 (2025-11-13)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
Don't call undo/redo commands in response to beforeinput events when the view isn't editable. Add isHistoryTransaction
|
||||
|
||||
### New features
|
||||
|
||||
The new `isHistoryTransaction` predicate can be used to recognize transactions generated by the history system.
|
||||
|
||||
## 1.4.1 (2024-07-10)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
Fix an issue where mark steps could cause the history to treat otherwise adjacent changes as non-adjacent, and start superfluous new undo events.
|
||||
|
||||
## 1.4.0 (2024-03-21)
|
||||
|
||||
### New features
|
||||
|
||||
Add `undoNoScroll`/`redoNoScroll` commands.
|
||||
|
||||
## 1.3.2 (2023-05-17)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
Include CommonJS type declarations in the package to please new TypeScript resolution settings.
|
||||
|
||||
## 1.3.1 (2023-04-26)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
Fix an issue where, if a composition moved the selection, it might get split across multiple undo events.
|
||||
|
||||
## 1.3.0 (2022-05-30)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
Fix an issue where the default behavior of history beforeInput events could still go through when the undo history is empty.
|
||||
|
||||
### New features
|
||||
|
||||
Include TypeScript type declarations.
|
||||
|
||||
## 1.2.0 (2021-08-18)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
Fix a bug that caused the `beforeInput` handler to not actually invoke the history commands properly.
|
||||
|
||||
### New features
|
||||
|
||||
The history plugin now listens for `beforeInput` events with `historyUndo` or `historyRedo` type, and applies the undo/redo commands when they occur.
|
||||
|
||||
## 1.1.3 (2019-12-10)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
Fixes a regression where appeneded transactions were combined into the previous history event even if that had been explicitly closed.
|
||||
|
||||
## 1.1.2 (2019-11-20)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
Rename ES module files to use a .js extension, since Webpack gets confused by .mjs
|
||||
|
||||
## 1.1.1 (2019-11-19)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
The file referred to in the package's `module` field now is compiled down to ES5.
|
||||
|
||||
## 1.1.0 (2019-11-08)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
Prevents appended transactions from starting a new event group, even if they were created a while after the previous transaction.
|
||||
|
||||
### New features
|
||||
|
||||
Add a `module` field to package json file.
|
||||
|
||||
## 1.0.4 (2019-02-19)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
Fix a bug that corrupted selection data in the history when applying remote steps in some cases.
|
||||
|
||||
## 1.0.3 (2018-10-08)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
Appending a transaction to an undo transaction will no longer immediately clear the redo history.
|
||||
|
||||
When handling appended transactions, the history will keep the last step in the original transaction, not the one from the appended transaction, for testing whether a subsequent transaction is adjacent to the previous one.
|
||||
|
||||
## 1.0.2 (2018-03-13)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
Fix a bug that could corrupt the history when rebasing changes because of collaborative editing.
|
||||
|
||||
## 1.0.1 (2018-03-05)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
Fix crash that could occur (in specific circumstance) when redoing.
|
||||
|
||||
## 0.24.0 (2017-09-25)
|
||||
|
||||
### New features
|
||||
|
||||
It is no longer necessary to manually enable the `preserveItems` option to the history plugin when using collaborative editing. (This behavior is now automatically enabled when necessary.)
|
||||
|
||||
## 0.20.0 (2017-04-03)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
Appended transactions no longer generate undo history events.
|
||||
|
||||
## 0.19.0 (2017-03-16)
|
||||
|
||||
### New features
|
||||
|
||||
A new function [`closeHistory`](https://prosemirror.net/docs/ref/version/0.19.0.html#history.closeHistory) can be used to force separation of history events at the start of a given transaction.
|
||||
|
||||
## 0.18.0 (2017-02-24)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
Fix a problem where simultaneous collaborative editing could break the undo history.
|
||||
|
||||
## 0.17.1 (2017-02-02)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
Fix issue where collaborative editing corner cases could corrupt the history.
|
||||
|
||||
## 0.12.1 (2016-11-01)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
Fix crash in undo or redo commands when the history is empty.
|
||||
|
||||
## 0.12.0 (2016-10-21)
|
||||
|
||||
### Breaking changes
|
||||
|
||||
The [`history`](https://prosemirror.net/docs/ref/version/0.12.0.html#history.history) export is now a function
|
||||
that creates a history plugin, rather than a plugin instance.
|
||||
|
||||
### New features
|
||||
|
||||
Add a
|
||||
[`newGroupDelay`](https://prosemirror.net/docs/ref/version/0.12.0.html#history.history^config.newGroupDelay) plugin
|
||||
option. This brings back the behavior where pausing between edits will
|
||||
automatically cause the history to put subsequent changes in a new
|
||||
undo event.
|
||||
|
||||
## 0.11.0 (2016-09-21)
|
||||
|
||||
### Breaking changes
|
||||
|
||||
Moved into a separate module. Now acts as a plugin that can be omitted
|
||||
or replaced by a different implementation if desired.
|
||||
|
||||
Merging subsequent changes into a single undo 'event' is now done by
|
||||
proximity in the document (the changes must touch) rather than in
|
||||
time. This will probably have to be further refined.
|
||||
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
# How to contribute
|
||||
|
||||
- [Getting help](#getting-help)
|
||||
- [Submitting bug reports](#submitting-bug-reports)
|
||||
- [Contributing code](#contributing-code)
|
||||
|
||||
## Getting help
|
||||
|
||||
Community discussion, questions, and informal bug reporting is done on the
|
||||
[discuss.ProseMirror forum](http://discuss.prosemirror.net).
|
||||
|
||||
## Submitting bug reports
|
||||
|
||||
Report bugs on the
|
||||
[GitHub issue tracker](http://github.com/prosemirror/prosemirror/issues).
|
||||
Before reporting a bug, please read these pointers.
|
||||
|
||||
- The issue tracker is for *bugs*, not requests for help. Questions
|
||||
should be asked on the [forum](http://discuss.prosemirror.net).
|
||||
|
||||
- Include information about the version of the code that exhibits the
|
||||
problem. For browser-related issues, include the browser and browser
|
||||
version on which the problem occurred.
|
||||
|
||||
- Mention very precisely what went wrong. "X is broken" is not a good
|
||||
bug report. What did you expect to happen? What happened instead?
|
||||
Describe the exact steps a maintainer has to take to make the
|
||||
problem occur. A screencast can be useful, but is no substitute for
|
||||
a textual description.
|
||||
|
||||
- A great way to make it easy to reproduce your problem, if it can not
|
||||
be trivially reproduced on the website demos, is to submit a script
|
||||
that triggers the issue.
|
||||
|
||||
## Contributing code
|
||||
|
||||
If you want to make a change that involves a significant overhaul of
|
||||
the code or introduces a user-visible new feature, create an
|
||||
[RFC](https://github.com/ProseMirror/rfcs/) first with your proposal.
|
||||
|
||||
- Make sure you have a [GitHub Account](https://github.com/signup/free)
|
||||
|
||||
- Fork the relevant repository
|
||||
([how to fork a repo](https://help.github.com/articles/fork-a-repo))
|
||||
|
||||
- Create a local checkout of the code. You can use the
|
||||
[main repository](https://github.com/prosemirror/prosemirror) to
|
||||
easily check out all core modules.
|
||||
|
||||
- Make your changes, and commit them
|
||||
|
||||
- Follow the code style of the rest of the project (see below). Run
|
||||
`npm run lint` (in the main repository checkout) to make sure that
|
||||
the linter is happy.
|
||||
|
||||
- If your changes are easy to test or likely to regress, add tests in
|
||||
the relevant `test/` directory. Either put them in an existing
|
||||
`test-*.js` file, if they fit there, or add a new file.
|
||||
|
||||
- Make sure all tests pass. Run `npm run test` to verify tests pass
|
||||
(you will need Node.js v6+).
|
||||
|
||||
- Submit a pull request ([how to create a pull request](https://help.github.com/articles/fork-a-repo)).
|
||||
Don't put more than one feature/fix in a single pull request.
|
||||
|
||||
By contributing code to ProseMirror you
|
||||
|
||||
- Agree to license the contributed code under the project's [MIT
|
||||
license](https://github.com/ProseMirror/prosemirror/blob/master/LICENSE).
|
||||
|
||||
- Confirm that you have the right to contribute and license the code
|
||||
in question. (Either you hold all rights on the code, or the rights
|
||||
holder has explicitly granted the right to use it like this,
|
||||
through a compatible open source license or through a direct
|
||||
agreement with you.)
|
||||
|
||||
### Coding standards
|
||||
|
||||
- ES6 syntax, targeting an ES5 runtime (i.e. don't use library
|
||||
elements added by ES6, don't use ES7/ES.next syntax).
|
||||
|
||||
- 2 spaces per indentation level, no tabs.
|
||||
|
||||
- No semicolons except when necessary.
|
||||
|
||||
- Follow the surrounding code when it comes to spacing, brace
|
||||
placement, etc.
|
||||
|
||||
- Brace-less single-statement bodies are encouraged (whenever they
|
||||
don't impact readability).
|
||||
|
||||
- [getdocs](https://github.com/marijnh/getdocs)-style doc comments
|
||||
above items that are part of the public API.
|
||||
|
||||
- When documenting non-public items, you can put the type after a
|
||||
single colon, so that getdocs doesn't pick it up and add it to the
|
||||
API reference.
|
||||
|
||||
- The linter (`npm run lint`) complains about unused variables and
|
||||
functions. Prefix their names with an underscore to muffle it.
|
||||
|
||||
- ProseMirror does *not* follow JSHint or JSLint prescribed style.
|
||||
Patches that try to 'fix' code to pass one of these linters will not
|
||||
be accepted.
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
Copyright (C) 2015-2017 by Marijn Haverbeke <marijn@haverbeke.berlin> and others
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
# prosemirror-history
|
||||
|
||||
[ [**WEBSITE**](https://prosemirror.net) | [**ISSUES**](https://github.com/prosemirror/prosemirror/issues) | [**FORUM**](https://discuss.prosemirror.net) | [**CHANGELOG**](https://github.com/ProseMirror/prosemirror-history/blob/master/CHANGELOG.md) ]
|
||||
|
||||
This is a [core module](https://prosemirror.net/docs/ref/#history) of [ProseMirror](https://prosemirror.net).
|
||||
ProseMirror is a well-behaved rich semantic content editor based on
|
||||
contentEditable, with support for collaborative editing and custom
|
||||
document schemas.
|
||||
|
||||
This [module](https://prosemirror.net/docs/ref/#history) implements an
|
||||
undo/redo history plugin for ProseMirror.
|
||||
|
||||
The [project page](https://prosemirror.net) has more information, a
|
||||
number of [examples](https://prosemirror.net/examples/) and the
|
||||
[documentation](https://prosemirror.net/docs/).
|
||||
|
||||
This code is released under an
|
||||
[MIT license](https://github.com/prosemirror/prosemirror/tree/master/LICENSE).
|
||||
There's a [forum](http://discuss.prosemirror.net) for general
|
||||
discussion and support requests, and the
|
||||
[Github bug tracker](https://github.com/prosemirror/prosemirror/issues)
|
||||
is the place to report issues.
|
||||
|
||||
We aim to be an inclusive, welcoming community. To make that explicit,
|
||||
we have a [code of
|
||||
conduct](http://contributor-covenant.org/version/1/1/0/) that applies
|
||||
to communication around the project.
|
||||
+382
@@ -0,0 +1,382 @@
|
||||
'use strict';
|
||||
|
||||
function _typeof(o) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof(o); }
|
||||
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
|
||||
function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, _toPropertyKey(descriptor.key), descriptor); } }
|
||||
function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); Object.defineProperty(Constructor, "prototype", { writable: false }); return Constructor; }
|
||||
function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return _typeof(key) === "symbol" ? key : String(key); }
|
||||
function _toPrimitive(input, hint) { if (_typeof(input) !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (_typeof(res) !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); }
|
||||
var RopeSequence = require('rope-sequence');
|
||||
var prosemirrorTransform = require('prosemirror-transform');
|
||||
var prosemirrorState = require('prosemirror-state');
|
||||
var max_empty_items = 500;
|
||||
var Branch = function () {
|
||||
function Branch(items, eventCount) {
|
||||
_classCallCheck(this, Branch);
|
||||
this.items = items;
|
||||
this.eventCount = eventCount;
|
||||
}
|
||||
_createClass(Branch, [{
|
||||
key: "popEvent",
|
||||
value: function popEvent(state, preserveItems) {
|
||||
var _this = this;
|
||||
if (this.eventCount == 0) return null;
|
||||
var end = this.items.length;
|
||||
for (;; end--) {
|
||||
var next = this.items.get(end - 1);
|
||||
if (next.selection) {
|
||||
--end;
|
||||
break;
|
||||
}
|
||||
}
|
||||
var remap, mapFrom;
|
||||
if (preserveItems) {
|
||||
remap = this.remapping(end, this.items.length);
|
||||
mapFrom = remap.maps.length;
|
||||
}
|
||||
var transform = state.tr;
|
||||
var selection, remaining;
|
||||
var addAfter = [],
|
||||
addBefore = [];
|
||||
this.items.forEach(function (item, i) {
|
||||
if (!item.step) {
|
||||
if (!remap) {
|
||||
remap = _this.remapping(end, i + 1);
|
||||
mapFrom = remap.maps.length;
|
||||
}
|
||||
mapFrom--;
|
||||
addBefore.push(item);
|
||||
return;
|
||||
}
|
||||
if (remap) {
|
||||
addBefore.push(new Item(item.map));
|
||||
var step = item.step.map(remap.slice(mapFrom)),
|
||||
map;
|
||||
if (step && transform.maybeStep(step).doc) {
|
||||
map = transform.mapping.maps[transform.mapping.maps.length - 1];
|
||||
addAfter.push(new Item(map, undefined, undefined, addAfter.length + addBefore.length));
|
||||
}
|
||||
mapFrom--;
|
||||
if (map) remap.appendMap(map, mapFrom);
|
||||
} else {
|
||||
transform.maybeStep(item.step);
|
||||
}
|
||||
if (item.selection) {
|
||||
selection = remap ? item.selection.map(remap.slice(mapFrom)) : item.selection;
|
||||
remaining = new Branch(_this.items.slice(0, end).append(addBefore.reverse().concat(addAfter)), _this.eventCount - 1);
|
||||
return false;
|
||||
}
|
||||
}, this.items.length, 0);
|
||||
return {
|
||||
remaining: remaining,
|
||||
transform: transform,
|
||||
selection: selection
|
||||
};
|
||||
}
|
||||
}, {
|
||||
key: "addTransform",
|
||||
value: function addTransform(transform, selection, histOptions, preserveItems) {
|
||||
var newItems = [],
|
||||
eventCount = this.eventCount;
|
||||
var oldItems = this.items,
|
||||
lastItem = !preserveItems && oldItems.length ? oldItems.get(oldItems.length - 1) : null;
|
||||
for (var i = 0; i < transform.steps.length; i++) {
|
||||
var step = transform.steps[i].invert(transform.docs[i]);
|
||||
var item = new Item(transform.mapping.maps[i], step, selection),
|
||||
merged = void 0;
|
||||
if (merged = lastItem && lastItem.merge(item)) {
|
||||
item = merged;
|
||||
if (i) newItems.pop();else oldItems = oldItems.slice(0, oldItems.length - 1);
|
||||
}
|
||||
newItems.push(item);
|
||||
if (selection) {
|
||||
eventCount++;
|
||||
selection = undefined;
|
||||
}
|
||||
if (!preserveItems) lastItem = item;
|
||||
}
|
||||
var overflow = eventCount - histOptions.depth;
|
||||
if (overflow > DEPTH_OVERFLOW) {
|
||||
oldItems = cutOffEvents(oldItems, overflow);
|
||||
eventCount -= overflow;
|
||||
}
|
||||
return new Branch(oldItems.append(newItems), eventCount);
|
||||
}
|
||||
}, {
|
||||
key: "remapping",
|
||||
value: function remapping(from, to) {
|
||||
var maps = new prosemirrorTransform.Mapping();
|
||||
this.items.forEach(function (item, i) {
|
||||
var mirrorPos = item.mirrorOffset != null && i - item.mirrorOffset >= from ? maps.maps.length - item.mirrorOffset : undefined;
|
||||
maps.appendMap(item.map, mirrorPos);
|
||||
}, from, to);
|
||||
return maps;
|
||||
}
|
||||
}, {
|
||||
key: "addMaps",
|
||||
value: function addMaps(array) {
|
||||
if (this.eventCount == 0) return this;
|
||||
return new Branch(this.items.append(array.map(function (map) {
|
||||
return new Item(map);
|
||||
})), this.eventCount);
|
||||
}
|
||||
}, {
|
||||
key: "rebased",
|
||||
value: function rebased(rebasedTransform, rebasedCount) {
|
||||
if (!this.eventCount) return this;
|
||||
var rebasedItems = [],
|
||||
start = Math.max(0, this.items.length - rebasedCount);
|
||||
var mapping = rebasedTransform.mapping;
|
||||
var newUntil = rebasedTransform.steps.length;
|
||||
var eventCount = this.eventCount;
|
||||
this.items.forEach(function (item) {
|
||||
if (item.selection) eventCount--;
|
||||
}, start);
|
||||
var iRebased = rebasedCount;
|
||||
this.items.forEach(function (item) {
|
||||
var pos = mapping.getMirror(--iRebased);
|
||||
if (pos == null) return;
|
||||
newUntil = Math.min(newUntil, pos);
|
||||
var map = mapping.maps[pos];
|
||||
if (item.step) {
|
||||
var step = rebasedTransform.steps[pos].invert(rebasedTransform.docs[pos]);
|
||||
var selection = item.selection && item.selection.map(mapping.slice(iRebased + 1, pos));
|
||||
if (selection) eventCount++;
|
||||
rebasedItems.push(new Item(map, step, selection));
|
||||
} else {
|
||||
rebasedItems.push(new Item(map));
|
||||
}
|
||||
}, start);
|
||||
var newMaps = [];
|
||||
for (var i = rebasedCount; i < newUntil; i++) newMaps.push(new Item(mapping.maps[i]));
|
||||
var items = this.items.slice(0, start).append(newMaps).append(rebasedItems);
|
||||
var branch = new Branch(items, eventCount);
|
||||
if (branch.emptyItemCount() > max_empty_items) branch = branch.compress(this.items.length - rebasedItems.length);
|
||||
return branch;
|
||||
}
|
||||
}, {
|
||||
key: "emptyItemCount",
|
||||
value: function emptyItemCount() {
|
||||
var count = 0;
|
||||
this.items.forEach(function (item) {
|
||||
if (!item.step) count++;
|
||||
});
|
||||
return count;
|
||||
}
|
||||
}, {
|
||||
key: "compress",
|
||||
value: function compress() {
|
||||
var upto = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : this.items.length;
|
||||
var remap = this.remapping(0, upto),
|
||||
mapFrom = remap.maps.length;
|
||||
var items = [],
|
||||
events = 0;
|
||||
this.items.forEach(function (item, i) {
|
||||
if (i >= upto) {
|
||||
items.push(item);
|
||||
if (item.selection) events++;
|
||||
} else if (item.step) {
|
||||
var step = item.step.map(remap.slice(mapFrom)),
|
||||
map = step && step.getMap();
|
||||
mapFrom--;
|
||||
if (map) remap.appendMap(map, mapFrom);
|
||||
if (step) {
|
||||
var selection = item.selection && item.selection.map(remap.slice(mapFrom));
|
||||
if (selection) events++;
|
||||
var newItem = new Item(map.invert(), step, selection),
|
||||
merged,
|
||||
last = items.length - 1;
|
||||
if (merged = items.length && items[last].merge(newItem)) items[last] = merged;else items.push(newItem);
|
||||
}
|
||||
} else if (item.map) {
|
||||
mapFrom--;
|
||||
}
|
||||
}, this.items.length, 0);
|
||||
return new Branch(RopeSequence.from(items.reverse()), events);
|
||||
}
|
||||
}]);
|
||||
return Branch;
|
||||
}();
|
||||
Branch.empty = new Branch(RopeSequence.empty, 0);
|
||||
function cutOffEvents(items, n) {
|
||||
var cutPoint;
|
||||
items.forEach(function (item, i) {
|
||||
if (item.selection && n-- == 0) {
|
||||
cutPoint = i;
|
||||
return false;
|
||||
}
|
||||
});
|
||||
return items.slice(cutPoint);
|
||||
}
|
||||
var Item = function () {
|
||||
function Item(map, step, selection, mirrorOffset) {
|
||||
_classCallCheck(this, Item);
|
||||
this.map = map;
|
||||
this.step = step;
|
||||
this.selection = selection;
|
||||
this.mirrorOffset = mirrorOffset;
|
||||
}
|
||||
_createClass(Item, [{
|
||||
key: "merge",
|
||||
value: function merge(other) {
|
||||
if (this.step && other.step && !other.selection) {
|
||||
var step = other.step.merge(this.step);
|
||||
if (step) return new Item(step.getMap().invert(), step, this.selection);
|
||||
}
|
||||
}
|
||||
}]);
|
||||
return Item;
|
||||
}();
|
||||
var HistoryState = _createClass(function HistoryState(done, undone, prevRanges, prevTime, prevComposition) {
|
||||
_classCallCheck(this, HistoryState);
|
||||
this.done = done;
|
||||
this.undone = undone;
|
||||
this.prevRanges = prevRanges;
|
||||
this.prevTime = prevTime;
|
||||
this.prevComposition = prevComposition;
|
||||
});
|
||||
var DEPTH_OVERFLOW = 20;
|
||||
function applyTransaction(history, state, tr, options) {
|
||||
var historyTr = tr.getMeta(historyKey),
|
||||
rebased;
|
||||
if (historyTr) return historyTr.historyState;
|
||||
if (tr.getMeta(closeHistoryKey)) history = new HistoryState(history.done, history.undone, null, 0, -1);
|
||||
var appended = tr.getMeta("appendedTransaction");
|
||||
if (tr.steps.length == 0) {
|
||||
return history;
|
||||
} else if (appended && appended.getMeta(historyKey)) {
|
||||
if (appended.getMeta(historyKey).redo) return new HistoryState(history.done.addTransform(tr, undefined, options, mustPreserveItems(state)), history.undone, rangesFor(tr.mapping.maps), history.prevTime, history.prevComposition);else return new HistoryState(history.done, history.undone.addTransform(tr, undefined, options, mustPreserveItems(state)), null, history.prevTime, history.prevComposition);
|
||||
} else if (tr.getMeta("addToHistory") !== false && !(appended && appended.getMeta("addToHistory") === false)) {
|
||||
var composition = tr.getMeta("composition");
|
||||
var newGroup = history.prevTime == 0 || !appended && history.prevComposition != composition && (history.prevTime < (tr.time || 0) - options.newGroupDelay || !isAdjacentTo(tr, history.prevRanges));
|
||||
var prevRanges = appended ? mapRanges(history.prevRanges, tr.mapping) : rangesFor(tr.mapping.maps);
|
||||
return new HistoryState(history.done.addTransform(tr, newGroup ? state.selection.getBookmark() : undefined, options, mustPreserveItems(state)), Branch.empty, prevRanges, tr.time, composition == null ? history.prevComposition : composition);
|
||||
} else if (rebased = tr.getMeta("rebased")) {
|
||||
return new HistoryState(history.done.rebased(tr, rebased), history.undone.rebased(tr, rebased), mapRanges(history.prevRanges, tr.mapping), history.prevTime, history.prevComposition);
|
||||
} else {
|
||||
return new HistoryState(history.done.addMaps(tr.mapping.maps), history.undone.addMaps(tr.mapping.maps), mapRanges(history.prevRanges, tr.mapping), history.prevTime, history.prevComposition);
|
||||
}
|
||||
}
|
||||
function isAdjacentTo(transform, prevRanges) {
|
||||
if (!prevRanges) return false;
|
||||
if (!transform.docChanged) return true;
|
||||
var adjacent = false;
|
||||
transform.mapping.maps[0].forEach(function (start, end) {
|
||||
for (var i = 0; i < prevRanges.length; i += 2) if (start <= prevRanges[i + 1] && end >= prevRanges[i]) adjacent = true;
|
||||
});
|
||||
return adjacent;
|
||||
}
|
||||
function rangesFor(maps) {
|
||||
var result = [];
|
||||
for (var i = maps.length - 1; i >= 0 && result.length == 0; i--) maps[i].forEach(function (_from, _to, from, to) {
|
||||
return result.push(from, to);
|
||||
});
|
||||
return result;
|
||||
}
|
||||
function mapRanges(ranges, mapping) {
|
||||
if (!ranges) return null;
|
||||
var result = [];
|
||||
for (var i = 0; i < ranges.length; i += 2) {
|
||||
var from = mapping.map(ranges[i], 1),
|
||||
to = mapping.map(ranges[i + 1], -1);
|
||||
if (from <= to) result.push(from, to);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
function histTransaction(history, state, redo) {
|
||||
var preserveItems = mustPreserveItems(state);
|
||||
var histOptions = historyKey.get(state).spec.config;
|
||||
var pop = (redo ? history.undone : history.done).popEvent(state, preserveItems);
|
||||
if (!pop) return null;
|
||||
var selection = pop.selection.resolve(pop.transform.doc);
|
||||
var added = (redo ? history.done : history.undone).addTransform(pop.transform, state.selection.getBookmark(), histOptions, preserveItems);
|
||||
var newHist = new HistoryState(redo ? added : pop.remaining, redo ? pop.remaining : added, null, 0, -1);
|
||||
return pop.transform.setSelection(selection).setMeta(historyKey, {
|
||||
redo: redo,
|
||||
historyState: newHist
|
||||
});
|
||||
}
|
||||
var cachedPreserveItems = false,
|
||||
cachedPreserveItemsPlugins = null;
|
||||
function mustPreserveItems(state) {
|
||||
var plugins = state.plugins;
|
||||
if (cachedPreserveItemsPlugins != plugins) {
|
||||
cachedPreserveItems = false;
|
||||
cachedPreserveItemsPlugins = plugins;
|
||||
for (var i = 0; i < plugins.length; i++) if (plugins[i].spec.historyPreserveItems) {
|
||||
cachedPreserveItems = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return cachedPreserveItems;
|
||||
}
|
||||
function closeHistory(tr) {
|
||||
return tr.setMeta(closeHistoryKey, true);
|
||||
}
|
||||
var historyKey = new prosemirrorState.PluginKey("history");
|
||||
var closeHistoryKey = new prosemirrorState.PluginKey("closeHistory");
|
||||
function history() {
|
||||
var config = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
|
||||
config = {
|
||||
depth: config.depth || 100,
|
||||
newGroupDelay: config.newGroupDelay || 500
|
||||
};
|
||||
return new prosemirrorState.Plugin({
|
||||
key: historyKey,
|
||||
state: {
|
||||
init: function init() {
|
||||
return new HistoryState(Branch.empty, Branch.empty, null, 0, -1);
|
||||
},
|
||||
apply: function apply(tr, hist, state) {
|
||||
return applyTransaction(hist, state, tr, config);
|
||||
}
|
||||
},
|
||||
config: config,
|
||||
props: {
|
||||
handleDOMEvents: {
|
||||
beforeinput: function beforeinput(view, e) {
|
||||
var inputType = e.inputType;
|
||||
var command = inputType == "historyUndo" ? undo : inputType == "historyRedo" ? redo : null;
|
||||
if (!command || !view.editable) return false;
|
||||
e.preventDefault();
|
||||
return command(view.state, view.dispatch);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
function buildCommand(redo, scroll) {
|
||||
return function (state, dispatch) {
|
||||
var hist = historyKey.getState(state);
|
||||
if (!hist || (redo ? hist.undone : hist.done).eventCount == 0) return false;
|
||||
if (dispatch) {
|
||||
var tr = histTransaction(hist, state, redo);
|
||||
if (tr) dispatch(scroll ? tr.scrollIntoView() : tr);
|
||||
}
|
||||
return true;
|
||||
};
|
||||
}
|
||||
var undo = buildCommand(false, true);
|
||||
var redo = buildCommand(true, true);
|
||||
var undoNoScroll = buildCommand(false, false);
|
||||
var redoNoScroll = buildCommand(true, false);
|
||||
function undoDepth(state) {
|
||||
var hist = historyKey.getState(state);
|
||||
return hist ? hist.done.eventCount : 0;
|
||||
}
|
||||
function redoDepth(state) {
|
||||
var hist = historyKey.getState(state);
|
||||
return hist ? hist.undone.eventCount : 0;
|
||||
}
|
||||
function isHistoryTransaction(tr) {
|
||||
return tr.getMeta(historyKey) != null;
|
||||
}
|
||||
exports.closeHistory = closeHistory;
|
||||
exports.history = history;
|
||||
exports.isHistoryTransaction = isHistoryTransaction;
|
||||
exports.redo = redo;
|
||||
exports.redoDepth = redoDepth;
|
||||
exports.redoNoScroll = redoNoScroll;
|
||||
exports.undo = undo;
|
||||
exports.undoDepth = undoDepth;
|
||||
exports.undoNoScroll = undoNoScroll;
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
import { Transaction, Plugin, Command, EditorState } from 'prosemirror-state';
|
||||
|
||||
/**
|
||||
Set a flag on the given transaction that will prevent further steps
|
||||
from being appended to an existing history event (so that they
|
||||
require a separate undo command to undo).
|
||||
*/
|
||||
declare function closeHistory(tr: Transaction): Transaction;
|
||||
interface HistoryOptions {
|
||||
/**
|
||||
The amount of history events that are collected before the
|
||||
oldest events are discarded. Defaults to 100.
|
||||
*/
|
||||
depth?: number;
|
||||
/**
|
||||
The delay between changes after which a new group should be
|
||||
started. Defaults to 500 (milliseconds). Note that when changes
|
||||
aren't adjacent, a new group is always started.
|
||||
*/
|
||||
newGroupDelay?: number;
|
||||
}
|
||||
/**
|
||||
Returns a plugin that enables the undo history for an editor. The
|
||||
plugin will track undo and redo stacks, which can be used with the
|
||||
[`undo`](https://prosemirror.net/docs/ref/#history.undo) and [`redo`](https://prosemirror.net/docs/ref/#history.redo) commands.
|
||||
|
||||
You can set an `"addToHistory"` [metadata
|
||||
property](https://prosemirror.net/docs/ref/#state.Transaction.setMeta) of `false` on a transaction
|
||||
to prevent it from being rolled back by undo.
|
||||
*/
|
||||
declare function history(config?: HistoryOptions): Plugin;
|
||||
/**
|
||||
A command function that undoes the last change, if any.
|
||||
*/
|
||||
declare const undo: Command;
|
||||
/**
|
||||
A command function that redoes the last undone change, if any.
|
||||
*/
|
||||
declare const redo: Command;
|
||||
/**
|
||||
A command function that undoes the last change. Don't scroll the
|
||||
selection into view.
|
||||
*/
|
||||
declare const undoNoScroll: Command;
|
||||
/**
|
||||
A command function that redoes the last undone change. Don't
|
||||
scroll the selection into view.
|
||||
*/
|
||||
declare const redoNoScroll: Command;
|
||||
/**
|
||||
The amount of undoable events available in a given state.
|
||||
*/
|
||||
declare function undoDepth(state: EditorState): any;
|
||||
/**
|
||||
The amount of redoable events available in a given editor state.
|
||||
*/
|
||||
declare function redoDepth(state: EditorState): any;
|
||||
/**
|
||||
Returns true if the given transaction was generated by the history
|
||||
plugin.
|
||||
*/
|
||||
declare function isHistoryTransaction(tr: Transaction): boolean;
|
||||
|
||||
export { closeHistory, history, isHistoryTransaction, redo, redoDepth, redoNoScroll, undo, undoDepth, undoNoScroll };
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
import { Transaction, Plugin, Command, EditorState } from 'prosemirror-state';
|
||||
|
||||
/**
|
||||
Set a flag on the given transaction that will prevent further steps
|
||||
from being appended to an existing history event (so that they
|
||||
require a separate undo command to undo).
|
||||
*/
|
||||
declare function closeHistory(tr: Transaction): Transaction;
|
||||
interface HistoryOptions {
|
||||
/**
|
||||
The amount of history events that are collected before the
|
||||
oldest events are discarded. Defaults to 100.
|
||||
*/
|
||||
depth?: number;
|
||||
/**
|
||||
The delay between changes after which a new group should be
|
||||
started. Defaults to 500 (milliseconds). Note that when changes
|
||||
aren't adjacent, a new group is always started.
|
||||
*/
|
||||
newGroupDelay?: number;
|
||||
}
|
||||
/**
|
||||
Returns a plugin that enables the undo history for an editor. The
|
||||
plugin will track undo and redo stacks, which can be used with the
|
||||
[`undo`](https://prosemirror.net/docs/ref/#history.undo) and [`redo`](https://prosemirror.net/docs/ref/#history.redo) commands.
|
||||
|
||||
You can set an `"addToHistory"` [metadata
|
||||
property](https://prosemirror.net/docs/ref/#state.Transaction.setMeta) of `false` on a transaction
|
||||
to prevent it from being rolled back by undo.
|
||||
*/
|
||||
declare function history(config?: HistoryOptions): Plugin;
|
||||
/**
|
||||
A command function that undoes the last change, if any.
|
||||
*/
|
||||
declare const undo: Command;
|
||||
/**
|
||||
A command function that redoes the last undone change, if any.
|
||||
*/
|
||||
declare const redo: Command;
|
||||
/**
|
||||
A command function that undoes the last change. Don't scroll the
|
||||
selection into view.
|
||||
*/
|
||||
declare const undoNoScroll: Command;
|
||||
/**
|
||||
A command function that redoes the last undone change. Don't
|
||||
scroll the selection into view.
|
||||
*/
|
||||
declare const redoNoScroll: Command;
|
||||
/**
|
||||
The amount of undoable events available in a given state.
|
||||
*/
|
||||
declare function undoDepth(state: EditorState): any;
|
||||
/**
|
||||
The amount of redoable events available in a given editor state.
|
||||
*/
|
||||
declare function redoDepth(state: EditorState): any;
|
||||
/**
|
||||
Returns true if the given transaction was generated by the history
|
||||
plugin.
|
||||
*/
|
||||
declare function isHistoryTransaction(tr: Transaction): boolean;
|
||||
|
||||
export { closeHistory, history, isHistoryTransaction, redo, redoDepth, redoNoScroll, undo, undoDepth, undoNoScroll };
|
||||
+453
@@ -0,0 +1,453 @@
|
||||
import RopeSequence from 'rope-sequence';
|
||||
import { Mapping } from 'prosemirror-transform';
|
||||
import { PluginKey, Plugin } from 'prosemirror-state';
|
||||
|
||||
// ProseMirror's history isn't simply a way to roll back to a previous
|
||||
// state, because ProseMirror supports applying changes without adding
|
||||
// them to the history (for example during collaboration).
|
||||
//
|
||||
// To this end, each 'Branch' (one for the undo history and one for
|
||||
// the redo history) keeps an array of 'Items', which can optionally
|
||||
// hold a step (an actual undoable change), and always hold a position
|
||||
// map (which is needed to move changes below them to apply to the
|
||||
// current document).
|
||||
//
|
||||
// An item that has both a step and a selection bookmark is the start
|
||||
// of an 'event' — a group of changes that will be undone or redone at
|
||||
// once. (It stores only the bookmark, since that way we don't have to
|
||||
// provide a document until the selection is actually applied, which
|
||||
// is useful when compressing.)
|
||||
// Used to schedule history compression
|
||||
const max_empty_items = 500;
|
||||
class Branch {
|
||||
constructor(items, eventCount) {
|
||||
this.items = items;
|
||||
this.eventCount = eventCount;
|
||||
}
|
||||
// Pop the latest event off the branch's history and apply it
|
||||
// to a document transform.
|
||||
popEvent(state, preserveItems) {
|
||||
if (this.eventCount == 0)
|
||||
return null;
|
||||
let end = this.items.length;
|
||||
for (;; end--) {
|
||||
let next = this.items.get(end - 1);
|
||||
if (next.selection) {
|
||||
--end;
|
||||
break;
|
||||
}
|
||||
}
|
||||
let remap, mapFrom;
|
||||
if (preserveItems) {
|
||||
remap = this.remapping(end, this.items.length);
|
||||
mapFrom = remap.maps.length;
|
||||
}
|
||||
let transform = state.tr;
|
||||
let selection, remaining;
|
||||
let addAfter = [], addBefore = [];
|
||||
this.items.forEach((item, i) => {
|
||||
if (!item.step) {
|
||||
if (!remap) {
|
||||
remap = this.remapping(end, i + 1);
|
||||
mapFrom = remap.maps.length;
|
||||
}
|
||||
mapFrom--;
|
||||
addBefore.push(item);
|
||||
return;
|
||||
}
|
||||
if (remap) {
|
||||
addBefore.push(new Item(item.map));
|
||||
let step = item.step.map(remap.slice(mapFrom)), map;
|
||||
if (step && transform.maybeStep(step).doc) {
|
||||
map = transform.mapping.maps[transform.mapping.maps.length - 1];
|
||||
addAfter.push(new Item(map, undefined, undefined, addAfter.length + addBefore.length));
|
||||
}
|
||||
mapFrom--;
|
||||
if (map)
|
||||
remap.appendMap(map, mapFrom);
|
||||
}
|
||||
else {
|
||||
transform.maybeStep(item.step);
|
||||
}
|
||||
if (item.selection) {
|
||||
selection = remap ? item.selection.map(remap.slice(mapFrom)) : item.selection;
|
||||
remaining = new Branch(this.items.slice(0, end).append(addBefore.reverse().concat(addAfter)), this.eventCount - 1);
|
||||
return false;
|
||||
}
|
||||
}, this.items.length, 0);
|
||||
return { remaining: remaining, transform, selection: selection };
|
||||
}
|
||||
// Create a new branch with the given transform added.
|
||||
addTransform(transform, selection, histOptions, preserveItems) {
|
||||
let newItems = [], eventCount = this.eventCount;
|
||||
let oldItems = this.items, lastItem = !preserveItems && oldItems.length ? oldItems.get(oldItems.length - 1) : null;
|
||||
for (let i = 0; i < transform.steps.length; i++) {
|
||||
let step = transform.steps[i].invert(transform.docs[i]);
|
||||
let item = new Item(transform.mapping.maps[i], step, selection), merged;
|
||||
if (merged = lastItem && lastItem.merge(item)) {
|
||||
item = merged;
|
||||
if (i)
|
||||
newItems.pop();
|
||||
else
|
||||
oldItems = oldItems.slice(0, oldItems.length - 1);
|
||||
}
|
||||
newItems.push(item);
|
||||
if (selection) {
|
||||
eventCount++;
|
||||
selection = undefined;
|
||||
}
|
||||
if (!preserveItems)
|
||||
lastItem = item;
|
||||
}
|
||||
let overflow = eventCount - histOptions.depth;
|
||||
if (overflow > DEPTH_OVERFLOW) {
|
||||
oldItems = cutOffEvents(oldItems, overflow);
|
||||
eventCount -= overflow;
|
||||
}
|
||||
return new Branch(oldItems.append(newItems), eventCount);
|
||||
}
|
||||
remapping(from, to) {
|
||||
let maps = new Mapping;
|
||||
this.items.forEach((item, i) => {
|
||||
let mirrorPos = item.mirrorOffset != null && i - item.mirrorOffset >= from
|
||||
? maps.maps.length - item.mirrorOffset : undefined;
|
||||
maps.appendMap(item.map, mirrorPos);
|
||||
}, from, to);
|
||||
return maps;
|
||||
}
|
||||
addMaps(array) {
|
||||
if (this.eventCount == 0)
|
||||
return this;
|
||||
return new Branch(this.items.append(array.map(map => new Item(map))), this.eventCount);
|
||||
}
|
||||
// When the collab module receives remote changes, the history has
|
||||
// to know about those, so that it can adjust the steps that were
|
||||
// rebased on top of the remote changes, and include the position
|
||||
// maps for the remote changes in its array of items.
|
||||
rebased(rebasedTransform, rebasedCount) {
|
||||
if (!this.eventCount)
|
||||
return this;
|
||||
let rebasedItems = [], start = Math.max(0, this.items.length - rebasedCount);
|
||||
let mapping = rebasedTransform.mapping;
|
||||
let newUntil = rebasedTransform.steps.length;
|
||||
let eventCount = this.eventCount;
|
||||
this.items.forEach(item => { if (item.selection)
|
||||
eventCount--; }, start);
|
||||
let iRebased = rebasedCount;
|
||||
this.items.forEach(item => {
|
||||
let pos = mapping.getMirror(--iRebased);
|
||||
if (pos == null)
|
||||
return;
|
||||
newUntil = Math.min(newUntil, pos);
|
||||
let map = mapping.maps[pos];
|
||||
if (item.step) {
|
||||
let step = rebasedTransform.steps[pos].invert(rebasedTransform.docs[pos]);
|
||||
let selection = item.selection && item.selection.map(mapping.slice(iRebased + 1, pos));
|
||||
if (selection)
|
||||
eventCount++;
|
||||
rebasedItems.push(new Item(map, step, selection));
|
||||
}
|
||||
else {
|
||||
rebasedItems.push(new Item(map));
|
||||
}
|
||||
}, start);
|
||||
let newMaps = [];
|
||||
for (let i = rebasedCount; i < newUntil; i++)
|
||||
newMaps.push(new Item(mapping.maps[i]));
|
||||
let items = this.items.slice(0, start).append(newMaps).append(rebasedItems);
|
||||
let branch = new Branch(items, eventCount);
|
||||
if (branch.emptyItemCount() > max_empty_items)
|
||||
branch = branch.compress(this.items.length - rebasedItems.length);
|
||||
return branch;
|
||||
}
|
||||
emptyItemCount() {
|
||||
let count = 0;
|
||||
this.items.forEach(item => { if (!item.step)
|
||||
count++; });
|
||||
return count;
|
||||
}
|
||||
// Compressing a branch means rewriting it to push the air (map-only
|
||||
// items) out. During collaboration, these naturally accumulate
|
||||
// because each remote change adds one. The `upto` argument is used
|
||||
// to ensure that only the items below a given level are compressed,
|
||||
// because `rebased` relies on a clean, untouched set of items in
|
||||
// order to associate old items with rebased steps.
|
||||
compress(upto = this.items.length) {
|
||||
let remap = this.remapping(0, upto), mapFrom = remap.maps.length;
|
||||
let items = [], events = 0;
|
||||
this.items.forEach((item, i) => {
|
||||
if (i >= upto) {
|
||||
items.push(item);
|
||||
if (item.selection)
|
||||
events++;
|
||||
}
|
||||
else if (item.step) {
|
||||
let step = item.step.map(remap.slice(mapFrom)), map = step && step.getMap();
|
||||
mapFrom--;
|
||||
if (map)
|
||||
remap.appendMap(map, mapFrom);
|
||||
if (step) {
|
||||
let selection = item.selection && item.selection.map(remap.slice(mapFrom));
|
||||
if (selection)
|
||||
events++;
|
||||
let newItem = new Item(map.invert(), step, selection), merged, last = items.length - 1;
|
||||
if (merged = items.length && items[last].merge(newItem))
|
||||
items[last] = merged;
|
||||
else
|
||||
items.push(newItem);
|
||||
}
|
||||
}
|
||||
else if (item.map) {
|
||||
mapFrom--;
|
||||
}
|
||||
}, this.items.length, 0);
|
||||
return new Branch(RopeSequence.from(items.reverse()), events);
|
||||
}
|
||||
}
|
||||
Branch.empty = new Branch(RopeSequence.empty, 0);
|
||||
function cutOffEvents(items, n) {
|
||||
let cutPoint;
|
||||
items.forEach((item, i) => {
|
||||
if (item.selection && (n-- == 0)) {
|
||||
cutPoint = i;
|
||||
return false;
|
||||
}
|
||||
});
|
||||
return items.slice(cutPoint);
|
||||
}
|
||||
class Item {
|
||||
constructor(
|
||||
// The (forward) step map for this item.
|
||||
map,
|
||||
// The inverted step
|
||||
step,
|
||||
// If this is non-null, this item is the start of a group, and
|
||||
// this selection is the starting selection for the group (the one
|
||||
// that was active before the first step was applied)
|
||||
selection,
|
||||
// If this item is the inverse of a previous mapping on the stack,
|
||||
// this points at the inverse's offset
|
||||
mirrorOffset) {
|
||||
this.map = map;
|
||||
this.step = step;
|
||||
this.selection = selection;
|
||||
this.mirrorOffset = mirrorOffset;
|
||||
}
|
||||
merge(other) {
|
||||
if (this.step && other.step && !other.selection) {
|
||||
let step = other.step.merge(this.step);
|
||||
if (step)
|
||||
return new Item(step.getMap().invert(), step, this.selection);
|
||||
}
|
||||
}
|
||||
}
|
||||
// The value of the state field that tracks undo/redo history for that
|
||||
// state. Will be stored in the plugin state when the history plugin
|
||||
// is active.
|
||||
class HistoryState {
|
||||
constructor(done, undone, prevRanges, prevTime, prevComposition) {
|
||||
this.done = done;
|
||||
this.undone = undone;
|
||||
this.prevRanges = prevRanges;
|
||||
this.prevTime = prevTime;
|
||||
this.prevComposition = prevComposition;
|
||||
}
|
||||
}
|
||||
const DEPTH_OVERFLOW = 20;
|
||||
// Record a transformation in undo history.
|
||||
function applyTransaction(history, state, tr, options) {
|
||||
let historyTr = tr.getMeta(historyKey), rebased;
|
||||
if (historyTr)
|
||||
return historyTr.historyState;
|
||||
if (tr.getMeta(closeHistoryKey))
|
||||
history = new HistoryState(history.done, history.undone, null, 0, -1);
|
||||
let appended = tr.getMeta("appendedTransaction");
|
||||
if (tr.steps.length == 0) {
|
||||
return history;
|
||||
}
|
||||
else if (appended && appended.getMeta(historyKey)) {
|
||||
if (appended.getMeta(historyKey).redo)
|
||||
return new HistoryState(history.done.addTransform(tr, undefined, options, mustPreserveItems(state)), history.undone, rangesFor(tr.mapping.maps), history.prevTime, history.prevComposition);
|
||||
else
|
||||
return new HistoryState(history.done, history.undone.addTransform(tr, undefined, options, mustPreserveItems(state)), null, history.prevTime, history.prevComposition);
|
||||
}
|
||||
else if (tr.getMeta("addToHistory") !== false && !(appended && appended.getMeta("addToHistory") === false)) {
|
||||
// Group transforms that occur in quick succession into one event.
|
||||
let composition = tr.getMeta("composition");
|
||||
let newGroup = history.prevTime == 0 ||
|
||||
(!appended && history.prevComposition != composition &&
|
||||
(history.prevTime < (tr.time || 0) - options.newGroupDelay || !isAdjacentTo(tr, history.prevRanges)));
|
||||
let prevRanges = appended ? mapRanges(history.prevRanges, tr.mapping) : rangesFor(tr.mapping.maps);
|
||||
return new HistoryState(history.done.addTransform(tr, newGroup ? state.selection.getBookmark() : undefined, options, mustPreserveItems(state)), Branch.empty, prevRanges, tr.time, composition == null ? history.prevComposition : composition);
|
||||
}
|
||||
else if (rebased = tr.getMeta("rebased")) {
|
||||
// Used by the collab module to tell the history that some of its
|
||||
// content has been rebased.
|
||||
return new HistoryState(history.done.rebased(tr, rebased), history.undone.rebased(tr, rebased), mapRanges(history.prevRanges, tr.mapping), history.prevTime, history.prevComposition);
|
||||
}
|
||||
else {
|
||||
return new HistoryState(history.done.addMaps(tr.mapping.maps), history.undone.addMaps(tr.mapping.maps), mapRanges(history.prevRanges, tr.mapping), history.prevTime, history.prevComposition);
|
||||
}
|
||||
}
|
||||
function isAdjacentTo(transform, prevRanges) {
|
||||
if (!prevRanges)
|
||||
return false;
|
||||
if (!transform.docChanged)
|
||||
return true;
|
||||
let adjacent = false;
|
||||
transform.mapping.maps[0].forEach((start, end) => {
|
||||
for (let i = 0; i < prevRanges.length; i += 2)
|
||||
if (start <= prevRanges[i + 1] && end >= prevRanges[i])
|
||||
adjacent = true;
|
||||
});
|
||||
return adjacent;
|
||||
}
|
||||
function rangesFor(maps) {
|
||||
let result = [];
|
||||
for (let i = maps.length - 1; i >= 0 && result.length == 0; i--)
|
||||
maps[i].forEach((_from, _to, from, to) => result.push(from, to));
|
||||
return result;
|
||||
}
|
||||
function mapRanges(ranges, mapping) {
|
||||
if (!ranges)
|
||||
return null;
|
||||
let result = [];
|
||||
for (let i = 0; i < ranges.length; i += 2) {
|
||||
let from = mapping.map(ranges[i], 1), to = mapping.map(ranges[i + 1], -1);
|
||||
if (from <= to)
|
||||
result.push(from, to);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
// Apply the latest event from one branch to the document and shift the event
|
||||
// onto the other branch.
|
||||
function histTransaction(history, state, redo) {
|
||||
let preserveItems = mustPreserveItems(state);
|
||||
let histOptions = historyKey.get(state).spec.config;
|
||||
let pop = (redo ? history.undone : history.done).popEvent(state, preserveItems);
|
||||
if (!pop)
|
||||
return null;
|
||||
let selection = pop.selection.resolve(pop.transform.doc);
|
||||
let added = (redo ? history.done : history.undone).addTransform(pop.transform, state.selection.getBookmark(), histOptions, preserveItems);
|
||||
let newHist = new HistoryState(redo ? added : pop.remaining, redo ? pop.remaining : added, null, 0, -1);
|
||||
return pop.transform.setSelection(selection).setMeta(historyKey, { redo, historyState: newHist });
|
||||
}
|
||||
let cachedPreserveItems = false, cachedPreserveItemsPlugins = null;
|
||||
// Check whether any plugin in the given state has a
|
||||
// `historyPreserveItems` property in its spec, in which case we must
|
||||
// preserve steps exactly as they came in, so that they can be
|
||||
// rebased.
|
||||
function mustPreserveItems(state) {
|
||||
let plugins = state.plugins;
|
||||
if (cachedPreserveItemsPlugins != plugins) {
|
||||
cachedPreserveItems = false;
|
||||
cachedPreserveItemsPlugins = plugins;
|
||||
for (let i = 0; i < plugins.length; i++)
|
||||
if (plugins[i].spec.historyPreserveItems) {
|
||||
cachedPreserveItems = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return cachedPreserveItems;
|
||||
}
|
||||
/**
|
||||
Set a flag on the given transaction that will prevent further steps
|
||||
from being appended to an existing history event (so that they
|
||||
require a separate undo command to undo).
|
||||
*/
|
||||
function closeHistory(tr) {
|
||||
return tr.setMeta(closeHistoryKey, true);
|
||||
}
|
||||
const historyKey = new PluginKey("history");
|
||||
const closeHistoryKey = new PluginKey("closeHistory");
|
||||
/**
|
||||
Returns a plugin that enables the undo history for an editor. The
|
||||
plugin will track undo and redo stacks, which can be used with the
|
||||
[`undo`](https://prosemirror.net/docs/ref/#history.undo) and [`redo`](https://prosemirror.net/docs/ref/#history.redo) commands.
|
||||
|
||||
You can set an `"addToHistory"` [metadata
|
||||
property](https://prosemirror.net/docs/ref/#state.Transaction.setMeta) of `false` on a transaction
|
||||
to prevent it from being rolled back by undo.
|
||||
*/
|
||||
function history(config = {}) {
|
||||
config = { depth: config.depth || 100,
|
||||
newGroupDelay: config.newGroupDelay || 500 };
|
||||
return new Plugin({
|
||||
key: historyKey,
|
||||
state: {
|
||||
init() {
|
||||
return new HistoryState(Branch.empty, Branch.empty, null, 0, -1);
|
||||
},
|
||||
apply(tr, hist, state) {
|
||||
return applyTransaction(hist, state, tr, config);
|
||||
}
|
||||
},
|
||||
config,
|
||||
props: {
|
||||
handleDOMEvents: {
|
||||
beforeinput(view, e) {
|
||||
let inputType = e.inputType;
|
||||
let command = inputType == "historyUndo" ? undo : inputType == "historyRedo" ? redo : null;
|
||||
if (!command || !view.editable)
|
||||
return false;
|
||||
e.preventDefault();
|
||||
return command(view.state, view.dispatch);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
function buildCommand(redo, scroll) {
|
||||
return (state, dispatch) => {
|
||||
let hist = historyKey.getState(state);
|
||||
if (!hist || (redo ? hist.undone : hist.done).eventCount == 0)
|
||||
return false;
|
||||
if (dispatch) {
|
||||
let tr = histTransaction(hist, state, redo);
|
||||
if (tr)
|
||||
dispatch(scroll ? tr.scrollIntoView() : tr);
|
||||
}
|
||||
return true;
|
||||
};
|
||||
}
|
||||
/**
|
||||
A command function that undoes the last change, if any.
|
||||
*/
|
||||
const undo = buildCommand(false, true);
|
||||
/**
|
||||
A command function that redoes the last undone change, if any.
|
||||
*/
|
||||
const redo = buildCommand(true, true);
|
||||
/**
|
||||
A command function that undoes the last change. Don't scroll the
|
||||
selection into view.
|
||||
*/
|
||||
const undoNoScroll = buildCommand(false, false);
|
||||
/**
|
||||
A command function that redoes the last undone change. Don't
|
||||
scroll the selection into view.
|
||||
*/
|
||||
const redoNoScroll = buildCommand(true, false);
|
||||
/**
|
||||
The amount of undoable events available in a given state.
|
||||
*/
|
||||
function undoDepth(state) {
|
||||
let hist = historyKey.getState(state);
|
||||
return hist ? hist.done.eventCount : 0;
|
||||
}
|
||||
/**
|
||||
The amount of redoable events available in a given editor state.
|
||||
*/
|
||||
function redoDepth(state) {
|
||||
let hist = historyKey.getState(state);
|
||||
return hist ? hist.undone.eventCount : 0;
|
||||
}
|
||||
/**
|
||||
Returns true if the given transaction was generated by the history
|
||||
plugin.
|
||||
*/
|
||||
function isHistoryTransaction(tr) {
|
||||
return tr.getMeta(historyKey) != null;
|
||||
}
|
||||
|
||||
export { closeHistory, history, isHistoryTransaction, redo, redoDepth, redoNoScroll, undo, undoDepth, undoNoScroll };
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"name": "prosemirror-history",
|
||||
"version": "1.5.0",
|
||||
"description": "Undo history for ProseMirror",
|
||||
"type": "module",
|
||||
"main": "dist/index.cjs",
|
||||
"module": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"exports": {
|
||||
"import": "./dist/index.js",
|
||||
"require": "./dist/index.cjs"
|
||||
},
|
||||
"sideEffects": false,
|
||||
"license": "MIT",
|
||||
"maintainers": [
|
||||
{
|
||||
"name": "Marijn Haverbeke",
|
||||
"email": "marijn@haverbeke.berlin",
|
||||
"web": "http://marijnhaverbeke.nl"
|
||||
}
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git://github.com/prosemirror/prosemirror-history.git"
|
||||
},
|
||||
"dependencies": {
|
||||
"prosemirror-state": "^1.2.2",
|
||||
"prosemirror-transform": "^1.0.0",
|
||||
"prosemirror-view": "^1.31.0",
|
||||
"rope-sequence": "^1.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@prosemirror/buildhelper": "^0.1.5",
|
||||
"prosemirror-test-builder": "^1.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "pm-runtests",
|
||||
"prepare": "pm-buildhelper src/history.ts"
|
||||
}
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
An implementation of an undo/redo history for ProseMirror. This
|
||||
history is _selective_, meaning it does not just roll back to a
|
||||
previous state but can undo some changes while keeping other, later
|
||||
changes intact. (This is necessary for collaborative editing, and
|
||||
comes up in other situations as well.)
|
||||
|
||||
@history
|
||||
|
||||
@undo
|
||||
|
||||
@redo
|
||||
|
||||
@undoNoScroll
|
||||
|
||||
@redoNoScroll
|
||||
|
||||
@undoDepth
|
||||
|
||||
@redoDepth
|
||||
|
||||
@closeHistory
|
||||
|
||||
@isHistoryTransaction
|
||||
+465
@@ -0,0 +1,465 @@
|
||||
import RopeSequence from "rope-sequence"
|
||||
import {Mapping, Step, StepMap, Transform} from "prosemirror-transform"
|
||||
import {Plugin, Command, PluginKey, EditorState, Transaction, SelectionBookmark} from "prosemirror-state"
|
||||
|
||||
// ProseMirror's history isn't simply a way to roll back to a previous
|
||||
// state, because ProseMirror supports applying changes without adding
|
||||
// them to the history (for example during collaboration).
|
||||
//
|
||||
// To this end, each 'Branch' (one for the undo history and one for
|
||||
// the redo history) keeps an array of 'Items', which can optionally
|
||||
// hold a step (an actual undoable change), and always hold a position
|
||||
// map (which is needed to move changes below them to apply to the
|
||||
// current document).
|
||||
//
|
||||
// An item that has both a step and a selection bookmark is the start
|
||||
// of an 'event' — a group of changes that will be undone or redone at
|
||||
// once. (It stores only the bookmark, since that way we don't have to
|
||||
// provide a document until the selection is actually applied, which
|
||||
// is useful when compressing.)
|
||||
|
||||
// Used to schedule history compression
|
||||
const max_empty_items = 500
|
||||
|
||||
class Branch {
|
||||
constructor(readonly items: RopeSequence<Item>, readonly eventCount: number) {}
|
||||
|
||||
// Pop the latest event off the branch's history and apply it
|
||||
// to a document transform.
|
||||
popEvent(state: EditorState, preserveItems: boolean) {
|
||||
if (this.eventCount == 0) return null
|
||||
|
||||
let end = this.items.length
|
||||
for (;; end--) {
|
||||
let next = this.items.get(end - 1)
|
||||
if (next.selection) { --end; break }
|
||||
}
|
||||
|
||||
let remap: Mapping | undefined, mapFrom: number | undefined
|
||||
if (preserveItems) {
|
||||
remap = this.remapping(end, this.items.length)
|
||||
mapFrom = remap.maps.length
|
||||
}
|
||||
let transform = state.tr
|
||||
let selection: SelectionBookmark | undefined, remaining: Branch | undefined
|
||||
let addAfter: Item[] = [], addBefore: Item[] = []
|
||||
|
||||
this.items.forEach((item, i) => {
|
||||
if (!item.step) {
|
||||
if (!remap) {
|
||||
remap = this.remapping(end, i + 1)
|
||||
mapFrom = remap.maps.length
|
||||
}
|
||||
mapFrom!--
|
||||
addBefore.push(item)
|
||||
return
|
||||
}
|
||||
|
||||
if (remap) {
|
||||
addBefore.push(new Item(item.map))
|
||||
let step = item.step.map(remap.slice(mapFrom)), map
|
||||
|
||||
if (step && transform.maybeStep(step).doc) {
|
||||
map = transform.mapping.maps[transform.mapping.maps.length - 1]
|
||||
addAfter.push(new Item(map, undefined, undefined, addAfter.length + addBefore.length))
|
||||
}
|
||||
mapFrom!--
|
||||
if (map) remap.appendMap(map, mapFrom)
|
||||
} else {
|
||||
transform.maybeStep(item.step)
|
||||
}
|
||||
|
||||
if (item.selection) {
|
||||
selection = remap ? item.selection.map(remap.slice(mapFrom)) : item.selection
|
||||
remaining = new Branch(this.items.slice(0, end).append(addBefore.reverse().concat(addAfter)), this.eventCount - 1)
|
||||
return false
|
||||
}
|
||||
}, this.items.length, 0)
|
||||
|
||||
return {remaining: remaining!, transform, selection: selection!}
|
||||
}
|
||||
|
||||
// Create a new branch with the given transform added.
|
||||
addTransform(transform: Transform, selection: SelectionBookmark | undefined,
|
||||
histOptions: Required<HistoryOptions>, preserveItems: boolean) {
|
||||
let newItems = [], eventCount = this.eventCount
|
||||
let oldItems = this.items, lastItem = !preserveItems && oldItems.length ? oldItems.get(oldItems.length - 1) : null
|
||||
|
||||
for (let i = 0; i < transform.steps.length; i++) {
|
||||
let step = transform.steps[i].invert(transform.docs[i])
|
||||
let item = new Item(transform.mapping.maps[i], step, selection), merged
|
||||
if (merged = lastItem && lastItem.merge(item)) {
|
||||
item = merged
|
||||
if (i) newItems.pop()
|
||||
else oldItems = oldItems.slice(0, oldItems.length - 1)
|
||||
}
|
||||
newItems.push(item)
|
||||
if (selection) {
|
||||
eventCount++
|
||||
selection = undefined
|
||||
}
|
||||
if (!preserveItems) lastItem = item
|
||||
}
|
||||
let overflow = eventCount - histOptions.depth
|
||||
if (overflow > DEPTH_OVERFLOW) {
|
||||
oldItems = cutOffEvents(oldItems, overflow)
|
||||
eventCount -= overflow
|
||||
}
|
||||
return new Branch(oldItems.append(newItems), eventCount)
|
||||
}
|
||||
|
||||
remapping(from: number, to: number): Mapping {
|
||||
let maps = new Mapping
|
||||
this.items.forEach((item, i) => {
|
||||
let mirrorPos = item.mirrorOffset != null && i - item.mirrorOffset >= from
|
||||
? maps.maps.length - item.mirrorOffset : undefined
|
||||
maps.appendMap(item.map, mirrorPos)
|
||||
}, from, to)
|
||||
return maps
|
||||
}
|
||||
|
||||
addMaps(array: readonly StepMap[]) {
|
||||
if (this.eventCount == 0) return this
|
||||
return new Branch(this.items.append(array.map(map => new Item(map))), this.eventCount)
|
||||
}
|
||||
|
||||
// When the collab module receives remote changes, the history has
|
||||
// to know about those, so that it can adjust the steps that were
|
||||
// rebased on top of the remote changes, and include the position
|
||||
// maps for the remote changes in its array of items.
|
||||
rebased(rebasedTransform: Transform, rebasedCount: number) {
|
||||
if (!this.eventCount) return this
|
||||
|
||||
let rebasedItems: Item[] = [], start = Math.max(0, this.items.length - rebasedCount)
|
||||
|
||||
let mapping = rebasedTransform.mapping
|
||||
let newUntil = rebasedTransform.steps.length
|
||||
let eventCount = this.eventCount
|
||||
this.items.forEach(item => { if (item.selection) eventCount-- }, start)
|
||||
|
||||
let iRebased = rebasedCount
|
||||
this.items.forEach(item => {
|
||||
let pos = mapping.getMirror(--iRebased)
|
||||
if (pos == null) return
|
||||
newUntil = Math.min(newUntil, pos)
|
||||
let map = mapping.maps[pos]
|
||||
if (item.step) {
|
||||
let step = rebasedTransform.steps[pos].invert(rebasedTransform.docs[pos])
|
||||
let selection = item.selection && item.selection.map(mapping.slice(iRebased + 1, pos))
|
||||
if (selection) eventCount++
|
||||
rebasedItems.push(new Item(map, step, selection))
|
||||
} else {
|
||||
rebasedItems.push(new Item(map))
|
||||
}
|
||||
}, start)
|
||||
|
||||
let newMaps = []
|
||||
for (let i = rebasedCount; i < newUntil; i++)
|
||||
newMaps.push(new Item(mapping.maps[i]))
|
||||
let items = this.items.slice(0, start).append(newMaps).append(rebasedItems)
|
||||
let branch = new Branch(items, eventCount)
|
||||
|
||||
if (branch.emptyItemCount() > max_empty_items)
|
||||
branch = branch.compress(this.items.length - rebasedItems.length)
|
||||
return branch
|
||||
}
|
||||
|
||||
emptyItemCount() {
|
||||
let count = 0
|
||||
this.items.forEach(item => { if (!item.step) count++ })
|
||||
return count
|
||||
}
|
||||
|
||||
// Compressing a branch means rewriting it to push the air (map-only
|
||||
// items) out. During collaboration, these naturally accumulate
|
||||
// because each remote change adds one. The `upto` argument is used
|
||||
// to ensure that only the items below a given level are compressed,
|
||||
// because `rebased` relies on a clean, untouched set of items in
|
||||
// order to associate old items with rebased steps.
|
||||
compress(upto = this.items.length) {
|
||||
let remap = this.remapping(0, upto), mapFrom = remap.maps.length
|
||||
let items: Item[] = [], events = 0
|
||||
this.items.forEach((item, i) => {
|
||||
if (i >= upto) {
|
||||
items.push(item)
|
||||
if (item.selection) events++
|
||||
} else if (item.step) {
|
||||
let step = item.step.map(remap.slice(mapFrom)), map = step && step.getMap()
|
||||
mapFrom--
|
||||
if (map) remap.appendMap(map, mapFrom)
|
||||
if (step) {
|
||||
let selection = item.selection && item.selection.map(remap.slice(mapFrom))
|
||||
if (selection) events++
|
||||
let newItem = new Item(map!.invert(), step, selection), merged, last = items.length - 1
|
||||
if (merged = items.length && items[last].merge(newItem))
|
||||
items[last] = merged
|
||||
else
|
||||
items.push(newItem)
|
||||
}
|
||||
} else if (item.map) {
|
||||
mapFrom--
|
||||
}
|
||||
}, this.items.length, 0)
|
||||
return new Branch(RopeSequence.from(items.reverse()), events)
|
||||
}
|
||||
|
||||
static empty = new Branch(RopeSequence.empty, 0)
|
||||
}
|
||||
|
||||
function cutOffEvents(items: RopeSequence<Item>, n: number) {
|
||||
let cutPoint: number | undefined
|
||||
items.forEach((item, i) => {
|
||||
if (item.selection && (n-- == 0)) {
|
||||
cutPoint = i
|
||||
return false
|
||||
}
|
||||
})
|
||||
return items.slice(cutPoint!)
|
||||
}
|
||||
|
||||
class Item {
|
||||
constructor(
|
||||
// The (forward) step map for this item.
|
||||
readonly map: StepMap,
|
||||
// The inverted step
|
||||
readonly step?: Step,
|
||||
// If this is non-null, this item is the start of a group, and
|
||||
// this selection is the starting selection for the group (the one
|
||||
// that was active before the first step was applied)
|
||||
readonly selection?: SelectionBookmark,
|
||||
// If this item is the inverse of a previous mapping on the stack,
|
||||
// this points at the inverse's offset
|
||||
readonly mirrorOffset?: number
|
||||
) {}
|
||||
|
||||
merge(other: Item) {
|
||||
if (this.step && other.step && !other.selection) {
|
||||
let step = other.step.merge(this.step)
|
||||
if (step) return new Item(step.getMap().invert(), step, this.selection)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The value of the state field that tracks undo/redo history for that
|
||||
// state. Will be stored in the plugin state when the history plugin
|
||||
// is active.
|
||||
class HistoryState {
|
||||
constructor(
|
||||
readonly done: Branch,
|
||||
readonly undone: Branch,
|
||||
readonly prevRanges: readonly number[] | null,
|
||||
readonly prevTime: number,
|
||||
readonly prevComposition: number
|
||||
) {}
|
||||
}
|
||||
|
||||
const DEPTH_OVERFLOW = 20
|
||||
|
||||
// Record a transformation in undo history.
|
||||
function applyTransaction(history: HistoryState, state: EditorState, tr: Transaction, options: Required<HistoryOptions>) {
|
||||
let historyTr = tr.getMeta(historyKey), rebased
|
||||
if (historyTr) return historyTr.historyState
|
||||
|
||||
if (tr.getMeta(closeHistoryKey)) history = new HistoryState(history.done, history.undone, null, 0, -1)
|
||||
|
||||
let appended = tr.getMeta("appendedTransaction")
|
||||
|
||||
if (tr.steps.length == 0) {
|
||||
return history
|
||||
} else if (appended && appended.getMeta(historyKey)) {
|
||||
if (appended.getMeta(historyKey).redo)
|
||||
return new HistoryState(history.done.addTransform(tr, undefined, options, mustPreserveItems(state)),
|
||||
history.undone, rangesFor(tr.mapping.maps),
|
||||
history.prevTime, history.prevComposition)
|
||||
else
|
||||
return new HistoryState(history.done, history.undone.addTransform(tr, undefined, options, mustPreserveItems(state)),
|
||||
null, history.prevTime, history.prevComposition)
|
||||
} else if (tr.getMeta("addToHistory") !== false && !(appended && appended.getMeta("addToHistory") === false)) {
|
||||
// Group transforms that occur in quick succession into one event.
|
||||
let composition = tr.getMeta("composition")
|
||||
let newGroup = history.prevTime == 0 ||
|
||||
(!appended && history.prevComposition != composition &&
|
||||
(history.prevTime < (tr.time || 0) - options.newGroupDelay || !isAdjacentTo(tr, history.prevRanges!)))
|
||||
let prevRanges = appended ? mapRanges(history.prevRanges!, tr.mapping) : rangesFor(tr.mapping.maps)
|
||||
return new HistoryState(history.done.addTransform(tr, newGroup ? state.selection.getBookmark() : undefined,
|
||||
options, mustPreserveItems(state)),
|
||||
Branch.empty, prevRanges, tr.time, composition == null ? history.prevComposition : composition)
|
||||
} else if (rebased = tr.getMeta("rebased")) {
|
||||
// Used by the collab module to tell the history that some of its
|
||||
// content has been rebased.
|
||||
return new HistoryState(history.done.rebased(tr, rebased),
|
||||
history.undone.rebased(tr, rebased),
|
||||
mapRanges(history.prevRanges!, tr.mapping), history.prevTime, history.prevComposition)
|
||||
} else {
|
||||
return new HistoryState(history.done.addMaps(tr.mapping.maps),
|
||||
history.undone.addMaps(tr.mapping.maps),
|
||||
mapRanges(history.prevRanges!, tr.mapping), history.prevTime, history.prevComposition)
|
||||
}
|
||||
}
|
||||
|
||||
function isAdjacentTo(transform: Transform, prevRanges: readonly number[]) {
|
||||
if (!prevRanges) return false
|
||||
if (!transform.docChanged) return true
|
||||
let adjacent = false
|
||||
transform.mapping.maps[0].forEach((start, end) => {
|
||||
for (let i = 0; i < prevRanges.length; i += 2)
|
||||
if (start <= prevRanges[i + 1] && end >= prevRanges[i])
|
||||
adjacent = true
|
||||
})
|
||||
return adjacent
|
||||
}
|
||||
|
||||
function rangesFor(maps: readonly StepMap[]) {
|
||||
let result: number[] = []
|
||||
for (let i = maps.length - 1; i >= 0 && result.length == 0; i--)
|
||||
maps[i].forEach((_from, _to, from, to) => result.push(from, to))
|
||||
return result
|
||||
}
|
||||
|
||||
function mapRanges(ranges: readonly number[], mapping: Mapping) {
|
||||
if (!ranges) return null
|
||||
let result = []
|
||||
for (let i = 0; i < ranges.length; i += 2) {
|
||||
let from = mapping.map(ranges[i], 1), to = mapping.map(ranges[i + 1], -1)
|
||||
if (from <= to) result.push(from, to)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// Apply the latest event from one branch to the document and shift the event
|
||||
// onto the other branch.
|
||||
function histTransaction(history: HistoryState, state: EditorState, redo: boolean): Transaction | null {
|
||||
let preserveItems = mustPreserveItems(state)
|
||||
let histOptions = (historyKey.get(state)!.spec as any).config as Required<HistoryOptions>
|
||||
let pop = (redo ? history.undone : history.done).popEvent(state, preserveItems)
|
||||
if (!pop) return null
|
||||
|
||||
let selection = pop.selection!.resolve(pop.transform.doc)
|
||||
let added = (redo ? history.done : history.undone).addTransform(pop.transform, state.selection.getBookmark(),
|
||||
histOptions, preserveItems)
|
||||
|
||||
let newHist = new HistoryState(redo ? added : pop.remaining, redo ? pop.remaining : added, null, 0, -1)
|
||||
return pop.transform.setSelection(selection).setMeta(historyKey, {redo, historyState: newHist})
|
||||
}
|
||||
|
||||
let cachedPreserveItems = false, cachedPreserveItemsPlugins: readonly Plugin[] | null = null
|
||||
// Check whether any plugin in the given state has a
|
||||
// `historyPreserveItems` property in its spec, in which case we must
|
||||
// preserve steps exactly as they came in, so that they can be
|
||||
// rebased.
|
||||
function mustPreserveItems(state: EditorState) {
|
||||
let plugins = state.plugins
|
||||
if (cachedPreserveItemsPlugins != plugins) {
|
||||
cachedPreserveItems = false
|
||||
cachedPreserveItemsPlugins = plugins
|
||||
for (let i = 0; i < plugins.length; i++) if ((plugins[i].spec as any).historyPreserveItems) {
|
||||
cachedPreserveItems = true
|
||||
break
|
||||
}
|
||||
}
|
||||
return cachedPreserveItems
|
||||
}
|
||||
|
||||
/// Set a flag on the given transaction that will prevent further steps
|
||||
/// from being appended to an existing history event (so that they
|
||||
/// require a separate undo command to undo).
|
||||
export function closeHistory(tr: Transaction) {
|
||||
return tr.setMeta(closeHistoryKey, true)
|
||||
}
|
||||
|
||||
const historyKey = new PluginKey("history")
|
||||
const closeHistoryKey = new PluginKey("closeHistory")
|
||||
|
||||
interface HistoryOptions {
|
||||
/// The amount of history events that are collected before the
|
||||
/// oldest events are discarded. Defaults to 100.
|
||||
depth?: number
|
||||
|
||||
/// The delay between changes after which a new group should be
|
||||
/// started. Defaults to 500 (milliseconds). Note that when changes
|
||||
/// aren't adjacent, a new group is always started.
|
||||
newGroupDelay?: number
|
||||
}
|
||||
|
||||
/// Returns a plugin that enables the undo history for an editor. The
|
||||
/// plugin will track undo and redo stacks, which can be used with the
|
||||
/// [`undo`](#history.undo) and [`redo`](#history.redo) commands.
|
||||
///
|
||||
/// You can set an `"addToHistory"` [metadata
|
||||
/// property](#state.Transaction.setMeta) of `false` on a transaction
|
||||
/// to prevent it from being rolled back by undo.
|
||||
export function history(config: HistoryOptions = {}): Plugin {
|
||||
config = {depth: config.depth || 100,
|
||||
newGroupDelay: config.newGroupDelay || 500}
|
||||
|
||||
return new Plugin({
|
||||
key: historyKey,
|
||||
|
||||
state: {
|
||||
init() {
|
||||
return new HistoryState(Branch.empty, Branch.empty, null, 0, -1)
|
||||
},
|
||||
apply(tr, hist, state) {
|
||||
return applyTransaction(hist, state, tr, config as Required<HistoryOptions>)
|
||||
}
|
||||
},
|
||||
|
||||
config,
|
||||
|
||||
props: {
|
||||
handleDOMEvents: {
|
||||
beforeinput(view, e: Event) {
|
||||
let inputType = (e as InputEvent).inputType
|
||||
let command = inputType == "historyUndo" ? undo : inputType == "historyRedo" ? redo : null
|
||||
if (!command || !view.editable) return false
|
||||
e.preventDefault()
|
||||
return command(view.state, view.dispatch)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function buildCommand(redo: boolean, scroll: boolean): Command {
|
||||
return (state, dispatch) => {
|
||||
let hist = historyKey.getState(state)
|
||||
if (!hist || (redo ? hist.undone : hist.done).eventCount == 0) return false
|
||||
if (dispatch) {
|
||||
let tr = histTransaction(hist, state, redo)
|
||||
if (tr) dispatch(scroll ? tr.scrollIntoView() : tr)
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
/// A command function that undoes the last change, if any.
|
||||
export const undo = buildCommand(false, true)
|
||||
|
||||
/// A command function that redoes the last undone change, if any.
|
||||
export const redo = buildCommand(true, true)
|
||||
|
||||
/// A command function that undoes the last change. Don't scroll the
|
||||
/// selection into view.
|
||||
export const undoNoScroll = buildCommand(false, false)
|
||||
|
||||
/// A command function that redoes the last undone change. Don't
|
||||
/// scroll the selection into view.
|
||||
export const redoNoScroll = buildCommand(true, false)
|
||||
|
||||
/// The amount of undoable events available in a given state.
|
||||
export function undoDepth(state: EditorState) {
|
||||
let hist = historyKey.getState(state)
|
||||
return hist ? hist.done.eventCount : 0
|
||||
}
|
||||
|
||||
/// The amount of redoable events available in a given editor state.
|
||||
export function redoDepth(state: EditorState) {
|
||||
let hist = historyKey.getState(state)
|
||||
return hist ? hist.undone.eventCount : 0
|
||||
}
|
||||
|
||||
/// Returns true if the given transaction was generated by the history
|
||||
/// plugin.
|
||||
export function isHistoryTransaction(tr: Transaction) {
|
||||
return tr.getMeta(historyKey) != null
|
||||
}
|
||||
Reference in New Issue
Block a user