Compare commits

...

10 Commits

Author SHA1 Message Date
Kevin Jahns
fc4d6165b4 13.0.0-96 2019-08-20 22:29:56 +02:00
Kevin Jahns
251c8aaefc UndoManager configuration to filter deletes 2019-08-20 22:28:49 +02:00
Kevin Jahns
1337d38ada 13.0.0-95 2019-08-09 01:18:15 +02:00
Kevin Jahns
f5c66e41cb audit 2019-08-09 01:16:40 +02:00
Kevin Jahns
0e7da017fe Use lib0/any-encoding instead of JSON 2019-08-09 01:15:46 +02:00
Kevin Jahns
36203af88e should not rely on all deconstructing features because not all parsers support it 2019-06-29 14:47:34 +02:00
Kevin Jahns
dd2b8bc6c7 13.0.0-94 2019-06-25 11:57:50 +02:00
Kevin Jahns
463065ac21 UndoManager: keep item before item is deleted (fixes some edge cases of followRedo) 2019-06-25 11:56:41 +02:00
Kevin Jahns
d064e6e96e UndoManager accepts an array of types as scope. Implements #156 2019-06-25 02:26:18 +02:00
Kevin Jahns
b1ed2df208 proper TOC links 2019-06-25 00:10:12 +02:00
11 changed files with 276 additions and 112 deletions

View File

@@ -25,10 +25,10 @@ suited for even large documents.
* [Getting Started](#Getting-Started) * [Getting Started](#Getting-Started)
* [API](#API) * [API](#API)
* [Shared Types](#Shared-Types) * [Shared Types](#Shared-Types)
* [Y.Doc](#Y.Doc) * [Y.Doc](#YDoc)
* [Document Updates](#Document-Updates) * [Document Updates](#Document-Updates)
* [Relative Positions](#Relative-Positions) * [Relative Positions](#Relative-Positions)
* [Y.UndoManager](#Y.UndoManager) * [Y.UndoManager](#YUndoManager)
* [Miscellaneous](#Miscellaneous) * [Miscellaneous](#Miscellaneous)
* [Typescript Declarations](#Typescript-Declarations) * [Typescript Declarations](#Typescript-Declarations)
* [Yjs CRDT Algorithm](#Yjs-CRDT-Algorithm) * [Yjs CRDT Algorithm](#Yjs-CRDT-Algorithm)
@@ -661,9 +661,9 @@ ytext.toString() // => 'abc'
``` ```
<dl> <dl>
<b><code>constructor(type:Y.AbstractType, <b><code>constructor(scope:Y.AbstractType|Array&lt;Y.AbstractType&gt;,
[trackedTransactionOrigins:Set&lt;any&gt;, [{captureTimeout: number}]])</code></b> [[{captureTimeout:number,trackedOrigins:Set&lt;any&gt;,deleteFilter:function(item):boolean}]])</code></b>
<dd></dd> <dd>Accepts either single type as scope or an array of types.</dd>
<b><code>undo()</code></b> <b><code>undo()</code></b>
<dd></dd> <dd></dd>
<b><code>redo()</code></b> <b><code>redo()</code></b>

123
package-lock.json generated
View File

@@ -1,13 +1,13 @@
{ {
"name": "yjs", "name": "yjs",
"version": "13.0.0-93", "version": "13.0.0-96",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {
"@babel/parser": { "@babel/parser": {
"version": "7.4.5", "version": "7.5.5",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.4.5.tgz", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.5.5.tgz",
"integrity": "sha512-9mUqkL1FF5T7f0WDFfAoDdiMVPWsdD1gZYzSnaXsxUCUqzuch/8of9G3VUSNiZmMBoRxT3neyVsqeiL/ZPcjew==", "integrity": "sha512-E5BN68cqR7dhKan1SfqgPGhQ178bkVKpXTPEXnFJBrEt8/DKRZlybmy+IgYLTeN7tp1R5Ccmbm2rBk17sHYU3g==",
"dev": true "dev": true
}, },
"@types/estree": { "@types/estree": {
@@ -431,12 +431,12 @@
"dev": true "dev": true
}, },
"catharsis": { "catharsis": {
"version": "0.8.10", "version": "0.8.11",
"resolved": "https://registry.npmjs.org/catharsis/-/catharsis-0.8.10.tgz", "resolved": "https://registry.npmjs.org/catharsis/-/catharsis-0.8.11.tgz",
"integrity": "sha512-l2OUaz/3PU3MZylspVFJvwHCVfWyvcduPq4lv3AzZ2pJzZCo7kNKFNyatwujD7XgvGkNAE/Jhhbh2uARNwNkfw==", "integrity": "sha512-a+xUyMV7hD1BrDQA/3iPV7oc+6W26BgVJO05PGEoatMyIuPScQKsde6i3YorWX1qs+AZjnJ18NqdKoCtKiNh1g==",
"dev": true, "dev": true,
"requires": { "requires": {
"lodash": "^4.17.11" "lodash": "^4.17.14"
} }
}, },
"chalk": { "chalk": {
@@ -2514,22 +2514,22 @@
} }
}, },
"jsdoc": { "jsdoc": {
"version": "3.6.2", "version": "3.6.3",
"resolved": "https://registry.npmjs.org/jsdoc/-/jsdoc-3.6.2.tgz", "resolved": "https://registry.npmjs.org/jsdoc/-/jsdoc-3.6.3.tgz",
"integrity": "sha512-S2vzg99C5+gb7FWlrK4TVdyzVPGGkdvpDkCEJH1JABi2PKzPeLu5/zZffcJUifgWUJqXWl41Hoc+MmuM2GukIg==", "integrity": "sha512-Yf1ZKA3r9nvtMWHO1kEuMZTlHOF8uoQ0vyo5eH7SQy5YeIiHM+B0DgKnn+X6y6KDYZcF7G2SPkKF+JORCXWE/A==",
"dev": true, "dev": true,
"requires": { "requires": {
"@babel/parser": "^7.4.4", "@babel/parser": "^7.4.4",
"bluebird": "^3.5.4", "bluebird": "^3.5.4",
"catharsis": "^0.8.10", "catharsis": "^0.8.11",
"escape-string-regexp": "^2.0.0", "escape-string-regexp": "^2.0.0",
"js2xmlparser": "^4.0.0", "js2xmlparser": "^4.0.0",
"klaw": "^3.0.0", "klaw": "^3.0.0",
"markdown-it": "^8.4.2", "markdown-it": "^8.4.2",
"markdown-it-anchor": "^5.0.2", "markdown-it-anchor": "^5.0.2",
"marked": "^0.6.2", "marked": "^0.7.0",
"mkdirp": "^0.5.1", "mkdirp": "^0.5.1",
"requizzle": "^0.2.2", "requizzle": "^0.2.3",
"strip-json-comments": "^3.0.1", "strip-json-comments": "^3.0.1",
"taffydb": "2.6.2", "taffydb": "2.6.2",
"underscore": "~1.9.1" "underscore": "~1.9.1"
@@ -2596,14 +2596,14 @@
} }
}, },
"lib0": { "lib0": {
"version": "0.0.5", "version": "0.0.6",
"resolved": "https://registry.npmjs.org/lib0/-/lib0-0.0.5.tgz", "resolved": "https://registry.npmjs.org/lib0/-/lib0-0.0.6.tgz",
"integrity": "sha512-3ElV6/t5Lv0Eczlnh/05q+Uq3RxQ/Q0zdN6LVtaUERQIDDZsP/CUXEGLsV8KZTgZwVFNCPGXNWYE+3WTOo+SHw==" "integrity": "sha512-drb8LcwZu2rAmTsXN0d3hFtZVbPE5ZUrsWf307Boc/v7IrmLq3lM5+OOMY672EysHTWeXo/OH54wRHyD6eFXXw=="
}, },
"linkify-it": { "linkify-it": {
"version": "2.1.0", "version": "2.2.0",
"resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-2.1.0.tgz", "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-2.2.0.tgz",
"integrity": "sha512-4REs8/062kV2DSHxNfq5183zrqXMl7WP0WzABH9IeJI+NLm429FgE1PDecltYfnOoFDFlZGh2T8PfZn0r+GTRg==", "integrity": "sha512-GnAl/knGn+i1U/wjBz3akz2stz+HrHLsxMwHQGofCDfPvlf+gDKN58UtfmUquTY4/MXeE2x7k19KQmeoZi94Iw==",
"dev": true, "dev": true,
"requires": { "requires": {
"uc.micro": "^1.0.1" "uc.micro": "^1.0.1"
@@ -2653,9 +2653,9 @@
} }
}, },
"lodash": { "lodash": {
"version": "4.17.11", "version": "4.17.15",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz",
"integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==", "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==",
"dev": true "dev": true
}, },
"lodash.assignin": { "lodash.assignin": {
@@ -2701,9 +2701,9 @@
"dev": true "dev": true
}, },
"lodash.merge": { "lodash.merge": {
"version": "4.6.1", "version": "4.6.2",
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.1.tgz", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
"integrity": "sha512-AOYza4+Hf5z1/0Hztxpm2/xiPZgi/cjMqdnKTUWTBSKchJlxXXuUSxCCl8rJlf4g6yww/j6mA8nC8Hw/EZWxKQ==", "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
"dev": true "dev": true
}, },
"lodash.pick": { "lodash.pick": {
@@ -2784,15 +2784,15 @@
} }
}, },
"markdown-it-anchor": { "markdown-it-anchor": {
"version": "5.1.0", "version": "5.2.4",
"resolved": "https://registry.npmjs.org/markdown-it-anchor/-/markdown-it-anchor-5.1.0.tgz", "resolved": "https://registry.npmjs.org/markdown-it-anchor/-/markdown-it-anchor-5.2.4.tgz",
"integrity": "sha512-wJOmyXzDUxI8iuowEsaQAKMQBButhSw8j64SpgcaL75QZYC/OSZV66Fnr50lfMLYNGtV0rJdw2fmLwXCT6T+bw==", "integrity": "sha512-n8zCGjxA3T+Mx1pG8HEgbJbkB8JFUuRkeTZQuIM8iPY6oQ8sWOPRZJDFC9a/pNg2QkHEjjGkhBEl/RSyzaDZ3A==",
"dev": true "dev": true
}, },
"marked": { "marked": {
"version": "0.6.2", "version": "0.7.0",
"resolved": "https://registry.npmjs.org/marked/-/marked-0.6.2.tgz", "resolved": "https://registry.npmjs.org/marked/-/marked-0.7.0.tgz",
"integrity": "sha512-LqxwVH3P/rqKX4EKGz7+c2G9r98WeM/SW34ybhgNGhUQNKtf1GmmSkJ6cDGJ/t6tiyae49qRkpyTw2B9HOrgUA==", "integrity": "sha512-c+yYdCZJQrsRjTPhUx7VKkApw9bwDkNbHUKo1ovgcfDjb2kc8rLuRbIFyXL5WOEUwzSSKo3IXpph2K6DqB/KZg==",
"dev": true "dev": true
}, },
"mdurl": { "mdurl": {
@@ -2865,9 +2865,9 @@
"dev": true "dev": true
}, },
"mixin-deep": { "mixin-deep": {
"version": "1.3.1", "version": "1.3.2",
"resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.1.tgz", "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz",
"integrity": "sha512-8ZItLHeEgaqEvd5lYBXfm4EZSFCX29Jb9K+lAHhDKzReKBQKj3R+7NOF6tjqYi9t4oI8VUfaWITJQm86wnXGNQ==", "integrity": "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==",
"dev": true, "dev": true,
"requires": { "requires": {
"for-in": "^1.0.2", "for-in": "^1.0.2",
@@ -3437,12 +3437,12 @@
} }
}, },
"requizzle": { "requizzle": {
"version": "0.2.2", "version": "0.2.3",
"resolved": "https://registry.npmjs.org/requizzle/-/requizzle-0.2.2.tgz", "resolved": "https://registry.npmjs.org/requizzle/-/requizzle-0.2.3.tgz",
"integrity": "sha512-oJ6y7JcUJkblRGhMByGNcszeLgU0qDxNKFCiUZR1XyzHyVsev+Mxb1tyygxLd1ORsKee1SA5BInFdUwY64GE/A==", "integrity": "sha512-YanoyJjykPxGHii0fZP0uUPEXpvqfBDxWV7s6GKAiiOsiqhX6vHNyW3Qzdmqp/iq/ExbhaGbVrjB4ruEVSM4GQ==",
"dev": true, "dev": true,
"requires": { "requires": {
"lodash": "^4.17.11" "lodash": "^4.17.14"
} }
}, },
"resolve": { "resolve": {
@@ -3648,9 +3648,9 @@
} }
}, },
"set-value": { "set-value": {
"version": "2.0.0", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.0.tgz", "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz",
"integrity": "sha512-hw0yxk9GT/Hr5yJEYnHNKYXkIA8mVJgd9ditYZCe16ZczcaELYYcfvaXesNACk2O8O0nTiPQcQhGUQj8JLzeeg==", "integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==",
"dev": true, "dev": true,
"requires": { "requires": {
"extend-shallow": "^2.0.1", "extend-shallow": "^2.0.1",
@@ -4176,38 +4176,15 @@
"dev": true "dev": true
}, },
"union-value": { "union-value": {
"version": "1.0.0", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.0.tgz", "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz",
"integrity": "sha1-XHHDTLW61dzr4+oM0IIHulqhrqQ=", "integrity": "sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==",
"dev": true, "dev": true,
"requires": { "requires": {
"arr-union": "^3.1.0", "arr-union": "^3.1.0",
"get-value": "^2.0.6", "get-value": "^2.0.6",
"is-extendable": "^0.1.1", "is-extendable": "^0.1.1",
"set-value": "^0.4.3" "set-value": "^2.0.1"
},
"dependencies": {
"extend-shallow": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
"integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
"dev": true,
"requires": {
"is-extendable": "^0.1.0"
}
},
"set-value": {
"version": "0.4.3",
"resolved": "https://registry.npmjs.org/set-value/-/set-value-0.4.3.tgz",
"integrity": "sha1-fbCPnT0i3H945Trzw79GZuzfzPE=",
"dev": true,
"requires": {
"extend-shallow": "^2.0.1",
"is-extendable": "^0.1.1",
"is-plain-object": "^2.0.1",
"to-object-path": "^0.3.0"
}
}
} }
}, },
"uniq": { "uniq": {
@@ -4385,6 +4362,14 @@
"dev": true, "dev": true,
"requires": { "requires": {
"lib0": "0.0.5" "lib0": "0.0.5"
},
"dependencies": {
"lib0": {
"version": "0.0.5",
"resolved": "https://registry.npmjs.org/lib0/-/lib0-0.0.5.tgz",
"integrity": "sha512-3ElV6/t5Lv0Eczlnh/05q+Uq3RxQ/Q0zdN6LVtaUERQIDDZsP/CUXEGLsV8KZTgZwVFNCPGXNWYE+3WTOo+SHw==",
"dev": true
}
} }
}, },
"yallist": { "yallist": {

View File

@@ -1,6 +1,6 @@
{ {
"name": "yjs", "name": "yjs",
"version": "13.0.0-93", "version": "13.0.0-96",
"description": "Shared Editing Library", "description": "Shared Editing Library",
"main": "./dist/yjs.js", "main": "./dist/yjs.js",
"module": "./dist/yjs.mjs", "module": "./dist/yjs.mjs",
@@ -51,11 +51,11 @@
}, },
"homepage": "http://y-js.org", "homepage": "http://y-js.org",
"dependencies": { "dependencies": {
"lib0": "0.0.5" "lib0": "0.0.6"
}, },
"devDependencies": { "devDependencies": {
"concurrently": "^3.6.1", "concurrently": "^3.6.1",
"jsdoc": "^3.6.2", "jsdoc": "^3.6.3",
"live-server": "^1.2.1", "live-server": "^1.2.1",
"rollup": "^1.11.3", "rollup": "^1.11.3",
"rollup-cli": "^1.0.9", "rollup-cli": "^1.0.9",

View File

@@ -21,6 +21,7 @@ export {
ContentEmbed, ContentEmbed,
ContentFormat, ContentFormat,
ContentJSON, ContentJSON,
ContentAny,
ContentString, ContentString,
ContentType, ContentType,
AbstractType, AbstractType,

View File

@@ -27,6 +27,7 @@ export * from './structs/ContentDeleted.js'
export * from './structs/ContentEmbed.js' export * from './structs/ContentEmbed.js'
export * from './structs/ContentFormat.js' export * from './structs/ContentFormat.js'
export * from './structs/ContentJSON.js' export * from './structs/ContentJSON.js'
export * from './structs/ContentAny.js'
export * from './structs/ContentString.js' export * from './structs/ContentString.js'
export * from './structs/ContentType.js' export * from './structs/ContentType.js'
export * from './structs/Item.js' export * from './structs/Item.js'

108
src/structs/ContentAny.js Normal file
View File

@@ -0,0 +1,108 @@
import {
Transaction, Item, StructStore // eslint-disable-line
} from '../internals.js'
import * as encoding from 'lib0/encoding.js'
import * as decoding from 'lib0/decoding.js'
/**
* @private
*/
export class ContentAny {
/**
* @param {Array<any>} arr
*/
constructor (arr) {
/**
* @type {Array<any>}
*/
this.arr = arr
}
/**
* @return {number}
*/
getLength () {
return this.arr.length
}
/**
* @return {Array<any>}
*/
getContent () {
return this.arr
}
/**
* @return {boolean}
*/
isCountable () {
return true
}
/**
* @return {ContentAny}
*/
copy () {
return new ContentAny(this.arr)
}
/**
* @param {number} offset
* @return {ContentAny}
*/
splice (offset) {
const right = new ContentAny(this.arr.slice(offset))
this.arr = this.arr.slice(0, offset)
return right
}
/**
* @param {ContentAny} right
* @return {boolean}
*/
mergeWith (right) {
this.arr = this.arr.concat(right.arr)
return true
}
/**
* @param {Transaction} transaction
* @param {Item} item
*/
integrate (transaction, item) {}
/**
* @param {Transaction} transaction
*/
delete (transaction) {}
/**
* @param {StructStore} store
*/
gc (store) {}
/**
* @param {encoding.Encoder} encoder
* @param {number} offset
*/
write (encoder, offset) {
const len = this.arr.length
encoding.writeVarUint(encoder, len - offset)
for (let i = offset; i < len; i++) {
const c = this.arr[i]
encoding.writeAny(encoder, c)
}
}
/**
* @return {number}
*/
getRef () {
return 8
}
}
/**
* @private
*
* @param {decoding.Decoder} decoder
* @return {ContentAny}
*/
export const readContentAny = decoder => {
const len = decoding.readVarUint(decoder)
const cs = []
for (let i = 0; i < len; i++) {
cs.push(decoding.readAny(decoder))
}
return new ContentAny(cs)
}

View File

@@ -18,6 +18,7 @@ import {
readContentDeleted, readContentDeleted,
readContentBinary, readContentBinary,
readContentJSON, readContentJSON,
readContentAny,
readContentString, readContentString,
readContentEmbed, readContentEmbed,
readContentFormat, readContentFormat,
@@ -561,7 +562,8 @@ export const contentRefs = [
readContentString, readContentString,
readContentEmbed, readContentEmbed,
readContentFormat, readContentFormat,
readContentType readContentType,
readContentAny
] ]
/** /**

View File

@@ -7,7 +7,7 @@ import {
nextID, nextID,
isVisible, isVisible,
ContentType, ContentType,
ContentJSON, ContentAny,
ContentBinary, ContentBinary,
createID, createID,
getItemCleanStart, getItemCleanStart,
@@ -374,7 +374,7 @@ export const typeListInsertGenericsAfter = (transaction, parent, referenceItem,
let jsonContent = [] let jsonContent = []
const packJsonContent = () => { const packJsonContent = () => {
if (jsonContent.length > 0) { if (jsonContent.length > 0) {
left = new Item(nextID(transaction), left, left === null ? null : left.lastId, right, right === null ? null : right.id, parent, null, new ContentJSON(jsonContent)) left = new Item(nextID(transaction), left, left === null ? null : left.lastId, right, right === null ? null : right.id, parent, null, new ContentAny(jsonContent))
left.integrate(transaction) left.integrate(transaction)
jsonContent = [] jsonContent = []
} }
@@ -503,7 +503,7 @@ export const typeMapSet = (transaction, parent, key, value) => {
const left = parent._map.get(key) || null const left = parent._map.get(key) || null
let content let content
if (value == null) { if (value == null) {
content = new ContentJSON([value]) content = new ContentAny([value])
} else { } else {
switch (value.constructor) { switch (value.constructor) {
case Number: case Number:
@@ -511,7 +511,7 @@ export const typeMapSet = (transaction, parent, key, value) => {
case Boolean: case Boolean:
case Array: case Array:
case String: case String:
content = new ContentJSON([value]) content = new ContentAny([value])
break break
case Uint8Array: case Uint8Array:
content = new ContentBinary(value) content = new ContentBinary(value)

View File

@@ -9,7 +9,7 @@ import {
createID, createID,
followRedone, followRedone,
getItemCleanStart, getItemCleanStart,
Doc, Item, GC, DeleteSet, AbstractType // eslint-disable-line Transaction, Doc, Item, GC, DeleteSet, AbstractType // eslint-disable-line
} from '../internals.js' } from '../internals.js'
import * as time from 'lib0/time.js' import * as time from 'lib0/time.js'
@@ -45,7 +45,7 @@ const popStackItem = (undoManager, stack, eventType) => {
*/ */
let result = null let result = null
const doc = undoManager.doc const doc = undoManager.doc
const type = undoManager.type const scope = undoManager.scope
transact(doc, transaction => { transact(doc, transaction => {
while (stack.length > 0 && result === null) { while (stack.length > 0 && result === null) {
const store = doc.store const store = doc.store
@@ -53,7 +53,7 @@ const popStackItem = (undoManager, stack, eventType) => {
const itemsToRedo = new Set() const itemsToRedo = new Set()
let performedChange = false let performedChange = false
iterateDeletedStructs(transaction, stackItem.ds, store, struct => { iterateDeletedStructs(transaction, stackItem.ds, store, struct => {
if (struct instanceof Item && isParentOf(type, struct)) { if (struct instanceof Item && scope.some(type => isParentOf(type, struct))) {
itemsToRedo.add(struct) itemsToRedo.add(struct)
} }
}) })
@@ -61,22 +61,34 @@ const popStackItem = (undoManager, stack, eventType) => {
performedChange = redoItem(transaction, item, itemsToRedo) !== null || performedChange performedChange = redoItem(transaction, item, itemsToRedo) !== null || performedChange
}) })
const structs = /** @type {Array<GC|Item>} */ (store.clients.get(doc.clientID)) const structs = /** @type {Array<GC|Item>} */ (store.clients.get(doc.clientID))
/**
* @type {Array<Item>}
*/
const itemsToDelete = []
iterateStructs(transaction, structs, stackItem.start, stackItem.len, struct => { iterateStructs(transaction, structs, stackItem.start, stackItem.len, struct => {
if (struct instanceof Item && struct.redone !== null) { if (struct instanceof Item && !struct.deleted && scope.some(type => isParentOf(type, /** @type {Item} */ (struct)))) {
let { item, diff } = followRedone(store, struct.id) if (struct.redone !== null) {
if (diff > 0) { let { item, diff } = followRedone(store, struct.id)
item = getItemCleanStart(transaction, store, struct.id) if (diff > 0) {
item = getItemCleanStart(transaction, store, createID(item.id.client, item.id.clock + diff))
}
if (item.length > stackItem.len) {
getItemCleanStart(transaction, store, createID(item.id.client, item.id.clock + stackItem.len))
}
struct = item
} }
if (item.length > stackItem.len) { itemsToDelete.push(struct)
getItemCleanStart(transaction, store, createID(item.id.client, item.id.clock + stackItem.len))
}
struct = item
}
if (!struct.deleted && isParentOf(type, /** @type {Item} */ (struct))) {
struct.delete(transaction)
performedChange = true
} }
}) })
// We want to delete in reverse order so that children are deleted before
// parents, so we have more information available when items are filtered.
for (let i = itemsToDelete.length - 1; i >= 0; i--) {
const item = itemsToDelete[i]
if (undoManager.deleteFilter(item)) {
item.delete(transaction)
performedChange = true
}
}
result = stackItem result = stackItem
if (result != null) { if (result != null) {
undoManager.emit('stack-item-popped', [{ stackItem: result, type: eventType }, undoManager]) undoManager.emit('stack-item-popped', [{ stackItem: result, type: eventType }, undoManager])
@@ -86,6 +98,16 @@ const popStackItem = (undoManager, stack, eventType) => {
return result return result
} }
/**
* @typedef {Object} UndoManagerOptions
* @property {number} [UndoManagerOptions.captureTimeout=500]
* @property {function(Item):boolean} [UndoManagerOptions.deleteFilter=()=>true] Sometimes
* it is necessary to filter whan an Undo/Redo operation can delete. If this
* filter returns false, the type/item won't be deleted even it is in the
* undo/redo scope.
* @property {Set<any>} [UndoManagerOptions.trackedOrigins=new Set([null])]
*/
/** /**
* Fires 'stack-item-added' event when a stack item was added to either the undo- or * Fires 'stack-item-added' event when a stack item was added to either the undo- or
* the redo-stack. You may store additional stack information via the * the redo-stack. You may store additional stack information via the
@@ -97,15 +119,18 @@ const popStackItem = (undoManager, stack, eventType) => {
*/ */
export class UndoManager extends Observable { export class UndoManager extends Observable {
/** /**
* @param {AbstractType<any>} type * @param {AbstractType<any>|Array<AbstractType<any>>} typeScope Accepts either a single type, or an array of types
* @param {Set<any>} [trackedTransactionOrigins=new Set([null])] * @param {UndoManagerOptions} options
* @param {object} [options={captureTimeout=500}]
*/ */
constructor (type, trackedTransactionOrigins = new Set([null]), { captureTimeout = 500 } = {}) { constructor (typeScope, { captureTimeout, deleteFilter = () => true, trackedOrigins = new Set([null]) } = {}) {
if (captureTimeout == null) {
captureTimeout = 500
}
super() super()
this.type = type this.scope = typeScope instanceof Array ? typeScope : [typeScope]
trackedTransactionOrigins.add(this) this.deleteFilter = deleteFilter
this.trackedTransactionOrigins = trackedTransactionOrigins trackedOrigins.add(this)
this.trackedOrigins = trackedOrigins
/** /**
* @type {Array<StackItem>} * @type {Array<StackItem>}
*/ */
@@ -121,11 +146,11 @@ export class UndoManager extends Observable {
*/ */
this.undoing = false this.undoing = false
this.redoing = false this.redoing = false
this.doc = /** @type {Doc} */ (type.doc) this.doc = /** @type {Doc} */ (this.scope[0].doc)
this.lastChange = 0 this.lastChange = 0
type.observeDeep((events, transaction) => { this.doc.on('afterTransaction', /** @param {Transaction} transaction */ transaction => {
// Only track certain transactions // Only track certain transactions
if (!this.trackedTransactionOrigins.has(transaction.origin) && (!transaction.origin || !this.trackedTransactionOrigins.has(transaction.origin.constructor))) { if (!this.scope.some(type => transaction.changedParentTypes.has(type)) || (!this.trackedOrigins.has(transaction.origin) && (!transaction.origin || !this.trackedOrigins.has(transaction.origin.constructor)))) {
return return
} }
const undoing = this.undoing const undoing = this.undoing
@@ -154,7 +179,7 @@ export class UndoManager extends Observable {
} }
// make sure that deleted structs are not gc'd // make sure that deleted structs are not gc'd
iterateDeletedStructs(transaction, transaction.deleteSet, transaction.doc.store, /** @param {Item|GC} item */ item => { iterateDeletedStructs(transaction, transaction.deleteSet, transaction.doc.store, /** @param {Item|GC} item */ item => {
if (item instanceof Item && isParentOf(type, item)) { if (item instanceof Item && this.scope.some(type => isParentOf(type, item))) {
keepItem(item) keepItem(item)
} }
}) })

View File

@@ -8,19 +8,21 @@ import {
readContentJSON, readContentJSON,
readContentEmbed, readContentEmbed,
readContentType, readContentType,
readContentFormat readContentFormat,
readContentAny
} from '../src/internals.js' } from '../src/internals.js'
/** /**
* @param {t.TestCase} tc * @param {t.TestCase} tc
*/ */
export const testStructReferences = tc => { export const testStructReferences = tc => {
t.assert(contentRefs.length === 8) t.assert(contentRefs.length === 9)
t.assert(contentRefs[1] === readContentDeleted) t.assert(contentRefs[1] === readContentDeleted)
t.assert(contentRefs[2] === readContentJSON) t.assert(contentRefs[2] === readContentJSON) // TODO: deprecate content json?
t.assert(contentRefs[3] === readContentBinary) t.assert(contentRefs[3] === readContentBinary)
t.assert(contentRefs[4] === readContentString) t.assert(contentRefs[4] === readContentString)
t.assert(contentRefs[5] === readContentEmbed) t.assert(contentRefs[5] === readContentEmbed)
t.assert(contentRefs[6] === readContentFormat) t.assert(contentRefs[6] === readContentFormat)
t.assert(contentRefs[7] === readContentType) t.assert(contentRefs[7] === readContentType)
t.assert(contentRefs[8] === readContentAny)
} }

View File

@@ -172,7 +172,7 @@ export const testUndoEvents = tc => {
export const testTrackClass = tc => { export const testTrackClass = tc => {
const { users, text0 } = init(tc, { users: 3 }) const { users, text0 } = init(tc, { users: 3 })
// only track origins that are numbers // only track origins that are numbers
const undoManager = new UndoManager(text0, new Set([Number])) const undoManager = new UndoManager(text0, { trackedOrigins: new Set([Number]) })
users[0].transact(() => { users[0].transact(() => {
text0.insert(0, 'abc') text0.insert(0, 'abc')
}, 42) }, 42)
@@ -180,3 +180,43 @@ export const testTrackClass = tc => {
undoManager.undo() undoManager.undo()
t.assert(text0.toString() === '') t.assert(text0.toString() === '')
} }
/**
* @param {t.TestCase} tc
*/
export const testTypeScope = tc => {
const { array0 } = init(tc, { users: 3 })
// only track origins that are numbers
const text0 = new Y.Text()
const text1 = new Y.Text()
array0.insert(0, [text0, text1])
const undoManager = new UndoManager(text0)
const undoManagerBoth = new UndoManager([text0, text1])
text1.insert(0, 'abc')
t.assert(undoManager.undoStack.length === 0)
t.assert(undoManagerBoth.undoStack.length === 1)
t.assert(text1.toString() === 'abc')
undoManager.undo()
t.assert(text1.toString() === 'abc')
undoManagerBoth.undo()
t.assert(text1.toString() === '')
}
/**
* @param {t.TestCase} tc
*/
export const testUndoDeleteFilter = tc => {
/**
* @type {Array<Y.Map<any>>}
*/
const array0 = /** @type {any} */ (init(tc, { users: 3 }).array0)
const undoManager = new UndoManager(array0, { deleteFilter: item => !(item instanceof Y.Item) || (item.content instanceof Y.ContentType && item.content.type._map.size === 0) })
const map0 = new Y.Map()
map0.set('hi', 1)
const map1 = new Y.Map()
array0.insert(0, [map0, map1])
undoManager.undo()
t.assert(array0.length === 1)
array0.get(0)
t.assert(Array.from(array0.get(0).keys()).length === 1)
}