Skip to content

Commit

Permalink
Apply anchor style when creating a change during text insertion
Browse files Browse the repository at this point in the history
  • Loading branch information
chacha912 committed Oct 17, 2023
1 parent 7c2b258 commit 7f9b76e
Show file tree
Hide file tree
Showing 2 changed files with 148 additions and 82 deletions.
224 changes: 142 additions & 82 deletions public/quill-two-clients.html
Original file line number Diff line number Diff line change
Expand Up @@ -12,35 +12,32 @@
<link rel="stylesheet" href="quill-two-clients.css" />
<script src="https://cdn.quilljs.com/1.3.6/quill.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/quill-cursors.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/color-hash.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/short-unique-id.min.js"></script>
</head>
<body>
<div class="client-container">
<div id="client-a">
Client A :
<button class="sync">Sync</button>
<button class="sync">Disconnect</button>
<div class="editor"></div>
<div class="document"></div>
<div class="document-text"></div>
</div>
<div id="client-b">
Client B :
<button class="sync">Sync</button>
<button class="sync">Disconnect</button>
<div class="editor"></div>
<div class="document"></div>
<div class="document-text"></div>
</div>
</div>

<script src="./yorkie.js"></script>
<script src="./util.js"></script>
<script>
const clientAElem = document.getElementById('client-a');
const clientBElem = document.getElementById('client-b');
const clientASyncButton = clientAElem?.querySelector('.sync');
const clientBSyncButton = clientBElem?.querySelector('.sync');
const documentKey = 'quill';
const documentKey = 'quill-two-clients';

function displayLog(clientElem, doc) {
const documentElem = clientElem?.getElementsByClassName('document')[0];
Expand All @@ -66,7 +63,7 @@

async function main() {
try {
async function createClientAndDocumentAndQuill(clientElem) {
async function createClientAndDocumentAndQuill(clientElem, presence) {
// 01. create client with RPCAddr(envoy) then activate it.
const client = new yorkie.Client('http://localhost:8080');
await client.activate();
Expand All @@ -75,7 +72,7 @@
const doc = new yorkie.Document(documentKey);

await client.attach(doc, {
isRealtimeSync: false,
initialPresence: presence,
});

doc.update((root) => {
Expand All @@ -101,6 +98,14 @@
}
});

doc.subscribe('others', (event) => {
if (event.type === 'unwatched') {
cursors.removeCursor(event.value.presence.username);
} else if (event.type === 'presence-changed') {
displayRemoteCursor(event.value);
}
});

await client.sync();

// 03. create an instance of Quill
Expand All @@ -109,7 +114,7 @@
const quill = new Quill(editorElem, {
modules: {
toolbar: [
['bold', 'italic', 'underline', 'strike'],
['bold', 'italic', 'underline', 'strike', 'link'],
['blockquote', 'code-block'],
[{ header: 1 }, { header: 2 }],
[{ list: 'ordered' }, { list: 'bullet' }],
Expand All @@ -124,111 +129,166 @@
['image', 'video'],
['clean'],
],
cursors: true,
},
theme: 'snow',
});

const cursors = quill.getModule('cursors');
function displayRemoteCursor(user) {
const {
clientID: id,
presence: { username, color, selection },
} = user;
if (!selection || id === client.getID()) return;
const range = doc
.getRoot()
.content.posRangeToIndexRange(selection);
cursors.createCursor(username, username, color);
cursors.moveCursor(username, {
index: range[0],
length: range[1] - range[0],
});
}

return { client, doc, quill };
}

const {
client: clientA,
doc: docA,
quill: quillA,
} = await createClientAndDocumentAndQuill(clientAElem);
} = await createClientAndDocumentAndQuill(clientAElem, {
username: 'clientA',
color: 'gold',
});

let isRealtimeA = true;
clientASyncButton.addEventListener('click', async () => {
await clientA.sync();
if (isRealtimeA) {
await clientA.pause(docA);
clientASyncButton.textContent = 'Connect';
} else {
await clientA.resume(docA);
clientASyncButton.textContent = 'Disconnect';
}
isRealtimeA = !isRealtimeA;
});

const {
client: clientB,
doc: docB,
quill: quillB,
} = await createClientAndDocumentAndQuill(clientBElem);
} = await createClientAndDocumentAndQuill(clientBElem, {
username: 'clientB',
color: 'dodgerblue',
});

let isRealtimeB = true;
clientBSyncButton.addEventListener('click', async () => {
await clientB.sync();
if (isRealtimeB) {
await clientB.pause(docB);
clientBSyncButton.textContent = 'Connect';
} else {
await clientB.resume(docB);
clientBSyncButton.textContent = 'Disconnect';
}
isRealtimeB = !isRealtimeB;
});

// 04. bind the document with the Quill.
// 04-1. Quill to Document.
function bindDocumentToQuill(client, doc, quill) {
quill.on('text-change', (delta, _, source) => {
if (source === 'api' || !delta.ops) {
return;
}
quill
.on('text-change', (delta, _, source) => {
if (source === 'api' || !delta.ops) {
return;
}

let from = 0,
to = 0;
console.log(
`%c quill: ${JSON.stringify(delta.ops)}`,
'color: green',
);
for (const op of delta.ops) {
if (op.attributes !== undefined || op.insert !== undefined) {
if (op.retain !== undefined) {
to = from + op.retain;
}
console.log(
`%c local: ${from}-${to}: ${op.insert} ${
op.attributes ? JSON.stringify(op.attributes) : '{}'
}`,
'color: green',
);

doc.update((root, presence) => {
let range;
if (
op.attributes !== undefined &&
op.insert === undefined
) {
root.content.setStyle(from, to, op.attributes);
} else if (op.insert !== undefined) {
if (to < from) {
to = from;
let from = 0,
to = 0;
console.log(
`%c quill: ${JSON.stringify(delta.ops)}`,
'color: green',
);
for (const op of delta.ops) {
if (op.attributes !== undefined || op.insert !== undefined) {
if (op.retain !== undefined) {
to = from + op.retain;
}
console.log(
`%c local: ${from}-${to}: ${op.insert} ${
op.attributes ? JSON.stringify(op.attributes) : '{}'
}`,
'color: green',
);

doc.update((root, presence) => {
let range;
if (
op.attributes !== undefined &&
op.insert === undefined
) {
root.content.setStyle(from, to, op.attributes);
} else if (op.insert !== undefined) {
if (to < from) {
to = from;
}

if (typeof op.insert === 'object') {
range = root.content.edit(from, to, ' ', {
embed: op.insert,
...op.attributes,
});
} else {
range = root.content.edit(
from,
to,
op.insert,
op.attributes,
);
}
from = to + op.insert.length;
}

if (typeof op.insert === 'object') {
range = root.content.edit(from, to, ' ', {
embed: op.insert,
...op.attributes,
if (range) {
presence.set({
selection: root.content.indexRangeToPosRange(range),
});
} else {
range = root.content.edit(
from,
to,
op.insert,
op.attributes,
);
}
from = to + op.insert.length;
}

if (range) {
presence.set({
selection: root.content.indexRangeToPosRange(range),
});
}
}, `update style by ${client.getID()}`);
} else if (op.delete !== undefined) {
to = from + op.delete;
console.log(`%c local: ${from}-${to}: ''`, 'color: green');

doc.update((root, presence) => {
const range = root.content.edit(from, to, '');
if (range) {
presence.set({
selection: root.content.indexRangeToPosRange(range),
});
}
}, `update content by ${client.getID()}`);
} else if (op.retain !== undefined) {
from = to + op.retain;
to = from;
}, `update style by ${client.getID()}`);
} else if (op.delete !== undefined) {
to = from + op.delete;
console.log(`%c local: ${from}-${to}: ''`, 'color: green');

doc.update((root, presence) => {
const range = root.content.edit(from, to, '');
if (range) {
presence.set({
selection: root.content.indexRangeToPosRange(range),
});
}
}, `update content by ${client.getID()}`);
} else if (op.retain !== undefined) {
from = to + op.retain;
to = from;
}
}
}
});
})
.on('selection-change', (range, _, source) => {
if (source === 'api' || !range) {
return;
}

doc.update((root, presence) => {
presence.set({
selection: root.content.indexRangeToPosRange([
range.index,
range.index + range.length,
]),
});
}, `update selection by ${client.getID()}`);
});
}
bindDocumentToQuill(clientA, docA, quillA);
bindDocumentToQuill(clientB, docB, quillB);
Expand Down
6 changes: 6 additions & 0 deletions src/document/crdt/rga_tree_split.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ interface RGATreeSplitValue {
length: number;

substring(indexStart: number, indexEnd?: number): RGATreeSplitValue;
setAttr(key: string, content: string, updatedAt: TimeTicket): void;
}

export interface StyleOperation {
Expand Down Expand Up @@ -686,6 +687,11 @@ export class RGATreeSplit<T extends RGATreeSplitValue> {
}),
);

const opset = this.findOpsetPreferToLeft(inserted, BoundaryType.Before);
const attrs = this.getAttrsFromAnchor(opset);
for (const [k, v] of attrs) {
value.setAttr(k, v, editedAt);
}
if (changes.length && changes[changes.length - 1].from === idx) {
changes[changes.length - 1].value = value;
} else {
Expand Down

0 comments on commit 7f9b76e

Please sign in to comment.