@y/y 14.0.0-rc.20 → 14.0.0-rc.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/src/utils/Doc.d.ts.map +1 -1
- package/dist/src/utils/Transaction.d.ts.map +1 -1
- package/dist/src/utils/YEvent.d.ts +1 -1
- package/dist/src/utils/transaction-helpers.d.ts +2 -2
- package/dist/src/utils/transaction-helpers.d.ts.map +1 -1
- package/dist/src/ytype.d.ts +103 -24
- package/dist/src/ytype.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/utils/Doc.js +3 -0
- package/src/utils/Transaction.js +31 -19
- package/src/utils/YEvent.js +2 -2
- package/src/utils/transaction-helpers.js +15 -15
- package/src/ytype.js +334 -164
package/src/ytype.js
CHANGED
|
@@ -10,6 +10,7 @@ import * as math from 'lib0/math'
|
|
|
10
10
|
import * as object from 'lib0/object'
|
|
11
11
|
import * as s from 'lib0/schema'
|
|
12
12
|
import * as traits from 'lib0/traits'
|
|
13
|
+
import { ObservableV2 } from 'lib0/observable'
|
|
13
14
|
import {
|
|
14
15
|
Item,
|
|
15
16
|
ContentAny,
|
|
@@ -50,11 +51,12 @@ const maxSearchMarker = 80
|
|
|
50
51
|
* @todo SHOULD NOT RETURN AN OBJECT!
|
|
51
52
|
* @param {Array<ContentAttribute<any>>?} attrs
|
|
52
53
|
* @param {boolean} deleted - whether the attributed item is deleted
|
|
53
|
-
* @return {Attribution
|
|
54
|
+
* @return {Attribution|undefined} `undefined` when there is no attribution — under lib0's tri-state
|
|
55
|
+
* that means "skip / inherit the builder's attribution context" (NOT `null`, which would clear it).
|
|
54
56
|
*/
|
|
55
57
|
export const createAttributionFromAttributionItems = (attrs, deleted) => {
|
|
56
58
|
if (attrs == null) {
|
|
57
|
-
return
|
|
59
|
+
return undefined
|
|
58
60
|
}
|
|
59
61
|
/**
|
|
60
62
|
* @type {Attribution}
|
|
@@ -98,14 +100,14 @@ export class ItemTextListPosition {
|
|
|
98
100
|
* @param {Item|null} left
|
|
99
101
|
* @param {Item|null} right
|
|
100
102
|
* @param {number} index
|
|
101
|
-
* @param {Map<string,any>}
|
|
103
|
+
* @param {Map<string,any>} currentFormats
|
|
102
104
|
* @param {AbstractRenderer} renderer
|
|
103
105
|
*/
|
|
104
|
-
constructor (left, right, index,
|
|
106
|
+
constructor (left, right, index, currentFormats, renderer) {
|
|
105
107
|
this.left = left
|
|
106
108
|
this.right = right
|
|
107
109
|
this.index = index
|
|
108
|
-
this.
|
|
110
|
+
this.currentFormats = currentFormats
|
|
109
111
|
this.renderer = renderer
|
|
110
112
|
}
|
|
111
113
|
|
|
@@ -119,7 +121,7 @@ export class ItemTextListPosition {
|
|
|
119
121
|
switch (this.right.content.constructor) {
|
|
120
122
|
case ContentFormat:
|
|
121
123
|
if (!this.right.deleted) {
|
|
122
|
-
|
|
124
|
+
updateCurrentFormats(this.currentFormats, /** @type {ContentFormat} */ (this.right.content))
|
|
123
125
|
}
|
|
124
126
|
break
|
|
125
127
|
default:
|
|
@@ -134,22 +136,22 @@ export class ItemTextListPosition {
|
|
|
134
136
|
* @param {Transaction} transaction
|
|
135
137
|
* @param {YType} parent
|
|
136
138
|
* @param {number} length
|
|
137
|
-
* @param {Object<string,any>}
|
|
139
|
+
* @param {Object<string,any>} formats
|
|
138
140
|
*
|
|
139
141
|
* @function
|
|
140
142
|
*/
|
|
141
|
-
formatText (transaction, parent, length,
|
|
142
|
-
|
|
143
|
-
const
|
|
143
|
+
formatText (transaction, parent, length, formats) {
|
|
144
|
+
minimizeFormatChanges(this, formats)
|
|
145
|
+
const negatedFormats = insertFormats(transaction, parent, this, formats)
|
|
144
146
|
// iterate until first non-format or null is found
|
|
145
|
-
// delete all formats with
|
|
146
|
-
// also check the
|
|
147
|
+
// delete all formats with formats[format.key] != null
|
|
148
|
+
// also check the formats after the first non-format as we do not want to insert redundant negated formats there
|
|
147
149
|
// eslint-disable-next-line no-labels
|
|
148
150
|
iterationLoop: while (
|
|
149
151
|
this.right !== null &&
|
|
150
152
|
(length > 0 ||
|
|
151
153
|
(
|
|
152
|
-
|
|
154
|
+
negatedFormats.size > 0 &&
|
|
153
155
|
((this.right.deleted && this.renderer.contentLength(this.right) === 0) || this.right.content.constructor === ContentFormat)
|
|
154
156
|
)
|
|
155
157
|
)
|
|
@@ -158,21 +160,21 @@ export class ItemTextListPosition {
|
|
|
158
160
|
case ContentFormat: {
|
|
159
161
|
if (!this.right.deleted) {
|
|
160
162
|
const { key, value } = /** @type {ContentFormat} */ (this.right.content)
|
|
161
|
-
const attr =
|
|
163
|
+
const attr = formats[key]
|
|
162
164
|
if (attr !== undefined) {
|
|
163
|
-
if (
|
|
164
|
-
|
|
165
|
+
if (equalFormats(attr, value)) {
|
|
166
|
+
negatedFormats.delete(key)
|
|
165
167
|
} else {
|
|
166
168
|
if (length === 0) {
|
|
167
|
-
// no need to further extend
|
|
169
|
+
// no need to further extend negatedFormats
|
|
168
170
|
// eslint-disable-next-line no-labels
|
|
169
171
|
break iterationLoop
|
|
170
172
|
}
|
|
171
|
-
|
|
173
|
+
negatedFormats.set(key, value)
|
|
172
174
|
}
|
|
173
175
|
this.right.delete(transaction)
|
|
174
176
|
} else {
|
|
175
|
-
this.
|
|
177
|
+
this.currentFormats.set(key, value)
|
|
176
178
|
}
|
|
177
179
|
}
|
|
178
180
|
break
|
|
@@ -208,7 +210,7 @@ export class ItemTextListPosition {
|
|
|
208
210
|
if (length > 0) {
|
|
209
211
|
throw new Error('Exceeded content range')
|
|
210
212
|
}
|
|
211
|
-
|
|
213
|
+
insertNegatedFormats(transaction, parent, this, negatedFormats)
|
|
212
214
|
}
|
|
213
215
|
}
|
|
214
216
|
|
|
@@ -218,29 +220,29 @@ export class ItemTextListPosition {
|
|
|
218
220
|
* @param {Transaction} transaction
|
|
219
221
|
* @param {YType} parent
|
|
220
222
|
* @param {ItemTextListPosition} currPos
|
|
221
|
-
* @param {Map<string,any>}
|
|
223
|
+
* @param {Map<string,any>} negatedFormats
|
|
222
224
|
*
|
|
223
225
|
* @private
|
|
224
226
|
* @function
|
|
225
227
|
*/
|
|
226
|
-
const
|
|
227
|
-
// check if we really need to remove
|
|
228
|
+
const insertNegatedFormats = (transaction, parent, currPos, negatedFormats) => {
|
|
229
|
+
// check if we really need to remove formats
|
|
228
230
|
while (
|
|
229
231
|
currPos.right !== null && (
|
|
230
232
|
(currPos.right.deleted && (currPos.renderer === baseRenderer || currPos.renderer.contentLength(currPos.right) === 0)) || (
|
|
231
233
|
currPos.right.content.constructor === ContentFormat &&
|
|
232
|
-
|
|
234
|
+
equalFormats(negatedFormats.get(/** @type {ContentFormat} */ (currPos.right.content).key), /** @type {ContentFormat} */ (currPos.right.content).value)
|
|
233
235
|
)
|
|
234
236
|
)
|
|
235
237
|
) {
|
|
236
238
|
if (!currPos.right.deleted) {
|
|
237
|
-
|
|
239
|
+
negatedFormats.delete(/** @type {ContentFormat} */ (currPos.right.content).key)
|
|
238
240
|
}
|
|
239
241
|
currPos.forward()
|
|
240
242
|
}
|
|
241
243
|
const doc = transaction.doc
|
|
242
244
|
const ownClientId = doc.clientID
|
|
243
|
-
|
|
245
|
+
negatedFormats.forEach((val, key) => {
|
|
244
246
|
const left = currPos.left
|
|
245
247
|
const right = currPos.right
|
|
246
248
|
const nextFormat = new Item(createID(ownClientId, doc.store.getClock(ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, new ContentFormat(key, val))
|
|
@@ -251,34 +253,34 @@ const insertNegatedAttributes = (transaction, parent, currPos, negatedAttributes
|
|
|
251
253
|
}
|
|
252
254
|
|
|
253
255
|
/**
|
|
254
|
-
* @param {Map<string,any>}
|
|
256
|
+
* @param {Map<string,any>} currentFormats
|
|
255
257
|
* @param {ContentFormat} format
|
|
256
258
|
*
|
|
257
259
|
* @private
|
|
258
260
|
* @function
|
|
259
261
|
*/
|
|
260
|
-
const
|
|
262
|
+
const updateCurrentFormats = (currentFormats, format) => {
|
|
261
263
|
const { key, value } = format
|
|
262
264
|
if (value === null) {
|
|
263
|
-
|
|
265
|
+
currentFormats.delete(key)
|
|
264
266
|
} else {
|
|
265
|
-
|
|
267
|
+
currentFormats.set(key, value)
|
|
266
268
|
}
|
|
267
269
|
}
|
|
268
270
|
|
|
269
271
|
/**
|
|
270
272
|
* @param {ItemTextListPosition} currPos
|
|
271
|
-
* @param {Object<string,any>}
|
|
273
|
+
* @param {Object<string,any>} formats
|
|
272
274
|
*
|
|
273
275
|
* @private
|
|
274
276
|
* @function
|
|
275
277
|
*/
|
|
276
|
-
const
|
|
277
|
-
// go right while
|
|
278
|
+
const minimizeFormatChanges = (currPos, formats) => {
|
|
279
|
+
// go right while formats[right.key] === right.value (or right is deleted)
|
|
278
280
|
while (true) {
|
|
279
281
|
if (currPos.right === null) {
|
|
280
282
|
break
|
|
281
|
-
} else if (currPos.right.deleted ? (currPos.renderer.contentLength(currPos.right) === 0) : (!currPos.right.deleted && currPos.right.content.constructor === ContentFormat &&
|
|
283
|
+
} else if (currPos.right.deleted ? (currPos.renderer.contentLength(currPos.right) === 0) : (!currPos.right.deleted && currPos.right.content.constructor === ContentFormat && equalFormats(formats[(/** @type {ContentFormat} */ (currPos.right.content)).key] ?? null, /** @type {ContentFormat} */ (currPos.right.content).value))) {
|
|
282
284
|
//
|
|
283
285
|
} else {
|
|
284
286
|
break
|
|
@@ -291,30 +293,30 @@ const minimizeAttributeChanges = (currPos, attributes) => {
|
|
|
291
293
|
* @param {Transaction} transaction
|
|
292
294
|
* @param {YType} parent
|
|
293
295
|
* @param {ItemTextListPosition} currPos
|
|
294
|
-
* @param {Object<string,any>}
|
|
296
|
+
* @param {Object<string,any>} formats
|
|
295
297
|
* @return {Map<string,any>}
|
|
296
298
|
*
|
|
297
299
|
* @private
|
|
298
300
|
* @function
|
|
299
301
|
**/
|
|
300
|
-
const
|
|
302
|
+
const insertFormats = (transaction, parent, currPos, formats) => {
|
|
301
303
|
const doc = transaction.doc
|
|
302
304
|
const ownClientId = doc.clientID
|
|
303
|
-
const
|
|
305
|
+
const negatedFormats = new Map()
|
|
304
306
|
// insert format-start items
|
|
305
|
-
for (const key in
|
|
306
|
-
const val =
|
|
307
|
-
const currentVal = currPos.
|
|
308
|
-
if (!
|
|
309
|
-
// save negated
|
|
310
|
-
|
|
307
|
+
for (const key in formats) {
|
|
308
|
+
const val = formats[key]
|
|
309
|
+
const currentVal = currPos.currentFormats.get(key) ?? null
|
|
310
|
+
if (!equalFormats(currentVal, val)) {
|
|
311
|
+
// save negated format (set null if currentVal undefined)
|
|
312
|
+
negatedFormats.set(key, currentVal)
|
|
311
313
|
const { left, right } = currPos
|
|
312
314
|
currPos.right = new Item(createID(ownClientId, doc.store.getClock(ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, new ContentFormat(key, val))
|
|
313
315
|
currPos.right.integrate(transaction, 0)
|
|
314
316
|
currPos.forward()
|
|
315
317
|
}
|
|
316
318
|
}
|
|
317
|
-
return
|
|
319
|
+
return negatedFormats
|
|
318
320
|
}
|
|
319
321
|
|
|
320
322
|
/**
|
|
@@ -322,21 +324,21 @@ const insertAttributes = (transaction, parent, currPos, attributes) => {
|
|
|
322
324
|
* @param {YType} parent
|
|
323
325
|
* @param {ItemTextListPosition} currPos
|
|
324
326
|
* @param {import('./structs/Item.js').AbstractContent} content
|
|
325
|
-
* @param {Object<string,any>}
|
|
327
|
+
* @param {Object<string,any>} formats
|
|
326
328
|
*
|
|
327
329
|
* @private
|
|
328
330
|
* @function
|
|
329
331
|
**/
|
|
330
|
-
export const insertContent = (transaction, parent, currPos, content,
|
|
331
|
-
currPos.
|
|
332
|
-
if (
|
|
333
|
-
|
|
332
|
+
export const insertContent = (transaction, parent, currPos, content, formats) => {
|
|
333
|
+
currPos.currentFormats.forEach((_val, key) => {
|
|
334
|
+
if (formats[key] === undefined) {
|
|
335
|
+
formats[key] = null
|
|
334
336
|
}
|
|
335
337
|
})
|
|
336
338
|
const doc = transaction.doc
|
|
337
339
|
const ownClientId = doc.clientID
|
|
338
|
-
|
|
339
|
-
const
|
|
340
|
+
minimizeFormatChanges(currPos, formats)
|
|
341
|
+
const negatedFormats = insertFormats(transaction, parent, currPos, formats)
|
|
340
342
|
let { left, right, index } = currPos
|
|
341
343
|
if (parent._searchMarker) {
|
|
342
344
|
updateMarkerChanges(parent._searchMarker, currPos.index, content.getLength())
|
|
@@ -346,7 +348,7 @@ export const insertContent = (transaction, parent, currPos, content, attributes)
|
|
|
346
348
|
currPos.right = right
|
|
347
349
|
currPos.index = index
|
|
348
350
|
currPos.forward()
|
|
349
|
-
|
|
351
|
+
insertNegatedFormats(transaction, parent, currPos, negatedFormats)
|
|
350
352
|
}
|
|
351
353
|
|
|
352
354
|
/**
|
|
@@ -354,27 +356,27 @@ export const insertContent = (transaction, parent, currPos, content, attributes)
|
|
|
354
356
|
* @param {YType} parent
|
|
355
357
|
* @param {ItemTextListPosition} currPos
|
|
356
358
|
* @param {Array<any>|string} insert
|
|
357
|
-
* @param {Object<string,any>}
|
|
359
|
+
* @param {Object<string,any>} formats
|
|
358
360
|
*/
|
|
359
|
-
export const insertContentHelper = (transaction, parent, currPos, insert,
|
|
361
|
+
export const insertContentHelper = (transaction, parent, currPos, insert, formats) => {
|
|
360
362
|
if (s.$string.check(insert)) {
|
|
361
|
-
insertContent(transaction, parent, currPos, new ContentString(insert),
|
|
363
|
+
insertContent(transaction, parent, currPos, new ContentString(insert), formats)
|
|
362
364
|
} else {
|
|
363
365
|
insert = insert.map(ins => delta.$deltaAny.check(ins) ? YType.from(ins) : ins)
|
|
364
366
|
for (let i = 0; i < insert.length;) {
|
|
365
367
|
const first = insert[i]
|
|
366
368
|
if (first instanceof YType) {
|
|
367
|
-
insertContent(transaction, parent, currPos, new ContentType(first),
|
|
369
|
+
insertContent(transaction, parent, currPos, new ContentType(first), formats)
|
|
368
370
|
i++
|
|
369
371
|
} else if ($ydoc.check(first)) {
|
|
370
|
-
insertContent(transaction, parent, currPos, createContentDocFromDoc(first),
|
|
372
|
+
insertContent(transaction, parent, currPos, createContentDocFromDoc(first), formats)
|
|
371
373
|
i++
|
|
372
374
|
} else {
|
|
373
375
|
// insert "any" content
|
|
374
376
|
// compute slice len
|
|
375
377
|
let j = i + 1
|
|
376
378
|
for (; j < insert.length && !(insert[j] instanceof YType || $ydoc.check(insert[j])); j++) { /* nop */ }
|
|
377
|
-
insertContent(transaction, parent, currPos, new ContentAny((i === 0 && j === insert.length) ? insert : insert.slice(i, j)),
|
|
379
|
+
insertContent(transaction, parent, currPos, new ContentAny((i === 0 && j === insert.length) ? insert : insert.slice(i, j)), formats)
|
|
378
380
|
i = j
|
|
379
381
|
}
|
|
380
382
|
}
|
|
@@ -392,7 +394,7 @@ export const insertContentHelper = (transaction, parent, currPos, insert, attrib
|
|
|
392
394
|
*/
|
|
393
395
|
export const deleteText = (transaction, currPos, length) => {
|
|
394
396
|
const startLength = length
|
|
395
|
-
const
|
|
397
|
+
const startFormats = map.copy(currPos.currentFormats)
|
|
396
398
|
const start = currPos.right
|
|
397
399
|
while (length > 0 && currPos.right !== null) {
|
|
398
400
|
const item = currPos.right
|
|
@@ -428,7 +430,7 @@ export const deleteText = (transaction, currPos, length) => {
|
|
|
428
430
|
currPos.forward()
|
|
429
431
|
}
|
|
430
432
|
if (start) {
|
|
431
|
-
cleanupFormattingGap(transaction, start, currPos.right,
|
|
433
|
+
cleanupFormattingGap(transaction, start, currPos.right, startFormats, currPos.currentFormats)
|
|
432
434
|
}
|
|
433
435
|
const parent = /** @type {YType<any>} */ (/** @type {Item} */ (currPos.left || currPos.right).parent)
|
|
434
436
|
if (parent._searchMarker) {
|
|
@@ -631,14 +633,26 @@ export const callTypeObservers = (type, transaction, event) => {
|
|
|
631
633
|
}
|
|
632
634
|
|
|
633
635
|
/**
|
|
634
|
-
* Abstract Yjs Type class
|
|
636
|
+
* Abstract Yjs Type class.
|
|
637
|
+
*
|
|
638
|
+
* A `YType` is a {@link https://github.com/dmonad/lib0 lib0} `RDT` ("replicated data type", see
|
|
639
|
+
* `lib0/delta/rdt.js`): it emits a `'delta'` event whenever its state changes (carrying the change
|
|
640
|
+
* and the origin of the transaction that caused it), accepts foreign
|
|
641
|
+
* changes via {@link YType#applyDelta}, exposes its delta {@link YType#$delta schema}, and can be
|
|
642
|
+
* torn down via {@link YType#destroy}. This lets a `YType` be `bind()`-ed to any other RDT (another
|
|
643
|
+
* `YType`, an in-memory delta, a DOM subtree, …). The legacy {@link YType#observe `observe`} /
|
|
644
|
+
* {@link YType#observeDeep `observeDeep`} `YEvent` API continues to work alongside the `'delta'`
|
|
645
|
+
* channel.
|
|
646
|
+
*
|
|
635
647
|
* @template {delta.DeltaConf} [DConf=any]
|
|
648
|
+
* @extends {ObservableV2<{ delta: (delta: delta.Delta<DConf>, origin: any) => void, destroy: (type: YType<DConf>) => void }>}
|
|
636
649
|
*/
|
|
637
|
-
export class YType {
|
|
650
|
+
export class YType extends ObservableV2 {
|
|
638
651
|
/**
|
|
639
652
|
* @param {delta.DeltaConfGetName<DConf>?} name
|
|
640
653
|
*/
|
|
641
654
|
constructor (name = null) {
|
|
655
|
+
super()
|
|
642
656
|
/**
|
|
643
657
|
* @type {delta.DeltaConfGetName<DConf>}
|
|
644
658
|
*/
|
|
@@ -675,20 +689,127 @@ export class YType {
|
|
|
675
689
|
*/
|
|
676
690
|
this._searchMarker = null
|
|
677
691
|
/**
|
|
678
|
-
* @
|
|
679
|
-
*
|
|
692
|
+
* Maintained deep-delta cache backing {@link YType#delta}. `null` until `delta` is first
|
|
693
|
+
* accessed; thereafter kept current on every event of this type (incrementally, by applying the
|
|
694
|
+
* deep change) and re-diffed by {@link YType#useRenderer}. Cleared by {@link YType#clearCache}.
|
|
695
|
+
* @type {delta.DeltaBuilderAny | null}
|
|
680
696
|
*/
|
|
681
|
-
this.
|
|
697
|
+
this._delta = null
|
|
682
698
|
this._legacyTypeRef = this.name == null ? YXmlFragmentRefID : YXmlElementRefID
|
|
683
699
|
/**
|
|
684
700
|
* @type {Array<ArraySearchMarker>|null}
|
|
685
701
|
*/
|
|
686
702
|
this._searchMarker = []
|
|
687
703
|
/**
|
|
688
|
-
* Whether this YText contains
|
|
704
|
+
* Whether this YText contains formats.
|
|
689
705
|
* This flag is updated when a formatting item is integrated (see ContentFormat.integrate)
|
|
690
706
|
*/
|
|
691
707
|
this._hasFormatting = false
|
|
708
|
+
/**
|
|
709
|
+
* The active default renderer. Used by `toDelta`, `applyDelta`, and the events whenever no
|
|
710
|
+
* explicit renderer is passed. Change it via {@link YType#useRenderer}.
|
|
711
|
+
* @type {AbstractRenderer}
|
|
712
|
+
*/
|
|
713
|
+
this._renderer = baseRenderer
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
/**
|
|
717
|
+
* Schema of the deltas this type produces — part of the lib0 `RDT` interface.
|
|
718
|
+
*
|
|
719
|
+
* @type {s.Schema<delta.Delta<DConf>>}
|
|
720
|
+
*/
|
|
721
|
+
get $delta () {
|
|
722
|
+
return /** @type {any} */ (delta.$deltaAny)
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
/**
|
|
726
|
+
* The deep delta of this type (the full nested content tree, children rendered as their own
|
|
727
|
+
* deltas).
|
|
728
|
+
*
|
|
729
|
+
* The returned value is the type's **live** maintained cache: it is materialized on first access
|
|
730
|
+
* and then kept current on every event fired on this type (and re-diffed by
|
|
731
|
+
* {@link YType#useRenderer}), so a reference held across edits keeps updating in place. Clone it
|
|
732
|
+
* (e.g. `type.delta.clone()`) if you need a stable snapshot, and call {@link YType#clearCache} to
|
|
733
|
+
* drop the cache.
|
|
734
|
+
*
|
|
735
|
+
* Consider the returned delta **done** — it must not be edited from the outside. It is
|
|
736
|
+
* deliberately typed as a `Delta` (not a `DeltaBuilder`) so the mutating builder API is not
|
|
737
|
+
* reachable; editing it anyway would corrupt the cache without changing the CRDT. The proper way
|
|
738
|
+
* to change this type is {@link YType#applyDelta}.
|
|
739
|
+
*
|
|
740
|
+
* @type {delta.Delta<DConf>}
|
|
741
|
+
*/
|
|
742
|
+
get delta () {
|
|
743
|
+
if (this._delta === null) {
|
|
744
|
+
this._delta = this._renderDelta()
|
|
745
|
+
}
|
|
746
|
+
return /** @type {any} */ (this._delta)
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
/**
|
|
750
|
+
* Render the full deep current state into a fresh `isFinal` builder (so subsequent `.apply`s of
|
|
751
|
+
* deep changes update content in place). Uses this type's active renderer.
|
|
752
|
+
*
|
|
753
|
+
* @return {delta.DeltaBuilderAny}
|
|
754
|
+
*/
|
|
755
|
+
_renderDelta () {
|
|
756
|
+
const state = /** @type {delta.DeltaBuilderAny} */ (delta.create(this.name))
|
|
757
|
+
state.isFinal = true
|
|
758
|
+
state.apply(this.toDelta({ deep: true }))
|
|
759
|
+
return state
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
/**
|
|
763
|
+
* Discard the cached deep delta backing {@link YType#delta}.
|
|
764
|
+
*
|
|
765
|
+
* After `delta` is first accessed, the cache is updated on every event fired on this type (and
|
|
766
|
+
* re-diffed by {@link YType#useRenderer}). Call this to drop it — e.g. to reclaim memory, or to
|
|
767
|
+
* force an exact recomputation after editing while a non-base renderer is active (the incremental
|
|
768
|
+
* updates can drift from a fresh deep render in that case).
|
|
769
|
+
*/
|
|
770
|
+
clearCache () {
|
|
771
|
+
this._delta = null
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
/**
|
|
775
|
+
* Change the default renderer used by this type. After calling `useRenderer(renderer)`, the
|
|
776
|
+
* `toDelta`, `applyDelta`, and event methods all use `renderer` whenever no explicit renderer is
|
|
777
|
+
* passed (an explicit `{ renderer }` argument still overrides it per call).
|
|
778
|
+
*
|
|
779
|
+
* If the deep-delta cache ({@link YType#delta}) is being maintained, or a `'delta'` listener is
|
|
780
|
+
* attached, the content is re-rendered with the new renderer and the difference is emitted on the
|
|
781
|
+
* `'delta'` channel only (a renderer switch is not a CRDT change, so no `YEvent` is produced, and
|
|
782
|
+
* the emitted origin is `null` as no transaction is involved).
|
|
783
|
+
*
|
|
784
|
+
* @param {AbstractRenderer} renderer
|
|
785
|
+
* @return {this}
|
|
786
|
+
*/
|
|
787
|
+
useRenderer (renderer) {
|
|
788
|
+
const prev = this._renderer
|
|
789
|
+
const hasDeltaListeners = (this._observers.get('delta')?.size ?? 0) > 0
|
|
790
|
+
if (renderer !== prev && (this._delta !== null || hasDeltaListeners)) {
|
|
791
|
+
const oldState = this._delta ?? this._renderDelta()
|
|
792
|
+
this._renderer = renderer
|
|
793
|
+
const newState = this._renderDelta()
|
|
794
|
+
if (this._delta !== null) this._delta = newState
|
|
795
|
+
if (hasDeltaListeners) {
|
|
796
|
+
const d = /** @type {any} */ (delta.diff(/** @type {any} */ (oldState), /** @type {any} */ (newState)))
|
|
797
|
+
if (!d.isEmpty()) this.emit('delta', [d, null])
|
|
798
|
+
}
|
|
799
|
+
} else {
|
|
800
|
+
this._renderer = renderer
|
|
801
|
+
}
|
|
802
|
+
return this
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
/**
|
|
806
|
+
* Tear down this type as an `RDT`: emit the `'destroy'` event and unregister all `'delta'` /
|
|
807
|
+
* `'destroy'` listeners. The CRDT content and the `observe`/`observeDeep` handlers are left
|
|
808
|
+
* untouched — this only releases the RDT/binding observers.
|
|
809
|
+
*/
|
|
810
|
+
destroy () {
|
|
811
|
+
this.emit('destroy', [this])
|
|
812
|
+
super.destroy()
|
|
692
813
|
}
|
|
693
814
|
|
|
694
815
|
/**
|
|
@@ -760,6 +881,9 @@ export class YType {
|
|
|
760
881
|
_callObserver (transaction, parentSubs) {
|
|
761
882
|
const event = new YEvent(/** @type {any} */ (this), transaction, parentSubs)
|
|
762
883
|
callTypeObservers(/** @type {any} */ (this), transaction, event)
|
|
884
|
+
// Note: the RDT `'delta'` channel (and the deep-delta cache) is driven in the transaction
|
|
885
|
+
// cleanup's `changedParentTypes` loop (see Transaction.js) so it bubbles to ancestors like
|
|
886
|
+
// `observeDeep`, not here where only the directly-changed type is visible.
|
|
763
887
|
if (!transaction.local && this._searchMarker) {
|
|
764
888
|
this._searchMarker.length = 0
|
|
765
889
|
}
|
|
@@ -821,7 +945,7 @@ export class YType {
|
|
|
821
945
|
* @template {boolean} [Deep=false]
|
|
822
946
|
*
|
|
823
947
|
* @param {Object} [opts]
|
|
824
|
-
* @param {AbstractRenderer} [opts.renderer] - renders the content (with attributions); defaults to `baseRenderer`
|
|
948
|
+
* @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
|
|
825
949
|
* @param {IdSet?} [opts.itemsToRender]
|
|
826
950
|
* @param {boolean} [opts.retainInserts] - if true, retain rendered inserts with attributions
|
|
827
951
|
* @param {boolean} [opts.retainDeletes] - if true, retain rendered+attributed deletes only
|
|
@@ -833,7 +957,7 @@ export class YType {
|
|
|
833
957
|
* @public
|
|
834
958
|
*/
|
|
835
959
|
toDelta (opts = {}) {
|
|
836
|
-
const { renderer =
|
|
960
|
+
const { renderer = this._renderer, itemsToRender = null, retainInserts = false, retainDeletes = false, deletedItems = null, deep = false } = opts
|
|
837
961
|
const { modified = (deep && itemsToRender) ? computeModifiedFromItems(/** @type {Doc} */ (this.doc).store, itemsToRender) : null } = opts
|
|
838
962
|
const renderAttrs = modified?.get(this) || null
|
|
839
963
|
const renderChildren = modified == null || !modified.has(this) || /** @type {Set<string|null>} */ (modified.get(this)).has(null)
|
|
@@ -846,29 +970,29 @@ export class YType {
|
|
|
846
970
|
typeMapGetDelta(d, /** @type {any} */ (this), renderAttrs, renderer, deep, modified, deletedItems, itemsToRender, optsAll, optsAll)
|
|
847
971
|
if (renderChildren) {
|
|
848
972
|
/**
|
|
849
|
-
* @type {delta.
|
|
973
|
+
* @type {delta.Formats}
|
|
850
974
|
*/
|
|
851
|
-
let
|
|
852
|
-
let
|
|
975
|
+
let currentFormats = {} // saves all current formats for insert
|
|
976
|
+
let usingCurrentFormats = false
|
|
853
977
|
/**
|
|
854
|
-
* @type {delta.
|
|
978
|
+
* @type {delta.Formats}
|
|
855
979
|
*/
|
|
856
|
-
let
|
|
857
|
-
let
|
|
980
|
+
let changedFormats = {} // saves changed formats for retain
|
|
981
|
+
let usingChangedFormats = false
|
|
858
982
|
/**
|
|
859
|
-
* Logic for
|
|
860
|
-
* Everything that comes after
|
|
983
|
+
* Logic for format attribution
|
|
984
|
+
* Everything that comes after a format is formatted by the user that created it.
|
|
861
985
|
* Two exceptions:
|
|
862
986
|
* - the user resets formatting to the previously known formatting that is not attributed
|
|
863
|
-
* - the user deletes a
|
|
987
|
+
* - the user deletes a format and hence restores the previously known formatting
|
|
864
988
|
* that is not attributed.
|
|
865
|
-
* @type {delta.
|
|
989
|
+
* @type {delta.Formats}
|
|
866
990
|
*/
|
|
867
|
-
const
|
|
991
|
+
const previousUnattributedFormats = {} // contains previously known unattributed formatting
|
|
868
992
|
/**
|
|
869
|
-
* @type {delta.
|
|
993
|
+
* @type {delta.Formats}
|
|
870
994
|
*/
|
|
871
|
-
const
|
|
995
|
+
const previousFormats = {} // The value before changes
|
|
872
996
|
/**
|
|
873
997
|
* @type {Array<AttributedContent<any>>}
|
|
874
998
|
*/
|
|
@@ -897,11 +1021,11 @@ export class YType {
|
|
|
897
1021
|
// render (attributed) content even if it was deleted
|
|
898
1022
|
const renderContent = c.render && (!c.deleted || c.attrs != null)
|
|
899
1023
|
// content that was just deleted. It is not rendered as an insertion, because it doesn't
|
|
900
|
-
// have any
|
|
1024
|
+
// have any formats.
|
|
901
1025
|
const renderDelete = c.render && c.deleted
|
|
902
|
-
// existing content that should be retained, only adding changed
|
|
1026
|
+
// existing content that should be retained, only adding changed formats
|
|
903
1027
|
const retainContent = !c.render && (!c.deleted || c.attrs != null)
|
|
904
|
-
const attribution = (renderContent || c.content.constructor === ContentFormat) ? createAttributionFromAttributionItems(c.attrs, c.deleted) :
|
|
1028
|
+
const attribution = (renderContent || c.content.constructor === ContentFormat) ? createAttributionFromAttributionItems(c.attrs, c.deleted) : undefined
|
|
905
1029
|
switch (c.content.constructor) {
|
|
906
1030
|
case ContentDeleted: {
|
|
907
1031
|
if (renderDelete) d.delete(c.content.getLength())
|
|
@@ -909,18 +1033,26 @@ export class YType {
|
|
|
909
1033
|
}
|
|
910
1034
|
case ContentString:
|
|
911
1035
|
if (renderContent) {
|
|
912
|
-
d.usedAttributes = currentAttributes
|
|
913
|
-
usingCurrentAttributes = true
|
|
914
1036
|
if (c.deleted ? retainDeletes : retainInserts) {
|
|
915
|
-
|
|
1037
|
+
// a retain expresses the format *diff* against existing (cached) content, so use
|
|
1038
|
+
// `changedFormats`: a format removed this change (e.g. its marker was deleted)
|
|
1039
|
+
// is present there as a `null` clear, whereas `currentFormats` (absolute) can
|
|
1040
|
+
// only re-assert present formats and would silently keep a stale one.
|
|
1041
|
+
d.usedFormats = changedFormats
|
|
1042
|
+
usingChangedFormats = true
|
|
1043
|
+
// change render: a retained item with no attribution means its attribution was
|
|
1044
|
+
// removed → emit `null` (clear) rather than `{}` (skip). Present attribution merges.
|
|
1045
|
+
d.retain(/** @type {ContentString} */ (c.content).str.length, undefined, attribution ?? null)
|
|
916
1046
|
} else {
|
|
917
|
-
d.
|
|
1047
|
+
d.usedFormats = currentFormats
|
|
1048
|
+
usingCurrentFormats = true
|
|
1049
|
+
d.insert(/** @type {ContentString} */ (c.content).str, undefined, attribution)
|
|
918
1050
|
}
|
|
919
1051
|
} else if (renderDelete) {
|
|
920
1052
|
d.delete(c.content.getLength())
|
|
921
1053
|
} else if (retainContent) {
|
|
922
|
-
d.
|
|
923
|
-
|
|
1054
|
+
d.usedFormats = changedFormats
|
|
1055
|
+
usingChangedFormats = true
|
|
924
1056
|
d.retain(c.content.getLength())
|
|
925
1057
|
}
|
|
926
1058
|
break
|
|
@@ -930,19 +1062,24 @@ export class YType {
|
|
|
930
1062
|
case ContentType:
|
|
931
1063
|
case ContentBinary:
|
|
932
1064
|
if (renderContent) {
|
|
933
|
-
d.usedAttributes = currentAttributes
|
|
934
|
-
usingCurrentAttributes = true
|
|
935
1065
|
if (c.deleted ? retainDeletes : retainInserts) {
|
|
1066
|
+
// a retain expresses the format *diff* → use `changedFormats` (see ContentString)
|
|
1067
|
+
d.usedFormats = changedFormats
|
|
1068
|
+
usingChangedFormats = true
|
|
936
1069
|
if (c.deleted && c.content.constructor === ContentType) {
|
|
937
1070
|
// @todo use current transaction instead
|
|
938
|
-
d.modify(/** @type {any} */ (c.content).type.toDelta(optsAll),
|
|
1071
|
+
d.modify(/** @type {any} */ (c.content).type.toDelta(optsAll), undefined, attribution ?? null)
|
|
939
1072
|
} else {
|
|
940
|
-
d.retain(c.content.getLength(),
|
|
1073
|
+
d.retain(c.content.getLength(), undefined, attribution ?? null)
|
|
941
1074
|
}
|
|
942
1075
|
} else if (deep && c.content.constructor === ContentType) {
|
|
943
|
-
d.
|
|
1076
|
+
d.usedFormats = currentFormats
|
|
1077
|
+
usingCurrentFormats = true
|
|
1078
|
+
d.insert([/** @type {any} */(c.content).type.toDelta(optsAll)], undefined, attribution)
|
|
944
1079
|
} else {
|
|
945
|
-
d.
|
|
1080
|
+
d.usedFormats = currentFormats
|
|
1081
|
+
usingCurrentFormats = true
|
|
1082
|
+
d.insert(c.content.getContent(), undefined, attribution)
|
|
946
1083
|
}
|
|
947
1084
|
} else if (renderDelete) {
|
|
948
1085
|
d.delete(1)
|
|
@@ -951,98 +1088,127 @@ export class YType {
|
|
|
951
1088
|
// @todo use current transaction instead
|
|
952
1089
|
d.modify(/** @type {any} */ (c.content).type.toDelta(optsAll))
|
|
953
1090
|
} else {
|
|
954
|
-
d.
|
|
955
|
-
|
|
1091
|
+
d.usedFormats = changedFormats
|
|
1092
|
+
usingChangedFormats = true
|
|
956
1093
|
d.retain(1)
|
|
957
1094
|
}
|
|
958
1095
|
}
|
|
959
1096
|
break
|
|
960
1097
|
case ContentFormat: {
|
|
961
1098
|
const { key, value } = /** @type {ContentFormat} */ (c.content)
|
|
962
|
-
const
|
|
963
|
-
if (attribution != null && (c.deleted || !object.hasProperty(
|
|
964
|
-
|
|
1099
|
+
const currFormatVal = currentFormats[key] ?? null
|
|
1100
|
+
if (attribution != null && (c.deleted || !object.hasProperty(previousUnattributedFormats, key))) {
|
|
1101
|
+
previousUnattributedFormats[key] = c.deleted ? value : currFormatVal
|
|
965
1102
|
}
|
|
966
|
-
// @todo write a function "
|
|
967
|
-
// # Update
|
|
1103
|
+
// @todo write a function "updateCurrentFormats" and "updateChangedFormats"
|
|
1104
|
+
// # Update Formats
|
|
968
1105
|
if (renderContent || renderDelete) {
|
|
969
1106
|
// create fresh references
|
|
970
|
-
if (
|
|
971
|
-
|
|
972
|
-
|
|
1107
|
+
if (usingCurrentFormats) {
|
|
1108
|
+
currentFormats = object.assign({}, currentFormats)
|
|
1109
|
+
usingCurrentFormats = false
|
|
973
1110
|
}
|
|
974
|
-
if (
|
|
975
|
-
|
|
976
|
-
|
|
1111
|
+
if (usingChangedFormats) {
|
|
1112
|
+
usingChangedFormats = false
|
|
1113
|
+
changedFormats = object.assign({}, changedFormats)
|
|
977
1114
|
}
|
|
978
1115
|
}
|
|
979
1116
|
if (renderContent || renderDelete) {
|
|
980
1117
|
if (c.deleted) {
|
|
981
1118
|
// content was deleted, but is possibly attributed
|
|
982
|
-
if (!
|
|
983
|
-
if (
|
|
984
|
-
delete
|
|
1119
|
+
if (!equalFormats(value, currFormatVal)) { // do nothing if nothing changed
|
|
1120
|
+
if (equalFormats(currFormatVal, previousFormats[key] ?? null) && changedFormats[key] !== undefined) {
|
|
1121
|
+
delete changedFormats[key]
|
|
985
1122
|
} else {
|
|
986
|
-
|
|
1123
|
+
changedFormats[key] = currFormatVal
|
|
987
1124
|
}
|
|
988
|
-
// current
|
|
989
|
-
|
|
1125
|
+
// current formats doesn't change
|
|
1126
|
+
previousFormats[key] = value
|
|
990
1127
|
}
|
|
991
1128
|
} else { // !c.deleted
|
|
992
1129
|
// content was inserted, and is possibly attributed
|
|
993
|
-
if (
|
|
1130
|
+
if (equalFormats(value, currFormatVal)) {
|
|
994
1131
|
// item.delete(transaction)
|
|
995
|
-
} else if (
|
|
996
|
-
delete
|
|
1132
|
+
} else if (equalFormats(value, previousFormats[key] ?? null)) {
|
|
1133
|
+
delete changedFormats[key]
|
|
997
1134
|
} else {
|
|
998
|
-
|
|
1135
|
+
changedFormats[key] = value
|
|
999
1136
|
}
|
|
1000
1137
|
if (value == null) {
|
|
1001
|
-
delete
|
|
1138
|
+
delete currentFormats[key]
|
|
1002
1139
|
} else {
|
|
1003
|
-
|
|
1140
|
+
currentFormats[key] = value
|
|
1004
1141
|
}
|
|
1005
1142
|
}
|
|
1006
1143
|
} else if (retainContent && !c.deleted) {
|
|
1007
|
-
// fresh reference to
|
|
1008
|
-
if (
|
|
1009
|
-
|
|
1010
|
-
|
|
1144
|
+
// fresh reference to currentFormats only
|
|
1145
|
+
if (usingCurrentFormats) {
|
|
1146
|
+
currentFormats = object.assign({}, currentFormats)
|
|
1147
|
+
usingCurrentFormats = false
|
|
1011
1148
|
}
|
|
1012
|
-
if (
|
|
1013
|
-
|
|
1014
|
-
|
|
1149
|
+
if (usingChangedFormats && changedFormats[key] !== undefined) {
|
|
1150
|
+
usingChangedFormats = false
|
|
1151
|
+
changedFormats = object.assign({}, changedFormats)
|
|
1015
1152
|
}
|
|
1016
1153
|
if (value == null) {
|
|
1017
|
-
delete
|
|
1154
|
+
delete currentFormats[key]
|
|
1018
1155
|
} else {
|
|
1019
|
-
|
|
1156
|
+
currentFormats[key] = value
|
|
1020
1157
|
}
|
|
1021
|
-
delete
|
|
1022
|
-
|
|
1158
|
+
delete changedFormats[key]
|
|
1159
|
+
previousFormats[key] = value
|
|
1023
1160
|
}
|
|
1024
1161
|
// # Update Attributions
|
|
1025
|
-
|
|
1162
|
+
// A format marker deleted in a change render under an *attributing* renderer nets to no
|
|
1163
|
+
// attribution (its insert+delete suggestion cancels → `attribution == null`), yet it ends
|
|
1164
|
+
// the attributed range it opened: the following retained content must drop the stale
|
|
1165
|
+
// `{ format: { [key]: [] } }` the marker's insertion wrote to the cache. Emit an explicit
|
|
1166
|
+
// `null` leaf for the key (a context-wide `useAttribution(null)` cannot carry a per-key
|
|
1167
|
+
// clear). Conditions: only an attributing render (`renderer !== baseRenderer`; the base
|
|
1168
|
+
// renderer has no attributions to clear), and only when the deletion actually *removes*
|
|
1169
|
+
// the format — i.e. it reverts to no value (`currFormatVal == null`). If it reverts to a
|
|
1170
|
+
// still-present surrounding value (e.g. deleting a `bold:null` marker re-exposes an
|
|
1171
|
+
// enclosing attributed `bold:true`, as when re-bolding), the attribution is preserved.
|
|
1172
|
+
const isDeletedFormatClear = attribution == null && renderer !== baseRenderer && renderDelete && c.deleted && itemsToRender != null && currFormatVal == null && !equalFormats(value, currFormatVal)
|
|
1173
|
+
if (attribution != null || isDeletedFormatClear || object.hasProperty(previousUnattributedFormats, key)) {
|
|
1026
1174
|
/**
|
|
1027
1175
|
* @type {Attribution}
|
|
1028
1176
|
*/
|
|
1029
1177
|
const formattingAttribution = object.assign({}, d.usedAttribution)
|
|
1030
|
-
const
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
delete
|
|
1035
|
-
|
|
1178
|
+
const changedAttributedFormats = /** @type {{ [key: string]: Array<any>|null }} */ (formattingAttribution.format = object.assign({}, formattingAttribution.format ?? {}))
|
|
1179
|
+
const sameAsPreviousAttributions = equalFormats(previousUnattributedFormats[key], currentFormats[key] ?? null)
|
|
1180
|
+
if (isDeletedFormatClear) {
|
|
1181
|
+
changedAttributedFormats[key] = null
|
|
1182
|
+
delete previousUnattributedFormats[key]
|
|
1183
|
+
} else if (attribution == null && !sameAsPreviousAttributions) {
|
|
1184
|
+
// skip
|
|
1185
|
+
} else if (attribution == null || sameAsPreviousAttributions) {
|
|
1186
|
+
// an unattributed format was found or an attributed format
|
|
1187
|
+
// was found that resets to the previous status. When this format item is
|
|
1188
|
+
// itself rendered this transaction (`renderContent || renderDelete`) in a change/diff
|
|
1189
|
+
// render (`itemsToRender != null`), it is the END of an attributed format range:
|
|
1190
|
+
// emit an explicit clear (a `null` leaf) so the retained content drops any stale
|
|
1191
|
+
// `{ format: { [key]: [] } }` from the maintained `delta` cache — a bare context-skip
|
|
1192
|
+
// (`delete`) would leave it in place. For a merely-retained (unchanged) boundary, or a
|
|
1193
|
+
// full insert render (removal is already modeled as absence in `currentFormats`),
|
|
1194
|
+
// just drop the key: a change render must not emit ops for unchanged ranges, and
|
|
1195
|
+
// inserts must stay free of a spurious `{ format: { [key]: null } }`.
|
|
1196
|
+
if (attribution != null && itemsToRender != null && (renderContent || renderDelete)) {
|
|
1197
|
+
changedAttributedFormats[key] = null
|
|
1198
|
+
} else {
|
|
1199
|
+
delete changedAttributedFormats[key]
|
|
1200
|
+
}
|
|
1201
|
+
delete previousUnattributedFormats[key]
|
|
1036
1202
|
} else {
|
|
1037
|
-
const by =
|
|
1203
|
+
const by = changedAttributedFormats[key] = (changedAttributedFormats[key]?.slice() ?? [])
|
|
1038
1204
|
by.push(...((c.deleted ? attribution.delete : attribution.insert) ?? []))
|
|
1039
1205
|
const attributedAt = (c.deleted ? attribution.deleteAt : attribution.insertAt)
|
|
1040
1206
|
if (attributedAt) formattingAttribution.formatAt = attributedAt
|
|
1041
1207
|
}
|
|
1042
|
-
if (object.isEmpty(
|
|
1208
|
+
if (object.isEmpty(changedAttributedFormats)) {
|
|
1043
1209
|
d.useAttribution(null)
|
|
1044
|
-
} else if (attribution != null) {
|
|
1045
|
-
const attributedAt = (c.deleted ? attribution
|
|
1210
|
+
} else if (attribution != null || isDeletedFormatClear) {
|
|
1211
|
+
const attributedAt = (c.deleted ? attribution?.deleteAt : attribution?.insertAt)
|
|
1046
1212
|
if (attributedAt != null) formattingAttribution.formatAt = attributedAt
|
|
1047
1213
|
d.useAttribution(formattingAttribution)
|
|
1048
1214
|
}
|
|
@@ -1061,7 +1227,7 @@ export class YType {
|
|
|
1061
1227
|
* attributions.
|
|
1062
1228
|
*
|
|
1063
1229
|
* @param {Object} [opts]
|
|
1064
|
-
* @param {AbstractRenderer} [opts.renderer] - renders the content (with attributions); defaults to `baseRenderer`
|
|
1230
|
+
* @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
|
|
1065
1231
|
* @return {delta.Delta<DConf>}
|
|
1066
1232
|
*/
|
|
1067
1233
|
toDeltaDeep (opts = {}) {
|
|
@@ -1073,11 +1239,14 @@ export class YType {
|
|
|
1073
1239
|
*
|
|
1074
1240
|
* @param {delta.DeltaAny} d The changes to apply on this element.
|
|
1075
1241
|
* @param {Object} [opts]
|
|
1076
|
-
* @param {AbstractRenderer} [opts.renderer] - renders the content (with attributions); defaults to `baseRenderer`
|
|
1242
|
+
* @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
|
|
1243
|
+
* @return {null} The lib0 `RDT` "fix" of this apply — always `null`: a `YType` accepts every valid
|
|
1244
|
+
* delta as-is and never needs to self-correct.
|
|
1077
1245
|
*
|
|
1078
1246
|
* @public
|
|
1079
1247
|
*/
|
|
1080
|
-
applyDelta (d, { renderer =
|
|
1248
|
+
applyDelta (d, { renderer = this._renderer } = {}) {
|
|
1249
|
+
if (d.isEmpty()) return null
|
|
1081
1250
|
if (this.doc == null) {
|
|
1082
1251
|
(this._prelim || (this._prelim = /** @type {any} */ (delta.create()))).apply(d)
|
|
1083
1252
|
} else if (this._item?.deleted !== true) {
|
|
@@ -1118,7 +1287,7 @@ export class YType {
|
|
|
1118
1287
|
}
|
|
1119
1288
|
})
|
|
1120
1289
|
}
|
|
1121
|
-
return
|
|
1290
|
+
return null
|
|
1122
1291
|
}
|
|
1123
1292
|
|
|
1124
1293
|
/**
|
|
@@ -1224,7 +1393,7 @@ export class YType {
|
|
|
1224
1393
|
*
|
|
1225
1394
|
* @param {number} index The index to insert content at.
|
|
1226
1395
|
* @param {Array<delta.DeltaConfGetChildren<DConf>>|delta.DeltaConfGetText<DConf>} content Array of content to append.
|
|
1227
|
-
* @param {delta.
|
|
1396
|
+
* @param {delta.Formats} [format]
|
|
1228
1397
|
*/
|
|
1229
1398
|
insert (index, content, format) {
|
|
1230
1399
|
this.applyDelta(delta.create().retain(index).insert(/** @type {any} */ (content), format).done())
|
|
@@ -1245,7 +1414,7 @@ export class YType {
|
|
|
1245
1414
|
*
|
|
1246
1415
|
* @param {number} index The index to insert content at.
|
|
1247
1416
|
* @param {number} length The index to insert content at.
|
|
1248
|
-
* @param {delta.
|
|
1417
|
+
* @param {delta.Formats} formats
|
|
1249
1418
|
*
|
|
1250
1419
|
*/
|
|
1251
1420
|
format (index, length, formats) {
|
|
@@ -1519,7 +1688,7 @@ export const computeModifiedFromItems = (store, items) => {
|
|
|
1519
1688
|
* @param {any} b
|
|
1520
1689
|
* @return {boolean}
|
|
1521
1690
|
*/
|
|
1522
|
-
export const
|
|
1691
|
+
export const equalFormats = (a, b) => a === b || (typeof a === 'object' && typeof b === 'object' && a && b && object.equalFlat(a, b))
|
|
1523
1692
|
|
|
1524
1693
|
/**
|
|
1525
1694
|
* @template {delta.DeltaConf} DConf
|
|
@@ -1922,25 +2091,26 @@ export const typeMapGetDelta = (d, parent, attrsToRender, renderer, deep, modifi
|
|
|
1922
2091
|
*/
|
|
1923
2092
|
const cs = []
|
|
1924
2093
|
renderer.readContent(cs, item.id.client, item.id.clock, item.deleted, item.content, 1)
|
|
1925
|
-
|
|
1926
|
-
|
|
2094
|
+
if (cs.length === 0) return // the renderer surfaces nothing for this attribute (e.g. a diff renderer hiding an unchanged delete)
|
|
2095
|
+
const { deleted, attrs, content } = cs[cs.length - 1]
|
|
1927
2096
|
const attribution = createAttributionFromAttributionItems(attrs, deleted)
|
|
1928
2097
|
let c = array.last(content.getContent())
|
|
1929
2098
|
if (deleted) {
|
|
1930
|
-
if (
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
// (positive `InsertOp` with attribution, never `DeleteOp`).
|
|
2099
|
+
if (attribution != null) {
|
|
2100
|
+
// Item surfaced under attribution (suggestion view / diff renderer, either in snapshot mode
|
|
2101
|
+
// or in an event-driven render). The attribute is still observable in the rendered state, so
|
|
2102
|
+
// emit a positive `SetAttrOp` carrying the attribution metadata - matching how content
|
|
2103
|
+
// children are rendered for the same case (positive `InsertOp` with attribution, never
|
|
2104
|
+
// `DeleteOp`).
|
|
2105
|
+
if (itemsToRender == null || itemsToRender.hasId(item.lastId)) {
|
|
1938
2106
|
d.setAttr(key, c, attribution)
|
|
1939
|
-
} else {
|
|
1940
|
-
// Hard-deleted attribute (no AM-surfaced attribution): emit the
|
|
1941
|
-
// change op so event consumers can apply it.
|
|
1942
|
-
d.deleteAttr(key, attribution, c)
|
|
1943
2107
|
}
|
|
2108
|
+
} else if (itemsToRender != null && itemsToRender.hasId(item.lastId)) {
|
|
2109
|
+
// Hard-deleted attribute within a change render: emit the `deleteAttr` op so consumers (the
|
|
2110
|
+
// `YEvent` delta, RDT bindings, the maintained `delta` cache) can apply the removal. In
|
|
2111
|
+
// full-state mode (`itemsToRender == null`) the attribute is simply omitted (above renders
|
|
2112
|
+
// run with `render === false` for such items, so nothing was emitted before either).
|
|
2113
|
+
d.deleteAttr(key, attribution, c)
|
|
1944
2114
|
}
|
|
1945
2115
|
} else if (deep && c instanceof YType && modified?.has(c)) {
|
|
1946
2116
|
d.modifyAttr(key, c.toDelta(opts))
|