Is it possible to force a Y.Text deletion, and/or have a special `replace` method?

The question is a bit hard to phrase, so I think it is easier just to show:

import * as Y from 'yjs'

const y1 = new Y.Doc()
const y1text = y1.getText('text')

y1text.insert(0, '[ ] my todo')

const upStart = Y.encodeStateAsUpdate(y1)

const y2 = new Y.Doc()
Y.applyUpdate(y2, upStart)

y1.transact(() => {
  y1text.delete(1, 1)
  y1text.insert(1, 'x')
})

const y2text = y2.getText('text')

y2.transact(() => {
  y2text.delete(1, 1)
  y2text.insert(1, 'x')
})

const up1 = Y.encodeStateAsUpdate(y1)
const up2 = Y.encodeStateAsUpdate(y2)

const y3 = new Y.Doc()
Y.applyUpdate(y3, upStart)
Y.applyUpdate(y3, up1)
Y.applyUpdate(y3, up2)
console.log(y3.getText('text').toString()) // outputs: '[xx] my todo'
// I want it to output '[x] my todo' after both updates are applied

Basically, I have certain instances (in this case “checking” a todo list in text) where I know I always want to perform what I will call a “strong” deletion with a Y.Text. If multiple users make the same deletion, the logic employed by YJs’ CRDTs is to merge those deletions and then perform both insertions (excuse my layman terminology).

In this particular case, I do not want the deletions to be “merged”. I want whichever operations (update) come first to initially remove a space (' ') and then insert an 'x'. I then want whichever operations (update) are applied second to remove the original 'x' and replace it with their own 'x'.

The default YJs behavior is for that second update to “see” that the ' ' has already been removed, do nothing for the delete() step, and then insert a second 'x', which is not the desired behavior in this particular case.

Said in terms of user experience: if two users “check” the checkbox asynchronously, I want the final result to be "[x] my todo". Likewise if two users uncheck I want the result to be "[ ] my todo" rather than "[__] my todo" (with two spaces).

Is there any way to force YJs to do it like this for these particular operations? I understand that most of the time the standard behavior is what you want.

Yjs doesn’t have a “replace” feature. You can just insert/delete.

If you want replace behavior, you can use Y.Map.

You can also embed a Y.Map into Y.Text:

ytext.insertEmbed(pos, new Y.Map([['checked', true]]))
ytext.toDelta()[0].insert // => returns the ymap

If you want this to work with a text editor, you’d need to update the editor bindings.

1 Like