18 changed files with 525 additions and 54 deletions
Split View
Diff Options
-
2.bowerrc
-
2.gitignore
-
3bower.json
-
6server.js
-
25www/common/chainpad.js
-
0www/common/messages.js
-
0www/common/otaml.js
-
172www/common/toolbar.js
-
45www/index.html
-
8www/pad/errorbox.js
-
4www/pad/html-patcher.js
-
11www/pad/index.html
-
17www/pad/main.js
-
0www/pad/rangy.js
-
18www/pad/realtime-wysiwyg.js
-
29www/sheet/index.html
-
36www/sheet/inner.html
-
201www/sheet/main.js
@ -1,3 +1,3 @@ |
|||
{ |
|||
"directory" : "www/bower" |
|||
"directory" : "www/bower_components" |
|||
} |
|||
@ -1,3 +1,3 @@ |
|||
www/bower/* |
|||
www/bower_components/* |
|||
node_modules |
|||
/config.js |
|||
@ -0,0 +1,172 @@ |
|||
var Toolbar = function ($, container, Messages, myUserName, realtime) { |
|||
|
|||
/** Id of the element for getting debug info. */ |
|||
var DEBUG_LINK_CLS = 'rtwysiwyg-debug-link'; |
|||
|
|||
/** Id of the div containing the user list. */ |
|||
var USER_LIST_CLS = 'rtwysiwyg-user-list'; |
|||
|
|||
/** Id of the div containing the lag info. */ |
|||
var LAG_ELEM_CLS = 'rtwysiwyg-lag'; |
|||
|
|||
/** The toolbar class which contains the user list, debug link and lag. */ |
|||
var TOOLBAR_CLS = 'rtwysiwyg-toolbar'; |
|||
|
|||
/** Key in the localStore which indicates realtime activity should be disallowed. */ |
|||
var LOCALSTORAGE_DISALLOW = 'rtwysiwyg-disallow'; |
|||
|
|||
var SPINNER_DISAPPEAR_TIME = 3000; |
|||
var SPINNER = [ '-', '\\', '|', '/' ]; |
|||
|
|||
var uid = function () { |
|||
return 'rtwysiwyg-uid-' + String(Math.random()).substring(2); |
|||
}; |
|||
|
|||
var createRealtimeToolbar = function (container) { |
|||
var id = uid(); |
|||
$(container).prepend( |
|||
'<div class="' + TOOLBAR_CLS + '" id="' + id + '">' + |
|||
'<div class="rtwysiwyg-toolbar-leftside"></div>' + |
|||
'<div class="rtwysiwyg-toolbar-rightside"></div>' + |
|||
'</div>' |
|||
); |
|||
var toolbar = $('#'+id); |
|||
toolbar.append([ |
|||
'<style>', |
|||
'.' + TOOLBAR_CLS + ' {', |
|||
' color: #666;', |
|||
' font-weight: bold;', |
|||
// ' background-color: #f0f0ee;',
|
|||
// ' border-bottom: 1px solid #DDD;',
|
|||
// ' border-top: 3px solid #CCC;',
|
|||
// ' border-right: 2px solid #CCC;',
|
|||
// ' border-left: 2px solid #CCC;',
|
|||
' height: 26px;', |
|||
' margin-bottom: -3px;', |
|||
' display: inline-block;', |
|||
' width: 100%;', |
|||
'}', |
|||
'.' + TOOLBAR_CLS + ' div {', |
|||
' padding: 0 10px;', |
|||
' height: 1.5em;', |
|||
// ' background: #f0f0ee;',
|
|||
' line-height: 25px;', |
|||
' height: 22px;', |
|||
'}', |
|||
'.rtwysiwyg-toolbar-leftside {', |
|||
' float: left;', |
|||
'}', |
|||
'.rtwysiwyg-toolbar-rightside {', |
|||
' float: right;', |
|||
'}', |
|||
'.rtwysiwyg-lag {', |
|||
' float: right;', |
|||
'}', |
|||
'.rtwysiwyg-spinner {', |
|||
' float: left;', |
|||
'}', |
|||
'.gwt-TabBar {', |
|||
' display:none;', |
|||
'}', |
|||
'.' + DEBUG_LINK_CLS + ':link { color:transparent; }', |
|||
'.' + DEBUG_LINK_CLS + ':link:hover { color:blue; }', |
|||
'.gwt-TabPanelBottom { border-top: 0 none; }', |
|||
|
|||
'</style>' |
|||
].join('\n')); |
|||
return toolbar; |
|||
}; |
|||
|
|||
var createSpinner = function (container) { |
|||
var id = uid(); |
|||
$(container).append('<div class="rtwysiwyg-spinner" id="'+id+'"></div>'); |
|||
return $('#'+id)[0]; |
|||
}; |
|||
|
|||
var kickSpinner = function (spinnerElement, reversed) { |
|||
var txt = spinnerElement.textContent || '-'; |
|||
var inc = (reversed) ? -1 : 1; |
|||
spinnerElement.textContent = SPINNER[(SPINNER.indexOf(txt) + inc) % SPINNER.length]; |
|||
spinnerElement.timeout && clearTimeout(spinnerElement.timeout); |
|||
spinnerElement.timeout = setTimeout(function () { |
|||
spinnerElement.textContent = ''; |
|||
}, SPINNER_DISAPPEAR_TIME); |
|||
}; |
|||
|
|||
var createUserList = function (container) { |
|||
var id = uid(); |
|||
$(container).prepend('<div class="' + USER_LIST_CLS + '" id="'+id+'"></div>'); |
|||
return $('#'+id)[0]; |
|||
}; |
|||
|
|||
var updateUserList = function (myUserName, listElement, userList) { |
|||
var meIdx = userList.indexOf(myUserName); |
|||
if (meIdx === -1) { |
|||
listElement.textContent = Messages.synchronizing; |
|||
return; |
|||
} |
|||
if (userList.length === 1) { |
|||
listElement.textContent = Messages.editingAlone; |
|||
} else if (userList.length === 2) { |
|||
listElement.textContent = Messages.editingWithOneOtherPerson; |
|||
} else { |
|||
listElement.textContent = Messages.editingWith + ' ' + (userList.length - 1) + ' ' |
|||
Messages.otherPeople; |
|||
} |
|||
}; |
|||
|
|||
var createLagElement = function (container) { |
|||
var id = uid(); |
|||
$(container).append('<div class="' + LAG_ELEM_CLS + '" id="'+id+'"></div>'); |
|||
return $('#'+id)[0]; |
|||
}; |
|||
|
|||
var checkLag = function (realtime, lagElement) { |
|||
var lag = realtime.getLag(); |
|||
var lagSec = lag.lag/1000; |
|||
var lagMsg = Messages.lag + ' '; |
|||
if (lag.waiting && lagSec > 1) { |
|||
lagMsg += "?? " + Math.floor(lagSec); |
|||
} else { |
|||
lagMsg += lagSec; |
|||
} |
|||
lagElement.textContent = lagMsg; |
|||
}; |
|||
|
|||
|
|||
var toolbar = createRealtimeToolbar(container); |
|||
var userListElement = createUserList(toolbar.find('.rtwysiwyg-toolbar-leftside')); |
|||
var spinner = createSpinner(toolbar.find('.rtwysiwyg-toolbar-rightside')); |
|||
var lagElement = createLagElement(toolbar.find('.rtwysiwyg-toolbar-rightside')); |
|||
|
|||
var connected = false; |
|||
|
|||
realtime.onUserListChange(function (userList) { |
|||
if (userList.indexOf(myUserName) !== -1) { connected = true; } |
|||
if (!connected) { return; } |
|||
updateUserList(myUserName, userListElement, userList); |
|||
}); |
|||
|
|||
var ks = function () { |
|||
if (connected) { kickSpinner(spinner, false); } |
|||
}; |
|||
|
|||
realtime.onPatch(ks); |
|||
realtime.onMessage(ks); |
|||
|
|||
setInterval(function () { |
|||
if (!connected) { return; } |
|||
checkLag(realtime, lagElement); |
|||
}, 3000); |
|||
|
|||
return { |
|||
reconnecting: function () { |
|||
connected = false; |
|||
userListElement.textContent = Messages.reconnecting; |
|||
lagElement.textContent = ''; |
|||
}, |
|||
connected: function () { |
|||
connected = true; |
|||
} |
|||
}; |
|||
}; |
|||
@ -0,0 +1,11 @@ |
|||
<!DOCTYPE html> |
|||
<html> |
|||
<head> |
|||
<meta content="text/html; charset=utf-8" http-equiv="content-type"/> |
|||
<script data-main="main" src="/bower_components/requirejs/require.js"></script> |
|||
</head> |
|||
<body> |
|||
<textarea style="display:none" cols="80" id="editor1" name="editor1" rows="10"></textarea> |
|||
</body> |
|||
</html> |
|||
|
|||
@ -0,0 +1,29 @@ |
|||
<!DOCTYPE html> |
|||
<html> |
|||
<head> |
|||
<meta content="text/html; charset=utf-8" http-equiv="content-type"/> |
|||
<script data-main="main" src="/bower_components/requirejs/require.js"></script> |
|||
<style> |
|||
iframe { |
|||
position:fixed; |
|||
top:0px; |
|||
left:0px; |
|||
bottom:0px; |
|||
right:0px; |
|||
width:100%; |
|||
height:100%; |
|||
border:none; |
|||
margin:0; |
|||
padding:0; |
|||
overflow:hidden; |
|||
} |
|||
textarea { |
|||
display:none; |
|||
} |
|||
</style> |
|||
</head> |
|||
<body> |
|||
<iframe src="inner.html"></iframe> |
|||
<textarea id="sheet-json"></textarea> |
|||
</body> |
|||
</html> |
|||
@ -0,0 +1,36 @@ |
|||
<!DOCTYPE html> |
|||
<html> |
|||
<head> |
|||
<meta http-equiv="X-UA-Compatible" content="IE=Edge"/> |
|||
<meta http-equiv="Pragma" content="no-cache"> |
|||
<meta http-equiv="Expires" content="-1"> |
|||
<meta content="text/html;charset=utf-8" http-equiv="Content-Type"> |
|||
<meta content="utf-8" http-equiv="encoding"> |
|||
<title>jQuery.sheet - The Ajax Spreadsheet DTS Lazy Loading</title> |
|||
<script src="/bower_components/jquery-legacy/dist/jquery.js"></script> |
|||
<script src="/bower_components/jquery.sheet/parser/formula/formula.js"></script> |
|||
<script src="/bower_components/jquery.sheet/jquery.sheet.js"></script> |
|||
<script> |
|||
var numColumns = 10; |
|||
var numRows = 20; |
|||
var spreadsheetJson = [ { "title": "", "rows": [ ] } ]; |
|||
for (var i = 0; i < numRows; i++) { |
|||
var columns = []; |
|||
spreadsheetJson[0].rows.push({ columns: columns }); |
|||
for (var j = 0; j < numColumns; j++) { columns[j] = { "value": "" }; } |
|||
} |
|||
|
|||
$.sheet.preLoad('/bower_components/jquery.sheet/'); |
|||
$(function () { |
|||
window.sheetLdr = new Sheet.JSONLoader( spreadsheetJson ); |
|||
window.sh = $('#sheet-json').height($(window).height()).width($(window).width()).sheet({ |
|||
loader: window.sheetLdr, |
|||
minSize: { rows:numRows, cols:numColumns } |
|||
}); |
|||
}); |
|||
</script> |
|||
</head> |
|||
<body> |
|||
<div id="sheet-json" title="CryptSheet"></div> |
|||
</body> |
|||
</html> |
|||
@ -0,0 +1,201 @@ |
|||
define([ |
|||
'/api/config?cb=' + Math.random().toString(16).substring(2), |
|||
'/common/messages.js', |
|||
'/common/toolbar.js', |
|||
'/common/chainpad.js', |
|||
'/bower_components/jquery/dist/jquery.min.js', |
|||
'/bower_components/tweetnacl/nacl-fast.min.js', |
|||
'/common/otaml.js' |
|||
], function (Config, Messages) { |
|||
var Nacl = window.nacl; |
|||
var $ = jQuery; |
|||
var ChainPad = window.ChainPad; |
|||
var Otaml = window.Otaml; |
|||
var Toolbar = window.Toolbar; |
|||
|
|||
var module = { exports: {} }; |
|||
|
|||
var parseKey = function (str) { |
|||
var array = Nacl.util.decodeBase64(str); |
|||
var hash = Nacl.hash(array); |
|||
return { lookupKey: hash.subarray(32), cryptKey: hash.subarray(0,32) }; |
|||
}; |
|||
|
|||
var genKey = function () { |
|||
return Nacl.util.encodeBase64(Nacl.randomBytes(18)); |
|||
}; |
|||
|
|||
var userName = function () { |
|||
return 'Other-' + Nacl.util.encodeBase64(Nacl.randomBytes(8)); |
|||
}; |
|||
|
|||
var sheetToJson = function (ifrWindow) { |
|||
var xx = ifrWindow.sh[0].jS |
|||
var m = []; |
|||
for (var i = 0; i < xx.spreadsheets.length; i++) { |
|||
m[i]=[]; |
|||
var sheet = xx.spreadsheets[i]; |
|||
for (var j = 1; j < sheet.length; j++) { |
|||
m[i][j]=[]; |
|||
var row = sheet[j]; |
|||
for (var k = 1; k < row.length; k++) { |
|||
var col = row[k]; |
|||
m[i][j][k] = { value: col.value, formula: col.formula }; |
|||
} |
|||
} |
|||
} |
|||
return m; |
|||
}; |
|||
|
|||
var jsonToSheet = function (ifrWindow, json) { |
|||
var xx = ifrWindow.sh[0].jS; |
|||
for (var i = 0; i < xx.spreadsheets.length; i++) { |
|||
var sheet = xx.spreadsheets[i]; |
|||
for (var j = 1; j < sheet.length; j++) { |
|||
var row = sheet[j]; |
|||
for (var k = 1; k < row.length; k++) { |
|||
var col = row[k]; |
|||
var jcol = json[i][j][k]; |
|||
if (jcol.value === col.value && jcol.formula === col.formula) { continue; } |
|||
col.value = jcol.value; |
|||
col.formula = jcol.formula; |
|||
col.displayValue(); |
|||
} |
|||
} |
|||
} |
|||
}; |
|||
|
|||
var encryptStr = function (str, key) { |
|||
var array = Nacl.util.decodeUTF8(str); |
|||
var nonce = Nacl.randomBytes(24); |
|||
var packed = Nacl.secretbox(array, nonce, key); |
|||
if (!packed) { throw new Error(); } |
|||
return Nacl.util.encodeBase64(nonce) + "|" + Nacl.util.encodeBase64(packed); |
|||
}; |
|||
var decryptStr = function (str, key) { |
|||
var arr = str.split('|'); |
|||
if (arr.length !== 2) { throw new Error(); } |
|||
var nonce = Nacl.util.decodeBase64(arr[0]); |
|||
var packed = Nacl.util.decodeBase64(arr[1]); |
|||
var unpacked = Nacl.secretbox.open(packed, nonce, key); |
|||
if (!unpacked) { throw new Error(); } |
|||
return Nacl.util.encodeUTF8(unpacked); |
|||
}; |
|||
|
|||
// this is crap because of bencoding messages... it should go away....
|
|||
var splitMessage = function (msg, sending) { |
|||
var idx = 0; |
|||
var nl; |
|||
for (var i = ((sending) ? 0 : 1); i < 3; i++) { |
|||
nl = msg.indexOf(':',idx); |
|||
idx = nl + Number(msg.substring(idx,nl)) + 1; |
|||
} |
|||
return [ msg.substring(0,idx), msg.substring(msg.indexOf(':',idx) + 1) ]; |
|||
}; |
|||
|
|||
var encrypt = function (msg, key) { |
|||
var spl = splitMessage(msg, true); |
|||
var json = JSON.parse(spl[1]); |
|||
// non-patches are not encrypted.
|
|||
if (json[0] !== 2) { return msg; } |
|||
json[1] = encryptStr(JSON.stringify(json[1]), key); |
|||
var res = JSON.stringify(json); |
|||
return spl[0] + res.length + ':' + res; |
|||
}; |
|||
|
|||
var decrypt = function (msg, key) { |
|||
var spl = splitMessage(msg, false); |
|||
var json = JSON.parse(spl[1]); |
|||
// non-patches are not encrypted.
|
|||
if (json[0] !== 2) { return msg; } |
|||
if (typeof(json[1]) !== 'string') { throw new Error(); } |
|||
json[1] = JSON.parse(decryptStr(json[1], key)); |
|||
var res = JSON.stringify(json); |
|||
return spl[0] + res.length + ':' + res; |
|||
}; |
|||
|
|||
var applyChange = function(ctx, oldval, newval) { |
|||
if (oldval === newval) return; |
|||
|
|||
var commonStart = 0; |
|||
while (oldval.charAt(commonStart) === newval.charAt(commonStart)) { |
|||
commonStart++; |
|||
} |
|||
|
|||
var commonEnd = 0; |
|||
while (oldval.charAt(oldval.length - 1 - commonEnd) === newval.charAt(newval.length - 1 - commonEnd) && |
|||
commonEnd + commonStart < oldval.length && commonEnd + commonStart < newval.length) { |
|||
commonEnd++; |
|||
} |
|||
|
|||
if (oldval.length !== commonStart + commonEnd) { |
|||
ctx.remove(commonStart, oldval.length - commonStart - commonEnd); |
|||
} |
|||
if (newval.length !== commonStart + commonEnd) { |
|||
ctx.insert(commonStart, newval.slice(commonStart, newval.length - commonEnd)); |
|||
} |
|||
}; |
|||
|
|||
$(function () { |
|||
|
|||
if (window.location.href.indexOf('#') === -1) { |
|||
window.location.href = window.location.href + '#' + genKey(); |
|||
} |
|||
$(window).on('hashchange', function() { |
|||
window.location.reload(); |
|||
}); |
|||
|
|||
var $sheetJson = $('#sheet-json'); |
|||
var ifrw = $('iframe')[0].contentWindow; |
|||
var sheetEvent = function (realtime) { |
|||
var sheetJson = JSON.stringify(sheetToJson(ifrw)); |
|||
applyChange(realtime, realtime.getUserDoc(), sheetJson); |
|||
$sheetJson.text(sheetJson); |
|||
}; |
|||
|
|||
var eventPending = false; |
|||
var realtimeEvent = function (realtime) { |
|||
if (eventPending) { return; } |
|||
eventPending = true; |
|||
setTimeout(function () { |
|||
eventPending = false; |
|||
try{ |
|||
var data = window.data = realtime.getUserDoc(); |
|||
$sheetJson.text(data); |
|||
var json = JSON.parse(data); |
|||
jsonToSheet(ifrw, json); |
|||
}catch(e) { console.log(e.stack); } |
|||
}, 0); |
|||
}; |
|||
|
|||
var key = parseKey(window.location.hash.substring(1)); |
|||
var channel = Nacl.util.encodeBase64(key.lookupKey).substring(0,10); |
|||
var myUserName = userName(); |
|||
|
|||
var socket = new WebSocket(Config.websocketURL); |
|||
socket.onopen = function () { |
|||
var realtime = ChainPad.create( |
|||
myUserName, 'x', channel, '', { transformFunction: Otaml.transform }); |
|||
socket.onmessage = function (evt) { |
|||
var message = decrypt(evt.data, key.cryptKey); |
|||
realtime.message(message); |
|||
}; |
|||
realtime.onMessage(function (message) { |
|||
message = encrypt(message, key.cryptKey); |
|||
try { |
|||
socket.send(message); |
|||
} catch (e) { |
|||
console.log(e.stack); |
|||
} |
|||
}); |
|||
ifrw.sh.on('sheetCellEdited', function () { sheetEvent(realtime); }); |
|||
sheetEvent(realtime); |
|||
realtime.onPatch(function () { realtimeEvent(realtime); }); |
|||
|
|||
ifrw.$('.jSTitle').html(''); |
|||
Toolbar(ifrw.$, ifrw.$('.jSTitle'), Messages, myUserName, realtime); |
|||
|
|||
realtime.start(); |
|||
}; |
|||
}); |
|||
}); |
|||
Write
Preview
Loading…
Cancel
Save