mirror of
https://github.com/yjs/yjs.git
synced 2026-07-09 11:58:54 +02:00
applyDelta accepts an origin paramiter - conforming to RDT interface
This commit is contained in:
25
README.md
25
README.md
@@ -687,9 +687,9 @@ assign properties to ranges in the text. This makes it possible to implement
|
||||
rich-text bindings to this type.
|
||||
</p>
|
||||
<p>
|
||||
This type can also be transformed to the
|
||||
<a href="https://quilljs.com/docs/delta">delta format</a>. Similarly the
|
||||
YTextEvents compute changes as deltas.
|
||||
This type can also be transformed to a delta
|
||||
(<a href="https://github.com/dmonad/lib0">lib0/delta</a>). Similarly the
|
||||
events compute changes as deltas.
|
||||
</p>
|
||||
<pre>const ytext = new Y.Text()</pre>
|
||||
<dl>
|
||||
@@ -704,11 +704,17 @@ YTextEvents compute changes as deltas.
|
||||
<dd></dd>
|
||||
<b><code>format(index:number, length:number, formattingAttributes:Object<string,string>)</code></b>
|
||||
<dd>Assign formatting attributes to a range in the text</dd>
|
||||
<b><code>applyDelta(delta: Delta, opts:Object<string,any>)</code></b>
|
||||
<b><code>applyDelta(delta: Delta, [origin: any], [opts: {renderer: AbstractRenderer}])</code></b>
|
||||
<dd>
|
||||
See <a href="https://quilljs.com/docs/delta/">Quill Delta</a>
|
||||
Can set options for preventing remove ending newLines, default is true.
|
||||
<pre>ytext.applyDelta(delta, { sanitize: false })</pre>
|
||||
Apply a delta (<a href="https://github.com/dmonad/lib0">lib0/delta</a>)
|
||||
on this shared type. The optional <var>origin</var> is stored on the
|
||||
transaction (<code>transaction.origin</code>) and forwarded verbatim
|
||||
on the emitted <code>'delta'</code> event, so listeners can recognize
|
||||
— and skip — changes they produced themselves (see the lib0
|
||||
<code>RDT</code> spec). <code>opts.renderer</code> renders the content
|
||||
with attributions; positions in the delta are then interpreted
|
||||
relative to the attributed content.
|
||||
<pre>ytext.applyDelta(delta.create().retain(5).insert('!').done(), origin)</pre>
|
||||
</dd>
|
||||
<b><code>length:number</code></b>
|
||||
<dd></dd>
|
||||
@@ -716,9 +722,10 @@ YTextEvents compute changes as deltas.
|
||||
<dd>Transforms this type, without formatting options, into a string.</dd>
|
||||
<b><code>toJSON():string</code></b>
|
||||
<dd>See <code>toString</code></dd>
|
||||
<b><code>toDelta():Delta</code></b>
|
||||
<b><code>toDelta([opts: { renderer: AbstractRenderer }]):Delta</code></b>
|
||||
<dd>
|
||||
Transforms this type to a <a href="https://quilljs.com/docs/delta/">Quill Delta</a>
|
||||
Transforms this type to a delta (<a href="https://github.com/dmonad/lib0">lib0/delta</a>),
|
||||
optionally rendering attributed content with <code>opts.renderer</code>.
|
||||
</dd>
|
||||
<b><code>observe(function(YTextEvent, Transaction):void)</code></b>
|
||||
<dd>
|
||||
|
||||
@@ -26,7 +26,7 @@ this:
|
||||
```
|
||||
|
||||
In Yjs, we can now "attribute" changes with additional information. When we
|
||||
render content using methods like `toString()` or `getDelta()`, Yjs will render
|
||||
render content using methods like `toString()` or `toDelta()`, Yjs will render
|
||||
the unattributed content as-is, but it will render the attributed content with
|
||||
the additional information. As all changes in Yjs are identifyable by Ids, we
|
||||
can use `IdMap`s to map changes to "attributions". For example, we could
|
||||
@@ -35,69 +35,77 @@ attribute deletions and insertions of a change and render them:
|
||||
```js
|
||||
// We create some initial content "Hello World!". Then we create another
|
||||
// document that will have a bunch of changes (make "Hell" italic, replace "World"
|
||||
// with "Attribution").
|
||||
// with "attributions").
|
||||
const ydocVersion0 = new Y.Doc({ gc: false })
|
||||
ydocVersion0.getText().insert(0, 'Hello World!')
|
||||
ydocVersion0.get().insert(0, 'Hello World!')
|
||||
const ydoc = new Y.Doc({ gc: false })
|
||||
Y.applyUpdate(ydoc, Y.encodeStateAsUpdate(ydocVersion0))
|
||||
const ytext = ydoc.getText()
|
||||
ytext.applyDelta([{ retain: 4, attributes: { italic: true } }, { retain: 2 }, { delete: 5 }, { insert: 'attributions' }])
|
||||
// this represents to all insertions of ydoc
|
||||
const insertionSet = Y.createInsertionSetFromStructStore(ydoc.store)
|
||||
const ytext = ydoc.get()
|
||||
ytext.applyDelta(delta.create().retain(4, { italic: true }).retain(2).delete(5).insert('attributions').done())
|
||||
// this represents all insertions of ydoc
|
||||
const insertionSet = Y.createInsertSetFromStructStore(ydoc.store, false)
|
||||
const deleteSet = Y.createDeleteSetFromStructStore(ydoc.store)
|
||||
// exclude the changes from `ydocVersion0`
|
||||
const insertionSetDiff = Y.diffIdSet(insertionSet, Y.createInsertionSetFromStructStore(ydocVersion0.store))
|
||||
const insertionSetDiff = Y.diffIdSet(insertionSet, Y.createInsertSetFromStructStore(ydocVersion0.store, false))
|
||||
const deleteSetDiff = Y.diffIdSet(deleteSet, Y.createDeleteSetFromStructStore(ydocVersion0.store))
|
||||
// assign attributes to the diff
|
||||
const attributedInsertions = createIdMapFromIdSet(insertionSetDiff, [new Y.Attribution('insert', 'Bob')])
|
||||
const attributedDeletions = createIdMapFromIdSet(deleteSetDiff, [new Y.Attribution('delete', 'Bob')])
|
||||
const attributedInsertions = Y.createIdMapFromIdSet(insertionSetDiff, [Y.createContentAttribute('insert', 'Bob')])
|
||||
const attributedDeletions = Y.createIdMapFromIdSet(deleteSetDiff, [Y.createContentAttribute('delete', 'Bob')])
|
||||
// now we can define a renderer that maps these changes to output. One of the
|
||||
// implementations is the TwosetRenderer
|
||||
const renderer = new TwosetRenderer(attributedInsertions, attributedDeletions)
|
||||
const renderer = new Y.TwosetRenderer(attributedInsertions, attributedDeletions)
|
||||
// we render the attributed content with the renderer
|
||||
let attributedContent = ytext.toDelta({ renderer })
|
||||
console.log(JSON.stringify(attributedContent.toJSON().ops, null, 2))
|
||||
let expectedContent = delta.create().insert('Hell', { italic: true }, { attributes: { italic: ['Bob'] } }).insert('o ').insert('World', {}, { delete: ['Bob'] }).insert('attributions', {}, { insert: ['Bob'] }).insert('!')
|
||||
const attributedContent = ytext.toDelta({ renderer })
|
||||
console.log(JSON.stringify(attributedContent.toJSON(), null, 2))
|
||||
const expectedContent = delta.create().insert('Hell', { italic: true }, { format: { italic: ['Bob'] } }).insert('o ').insert('World', {}, { delete: ['Bob'] }).insert('attributions', {}, { insert: ['Bob'] }).insert('!')
|
||||
t.assert(attributedContent.equals(expectedContent))
|
||||
|
||||
// this is how the output would look like
|
||||
const output = [
|
||||
{
|
||||
"insert": "Hell",
|
||||
"attributes": {
|
||||
"italic": true
|
||||
const output = {
|
||||
"type": "delta",
|
||||
"children": [
|
||||
{
|
||||
"type": "insert",
|
||||
"insert": "Hell",
|
||||
"format": {
|
||||
"italic": true
|
||||
},
|
||||
"attribution": { // no "insert" attribution: the insertion "Hell" is not attributed to anyone
|
||||
"format": {
|
||||
"italic": [ // the formatting attribute "italic" was added by Bob
|
||||
"Bob"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"attribution": { // no "insert" attribution: the insertion "Hell" is not attributed to anyone
|
||||
"attributes": {
|
||||
"italic": [ // the attribute "italic" was added by Bob
|
||||
{
|
||||
"type": "insert",
|
||||
"insert": "o " // the insertion "o " has no attributions
|
||||
},
|
||||
{
|
||||
"type": "insert",
|
||||
"insert": "World",
|
||||
"attribution": { // the insertion "World" was deleted by Bob
|
||||
"delete": [
|
||||
"Bob"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "insert",
|
||||
"insert": "attributions", // the insertion "attributions" was inserted by Bob
|
||||
"attribution": {
|
||||
"insert": [
|
||||
"Bob"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "insert",
|
||||
"insert": "!" // the insertion "!" has no attributions
|
||||
}
|
||||
},
|
||||
{
|
||||
"insert": "o " // the insertion "o " has no attributions
|
||||
},
|
||||
{
|
||||
"insert": "World",
|
||||
"attribution": { // the insertion "World" was deleted by Bob
|
||||
"delete": [
|
||||
"Bob"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"insert": "attributions", // the insertion "attributions" was inserted by Bob
|
||||
"attribution": {
|
||||
"insert": [
|
||||
"Bob"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"insert": "!" // the insertion "!" has no attributions
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
We get a similar output to Google Docs: Insertions, Deletions, and changes to
|
||||
@@ -107,7 +115,7 @@ the editor to render those changes with background-color etc..
|
||||
Of course, we could associated changes also to multiple users like this:
|
||||
|
||||
```js
|
||||
const attributedDeletions = createIdMapFromIdSet(deleteSetDiff, [new Y.Attribution('insert', 'Bob'), new Y.Attribution('insert', 'OpenAI o3')])
|
||||
const attributedDeletions = Y.createIdMapFromIdSet(deleteSetDiff, [Y.createContentAttribute('insert', 'Bob'), Y.createContentAttribute('insert', 'OpenAI o3')])
|
||||
```
|
||||
|
||||
You could use the same output to calculate a real diff as well (consisting of
|
||||
@@ -119,4 +127,4 @@ this approach.
|
||||
|
||||
The attribution data is encoded very efficiently. The ids are encoded using
|
||||
run-length encoding and the Attributes are de-duplicated and only encoded once.
|
||||
The above example encodes in 20 bytes.
|
||||
The above example encodes in 27 bytes.
|
||||
|
||||
75
renderer.md
75
renderer.md
@@ -48,70 +48,67 @@ Deleted content is represented in attributed results to maintain authorship info
|
||||
|
||||
## API Reference
|
||||
|
||||
### YText
|
||||
### Y.Type
|
||||
|
||||
#### `getDelta([renderer])`
|
||||
In Yjs v14 all shared types (text, array, map, xml) are instances of the unified
|
||||
`Y.Type`.
|
||||
|
||||
Returns the delta representation of the YText content, optionally with attribution information.
|
||||
#### `toDelta([{ renderer }])`
|
||||
|
||||
Returns the delta representation (lib0/delta) of the type's content, optionally
|
||||
with attribution information.
|
||||
|
||||
**Parameters:**
|
||||
- `renderer` (optional): The renderer instance
|
||||
|
||||
**Returns:**
|
||||
- Array of delta operations, with attribution metadata if `renderer` is provided
|
||||
- A `Delta` describing the content, with attribution metadata if `renderer` is provided
|
||||
|
||||
**Examples:**
|
||||
|
||||
```javascript
|
||||
const ytext = new Y.Text()
|
||||
const ytext = ydoc.get()
|
||||
// Content is inserted during collaborative editing
|
||||
// Attribution is handled automatically by the server
|
||||
|
||||
// Without attribution
|
||||
const delta = ytext.getDelta()
|
||||
const d = ytext.toDelta()
|
||||
// [{ insert: 'hello world' }]
|
||||
|
||||
// With attribution
|
||||
const attributedDelta = ytext.getDelta({ renderer })
|
||||
const attributedDelta = ytext.toDelta({ renderer })
|
||||
// [
|
||||
// { insert: 'hello', attribution: { insert: ['kevin'] } },
|
||||
// { insert: ' world', attribution: { insert: ['alice'] } }
|
||||
// ]
|
||||
```
|
||||
|
||||
#### `toDelta([renderer])`
|
||||
#### `applyDelta(delta, [origin], [{ renderer }])`
|
||||
|
||||
Returns the content representation with optional attribution information.
|
||||
Applies a delta (lib0/delta) on the shared type. The optional `origin` is stored
|
||||
on the transaction (`transaction.origin`) and forwarded verbatim on the emitted
|
||||
`'delta'` event (lib0 RDT spec), so listeners can recognize — and skip — changes
|
||||
they produced themselves. When a `renderer` is provided, positions in the delta
|
||||
are interpreted relative to the attributed (rendered) content.
|
||||
|
||||
**Parameters:**
|
||||
- `delta`: The changes to apply
|
||||
- `origin` (optional): Origin of the transaction that applies this delta; defaults to `null`
|
||||
- `renderer` (optional): The renderer instance
|
||||
|
||||
### YEvent
|
||||
|
||||
#### `getDelta([{ renderer, deep }])`
|
||||
|
||||
Returns the changes of an event as a delta, optionally rendered with attribution
|
||||
information.
|
||||
|
||||
**Parameters:**
|
||||
- `renderer` (optional): The renderer instance
|
||||
- `deep` (optional): Render child types as deltas
|
||||
|
||||
**Returns:**
|
||||
- Content representation with attribution metadata if `renderer` is provided
|
||||
|
||||
### YArray
|
||||
|
||||
#### `toDelta([renderer])`
|
||||
|
||||
Returns the array content with optional attribution information for each element.
|
||||
|
||||
**Parameters:**
|
||||
- `renderer` (optional): The renderer instance
|
||||
|
||||
**Returns:**
|
||||
- Array content with attribution metadata if `renderer` is provided
|
||||
|
||||
### YMap
|
||||
|
||||
#### `toDelta([renderer])`
|
||||
|
||||
Returns the map content with optional attribution information for each key-value pair.
|
||||
|
||||
**Parameters:**
|
||||
- `renderer` (optional): The renderer instance
|
||||
|
||||
**Returns:**
|
||||
- Map content with attribution metadata if `renderer` is provided
|
||||
- A `Delta` describing the changes, with attribution metadata if `renderer` is provided
|
||||
|
||||
## Position Adjustments
|
||||
|
||||
@@ -124,7 +121,7 @@ When working with attributed content, position calculations must account for del
|
||||
ytext.toString() // "world"
|
||||
|
||||
// Attributed content (includes deleted content)
|
||||
ytext.getDelta({ renderer })
|
||||
ytext.toDelta({ renderer })
|
||||
// [
|
||||
// { insert: 'hello ', attribution: { delete: ['kevin'] } }, // positions 0-5
|
||||
// { insert: 'world' } // positions 6-10
|
||||
@@ -176,9 +173,9 @@ Display content with visual indicators of who created each part:
|
||||
|
||||
```javascript
|
||||
function renderWithAuthorship(ytext, renderer) {
|
||||
const attributedDelta = ytext.getDelta({ renderer })
|
||||
const attributedDelta = ytext.toDelta({ renderer })
|
||||
|
||||
return attributedDelta.map(op => {
|
||||
return attributedDelta.children.map(op => {
|
||||
const author = op.attribution?.insert?.[0] || 'unknown'
|
||||
const isDeleted = op.attribution?.delete
|
||||
|
||||
@@ -199,9 +196,9 @@ Track who made specific changes to content:
|
||||
```javascript
|
||||
function trackChanges(ytext, renderer) {
|
||||
ytext.observe((event, transaction) => {
|
||||
const changes = event.changes.getAttributedDelta?.(renderer) || event.changes.delta
|
||||
const changes = event.getDelta({ renderer })
|
||||
|
||||
changes.forEach(change => {
|
||||
changes.children.forEach(change => {
|
||||
if (change.attribution) {
|
||||
console.log(`Change by ${change.attribution.insert?.[0] || change.attribution.delete?.[0]}:`, change)
|
||||
}
|
||||
|
||||
11
src/ytype.js
11
src/ytype.js
@@ -1238,6 +1238,9 @@ export class YType extends ObservableV2 {
|
||||
* Apply a {@link Delta} on this shared type.
|
||||
*
|
||||
* @param {delta.DeltaAny} d The changes to apply on this element.
|
||||
* @param {any} [origin] Origin of the transaction that applies this delta (stored on
|
||||
* `transaction.origin` and forwarded verbatim on the emitted `'delta'` event, so listeners can
|
||||
* recognize — and skip — changes they produced themselves; see the lib0 `RDT` spec). Defaults to `null`.
|
||||
* @param {Object} [opts]
|
||||
* @param {AbstractRenderer} [opts.renderer] - renders the content (with attributions); defaults to this type's active renderer (see {@link YType#useRenderer}), i.e. `baseRenderer` unless changed
|
||||
* @return {null} The lib0 `RDT` "fix" of this apply — always `null`: a `YType` accepts every valid
|
||||
@@ -1245,7 +1248,7 @@ export class YType extends ObservableV2 {
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
applyDelta (d, { renderer = this._renderer } = {}) {
|
||||
applyDelta (d, origin = null, { renderer = this._renderer } = {}) {
|
||||
if (d.isEmpty()) return null
|
||||
if (this.doc == null) {
|
||||
(this._prelim || (this._prelim = /** @type {any} */ (delta.create()))).apply(d)
|
||||
@@ -1266,7 +1269,7 @@ export class YType extends ObservableV2 {
|
||||
let item = currPos.right
|
||||
while (item != null && (item.deleted || !item.countable)) { item = item.next }
|
||||
if (item == null || item.content.constructor !== ContentType) { error.unexpectedCase() }
|
||||
/** @type {ContentType} */ (item.content).type.applyDelta(op.value, { renderer })
|
||||
/** @type {ContentType} */ (item.content).type.applyDelta(op.value, origin, { renderer })
|
||||
currPos.formatText(transaction, /** @type {any} */ (this), 1, op.format || {})
|
||||
} else {
|
||||
error.unexpectedCase()
|
||||
@@ -1282,10 +1285,10 @@ export class YType extends ObservableV2 {
|
||||
if (!(sub instanceof YType)) {
|
||||
error.unexpectedCase()
|
||||
}
|
||||
sub.applyDelta(op.value, { renderer })
|
||||
sub.applyDelta(op.value, origin, { renderer })
|
||||
}
|
||||
}
|
||||
})
|
||||
}, origin)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ export const testAttributedEvents = _tc => {
|
||||
t.compare(d, delta.create().retain(11).insert('!', null, { insert: [] }).done())
|
||||
calledObserver = true
|
||||
})
|
||||
ytext.applyDelta(delta.create().retain(11).insert('!').done(), { renderer })
|
||||
ytext.applyDelta(delta.create().retain(11).insert('!').done(), null, { renderer })
|
||||
t.assert(calledObserver)
|
||||
}
|
||||
|
||||
@@ -69,7 +69,7 @@ export const testInsertionsMindingAttributedContent = _tc => {
|
||||
const renderer = Y.createDiffRenderer(v1, ydoc)
|
||||
const c1 = ytext.toDelta({ renderer })
|
||||
t.compare(c1, delta.create().insert('hello ').insert('world', null, { delete: [] }).done())
|
||||
ytext.applyDelta(delta.create().retain(11).insert('content').done(), { renderer })
|
||||
ytext.applyDelta(delta.create().retain(11).insert('content').done(), null, { renderer })
|
||||
t.assert(ytext.toString() === 'hello content')
|
||||
}
|
||||
|
||||
@@ -87,7 +87,7 @@ export const testInsertionsIntoAttributedContent = _tc => {
|
||||
const renderer = Y.createDiffRenderer(v1, ydoc)
|
||||
const c1 = ytext.toDelta({ renderer })
|
||||
t.compare(c1, delta.create().insert('hello ').insert('word', null, { insert: [] }).done())
|
||||
ytext.applyDelta(delta.create().retain(9).insert('l').done(), { renderer })
|
||||
ytext.applyDelta(delta.create().retain(9).insert('l').done(), null, { renderer })
|
||||
t.assert(ytext.toString() === 'hello world')
|
||||
}
|
||||
|
||||
@@ -276,6 +276,14 @@ export const testRdtDeltaEventOrigin = () => {
|
||||
capturedOrigin = myOrigin
|
||||
ytext.insert(5, ' world')
|
||||
t.assert(capturedOrigin === null)
|
||||
// the origin passed to `applyDelta` becomes the transaction origin and is forwarded on the event
|
||||
const applyOrigin = {}
|
||||
ytext.applyDelta(delta.create().retain(11).insert('!').done(), applyOrigin)
|
||||
t.assert(capturedOrigin === applyOrigin)
|
||||
// `applyDelta` without an explicit origin emits `null`
|
||||
capturedOrigin = applyOrigin
|
||||
ytext.applyDelta(delta.create().retain(12).insert('?').done())
|
||||
t.assert(capturedOrigin === null)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -191,7 +191,7 @@ export const testAttributions = _tc => {
|
||||
* UNattributed: 'world!'
|
||||
*/
|
||||
// Apply a change to the attributed content
|
||||
ytype.applyDelta(delta.create().retain(11).insert('!').done(), { renderer })
|
||||
ytype.applyDelta(delta.create().retain(11).insert('!').done(), null, { renderer })
|
||||
// // Equivalent to applying a change to the UNattributed content:
|
||||
// ytype.applyDelta(delta.create().retain(5).insert('!'))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user