@yorkie-js/sdk 0.6.37 → 0.6.39
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/dist/quill.html +493 -286
- package/dist/yorkie-js-sdk.es.js +1 -1
- package/dist/yorkie-js-sdk.js +1 -1
- package/package.json +2 -2
- package/dist/quill-two-clients.html +0 -504
- /package/dist/{quill-two-clients.css → quill.css} +0 -0
package/dist/quill.html
CHANGED
|
@@ -3,359 +3,566 @@
|
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
-
<title>
|
|
6
|
+
<title>Quill Example</title>
|
|
7
7
|
<link
|
|
8
|
-
href="https://cdn.
|
|
8
|
+
href="https://cdn.jsdelivr.net/npm/quill@2.0.3/dist/quill.snow.css"
|
|
9
9
|
rel="stylesheet"
|
|
10
10
|
/>
|
|
11
11
|
<link rel="stylesheet" href="style.css" />
|
|
12
|
-
<
|
|
13
|
-
<script src="https://cdn.jsdelivr.net/npm/quill
|
|
12
|
+
<link rel="stylesheet" href="quill.css" />
|
|
13
|
+
<script src="https://cdn.jsdelivr.net/npm/quill@2.0.3/dist/quill.js"></script>
|
|
14
|
+
<script src="https://cdn.jsdelivr.net/npm/quill-cursors@4.0.3/dist/quill-cursors.js"></script>
|
|
14
15
|
<script src="https://cdn.jsdelivr.net/npm/color-hash@1.0.3/dist/color-hash.js"></script>
|
|
15
16
|
</head>
|
|
16
17
|
<body>
|
|
17
|
-
<div
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
18
|
+
<div class="client-container">
|
|
19
|
+
<div id="client-a">
|
|
20
|
+
Client A ( id:<span class="client-id"></span>)
|
|
21
|
+
<span class="network-status"></span>
|
|
22
|
+
<div class="syncmode-option">
|
|
23
|
+
<span>SyncMode: </span>
|
|
24
|
+
<div class="realtime-sync">
|
|
25
|
+
<span class="realtime-sync-title">Realtime Sync</span>
|
|
26
|
+
<div class="option">
|
|
27
|
+
<input
|
|
28
|
+
type="radio"
|
|
29
|
+
id="realtime-pushpull-a"
|
|
30
|
+
name="syncMode-a"
|
|
31
|
+
value="pushpull"
|
|
32
|
+
checked
|
|
33
|
+
/>
|
|
34
|
+
<label for="realtime-pushpull-a">PushPull</label>
|
|
35
|
+
</div>
|
|
36
|
+
<div class="option">
|
|
37
|
+
<input
|
|
38
|
+
type="radio"
|
|
39
|
+
id="realtime-pushonly-a"
|
|
40
|
+
name="syncMode-a"
|
|
41
|
+
value="pushonly"
|
|
42
|
+
/>
|
|
43
|
+
<label for="realtime-pushonly-a">PushOnly</label>
|
|
44
|
+
</div>
|
|
45
|
+
<div class="option">
|
|
46
|
+
<input
|
|
47
|
+
type="radio"
|
|
48
|
+
id="realtime-syncoff-a"
|
|
49
|
+
name="syncMode-a"
|
|
50
|
+
value="syncoff"
|
|
51
|
+
/>
|
|
52
|
+
<label for="realtime-syncoff-a">SyncOff</label>
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
55
|
+
<div class="option">
|
|
56
|
+
<input
|
|
57
|
+
type="radio"
|
|
58
|
+
id="manual-a"
|
|
59
|
+
name="syncMode-a"
|
|
60
|
+
value="manual"
|
|
61
|
+
/>
|
|
62
|
+
<label for="manual-a">Manual Sync</label>
|
|
63
|
+
<button class="manual-sync">sync</button>
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
<div class="editor"></div>
|
|
67
|
+
<div class="online-clients"></div>
|
|
68
|
+
</div>
|
|
69
|
+
<div id="client-b">
|
|
70
|
+
Client B ( id:<span class="client-id"></span>)
|
|
71
|
+
<span class="network-status"></span>
|
|
72
|
+
<div class="syncmode-option">
|
|
73
|
+
<span>SyncMode: </span>
|
|
74
|
+
<div class="realtime-sync">
|
|
75
|
+
<span class="realtime-sync-title">Realtime Sync</span>
|
|
76
|
+
<div class="option">
|
|
77
|
+
<input
|
|
78
|
+
type="radio"
|
|
79
|
+
id="realtime-pushpull-b"
|
|
80
|
+
name="syncMode-b"
|
|
81
|
+
value="pushpull"
|
|
82
|
+
checked
|
|
83
|
+
/>
|
|
84
|
+
<label for="realtime-pushpull-b">PushPull</label>
|
|
85
|
+
</div>
|
|
86
|
+
<div class="option">
|
|
87
|
+
<input
|
|
88
|
+
type="radio"
|
|
89
|
+
id="realtime-pushonly-b"
|
|
90
|
+
name="syncMode-b"
|
|
91
|
+
value="pushonly"
|
|
92
|
+
/>
|
|
93
|
+
<label for="realtime-pushonly-b">PushOnly</label>
|
|
94
|
+
</div>
|
|
95
|
+
<div class="option">
|
|
96
|
+
<input
|
|
97
|
+
type="radio"
|
|
98
|
+
id="realtime-syncoff-b"
|
|
99
|
+
name="syncMode-b"
|
|
100
|
+
value="syncoff"
|
|
101
|
+
/>
|
|
102
|
+
<label for="realtime-syncoff-b">SyncOff</label>
|
|
103
|
+
</div>
|
|
104
|
+
</div>
|
|
105
|
+
<div class="option">
|
|
106
|
+
<input
|
|
107
|
+
type="radio"
|
|
108
|
+
id="manual-b"
|
|
109
|
+
name="syncMode-b"
|
|
110
|
+
value="manual"
|
|
111
|
+
/>
|
|
112
|
+
<label for="manual-b">Manual Sync</label>
|
|
113
|
+
<button class="manual-sync">sync</button>
|
|
114
|
+
</div>
|
|
115
|
+
</div>
|
|
116
|
+
<div class="editor"></div>
|
|
117
|
+
<div class="online-clients"></div>
|
|
118
|
+
</div>
|
|
119
|
+
</div>
|
|
22
120
|
<script type="module">
|
|
23
121
|
import './src/yorkie.ts';
|
|
24
122
|
import Network from './devtool/network.js';
|
|
25
123
|
|
|
26
|
-
const onlineClientsElem = document.getElementById('online-clients');
|
|
27
|
-
const documentElem = document.getElementById('document');
|
|
28
|
-
const documentTextElem = document.getElementById('document-text');
|
|
29
|
-
const networkStatusElem = document.getElementById('network-status');
|
|
30
124
|
const colorHash = new ColorHash();
|
|
31
|
-
const
|
|
125
|
+
const clientAElem = document.getElementById('client-a');
|
|
126
|
+
const clientBElem = document.getElementById('client-b');
|
|
127
|
+
const documentKey = 'quill-two-clients';
|
|
32
128
|
|
|
33
|
-
function
|
|
34
|
-
|
|
35
|
-
|
|
129
|
+
function filterNullAttrs(attributes) {
|
|
130
|
+
if (!attributes) return undefined;
|
|
131
|
+
|
|
132
|
+
const filtered = {};
|
|
133
|
+
let hasNonNullValue = false;
|
|
134
|
+
|
|
135
|
+
for (const [key, value] of Object.entries(attributes)) {
|
|
136
|
+
if (value !== null) {
|
|
137
|
+
filtered[key] = value;
|
|
138
|
+
hasNonNullValue = true;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return hasNonNullValue ? filtered : undefined;
|
|
36
143
|
}
|
|
37
144
|
|
|
38
|
-
function toDeltaOperation(textValue) {
|
|
145
|
+
function toDeltaOperation(textValue, filterNull = false) {
|
|
39
146
|
const { embed, ...restAttributes } = textValue.attributes ?? {};
|
|
40
|
-
|
|
41
147
|
if (embed) {
|
|
42
|
-
return {
|
|
148
|
+
return {
|
|
149
|
+
insert: JSON.parse(embed.toString()),
|
|
150
|
+
attributes: filterNull
|
|
151
|
+
? filterNullAttrs(restAttributes)
|
|
152
|
+
: restAttributes,
|
|
153
|
+
};
|
|
43
154
|
}
|
|
44
155
|
|
|
45
156
|
return {
|
|
46
157
|
insert: textValue.content || '',
|
|
47
|
-
attributes:
|
|
158
|
+
attributes: filterNull
|
|
159
|
+
? filterNullAttrs(textValue.attributes)
|
|
160
|
+
: textValue.attributes,
|
|
48
161
|
};
|
|
49
162
|
}
|
|
50
163
|
|
|
51
|
-
function displayOnlineClients(presences, myClientID) {
|
|
52
|
-
const clients = [];
|
|
53
|
-
for (const { clientID, presence } of presences) {
|
|
54
|
-
const clientElem = `<span class="client" style='background: ${presence.color}; color: white; margin-right:2px; padding:2px;'>${presence.name}</span>`;
|
|
55
|
-
if (myClientID === clientID) {
|
|
56
|
-
clients.unshift(clientElem);
|
|
57
|
-
continue;
|
|
58
|
-
}
|
|
59
|
-
clients.push(clientElem);
|
|
60
|
-
}
|
|
61
|
-
onlineClientsElem.innerHTML = clients.join('');
|
|
62
|
-
}
|
|
63
|
-
|
|
64
164
|
async function main() {
|
|
65
165
|
try {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
enableDevtools: true,
|
|
75
|
-
});
|
|
76
|
-
doc.subscribe(
|
|
77
|
-
'connection',
|
|
78
|
-
new Network(networkStatusElem).statusListener,
|
|
79
|
-
);
|
|
80
|
-
doc.subscribe('presence', (event) => {
|
|
81
|
-
if (event.type === 'presence-changed') return;
|
|
82
|
-
displayOnlineClients(doc.getPresences(), client.getID());
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
await client.attach(doc, {
|
|
86
|
-
initialPresence: {
|
|
87
|
-
name: client.getID().slice(-2),
|
|
88
|
-
color: colorHash.hex(client.getID().slice(-2)),
|
|
89
|
-
},
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
doc.update((root) => {
|
|
93
|
-
if (!root.content) {
|
|
94
|
-
root.content = new yorkie.Text();
|
|
95
|
-
root.content.edit(0, 0, '\n');
|
|
96
|
-
}
|
|
97
|
-
}, 'create content if not exists');
|
|
166
|
+
async function initializeRealtimeEditor(clientElem) {
|
|
167
|
+
// 01. create client with RPCAddr(envoy) then activate it.
|
|
168
|
+
const client = new yorkie.Client({
|
|
169
|
+
rpcAddr: 'http://localhost:8080',
|
|
170
|
+
});
|
|
171
|
+
await client.activate();
|
|
172
|
+
const clientID = client.getID().slice(-2);
|
|
173
|
+
clientElem.querySelector('.client-id').textContent = clientID;
|
|
98
174
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
175
|
+
// 02. create a document then attach it into the client.
|
|
176
|
+
const doc = new yorkie.Document(documentKey, {
|
|
177
|
+
enableDevtools: true,
|
|
178
|
+
});
|
|
179
|
+
doc.subscribe(
|
|
180
|
+
'connection',
|
|
181
|
+
new Network(clientElem.querySelector('.network-status'))
|
|
182
|
+
.statusListener,
|
|
183
|
+
);
|
|
184
|
+
const onlineClients = clientElem.querySelector('.online-clients');
|
|
185
|
+
doc.subscribe('presence', (event) => {
|
|
186
|
+
// Update online clients list
|
|
187
|
+
if (event.type !== 'presence-changed') {
|
|
188
|
+
const clientIDs = doc
|
|
189
|
+
.getPresences()
|
|
190
|
+
.map(({ clientID }) => clientID)
|
|
191
|
+
.join(', ');
|
|
192
|
+
onlineClients.textContent = clientIDs;
|
|
193
|
+
}
|
|
107
194
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
195
|
+
console.warn(
|
|
196
|
+
`%c${clientID}`,
|
|
197
|
+
`color:white; padding: 2px 4px; border-radius: 3px;
|
|
198
|
+
background: ${colorHash.hex(clientID)}; `,
|
|
199
|
+
event.type,
|
|
200
|
+
);
|
|
201
|
+
});
|
|
115
202
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
cursors.removeCursor(event.value.clientID);
|
|
119
|
-
} else if (event.type === 'presence-changed') {
|
|
120
|
-
updateCursor(event.value);
|
|
121
|
-
}
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
await client.sync();
|
|
125
|
-
|
|
126
|
-
// 03. create an instance of Quill
|
|
127
|
-
Quill.register('modules/cursors', QuillCursors);
|
|
128
|
-
const quill = new Quill('#editor', {
|
|
129
|
-
modules: {
|
|
130
|
-
toolbar: [
|
|
131
|
-
['bold', 'italic', 'underline', 'strike'],
|
|
132
|
-
['blockquote', 'code-block'],
|
|
133
|
-
[{ header: 1 }, { header: 2 }],
|
|
134
|
-
[{ list: 'ordered' }, { list: 'bullet' }],
|
|
135
|
-
[{ script: 'sub' }, { script: 'super' }],
|
|
136
|
-
[{ indent: '-1' }, { indent: '+1' }],
|
|
137
|
-
[{ direction: 'rtl' }],
|
|
138
|
-
[{ size: ['small', false, 'large', 'huge'] }],
|
|
139
|
-
[{ header: [1, 2, 3, 4, 5, 6, false] }],
|
|
140
|
-
[{ color: [] }, { background: [] }],
|
|
141
|
-
[{ font: [] }],
|
|
142
|
-
[{ align: [] }],
|
|
143
|
-
['image', 'video'],
|
|
144
|
-
['clean'],
|
|
145
|
-
],
|
|
146
|
-
cursors: true,
|
|
147
|
-
},
|
|
148
|
-
theme: 'snow',
|
|
149
|
-
});
|
|
150
|
-
const cursors = quill.getModule('cursors');
|
|
151
|
-
|
|
152
|
-
function updateCursor(user) {
|
|
153
|
-
const { clientID, presence } = user;
|
|
154
|
-
if (clientID === client.getID()) return;
|
|
155
|
-
// TODO(chacha912): After resolving the presence initialization issue(#608),
|
|
156
|
-
// remove the following check.
|
|
157
|
-
if (!presence) return;
|
|
158
|
-
|
|
159
|
-
const { name, color, selection } = presence;
|
|
160
|
-
if (!selection) return;
|
|
161
|
-
const range = doc.getRoot().content.posRangeToIndexRange(selection);
|
|
162
|
-
cursors.createCursor(clientID, name, color);
|
|
163
|
-
cursors.moveCursor(clientID, {
|
|
164
|
-
index: range[0],
|
|
165
|
-
length: range[1] - range[0],
|
|
203
|
+
await client.attach(doc, {
|
|
204
|
+
initialPresence: { name: clientID },
|
|
166
205
|
});
|
|
167
|
-
}
|
|
168
206
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
207
|
+
doc.update((root) => {
|
|
208
|
+
if (!root.content) {
|
|
209
|
+
root.content = new yorkie.Text();
|
|
210
|
+
root.content.edit(0, 0, '\n');
|
|
211
|
+
}
|
|
212
|
+
}, 'create content if not exists');
|
|
174
213
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
return;
|
|
214
|
+
// 02-2. subscribe document event.
|
|
215
|
+
doc.subscribe((event) => {
|
|
216
|
+
if (event.type === 'snapshot') {
|
|
217
|
+
// The text is replaced to snapshot and must be re-synced.
|
|
218
|
+
syncText(doc, quill);
|
|
181
219
|
}
|
|
182
220
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
221
|
+
console.warn(
|
|
222
|
+
`%c${clientID}`,
|
|
223
|
+
`color:white; padding: 2px 4px; border-radius: 3px;
|
|
224
|
+
background: ${colorHash.hex(clientID)}; `,
|
|
225
|
+
event.type,
|
|
188
226
|
);
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
doc.subscribe('$.content', (event) => {
|
|
230
|
+
if (event.type === 'remote-change') {
|
|
231
|
+
const { message, operations } = event.value;
|
|
232
|
+
handleOperations(quill, operations);
|
|
233
|
+
}
|
|
234
|
+
updateAllCursors();
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
doc.subscribe('presence', (event) => {
|
|
238
|
+
// Update cursors
|
|
239
|
+
if (event.type === 'initialized') {
|
|
240
|
+
updateAllCursors();
|
|
241
|
+
} else if (event.type === 'unwatched') {
|
|
242
|
+
cursors.removeCursor(event.value.clientID);
|
|
243
|
+
} else {
|
|
244
|
+
updateCursor(event.value);
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
// 03. create an instance of Quill
|
|
249
|
+
// Track composition state to prevent selection updates during IME input
|
|
250
|
+
let isComposing = false;
|
|
251
|
+
|
|
252
|
+
const editorElem = clientElem?.getElementsByClassName('editor')[0];
|
|
253
|
+
Quill.register('modules/cursors', QuillCursors);
|
|
254
|
+
const quill = new Quill(editorElem, {
|
|
255
|
+
modules: {
|
|
256
|
+
toolbar: [
|
|
257
|
+
['bold', 'italic', 'underline'],
|
|
258
|
+
[{ header: 1 }, { header: 2 }],
|
|
259
|
+
[{ list: 'ordered' }, { list: 'bullet' }],
|
|
260
|
+
['blockquote', 'code-block'],
|
|
261
|
+
['image', 'video'],
|
|
262
|
+
['clean'],
|
|
263
|
+
],
|
|
264
|
+
cursors: {
|
|
265
|
+
hideDelayMs: Number.MAX_VALUE,
|
|
266
|
+
},
|
|
267
|
+
},
|
|
268
|
+
theme: 'snow',
|
|
269
|
+
});
|
|
270
|
+
const cursors = quill.getModule('cursors');
|
|
271
|
+
|
|
272
|
+
function updateCursor(user) {
|
|
273
|
+
const { clientID, presence } = user;
|
|
274
|
+
// TODO(chacha912): After resolving the presence initialization issue(#608),
|
|
275
|
+
// remove the following check.
|
|
276
|
+
if (!presence) return;
|
|
277
|
+
|
|
278
|
+
const { name, selection } = presence;
|
|
279
|
+
if (!selection) return;
|
|
280
|
+
const range = doc
|
|
281
|
+
.getRoot()
|
|
282
|
+
.content.posRangeToIndexRange(selection);
|
|
283
|
+
cursors.createCursor(clientID, name, colorHash.hex(name));
|
|
284
|
+
cursors.moveCursor(clientID, {
|
|
285
|
+
index: range[0],
|
|
286
|
+
length: range[1] - range[0],
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function updateAllCursors() {
|
|
291
|
+
cursors.clearCursors();
|
|
292
|
+
for (const user of doc.getPresences()) {
|
|
293
|
+
updateCursor(user);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// 04. bind the document with the Quill.
|
|
298
|
+
// Track composition events to prevent selection updates during IME input
|
|
299
|
+
quill.root.addEventListener('compositionstart', () => {
|
|
300
|
+
isComposing = true;
|
|
301
|
+
});
|
|
302
|
+
quill.root.addEventListener('compositionend', () => {
|
|
303
|
+
isComposing = false;
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
// 04-1. Quill to Document.
|
|
307
|
+
quill
|
|
308
|
+
.on('text-change', (delta, _, source) => {
|
|
309
|
+
if (source === 'api' || !delta.ops) {
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
200
312
|
|
|
201
|
-
|
|
202
|
-
|
|
313
|
+
let from = 0,
|
|
314
|
+
to = 0;
|
|
315
|
+
console.log(
|
|
316
|
+
`%c quill: ${JSON.stringify(delta.ops)}`,
|
|
317
|
+
'color: green',
|
|
318
|
+
);
|
|
319
|
+
doc.update((root, presence) => {
|
|
320
|
+
for (const op of delta.ops) {
|
|
203
321
|
if (
|
|
204
|
-
op.attributes !== undefined
|
|
205
|
-
op.insert
|
|
322
|
+
op.attributes !== undefined ||
|
|
323
|
+
op.insert !== undefined
|
|
206
324
|
) {
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
325
|
+
if (
|
|
326
|
+
op.retain !== undefined &&
|
|
327
|
+
typeof op.retain === 'number'
|
|
328
|
+
) {
|
|
329
|
+
to = from + op.retain;
|
|
330
|
+
}
|
|
331
|
+
console.log(
|
|
332
|
+
`%c local: ${from}-${to}: ${op.insert} ${
|
|
333
|
+
op.attributes ? JSON.stringify(op.attributes) : '{}'
|
|
334
|
+
}`,
|
|
335
|
+
'color: green',
|
|
336
|
+
);
|
|
337
|
+
|
|
338
|
+
let range;
|
|
339
|
+
if (
|
|
340
|
+
op.attributes !== undefined &&
|
|
341
|
+
op.insert === undefined
|
|
342
|
+
) {
|
|
343
|
+
root.content.setStyle(from, to, op.attributes);
|
|
344
|
+
from = to;
|
|
345
|
+
} else if (op.insert !== undefined) {
|
|
346
|
+
if (to < from) {
|
|
347
|
+
to = from;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
if (typeof op.insert === 'object') {
|
|
351
|
+
range = root.content.edit(from, to, ' ', {
|
|
352
|
+
embed: JSON.stringify(op.insert),
|
|
353
|
+
...op.attributes,
|
|
354
|
+
});
|
|
355
|
+
} else {
|
|
356
|
+
range = root.content.edit(
|
|
357
|
+
from,
|
|
358
|
+
to,
|
|
359
|
+
op.insert,
|
|
360
|
+
op.attributes,
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
from =
|
|
364
|
+
to +
|
|
365
|
+
(typeof op.insert === 'string'
|
|
366
|
+
? op.insert.length
|
|
367
|
+
: 1);
|
|
210
368
|
to = from;
|
|
211
369
|
}
|
|
212
370
|
|
|
213
|
-
if (
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
...op.attributes,
|
|
371
|
+
if (range) {
|
|
372
|
+
presence.set({
|
|
373
|
+
selection: root.content.indexRangeToPosRange(range),
|
|
217
374
|
});
|
|
218
|
-
} else {
|
|
219
|
-
range = root.content.edit(
|
|
220
|
-
from,
|
|
221
|
-
to,
|
|
222
|
-
op.insert,
|
|
223
|
-
op.attributes,
|
|
224
|
-
);
|
|
225
375
|
}
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
if (
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
376
|
+
} else if (op.delete !== undefined) {
|
|
377
|
+
to = from + op.delete;
|
|
378
|
+
console.log(
|
|
379
|
+
`%c local: ${from}-${to}: ''`,
|
|
380
|
+
'color: green',
|
|
381
|
+
);
|
|
382
|
+
|
|
383
|
+
const range = root.content.edit(from, to, '');
|
|
384
|
+
if (range) {
|
|
385
|
+
presence.set({
|
|
386
|
+
selection: root.content.indexRangeToPosRange(range),
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
// After delete, 'to' should stay at 'from' since content was removed
|
|
390
|
+
to = from;
|
|
391
|
+
} else if (
|
|
392
|
+
op.retain !== undefined &&
|
|
393
|
+
typeof op.retain === 'number'
|
|
394
|
+
) {
|
|
395
|
+
from += op.retain;
|
|
396
|
+
to = from;
|
|
245
397
|
}
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
|
|
398
|
+
}
|
|
399
|
+
});
|
|
400
|
+
})
|
|
401
|
+
.on('selection-change', (range, _, source) => {
|
|
402
|
+
if (!range) {
|
|
403
|
+
return;
|
|
250
404
|
}
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
return;
|
|
256
|
-
}
|
|
257
|
-
// NOTE(chacha912): If the selection in the Quill editor does not match the range computed by yorkie,
|
|
258
|
-
// additional updates are necessary. This condition addresses situations where Quill's selection behaves
|
|
259
|
-
// differently, such as when inserting text before a range selection made by another user, causing
|
|
260
|
-
// the second character onwards to be included in the selection.
|
|
261
|
-
if (source === 'api') {
|
|
262
|
-
const [from, to] = doc
|
|
263
|
-
.getRoot()
|
|
264
|
-
.content.posRangeToIndexRange(doc.getMyPresence().selection);
|
|
265
|
-
const { index, length } = range;
|
|
266
|
-
if (from === index && to === index + length) {
|
|
405
|
+
|
|
406
|
+
// Ignore selection changes during composition (e.g., Korean IME input)
|
|
407
|
+
// to prevent cursor position from being broadcast incorrectly to other users
|
|
408
|
+
if (isComposing) {
|
|
267
409
|
return;
|
|
268
410
|
}
|
|
269
|
-
}
|
|
270
411
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
412
|
+
// NOTE(chacha912): If the selection in the Quill editor does not match the range computed by yorkie,
|
|
413
|
+
// additional updates are necessary. This condition addresses situations where Quill's selection behaves
|
|
414
|
+
// differently, such as when inserting text before a range selection made by another user, causing
|
|
415
|
+
// the second character onwards to be included in the selection.
|
|
416
|
+
if (source === 'api') {
|
|
417
|
+
const { selection } = doc.getMyPresence();
|
|
418
|
+
if (selection) {
|
|
419
|
+
const [from, to] = doc
|
|
420
|
+
.getRoot()
|
|
421
|
+
.content.posRangeToIndexRange(selection);
|
|
422
|
+
const { index, length } = range;
|
|
423
|
+
if (from === index && to === index + length) {
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
}
|
|
280
428
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
429
|
+
doc.update((root, presence) => {
|
|
430
|
+
presence.set({
|
|
431
|
+
selection: root.content.indexRangeToPosRange([
|
|
432
|
+
range.index,
|
|
433
|
+
range.index + range.length,
|
|
434
|
+
]),
|
|
435
|
+
});
|
|
436
|
+
}, `update selection by ${client.getID()}`);
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
// 04-2. document to Quill(remote).
|
|
440
|
+
function handleOperations(quill, ops) {
|
|
441
|
+
for (const op of ops) {
|
|
442
|
+
if (op.type === 'edit') {
|
|
443
|
+
const from = op.from;
|
|
444
|
+
const to = op.to;
|
|
445
|
+
const { insert, attributes } = toDeltaOperation(
|
|
446
|
+
op.value,
|
|
447
|
+
true,
|
|
448
|
+
);
|
|
449
|
+
console.log(
|
|
450
|
+
`%c remote: ${from}-${to}: ${insert}`,
|
|
451
|
+
'color: skyblue',
|
|
452
|
+
);
|
|
297
453
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
deltaOperations.push({ delete: retainTo });
|
|
303
|
-
}
|
|
304
|
-
if (insert) {
|
|
305
|
-
const deltaOp = { insert };
|
|
306
|
-
if (attributes) {
|
|
307
|
-
deltaOp.attributes = attributes;
|
|
454
|
+
const deltaOperations = [];
|
|
455
|
+
|
|
456
|
+
if (from > 0) {
|
|
457
|
+
deltaOperations.push({ retain: from });
|
|
308
458
|
}
|
|
309
|
-
deltaOperations.push(deltaOp);
|
|
310
|
-
}
|
|
311
|
-
} else if (op.type === 'style') {
|
|
312
|
-
const { attributes } = toDeltaOperation(op.value);
|
|
313
|
-
console.log(
|
|
314
|
-
`%c remote: ${from}-${to}: ${JSON.stringify(attributes)}`,
|
|
315
|
-
'color: skyblue',
|
|
316
|
-
);
|
|
317
459
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
if (
|
|
324
|
-
|
|
460
|
+
const deleteLength = to - from;
|
|
461
|
+
if (deleteLength > 0) {
|
|
462
|
+
deltaOperations.push({ delete: deleteLength });
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
if (insert) {
|
|
466
|
+
const op = { insert };
|
|
467
|
+
if (attributes) {
|
|
468
|
+
op.attributes = attributes;
|
|
469
|
+
}
|
|
470
|
+
deltaOperations.push(op);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
if (deltaOperations.length > 0) {
|
|
474
|
+
console.log(
|
|
475
|
+
`%c to quill: ${JSON.stringify(deltaOperations)}`,
|
|
476
|
+
'color: green',
|
|
477
|
+
);
|
|
478
|
+
const delta = new Quill.imports.delta(deltaOperations);
|
|
479
|
+
quill.updateContents(delta, 'api');
|
|
325
480
|
}
|
|
481
|
+
} else if (op.type === 'style') {
|
|
482
|
+
const from = op.from;
|
|
483
|
+
const to = op.to;
|
|
484
|
+
const { attributes } = toDeltaOperation(op.value, false);
|
|
485
|
+
console.log(
|
|
486
|
+
`%c remote: ${from}-${to}: ${JSON.stringify(attributes)}`,
|
|
487
|
+
'color: skyblue',
|
|
488
|
+
);
|
|
489
|
+
|
|
490
|
+
if (attributes) {
|
|
491
|
+
const deltaOperations = [];
|
|
492
|
+
|
|
493
|
+
if (from > 0) {
|
|
494
|
+
deltaOperations.push({ retain: from });
|
|
495
|
+
}
|
|
326
496
|
|
|
327
|
-
|
|
497
|
+
const op = { attributes };
|
|
498
|
+
const retainLength = to - from;
|
|
499
|
+
if (retainLength > 0) {
|
|
500
|
+
op.retain = retainLength;
|
|
501
|
+
}
|
|
502
|
+
deltaOperations.push(op);
|
|
503
|
+
|
|
504
|
+
console.log(
|
|
505
|
+
`%c to quill: ${JSON.stringify(deltaOperations)}`,
|
|
506
|
+
'color: green',
|
|
507
|
+
);
|
|
508
|
+
const delta = new Quill.imports.delta(deltaOperations);
|
|
509
|
+
quill.updateContents(delta, 'api');
|
|
510
|
+
}
|
|
328
511
|
}
|
|
329
512
|
}
|
|
330
|
-
|
|
331
|
-
prevTo = to;
|
|
332
513
|
}
|
|
333
514
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
515
|
+
// 05. synchronize text of document and Quill.
|
|
516
|
+
function syncText(doc, quill) {
|
|
517
|
+
const text = doc.getRoot().content;
|
|
518
|
+
const delta = new Quill.imports.delta(
|
|
519
|
+
text.values().map((val) => toDeltaOperation(val, true)),
|
|
338
520
|
);
|
|
339
|
-
quill.
|
|
521
|
+
quill.setContents(delta, 'api');
|
|
340
522
|
}
|
|
341
|
-
}
|
|
342
523
|
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
524
|
+
// 06. sync option
|
|
525
|
+
const option = clientElem.querySelector('.syncmode-option');
|
|
526
|
+
option.addEventListener('change', async (e) => {
|
|
527
|
+
if (!event.target.matches('input[type="radio"]')) {
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
const syncMode = event.target.value;
|
|
531
|
+
switch (syncMode) {
|
|
532
|
+
case 'pushpull':
|
|
533
|
+
await client.changeSyncMode(doc, 'realtime');
|
|
534
|
+
break;
|
|
535
|
+
case 'pushonly':
|
|
536
|
+
await client.changeSyncMode(doc, 'realtime-pushonly');
|
|
537
|
+
break;
|
|
538
|
+
case 'syncoff':
|
|
539
|
+
await client.changeSyncMode(doc, 'realtime-syncoff');
|
|
540
|
+
break;
|
|
541
|
+
case 'manual':
|
|
542
|
+
await client.changeSyncMode(doc, 'manual');
|
|
543
|
+
break;
|
|
544
|
+
default:
|
|
545
|
+
break;
|
|
546
|
+
}
|
|
547
|
+
});
|
|
548
|
+
const syncButton = clientElem.querySelector('.manual-sync');
|
|
549
|
+
syncButton.addEventListener('click', async () => {
|
|
550
|
+
await client.sync(doc);
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
syncText(doc, quill);
|
|
554
|
+
updateAllCursors();
|
|
555
|
+
|
|
556
|
+
return { client, doc, quill };
|
|
350
557
|
}
|
|
351
558
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
displayLog(documentElem, documentTextElem, doc);
|
|
559
|
+
await initializeRealtimeEditor(clientAElem);
|
|
560
|
+
await initializeRealtimeEditor(clientBElem);
|
|
355
561
|
} catch (e) {
|
|
356
562
|
console.error(e);
|
|
357
563
|
}
|
|
358
564
|
}
|
|
565
|
+
|
|
359
566
|
main();
|
|
360
567
|
</script>
|
|
361
568
|
</body>
|