From 0eb2165f313258d8c97a4a01888f34d9f31077f7 Mon Sep 17 00:00:00 2001 From: Caleb James DeLisle Date: Mon, 25 Sep 2017 15:45:08 +0200 Subject: [PATCH] Implement a new pad framework and make it work (seemingly) with /pad/ --- www/common/sframe-app-framework.js | 468 ++++++++++++++++++++++++ www/pad/inner.js | 553 ++++++----------------------- 2 files changed, 571 insertions(+), 450 deletions(-) create mode 100644 www/common/sframe-app-framework.js diff --git a/www/common/sframe-app-framework.js b/www/common/sframe-app-framework.js new file mode 100644 index 000000000..7bdd902d4 --- /dev/null +++ b/www/common/sframe-app-framework.js @@ -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. A file extension which will be proposed when saving the file. + // 2. 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. The MIME Type of the types of file to allow importing. + // 2. 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 }; +}); \ No newline at end of file diff --git a/www/pad/inner.js b/www/pad/inner.js index fde4a04ff..f0527010d 100644 --- a/www/pad/inner.js +++ b/www/pad/inner.js @@ -19,22 +19,15 @@ require(['/api/config'], function (ApiConfig) { }); define([ 'jquery', - '/bower_components/chainpad-crypto/crypto.js', '/bower_components/hyperjson/hyperjson.js', - '/common/toolbar3.js', + '/common/sframe-app-framework.js', '/common/cursor.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', + '/customize/messages.js', '/pad/links.js', '/bower_components/nthen/index.js', - '/common/sframe-common.js', '/api/config', - '/bower_components/file-saver/FileSaver.min.js', '/bower_components/diff-dom/diffDOM.js', 'css!/bower_components/bootstrap/dist/css/bootstrap.min.css', @@ -42,30 +35,17 @@ define([ 'less!/customize/src/less2/main.less', ], function ( $, - Crypto, Hyperjson, - Toolbar, + Framework, Cursor, - JsonOT, TypingTest, - JSONSortify, - TextPatcher, - Cryptpad, - Cryptget, + Messages, Links, nThen, - SFCommon, ApiConfig) { - var saveAs = window.saveAs; - var Messages = Cryptpad.Messages; var DiffDom = window.diffDOM; - var stringify = function (obj) { return JSONSortify(obj); }; - - window.Toolbar = Toolbar; - window.Hyperjson = Hyperjson; - var slice = function (coll) { return Array.prototype.slice.call(coll); }; @@ -87,21 +67,11 @@ define([ var module = window.REALTIME_MODULE = window.APP = { Hyperjson: Hyperjson, - TextPatcher: TextPatcher, logFights: true, fights: [], - Cryptpad: Cryptpad, 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) { return !(el && typeof(el.getAttribute) === 'function' && el.getAttribute('class') && @@ -114,10 +84,6 @@ define([ return hj; }; - var onConnectError = function () { - Cryptpad.errorLoadingScreen(Messages.websocketError); - }; - var domFromHTML = function (html) { return new DOMParser().parseFromString(html, 'text/html'); }; @@ -272,38 +238,53 @@ define([ }; }; - //////////////////////////////////////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////////////// - var andThen = function (editor, Ckeditor, common) { - //var $iframe = $('#pad-iframe').contents(); - //var secret = Cryptpad.getSecrets(); - //var readOnly = secret.keys && !secret.keys.editKeyStr; - //if (!secret.keys) { - // secret.keys = secret.key; - //} - var readOnly = false; // TODO - var cpNfInner; - var metadataMgr; - var onLocal; + var addToolbarHideBtn = function (framework, $bar) { + // Expand / collapse the toolbar + var $collapse = framework._.sfCommon.createButton(null, true); + $collapse.removeClass('fa-question'); + var updateIcon = function (isVisible) { + $collapse.removeClass('fa-caret-down').removeClass('fa-caret-up'); + if (!isVisible) { + framework.feedback('HIDETOOLBAR_PAD'); + $collapse.addClass('fa-caret-down'); + } + 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 $html = $bar.closest('html'); var $faLink = $html.find('head link[href*="/bower_components/components-font-awesome/css/font-awesome.min.css"]'); if ($faLink.length) { $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.$; [ml, ml.parentElement].forEach(function (el) { @@ -326,27 +307,15 @@ define([ if (href) { ifrWindow.open(bounceHref, '_blank'); } }; - var setEditable = module.setEditable = function (bool) { - if (bool) { - $(inner).css({ - color: '#333', - }); - } - 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; - //var UserList; - //var Metadata; + if (!framework.isReadOnly()) { + framework.onEditableChange(function () { + var locked = framework.isLocked(); + $(inner).css({ 'background-color': ((locked) ? '#aaa' : '') }); + inner.setAttribute('contenteditable', !locked); + }); + } - var getHeadingText = function () { + framework.setTitleRecommender(function () { var text; if (['h1', 'h2', 'h3'].some(function (t) { var $header = $(inner).find(t + ':first-of-type'); @@ -355,273 +324,38 @@ define([ return true; } })) { 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! - var applyHjson = function (shjson) { - var userDocStateDom = hjsonToDom(JSON.parse(shjson)); + framework.onContentUpdate(function (hjson) { + 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); (DD).apply(inner, patch); - if (readOnly) { + if (framework.isReadOnly()) { var $links = $(inner).find('a'); // off so that we don't end up with multiple identical handlers $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) { - readOnly = metadataMgr.getPrivateData().readOnly; - 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(); - if (!readOnly) { - // Expand / collapse the toolbar - 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 { - $('.cke_toolbox_main').hide(); - } - - /* add a history button */ - 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); - } + framework.setContentGetter(function () { + return Hyperjson.fromDOM(inner, isNotMagicLine, brFilter); + }); - /* add a forget button */ - var forgetCb = function (err) { - if (err) { return; } - setEditable(false); - }; - var $forgetPad = common.createButton('forget', true, {}, forgetCb); - $rightside.append($forgetPad); - }; + $bar.find('#cke_1_toolbar_collapser').hide(); + if (!framework.isReadOnly()) { + addToolbarHideBtn(framework, $bar); + } else { + $('.cke_toolbox_main').hide(); + } - // this should only ever get called once, when the chain syncs - realtimeOptions.onReady = function (info) { - console.log('onReady'); + framework.onReady(function (newPad) { if (!module.isMaximized) { module.isMaximized = true; $('iframe.cke_wysiwyg_frame').css('width', ''); @@ -629,110 +363,40 @@ define([ } $('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(); if (newPad) { + documentBody.innerHTML = Messages.initialState; cursor.setToEnd(); - } else { + } else if (framework.isReadOnly()) { 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"); - } - }; + framework.onDefaultContentNeeded(function () { + documentBody.innerHTML = Messages.initialState; + }); - cpNfInner = common.startRealtime(realtimeOptions); - metadataMgr = cpNfInner.metadataMgr; + framework.setFileImporter('text/html', function (content) { + return Hyperjson.fromDOM(domFromHTML(content).body); + }); - cpNfInner.onInfiniteSpinner(function () { - setEditable(false); - 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 of the
instead of the

. This makes it such that you @@ -743,7 +407,7 @@ define([ the first such keypress will not be inserted into the P. */ inner.addEventListener('keydown', cursor.brFix); - editor.on('change', onLocal); + editor.on('change', framework.localChange); // export the typing tests to the window. // call like `test = easyTest()` @@ -751,8 +415,8 @@ define([ window.easyTest = function () { cursor.update(); var start = cursor.Range.start; - var test = TypingTest.testInput(inner, start.el, start.offset, onLocal); - onLocal(); + var test = TypingTest.testInput(inner, start.el, start.offset, framework.localChange); + framework.localChange(); return test; }; @@ -765,25 +429,24 @@ define([ var id = classes[0]; if (typeof(id) === 'string') { - common.feedback(id.toUpperCase()); + framework.feedback(id.toUpperCase()); } }); + + framework.start(); }; var main = function () { var Ckeditor; var editor; - var common; + var framework; nThen(function (waitFor) { ckEditorAvailable(waitFor(function (ck) { Ckeditor = ck; require(['/pad/wysiwygarea-plugin.js'], waitFor()); })); - $(waitFor(function () { - Cryptpad.addLoadingScreen(); - })); - SFCommon.create(waitFor(function (c) { module.common = common = c; })); + $(waitFor()); }).nThen(function (waitFor) { Ckeditor.config.toolbarCanCollapse = true; if (screen.height < 800) { @@ -798,21 +461,11 @@ define([ customConfig: '/customize/ckeditor-config.js', }); editor.on('instanceReady', waitFor()); - }).nThen(function (/*waitFor*/) { - /*if (Ckeditor.env.safari) { - var fixIframe = function () { - $('iframe.cke_wysiwyg_frame').height($('#cke_1_contents').height()); - }; - $(window).resize(fixIframe); - fixIframe(); - }*/ + }).nThen(function (waitFor) { + Framework.create({}, waitFor(function (fw) { window.APP.framework = framework = fw; })); Links.addSupportForOpeningLinksInNewTab(Ckeditor)({editor: editor}); - Cryptpad.onError(function (info) { - if (info && info.type === "store") { - onConnectError(); - } - }); - andThen(editor, Ckeditor, common); + }).nThen(function (/*waitFor*/) { + andThen2(editor, Ckeditor, framework); }); }; main();