├── .gitignore ├── LICENSE ├── README.md ├── client.js ├── package.json ├── server.js └── static ├── index.html └── styles.css /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.swp 3 | package-lock.json 4 | static/dist 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Datavis Tech INC 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # json0-presence-demo 2 | 3 | A demo of plain text [presence](https://github.com/ottypes/docs/issues/29) using [ShareDB](https://github.com/Teamwork/sharedb) and [html-text-collab-ext](https://github.com/convergencelabs/html-text-collab-ext). 4 | 5 |  6 | 7 |  8 | 9 | This application was originally authored by [Stian Håklev](https://github.com/houshuang) in [github.com/houshuang/presence-demo](https://github.com/houshuang/presence-demo). The original application was simplified by Curran Kelleher down to a single `textarea` with presence. The purpose of this simplified version is provide a more minimal example to demonstrate that the [Presence work on json0](https://github.com/ottypes/json0/pull/31) functions within an integrated system. 10 | 11 | To run locally: 12 | 13 | ``` 14 | npm install 15 | npm run build && npm start 16 | ``` 17 | -------------------------------------------------------------------------------- /client.js: -------------------------------------------------------------------------------- 1 | const ShareDB = require('@teamwork/sharedb/lib/client'); 2 | const HtmlTextCollabExt = require('@convergence/html-text-collab-ext'); 3 | const StringBinding = require('sharedb-string-binding'); 4 | const { hcl } = require('d3-color'); 5 | 6 | // Use the json0 fork that implements presence. 7 | const json0 = require('@datavis-tech/ot-json0'); 8 | ShareDB.types.register(json0.type); 9 | ShareDB.types.defaultType = json0.type; 10 | 11 | // Open WebSocket connection to ShareDB server 12 | const socket = new WebSocket('ws://' + window.location.host); 13 | const connection = new ShareDB.Connection(socket); 14 | 15 | // Sample user names for local testing. 16 | const names = ['Peter', 'Anna', 'John', 'Ole', 'Niels']; 17 | 18 | // Colors for names. Fixed chroma and lightness, varying hue. 19 | const colors = names.map((name, i) => hcl(i / names.length * 360, 90, 35)); 20 | 21 | // Returns a color for a user name. 22 | const uidColor = uid => colors[names.findIndex(x => x === uid)]; 23 | 24 | // Renders the user name and color at the top. 25 | const renderNameplate = uid => { 26 | const nameplate = document.getElementById('nameplate'); 27 | nameplate.style = 'background-color: ' + uidColor(uid) + ';'; 28 | nameplate.innerText = uid; 29 | }; 30 | 31 | const textarea = document.getElementById('example'); 32 | 33 | // Update presence data when textarea is focused. 34 | textarea.addEventListener('focus', () => { 35 | updateCursorText( 36 | [textarea.selectionStart, textarea.selectionStart], 37 | uid, 38 | 'example' 39 | ); 40 | }); 41 | 42 | // Create local Doc instance. 43 | const doc = connection.get('examples', 'example'); 44 | 45 | // Generate a random uid and display it. 46 | const uid = names[Math.floor(Math.random() * names.length)]; 47 | renderNameplate(uid); 48 | 49 | const collaborators = {}; 50 | 51 | doc.subscribe(function(err) { 52 | if (err) throw err; 53 | 54 | const binding = new StringBinding(textarea, doc, ['example']); 55 | binding.setup(); 56 | 57 | const textEditor = new HtmlTextCollabExt.CollaborativeTextEditor({ 58 | control: textarea, 59 | onSelectionChanged: selection => 60 | updateCursorText([selection.anchor, selection.target], uid, 'example') 61 | }); 62 | 63 | const selectionManager = textEditor.selectionManager(); 64 | 65 | // When we receive information about updated presences, update the ui. 66 | doc.on('presence', (srcList, submitted) => { 67 | srcList.forEach(src => { 68 | if (!doc.presence[src]) return; 69 | 70 | // Unpack the json0 presence object. 71 | const presence = doc.presence[src]; 72 | const presencePath = presence.p; 73 | const presenceType = presence.t; 74 | const subPresence = presence.s; 75 | 76 | // Unpack the text0 sub-presence object. 77 | const userid = subPresence.u; 78 | if ( 79 | userid !== uid && 80 | subPresence && 81 | subPresence.s && 82 | subPresence.s.length > 0 83 | ) { 84 | const sel = subPresence.s[0]; 85 | 86 | if (!collaborators[userid]) { 87 | collaborators[userid] = selectionManager.addCollaborator( 88 | userid, 89 | userid, 90 | uidColor(userid) 91 | ); 92 | } 93 | 94 | collaborators[userid].setSelection({ 95 | anchor: sel[0], 96 | target: sel[1] 97 | }); 98 | 99 | collaborators[userid].flashCursorToolTip(2); 100 | } 101 | }); 102 | }); 103 | }); 104 | 105 | // Submits presence to the ShareDB document for the current cursor/selection. 106 | function updateCursorText(range, uid, text) { 107 | if (range) { 108 | doc.submitPresence({ 109 | p: [text], 110 | t: 'text0', 111 | s: { 112 | u: uid, 113 | c: 0, 114 | s: [range] 115 | } 116 | }); 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "json0-presence-demo", 3 | "version": "0.0.1", 4 | "description": "A demo of presence cursors with json0 and ShareDB.", 5 | "main": "server.js", 6 | "scripts": { 7 | "build": "mkdir -p static/dist/ && ./node_modules/.bin/browserify client.js -o static/dist/bundle.js", 8 | "watch": "./node_modules/.bin/watchify client.js -o static/dist/bundle.js -v", 9 | "test": "echo \"Error: no test specified\" && exit 1", 10 | "start": "node server.js" 11 | }, 12 | "author": "Stian Håklev", 13 | "contributors": [ 14 | "Curran Kelleher" 15 | ], 16 | "license": "MIT", 17 | "dependencies": { 18 | "@convergence/html-text-collab-ext": "^0.1.1", 19 | "@convergence/string-change-detector": "^0.1.8", 20 | "@datavis-tech/ot-json0": "^1.2.0", 21 | "@teamwork/sharedb": "^3.1.0", 22 | "@teamwork/websocket-json-stream": "^2.0.0", 23 | "d3-color": "^1.2.3", 24 | "express": "^4.16.4", 25 | "sharedb-string-binding": "^1.0.0", 26 | "textarea-caret": "^3.1.0", 27 | "watchify": "^3.11.1", 28 | "ws": "^6.2.1" 29 | }, 30 | "devDependencies": { 31 | "browserify": "^16.2.3" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | const http = require('http'); 2 | const express = require('express'); 3 | const ShareDB = require('@teamwork/sharedb'); 4 | const WebSocket = require('ws'); 5 | const WebSocketJSONStream = require('@teamwork/websocket-json-stream'); 6 | 7 | // Use the json0 fork that implements presence. 8 | const json0 = require('@datavis-tech/ot-json0'); 9 | ShareDB.types.register(json0.type); 10 | ShareDB.types.defaultType = json0.type; 11 | 12 | const backend = new ShareDB({ 13 | disableDocAction: true, 14 | disableSpaceDelimitedActions: true 15 | }); 16 | createDoc(startServer); 17 | 18 | // Create initial document. 19 | function createDoc(callback) { 20 | const connection = backend.connect(); 21 | const doc = connection.get('examples', 'example'); 22 | doc.fetch(function(err) { 23 | if (err) throw err; 24 | if (doc.type === null) { 25 | doc.create({ example: '' }, 'json0', callback); 26 | return; 27 | } 28 | callback(); 29 | }); 30 | } 31 | 32 | // Create a web server to serve files and listen to WebSocket connections 33 | function startServer() { 34 | const app = express(); 35 | app.use(express.static('static')); 36 | const server = http.createServer(app); 37 | 38 | const wss = new WebSocket.Server({ server: server }); 39 | wss.on('connection', function(ws, req) { 40 | const stream = new WebSocketJSONStream(ws); 41 | backend.listen(stream); 42 | }); 43 | 44 | server.listen(8080); 45 | console.log('Listening on http://localhost:8080'); 46 | } 47 | -------------------------------------------------------------------------------- /static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 |