@yorkie-js/sdk 0.6.0

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.
@@ -0,0 +1,477 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <title>Multi Example</title>
6
+ <link
7
+ href="https://cdn.quilljs.com/1.3.6/quill.snow.css"
8
+ rel="stylesheet"
9
+ />
10
+ <link rel="stylesheet" href="style.css" />
11
+ <script src="https://cdn.quilljs.com/1.3.6/quill.js"></script>
12
+ <script src="https://cdn.jsdelivr.net/npm/quill-cursors@3.1.0/dist/quill-cursors.min.js"></script>
13
+ <script src="https://cdn.jsdelivr.net/npm/color-hash@1.0.3/dist/color-hash.js"></script>
14
+ <script src="https://cdn.jsdelivr.net/npm/short-unique-id@4.4.4/dist/short-unique-id.min.js"></script>
15
+ </head>
16
+ <body>
17
+ <div>
18
+ <div id="network-status"></div>
19
+ <div id="online-clients"></div>
20
+ <div class="counter">
21
+ <h2>Counter</h2>
22
+ <button class="increaseButton">
23
+ count is <span class="count"></span>
24
+ </button>
25
+ </div>
26
+ <h2>Todo List</h2>
27
+ <div class="todos">
28
+ <ul class="todoList"></ul>
29
+ <div class="todoNew">
30
+ <input type="text" class="todoInput" placeholder="enter.." />
31
+ <button class="addButton">+</button>
32
+ </div>
33
+ </div>
34
+ <h2>Quill Editor</h2>
35
+ <div id="editor"></div>
36
+ <h2>yorkie document</h2>
37
+ <pre style="white-space: pre-wrap" id="log-holder"></pre>
38
+ </div>
39
+ <script src="./yorkie-js-sdk.js"></script>
40
+ <script src="./util.js"></script>
41
+ <script>
42
+ const statusHolder = document.getElementById('network-status');
43
+ const placeholder = document.getElementById('placeholder');
44
+ const onlineClientsHolder = document.getElementById('online-clients');
45
+ const logHolder = document.getElementById('log-holder');
46
+ const shortUniqueID = new ShortUniqueId();
47
+ const colorHash = new ColorHash();
48
+ const counter = document.querySelector('.count');
49
+ const counterIncreaseButton = document.querySelector('.increaseButton');
50
+ const todoList = document.querySelector('.todoList');
51
+ const todoInput = document.querySelector('.todoInput');
52
+ const addTodoButton = document.querySelector('.addButton');
53
+
54
+ function toDeltaOperation(textValue) {
55
+ const { embed, ...restAttributes } = textValue.attributes ?? {};
56
+
57
+ if (embed) {
58
+ return { insert: JSON.parse(embed), attributes: restAttributes };
59
+ }
60
+
61
+ return {
62
+ insert: textValue.content || '',
63
+ attributes: textValue.attributes,
64
+ };
65
+ }
66
+
67
+ function displayOnlineClients(presences, myClientID) {
68
+ const usernames = [];
69
+ for (const { clientID, presence } of presences) {
70
+ usernames.push(
71
+ myClientID === clientID
72
+ ? `<b>${presence.username}</b>`
73
+ : presence.username,
74
+ );
75
+ }
76
+ onlineClientsHolder.innerHTML = JSON.stringify(usernames);
77
+ }
78
+
79
+ async function main() {
80
+ try {
81
+ // 01-1. create client with RPCAddr.
82
+ const client = new yorkie.Client('http://localhost:8080');
83
+ // 01-2. activate client
84
+ await client.activate();
85
+
86
+ // 02. create a document then attach it into the client.
87
+ const doc = new yorkie.Document('multi-example', {
88
+ enableDevtools: true,
89
+ });
90
+ doc.subscribe('connection', new Network(statusHolder).statusListener);
91
+ doc.subscribe('presence', (event) => {
92
+ if (event.type === 'presence-changed') return;
93
+ displayOnlineClients(doc.getPresences(), client.getID());
94
+ });
95
+
96
+ doc.subscribe((event) => {
97
+ console.log('🟢 doc event', event);
98
+ if (event.type === 'snapshot') {
99
+ // The text is replaced to snapshot and must be re-synced.
100
+ syncText();
101
+ }
102
+ displayLog();
103
+ });
104
+
105
+ function displayLog() {
106
+ logHolder.innerHTML = JSON.stringify(doc.getRoot().toJS(), null, 2);
107
+ }
108
+
109
+ await client.attach(doc, {
110
+ initialPresence: { username: `user-${shortUniqueID()}` },
111
+ });
112
+
113
+ doc.update((root) => {
114
+ if (!root.counter) {
115
+ root.counter = new yorkie.Counter(yorkie.IntType, 0);
116
+ root.todos = [];
117
+ root.content = new yorkie.Text();
118
+ root.content.edit(0, 0, '\n');
119
+ root.obj = {
120
+ name: 'josh',
121
+ age: 14,
122
+ food: ['🍇', '🍌', '🍏'],
123
+ score: {
124
+ english: 80,
125
+ math: 90,
126
+ },
127
+ };
128
+ root.obj.score = { science: 100 };
129
+ delete root.obj.food;
130
+ }
131
+ }, 'initaialize doc');
132
+
133
+ // 03. Counter example
134
+ doc.subscribe('$.counter', (event) => {
135
+ console.log('🟣 counter event', event);
136
+ displayCount();
137
+ });
138
+
139
+ const displayCount = () => {
140
+ counter.textContent = doc.getValueByPath('$.counter').getValue();
141
+ // you can also get the value as follows:
142
+ // doc.getRoot().counter.getValue();
143
+ };
144
+
145
+ counterIncreaseButton.onclick = () => {
146
+ doc.update((root) => {
147
+ root.counter.increase(1);
148
+ });
149
+ };
150
+
151
+ // 04. Todo example
152
+ doc.subscribe('$.todos', (event) => {
153
+ console.log('🟡 todos event', event);
154
+
155
+ const { message, operations } = event.value;
156
+ for (const op of operations) {
157
+ const { type, path, index } = op;
158
+ switch (type) {
159
+ case 'add':
160
+ const value = doc.getValueByPath(`${path}.${index}`);
161
+ addTodo(value);
162
+ break;
163
+ default:
164
+ displayTodos();
165
+ break;
166
+ }
167
+ }
168
+
169
+ if (event.type === 'local-change') {
170
+ todoInput.value = '';
171
+ todoInput.focus();
172
+ }
173
+ });
174
+
175
+ function displayTodos() {
176
+ todoList.innerHTML = '';
177
+ doc.getValueByPath('$.todos').forEach((todo) => {
178
+ addTodo(todo);
179
+ });
180
+ }
181
+
182
+ function addTodo(text) {
183
+ const newTodo = document.createElement('li');
184
+ newTodo.classList.add('todoItem');
185
+ newTodo.innerHTML = `
186
+ <button class="moveUp">⬆</button>
187
+ <button class="moveDown">⬇</button>
188
+ <span class="itemName">${text}</span>
189
+ <button class="trash">🗑</button></li>
190
+ `;
191
+ todoList.appendChild(newTodo);
192
+ }
193
+ function handleAddTodo() {
194
+ const text = todoInput.value;
195
+ if (text === '') {
196
+ todoInput.focus();
197
+ return;
198
+ }
199
+ doc.update((root) => {
200
+ root.todos.push(text);
201
+ });
202
+ }
203
+
204
+ addTodoButton.addEventListener('click', handleAddTodo);
205
+ todoInput.addEventListener('keypress', (event) => {
206
+ if (event.key === 'Enter') {
207
+ handleAddTodo();
208
+ }
209
+ });
210
+ todoList.addEventListener('click', function (e) {
211
+ if (e.target.classList.contains('trash')) {
212
+ const li = e.target.parentNode;
213
+ const idx = Array.from(li.parentNode.children).indexOf(li);
214
+ doc.update((root) => {
215
+ const todoID = root.todos.getElementByIndex(idx).getID();
216
+ root.todos.deleteByID(todoID);
217
+ });
218
+ return;
219
+ }
220
+ if (e.target.classList.contains('moveUp')) {
221
+ const li = e.target.parentNode;
222
+ const idx = Array.from(li.parentNode.children).indexOf(li);
223
+ if (idx === 0) return;
224
+ doc.update((root) => {
225
+ const nextItem = root.todos.getElementByIndex(idx - 1);
226
+ const currItem = root.todos.getElementByIndex(idx);
227
+ root.todos.moveBefore(nextItem.getID(), currItem.getID());
228
+ });
229
+ return;
230
+ }
231
+ if (e.target.classList.contains('moveDown')) {
232
+ const li = e.target.parentNode;
233
+ const idx = Array.from(li.parentNode.children).indexOf(li);
234
+ if (idx === doc.getRoot().todos.length - 1) return;
235
+ doc.update((root) => {
236
+ const prevItem = root.todos.getElementByIndex(idx + 1);
237
+ const currItem = root.todos.getElementByIndex(idx);
238
+ root.todos.moveAfter(prevItem.getID(), currItem.getID());
239
+ });
240
+ return;
241
+ }
242
+ });
243
+
244
+ // 05. Quill example
245
+ doc.subscribe('$.content', (event) => {
246
+ console.log('🔵 quill event', event);
247
+ if (event.type === 'remote-change') {
248
+ const { actor, message, operations } = event.value;
249
+ handleOperations(operations, actor);
250
+ }
251
+ });
252
+ doc.subscribe('others', (event) => {
253
+ if (event.type === 'unwatched') {
254
+ cursors.removeCursor(event.value.presence.username);
255
+ } else if (event.type === 'presence-changed') {
256
+ displayRemoteCursor(event.value);
257
+ }
258
+ });
259
+
260
+ Quill.register('modules/cursors', QuillCursors);
261
+ const quill = new Quill('#editor', {
262
+ modules: {
263
+ toolbar: [
264
+ ['bold', 'italic', 'underline', 'strike'],
265
+ ['blockquote', 'code-block'],
266
+ [{ header: 1 }, { header: 2 }],
267
+ [{ list: 'ordered' }, { list: 'bullet' }],
268
+ [{ script: 'sub' }, { script: 'super' }],
269
+ [{ indent: '-1' }, { indent: '+1' }],
270
+ [{ direction: 'rtl' }],
271
+ [{ size: ['small', false, 'large', 'huge'] }],
272
+ [{ header: [1, 2, 3, 4, 5, 6, false] }],
273
+ [{ color: [] }, { background: [] }],
274
+ [{ font: [] }],
275
+ [{ align: [] }],
276
+ ['image', 'video'],
277
+ ['clean'],
278
+ ],
279
+ cursors: true,
280
+ },
281
+ theme: 'snow',
282
+ });
283
+ const cursors = quill.getModule('cursors');
284
+ function displayRemoteCursor(user) {
285
+ const {
286
+ clientID: id,
287
+ presence: { username, selection },
288
+ } = user;
289
+ if (!selection || id === client.getID()) return;
290
+ const range = doc.getRoot().content.posRangeToIndexRange(selection);
291
+ cursors.createCursor(username, username, colorHash.hex(username));
292
+ cursors.moveCursor(username, {
293
+ index: range[0],
294
+ length: range[1] - range[0],
295
+ });
296
+ }
297
+
298
+ // 05-1. Quill to Document.
299
+ quill
300
+ .on('text-change', (delta, _, source) => {
301
+ if (source === 'api' || !delta.ops) {
302
+ return;
303
+ }
304
+
305
+ let from = 0,
306
+ to = 0;
307
+ console.log(
308
+ `%c quill: ${JSON.stringify(delta.ops)}`,
309
+ 'color: green',
310
+ );
311
+ for (const op of delta.ops) {
312
+ if (op.attributes !== undefined || op.insert !== undefined) {
313
+ if (op.retain !== undefined) {
314
+ to = from + op.retain;
315
+ }
316
+ console.log(
317
+ `%c local: ${from}-${to}: ${op.insert} ${
318
+ op.attributes ? JSON.stringify(op.attributes) : '{}'
319
+ }`,
320
+ 'color: green',
321
+ );
322
+
323
+ doc.update((root, presence) => {
324
+ let range;
325
+ if (
326
+ op.attributes !== undefined &&
327
+ op.insert === undefined
328
+ ) {
329
+ root.content.setStyle(from, to, op.attributes);
330
+ } else if (op.insert !== undefined) {
331
+ if (to < from) {
332
+ to = from;
333
+ }
334
+
335
+ if (typeof op.insert === 'object') {
336
+ range = root.content.edit(from, to, ' ', {
337
+ embed: JSON.stringify(op.insert),
338
+ ...op.attributes,
339
+ });
340
+ } else {
341
+ range = root.content.edit(
342
+ from,
343
+ to,
344
+ op.insert,
345
+ op.attributes,
346
+ );
347
+ }
348
+ from = to + op.insert.length;
349
+ }
350
+
351
+ if (range) {
352
+ presence.set({
353
+ selection: root.content.indexRangeToPosRange(range),
354
+ });
355
+ }
356
+ }, `update style by ${client.getID()}`);
357
+ } else if (op.delete !== undefined) {
358
+ to = from + op.delete;
359
+ console.log(`%c local: ${from}-${to}: ''`, 'color: green');
360
+
361
+ doc.update((root, presence) => {
362
+ const range = root.content.edit(from, to, '');
363
+ if (range) {
364
+ presence.set({
365
+ selection: root.content.indexRangeToPosRange(range),
366
+ });
367
+ }
368
+ }, `update content by ${client.getID()}`);
369
+ } else if (op.retain !== undefined) {
370
+ from = to + op.retain;
371
+ to = from;
372
+ }
373
+ }
374
+ })
375
+ .on('selection-change', (range, _, source) => {
376
+ if (source === 'api' || !range) {
377
+ return;
378
+ }
379
+
380
+ doc.update((root, presence) => {
381
+ presence.set({
382
+ selection: root.content.indexRangeToPosRange([
383
+ range.index,
384
+ range.index + range.length,
385
+ ]),
386
+ });
387
+ }, `update selection by ${client.getID()}`);
388
+ });
389
+
390
+ // 05-2. Document to Quill(remote).
391
+ function handleOperations(ops, actor) {
392
+ const deltaOperations = [];
393
+ let prevTo = 0;
394
+ for (const op of ops) {
395
+ const actorName = doc.getPresence(actor).username;
396
+ const from = op.from;
397
+ const to = op.to;
398
+ const retainFrom = from - prevTo;
399
+ const retainTo = to - from;
400
+
401
+ if (op.type === 'edit') {
402
+ const { insert, attributes } = toDeltaOperation(op.value);
403
+ console.log(
404
+ `%c remote: ${from}-${to}: ${insert}`,
405
+ 'color: skyblue',
406
+ );
407
+
408
+ if (retainFrom) {
409
+ deltaOperations.push({ retain: retainFrom });
410
+ }
411
+ if (retainTo) {
412
+ deltaOperations.push({ delete: retainTo });
413
+ }
414
+ if (insert) {
415
+ const deltaOp = { insert };
416
+ if (attributes) {
417
+ deltaOp.attributes = attributes;
418
+ }
419
+ deltaOperations.push(deltaOp);
420
+ }
421
+ } else if (op.type === 'style') {
422
+ const { attributes } = toDeltaOperation(op.value);
423
+ console.log(
424
+ `%c remote: ${from}-${to}: ${JSON.stringify(attributes)}`,
425
+ 'color: skyblue',
426
+ );
427
+
428
+ if (retainFrom) {
429
+ deltaOperations.push({ retain: retainFrom });
430
+ }
431
+ if (attributes) {
432
+ const deltaOp = { attributes };
433
+ if (retainTo) {
434
+ deltaOp.retain = retainTo;
435
+ }
436
+
437
+ deltaOperations.push(deltaOp);
438
+ }
439
+ }
440
+
441
+ prevTo = to;
442
+ }
443
+
444
+ if (deltaOperations.length) {
445
+ console.log(
446
+ `%c to quill: ${JSON.stringify(deltaOperations)}`,
447
+ 'color: green',
448
+ );
449
+ quill.updateContents({ ops: deltaOperations }, 'api');
450
+ }
451
+ }
452
+
453
+ // 05-3. synchronize text of document and Quill.
454
+ function syncText() {
455
+ const text = doc.getRoot().content;
456
+ const delta = {
457
+ ops: text.values().map((val) => toDeltaOperation(val)),
458
+ };
459
+ quill.setContents(delta, 'api');
460
+ }
461
+
462
+ syncText();
463
+ displayCount();
464
+ displayTodos();
465
+ displayLog();
466
+ for (const user of doc.getPresences()) {
467
+ displayRemoteCursor(user);
468
+ }
469
+ } catch (e) {
470
+ console.error(e);
471
+ }
472
+ }
473
+
474
+ main();
475
+ </script>
476
+ </body>
477
+ </html>