sn-collab-editor 0.1.6 → 0.1.7
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.
- checksums.yaml +4 -4
- data/app/assets/javascripts/collab/cable.js +121 -11
- data/app/assets/javascripts/collab/documents.js +142 -52
- data/app/assets/stylesheets/collab/{application.css → application.css.scss} +31 -4
- data/app/channels/edit_channel.rb +16 -10
- data/app/controllers/collab/documents_controller.rb +2 -2
- data/app/models/collab/document.rb +1 -0
- data/app/models/collab/patch.rb +5 -0
- data/app/views/collab/documents/show.html.erb +11 -4
- data/app/views/layouts/collab/application.html.erb +5 -0
- data/db/migrate/20170222133537_create_collab_documents.rb +8 -3
- data/lib/collab/engine.rb +1 -2
- data/lib/collab/version.rb +1 -1
- data/vendor/assets/javascripts/TextPatcher.js +154 -0
- data/vendor/assets/javascripts/chainpad.js +1588 -0
- data/vendor/assets/javascripts/codemirror.js +9231 -0
- data/vendor/assets/javascripts/markdown.js +816 -0
- data/vendor/assets/stylesheets/codemirror.css +341 -0
- metadata +9 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 584e0566ecfea667d8de83f27f541eff2dff10f1
|
4
|
+
data.tar.gz: d3fd5591f13379168c8037db93a18c751635a96e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 68145c660dd250530bceb127b67bcd402315c63ebf017da2ca08bb901a5e178cc62ed6c14c9bc682a775d8fa6ae1a18c9f2d59d2d322da53f8982a3fa398124d
|
7
|
+
data.tar.gz: 5e8e96e7a82dfb96d6d76cefdea4cf74708f4a074245e9d76f9e34d4c26a59865a54d0ae926d2ddcc6d809338d0da99a5e5e7b64be094f14dc9bd3319f1dc37d
|
@@ -9,35 +9,145 @@ document.addEventListener("DOMContentLoaded", function(event) {
|
|
9
9
|
|
10
10
|
var clientId = Math.random()*100;
|
11
11
|
|
12
|
+
var editor;
|
13
|
+
App.registerChainpadObserver = function(inEditor) {
|
14
|
+
editor = inEditor;
|
15
|
+
}.bind(this)
|
16
|
+
|
17
|
+
var _chainpad, patchText;
|
18
|
+
|
19
|
+
function initChainpad() {
|
20
|
+
_chainpad = ChainPad.create({
|
21
|
+
checkpointInterval: 3,
|
22
|
+
logLevel: 0
|
23
|
+
});
|
24
|
+
|
25
|
+
patchText = TextPatcher.create({
|
26
|
+
realtime: _chainpad,
|
27
|
+
})
|
28
|
+
|
29
|
+
_chainpad.onChange(function (offset, toRemove, toInsert) {
|
30
|
+
var currentContent = editor.getContent();
|
31
|
+
var newContent = currentContent.substring(0, offset) + toInsert + currentContent.substring(offset + toRemove);
|
32
|
+
|
33
|
+
var op = {offset: offset, toRemove: toRemove, toInsert: toInsert};
|
34
|
+
var oldCursor = {};
|
35
|
+
oldCursor.selectionStart = cursorToPos(App.editor.getCursor('from'), currentContent);
|
36
|
+
oldCursor.selectionEnd = cursorToPos(App.editor.getCursor('to'), currentContent);
|
37
|
+
|
38
|
+
editor.setContent(newContent);
|
39
|
+
|
40
|
+
var selects = ['selectionStart', 'selectionEnd'].map(function (attr) {
|
41
|
+
return TextPatcher.transformCursor(oldCursor[attr], op);
|
42
|
+
});
|
43
|
+
|
44
|
+
if(selects[0] === selects[1]) {
|
45
|
+
App.editor.setCursor(posToCursor(selects[0], newContent));
|
46
|
+
}
|
47
|
+
else {
|
48
|
+
App.editor.setSelection(posToCursor(selects[0], newContent), posToCursor(selects[1], newContent));
|
49
|
+
}
|
50
|
+
});
|
51
|
+
|
52
|
+
_chainpad.onMessage(function(message, cb){
|
53
|
+
var success = App.socket.channel.post(message);
|
54
|
+
setTimeout(function () {
|
55
|
+
if(!success) {
|
56
|
+
console.log("Message not successful");
|
57
|
+
}
|
58
|
+
cb();
|
59
|
+
}, 1);
|
60
|
+
})
|
61
|
+
|
62
|
+
_chainpad.start();
|
63
|
+
}
|
64
|
+
|
65
|
+
function getChainpad() {
|
66
|
+
return _chainpad;
|
67
|
+
}
|
68
|
+
|
69
|
+
|
70
|
+
App.textEditorDidMakeChanges = function(text) {
|
71
|
+
if(patchText) {
|
72
|
+
patchText(text);
|
73
|
+
}
|
74
|
+
}
|
75
|
+
|
76
|
+
function posToCursor(position, newText) {
|
77
|
+
var cursor = {
|
78
|
+
line: 0,
|
79
|
+
ch: 0
|
80
|
+
};
|
81
|
+
var textLines = newText.substr(0, position).split("\n");
|
82
|
+
cursor.line = textLines.length - 1;
|
83
|
+
cursor.ch = textLines[cursor.line].length;
|
84
|
+
return cursor;
|
85
|
+
}
|
86
|
+
|
87
|
+
function cursorToPos(cursor, oldText) {
|
88
|
+
var cLine = cursor.line;
|
89
|
+
var cCh = cursor.ch;
|
90
|
+
var pos = 0;
|
91
|
+
var textLines = oldText.split("\n");
|
92
|
+
for (var line = 0; line <= cLine; line++) {
|
93
|
+
if(line < cLine) {
|
94
|
+
pos += textLines[line].length+1;
|
95
|
+
}
|
96
|
+
else if(line === cLine) {
|
97
|
+
pos += cCh;
|
98
|
+
}
|
99
|
+
}
|
100
|
+
return pos;
|
101
|
+
};
|
102
|
+
|
12
103
|
App.socket = {};
|
13
104
|
App.socket.cable = ActionCable.createConsumer("/collab/cable");
|
14
105
|
|
106
|
+
var ignoreNextMessage = false;
|
107
|
+
|
15
108
|
App.socket.subscribeToDoc = function(docId, callback) {
|
16
109
|
App.socket.channel = App.socket.cable.subscriptions.create({channel: "EditChannel", doc_id: docId, client_id: clientId}, {
|
17
110
|
connected: function() {
|
18
|
-
|
19
|
-
|
20
|
-
},
|
111
|
+
App.socket.channel.retrieve();
|
112
|
+
}.bind(this),
|
21
113
|
|
22
114
|
disconnected: function() {
|
23
|
-
// Called when the subscription has been terminated by the server
|
24
115
|
},
|
25
116
|
|
26
117
|
received: function(data) {
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
118
|
+
if(data.client_id == clientId && !data.initial_retrieve) {
|
119
|
+
return;
|
120
|
+
}
|
121
|
+
|
122
|
+
var patches = [];
|
123
|
+
if(data.initial_retrieve) {
|
124
|
+
initChainpad();
|
125
|
+
patches = data.patches;
|
126
|
+
} else if(data.patch) {
|
127
|
+
patches = [data.patch];
|
31
128
|
}
|
129
|
+
|
130
|
+
patches = patches.map(function(patch){
|
131
|
+
return App.crypto.decrypt(patch.content, App.encryptionKey(), patch.iv, patch.auth, App.authKey());
|
132
|
+
})
|
133
|
+
|
134
|
+
patches.forEach(function(patch){
|
135
|
+
getChainpad().message(patch);
|
136
|
+
})
|
32
137
|
},
|
33
138
|
|
34
139
|
retrieve: function() {
|
35
140
|
return this.perform('retrieve', {doc_id: docId, client_id: clientId});
|
36
141
|
},
|
37
142
|
|
38
|
-
post: function(
|
39
|
-
|
40
|
-
|
143
|
+
post: function(patch) {
|
144
|
+
if(ignoreNextMessage) {
|
145
|
+
ignoreNextMessage = false;
|
146
|
+
return;
|
147
|
+
}
|
148
|
+
|
149
|
+
var result = App.crypto.encrypt(patch, App.encryptionKey(), App.authKey());
|
150
|
+
var data = {content: result.cipher, iv: result.iv, auth: result.auth, edit_token: App.editToken, doc_id: docId, client_id: clientId};
|
41
151
|
return this.perform('post', data);
|
42
152
|
}
|
43
153
|
});
|
@@ -3,61 +3,117 @@
|
|
3
3
|
|
4
4
|
document.addEventListener("DOMContentLoaded", function(event) {
|
5
5
|
|
6
|
-
var editor
|
6
|
+
var textarea, editor, isSubscribedToDoc;
|
7
|
+
var isInSN = window.parent != window;
|
8
|
+
|
9
|
+
function getEditorValue() {
|
10
|
+
return editor.getDoc().getValue() || "";
|
11
|
+
}
|
12
|
+
|
13
|
+
function configureEditor() {
|
14
|
+
textarea = document.getElementById("editor");
|
15
|
+
|
16
|
+
editor = App.editor = CodeMirror.fromTextArea(textarea, {
|
17
|
+
mode: "text/html",
|
18
|
+
lineNumbers: true,
|
19
|
+
lineWrapping: true,
|
20
|
+
mode: "markdown"
|
21
|
+
});
|
22
|
+
|
23
|
+
editor.on("change", function(cm, change){
|
24
|
+
if(isSubscribedToDoc) {
|
25
|
+
App.textEditorDidMakeChanges(getEditorValue());
|
26
|
+
}
|
27
|
+
sendDocToSN()
|
28
|
+
})
|
29
|
+
}
|
30
|
+
|
31
|
+
App.registerChainpadObserver({
|
32
|
+
getContent: function() {
|
33
|
+
return getEditorValue();
|
34
|
+
},
|
35
|
+
|
36
|
+
setContent: function(content) {
|
37
|
+
editor.getDoc().setValue(content);
|
38
|
+
}
|
39
|
+
})
|
7
40
|
|
8
41
|
function createNewDocument() {
|
9
42
|
window.location.href = "/collab/doc/new";
|
10
43
|
}
|
11
44
|
|
12
45
|
function subscribeToDocId(docId) {
|
13
|
-
|
14
|
-
|
15
|
-
|
46
|
+
configureEditor();
|
47
|
+
App.socket.subscribeToDoc(docId, function(message){})
|
48
|
+
isSubscribedToDoc = true;
|
49
|
+
refreshKey();
|
50
|
+
|
51
|
+
var incomingText = sessionStorage.getItem("sn_text");
|
52
|
+
if(incomingText) {
|
53
|
+
editor.getDoc().setValue(incomingText);
|
54
|
+
sessionStorage.removeItem("sn_text");
|
55
|
+
}
|
16
56
|
}
|
17
57
|
|
18
58
|
var location = window.location.href;
|
19
59
|
var uuidResults = location.match(/[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}/i);
|
20
60
|
var uuid = uuidResults ? uuidResults[0] : null;
|
21
61
|
var hasDocument = uuid != null;
|
22
|
-
var noteId = sessionStorage.getItem("
|
62
|
+
var noteId = sessionStorage.getItem("sn_lastNoteId");
|
23
63
|
|
24
64
|
var key, didGenerateKey;
|
25
|
-
if(location.indexOf("#key=") != -1) {
|
26
|
-
key = location.split("#key=").slice(-1)[0];
|
27
|
-
} else {
|
28
|
-
key = App.crypto.generateRandomKey(32);
|
29
|
-
didGenerateKey = true;
|
30
|
-
}
|
31
|
-
setKey(key);
|
32
65
|
|
33
|
-
|
34
|
-
if(
|
35
|
-
|
36
|
-
window.parent.postMessage({text: buildParamsString({id: uuid, key: key}), id: noteId}, '*');
|
37
|
-
subscribeToDocId(uuid);
|
66
|
+
function refreshKey() {
|
67
|
+
if(location.indexOf("#key=") != -1) {
|
68
|
+
key = location.split("#key=").slice(-1)[0];
|
38
69
|
} else {
|
39
|
-
|
40
|
-
|
41
|
-
} else {
|
42
|
-
if(!hasDocument) {
|
43
|
-
createNewDocument();
|
44
|
-
} else {
|
45
|
-
subscribeToDocId(uuid);
|
70
|
+
key = App.crypto.generateRandomKey(32);
|
71
|
+
didGenerateKey = true;
|
46
72
|
}
|
73
|
+
setKey(key);
|
47
74
|
}
|
48
75
|
|
49
76
|
function setKey(key) {
|
50
77
|
App.key = key;
|
51
|
-
var span = document.getElementById("url-key");
|
52
|
-
if(span) {
|
53
|
-
span.textContent = "#key=" + key;
|
54
|
-
}
|
55
78
|
|
56
79
|
if(window.location.href.indexOf("#key") == -1) {
|
57
80
|
window.history.pushState('Document', 'Document', "#key=" + key);
|
58
81
|
}
|
82
|
+
|
83
|
+
if(textarea) {
|
84
|
+
var url = window.location.href;
|
85
|
+
var etString = "?et=";
|
86
|
+
var etIndex = url.indexOf(etString);
|
87
|
+
var editingUrl, viewingUrl, editToken;
|
88
|
+
if(etIndex != -1) {
|
89
|
+
// has edit token
|
90
|
+
var editToken = App.editToken = url.substring(etIndex + etString.length, url.indexOf("#"));
|
91
|
+
editingUrl = url;
|
92
|
+
viewingUrl = url.replace(etString + editToken, "");
|
93
|
+
} else {
|
94
|
+
// no edit token
|
95
|
+
viewingUrl = url;
|
96
|
+
}
|
97
|
+
|
98
|
+
var editingElement = document.getElementById("editing-url");
|
99
|
+
if(editingUrl) {
|
100
|
+
editingElement.innerHTML = editingUrl;
|
101
|
+
editingElement.href = editingUrl;
|
102
|
+
sendDocToSN();
|
103
|
+
} else {
|
104
|
+
var editingWrapper = document.getElementById("editing-url-wrapper");
|
105
|
+
editingWrapper.parentNode.removeChild(editingWrapper);
|
106
|
+
}
|
107
|
+
|
108
|
+
var viewingElement = document.getElementById("viewing-url");
|
109
|
+
viewingElement.innerHTML = viewingUrl;
|
110
|
+
viewingElement.href = viewingUrl;
|
111
|
+
}
|
59
112
|
}
|
60
113
|
|
114
|
+
|
115
|
+
// ** Communication with Standard Notes App **
|
116
|
+
|
61
117
|
function buildParamsString(doc) {
|
62
118
|
var string = "";
|
63
119
|
for(var key in doc) {
|
@@ -67,34 +123,68 @@ document.addEventListener("DOMContentLoaded", function(event) {
|
|
67
123
|
return string;
|
68
124
|
}
|
69
125
|
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
126
|
+
function sendDocToSN() {
|
127
|
+
if(!isInSN) {
|
128
|
+
return;
|
129
|
+
}
|
74
130
|
|
75
|
-
var
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
params[key] = value;
|
83
|
-
})
|
131
|
+
var noteBody = buildParamsString({url: window.location.href});
|
132
|
+
if(editor) {
|
133
|
+
var disclaimer = "// text you enter below will not transfer to the live editor.\n// the below is just a backup for your records";
|
134
|
+
noteBody += "\n\n" + disclaimer + "\n\n" + getEditorValue();
|
135
|
+
}
|
136
|
+
window.parent.postMessage({text: noteBody, id: noteId}, '*');
|
137
|
+
}
|
84
138
|
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
139
|
+
if(isInSN) {
|
140
|
+
if(hasDocument) {
|
141
|
+
// inform parent of new document
|
142
|
+
sendDocToSN();
|
143
|
+
subscribeToDocId(uuid);
|
89
144
|
} else {
|
90
|
-
|
145
|
+
window.parent.postMessage({status: "ready"}, '*');
|
91
146
|
}
|
92
|
-
|
147
|
+
window.addEventListener("message", function(event){
|
148
|
+
var text = event.data.text || "";
|
149
|
+
sessionStorage.setItem("sn_lastNoteId", event.data.id);
|
93
150
|
|
94
|
-
|
95
|
-
|
96
|
-
var
|
97
|
-
|
98
|
-
|
151
|
+
var splitTarget = "%%Do not modify above this line%%";
|
152
|
+
var comps = text.split(splitTarget);
|
153
|
+
var snText;
|
154
|
+
var hasParams = text.indexOf(splitTarget) != -1;
|
155
|
+
if(hasParams) {
|
156
|
+
snText = comps[1];
|
157
|
+
} else {
|
158
|
+
snText = text;
|
159
|
+
sessionStorage.setItem("sn_text", snText)
|
160
|
+
}
|
161
|
+
|
162
|
+
|
163
|
+
var paramString = comps[0];
|
164
|
+
var params = {};
|
165
|
+
var lines = paramString.split("\n");
|
166
|
+
lines.forEach(function(line){
|
167
|
+
var comps = line.split(": ");
|
168
|
+
var key = comps[0];
|
169
|
+
var value = comps[1];
|
170
|
+
params[key] = value;
|
171
|
+
})
|
172
|
+
|
173
|
+
let url = params["url"];
|
174
|
+
if (url) {
|
175
|
+
window.location.href = url;
|
176
|
+
} else {
|
177
|
+
createNewDocument();
|
178
|
+
}
|
179
|
+
}, false);
|
99
180
|
}
|
181
|
+
|
182
|
+
if(!isInSN){
|
183
|
+
if(!hasDocument) {
|
184
|
+
createNewDocument();
|
185
|
+
} else {
|
186
|
+
subscribeToDocId(uuid);
|
187
|
+
}
|
188
|
+
}
|
189
|
+
|
100
190
|
})
|
@@ -29,22 +29,49 @@ html, body {
|
|
29
29
|
flex-direction: column;
|
30
30
|
}
|
31
31
|
|
32
|
-
|
32
|
+
.CodeMirror {
|
33
33
|
flex: 1;
|
34
34
|
width: 100%;
|
35
35
|
height: 100%;
|
36
36
|
resize: none;
|
37
|
-
font-size:
|
38
|
-
border: 1px solid rgb(226, 226, 226);
|
37
|
+
font-size: 16px;
|
39
38
|
border-radius: 4px;
|
40
39
|
padding: 15px;
|
41
40
|
margin-top: 10px;
|
42
41
|
font-family: monospace;
|
42
|
+
margin-top: 10px;
|
43
|
+
padding: 0;
|
43
44
|
}
|
44
45
|
|
45
|
-
|
46
|
+
.url-wrapper {
|
46
47
|
word-wrap: break-word;
|
47
48
|
word-break: break-all;
|
49
|
+
font-size: 10px;
|
50
|
+
margin-bottom: 3px;
|
51
|
+
|
52
|
+
opacity: 0.3;
|
53
|
+
|
54
|
+
&:hover {
|
55
|
+
opacity: 1.0;
|
56
|
+
}
|
57
|
+
|
58
|
+
.label {
|
59
|
+
font-weight: bold;
|
60
|
+
}
|
61
|
+
|
62
|
+
.url {
|
63
|
+
font-weight: bold;
|
64
|
+
color: rgba(black, 0.5);
|
65
|
+
font-size: 10px;
|
66
|
+
text-decoration: none;
|
67
|
+
|
68
|
+
|
69
|
+
&:visited { text-decoration: none; color:black; }
|
70
|
+
&:hover {
|
71
|
+
text-decoration: underline;
|
72
|
+
color: black;
|
73
|
+
}
|
74
|
+
}
|
48
75
|
}
|
49
76
|
|
50
77
|
* {
|