Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b792902f17 | ||
|
|
83b7c6839e | ||
|
|
65c4d40a87 | ||
|
|
942c8a267b | ||
|
|
eda085936a | ||
|
|
12be6c006a | ||
|
|
5d862477cd | ||
|
|
c398448152 | ||
|
|
2fbba13246 | ||
|
|
885a740470 | ||
|
|
aedd4c8bf3 | ||
|
|
9563612126 | ||
|
|
ce098d0ac2 | ||
|
|
08801dd406 | ||
|
|
3741f43a11 |
@@ -91,6 +91,7 @@ are implemented in separate modules.
|
|||||||
| [CodeMirror](https://codemirror.net/) | ✔ | [y-codemirror](https://github.com/yjs/y-codemirror) | [demo](https://demos.yjs.dev/codemirror/codemirror.html) |
|
| [CodeMirror](https://codemirror.net/) | ✔ | [y-codemirror](https://github.com/yjs/y-codemirror) | [demo](https://demos.yjs.dev/codemirror/codemirror.html) |
|
||||||
| [Monaco](https://microsoft.github.io/monaco-editor/) | ✔ | [y-monaco](https://github.com/yjs/y-monaco) | [demo](https://demos.yjs.dev/monaco/monaco.html) |
|
| [Monaco](https://microsoft.github.io/monaco-editor/) | ✔ | [y-monaco](https://github.com/yjs/y-monaco) | [demo](https://demos.yjs.dev/monaco/monaco.html) |
|
||||||
| [Slate](https://github.com/ianstormtaylor/slate) | ✔ | [slate-yjs](https://github.com/bitphinix/slate-yjs) | [demo](https://bitphinix.github.io/slate-yjs-example) |
|
| [Slate](https://github.com/ianstormtaylor/slate) | ✔ | [slate-yjs](https://github.com/bitphinix/slate-yjs) | [demo](https://bitphinix.github.io/slate-yjs-example) |
|
||||||
|
| [BlockSuite](https://github.com/toeverything/blocksuite) | ✔ | (native) | [demo](https://blocksuite-toeverything.vercel.app/?init) |
|
||||||
| [valtio](https://github.com/pmndrs/valtio) | | [valtio-yjs](https://github.com/dai-shi/valtio-yjs) | [demo](https://codesandbox.io/s/valtio-yjs-demo-ox3iy) |
|
| [valtio](https://github.com/pmndrs/valtio) | | [valtio-yjs](https://github.com/dai-shi/valtio-yjs) | [demo](https://codesandbox.io/s/valtio-yjs-demo-ox3iy) |
|
||||||
| [immer](https://github.com/immerjs/immer) | | [immer-yjs](https://github.com/sep2/immer-yjs) | [demo](https://codesandbox.io/s/immer-yjs-demo-6e0znb) |
|
| [immer](https://github.com/immerjs/immer) | | [immer-yjs](https://github.com/sep2/immer-yjs) | [demo](https://codesandbox.io/s/immer-yjs-demo-6e0znb) |
|
||||||
| React / Vue / Svelte / MobX | | [SyncedStore](https://syncedstore.org) | [demo](https://syncedstore.org/docs/react) |
|
| React / Vue / Svelte / MobX | | [SyncedStore](https://syncedstore.org) | [demo](https://syncedstore.org/docs/react) |
|
||||||
|
|||||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "yjs",
|
"name": "yjs",
|
||||||
"version": "13.6.2",
|
"version": "13.6.5",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "yjs",
|
"name": "yjs",
|
||||||
"version": "13.6.2",
|
"version": "13.6.5",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"lib0": "^0.2.74"
|
"lib0": "^0.2.74"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "yjs",
|
"name": "yjs",
|
||||||
"version": "13.6.2",
|
"version": "13.6.5",
|
||||||
"description": "Shared Editing Library",
|
"description": "Shared Editing Library",
|
||||||
"main": "./dist/yjs.cjs",
|
"main": "./dist/yjs.cjs",
|
||||||
"module": "./dist/yjs.mjs",
|
"module": "./dist/yjs.mjs",
|
||||||
|
|||||||
@@ -476,6 +476,56 @@ export const cleanupYTextFormatting = type => {
|
|||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This will be called by the transction once the event handlers are called to potentially cleanup
|
||||||
|
* formatting attributes.
|
||||||
|
*
|
||||||
|
* @param {Transaction} transaction
|
||||||
|
*/
|
||||||
|
export const cleanupYTextAfterTransaction = transaction => {
|
||||||
|
/**
|
||||||
|
* @type {Set<YText>}
|
||||||
|
*/
|
||||||
|
const needFullCleanup = new Set()
|
||||||
|
// check if another formatting item was inserted
|
||||||
|
const doc = transaction.doc
|
||||||
|
for (const [client, afterClock] of transaction.afterState.entries()) {
|
||||||
|
const clock = transaction.beforeState.get(client) || 0
|
||||||
|
if (afterClock === clock) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
iterateStructs(transaction, /** @type {Array<Item|GC>} */ (doc.store.clients.get(client)), clock, afterClock, item => {
|
||||||
|
if (
|
||||||
|
!item.deleted && /** @type {Item} */ (item).content.constructor === ContentFormat && item.constructor !== GC
|
||||||
|
) {
|
||||||
|
needFullCleanup.add(/** @type {any} */ (item).parent)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// cleanup in a new transaction
|
||||||
|
transact(doc, (t) => {
|
||||||
|
iterateDeletedStructs(transaction, transaction.deleteSet, item => {
|
||||||
|
if (item instanceof GC || needFullCleanup.has(/** @type {YText} */ (item.parent))) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const parent = /** @type {YText} */ (item.parent)
|
||||||
|
if (item.content.constructor === ContentFormat) {
|
||||||
|
needFullCleanup.add(parent)
|
||||||
|
} else {
|
||||||
|
// If no formatting attribute was inserted or deleted, we can make due with contextless
|
||||||
|
// formatting cleanups.
|
||||||
|
// Contextless: it is not necessary to compute currentAttributes for the affected position.
|
||||||
|
cleanupContextlessFormattingGap(t, item)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// If a formatting item was inserted, we simply clean the whole type.
|
||||||
|
// We need to compute currentAttributes for the current position anyway.
|
||||||
|
for (const yText of needFullCleanup) {
|
||||||
|
cleanupYTextFormatting(yText)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Transaction} transaction
|
* @param {Transaction} transaction
|
||||||
* @param {ItemTextListPosition} currPos
|
* @param {ItemTextListPosition} currPos
|
||||||
@@ -859,55 +909,10 @@ export class YText extends AbstractType {
|
|||||||
_callObserver (transaction, parentSubs) {
|
_callObserver (transaction, parentSubs) {
|
||||||
super._callObserver(transaction, parentSubs)
|
super._callObserver(transaction, parentSubs)
|
||||||
const event = new YTextEvent(this, transaction, parentSubs)
|
const event = new YTextEvent(this, transaction, parentSubs)
|
||||||
const doc = transaction.doc
|
|
||||||
callTypeObservers(this, transaction, event)
|
callTypeObservers(this, transaction, event)
|
||||||
// If a remote change happened, we try to cleanup potential formatting duplicates.
|
// If a remote change happened, we try to cleanup potential formatting duplicates.
|
||||||
if (!transaction.local) {
|
if (!transaction.local) {
|
||||||
// check if another formatting item was inserted
|
transaction._needFormattingCleanup = true
|
||||||
let foundFormattingItem = false
|
|
||||||
for (const [client, afterClock] of transaction.afterState.entries()) {
|
|
||||||
const clock = transaction.beforeState.get(client) || 0
|
|
||||||
if (afterClock === clock) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
iterateStructs(transaction, /** @type {Array<Item|GC>} */ (doc.store.clients.get(client)), clock, afterClock, item => {
|
|
||||||
if (!item.deleted && /** @type {Item} */ (item).content.constructor === ContentFormat) {
|
|
||||||
foundFormattingItem = true
|
|
||||||
}
|
|
||||||
})
|
|
||||||
if (foundFormattingItem) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!foundFormattingItem) {
|
|
||||||
iterateDeletedStructs(transaction, transaction.deleteSet, item => {
|
|
||||||
if (item instanceof GC || foundFormattingItem) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (item.parent === this && item.content.constructor === ContentFormat) {
|
|
||||||
foundFormattingItem = true
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
transact(doc, (t) => {
|
|
||||||
if (foundFormattingItem) {
|
|
||||||
// If a formatting item was inserted, we simply clean the whole type.
|
|
||||||
// We need to compute currentAttributes for the current position anyway.
|
|
||||||
cleanupYTextFormatting(this)
|
|
||||||
} else {
|
|
||||||
// If no formatting attribute was inserted, we can make due with contextless
|
|
||||||
// formatting cleanups.
|
|
||||||
// Contextless: it is not necessary to compute currentAttributes for the affected position.
|
|
||||||
iterateDeletedStructs(t, t.deleteSet, item => {
|
|
||||||
if (item instanceof GC) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (item.parent === this) {
|
|
||||||
cleanupContextlessFormattingGap(t, item)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -335,7 +335,7 @@ export const readAndApplyDeleteSet = (decoder, transaction, store) => {
|
|||||||
*/
|
*/
|
||||||
export const equalDeleteSets = (ds1, ds2) => {
|
export const equalDeleteSets = (ds1, ds2) => {
|
||||||
if (ds1.clients.size !== ds2.clients.size) return false
|
if (ds1.clients.size !== ds2.clients.size) return false
|
||||||
ds1.clients.forEach((deleteItems1, client) => {
|
for (const [client, deleteItems1] of ds1.clients.entries()) {
|
||||||
const deleteItems2 = /** @type {Array<import('../internals.js').DeleteItem>} */ (ds2.clients.get(client))
|
const deleteItems2 = /** @type {Array<import('../internals.js').DeleteItem>} */ (ds2.clients.get(client))
|
||||||
if (deleteItems2 === undefined || deleteItems1.length !== deleteItems2.length) return false
|
if (deleteItems2 === undefined || deleteItems1.length !== deleteItems2.length) return false
|
||||||
for (let i = 0; i < deleteItems1.length; i++) {
|
for (let i = 0; i < deleteItems1.length; i++) {
|
||||||
@@ -345,6 +345,6 @@ export const equalDeleteSets = (ds1, ds2) => {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
Item,
|
Item,
|
||||||
generateNewClientId,
|
generateNewClientId,
|
||||||
createID,
|
createID,
|
||||||
|
cleanupYTextAfterTransaction,
|
||||||
UpdateEncoderV1, UpdateEncoderV2, GC, StructStore, AbstractType, AbstractStruct, YEvent, Doc // eslint-disable-line
|
UpdateEncoderV1, UpdateEncoderV2, GC, StructStore, AbstractType, AbstractStruct, YEvent, Doc // eslint-disable-line
|
||||||
} from '../internals.js'
|
} from '../internals.js'
|
||||||
|
|
||||||
@@ -114,6 +115,10 @@ export class Transaction {
|
|||||||
* @type {Set<Doc>}
|
* @type {Set<Doc>}
|
||||||
*/
|
*/
|
||||||
this.subdocsLoaded = new Set()
|
this.subdocsLoaded = new Set()
|
||||||
|
/**
|
||||||
|
* @type {boolean}
|
||||||
|
*/
|
||||||
|
this._needFormattingCleanup = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -270,31 +275,34 @@ const cleanupTransactions = (transactionCleanups, i) => {
|
|||||||
)
|
)
|
||||||
fs.push(() => {
|
fs.push(() => {
|
||||||
// deep observe events
|
// deep observe events
|
||||||
transaction.changedParentTypes.forEach((events, type) =>
|
transaction.changedParentTypes.forEach((events, type) => {
|
||||||
fs.push(() => {
|
// We need to think about the possibility that the user transforms the
|
||||||
// We need to think about the possibility that the user transforms the
|
// Y.Doc in the event.
|
||||||
// Y.Doc in the event.
|
if (type._dEH.l.length > 0 && (type._item === null || !type._item.deleted)) {
|
||||||
if (type._item === null || !type._item.deleted) {
|
events = events
|
||||||
events = events
|
.filter(event =>
|
||||||
.filter(event =>
|
event.target._item === null || !event.target._item.deleted
|
||||||
event.target._item === null || !event.target._item.deleted
|
)
|
||||||
)
|
events
|
||||||
events
|
.forEach(event => {
|
||||||
.forEach(event => {
|
event.currentTarget = type
|
||||||
event.currentTarget = type
|
// path is relative to the current target
|
||||||
})
|
event._path = null
|
||||||
// sort events by path length so that top-level events are fired first.
|
})
|
||||||
events
|
// sort events by path length so that top-level events are fired first.
|
||||||
.sort((event1, event2) => event1.path.length - event2.path.length)
|
events
|
||||||
// We don't need to check for events.length
|
.sort((event1, event2) => event1.path.length - event2.path.length)
|
||||||
// because we know it has at least one element
|
// We don't need to check for events.length
|
||||||
callEventHandlerListeners(type._dEH, events, transaction)
|
// because we know it has at least one element
|
||||||
}
|
callEventHandlerListeners(type._dEH, events, transaction)
|
||||||
})
|
}
|
||||||
)
|
})
|
||||||
fs.push(() => doc.emit('afterTransaction', [transaction, doc]))
|
|
||||||
})
|
})
|
||||||
|
fs.push(() => doc.emit('afterTransaction', [transaction, doc]))
|
||||||
callAll(fs, [])
|
callAll(fs, [])
|
||||||
|
if (transaction._needFormattingCleanup) {
|
||||||
|
cleanupYTextAfterTransaction(transaction)
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
// Replace deleted items with ItemDeleted / GC.
|
// Replace deleted items with ItemDeleted / GC.
|
||||||
// This is where content is actually remove from the Yjs Doc.
|
// This is where content is actually remove from the Yjs Doc.
|
||||||
|
|||||||
@@ -44,6 +44,10 @@ export class YEvent {
|
|||||||
* @type {null | Array<{ insert?: string | Array<any> | object | AbstractType<any>, retain?: number, delete?: number, attributes?: Object<string, any> }>}
|
* @type {null | Array<{ insert?: string | Array<any> | object | AbstractType<any>, retain?: number, delete?: number, attributes?: Object<string, any> }>}
|
||||||
*/
|
*/
|
||||||
this._delta = null
|
this._delta = null
|
||||||
|
/**
|
||||||
|
* @type {Array<string|number>|null}
|
||||||
|
*/
|
||||||
|
this._path = null
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -60,8 +64,7 @@ export class YEvent {
|
|||||||
* type === event.target // => true
|
* type === event.target // => true
|
||||||
*/
|
*/
|
||||||
get path () {
|
get path () {
|
||||||
// @ts-ignore _item is defined because target is integrated
|
return this._path || (this._path = getPathTo(this.currentTarget, this.target))
|
||||||
return getPathTo(this.currentTarget, this.target)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -337,6 +337,34 @@ export const testObserversUsingObservedeep = tc => {
|
|||||||
compare(users)
|
compare(users)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {t.TestCase} tc
|
||||||
|
*/
|
||||||
|
export const testPathsOfSiblingEvents = tc => {
|
||||||
|
const { users, map0 } = init(tc, { users: 2 })
|
||||||
|
/**
|
||||||
|
* @type {Array<Array<string|number>>}
|
||||||
|
*/
|
||||||
|
const pathes = []
|
||||||
|
let calls = 0
|
||||||
|
const doc = users[0]
|
||||||
|
map0.set('map', new Y.Map())
|
||||||
|
map0.get('map').set('text1', new Y.Text('initial'))
|
||||||
|
map0.observeDeep(events => {
|
||||||
|
events.forEach(event => {
|
||||||
|
pathes.push(event.path)
|
||||||
|
})
|
||||||
|
calls++
|
||||||
|
})
|
||||||
|
doc.transact(() => {
|
||||||
|
map0.get('map').get('text1').insert(0, 'post-')
|
||||||
|
map0.get('map').set('text2', new Y.Text('new'))
|
||||||
|
})
|
||||||
|
t.assert(calls === 1)
|
||||||
|
t.compare(pathes, [['map'], ['map', 'text1']])
|
||||||
|
compare(users)
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: Test events in Y.Map
|
// TODO: Test events in Y.Map
|
||||||
/**
|
/**
|
||||||
* @param {Object<string,any>} is
|
* @param {Object<string,any>} is
|
||||||
|
|||||||
Reference in New Issue
Block a user