rename /_socket/ app to /p/

This commit is contained in:
ansuz
2016-04-13 16:01:45 +02:00
parent a1fe941f69
commit c867ab04ab
4 changed files with 0 additions and 0 deletions

30
www/p/index.html Normal file
View File

@@ -0,0 +1,30 @@
<!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>
html, body {
margin: 0px;
padding: 0px;
}
#pad-iframe {
position:fixed;
top:0px;
left:0px;
bottom:0px;
right:0px;
width:100%;
height:100%;
border:none;
margin:0;
padding:0;
overflow:hidden;
}
</style>
</head>
<body>
<iframe id="pad-iframe" src="inner.html"></iframe>
</body>
</html>

12
www/p/inner.html Normal file
View File

@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html>
<head>
<meta content="text/html; charset=utf-8" http-equiv="content-type"/>
<script src="/bower_components/jquery/dist/jquery.min.js"></script>
<script src="/bower_components/ckeditor/ckeditor.js"></script>
</head>
<body>
<textarea style="display:none" id="editor1" name="editor1"></textarea>
</body>
</html>

320
www/p/main.js Normal file
View File

@@ -0,0 +1,320 @@
define([
'/api/config?cb=' + Math.random().toString(16).substring(2),
'/common/messages.js',
'/common/crypto.js',
'/common/RealtimeTextSocket.js',
'/common/hyperjson.js',
'/common/hyperscript.js',
'/_socket/toolbar.js',
'/common/cursor.js',
'/common/json-ot.js',
'/common/TypingTests.js',
'/bower_components/diff-dom/diffDOM.js',
'/bower_components/jquery/dist/jquery.min.js',
'/customize/pad.js'
], function (Config, Messages, Crypto, realtimeInput, Hyperjson, Hyperscript, Toolbar, Cursor, JsonOT, TypingTest) {
var $ = window.jQuery;
var ifrw = $('#pad-iframe')[0].contentWindow;
var Ckeditor; // to be initialized later...
var DiffDom = window.diffDOM;
window.Hyperjson = Hyperjson;
var hjsonToDom = function (H) {
return Hyperjson.callOn(H, Hyperscript);
};
var userName = Crypto.rand64(8),
toolbar;
var module = window.REALTIME_MODULE = {
Hyperjson: Hyperjson,
Hyperscript: Hyperscript
};
var isNotMagicLine = function (el) {
// factor as:
// return !(el.tagName === 'SPAN' && el.contentEditable === 'false');
var filter = (el.tagName === 'SPAN' &&
el.getAttribute('contentEditable') === 'false');
if (filter) {
console.log("[hyperjson.serializer] prevented an element" +
"from being serialized:", el);
return false;
}
return true;
};
/* catch `type="_moz"` before it goes over the wire */
var brFilter = function (hj) {
if (hj[1].type === '_moz') { hj[1].type = undefined; }
return hj;
};
var stringifyDOM = function (dom) {
return JSON.stringify(Hyperjson.fromDOM(dom, isNotMagicLine, brFilter));
};
var andThen = function (Ckeditor) {
$(window).on('hashchange', function() {
window.location.reload();
});
if (window.location.href.indexOf('#') === -1) {
window.location.href = window.location.href + '#' + Crypto.genKey();
return;
}
var fixThings = false;
var key = Crypto.parseKey(window.location.hash.substring(1));
var editor = window.editor = Ckeditor.replace('editor1', {
// https://dev.ckeditor.com/ticket/10907
needsBrFiller: fixThings,
needsNbspFiller: fixThings,
removeButtons: 'Source,Maximize',
// magicline plugin inserts html crap into the document which is not part of the
// document itself and causes problems when it's sent across the wire and reflected back
// but we filter it now, so that's ok.
removePlugins: 'resize'
});
editor.on('instanceReady', function (Ckeditor) {
editor.execCommand('maximize');
var documentBody = ifrw.$('iframe')[0].contentDocument.body;
documentBody.innerHTML = Messages.initialState;
var inner = window.inner = documentBody;
var cursor = window.cursor = Cursor(inner);
var setEditable = function (bool) {
// careful about putting attributes onto the DOM
// they get put into the chain, and you can have trouble
// getting rid of them later
//inner.style.backgroundColor = bool? 'white': 'grey';
inner.setAttribute('contenteditable', bool);
};
// don't let the user edit until the pad is ready
setEditable(false);
var diffOptions = {
preDiffApply: function (info) {
/* DiffDOM will filter out magicline plugin elements
in practice this will make it impossible to use it
while someone else is typing, which could be annoying.
we should check when such an element is going to be
removed, and prevent that from happening. */
if (info.node && info.node.tagName === 'SPAN' &&
info.node.getAttribute('contentEditable') === "false") {
// it seems to be a magicline plugin element...
if (info.diff.action === 'removeElement') {
// and you're about to remove it...
// this probably isn't what you want
/*
I have never seen this in the console, but the
magic line is still getting removed on remote
edits. This suggests that it's getting removed
by something other than diffDom.
*/
console.log("preventing removal of the magic line!");
// return true to prevent diff application
return true;
}
}
// no use trying to recover the cursor if it doesn't exist
if (!cursor.exists()) { return; }
/* frame is either 0, 1, 2, or 3, depending on which
cursor frames were affected: none, first, last, or both
*/
var frame = info.frame = cursor.inNode(info.node);
if (!frame) { return; }
if (typeof info.diff.oldValue === 'string' && typeof info.diff.newValue === 'string') {
var pushes = cursor.pushDelta(info.diff.oldValue, info.diff.newValue);
if (frame & 1) {
// push cursor start if necessary
if (pushes.commonStart < cursor.Range.start.offset) {
cursor.Range.start.offset += pushes.delta;
}
}
if (frame & 2) {
// push cursor end if necessary
if (pushes.commonStart < cursor.Range.end.offset) {
cursor.Range.end.offset += pushes.delta;
}
}
}
},
postDiffApply: function (info) {
if (info.frame) {
if (info.node) {
if (info.frame & 1) { cursor.fixStart(info.node); }
if (info.frame & 2) { cursor.fixEnd(info.node); }
} else { console.error("info.node did not exist"); }
var sel = cursor.makeSelection();
var range = cursor.makeRange();
cursor.fixSelection(sel, range);
}
}
};
var now = function () { return new Date().getTime(); };
var realtimeOptions = {
// configuration :D
doc: inner,
// provide initialstate...
initialState: stringifyDOM(inner) || '{}',
// really basic operational transform
// reject patch if it results in invalid JSON
transformFunction : JsonOT.validate,
websocketURL: Config.websocketURL,
// username
userName: userName,
// communication channel name
channel: key.channel,
// encryption key
cryptKey: key.cryptKey
};
var DD = new DiffDom(diffOptions);
// apply patches, and try not to lose the cursor in the process!
var applyHjson = function (shjson) {
var userDocStateDom = hjsonToDom(JSON.parse(shjson));
/* in the DOM contentEditable is "false"
while "contenteditable" is undefined.
When it goes over the wire, it seems hyperjson transforms it.
of course, hyperjson simply gets attributes from the DOM.
el.attributes returns 'contenteditable', so we have to correct for that
There are quite possibly all sorts of other attributes which might lose
information, and we won't know what they are until after we've lost them.
this comes from hyperscript line 101. FIXME maybe
*/
userDocStateDom.setAttribute("contenteditable", "true"); // lol wtf
var patch = (DD).diff(inner, userDocStateDom);
(DD).apply(inner, patch);
};
var initializing = true;
var onRemote = realtimeOptions.onRemote = function (info) {
if (initializing) { return; }
var shjson = info.realtime.getUserDoc();
// remember where the cursor is
cursor.update();
// build a dom from HJSON, diff, and patch the editor
applyHjson(shjson);
var shjson2 = stringifyDOM(inner);
if (shjson2 !== shjson) {
console.error("shjson2 !== shjson");
module.realtimeInput.patchText(shjson2);
}
};
var onInit = realtimeOptions.onInit = function (info) {
var $bar = $('#pad-iframe')[0].contentWindow.$('#cke_1_toolbox');
toolbar = info.realtime.toolbar = Toolbar.create($bar, userName, info.realtime);
/* TODO handle disconnects and such*/
};
var onReady = realtimeOptions.onReady = function (info) {
console.log("Unlocking editor");
initializing = false;
setEditable(true);
var shjson = info.realtime.getUserDoc();
applyHjson(shjson);
};
var onAbort = realtimeOptions.onAbort = function (info) {
console.log("Aborting the session!");
// stop the user from continuing to edit
// by setting the editable to false
setEditable(false);
toolbar.failed();
};
var rti = module.realtimeInput = realtimeInput.start(realtimeOptions);
/* It's incredibly important that you assign 'rti.onLocal'
It's used inside of realtimeInput to make sure that all changes
make it into chainpad.
It's being assigned this way because it can't be passed in, and
and can't be easily returned from realtime input without making
the code less extensible.
*/
var propogate = rti.onLocal = function () {
var shjson = stringifyDOM(inner);
if (!rti.patchText(shjson)) {
return;
}
rti.onEvent(shjson);
};
/* hitting enter makes a new line, but places the cursor inside
of the <br> instead of the <p>. This makes it such that you
cannot type until you click, which is rather unnacceptable.
If the cursor is ever inside such a <br>, you probably want
to push it out to the parent element, which ought to be a
paragraph tag. This needs to be done on keydown, otherwise
the first such keypress will not be inserted into the P. */
inner.addEventListener('keydown', cursor.brFix);
var easyTest = window.easyTest = function () {
cursor.update();
var start = cursor.Range.start;
var test = TypingTest.testInput(inner, start.el, start.offset, propogate);
propogate();
return test;
};
editor.on('change', propogate);
});
};
var interval = 100;
var first = function () {
Ckeditor = ifrw.CKEDITOR;
if (Ckeditor) {
andThen(Ckeditor);
} else {
console.log("Ckeditor was not defined. Trying again in %sms",interval);
setTimeout(first, interval);
}
};
$(first);
});

233
www/p/toolbar.js Normal file
View File

@@ -0,0 +1,233 @@
define([
'/common/messages.js'
], function (Messages) {
/** 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 = $container.find('#'+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 + ' a {',
' float: right;',
'}',
'.' + TOOLBAR_CLS + ' div {',
' padding: 0 10px;',
' height: 1.5em;',
// ' background: #f0f0ee;',
' line-height: 25px;',
' height: 22px;',
'}',
'.' + TOOLBAR_CLS + ' div.rtwysiwyg-back {',
' padding: 0;',
' font-weight: bold;',
' cursor: pointer;',
' color: #000;',
'}',
'.rtwysiwyg-toolbar-leftside div {',
' float: left;',
'}',
'.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 createEscape = function ($container) {
var id = uid();
$container.append('<div class="rtwysiwyg-back" id="' + id + '">&#8656; Back</div>');
var $ret = $container.find('#'+id);
$ret.on('click', function () {
window.location.href = '/';
});
return $ret[0];
};
var createSpinner = function ($container) {
var id = uid();
$container.append('<div class="rtwysiwyg-spinner" id="'+id+'"></div>');
return $container.find('#'+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];
if (spinnerElement.timeout) { clearTimeout(spinnerElement.timeout); }
spinnerElement.timeout = setTimeout(function () {
spinnerElement.textContent = '';
}, SPINNER_DISAPPEAR_TIME);
};
var createUserList = function ($container) {
var id = uid();
$container.append('<div class="' + USER_LIST_CLS + '" id="'+id+'"></div>');
return $container.find('#'+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 $container.find('#'+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;
};
// this is a little hack, it should go in it's own file.
// FIXME ok, so let's put it in its own file then
// TODO there should also be a 'clear recent pads' button
var rememberPad = function () {
// FIXME, this is overly complicated, use array methods
var recentPadsStr = localStorage['CryptPad_RECENTPADS'];
var recentPads = [];
if (recentPadsStr) { recentPads = JSON.parse(recentPadsStr); }
// TODO use window.location.hash or something like that
if (window.location.href.indexOf('#') === -1) { return; }
var now = new Date();
var out = [];
for (var i = recentPads.length; i >= 0; i--) {
if (recentPads[i] &&
// TODO precompute this time value, maybe make it configurable?
// FIXME precompute the date too, why getTime every time?
now.getTime() - recentPads[i][1] < (1000*60*60*24*30) &&
recentPads[i][0] !== window.location.href)
{
out.push(recentPads[i]);
}
}
out.push([window.location.href, now.getTime()]);
localStorage['CryptPad_RECENTPADS'] = JSON.stringify(out);
};
var create = function ($container, myUserName, realtime) {
var toolbar = createRealtimeToolbar($container);
createEscape(toolbar.find('.rtwysiwyg-toolbar-leftside'));
var userListElement = createUserList(toolbar.find('.rtwysiwyg-toolbar-leftside'));
var spinner = createSpinner(toolbar.find('.rtwysiwyg-toolbar-rightside'));
var lagElement = createLagElement(toolbar.find('.rtwysiwyg-toolbar-rightside'));
rememberPad();
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);
// Try to filter out non-patch messages, doesn't have to be perfect this is just the spinner
realtime.onMessage(function (msg) { if (msg.indexOf(':[2,') > -1) { ks(); } });
setInterval(function () {
if (!connected) { return; }
checkLag(realtime, lagElement);
}, 3000);
return {
failed: function () {
connected = false;
userListElement.textContent = '';
lagElement.textContent = '';
},
reconnecting: function () {
connected = false;
userListElement.textContent = Messages.reconnecting;
lagElement.textContent = '';
},
connected: function () {
connected = true;
}
};
};
return { create: create };
});