Implement a new pad framework and make it work (seemingly) with /pad/

pull/1/head
Caleb James DeLisle 7 years ago
parent d9845d3450
commit 0eb2165f31

@ -0,0 +1,468 @@
define([
'jquery',
'/bower_components/hyperjson/hyperjson.js',
'/common/toolbar3.js',
'/bower_components/chainpad-json-validator/json-ot.js',
'/common/TypingTests.js',
'json.sortify',
'/bower_components/textpatcher/TextPatcher.js',
'/common/cryptpad-common.js',
'/common/cryptget.js',
'/pad/links.js',
'/bower_components/nthen/index.js',
'/common/sframe-common.js',
'/api/config',
'/customize/messages.js',
'/common/common-util.js',
'css!/bower_components/bootstrap/dist/css/bootstrap.min.css',
'less!/bower_components/components-font-awesome/css/font-awesome.min.css',
'less!/customize/src/less2/main.less',
], function (
$,
Hyperjson,
Toolbar,
JsonOT,
TypingTest,
JSONSortify,
TextPatcher,
Cryptpad,
Cryptget,
Links,
nThen,
SFCommon,
ApiConfig,
Messages,
Util)
{
var SaveAs = window.saveAs;
var UNINITIALIZED = 'UNINITIALIZED';
var STATE = Object.freeze({
DISCONNECTED: 'DISCONNECTED',
FORGOTTEN: 'FORGOTTEN',
INFINITE_SPINNER: 'INFINITE_SPINNER',
INITIALIZING: 'INITIALIZING',
HISTORY_MODE: 'HISTORY_MODE',
READY: 'READY'
});
var onConnectError = function () {
Cryptpad.errorLoadingScreen(Messages.websocketError);
};
var create = function (options, cb) {
var evContentUpdate = Util.mkEvent();
var evEditableStateChange = Util.mkEvent();
var evOnReady = Util.mkEvent();
var evOnDefaultContentNeeded = Util.mkEvent();
var evStart = Util.mkEvent(true);
var common;
var cpNfInner;
var textPatcher;
var readOnly;
var title;
var toolbar;
var state = STATE.DISCONNECTED;
var titleRecommender = function () { return false; };
var contentGetter = function () { return UNINITIALIZED; };
var normalize = function (x) { return x; };
var extractMetadata = function (content) {
var meta = {};
if (Array.isArray(content)) {
var m = content.pop();
if (typeof(m.metadata) === 'object') {
// pad
meta = m.metadata;
} else {
content.push(m);
}
} else if (typeof(content.metadata) === 'object') {
meta = content.metadata;
delete content.metadata;
}
return meta;
};
var isEditable = function () {
return (state === STATE.READY && !readOnly);
};
var stateChange = function (newState) {
var wasEditable = isEditable();
if (state === STATE.INFINITE_SPINNER) { return; }
if (newState === STATE.INFINITE_SPINNER) {
state = newState;
} else if (state === STATE.DISCONNECTED && newState !== STATE.INITIALIZING) {
throw new Error("Cannot transition from DISCONNECTED to " + newState);
} else if (state !== STATE.READY && newState === STATE.HISTORY_MODE) {
throw new Error("Cannot transition from " + state + " to " + newState);
} else {
state = newState;
}
switch (state) {
case STATE.DISCONNECTED:
case STATE.INITIALIZING: {
evStart.reg(function () { toolbar.reconnecting(); });
break;
}
case STATE.INFINITE_SPINNER: {
evStart.reg(function () { toolbar.failed(); });
break;
}
default:
}
if (wasEditable !== isEditable()) { evEditableStateChange.fire(isEditable()); }
};
var onRemote = function () {
if (state !== STATE.READY) { return; }
var oldContent = normalize(contentGetter());
var newContentStr = cpNfInner.realtime.getUserDoc();
var newContent = normalize(JSON.parse(newContentStr));
var meta = extractMetadata(newContent);
cpNfInner.metadataMgr.updateMetadata(meta);
evContentUpdate.fire(newContent);
if (!readOnly) {
var newContent2NoMeta = normalize(contentGetter());
var newContent2StrNoMeta = JSONSortify(newContent2NoMeta);
var newContentStrNoMeta = JSONSortify(newContent);
if (newContent2StrNoMeta !== newContentStrNoMeta) {
console.error("shjson2 !== shjson");
textPatcher(newContent2StrNoMeta);
/* pushing back over the wire is necessary, but it can
result in a feedback loop, which we call a browser
fight */
if (module.logFights) {
// what changed?
var op = TextPatcher.diff(newContentStrNoMeta, newContent2StrNoMeta);
// log the changes
TextPatcher.log(newContentStrNoMeta, op);
var sop = JSON.stringify(TextPatcher.format(newContentStrNoMeta, op));
var index = module.fights.indexOf(sop);
if (index === -1) {
module.fights.push(sop);
console.log("Found a new type of browser disagreement");
console.log("You can inspect the list in your " +
"console at `REALTIME_MODULE.fights`");
console.log(module.fights);
} else {
console.log("Encountered a known browser disagreement: " +
"available at `REALTIME_MODULE.fights[%s]`", index);
}
}
}
}
// Notify only when the content has changed, not when someone has joined/left
if (JSONSortify(newContent) !== JSONSortify(oldContent)) {
common.notify();
}
};
var setHistoryMode = function (bool, update) {
stateChange((bool) ? STATE.HISTORY_MODE : STATE.READY);
if (!bool && update) { onRemote(); }
};
var onLocal = function () {
if (state !== STATE.READY) { return; }
if (readOnly) { return; }
// stringify the json and send it into chainpad
var content = normalize(contentGetter());
if (typeof(content) !== 'object') {
if (content === UNINITIALIZED) { return; }
throw new Error("Content must be an object or array, type is " + typeof(content));
}
if (Array.isArray(content)) {
// Pad
content.push({ metadata: cpNfInner.metadataMgr.getMetadataLazy() });
} else {
content.metadata = cpNfInner.metadataMgr.getMetadataLazy();
}
var contentStr = JSONSortify(content);
textPatcher(contentStr);
if (cpNfInner.chainpad.getUserDoc() !== contentStr) {
console.error("realtime.getUserDoc() !== shjson");
}
};
var emitResize = function () {
var evt = window.document.createEvent('UIEvents');
evt.initUIEvent('resize', true, false, window, 0);
window.dispatchEvent(evt);
};
var onReady = function () {
var newContentStr = cpNfInner.chainpad.getUserDoc();
var newPad = false;
if (newContentStr === '') { newPad = true; }
if (!newPad) {
var newContent = JSON.parse(newContentStr);
var meta = extractMetadata(newContent);
cpNfInner.metadataMgr.updateMetadata(meta);
newContent = normalize(newContent);
evContentUpdate.fire(newContent);
if (!readOnly) {
var newContent2NoMeta = normalize(contentGetter());
var newContent2StrNoMeta = JSONSortify(newContent2NoMeta);
var newContentStrNoMeta = JSONSortify(newContent);
if (newContent2StrNoMeta !== newContentStrNoMeta) {
console.log('err');
console.error("shjson2 !== shjson");
console.log(newContent2StrNoMeta);
console.log(newContentStrNoMeta);
Cryptpad.errorLoadingScreen(Messages.wrongApp);
throw new Error();
}
}
} else {
title.updateTitle(Cryptpad.initialName || title.defaultTitle);
}
if (!readOnly) { onLocal(); }
evOnReady.fire(newPad);
Cryptpad.removeLoadingScreen(emitResize);
stateChange(STATE.READY);
if (newPad) {
common.openTemplatePicker();
}
};
var onConnectionChange = function (info) {
stateChange(info.state ? STATE.INITIALIZING : STATE.DISCONNECTED);
if (info.state) {
Cryptpad.findOKButton().click();
} else {
Cryptpad.alert(Messages.common_connectionLost, undefined, true);
}
};
var setFileExporter = function (extension, fe) {
var $export = common.createButton('export', true, {}, function () {
var suggestion = title.suggestTitle('cryptpad-document');
Cryptpad.prompt(Messages.exportPrompt,
Cryptpad.fixFileName(suggestion) + '.html', function (filename)
{
if (!(typeof(filename) === 'string' && filename)) { return; }
var blob = fe();
SaveAs(blob, filename);
});
});
toolbar.$drawer.append($export);
};
var setFileImporter = function (mimeType, fi) {
if (readOnly) { return; }
toolbar.$drawer.append(
common.createButton('import', true, { accept: mimeType }, function (c) {
evContentUpdate.fire(fi(c));
onLocal();
})
);
};
var feedback = function (action, force) {
if (state === STATE.DISCONNECTED || state === STATE.INITIALIZING) { return; }
common.feedback(action, force);
};
nThen(function (waitFor) {
SFCommon.create(waitFor(function (c) { common = c; }));
}).nThen(function (waitFor) {
cpNfInner = common.startRealtime({
// really basic operational transform
transformFunction: options.transformFunction || JsonOT.validate,
// cryptpad debug logging (default is 1)
// logLevel: 0,
validateContent: options.validateContent || function (content) {
try {
JSON.parse(content);
return true;
} catch (e) {
console.log("Failed to parse, rejecting patch");
return false;
}
},
onRemote: function () { evStart.reg(onRemote); },
onLocal: function () { evStart.reg(onLocal); },
onInit: function () { stateChange(STATE.INITIALIZING); },
onReady: function () { evStart.reg(onReady); },
onConnectionChange: onConnectionChange
});
var privReady = Util.once(waitFor());
var checkReady = function () {
if (typeof(cpNfInner.metadataMgr.getPrivateData().readOnly) === 'boolean') {
readOnly = cpNfInner.metadataMgr.getPrivateData().readOnly;
privReady();
}
};
cpNfInner.metadataMgr.onChange(checkReady);
checkReady();
textPatcher = TextPatcher.create({ realtime: cpNfInner.chainpad });
cpNfInner.onInfiniteSpinner(function () {
toolbar.failed();
cpNfInner.chainpad.abort();
stateChange(STATE.INFINITE_SPINNER);
Cryptpad.confirm(Messages.realtime_unrecoverableError, function (yes) {
if (!yes) { return; }
common.gotoURL();
});
});
//Cryptpad.onLogout(function () { ... });
Cryptpad.onError(function (info) {
if (info && info.type === "store") {
onConnectError();
}
});
}).nThen(function () {
var $bar = $('#cke_1_toolbox'); // TODO
if (!$bar.length) { throw new Error(); }
title = common.createTitle({ getHeadingText: titleRecommender }, onLocal);
var configTb = {
displayed: ['userlist', 'title', 'useradmin', 'spinner', 'newpad', 'share', 'limit'],
title: title.getTitleConfig(),
metadataMgr: cpNfInner.metadataMgr,
readOnly: readOnly,
ifrw: window,
realtime: cpNfInner.chainpad,
common: Cryptpad,
sfCommon: common,
$container: $bar,
$contentContainer: $('#cke_1_contents'), // TODO
};
toolbar = Toolbar.create(configTb);
title.setToolbar(toolbar);
/* add a history button */
var histConfig = {
onLocal: onLocal,
onRemote: onRemote,
setHistory: setHistoryMode,
applyVal: function (val) {
evContentUpdate.fire(JSON.parse(val) || ["BODY",{},[]]);
},
$toolbar: $bar
};
var $hist = common.createButton('history', true, {histConfig: histConfig});
toolbar.$drawer.append($hist);
if (!cpNfInner.metadataMgr.getPrivateData().isTemplate) {
var templateObj = {
rt: cpNfInner.chainpad,
getTitle: function () { return cpNfInner.metadataMgr.getMetadata().title; }
};
var $templateButton = common.createButton('template', true, templateObj);
toolbar.$rightside.append($templateButton);
}
/* add a forget button */
toolbar.$rightside.append(common.createButton('forget', true, {}, function (err) {
if (err) { return; }
stateChange(STATE.HISTORY_MODE);
}));
var $tags = common.createButton('hashtag', true);
toolbar.$rightside.append($tags);
cb(Object.freeze({
// Register an event to be informed of a content update coming from remote
// This event will pass you the object.
onContentUpdate: evContentUpdate.reg,
// Set the content supplier, this is the function which will supply the content
// in the pad when requested by the framework.
setContentGetter: function (cg) { contentGetter = cg; },
// Inform the framework that the content of the pad has been changed locally.
localChange: onLocal,
// Register to be informed if the state (whether the document is editable) changes.
onEditableChange: evEditableStateChange.reg,
// Determine whether the UI should be locked for editing.
isLocked: function () { return state !== STATE.READY; },
// Determine whether the pad is a "read only" pad and cannot be changed.
isReadOnly: function () { return readOnly; },
// Call this to supply a function which can recommend a good title for the pad,
// if possible.
setTitleRecommender: function (ush) { titleRecommender = ush; },
// Register to be called when the pad has completely loaded
// (just before the loading screen is removed).
onReady: evOnReady.reg,
// Register to be called when a new pad is being setup and default content is
// needed. When you are called back you must put the content in the UI and then
// return and then the content getter (setContentGetter()) will be called.
onDefaultContentNeeded: evOnDefaultContentNeeded.reg,
// Set a file exporter, this takes 2 arguments.
// 1. <string> A file extension which will be proposed when saving the file.
// 2. <function> A function which when called, will return a Blob containing the
// file to be saved.
setFileExporter: setFileExporter,
// Set a file importer, this takes 2 arguments.
// 1. <string> The MIME Type of the types of file to allow importing.
// 2. <function> A function which takes a single string argument and puts the
// content into the UI.
setFileImporter: setFileImporter,
// Set a function which will normalize the content returned by the content getter
// such as removing extra fields.
setNormalizer: function (n) { normalize = n; },
// Call the CryptPad feedback API.
feedback: feedback,
// Call this after all of the handlers are setup.
start: evStart.fire,
// Determine the internal state of the framework.
getState: function () { return state; },
// Internals
_: {
sfCommon: common,
toolbar: toolbar,
cpNfInner: cpNfInner,
title: title
}
}));
});
};
return { create: create };
});

@ -19,22 +19,15 @@ require(['/api/config'], function (ApiConfig) {
}); });
define([ define([
'jquery', 'jquery',
'/bower_components/chainpad-crypto/crypto.js',
'/bower_components/hyperjson/hyperjson.js', '/bower_components/hyperjson/hyperjson.js',
'/common/toolbar3.js', '/common/sframe-app-framework.js',
'/common/cursor.js', '/common/cursor.js',
'/bower_components/chainpad-json-validator/json-ot.js',
'/common/TypingTests.js', '/common/TypingTests.js',
'json.sortify', '/customize/messages.js',
'/bower_components/textpatcher/TextPatcher.js',
'/common/cryptpad-common.js',
'/common/cryptget.js',
'/pad/links.js', '/pad/links.js',
'/bower_components/nthen/index.js', '/bower_components/nthen/index.js',
'/common/sframe-common.js',
'/api/config', '/api/config',
'/bower_components/file-saver/FileSaver.min.js',
'/bower_components/diff-dom/diffDOM.js', '/bower_components/diff-dom/diffDOM.js',
'css!/bower_components/bootstrap/dist/css/bootstrap.min.css', 'css!/bower_components/bootstrap/dist/css/bootstrap.min.css',
@ -42,30 +35,17 @@ define([
'less!/customize/src/less2/main.less', 'less!/customize/src/less2/main.less',
], function ( ], function (
$, $,
Crypto,
Hyperjson, Hyperjson,
Toolbar, Framework,
Cursor, Cursor,
JsonOT,
TypingTest, TypingTest,
JSONSortify, Messages,
TextPatcher,
Cryptpad,
Cryptget,
Links, Links,
nThen, nThen,
SFCommon,
ApiConfig) ApiConfig)
{ {
var saveAs = window.saveAs;
var Messages = Cryptpad.Messages;
var DiffDom = window.diffDOM; var DiffDom = window.diffDOM;
var stringify = function (obj) { return JSONSortify(obj); };
window.Toolbar = Toolbar;
window.Hyperjson = Hyperjson;
var slice = function (coll) { var slice = function (coll) {
return Array.prototype.slice.call(coll); return Array.prototype.slice.call(coll);
}; };
@ -87,21 +67,11 @@ define([
var module = window.REALTIME_MODULE = window.APP = { var module = window.REALTIME_MODULE = window.APP = {
Hyperjson: Hyperjson, Hyperjson: Hyperjson,
TextPatcher: TextPatcher,
logFights: true, logFights: true,
fights: [], fights: [],
Cryptpad: Cryptpad,
Cursor: Cursor, Cursor: Cursor,
}; };
var emitResize = module.emitResize = function () {
var evt = window.document.createEvent('UIEvents');
evt.initUIEvent('resize', true, false, window, 0);
window.dispatchEvent(evt);
};
var toolbar;
var isNotMagicLine = function (el) { var isNotMagicLine = function (el) {
return !(el && typeof(el.getAttribute) === 'function' && return !(el && typeof(el.getAttribute) === 'function' &&
el.getAttribute('class') && el.getAttribute('class') &&
@ -114,10 +84,6 @@ define([
return hj; return hj;
}; };
var onConnectError = function () {
Cryptpad.errorLoadingScreen(Messages.websocketError);
};
var domFromHTML = function (html) { var domFromHTML = function (html) {
return new DOMParser().parseFromString(html, 'text/html'); return new DOMParser().parseFromString(html, 'text/html');
}; };
@ -272,38 +238,53 @@ define([
}; };
}; };
////////////////////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////////////////////
var andThen = function (editor, Ckeditor, common) { var addToolbarHideBtn = function (framework, $bar) {
//var $iframe = $('#pad-iframe').contents(); // Expand / collapse the toolbar
//var secret = Cryptpad.getSecrets(); var $collapse = framework._.sfCommon.createButton(null, true);
//var readOnly = secret.keys && !secret.keys.editKeyStr; $collapse.removeClass('fa-question');
//if (!secret.keys) { var updateIcon = function (isVisible) {
// secret.keys = secret.key; $collapse.removeClass('fa-caret-down').removeClass('fa-caret-up');
//} if (!isVisible) {
var readOnly = false; // TODO framework.feedback('HIDETOOLBAR_PAD');
var cpNfInner; $collapse.addClass('fa-caret-down');
var metadataMgr; }
var onLocal; else {
framework.feedback('SHOWTOOLBAR_PAD');
$collapse.addClass('fa-caret-up');
}
};
updateIcon();
$collapse.click(function () {
$(window).trigger('resize');
$('.cke_toolbox_main').toggle();
$(window).trigger('cryptpad-ck-toolbar');
var isVisible = $bar.find('.cke_toolbox_main').is(':visible');
framework._.sfCommon.setAttribute(['pad', 'showToolbar'], isVisible);
updateIcon(isVisible);
});
framework._.sfCommon.getAttribute(['pad', 'showToolbar'], function (err, data) {
if (typeof(data) === "undefined" || data) {
$('.cke_toolbox_main').show();
updateIcon(true);
return;
}
$('.cke_toolbox_main').hide();
updateIcon(false);
});
framework._.toolbar.$rightside.append($collapse);
};
var andThen2 = function (editor, Ckeditor, framework) {
var $bar = $('#cke_1_toolbox'); var $bar = $('#cke_1_toolbox');
var $html = $bar.closest('html'); var $html = $bar.closest('html');
var $faLink = $html.find('head link[href*="/bower_components/components-font-awesome/css/font-awesome.min.css"]'); var $faLink = $html.find('head link[href*="/bower_components/components-font-awesome/css/font-awesome.min.css"]');
if ($faLink.length) { if ($faLink.length) {
$html.find('iframe').contents().find('head').append($faLink.clone()); $html.find('iframe').contents().find('head').append($faLink.clone());
} }
var isHistoryMode = false;
if (readOnly) {
$('#cke_1_toolbox > .cke_toolbox_main').hide();
}
/* add a class to the magicline plugin so we can pick it out more easily */
var ml = Ckeditor.instances.editor1.plugins.magicline.backdoor.that.line.$; var ml = Ckeditor.instances.editor1.plugins.magicline.backdoor.that.line.$;
[ml, ml.parentElement].forEach(function (el) { [ml, ml.parentElement].forEach(function (el) {
@ -326,27 +307,15 @@ define([
if (href) { ifrWindow.open(bounceHref, '_blank'); } if (href) { ifrWindow.open(bounceHref, '_blank'); }
}; };
var setEditable = module.setEditable = function (bool) { if (!framework.isReadOnly()) {
if (bool) { framework.onEditableChange(function () {
$(inner).css({ var locked = framework.isLocked();
color: '#333', $(inner).css({ 'background-color': ((locked) ? '#aaa' : '') });
inner.setAttribute('contenteditable', !locked);
}); });
} }
if (!readOnly || !bool) {
inner.setAttribute('contenteditable', bool);
}
};
// don't let the user edit until the pad is ready
setEditable(false);
var initializing = true;
var Title; framework.setTitleRecommender(function () {
//var UserList;
//var Metadata;
var getHeadingText = function () {
var text; var text;
if (['h1', 'h2', 'h3'].some(function (t) { if (['h1', 'h2', 'h3'].some(function (t) {
var $header = $(inner).find(t + ':first-of-type'); var $header = $(inner).find(t + ':first-of-type');
@ -355,273 +324,38 @@ define([
return true; return true;
} }
})) { return text; } })) { return text; }
}; });
var DD = new DiffDom(mkDiffOptions(cursor, readOnly)); var DD = new DiffDom(mkDiffOptions(cursor, framework.isReadOnly()));
// apply patches, and try not to lose the cursor in the process! // apply patches, and try not to lose the cursor in the process!
var applyHjson = function (shjson) { framework.onContentUpdate(function (hjson) {
var userDocStateDom = hjsonToDom(JSON.parse(shjson)); var userDocStateDom = hjsonToDom(hjson);
userDocStateDom.setAttribute("contenteditable",
inner.getAttribute('contenteditable'));
if (!readOnly && !initializing) {
userDocStateDom.setAttribute("contenteditable", "true"); // lol wtf
} else if (readOnly) {
userDocStateDom.removeAttribute("contenteditable");
}
var patch = (DD).diff(inner, userDocStateDom); var patch = (DD).diff(inner, userDocStateDom);
(DD).apply(inner, patch); (DD).apply(inner, patch);
if (readOnly) { if (framework.isReadOnly()) {
var $links = $(inner).find('a'); var $links = $(inner).find('a');
// off so that we don't end up with multiple identical handlers // off so that we don't end up with multiple identical handlers
$links.off('click', openLink).on('click', openLink); $links.off('click', openLink).on('click', openLink);
} }
};
var stringifyDOM = module.stringifyDOM = function (dom) {
var hjson = Hyperjson.fromDOM(dom, isNotMagicLine, brFilter);
hjson[3] = {
metadata: metadataMgr.getMetadataLazy()
};
/*hjson[3] = { TODO
users: UserList.userData,
defaultTitle: Title.defaultTitle,
type: 'pad'
}
};
if (!initializing) {
hjson[3].metadata.title = Title.title;
} else if (Cryptpad.initialName && !hjson[3].metadata.title) {
hjson[3].metadata.title = Cryptpad.initialName;
}*/
return stringify(hjson);
};
var realtimeOptions = {
readOnly: readOnly,
// really basic operational transform
transformFunction : JsonOT.validate,
// cryptpad debug logging (default is 1)
// logLevel: 0,
validateContent: function (content) {
try {
JSON.parse(content);
return true;
} catch (e) {
console.log("Failed to parse, rejecting patch");
return false;
}
}
};
var setHistory = function (bool, update) {
isHistoryMode = bool;
setEditable(!bool);
if (!bool && update) {
realtimeOptions.onRemote();
}
};
realtimeOptions.onRemote = function () {
if (initializing) { return; }
if (isHistoryMode) { return; }
var oldShjson = stringifyDOM(inner);
var shjson = module.realtime.getUserDoc();
// remember where the cursor is
cursor.update();
// Update the user list (metadata) from the hyperjson
// TODO Metadata.update(shjson);
var newInner = JSON.parse(shjson);
var newSInner;
if (newInner.length > 2) {
newSInner = stringify(newInner[2]);
}
if (newInner[3]) {
metadataMgr.updateMetadata(newInner[3].metadata);
}
// build a dom from HJSON, diff, and patch the editor
applyHjson(shjson);
if (!readOnly) {
var shjson2 = stringifyDOM(inner);
// TODO
//shjson = JSON.stringify(JSON.parse(shjson).slice(0,3));
if (shjson2 !== shjson) {
console.error("shjson2 !== shjson");
module.patchText(shjson2);
/* pushing back over the wire is necessary, but it can
result in a feedback loop, which we call a browser
fight */
if (module.logFights) {
// what changed?
var op = TextPatcher.diff(shjson, shjson2);
// log the changes
TextPatcher.log(shjson, op);
var sop = JSON.stringify(TextPatcher.format(shjson, op));
var index = module.fights.indexOf(sop);
if (index === -1) {
module.fights.push(sop);
console.log("Found a new type of browser disagreement");
console.log("You can inspect the list in your " +
"console at `REALTIME_MODULE.fights`");
console.log(module.fights);
} else {
console.log("Encountered a known browser disagreement: " +
"available at `REALTIME_MODULE.fights[%s]`", index);
}
}
}
}
// Notify only when the content has changed, not when someone has joined/left
var oldSInner = stringify(JSON.parse(oldShjson)[2]);
if (newSInner && newSInner !== oldSInner) {
common.notify();
}
};
var exportFile = function () {
var html = getHTML(inner);
var suggestion = Title.suggestTitle('cryptpad-document');
Cryptpad.prompt(Messages.exportPrompt,
Cryptpad.fixFileName(suggestion) + '.html', function (filename) {
if (!(typeof(filename) === 'string' && filename)) { return; }
var blob = new Blob([html], {type: "text/html;charset=utf-8"});
saveAs(blob, filename);
}); });
};
var importFile = function (content) {
var shjson = stringify(Hyperjson.fromDOM(domFromHTML(content).body));
applyHjson(shjson);
realtimeOptions.onLocal();
};
realtimeOptions.onInit = function (info) { framework.setContentGetter(function () {
readOnly = metadataMgr.getPrivateData().readOnly; return Hyperjson.fromDOM(inner, isNotMagicLine, brFilter);
console.log('onInit');
var titleCfg = { getHeadingText: getHeadingText };
Title = common.createTitle(titleCfg, realtimeOptions.onLocal);
var configTb = {
displayed: ['userlist', 'title', 'useradmin', 'spinner', 'newpad', 'share', 'limit'],
title: Title.getTitleConfig(),
metadataMgr: metadataMgr,
readOnly: readOnly,
ifrw: window,
realtime: info.realtime,
common: Cryptpad,
sfCommon: common,
$container: $bar,
$contentContainer: $('#cke_1_contents'),
};
toolbar = info.realtime.toolbar = Toolbar.create(configTb);
Title.setToolbar(toolbar);
var $rightside = toolbar.$rightside;
var $drawer = toolbar.$drawer;
var src = 'less!/customize/src/less/toolbar.less';
require([
src
], function () {
var $html = $bar.closest('html');
$html
.find('head style[data-original-src="' + src.replace(/less!/, '') + '"]')
.appendTo($html.find('head'));
}); });
$bar.find('#cke_1_toolbar_collapser').hide(); $bar.find('#cke_1_toolbar_collapser').hide();
if (!readOnly) { if (!framework.isReadOnly()) {
// Expand / collapse the toolbar addToolbarHideBtn(framework, $bar);
var $collapse = common.createButton(null, true);
$collapse.removeClass('fa-question');
var updateIcon = function (isVisible) {
$collapse.removeClass('fa-caret-down').removeClass('fa-caret-up');
if (!isVisible) {
if (!initializing) { common.feedback('HIDETOOLBAR_PAD'); }
$collapse.addClass('fa-caret-down');
}
else {
if (!initializing) { common.feedback('SHOWTOOLBAR_PAD'); }
$collapse.addClass('fa-caret-up');
}
};
updateIcon();
$collapse.click(function () {
$(window).trigger('resize');
$('.cke_toolbox_main').toggle();
$(window).trigger('cryptpad-ck-toolbar');
var isVisible = $bar.find('.cke_toolbox_main').is(':visible');
common.setAttribute(['pad', 'showToolbar'], isVisible);
updateIcon(isVisible);
});
common.getAttribute(['pad', 'showToolbar'], function (err, data) {
if (typeof(data) === "undefined" || data) {
$('.cke_toolbox_main').show();
updateIcon(true);
return;
}
$('.cke_toolbox_main').hide();
updateIcon(false);
});
$rightside.append($collapse);
} else { } else {
$('.cke_toolbox_main').hide(); $('.cke_toolbox_main').hide();
} }
/* add a history button */ framework.onReady(function (newPad) {
var histConfig = {
onLocal: realtimeOptions.onLocal,
onRemote: realtimeOptions.onRemote,
setHistory: setHistory,
applyVal: function (val) { applyHjson(val || '["BODY",{},[]]'); },
$toolbar: $bar
};
var $hist = common.createButton('history', true, {histConfig: histConfig});
$drawer.append($hist);
if (!metadataMgr.getPrivateData().isTemplate) {
var templateObj = {
rt: info.realtime,
getTitle: function () { return metadataMgr.getMetadata().title; }
};
var $templateButton = common.createButton('template', true, templateObj);
$rightside.append($templateButton);
}
/* add an export button */
var $export = common.createButton('export', true, {}, exportFile);
$drawer.append($export);
if (!readOnly) {
/* add an import button */
var $import = common.createButton('import', true, {
accept: 'text/html'
}, importFile);
$drawer.append($import);
}
/* add a forget button */
var forgetCb = function (err) {
if (err) { return; }
setEditable(false);
};
var $forgetPad = common.createButton('forget', true, {}, forgetCb);
$rightside.append($forgetPad);
};
// this should only ever get called once, when the chain syncs
realtimeOptions.onReady = function (info) {
console.log('onReady');
if (!module.isMaximized) { if (!module.isMaximized) {
module.isMaximized = true; module.isMaximized = true;
$('iframe.cke_wysiwyg_frame').css('width', ''); $('iframe.cke_wysiwyg_frame').css('width', '');
@ -629,110 +363,40 @@ define([
} }
$('body').addClass('app-pad'); $('body').addClass('app-pad');
if (module.realtime !== info.realtime) {
module.patchText = TextPatcher.create({
realtime: info.realtime,
//logging: true,
});
}
module.realtime = info.realtime;
var shjson = module.realtime.getUserDoc();
var newPad = false;
if (shjson === '') { newPad = true; }
if (!newPad) {
applyHjson(shjson);
// Update the user list (metadata) from the hyperjson
// XXX Metadata.update(shjson);
var parsed = JSON.parse(shjson);
if (parsed[3] && parsed[3].metadata) {
metadataMgr.updateMetadata(parsed[3].metadata);
}
if (!readOnly) {
var shjson2 = stringifyDOM(inner);
var hjson2 = JSON.parse(shjson2).slice(0,3);
var hjson = JSON.parse(shjson).slice(0,3);
if (stringify(hjson2) !== stringify(hjson)) {
console.log('err');
console.error("shjson2 !== shjson");
console.log(stringify(hjson2));
console.log(stringify(hjson));
Cryptpad.errorLoadingScreen(Messages.wrongApp);
throw new Error();
}
}
} else {
Title.updateTitle(Cryptpad.initialName || Title.defaultTitle);
documentBody.innerHTML = Messages.initialState;
}
Cryptpad.removeLoadingScreen(emitResize);
setEditable(!readOnly);
initializing = false;
if (readOnly) { return; }
if (newPad) {
common.openTemplatePicker();
}
onLocal();
editor.focus(); editor.focus();
if (newPad) { if (newPad) {
documentBody.innerHTML = Messages.initialState;
cursor.setToEnd(); cursor.setToEnd();
} else { } else if (framework.isReadOnly()) {
cursor.setToStart(); cursor.setToStart();
} }
}; });
realtimeOptions.onConnectionChange = function (info) {
setEditable(info.state);
//toolbar.failed(); TODO
if (info.state) {
initializing = true;
//toolbar.reconnecting(info.myId); // TODO
Cryptpad.findOKButton().click();
} else {
Cryptpad.alert(Messages.common_connectionLost, undefined, true);
}
};
realtimeOptions.onError = onConnectError;
onLocal = realtimeOptions.onLocal = function () {
console.log('onlocal');
if (initializing) { return; }
if (isHistoryMode) { return; }
if (readOnly) { return; }
// stringify the json and send it into chainpad
var shjson = stringifyDOM(inner);
module.patchText(shjson);
if (module.realtime.getUserDoc() !== shjson) {
console.error("realtime.getUserDoc() !== shjson");
}
};
cpNfInner = common.startRealtime(realtimeOptions); framework.onDefaultContentNeeded(function () {
metadataMgr = cpNfInner.metadataMgr; documentBody.innerHTML = Messages.initialState;
});
cpNfInner.onInfiniteSpinner(function () { framework.setFileImporter('text/html', function (content) {
setEditable(false); return Hyperjson.fromDOM(domFromHTML(content).body);
Cryptpad.confirm(Messages.realtime_unrecoverableError, function (yes) {
if (!yes) { return; }
common.gotoURL();
//window.parent.location.reload();
}); });
framework.setFileExporter("html", function () {
var html = getHTML(inner);
var blob = new Blob([html], {type: "text/html;charset=utf-8"});
return blob;
}); });
Cryptpad.onLogout(function () { setEditable(false); }); framework.setNormalizer(function (hjson) {
return [
'BODY',
{
"class": "cke_editable cke_editable_themed cke_contents_ltr cke_show_borders",
"contenteditable": "true",
"spellcheck":"false"
},
hjson[2]
];
});
/* hitting enter makes a new line, but places the cursor inside /* hitting enter makes a new line, but places the cursor inside
of the <br> instead of the <p>. This makes it such that you of the <br> instead of the <p>. This makes it such that you
@ -743,7 +407,7 @@ define([
the first such keypress will not be inserted into the P. */ the first such keypress will not be inserted into the P. */
inner.addEventListener('keydown', cursor.brFix); inner.addEventListener('keydown', cursor.brFix);
editor.on('change', onLocal); editor.on('change', framework.localChange);
// export the typing tests to the window. // export the typing tests to the window.
// call like `test = easyTest()` // call like `test = easyTest()`
@ -751,8 +415,8 @@ define([
window.easyTest = function () { window.easyTest = function () {
cursor.update(); cursor.update();
var start = cursor.Range.start; var start = cursor.Range.start;
var test = TypingTest.testInput(inner, start.el, start.offset, onLocal); var test = TypingTest.testInput(inner, start.el, start.offset, framework.localChange);
onLocal(); framework.localChange();
return test; return test;
}; };
@ -765,25 +429,24 @@ define([
var id = classes[0]; var id = classes[0];
if (typeof(id) === 'string') { if (typeof(id) === 'string') {
common.feedback(id.toUpperCase()); framework.feedback(id.toUpperCase());
} }
}); });
framework.start();
}; };
var main = function () { var main = function () {
var Ckeditor; var Ckeditor;
var editor; var editor;
var common; var framework;
nThen(function (waitFor) { nThen(function (waitFor) {
ckEditorAvailable(waitFor(function (ck) { ckEditorAvailable(waitFor(function (ck) {
Ckeditor = ck; Ckeditor = ck;
require(['/pad/wysiwygarea-plugin.js'], waitFor()); require(['/pad/wysiwygarea-plugin.js'], waitFor());
})); }));
$(waitFor(function () { $(waitFor());
Cryptpad.addLoadingScreen();
}));
SFCommon.create(waitFor(function (c) { module.common = common = c; }));
}).nThen(function (waitFor) { }).nThen(function (waitFor) {
Ckeditor.config.toolbarCanCollapse = true; Ckeditor.config.toolbarCanCollapse = true;
if (screen.height < 800) { if (screen.height < 800) {
@ -798,21 +461,11 @@ define([
customConfig: '/customize/ckeditor-config.js', customConfig: '/customize/ckeditor-config.js',
}); });
editor.on('instanceReady', waitFor()); editor.on('instanceReady', waitFor());
}).nThen(function (/*waitFor*/) { }).nThen(function (waitFor) {
/*if (Ckeditor.env.safari) { Framework.create({}, waitFor(function (fw) { window.APP.framework = framework = fw; }));
var fixIframe = function () {
$('iframe.cke_wysiwyg_frame').height($('#cke_1_contents').height());
};
$(window).resize(fixIframe);
fixIframe();
}*/
Links.addSupportForOpeningLinksInNewTab(Ckeditor)({editor: editor}); Links.addSupportForOpeningLinksInNewTab(Ckeditor)({editor: editor});
Cryptpad.onError(function (info) { }).nThen(function (/*waitFor*/) {
if (info && info.type === "store") { andThen2(editor, Ckeditor, framework);
onConnectError();
}
});
andThen(editor, Ckeditor, common);
}); });
}; };
main(); main();

Loading…
Cancel
Save