├── .gitignore ├── demo ├── .gitignore ├── package.json ├── index.js ├── demo.js ├── css │ └── demo.css └── index.html ├── package.json ├── README.md ├── index.js └── realtime-editor.js /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /data/ -------------------------------------------------------------------------------- /demo/.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /data/ -------------------------------------------------------------------------------- /demo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "realtime-editor-demo", 3 | "version": "1.0.0", 4 | "description": "A short demo example for realtime-editor", 5 | "author": "T.A.K.E.", 6 | "contributors": [ 7 | { 8 | "name": "T.A.K.E.", 9 | "email": "take@takedesign.dk" 10 | } 11 | ], 12 | "dependencies": { 13 | "express": "^4.13.4", 14 | "realtime-editor": "*" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /demo/index.js: -------------------------------------------------------------------------------- 1 | // Modules 2 | var express = require('express'); 3 | app = express(), 4 | http = require('http').Server(app), 5 | realtimeEditor = require('realtime-editor'); 6 | 7 | // App routing 8 | app.get('/', function (req, res) { 9 | res.sendFile(__dirname + '/index.html'); 10 | 11 | app.use("/node_modules", express.static(__dirname + "/node_modules")); 12 | app.use("/css", express.static(__dirname + "/css")); 13 | app.use("/js", express.static(__dirname + "/js")); 14 | }); 15 | 16 | // realtimeEditor hook for saving content 17 | // the data object have several properties including a custom property object which can hold your app specific IDs 18 | realtimeEditor.onSave(function (data) { 19 | console.log('realtimeEditor.onSave: ', data); 20 | }); 21 | 22 | http.listen(2000, function () { 23 | console.log('listening on *:2000'); 24 | }); -------------------------------------------------------------------------------- /demo/demo.js: -------------------------------------------------------------------------------- 1 | // Modules 2 | var express = require('express'); 3 | app = express(), 4 | http = require('http').Server(app), 5 | io = require('socket.io')(http), 6 | realtimeEditor = require('realtime-editor'); 7 | 8 | // App routing 9 | app.get('/', function (req, res) { 10 | res.sendFile(__dirname + '/index.html'); 11 | 12 | app.use("/node_modules", express.static(__dirname + "/node_modules")); 13 | app.use("/css", express.static(__dirname + "/css")); 14 | app.use("/js", express.static(__dirname + "/js")); 15 | }); 16 | 17 | // realtimeEditor hook for saving content 18 | // the data object have several properties including a custom property object which can hold your app specific IDs 19 | realtimeEditor.onSave(function (data) { 20 | console.log('realtimeEditor.onSave: ', data); 21 | }); 22 | 23 | 24 | http.listen(2000, function () { 25 | console.log('listening on *:2000'); 26 | }); -------------------------------------------------------------------------------- /demo/css/demo.css: -------------------------------------------------------------------------------- 1 | body { 2 | background: #EEE; 3 | } 4 | 5 | .customGridPlacement { 6 | justify-content: center; 7 | margin-top: 20px; 8 | } 9 | 10 | 11 | 12 | .realtimeWrapper { 13 | position: relative; 14 | margin-top: 30px; 15 | } 16 | 17 | .realtimeEditor { 18 | padding-bottom: 5px; 19 | min-height: 66px; 20 | border-bottom: 1px solid #ddd; 21 | color: #009688; 22 | -webkit-text-fill-color: rgba(0,0,0,0.65); 23 | } 24 | 25 | .realtimeEditor:focus { 26 | outline: none; 27 | } 28 | 29 | .realtimeLabel { 30 | position: absolute; 31 | top: 0; 32 | left: 0; 33 | right: 0; 34 | bottom: 0; 35 | color: #111; 36 | width: 100%; 37 | font-size: 16px; 38 | pointer-events: none; 39 | color: rgba(0, 0, 0, 0.38); 40 | -webkit-transition: 0.2s ease; 41 | -ms-transition: 0.2s ease; 42 | transition: 0.2s ease; 43 | } 44 | 45 | .realtimeWrapper.is-focused .realtimeLabel, .realtimeWrapper.is-dirty .realtimeLabel, 46 | .realtimeEditor:focus + label { 47 | margin-top: -20px; 48 | font-size: 12px; 49 | color: #009688; 50 | } 51 | 52 | .realtimeLabel:after { 53 | content: ''; 54 | background-color: #009688; 55 | left: 45%; 56 | width: 10px; 57 | height: 2px; 58 | visibility: hidden; 59 | position: absolute; 60 | bottom: 0; 61 | -webkit-transition: 0.2s ease; 62 | -ms-transition: 0.2s ease; 63 | transition: 0.2s ease; 64 | } 65 | 66 | .realtimeWrapper.is-focused .realtimeLabel:after { 67 | left: 0; 68 | visibility: visible; 69 | width: 100%; 70 | } 71 | 72 | .realtimeEditor:focus + label:after { 73 | left: 0; 74 | visibility: visible; 75 | width: 100%; 76 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "realtime-editor", 3 | "version": "0.5.0", 4 | "description": "Simple collaborative textareas - unstable beta", 5 | "author": { 6 | "name": "T.A.K.E.", 7 | "email": "take@takedesign.dk", 8 | "url": "http://takedesign.dk" 9 | }, 10 | "main": "index.js", 11 | "scripts": { 12 | "test": "echo \"Error: no test specified\" && exit 1" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "https://github.com/takedesign/realtime-editor.git" 17 | }, 18 | "license": "Apache Version 2.0", 19 | "dependencies": { 20 | "diff-match-patch": "^1.0.0", 21 | "socket.io": "^1.7.3" 22 | }, 23 | "devDependencies": { 24 | "jshint": "^2.9.1" 25 | }, 26 | "_id": "realtime-editor@0.3.0", 27 | "_shasum": "b7acbfd76681bc4572dcb87c031ce7fc98741ecc", 28 | "_from": "realtime-editor@*", 29 | "_npmVersion": "3.8.9", 30 | "_nodeVersion": "4.4.4", 31 | "_npmUser": { 32 | "name": "take", 33 | "email": "take@takedesign.dk" 34 | }, 35 | "dist": { 36 | "shasum": "b7acbfd76681bc4572dcb87c031ce7fc98741ecc", 37 | "tarball": "https://registry.npmjs.org/realtime-editor/-/realtime-editor-0.3.0.tgz" 38 | }, 39 | "maintainers": [ 40 | { 41 | "name": "take", 42 | "email": "take@takedesign.dk" 43 | } 44 | ], 45 | "_npmOperationalInternal": { 46 | "host": "packages-12-west.internal.npmjs.com", 47 | "tmp": "tmp/realtime-editor-0.3.0.tgz_1462549149459_0.9156533244531602" 48 | }, 49 | "directories": {}, 50 | "_resolved": "https://registry.npmjs.org/realtime-editor/-/realtime-editor-0.3.0.tgz" 51 | } 52 | -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | T.A.K.E. Realtime-editor 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 |
23 |
24 | Realtime-editor 25 | 26 |
27 | 28 | 32 |
33 |
34 | 35 |
36 | Realtime-editor 37 | 41 |
42 | 43 |
44 |
45 |
46 |
47 |

A simple collaborative textarea

48 |

Open this in another tab aswell to see it in action

49 |
50 | 51 |
52 |
53 | 54 |
55 |
56 |

57 |
58 | 59 |
60 | 61 |
62 |
63 | 64 |
65 |
66 | 67 |
68 |
69 |

70 |
71 | 72 |
73 | 74 |
75 |
76 |
77 |
78 | 79 |
80 |
81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 137 | 138 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # realtime-editor 2 | Ever had the same feeling as us about how complicated and soul-crushing it can be to implement some sort of a collaborative editor? ..Even a simple one? 3 | 4 | If Etherpad are either too big or too much of what you need and shareJS doesnt fit your application (as ours don't since we build upon [socket.io](https://www.npmjs.com/package/socket.io) this plugin might be what you're looking for. 5 | 6 | Here at [T.A.K.E.](http://takedesign.dk/) we have made a very simple "textarea" where the only needs required were to have it be on some sort of collaborative level while not requiring insane amount of server configuration nor external/extra db logic. 7 | 8 | 9 | This realtime-editor is a lightweight node module with a server and client side script. It uses [socket.io](https://www.npmjs.com/package/socket.io) and [diff-match-patch](https://code.google.com/p/google-diff-match-patch/). It doesnt solve all the collaborative problems or needs but if it fits your needs go ahead and give it a try. 10 | 11 | 12 | NOTE: Before we begin, this is a really early beta version and is quite unstable.. more updates and documentations is on its way.. 13 | 14 | 15 | Setup 16 | -------- 17 | Npm install the sucker and include it to your server index.js 18 | 19 | ```js 20 | npm install realtime-editor 21 | ``` 22 | ```js 23 | var realtimeEditor = require('realtime-editor'); 24 | ``` 25 | 26 | Add the client part aswell to your application's index.html 27 | 28 | ```html 29 | 30 | ``` 31 | 32 | And dont forget the 2 dependencies socket.io and diff-match-patch client parts aswell if you dont have them included allready 33 | 34 | ```html 35 | 36 | 37 | ``` 38 | 39 | 40 | Usage 41 | -------- 42 | 43 | It's currently build around MDL-Lite's material design framework but it should work without it (Dont blame us if doesnt!). 44 | 45 | For the MDL styles check the example in the demo folder. For now, here is the bare one. Feel free to include your own styles and a label tag inside the div wrapper at the bottom 46 | 47 | ```html 48 |
49 |
50 |

51 |
52 |
53 | ``` 54 | 55 | Now init socket.io client part and the the textarea through javascript 56 | 57 | ```js 58 | var socket = io.connect(); 59 | 60 | var editor = new realtimeEditor(options); 61 | ``` 62 | 63 | The options argument needs atleast the id of the text field aswell as an unique identifier fx a project id. 64 | It takes several others optional parameters such as an user color. 65 | 66 | The text property consist of an array with an object for each line created in it. The array can either start empty or with some data (fx. stored from your database). 67 | The format of the objects inside the text array needs to have the properties as shown below, alltho they are auto generated when new lines are created, but make sure you save the whole text array when storing it to your database. 68 | 69 | ```js 70 | var options = { 71 | id: 'textarea1', // unique to the textfield 72 | projectId: 'someUniqueIdentifier', // required in order to have several active editors on the same page 73 | room: 'uniqueTextRoom', // unique room id, default is projectId combined with the element id 74 | text: [ // init the textarea with the newest text 75 | { 76 | author: '', 77 | text: 'line_1', 78 | id: '1459856606818_16407750' // id of the line auto generated. 79 | }, 80 | { 81 | author: '', 82 | text: 'line_2', 83 | id: '1459865117436_19682870' 84 | }, 85 | { 86 | author: '', 87 | text: 'line_3', 88 | id: '1459865208855_19888940' 89 | } 90 | ], 91 | custom: { // custom object such as specific appication IDs. Fx in order to save it on the server side 92 | appId: 1, 93 | customProperty: 'some_application_specific_here' 94 | } 95 | }; 96 | 97 | new realtimeEditor(options); 98 | ``` 99 | 100 | 101 | Options 102 | -------- 103 | 104 | | Parameter | Type | Default | Description | 105 | | ------------- | --------- | ------------- | --------------------------------------------------------------------- | 106 | | id | string | undefined | The id of the textarea. Is requried | 107 | | projectId | integer | 1 | Will be renamed at some point. is required in order to have multiple editors on same page | 108 | | room | string | projectId + id| Room name for socket.io. Make sure its unqiue in order to avoid conflicts. Default is the id of the textarea | 109 | | color | string | random | Set a user color as such #1d1d1d | 110 | | author | string | random | Set an id of the user. make sure its unique and no spaces | 111 | | authorName | string | random | Set name of author. random name is generated if none applied. Not complete | 112 | | message | string | Connection lost. please wait.. | Message will be desplayed below when socket connection is lost. Change it here to fit your language | 113 | | custom | object | {} | This is where you add your applications specific properties incase you want to do something with the data like save it to your own db in a hook | 114 | 115 | 116 | Hooks 117 | -------- 118 | 119 | On your server side you can add a hook which will fire when something changes 120 | 121 | 122 | ```js 123 | var editor = new realtimeEditor(options); 124 | 125 | realtimeEditor.onSave(function (data) { 126 | // do something with the data object here like stringify it and save it to your fauvorite db 127 | }); 128 | ``` 129 | 130 | 131 | Demo 132 | -------- 133 | 134 | A demo is included. Check it out by cloning the demo folder, go into it and run ```npm install``` followed by a ```node demo.js``` 135 | Open your browser and go to http://localhost:2000 to see the example 136 | 137 | 138 | Todo 139 | -------- 140 | * Atm you cant write on same line as it updates the text per line 141 | * More stable version aka. better server testing / fallback 142 | * Undo/redo availability (keyboard shortcuts) 143 | * More test! 144 | * Author text string on an individual line is not getting set correctly atm 145 | * Gif demo example.. gotta have those animated gifs! 146 | * maybe include text styling in the long run like a WYSIWYG editor 147 | * did I mention test? 148 | 149 | 150 | Keep making it better 151 | -------- 152 | Feel free to donate in order to help us out. 153 | Any amount will be greatly appreciated, for the many hours invested into this, aswell as in future developement. 154 | 155 | [![paypal](https://www.paypalobjects.com/en_US/DK/i/btn/btn_donateCC_LG.gif)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=WBXRF3VJD2MJY) 156 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | // realtime-editor 2 | // 3 | // server side part 4 | // 5 | 6 | var emitter = require('events'), 7 | //io = require('socket.io')(http), 8 | diffMatchPatch = require('diff-match-patch'), 9 | dmp = new diffMatchPatch(); 10 | 11 | 12 | function realtimeEditor (parameters, custom) { 13 | this.emitter = new emitter(); 14 | 15 | this.options = parameters || {}; 16 | this.textarea = {}; 17 | 18 | this.init(); 19 | } 20 | 21 | 22 | // init the socket.io connection to coEditor 23 | realtimeEditor.prototype.init = function () { 24 | var that = this; 25 | 26 | io.sockets.on('connection', function (socket) { 27 | 28 | socket.on('rtEditorSync', function (data, callback) { 29 | that.syncText(data, function (res) { 30 | socket.broadcast.to(data.room).emit('rtEditorBroadcast', res); 31 | }); 32 | }); 33 | 34 | socket.on('rtEditorJoin', function (data, callback) { 35 | socket.rtEditor = { 36 | room: data.room 37 | }; 38 | 39 | socket.join(data.room, function () { 40 | // check if there is a data object allready 41 | 42 | /*if (that.textarea[data.projectId] !== undefined) { 43 | if (that.textarea[data.projectId][data.targetId] !== undefined) { 44 | 45 | data.text = that.textarea[data.projectId][data.targetId].data; 46 | } 47 | }*/ 48 | 49 | //console.log('join room:', data.room); 50 | 51 | 52 | if (that.textarea[data.projectId] === undefined) { 53 | that.textarea[data.projectId] = { 54 | projectId: data.projectId 55 | }; 56 | } 57 | 58 | if (that.textarea[data.projectId][data.targetId] === undefined) { 59 | that.textarea[data.projectId][data.targetId] = { 60 | targetId: data.targetId, 61 | data: data.text, 62 | timeout: 0 63 | }; 64 | } 65 | 66 | if (callback !== undefined) { 67 | callback({mesage: 'done joining the room: ' + data.room, data: that.textarea[data.projectId][data.targetId].data}); 68 | } 69 | }); 70 | }); 71 | 72 | socket.on('rtEditorRejoin', function (data, callback) { 73 | socket.join(data.room, function () { 74 | if (callback !== undefined) { 75 | callback({mesage: 'done rejoining the room: ' + data.room, data: data}); 76 | } 77 | }); 78 | }); 79 | 80 | socket.on('rtEditorExit', function (data, callback) { 81 | socket.leave(data.room, function () { 82 | if (callback !== undefined) { 83 | callback({mesage: 'done leaving room: ' + data.room, data: data}); 84 | } 85 | }); 86 | }); 87 | 88 | socket.on('disconnect', function () { 89 | /*console.log('on plugin disconnect', socket.rtEditor); 90 | 91 | for (var editor in socket.rtEditor) { 92 | console.log('clear cursor from this:', socket.rtEditor[editor]); 93 | 94 | socket.broadcast.to(data.room).emit('rtEditorBroadcast', {}); 95 | }*/ 96 | }); 97 | 98 | }); 99 | }; 100 | 101 | 102 | // emit back to parameter callback 103 | realtimeEditor.prototype.emit = function (name, data) { 104 | if (this.options[name] !== undefined) { 105 | this.options[name](data); 106 | } 107 | }; 108 | 109 | // sync the text to the server object 110 | realtimeEditor.prototype.syncText = function (data, callback) { 111 | //console.log('syncText', data); 112 | 113 | var line, previousLine, currentText, currentTextIndex, previousLineIndex, loopedLine, 114 | diff, patchText, resultText, 115 | that = this; 116 | 117 | //console.log('data', data); 118 | 119 | if (this.textarea[data.projectId] === undefined) { 120 | this.textarea[data.projectId] = { 121 | projectId: data.projectId 122 | }; 123 | } 124 | 125 | if (this.textarea[data.projectId][data.targetId] === undefined) { 126 | // create object if first time and save all current lines 127 | this.textarea[data.projectId][data.targetId] = { 128 | targetId: data.targetId, 129 | data: data.savedLines 130 | }; 131 | } 132 | 133 | // patch changes only and save all current lines after patch 134 | //console.log('patch'); 135 | 136 | for (var l = 0; l < this.textarea[data.projectId][data.targetId].data.length; l++) { 137 | line = this.textarea[data.projectId][data.targetId].data[l]; 138 | 139 | // find line to patch 140 | if (line.id === data.activeLineId) { 141 | currentText = line.text; 142 | currentTextIndex = l; 143 | //console.log('found currentText to patch', data.activeLineText); 144 | } 145 | 146 | // find previous line if any 147 | if (line.id === data.previousLineId) { 148 | previousLine = line.text; 149 | previousLineIndex = l; 150 | 151 | //console.log('found previousLine to fix', data.previousLineText); 152 | } 153 | } 154 | 155 | if (data.type === 'modifyLine') { // patch existing line 156 | diff = dmp.diff_main(currentText, data.activeLineText); 157 | patchText = dmp.patch_make(currentText, data.activeLineText, diff); 158 | resultText = dmp.patch_apply(patchText, currentText); 159 | 160 | this.textarea[data.projectId][data.targetId].data[currentTextIndex].text = resultText[0]; 161 | this.textarea[data.projectId][data.targetId].data[currentTextIndex].author = data.author; 162 | } else if (data.type === 'newLine') { // add new line 163 | if (previousLineIndex === (this.textarea[data.projectId][data.targetId].data.length - 1)) { 164 | // if last line append 165 | this.textarea[data.projectId][data.targetId].data.push({ 166 | id: data.activeLineId, 167 | text: data.activeLineText, 168 | author: data.author 169 | }); 170 | } else { 171 | // if not last line insertBefore 172 | this.textarea[data.projectId][data.targetId].data.splice(previousLineIndex + 1, 0, { 173 | id: data.activeLineId, 174 | text: data.activeLineText, 175 | author: data.author 176 | }); 177 | } 178 | } else if (data.type === 'breakLine') { 179 | if (previousLineIndex === (this.textarea[data.projectId][data.targetId].data.length - 1)) { 180 | // if last line append 181 | this.textarea[data.projectId][data.targetId].data.push({ 182 | id: data.activeLineId, 183 | text: data.activeLineText, 184 | author: data.author 185 | }); 186 | } else { 187 | // if not last line insertBefore 188 | this.textarea[data.projectId][data.targetId].data.splice(previousLineIndex + 1, 0, { 189 | id: data.activeLineId, 190 | text: data.activeLineText, 191 | author: data.author 192 | }); 193 | } 194 | 195 | 196 | diff = dmp.diff_main(previousLine, data.previousLineText); 197 | patchText = dmp.patch_make(previousLine, data.previousLineText, diff); 198 | resultText = dmp.patch_apply(patchText, previousLine); 199 | 200 | this.textarea[data.projectId][data.targetId].data[previousLineIndex].text = (resultText[0] === '' ? '
' : resultText[0]); 201 | } else if (data.type === 'pastedContent') { 202 | for (var n = 0; n < data.newLines.length; n++) { 203 | if (n === 0) { 204 | // if first line 205 | 206 | for (var d = 0; d < this.textarea[data.projectId][data.targetId].data.length; d++) { 207 | loopedLine = this.textarea[data.projectId][data.targetId].data[d]; 208 | 209 | if (loopedLine.id === data.newLines[n].id) { 210 | 211 | diff = dmp.diff_main(loopedLine.text, data.newLines[n].text); 212 | patchText = dmp.patch_make(loopedLine.text, data.newLines[n].text, diff); 213 | resultText = dmp.patch_apply(patchText, loopedLine.text); 214 | 215 | loopedLine.text = resultText[0]; 216 | 217 | previousLine = d; 218 | } 219 | } 220 | 221 | // diff & patch first line of content 222 | 223 | } else { 224 | this.textarea[data.projectId][data.targetId].data.splice((previousLine + 1), 0, { 225 | id: data.newLines[n].id, 226 | text: data.newLines[n].text, 227 | author: data.newLines[n].author, 228 | }); 229 | 230 | previousLine = n; 231 | } 232 | 233 | } 234 | 235 | } 236 | 237 | 238 | // Handle deleted lines 239 | if (data.deletedLines !== undefined) { 240 | if (data.deletedLines.length > 0) { 241 | for (var l = 0; l < data.deletedLines.length; l++) { 242 | for (var d = this.textarea[data.projectId][data.targetId].data.length - 1; d >= 0; d--) { 243 | if (this.textarea[data.projectId][data.targetId].data[d].id === data.deletedLines[l]) { 244 | this.textarea[data.projectId][data.targetId].data.splice(d, 1); 245 | } 246 | } 247 | } 248 | } 249 | } 250 | 251 | 252 | 253 | //console.log('data', this.textarea[data.projectId][data.targetId]); 254 | 255 | /*if (callback !== undefined) { 256 | callback(this.textarea[data.projectId]); 257 | }*/ 258 | 259 | 260 | // server demo test 261 | //var diff = dmp.diff_main(serverText, 'h1 elo'); 262 | //var patch_list = dmp.patch_make(serverText, 'h1 elo', diff); 263 | //var result = dmp.patch_apply(patch_list, serverText); 264 | 265 | //console.log('sync text', this.options); 266 | 267 | 268 | // callbacks 269 | callback(data); 270 | 271 | if (data.type !== 'clearCursor' && data.type !== 'moveCursor') { 272 | clearTimeout(this.textarea[data.projectId][data.targetId].timeout); 273 | 274 | // timeout to avoid db spam 275 | this.textarea[data.projectId][data.targetId].timeout = setTimeout(function () { 276 | that.emitter.emit('onSave', data); 277 | }, 700); 278 | } 279 | }; 280 | 281 | 282 | 283 | realtimeEditor.prototype.onSave = function (callback) { 284 | this.emitter.on('onSave', function (data) { 285 | 286 | // emit specific information parts 287 | // add more properties from the data object if needed here 288 | var savedData = { 289 | targetId: data.targetId, 290 | author: data.author, 291 | text: data.savedLines, 292 | custom: data.custom 293 | }; 294 | 295 | callback(savedData); 296 | }); 297 | 298 | }; 299 | 300 | module.exports = new realtimeEditor; -------------------------------------------------------------------------------- /realtime-editor.js: -------------------------------------------------------------------------------- 1 | // realtime-editor 2 | // 3 | // client side part 4 | // 5 | 6 | function realtimeEditor (options) { 7 | var that = this, 8 | random = Math.floor((Math.random() * 100000) + 1), 9 | div; 10 | 11 | // The .bind method from Prototype.js 12 | if (!Function.prototype.bind) { // check if native implementation is not available (it is for ES5+) 13 | Function.prototype.bind = function () { 14 | var fn = this, 15 | args = Array.prototype.slice.call(arguments), 16 | object = args.shift(); 17 | 18 | return function () { 19 | return fn.apply(object, args.concat(Array.prototype.slice.call(arguments))); 20 | }; 21 | }; 22 | } 23 | 24 | // required checks 25 | if (options.id === undefined) { 26 | console.error('realtimeEditor: textarea id is required'); 27 | 28 | return; 29 | } 30 | 31 | if (options.text === undefined) { 32 | console.error('realtimeEditor: textarea text array is required'); 33 | 34 | return; 35 | } 36 | 37 | // temp values 38 | if (options.author === undefined && sessionStorage.tempRealtimeauthor === undefined) { 39 | sessionStorage.tempRealtimeauthor = 'user' + random; 40 | } 41 | 42 | if (options.authorName === undefined && sessionStorage.tempRealtimeAuthorName === undefined) { 43 | sessionStorage.tempRealtimeAuthorName = 'user' + random; 44 | } 45 | 46 | if (options.color === undefined && sessionStorage.tempRealtimeColor === undefined) { 47 | sessionStorage.tempRealtimeColor = '#' + Math.floor(Math.random() * 16777215).toString(16); 48 | } 49 | 50 | // set standing variables 51 | this.author = options.author || sessionStorage.tempRealtimeauthor; 52 | this.authorName = options.authorName || sessionStorage.tempRealtimeAuthorName; 53 | this.id = options.id; 54 | this.text = (options.text.length > 0 ? options.text : this.emptyLine()); 55 | this.color = options.color || sessionStorage.tempRealtimeColor; 56 | this.lineHeight = options.lineHeight || 20; 57 | this.editor = document.getElementById(options.id); 58 | this.projectId = options.projectId || 1; 59 | this.room = (options.room === undefined ? (options.projectId + '' + options.id) : options.room); 60 | 61 | this.message = options.message || 'Connection lost. please wait..'; 62 | this.custom = options.custom || {}; 63 | 64 | this.update.bind(this); 65 | 66 | if (socket) { 67 | socket.emit('rtEditorJoin', {room: that.room, targetId: that.id, projectId: that.projectId, text: that.text}, function (res) { 68 | that.text = res.data; 69 | 70 | that.loadText(); 71 | }); 72 | 73 | socket.on('connect', function () { 74 | socket.emit('rtEditorRejoin', {room: that.room}, function (res) { 75 | var data = { 76 | room: that.room, 77 | targetId: that.id, 78 | projectId: that.projectId, 79 | type: 'clearCursor', 80 | savedLines: that.getLines() 81 | }; 82 | 83 | that.send(data); 84 | 85 | that.editor.style.opacity = 1; 86 | that.editor.contentEditable = true; 87 | 88 | that.toggleMessage('hide'); 89 | }); 90 | }); 91 | 92 | if (socket.rtEditorBroadcast === undefined) { 93 | socket.rtEditorBroadcast = true; 94 | socket.on('rtEditorBroadcast', function (data) { 95 | that.update(data); 96 | }); 97 | } 98 | 99 | if (socket.rtEditorDisconnect === undefined) { 100 | socket.rtEditorDisconnect = true; 101 | socket.on('disconnect', function () { 102 | that.editor.style.opacity = 0.7; 103 | that.editor.contentEditable = false; 104 | that.toggleMessage('show'); 105 | }); 106 | } 107 | 108 | 109 | } else { 110 | console.error('realtimeEditor: socket.io not detected'); 111 | } 112 | } 113 | 114 | // show the text 115 | realtimeEditor.prototype.loadText = function () { 116 | this.editor.addEventListener('focus', this.onFocus.bind(this), false); 117 | this.editor.addEventListener('blur', this.onBlur.bind(this), false); 118 | this.editor.addEventListener('click', this.onClick.bind(this), false); 119 | this.editor.addEventListener('keydown', this.keydown.bind(this), false); 120 | this.editor.addEventListener('keyup', this.keyup.bind(this), false); 121 | this.editor.addEventListener('paste', this.paste.bind(this), false); 122 | 123 | // check if selected field is not active 124 | if (document.activeElement.id !== this.editor.id) { 125 | this.editor.innerHTML = ''; 126 | 127 | if (this.text.length > 0) { 128 | for (var t = 0; t < this.text.length; t++) { 129 | div = document.createElement('div'); 130 | 131 | div.id = this.text[t].id; 132 | div.innerHTML = this.text[t].text; 133 | 134 | this.editor.appendChild(div); 135 | 136 | this.editor.parentNode.classList.add('is-dirty'); 137 | } 138 | } else { 139 | // if existing data array is empty insert default empty lines 140 | this.insertDefault(this.editor); 141 | } 142 | } 143 | }; 144 | 145 | // create a new empty line 146 | realtimeEditor.prototype.emptyLine = function () { 147 | return [ 148 | { 149 | author: this.author, 150 | text: '
', 151 | id: new Date().getTime() + '_' + Math.floor(Math.random() * (2000000 - 0)) + 0 152 | } 153 | ]; 154 | }; 155 | 156 | // on focus add active class 157 | realtimeEditor.prototype.onFocus = function (event) { 158 | event.target.parentNode.classList.add('is-focused'); 159 | }; 160 | 161 | // on blur remove active class 162 | realtimeEditor.prototype.onBlur = function (event) { 163 | var data = { 164 | room: this.room, 165 | color: this.color, 166 | targetId: this.id, 167 | projectId: this.projectId, 168 | type: 'clearCursor', 169 | author: this.author, 170 | savedLines: this.getLines() 171 | }; 172 | 173 | event.target.parentNode.classList.remove('is-focused'); 174 | 175 | this.send(data); 176 | }; 177 | 178 | // on click 179 | realtimeEditor.prototype.onClick = function (event) { 180 | var data = { 181 | room: this.room, 182 | color: this.color, 183 | targetId: this.id, 184 | projectId: this.projectId, 185 | type: 'moveCursor', 186 | author: this.author, 187 | authorName: this.authorName, 188 | savedLines: this.getLines(), 189 | caretPos: this.getCaret() 190 | }; 191 | 192 | if (data.caretPos.activeLine.tagName === 'DIV') { 193 | this.send(data); 194 | } 195 | }; 196 | 197 | // get caret line index and offset 198 | realtimeEditor.prototype.getCaret = function (event) { 199 | var selection = window.getSelection(), 200 | caret = {}; 201 | 202 | if (selection.anchorNode !== null) { 203 | if (selection.anchorNode.nodeType === 1) { 204 | caret.activeLine = selection.anchorNode; 205 | } else { 206 | caret.activeLine = selection.anchorNode.parentNode; 207 | } 208 | 209 | caret.activeLineId = caret.activeLine.id; 210 | caret.offset = selection.anchorOffset; 211 | caret.lineIndex = [].indexOf.call(caret.activeLine.parentNode.children, caret.activeLine); 212 | } 213 | 214 | return caret; 215 | }; 216 | 217 | // Paste 218 | realtimeEditor.prototype.paste = function (event) { 219 | var pasteLine = event.target, 220 | textarea = pasteLine.parentNode, 221 | selection = window.getSelection(), 222 | range = document.createRange(), 223 | pasteLineText = pasteLine.innerHTML, 224 | caretOffset = selection.getRangeAt(0).startOffset, 225 | pastedLines = event.clipboardData.getData('text/plain').split('\n'), 226 | div, 227 | previousLine, 228 | timeId; 229 | 230 | event.preventDefault(); 231 | 232 | pasteLineText = pasteLine.innerHTML; 233 | 234 | for (var p = 0; p < pastedLines.length; p++) { 235 | // special stuff for first pasted line 236 | if (p === 0) { 237 | 238 | // add remaining of line one if only one line was pasted in 239 | if (pastedLines.length === 1) { 240 | pasteLine.innerHTML = pasteLineText.substr(0, caretOffset) + pastedLines[p] + pasteLineText.substr(caretOffset); 241 | } else { 242 | pasteLine.innerHTML = pasteLineText.substr(0, caretOffset) + pastedLines[p]; 243 | } 244 | 245 | previousLine = pasteLine; 246 | } else { 247 | div = document.createElement('div'); 248 | timeId = new Date().getTime() + '_' + Math.floor(Math.random() * (2000000 - 0)) + 0; 249 | 250 | div.id = timeId; 251 | div.innerHTML = (p === (pastedLines.length - 1) ? pastedLines[p] + pasteLineText.substr(caretOffset) : pastedLines[p]); 252 | 253 | textarea.insertBefore(div, previousLine.nextSibling); 254 | 255 | previousLine = div; 256 | } 257 | 258 | this.newLines.push({ 259 | id: (p === 0 ? pasteLine.id : timeId), 260 | text: previousLine.innerHTML.replace(/<\/?[^>]+(>|$)/g, ''), 261 | author: sessionStorage.uniId || '' 262 | }); 263 | 264 | // place caret when last inserted row 265 | if (p === (pastedLines.length - 1)) { 266 | //console.log('place the damn caret', caretOffset); 267 | previousLine.focus(); 268 | 269 | // if only one line was pasted add the caretOffset 270 | if (pastedLines.length === 1) { 271 | selection.collapse(previousLine.childNodes[0], (caretOffset + pastedLines[pastedLines.length - 1].length)); 272 | } else { 273 | selection.collapse(previousLine.childNodes[0], pastedLines[pastedLines.length - 1].length); 274 | } 275 | 276 | //range.setStart(previousLine, 0); 277 | //range.collapse(true); 278 | } 279 | } 280 | 281 | this.clipboardData = { 282 | pasteLine: pasteLine.id, 283 | indexLine: [].indexOf.call(pasteLine.parentNode.children, pasteLine), 284 | lines: event.clipboardData.getData('text/plain').split('\n') 285 | }; 286 | }; 287 | 288 | 289 | // default tree empty lines 290 | realtimeEditor.prototype.insertDefault = function (target) { 291 | var div; 292 | 293 | target.innerHTML = ''; 294 | 295 | for (var p = 0; p < 1; p++) { 296 | div = document.createElement('div'); 297 | 298 | div.id = new Date().getTime() + '_' + Math.floor(Math.random() * (2000000 - 0)) + 0; 299 | div.innerHTML = '
'; 300 | 301 | target.appendChild(div); 302 | } 303 | }; 304 | 305 | // Handle keydown 306 | realtimeEditor.prototype.keydown = function (event) { 307 | var textarea = event.target, 308 | timeId = new Date().getTime() + '_' + Math.floor(Math.random() * (2000000 - 0)) + 0, 309 | lines = textarea.getElementsByTagName('div'), 310 | div = document.createElement('div'), 311 | selection = window.getSelection(), 312 | activeLine; 313 | 314 | // reset paste boolean 315 | this.clipboardData = {}; 316 | this.newLines = []; 317 | 318 | if (event.ctrlKey) { 319 | if (event.keyCode == 90) { // undo 320 | event.preventDefault(); 321 | 322 | console.log('undo - not implemented yet'); 323 | return; 324 | } else if (event.keyCode == 89) { // redo 325 | event.preventDefault(); 326 | 327 | console.log('redo - not implemented yet'); 328 | return; 329 | } 330 | } 331 | 332 | if (event.keyCode === 13) { 333 | // only adapt if Firefox on enter 334 | if (window.navigator.userAgent.indexOf('Firefox') > -1 || window.navigator.userAgent.indexOf('Edge') > -1) { 335 | if (selection.anchorNode.nodeType === 1) { 336 | activeLine = selection.anchorNode; 337 | } else { 338 | activeLine = selection.anchorNode.parentNode; 339 | } 340 | 341 | event.preventDefault(); 342 | 343 | div.id = timeId; 344 | 345 | activeLine.parentNode.insertBefore(div, activeLine.nextSibling); 346 | 347 | div.focus(); 348 | 349 | selection.collapse(div, 0); 350 | } 351 | } 352 | 353 | this.storedLines = []; 354 | 355 | for (var l = 0; l < lines.length; l++) { 356 | this.storedLines.push(lines[l].id); 357 | } 358 | }; 359 | 360 | // Handle keyup 361 | realtimeEditor.prototype.keyup = function (event) { 362 | if (event.keyCode === 16 || event.keyCode === 17 || event.keyCode === 18) { // prevent shift, ctrl & alt default 363 | return false; 364 | } 365 | 366 | var textarea = event.target, 367 | lines = textarea.getElementsByTagName('div'), 368 | selection = window.getSelection(), 369 | range = selection.getRangeAt(0), 370 | timeId = new Date().getTime() + '_' + Math.floor(Math.random() * (2000000 - 0)) + 0, 371 | deletedLines = [], 372 | newLines = [], 373 | savedLines = [], 374 | newLineCounter = 0, 375 | data, 376 | currentIndex, 377 | previousLine, 378 | activeLine, 379 | type, 380 | lineExist; 381 | 382 | // check deleted lines 383 | if (lines.length < this.storedLines.length) { 384 | for (var s = 0; s < this.storedLines.length; s++) { 385 | lineExist = false; 386 | 387 | for (l = 0; l < lines.length; l++) { 388 | if (lines[l].id === this.storedLines[s]) { 389 | lineExist = true; 390 | } 391 | } 392 | 393 | if (lineExist === false) { 394 | deletedLines.push(this.storedLines[s]); 395 | } 396 | } 397 | } 398 | 399 | // get active line based on caret position 400 | 401 | if (window.getSelection().anchorNode.nodeType === 1) { 402 | activeLine = window.getSelection().anchorNode; 403 | } else { 404 | activeLine = window.getSelection().anchorNode.parentNode; 405 | } 406 | 407 | if (event.keyCode === 13) { // if enter 408 | if (activeLine.innerHTML === '
') { 409 | type = 'newLine'; 410 | } else { 411 | type = 'breakLine'; 412 | 413 | if (activeLine.innerHTML === '') { 414 | activeLine.innerHTML = '
'; 415 | } 416 | } 417 | 418 | if (textarea.parentNode.classList.contains('is-dirty') === false) { 419 | textarea.parentNode.classList.add('is-dirty'); 420 | } 421 | } else if (event.keyCode === 37 || event.keyCode === 38 || event.keyCode === 39 || event.keyCode === 40) { // if arrows 422 | type = 'moveCursor'; 423 | timeId = activeLine.id; 424 | } else { 425 | type = 'modifyLine'; 426 | timeId = activeLine.id; 427 | 428 | if (textarea.parentNode.classList.contains('is-dirty') === false) { 429 | textarea.parentNode.classList.add('is-dirty'); 430 | } 431 | } 432 | 433 | currentIndex = [].indexOf.call(activeLine.parentNode.children, activeLine); 434 | previousLine = activeLine.previousSibling; 435 | 436 | if (currentIndex === 0) { 437 | previousLineId = 'firstLine'; 438 | } else { 439 | previousLineId = previousLine.id; 440 | } 441 | 442 | activeLine.id = timeId; 443 | 444 | // Check and handle paste content 445 | if (this.clipboardData.lines !== undefined) { 446 | if (this.clipboardData.lines.length > 0) { 447 | type = 'pastedContent'; 448 | 449 | //console.log('clipboardData', this.clipboardData); 450 | } 451 | } 452 | 453 | for (var l = 0; l < lines.length; l++) { 454 | savedLines.push({ 455 | id: lines[l].id, 456 | text: lines[l].innerHTML, 457 | author: '' 458 | }); 459 | } 460 | 461 | //console.log('savedLines', savedLines); 462 | 463 | data = { 464 | room: this.room, 465 | color: this.color, 466 | targetId: textarea.id, 467 | projectId: this.projectId, 468 | activeLineId: timeId, 469 | activeLineText: (activeLine.innerHTML === '
' ? '
' : activeLine.innerHTML.replace(/<\/?[^>]+(>|$)/g, '')), 470 | previousLineId: previousLineId, 471 | previousLineText: (type === 'breakLine' ? previousLine.innerHTML.replace(/<\/?[^>]+(>|$)/g, '') : undefined), 472 | type: type, 473 | author: this.author, 474 | indexLine: [].indexOf.call(activeLine.parentNode.children, activeLine), 475 | savedLines: savedLines, 476 | linesAmount: lines.length, 477 | deletedLines: deletedLines, 478 | newLines: this.newLines, 479 | caretPos: this.getCaret(), 480 | custom: this.custom 481 | }; 482 | 483 | this.send(data); 484 | }; 485 | 486 | realtimeEditor.prototype.deletedLines = function (data) { 487 | var textarea = document.getElementById(data.targetId), 488 | line; 489 | 490 | for (var i = data.deletedLines.length - 1; i >= 0; i--) { 491 | line = document.getElementById(data.deletedLines[i]); 492 | 493 | textarea.removeChild(line); 494 | } 495 | }; 496 | 497 | // send 498 | realtimeEditor.prototype.send = function (data) { 499 | socket.emit('rtEditorSync', data, function (res) { 500 | //console.log('realtimeEditor res: ', res); 501 | }); 502 | }; 503 | 504 | // Patch & Update specific editor with changes 505 | realtimeEditor.prototype.update = function (data) { 506 | var target = document.getElementById(data.targetId), 507 | line = document.getElementById(data.activeLineId), 508 | dmp = new diff_match_patch(), 509 | loopedLine, 510 | previousLine, 511 | currentText, 512 | div, 513 | diff, 514 | patchText, 515 | resultText, 516 | data, 517 | currentTextIndex, 518 | previousLineIndex; 519 | 520 | if (target !== null) { 521 | /*for (var l = 0; l < this.text.length; l++) { 522 | line = this.text[l]; 523 | 524 | // find line to patch 525 | if (line.id === data.activeLineId) { 526 | currentText = line.text; 527 | currentTextIndex = l; 528 | //console.log('found currentText to patch', data.activeLineText); 529 | } 530 | 531 | // find previous line if any 532 | if (line.id === data.previousLineId) { 533 | previousLine = line.text; 534 | previousLineIndex = l; 535 | 536 | //console.log('found previousLine to fix', data.previousLineText); 537 | } 538 | }*/ 539 | 540 | if (data.type === 'modifyLine') { // patch existing line 541 | currentText = line.innerHTML; 542 | 543 | diff = dmp.diff_main(currentText, data.activeLineText); 544 | patchText = dmp.patch_make(currentText, data.activeLineText, diff); 545 | resultText = dmp.patch_apply(patchText, currentText); 546 | 547 | line.innerHTML = resultText[0]; 548 | 549 | // data object 550 | //this.text[currentTextIndex].text = resultText[0]; 551 | //this.text[currentTextIndex].author = data.author; 552 | } else if (data.type === 'newLine') { // add new line 553 | previousLine = document.getElementById(data.previousLineId); 554 | div = document.createElement('div'); 555 | div.id = data.activeLineId; 556 | div.innerHTML = data.activeLineText; 557 | 558 | target.insertBefore(div, previousLine.nextSibling); 559 | 560 | // data object 561 | 562 | } else if (data.type === 'breakLine') { 563 | previousLine = document.getElementById(data.previousLineId); 564 | div = document.createElement('div'); 565 | div.id = data.activeLineId; 566 | div.innerHTML = data.activeLineText; 567 | 568 | target.insertBefore(div, previousLine.nextSibling); 569 | 570 | diff = dmp.diff_main(previousLine.innerHTML, data.previousLineText); 571 | patchText = dmp.patch_make(previousLine.innerHTML, data.previousLineText, diff); 572 | resultText = dmp.patch_apply(patchText, previousLine.innerHTML); 573 | 574 | previousLine.innerHTML = (resultText[0] === '' ? '
' : resultText[0]); 575 | } else if (data.type === 'pastedContent') { 576 | for (var n = 0; n < data.newLines.length; n++) { 577 | if (n === 0) { 578 | loopedLine = document.getElementById(data.newLines[n].id); 579 | 580 | // diff & patch first line of content 581 | diff = dmp.diff_main(loopedLine.innerHTML, data.newLines[n].text); 582 | patchText = dmp.patch_make(loopedLine.innerHTML, data.newLines[n].text, diff); 583 | resultText = dmp.patch_apply(patchText, loopedLine.innerHTML); 584 | 585 | loopedLine.innerHTML = resultText[0]; 586 | 587 | previousLine = loopedLine; 588 | } else { 589 | div = document.createElement('div'); 590 | div.id = data.newLines[n].id; 591 | div.innerHTML = data.newLines[n].text; 592 | 593 | target.insertBefore(div, previousLine.nextSibling); 594 | 595 | previousLine = div; 596 | } 597 | } 598 | } 599 | 600 | 601 | // Handle deleted lines 602 | if (data.deletedLines) { 603 | if (data.deletedLines.length > 0) { 604 | this.deletedLines(data); 605 | } 606 | } 607 | 608 | 609 | // move the recived data's user cursor 610 | if (data.type === 'clearCursor') { 611 | this.clearCursor(data); 612 | } else { 613 | if (document.getElementById(data.caretPos.activeLineId) !== null) { 614 | this.moveCursor(data); 615 | } 616 | } 617 | 618 | // move the client user cursor 619 | // only when data.type !== moveCursor to avoid infinite broadcast loop 620 | if (data.type !== 'moveCursor') { 621 | data = { 622 | room: this.room, 623 | color: this.color, 624 | targetId: this.id, 625 | projectId: this.projectId, 626 | type: 'moveCursor', 627 | author: this.author, 628 | savedLines: this.getLines(), 629 | caretPos: this.getCaret() 630 | }; 631 | 632 | if (data.caretPos.activeLine !== undefined) { 633 | this.send(data); 634 | } 635 | } 636 | } 637 | }; 638 | 639 | realtimeEditor.prototype.getLines = function (data) { 640 | var lines = this.editor.getElementsByTagName('div'), 641 | savedLines = []; 642 | 643 | for (var l = 0; l < lines.length; l++) { 644 | savedLines.push({ 645 | id: lines[l].id, 646 | text: lines[l].innerHTML, 647 | author: '' 648 | }); 649 | } 650 | 651 | return savedLines; 652 | }; 653 | 654 | // move cursor 655 | // data properties required author, caretPos 656 | realtimeEditor.prototype.moveCursor = function (data) { 657 | var user = document.getElementById(data.author), 658 | target = document.getElementById(data.targetId), 659 | offsetText = data.savedLines[data.caretPos.lineIndex].text.substr(0, data.caretPos.offset), 660 | computedStyles = window.getComputedStyle(document.getElementById(data.caretPos.activeLineId), null), 661 | font = computedStyles.getPropertyValue('font-weight') + ' ' + computedStyles.getPropertyValue('font-size') + ' ' + computedStyles.getPropertyValue('font-family'), 662 | name; 663 | 664 | if (user === null && target !== null) { 665 | user = document.createElement('span'); 666 | name = document.createElement('span'); 667 | 668 | user.id = data.author; 669 | user.className = 'realtimeEditorUser'; 670 | user.style.cssText = 'position: absolute; background-color: ' + data.color + '; width: 2px; height: 20px; top: 0; left: 0;'; 671 | user.style.top = (data.caretPos.lineIndex * this.lineHeight) + 'px'; 672 | user.style.left = this.getTextWidth(offsetText, font) + 'px'; 673 | user.contentEditable = false; 674 | 675 | name.style.cssText = 'position: absolute; opacity: 0; transition: 0.2s ease; visibility: hidden; min-width: ' + (this.getTextWidth(data.authorName, 'normal 10px Roboto, Helvetica, Arial') + 5) + 'px; z-index: 1; font-size: 10px; background-color: ' + data.color + '; color: #FFF !important; height: 15px; line-height: 15px; padding-left: 5px; bottom: 20px'; 676 | name.innerHTML = data.authorName; 677 | 678 | user.appendChild(name); 679 | target.appendChild(user); 680 | 681 | user.addEventListener('mouseenter', this.showName, false); 682 | user.addEventListener('mouseleave', this.hideName, false); 683 | } else { 684 | user.style.top = (data.caretPos.lineIndex * this.lineHeight) + 'px'; 685 | user.style.left = this.getTextWidth(offsetText, font) + 'px'; 686 | } 687 | }; 688 | 689 | // get the width of the text to calculate in pixel the caret position 690 | realtimeEditor.prototype.getTextWidth = function (text, font) { 691 | var canvas = this.getTextWidth.canvas || (this.getTextWidth.canvas = document.createElement("canvas")), // re-use canvas object for better performance 692 | context = canvas.getContext("2d"), 693 | metrics; 694 | 695 | context.font = font; 696 | 697 | metrics = context.measureText(text); 698 | 699 | return Math.floor(metrics.width); 700 | }; 701 | 702 | // remove the user color node 703 | realtimeEditor.prototype.clearCursor = function (data) { 704 | var user = document.getElementById(data.author); 705 | 706 | if (user !== null) { 707 | user.parentNode.removeChild(user); 708 | } 709 | }; 710 | 711 | // show caret user name 712 | realtimeEditor.prototype.showName = function (event) { 713 | var name = this.getElementsByTagName('span')[0]; 714 | 715 | name.style.opacity = 1; 716 | name.style.visibility = 'visible'; 717 | }; 718 | 719 | realtimeEditor.prototype.hideName = function (event) { 720 | var name = this.getElementsByTagName('span')[0]; 721 | 722 | name.style.opacity = 0; 723 | name.style.visibility = 'hidden'; 724 | }; 725 | 726 | // toggle message upon disconnect 727 | realtimeEditor.prototype.toggleMessage = function (action) { 728 | var message = document.getElementById('rtEditor_' + this.id), 729 | div; 730 | 731 | if (message === null) { 732 | div = document.createElement('div'); 733 | 734 | div.id = 'rtEditor_' + this.id; 735 | div.style.font = 'italic 14px Roboto, Helvetica, Arial'; 736 | div.innerHTML = this.message; 737 | } 738 | 739 | if (action === 'show') { 740 | this.editor.parentNode.appendChild(div); 741 | } else { 742 | if (message !== null) { 743 | message.parentNode.removeChild(message); 744 | } 745 | } 746 | }; 747 | 748 | realtimeEditor.prototype.exit = function (room, callback) { 749 | if (socket) { 750 | socket.emit('rtEditorExit', {room: room}, function (res) { 751 | if (callback !== undefined) { 752 | callback(res); 753 | } 754 | }); 755 | } else { 756 | console.error('realtimeEditor: cant leave room, socket.io not detected'); 757 | } 758 | }; --------------------------------------------------------------------------------