@y/y 14.0.0-rc.20 → 14.0.0-rc.21
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 +96 -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 +327 -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,25 @@ 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, accepts foreign
|
|
640
|
+
* changes via {@link YType#applyDelta}, exposes its delta {@link YType#$delta schema}, and can be
|
|
641
|
+
* torn down via {@link YType#destroy}. This lets a `YType` be `bind()`-ed to any other RDT (another
|
|
642
|
+
* `YType`, an in-memory delta, a DOM subtree, …). The legacy {@link YType#observe `observe`} /
|
|
643
|
+
* {@link YType#observeDeep `observeDeep`} `YEvent` API continues to work alongside the `'delta'`
|
|
644
|
+
* channel.
|
|
645
|
+
*
|
|
635
646
|
* @template {delta.DeltaConf} [DConf=any]
|
|
647
|
+
* @extends {ObservableV2<{ delta: (delta: delta.Delta<DConf>) => void, destroy: (type: YType<DConf>) => void }>}
|
|
636
648
|
*/
|
|
637
|
-
export class YType {
|
|
649
|
+
export class YType extends ObservableV2 {
|
|
638
650
|
/**
|
|
639
651
|
* @param {delta.DeltaConfGetName<DConf>?} name
|
|
640
652
|
*/
|
|
641
653
|
constructor (name = null) {
|
|
654
|
+
super()
|
|
642
655
|
/**
|
|
643
656
|
* @type {delta.DeltaConfGetName<DConf>}
|
|
644
657
|
*/
|
|
@@ -675,20 +688,121 @@ export class YType {
|
|
|
675
688
|
*/
|
|
676
689
|
this._searchMarker = null
|
|
677
690
|
/**
|
|
678
|
-
* @
|
|
679
|
-
*
|
|
691
|
+
* Maintained deep-delta cache backing {@link YType#delta}. `null` until `delta` is first
|
|
692
|
+
* accessed; thereafter kept current on every event of this type (incrementally, by applying the
|
|
693
|
+
* deep change) and re-diffed by {@link YType#useRenderer}. Cleared by {@link YType#clearCache}.
|
|
694
|
+
* @type {delta.DeltaBuilderAny | null}
|
|
680
695
|
*/
|
|
681
|
-
this.
|
|
696
|
+
this._delta = null
|
|
682
697
|
this._legacyTypeRef = this.name == null ? YXmlFragmentRefID : YXmlElementRefID
|
|
683
698
|
/**
|
|
684
699
|
* @type {Array<ArraySearchMarker>|null}
|
|
685
700
|
*/
|
|
686
701
|
this._searchMarker = []
|
|
687
702
|
/**
|
|
688
|
-
* Whether this YText contains
|
|
703
|
+
* Whether this YText contains formats.
|
|
689
704
|
* This flag is updated when a formatting item is integrated (see ContentFormat.integrate)
|
|
690
705
|
*/
|
|
691
706
|
this._hasFormatting = false
|
|
707
|
+
/**
|
|
708
|
+
* The active default renderer. Used by `toDelta`, `applyDelta`, and the events whenever no
|
|
709
|
+
* explicit renderer is passed. Change it via {@link YType#useRenderer}.
|
|
710
|
+
* @type {AbstractRenderer}
|
|
711
|
+
*/
|
|
712
|
+
this._renderer = baseRenderer
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
/**
|
|
716
|
+
* Schema of the deltas this type produces — part of the lib0 `RDT` interface.
|
|
717
|
+
*
|
|
718
|
+
* @type {s.Schema<delta.Delta<DConf>>}
|
|
719
|
+
*/
|
|
720
|
+
get $delta () {
|
|
721
|
+
return /** @type {any} */ (delta.$deltaAny)
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
/**
|
|
725
|
+
* The deep delta of this type (the full nested content tree, children rendered as their own
|
|
726
|
+
* deltas).
|
|
727
|
+
*
|
|
728
|
+
* The returned value is the type's **live** maintained cache: it is materialized on first access
|
|
729
|
+
* and then kept current on every event fired on this type (and re-diffed by
|
|
730
|
+
* {@link YType#useRenderer}), so a reference held across edits keeps updating in place. Clone it
|
|
731
|
+
* (e.g. `type.delta.clone()`) if you need a stable snapshot, and call {@link YType#clearCache} to
|
|
732
|
+
* drop the cache.
|
|
733
|
+
*
|
|
734
|
+
* @type {delta.Delta<DConf>}
|
|
735
|
+
*/
|
|
736
|
+
get delta () {
|
|
737
|
+
if (this._delta === null) {
|
|
738
|
+
this._delta = this._renderDelta()
|
|
739
|
+
}
|
|
740
|
+
return /** @type {any} */ (this._delta)
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
/**
|
|
744
|
+
* Render the full deep current state into a fresh `isFinal` builder (so subsequent `.apply`s of
|
|
745
|
+
* deep changes update content in place). Uses this type's active renderer.
|
|
746
|
+
*
|
|
747
|
+
* @return {delta.DeltaBuilderAny}
|
|
748
|
+
*/
|
|
749
|
+
_renderDelta () {
|
|
750
|
+
const state = /** @type {delta.DeltaBuilderAny} */ (delta.create(this.name))
|
|
751
|
+
state.isFinal = true
|
|
752
|
+
state.apply(this.toDelta({ deep: true }))
|
|
753
|
+
return state
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
/**
|
|
757
|
+
* Discard the cached deep delta backing {@link YType#delta}.
|
|
758
|
+
*
|
|
759
|
+
* After `delta` is first accessed, the cache is updated on every event fired on this type (and
|
|
760
|
+
* re-diffed by {@link YType#useRenderer}). Call this to drop it — e.g. to reclaim memory, or to
|
|
761
|
+
* force an exact recomputation after editing while a non-base renderer is active (the incremental
|
|
762
|
+
* updates can drift from a fresh deep render in that case).
|
|
763
|
+
*/
|
|
764
|
+
clearCache () {
|
|
765
|
+
this._delta = null
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
/**
|
|
769
|
+
* Change the default renderer used by this type. After calling `useRenderer(renderer)`, the
|
|
770
|
+
* `toDelta`, `applyDelta`, and event methods all use `renderer` whenever no explicit renderer is
|
|
771
|
+
* passed (an explicit `{ renderer }` argument still overrides it per call).
|
|
772
|
+
*
|
|
773
|
+
* If the deep-delta cache ({@link YType#delta}) is being maintained, or a `'delta'` listener is
|
|
774
|
+
* attached, the content is re-rendered with the new renderer and the difference is emitted on the
|
|
775
|
+
* `'delta'` channel only (a renderer switch is not a CRDT change, so no `YEvent` is produced).
|
|
776
|
+
*
|
|
777
|
+
* @param {AbstractRenderer} renderer
|
|
778
|
+
* @return {this}
|
|
779
|
+
*/
|
|
780
|
+
useRenderer (renderer) {
|
|
781
|
+
const prev = this._renderer
|
|
782
|
+
const hasDeltaListeners = (this._observers.get('delta')?.size ?? 0) > 0
|
|
783
|
+
if (renderer !== prev && (this._delta !== null || hasDeltaListeners)) {
|
|
784
|
+
const oldState = this._delta ?? this._renderDelta()
|
|
785
|
+
this._renderer = renderer
|
|
786
|
+
const newState = this._renderDelta()
|
|
787
|
+
if (this._delta !== null) this._delta = newState
|
|
788
|
+
if (hasDeltaListeners) {
|
|
789
|
+
const d = /** @type {any} */ (delta.diff(/** @type {any} */ (oldState), /** @type {any} */ (newState)))
|
|
790
|
+
if (!d.isEmpty()) this.emit('delta', [d])
|
|
791
|
+
}
|
|
792
|
+
} else {
|
|
793
|
+
this._renderer = renderer
|
|
794
|
+
}
|
|
795
|
+
return this
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
/**
|
|
799
|
+
* Tear down this type as an `RDT`: emit the `'destroy'` event and unregister all `'delta'` /
|
|
800
|
+
* `'destroy'` listeners. The CRDT content and the `observe`/`observeDeep` handlers are left
|
|
801
|
+
* untouched — this only releases the RDT/binding observers.
|
|
802
|
+
*/
|
|
803
|
+
destroy () {
|
|
804
|
+
this.emit('destroy', [this])
|
|
805
|
+
super.destroy()
|
|
692
806
|
}
|
|
693
807
|
|
|
694
808
|
/**
|
|
@@ -760,6 +874,9 @@ export class YType {
|
|
|
760
874
|
_callObserver (transaction, parentSubs) {
|
|
761
875
|
const event = new YEvent(/** @type {any} */ (this), transaction, parentSubs)
|
|
762
876
|
callTypeObservers(/** @type {any} */ (this), transaction, event)
|
|
877
|
+
// Note: the RDT `'delta'` channel (and the deep-delta cache) is driven in the transaction
|
|
878
|
+
// cleanup's `changedParentTypes` loop (see Transaction.js) so it bubbles to ancestors like
|
|
879
|
+
// `observeDeep`, not here where only the directly-changed type is visible.
|
|
763
880
|
if (!transaction.local && this._searchMarker) {
|
|
764
881
|
this._searchMarker.length = 0
|
|
765
882
|
}
|
|
@@ -821,7 +938,7 @@ export class YType {
|
|
|
821
938
|
* @template {boolean} [Deep=false]
|
|
822
939
|
*
|
|
823
940
|
* @param {Object} [opts]
|
|
824
|
-
* @param {AbstractRenderer} [opts.renderer] - renders the content (with attributions); defaults to `baseRenderer`
|
|
941
|
+
* @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
942
|
* @param {IdSet?} [opts.itemsToRender]
|
|
826
943
|
* @param {boolean} [opts.retainInserts] - if true, retain rendered inserts with attributions
|
|
827
944
|
* @param {boolean} [opts.retainDeletes] - if true, retain rendered+attributed deletes only
|
|
@@ -833,7 +950,7 @@ export class YType {
|
|
|
833
950
|
* @public
|
|
834
951
|
*/
|
|
835
952
|
toDelta (opts = {}) {
|
|
836
|
-
const { renderer =
|
|
953
|
+
const { renderer = this._renderer, itemsToRender = null, retainInserts = false, retainDeletes = false, deletedItems = null, deep = false } = opts
|
|
837
954
|
const { modified = (deep && itemsToRender) ? computeModifiedFromItems(/** @type {Doc} */ (this.doc).store, itemsToRender) : null } = opts
|
|
838
955
|
const renderAttrs = modified?.get(this) || null
|
|
839
956
|
const renderChildren = modified == null || !modified.has(this) || /** @type {Set<string|null>} */ (modified.get(this)).has(null)
|
|
@@ -846,29 +963,29 @@ export class YType {
|
|
|
846
963
|
typeMapGetDelta(d, /** @type {any} */ (this), renderAttrs, renderer, deep, modified, deletedItems, itemsToRender, optsAll, optsAll)
|
|
847
964
|
if (renderChildren) {
|
|
848
965
|
/**
|
|
849
|
-
* @type {delta.
|
|
966
|
+
* @type {delta.Formats}
|
|
850
967
|
*/
|
|
851
|
-
let
|
|
852
|
-
let
|
|
968
|
+
let currentFormats = {} // saves all current formats for insert
|
|
969
|
+
let usingCurrentFormats = false
|
|
853
970
|
/**
|
|
854
|
-
* @type {delta.
|
|
971
|
+
* @type {delta.Formats}
|
|
855
972
|
*/
|
|
856
|
-
let
|
|
857
|
-
let
|
|
973
|
+
let changedFormats = {} // saves changed formats for retain
|
|
974
|
+
let usingChangedFormats = false
|
|
858
975
|
/**
|
|
859
|
-
* Logic for
|
|
860
|
-
* Everything that comes after
|
|
976
|
+
* Logic for format attribution
|
|
977
|
+
* Everything that comes after a format is formatted by the user that created it.
|
|
861
978
|
* Two exceptions:
|
|
862
979
|
* - the user resets formatting to the previously known formatting that is not attributed
|
|
863
|
-
* - the user deletes a
|
|
980
|
+
* - the user deletes a format and hence restores the previously known formatting
|
|
864
981
|
* that is not attributed.
|
|
865
|
-
* @type {delta.
|
|
982
|
+
* @type {delta.Formats}
|
|
866
983
|
*/
|
|
867
|
-
const
|
|
984
|
+
const previousUnattributedFormats = {} // contains previously known unattributed formatting
|
|
868
985
|
/**
|
|
869
|
-
* @type {delta.
|
|
986
|
+
* @type {delta.Formats}
|
|
870
987
|
*/
|
|
871
|
-
const
|
|
988
|
+
const previousFormats = {} // The value before changes
|
|
872
989
|
/**
|
|
873
990
|
* @type {Array<AttributedContent<any>>}
|
|
874
991
|
*/
|
|
@@ -897,11 +1014,11 @@ export class YType {
|
|
|
897
1014
|
// render (attributed) content even if it was deleted
|
|
898
1015
|
const renderContent = c.render && (!c.deleted || c.attrs != null)
|
|
899
1016
|
// content that was just deleted. It is not rendered as an insertion, because it doesn't
|
|
900
|
-
// have any
|
|
1017
|
+
// have any formats.
|
|
901
1018
|
const renderDelete = c.render && c.deleted
|
|
902
|
-
// existing content that should be retained, only adding changed
|
|
1019
|
+
// existing content that should be retained, only adding changed formats
|
|
903
1020
|
const retainContent = !c.render && (!c.deleted || c.attrs != null)
|
|
904
|
-
const attribution = (renderContent || c.content.constructor === ContentFormat) ? createAttributionFromAttributionItems(c.attrs, c.deleted) :
|
|
1021
|
+
const attribution = (renderContent || c.content.constructor === ContentFormat) ? createAttributionFromAttributionItems(c.attrs, c.deleted) : undefined
|
|
905
1022
|
switch (c.content.constructor) {
|
|
906
1023
|
case ContentDeleted: {
|
|
907
1024
|
if (renderDelete) d.delete(c.content.getLength())
|
|
@@ -909,18 +1026,26 @@ export class YType {
|
|
|
909
1026
|
}
|
|
910
1027
|
case ContentString:
|
|
911
1028
|
if (renderContent) {
|
|
912
|
-
d.usedAttributes = currentAttributes
|
|
913
|
-
usingCurrentAttributes = true
|
|
914
1029
|
if (c.deleted ? retainDeletes : retainInserts) {
|
|
915
|
-
|
|
1030
|
+
// a retain expresses the format *diff* against existing (cached) content, so use
|
|
1031
|
+
// `changedFormats`: a format removed this change (e.g. its marker was deleted)
|
|
1032
|
+
// is present there as a `null` clear, whereas `currentFormats` (absolute) can
|
|
1033
|
+
// only re-assert present formats and would silently keep a stale one.
|
|
1034
|
+
d.usedFormats = changedFormats
|
|
1035
|
+
usingChangedFormats = true
|
|
1036
|
+
// change render: a retained item with no attribution means its attribution was
|
|
1037
|
+
// removed → emit `null` (clear) rather than `{}` (skip). Present attribution merges.
|
|
1038
|
+
d.retain(/** @type {ContentString} */ (c.content).str.length, undefined, attribution ?? null)
|
|
916
1039
|
} else {
|
|
917
|
-
d.
|
|
1040
|
+
d.usedFormats = currentFormats
|
|
1041
|
+
usingCurrentFormats = true
|
|
1042
|
+
d.insert(/** @type {ContentString} */ (c.content).str, undefined, attribution)
|
|
918
1043
|
}
|
|
919
1044
|
} else if (renderDelete) {
|
|
920
1045
|
d.delete(c.content.getLength())
|
|
921
1046
|
} else if (retainContent) {
|
|
922
|
-
d.
|
|
923
|
-
|
|
1047
|
+
d.usedFormats = changedFormats
|
|
1048
|
+
usingChangedFormats = true
|
|
924
1049
|
d.retain(c.content.getLength())
|
|
925
1050
|
}
|
|
926
1051
|
break
|
|
@@ -930,19 +1055,24 @@ export class YType {
|
|
|
930
1055
|
case ContentType:
|
|
931
1056
|
case ContentBinary:
|
|
932
1057
|
if (renderContent) {
|
|
933
|
-
d.usedAttributes = currentAttributes
|
|
934
|
-
usingCurrentAttributes = true
|
|
935
1058
|
if (c.deleted ? retainDeletes : retainInserts) {
|
|
1059
|
+
// a retain expresses the format *diff* → use `changedFormats` (see ContentString)
|
|
1060
|
+
d.usedFormats = changedFormats
|
|
1061
|
+
usingChangedFormats = true
|
|
936
1062
|
if (c.deleted && c.content.constructor === ContentType) {
|
|
937
1063
|
// @todo use current transaction instead
|
|
938
|
-
d.modify(/** @type {any} */ (c.content).type.toDelta(optsAll),
|
|
1064
|
+
d.modify(/** @type {any} */ (c.content).type.toDelta(optsAll), undefined, attribution ?? null)
|
|
939
1065
|
} else {
|
|
940
|
-
d.retain(c.content.getLength(),
|
|
1066
|
+
d.retain(c.content.getLength(), undefined, attribution ?? null)
|
|
941
1067
|
}
|
|
942
1068
|
} else if (deep && c.content.constructor === ContentType) {
|
|
943
|
-
d.
|
|
1069
|
+
d.usedFormats = currentFormats
|
|
1070
|
+
usingCurrentFormats = true
|
|
1071
|
+
d.insert([/** @type {any} */(c.content).type.toDelta(optsAll)], undefined, attribution)
|
|
944
1072
|
} else {
|
|
945
|
-
d.
|
|
1073
|
+
d.usedFormats = currentFormats
|
|
1074
|
+
usingCurrentFormats = true
|
|
1075
|
+
d.insert(c.content.getContent(), undefined, attribution)
|
|
946
1076
|
}
|
|
947
1077
|
} else if (renderDelete) {
|
|
948
1078
|
d.delete(1)
|
|
@@ -951,98 +1081,127 @@ export class YType {
|
|
|
951
1081
|
// @todo use current transaction instead
|
|
952
1082
|
d.modify(/** @type {any} */ (c.content).type.toDelta(optsAll))
|
|
953
1083
|
} else {
|
|
954
|
-
d.
|
|
955
|
-
|
|
1084
|
+
d.usedFormats = changedFormats
|
|
1085
|
+
usingChangedFormats = true
|
|
956
1086
|
d.retain(1)
|
|
957
1087
|
}
|
|
958
1088
|
}
|
|
959
1089
|
break
|
|
960
1090
|
case ContentFormat: {
|
|
961
1091
|
const { key, value } = /** @type {ContentFormat} */ (c.content)
|
|
962
|
-
const
|
|
963
|
-
if (attribution != null && (c.deleted || !object.hasProperty(
|
|
964
|
-
|
|
1092
|
+
const currFormatVal = currentFormats[key] ?? null
|
|
1093
|
+
if (attribution != null && (c.deleted || !object.hasProperty(previousUnattributedFormats, key))) {
|
|
1094
|
+
previousUnattributedFormats[key] = c.deleted ? value : currFormatVal
|
|
965
1095
|
}
|
|
966
|
-
// @todo write a function "
|
|
967
|
-
// # Update
|
|
1096
|
+
// @todo write a function "updateCurrentFormats" and "updateChangedFormats"
|
|
1097
|
+
// # Update Formats
|
|
968
1098
|
if (renderContent || renderDelete) {
|
|
969
1099
|
// create fresh references
|
|
970
|
-
if (
|
|
971
|
-
|
|
972
|
-
|
|
1100
|
+
if (usingCurrentFormats) {
|
|
1101
|
+
currentFormats = object.assign({}, currentFormats)
|
|
1102
|
+
usingCurrentFormats = false
|
|
973
1103
|
}
|
|
974
|
-
if (
|
|
975
|
-
|
|
976
|
-
|
|
1104
|
+
if (usingChangedFormats) {
|
|
1105
|
+
usingChangedFormats = false
|
|
1106
|
+
changedFormats = object.assign({}, changedFormats)
|
|
977
1107
|
}
|
|
978
1108
|
}
|
|
979
1109
|
if (renderContent || renderDelete) {
|
|
980
1110
|
if (c.deleted) {
|
|
981
1111
|
// content was deleted, but is possibly attributed
|
|
982
|
-
if (!
|
|
983
|
-
if (
|
|
984
|
-
delete
|
|
1112
|
+
if (!equalFormats(value, currFormatVal)) { // do nothing if nothing changed
|
|
1113
|
+
if (equalFormats(currFormatVal, previousFormats[key] ?? null) && changedFormats[key] !== undefined) {
|
|
1114
|
+
delete changedFormats[key]
|
|
985
1115
|
} else {
|
|
986
|
-
|
|
1116
|
+
changedFormats[key] = currFormatVal
|
|
987
1117
|
}
|
|
988
|
-
// current
|
|
989
|
-
|
|
1118
|
+
// current formats doesn't change
|
|
1119
|
+
previousFormats[key] = value
|
|
990
1120
|
}
|
|
991
1121
|
} else { // !c.deleted
|
|
992
1122
|
// content was inserted, and is possibly attributed
|
|
993
|
-
if (
|
|
1123
|
+
if (equalFormats(value, currFormatVal)) {
|
|
994
1124
|
// item.delete(transaction)
|
|
995
|
-
} else if (
|
|
996
|
-
delete
|
|
1125
|
+
} else if (equalFormats(value, previousFormats[key] ?? null)) {
|
|
1126
|
+
delete changedFormats[key]
|
|
997
1127
|
} else {
|
|
998
|
-
|
|
1128
|
+
changedFormats[key] = value
|
|
999
1129
|
}
|
|
1000
1130
|
if (value == null) {
|
|
1001
|
-
delete
|
|
1131
|
+
delete currentFormats[key]
|
|
1002
1132
|
} else {
|
|
1003
|
-
|
|
1133
|
+
currentFormats[key] = value
|
|
1004
1134
|
}
|
|
1005
1135
|
}
|
|
1006
1136
|
} else if (retainContent && !c.deleted) {
|
|
1007
|
-
// fresh reference to
|
|
1008
|
-
if (
|
|
1009
|
-
|
|
1010
|
-
|
|
1137
|
+
// fresh reference to currentFormats only
|
|
1138
|
+
if (usingCurrentFormats) {
|
|
1139
|
+
currentFormats = object.assign({}, currentFormats)
|
|
1140
|
+
usingCurrentFormats = false
|
|
1011
1141
|
}
|
|
1012
|
-
if (
|
|
1013
|
-
|
|
1014
|
-
|
|
1142
|
+
if (usingChangedFormats && changedFormats[key] !== undefined) {
|
|
1143
|
+
usingChangedFormats = false
|
|
1144
|
+
changedFormats = object.assign({}, changedFormats)
|
|
1015
1145
|
}
|
|
1016
1146
|
if (value == null) {
|
|
1017
|
-
delete
|
|
1147
|
+
delete currentFormats[key]
|
|
1018
1148
|
} else {
|
|
1019
|
-
|
|
1149
|
+
currentFormats[key] = value
|
|
1020
1150
|
}
|
|
1021
|
-
delete
|
|
1022
|
-
|
|
1151
|
+
delete changedFormats[key]
|
|
1152
|
+
previousFormats[key] = value
|
|
1023
1153
|
}
|
|
1024
1154
|
// # Update Attributions
|
|
1025
|
-
|
|
1155
|
+
// A format marker deleted in a change render under an *attributing* renderer nets to no
|
|
1156
|
+
// attribution (its insert+delete suggestion cancels → `attribution == null`), yet it ends
|
|
1157
|
+
// the attributed range it opened: the following retained content must drop the stale
|
|
1158
|
+
// `{ format: { [key]: [] } }` the marker's insertion wrote to the cache. Emit an explicit
|
|
1159
|
+
// `null` leaf for the key (a context-wide `useAttribution(null)` cannot carry a per-key
|
|
1160
|
+
// clear). Conditions: only an attributing render (`renderer !== baseRenderer`; the base
|
|
1161
|
+
// renderer has no attributions to clear), and only when the deletion actually *removes*
|
|
1162
|
+
// the format — i.e. it reverts to no value (`currFormatVal == null`). If it reverts to a
|
|
1163
|
+
// still-present surrounding value (e.g. deleting a `bold:null` marker re-exposes an
|
|
1164
|
+
// enclosing attributed `bold:true`, as when re-bolding), the attribution is preserved.
|
|
1165
|
+
const isDeletedFormatClear = attribution == null && renderer !== baseRenderer && renderDelete && c.deleted && itemsToRender != null && currFormatVal == null && !equalFormats(value, currFormatVal)
|
|
1166
|
+
if (attribution != null || isDeletedFormatClear || object.hasProperty(previousUnattributedFormats, key)) {
|
|
1026
1167
|
/**
|
|
1027
1168
|
* @type {Attribution}
|
|
1028
1169
|
*/
|
|
1029
1170
|
const formattingAttribution = object.assign({}, d.usedAttribution)
|
|
1030
|
-
const
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
delete
|
|
1035
|
-
|
|
1171
|
+
const changedAttributedFormats = /** @type {{ [key: string]: Array<any>|null }} */ (formattingAttribution.format = object.assign({}, formattingAttribution.format ?? {}))
|
|
1172
|
+
const sameAsPreviousAttributions = equalFormats(previousUnattributedFormats[key], currentFormats[key] ?? null)
|
|
1173
|
+
if (isDeletedFormatClear) {
|
|
1174
|
+
changedAttributedFormats[key] = null
|
|
1175
|
+
delete previousUnattributedFormats[key]
|
|
1176
|
+
} else if (attribution == null && !sameAsPreviousAttributions) {
|
|
1177
|
+
// skip
|
|
1178
|
+
} else if (attribution == null || sameAsPreviousAttributions) {
|
|
1179
|
+
// an unattributed format was found or an attributed format
|
|
1180
|
+
// was found that resets to the previous status. When this format item is
|
|
1181
|
+
// itself rendered this transaction (`renderContent || renderDelete`) in a change/diff
|
|
1182
|
+
// render (`itemsToRender != null`), it is the END of an attributed format range:
|
|
1183
|
+
// emit an explicit clear (a `null` leaf) so the retained content drops any stale
|
|
1184
|
+
// `{ format: { [key]: [] } }` from the maintained `delta` cache — a bare context-skip
|
|
1185
|
+
// (`delete`) would leave it in place. For a merely-retained (unchanged) boundary, or a
|
|
1186
|
+
// full insert render (removal is already modeled as absence in `currentFormats`),
|
|
1187
|
+
// just drop the key: a change render must not emit ops for unchanged ranges, and
|
|
1188
|
+
// inserts must stay free of a spurious `{ format: { [key]: null } }`.
|
|
1189
|
+
if (attribution != null && itemsToRender != null && (renderContent || renderDelete)) {
|
|
1190
|
+
changedAttributedFormats[key] = null
|
|
1191
|
+
} else {
|
|
1192
|
+
delete changedAttributedFormats[key]
|
|
1193
|
+
}
|
|
1194
|
+
delete previousUnattributedFormats[key]
|
|
1036
1195
|
} else {
|
|
1037
|
-
const by =
|
|
1196
|
+
const by = changedAttributedFormats[key] = (changedAttributedFormats[key]?.slice() ?? [])
|
|
1038
1197
|
by.push(...((c.deleted ? attribution.delete : attribution.insert) ?? []))
|
|
1039
1198
|
const attributedAt = (c.deleted ? attribution.deleteAt : attribution.insertAt)
|
|
1040
1199
|
if (attributedAt) formattingAttribution.formatAt = attributedAt
|
|
1041
1200
|
}
|
|
1042
|
-
if (object.isEmpty(
|
|
1201
|
+
if (object.isEmpty(changedAttributedFormats)) {
|
|
1043
1202
|
d.useAttribution(null)
|
|
1044
|
-
} else if (attribution != null) {
|
|
1045
|
-
const attributedAt = (c.deleted ? attribution
|
|
1203
|
+
} else if (attribution != null || isDeletedFormatClear) {
|
|
1204
|
+
const attributedAt = (c.deleted ? attribution?.deleteAt : attribution?.insertAt)
|
|
1046
1205
|
if (attributedAt != null) formattingAttribution.formatAt = attributedAt
|
|
1047
1206
|
d.useAttribution(formattingAttribution)
|
|
1048
1207
|
}
|
|
@@ -1061,7 +1220,7 @@ export class YType {
|
|
|
1061
1220
|
* attributions.
|
|
1062
1221
|
*
|
|
1063
1222
|
* @param {Object} [opts]
|
|
1064
|
-
* @param {AbstractRenderer} [opts.renderer] - renders the content (with attributions); defaults to `baseRenderer`
|
|
1223
|
+
* @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
1224
|
* @return {delta.Delta<DConf>}
|
|
1066
1225
|
*/
|
|
1067
1226
|
toDeltaDeep (opts = {}) {
|
|
@@ -1073,11 +1232,14 @@ export class YType {
|
|
|
1073
1232
|
*
|
|
1074
1233
|
* @param {delta.DeltaAny} d The changes to apply on this element.
|
|
1075
1234
|
* @param {Object} [opts]
|
|
1076
|
-
* @param {AbstractRenderer} [opts.renderer] - renders the content (with attributions); defaults to `baseRenderer`
|
|
1235
|
+
* @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
|
|
1236
|
+
* @return {null} The lib0 `RDT` "fix" of this apply — always `null`: a `YType` accepts every valid
|
|
1237
|
+
* delta as-is and never needs to self-correct.
|
|
1077
1238
|
*
|
|
1078
1239
|
* @public
|
|
1079
1240
|
*/
|
|
1080
|
-
applyDelta (d, { renderer =
|
|
1241
|
+
applyDelta (d, { renderer = this._renderer } = {}) {
|
|
1242
|
+
if (d.isEmpty()) return null
|
|
1081
1243
|
if (this.doc == null) {
|
|
1082
1244
|
(this._prelim || (this._prelim = /** @type {any} */ (delta.create()))).apply(d)
|
|
1083
1245
|
} else if (this._item?.deleted !== true) {
|
|
@@ -1118,7 +1280,7 @@ export class YType {
|
|
|
1118
1280
|
}
|
|
1119
1281
|
})
|
|
1120
1282
|
}
|
|
1121
|
-
return
|
|
1283
|
+
return null
|
|
1122
1284
|
}
|
|
1123
1285
|
|
|
1124
1286
|
/**
|
|
@@ -1224,7 +1386,7 @@ export class YType {
|
|
|
1224
1386
|
*
|
|
1225
1387
|
* @param {number} index The index to insert content at.
|
|
1226
1388
|
* @param {Array<delta.DeltaConfGetChildren<DConf>>|delta.DeltaConfGetText<DConf>} content Array of content to append.
|
|
1227
|
-
* @param {delta.
|
|
1389
|
+
* @param {delta.Formats} [format]
|
|
1228
1390
|
*/
|
|
1229
1391
|
insert (index, content, format) {
|
|
1230
1392
|
this.applyDelta(delta.create().retain(index).insert(/** @type {any} */ (content), format).done())
|
|
@@ -1245,7 +1407,7 @@ export class YType {
|
|
|
1245
1407
|
*
|
|
1246
1408
|
* @param {number} index The index to insert content at.
|
|
1247
1409
|
* @param {number} length The index to insert content at.
|
|
1248
|
-
* @param {delta.
|
|
1410
|
+
* @param {delta.Formats} formats
|
|
1249
1411
|
*
|
|
1250
1412
|
*/
|
|
1251
1413
|
format (index, length, formats) {
|
|
@@ -1519,7 +1681,7 @@ export const computeModifiedFromItems = (store, items) => {
|
|
|
1519
1681
|
* @param {any} b
|
|
1520
1682
|
* @return {boolean}
|
|
1521
1683
|
*/
|
|
1522
|
-
export const
|
|
1684
|
+
export const equalFormats = (a, b) => a === b || (typeof a === 'object' && typeof b === 'object' && a && b && object.equalFlat(a, b))
|
|
1523
1685
|
|
|
1524
1686
|
/**
|
|
1525
1687
|
* @template {delta.DeltaConf} DConf
|
|
@@ -1922,25 +2084,26 @@ export const typeMapGetDelta = (d, parent, attrsToRender, renderer, deep, modifi
|
|
|
1922
2084
|
*/
|
|
1923
2085
|
const cs = []
|
|
1924
2086
|
renderer.readContent(cs, item.id.client, item.id.clock, item.deleted, item.content, 1)
|
|
1925
|
-
|
|
1926
|
-
|
|
2087
|
+
if (cs.length === 0) return // the renderer surfaces nothing for this attribute (e.g. a diff renderer hiding an unchanged delete)
|
|
2088
|
+
const { deleted, attrs, content } = cs[cs.length - 1]
|
|
1927
2089
|
const attribution = createAttributionFromAttributionItems(attrs, deleted)
|
|
1928
2090
|
let c = array.last(content.getContent())
|
|
1929
2091
|
if (deleted) {
|
|
1930
|
-
if (
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
// (positive `InsertOp` with attribution, never `DeleteOp`).
|
|
2092
|
+
if (attribution != null) {
|
|
2093
|
+
// Item surfaced under attribution (suggestion view / diff renderer, either in snapshot mode
|
|
2094
|
+
// or in an event-driven render). The attribute is still observable in the rendered state, so
|
|
2095
|
+
// emit a positive `SetAttrOp` carrying the attribution metadata - matching how content
|
|
2096
|
+
// children are rendered for the same case (positive `InsertOp` with attribution, never
|
|
2097
|
+
// `DeleteOp`).
|
|
2098
|
+
if (itemsToRender == null || itemsToRender.hasId(item.lastId)) {
|
|
1938
2099
|
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
2100
|
}
|
|
2101
|
+
} else if (itemsToRender != null && itemsToRender.hasId(item.lastId)) {
|
|
2102
|
+
// Hard-deleted attribute within a change render: emit the `deleteAttr` op so consumers (the
|
|
2103
|
+
// `YEvent` delta, RDT bindings, the maintained `delta` cache) can apply the removal. In
|
|
2104
|
+
// full-state mode (`itemsToRender == null`) the attribute is simply omitted (above renders
|
|
2105
|
+
// run with `render === false` for such items, so nothing was emitted before either).
|
|
2106
|
+
d.deleteAttr(key, attribution, c)
|
|
1944
2107
|
}
|
|
1945
2108
|
} else if (deep && c instanceof YType && modified?.has(c)) {
|
|
1946
2109
|
d.modifyAttr(key, c.toDelta(opts))
|