Skip to content

Commit 9e8b166

Browse files
author
Alec Gibson
committed
Add presence example app
1 parent 338aefd commit 9e8b166

File tree

8 files changed

+308
-0
lines changed

8 files changed

+308
-0
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
static/dist/

examples/rich-text-presence/README.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# Collaborative Rich Text Editor with ShareDB
2+
3+
This is a collaborative rich text editor using [Quill](https://github.com/quilljs/quill) and the [rich-text OT type](https://github.com/ottypes/rich-text).
4+
5+
In this demo, data is not persisted. To persist data, run a Mongo
6+
server and initialize ShareDB with the
7+
[ShareDBMongo](https://github.com/share/sharedb-mongo) database adapter.
8+
9+
## Install dependencies
10+
```
11+
npm install
12+
```
13+
14+
## Build JavaScript bundle and run server
15+
```
16+
npm run build && npm start
17+
```
18+
19+
## Run app in browser
20+
Load [http://localhost:8080](http://localhost:8080)

examples/rich-text-presence/client.js

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
var ReconnectingWebSocket = require('reconnecting-websocket');
2+
var sharedb = require('sharedb/lib/client');
3+
var richText = require('./rich-text');
4+
var Quill = require('quill');
5+
var QuillCursors = require('quill-cursors');
6+
var tinycolor = require('tinycolor2');
7+
8+
sharedb.types.register(richText.type);
9+
Quill.register('modules/cursors', QuillCursors);
10+
11+
var clients = [];
12+
var colors = {};
13+
14+
var collection = 'examples';
15+
var id = 'richtext';
16+
17+
var editorContainer = document.querySelector('.editor-container');
18+
document.querySelector('#add-client').addEventListener('click', function() {
19+
addClient();
20+
});
21+
22+
addClient();
23+
addClient();
24+
25+
function addClient() {
26+
var socket = new ReconnectingWebSocket('ws://' + window.location.host);
27+
var connection = new sharedb.Connection(socket);
28+
var doc = connection.get(collection, id);
29+
doc.subscribe(function(err) {
30+
if (err) throw err;
31+
var quill = initialiseQuill(doc);
32+
var color = '#' + tinycolor.random().toHex();
33+
var id = 'client-' + (clients.length + 1);
34+
colors[id] = color;
35+
36+
clients.push({
37+
quill: quill,
38+
doc: doc,
39+
color: color
40+
});
41+
42+
document.querySelector('#' + id + ' h1').style.color = color;
43+
});
44+
}
45+
46+
function initialiseQuill(doc) {
47+
var quill = new Quill(quillContainer(), {
48+
theme: 'bubble',
49+
modules: {
50+
cursors: true
51+
}
52+
});
53+
var cursors = quill.getModule('cursors');
54+
var index = clients.length;
55+
56+
quill.setContents(doc.data);
57+
58+
quill.on('text-change', function(delta, oldDelta, source) {
59+
if (source !== 'user') return;
60+
doc.submitOp(delta);
61+
});
62+
63+
doc.on('op', function(op, source) {
64+
if (source) return;
65+
quill.updateContents(op);
66+
});
67+
68+
var presence = doc.connection.getDocPresence(collection, id);
69+
presence.subscribe(function(error) {
70+
if (error) throw error;
71+
});
72+
var localPresence = presence.create('client-' + (index + 1));
73+
74+
quill.on('selection-change', function(range) {
75+
// Ignore blurring, so that we can see lots of users in the
76+
// same window
77+
if (!range) return;
78+
localPresence.submit(range, function(error) {
79+
if (error) throw error;
80+
});
81+
});
82+
83+
presence.on('receive', function(id, range) {
84+
cursors.createCursor(id, id, colors[id]);
85+
cursors.moveCursor(id, range);
86+
});
87+
88+
return quill;
89+
}
90+
91+
function quillContainer() {
92+
var wrapper = document.createElement('div');
93+
wrapper.classList.add('editor');
94+
var index = clients.length;
95+
wrapper.id = 'client-' + (index + 1);
96+
97+
wrapper.innerHTML =
98+
' <h1>Client' + (index + 1) + '</h1>' +
99+
' <button class="remove-client">Remove</button>' +
100+
' <button class="client-connection connected">Disconnect</button>' +
101+
' <div class="quill"></div>';
102+
103+
wrapper.querySelector('.remove-client').addEventListener('click', function() {
104+
removeClient(clients[index]);
105+
});
106+
107+
var connectionButton = wrapper.querySelector('.client-connection');
108+
connectionButton.addEventListener('click', function() {
109+
toggleConnection(connectionButton, clients[index]);
110+
});
111+
112+
editorContainer.appendChild(wrapper);
113+
return wrapper.querySelector('.quill');
114+
}
115+
116+
function toggleConnection(button, client) {
117+
if (button.classList.contains('connected')) {
118+
button.classList.remove('connected');
119+
button.textContent = 'Connect';
120+
disconnectClient(client);
121+
} else {
122+
button.classList.add('connected');
123+
button.textContent = 'Disconnect';
124+
connectClient(client);
125+
}
126+
}
127+
128+
function disconnectClient(client) {
129+
client.doc.connection.close();
130+
}
131+
132+
function connectClient(client) {
133+
var socket = new ReconnectingWebSocket('ws://' + window.location.host);
134+
client.doc.connection.bindToSocket(socket);
135+
}
136+
137+
function removeClient(client) {
138+
client.quill.root.parentElement.parentElement.remove();
139+
client.doc.destroy(function(error) {
140+
if (error) throw error;
141+
});
142+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
{
2+
"name": "sharedb-example-rich-text-presence",
3+
"version": "1.0.0",
4+
"description": "An example of presence using ShareDB and Quill",
5+
"main": "server.js",
6+
"scripts": {
7+
"build": "mkdir -p static/dist/ && ./node_modules/.bin/browserify client.js -o static/dist/bundle.js",
8+
"test": "echo \"Error: no test specified\" && exit 1",
9+
"start": "node server.js"
10+
},
11+
"author": "Nate Smith",
12+
"contributors": [
13+
"Avital Oliver <[email protected]> (https://aoliver.org/)",
14+
"Alec Gibson <[email protected]>"
15+
],
16+
"license": "MIT",
17+
"dependencies": {
18+
"@teamwork/websocket-json-stream": "^2.0.0",
19+
"express": "^4.17.1",
20+
"quill": "^1.3.7",
21+
"quill-cursors": "^2.2.1",
22+
"reconnecting-websocket": "^4.2.0",
23+
"rich-text": "^4.0.0",
24+
"sharedb": "file:../../",
25+
"tinycolor2": "^1.4.1",
26+
"ws": "^7.2.0"
27+
},
28+
"devDependencies": {
29+
"browserify": "^16.5.0"
30+
}
31+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
var richText = require('rich-text');
2+
3+
richText.type.transformPresence = function(presence, op, isOwnOp) {
4+
if (!presence) {
5+
return null;
6+
}
7+
8+
var start = presence.index;
9+
var end = presence.index + presence.length;
10+
var delta = new richText.Delta(op);
11+
start = delta.transformPosition(start, !isOwnOp);
12+
end = delta.transformPosition(end, !isOwnOp);
13+
14+
return Object.assign({}, presence, {
15+
index: start,
16+
length: end - start
17+
});
18+
};
19+
20+
module.exports = richText;

examples/rich-text-presence/server.js

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
var http = require('http');
2+
var express = require('express');
3+
var ShareDB = require('sharedb');
4+
var richText = require('./rich-text');
5+
var WebSocket = require('ws');
6+
var WebSocketJSONStream = require('@teamwork/websocket-json-stream');
7+
8+
ShareDB.types.register(richText.type);
9+
var backend = new ShareDB();
10+
createDoc(startServer);
11+
12+
// Create initial document then fire callback
13+
function createDoc(callback) {
14+
var connection = backend.connect();
15+
var doc = connection.get('examples', 'richtext');
16+
doc.fetch(function(err) {
17+
if (err) throw err;
18+
if (doc.type === null) {
19+
doc.create([{insert: 'Hi!'}], 'rich-text', callback);
20+
return;
21+
}
22+
callback();
23+
});
24+
}
25+
26+
function startServer() {
27+
// Create a web server to serve files and listen to WebSocket connections
28+
var app = express();
29+
app.use(express.static('static'));
30+
app.use(express.static('node_modules/quill/dist'));
31+
var server = http.createServer(app);
32+
33+
// Connect any incoming WebSocket connection to ShareDB
34+
var wss = new WebSocket.Server({server: server});
35+
wss.on('connection', function(ws) {
36+
var stream = new WebSocketJSONStream(ws);
37+
backend.listen(stream);
38+
});
39+
40+
server.listen(8080);
41+
console.log('Listening on http://localhost:8080');
42+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<!DOCTYPE html>
2+
<meta charset="utf-8">
3+
<title>ShareDB Rich Text</title>
4+
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0"/>
5+
<link href="quill.bubble.css" rel="stylesheet">
6+
<link href="style.css" rel="stylesheet">
7+
8+
<div class="controls">
9+
<button id="add-client">
10+
Add client
11+
</button>
12+
</div>
13+
<div class="editor-container"></div>
14+
15+
<script src="dist/bundle.js"></script>
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
h1 {
2+
margin: 0 10px;
3+
display: inline-block;
4+
}
5+
6+
button {
7+
font-size: 20px;
8+
}
9+
10+
.controls {
11+
width: 100%;
12+
text-align: center;
13+
margin: 20px;
14+
}
15+
16+
.editor-container {
17+
width: 100%;
18+
}
19+
20+
.editor {
21+
display: inline-block;
22+
width: 50%;
23+
margin-bottom: 20px;
24+
}
25+
26+
.ql-container {
27+
padding: 10px;
28+
}
29+
30+
.ql-editor {
31+
/* TODO: Colour code with cursor? */
32+
border: 1px solid grey;
33+
}
34+
35+
.ql-tooltip {
36+
display: none;
37+
}

0 commit comments

Comments
 (0)