define([ 'jquery', '/bower_components/hyperjson/hyperjson.js', '/common/toolbar3.js', 'json.sortify', '/bower_components/nthen/index.js', '/common/sframe-common.js', '/customize/messages.js', '/common/common-util.js', '/common/common-interface.js', '/common/common-thumbnail.js', '/common/common-feedback.js', '/customize/application_config.js', '/bower_components/chainpad/chainpad.dist.js', '/bower_components/file-saver/FileSaver.min.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, JSONSortify, nThen, SFCommon, Messages, Util, UI, Thumb, Feedback, AppConfig, ChainPad) { 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 badStateTimeout = typeof(AppConfig.badStateTimeout) === 'number' ? AppConfig.badStateTimeout : 30000; var create = function (options, cb) { var evContentUpdate = Util.mkEvent(); var evEditableStateChange = Util.mkEvent(); var evOnReady = Util.mkEvent(true); var evOnDefaultContentNeeded = Util.mkEvent(); var evStart = Util.mkEvent(true); var mediaTagEmbedder; var $embedButton; var common; var cpNfInner; var readOnly; var title; var toolbar; var state = STATE.DISCONNECTED; var firstConnection = true; var toolbarContainer = options.toolbarContainer || (function () { throw new Error("toolbarContainer must be specified"); }()); var contentContainer = options.contentContainer || (function () { throw new Error("contentContainer must be specified"); }()); var titleRecommender = function () { return false; }; var contentGetter = function () { return UNINITIALIZED; }; var normalize0 = function (x) { return x; }; var normalize = function (x) { x = normalize0(x); if (Array.isArray(x)) { var outa = Array.prototype.slice.call(x); if (typeof(outa[outa.length-1].metadata) === 'object') { outa.pop(); } return outa; } else if (typeof(x) === 'object') { var outo = $.extend({}, x); delete outo.metadata; return outo; } }; var extractMetadata = function (content) { if (Array.isArray(content)) { var m = content[content.length - 1]; if (typeof(m.metadata) === 'object') { // pad return m.metadata; } } else if (typeof(content.metadata) === 'object') { return content.metadata; } return; }; var stateChange = function (newState) { var wasEditable = (state === STATE.READY); if (state === STATE.INFINITE_SPINNER && newState !== STATE.READY) { 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 () { if (firstConnection) { toolbar.initializing(); return; } toolbar.reconnecting(); }); break; } case STATE.INFINITE_SPINNER: { evStart.reg(function () { toolbar.failed(); }); break; } case STATE.FORGOTTEN: { evStart.reg(function () { toolbar.forgotten(); }); break; } default: } if (wasEditable !== (state === STATE.READY)) { evEditableStateChange.fire(state === STATE.READY); } }; var contentUpdate = function (newContent) { try { evContentUpdate.fire(newContent); } catch (e) { console.log(e.stack); UI.errorLoadingScreen(e.message); } }; var onLocal; var onRemote = function () { if (state !== STATE.READY) { return; } var oldContent = normalize(contentGetter()); var newContentStr = cpNfInner.chainpad.getUserDoc(); var newContent = JSON.parse(newContentStr); var meta = extractMetadata(newContent); cpNfInner.metadataMgr.updateMetadata(meta); newContent = normalize(newContent); contentUpdate(newContent); if (!readOnly) { var newContent2NoMeta = normalize(contentGetter()); var newContent2StrNoMeta = JSONSortify(newContent2NoMeta); var newContentStrNoMeta = JSONSortify(newContent); if (newContent2StrNoMeta !== newContentStrNoMeta) { console.error("shjson2 !== shjson"); onLocal(); /* pushing back over the wire is necessary, but it can result in a feedback loop, which we call a browser fight */ // what changed? var ops = ChainPad.Diff.diff(newContentStrNoMeta, newContent2StrNoMeta); // log the changes console.log(newContentStrNoMeta); console.log(ops); var sop = JSON.stringify([ newContentStrNoMeta, ops ]); var fights = window.CryptPad_fights = window.CryptPad_fights || []; var index = fights.indexOf(sop); if (index === -1) { 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(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(); } }; 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); cpNfInner.chainpad.contentUpdate(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); cpNfInner.metadataMgr.updateMetadata(extractMetadata(newContent)); newContent = normalize(newContent); contentUpdate(newContent); } else { if (!cpNfInner.metadataMgr.getPrivateData().isNewFile) { // We're getting 'new pad' but there is an existing file // We don't know exactly why this can happen but under no circumstances // should we overwrite the content, so lets just try again. common.gotoURL(); return; } console.log('updating title'); title.updateTitle(title.defaultTitle); evOnDefaultContentNeeded.fire(); } stateChange(STATE.READY); firstConnection = false; if (!readOnly) { onLocal(); } evOnReady.fire(newPad); UI.removeLoadingScreen(emitResize); var privateDat = cpNfInner.metadataMgr.getPrivateData(); if (options.thumbnail && privateDat.thumbnails) { var hash = privateDat.availableHashes.editHash || privateDat.availableHashes.viewHash; if (hash) { options.thumbnail.href = privateDat.pathname + '#' + hash; options.thumbnail.getContent = function () { if (!cpNfInner.chainpad) { return; } return cpNfInner.chainpad.getUserDoc(); }; Thumb.initPadThumbnails(common, options.thumbnail); } } if (newPad && !AppConfig.displayCreationScreen) { common.openTemplatePicker(); } }; var onConnectionChange = function (info) { stateChange(info.state ? STATE.INITIALIZING : STATE.DISCONNECTED); if (info.state) { UI.findOKButton().click(); } else { UI.alert(Messages.common_connectionLost, undefined, true); } }; var setFileExporter = function (extension, fe, async) { var $export = common.createButton('export', true, {}, function () { var ext = (typeof(extension) === 'function') ? extension() : extension; var suggestion = title.suggestTitle('cryptpad-document'); UI.prompt(Messages.exportPrompt, Util.fixFileName(suggestion) + '.' + ext, function (filename) { if (!(typeof(filename) === 'string' && filename)) { return; } if (async) { fe(function (blob) { SaveAs(blob, filename); }); return; } var blob = fe(); SaveAs(blob, filename); }); }); toolbar.$drawer.append($export); }; var setFileImporter = function (options, fi, async) { if (readOnly) { return; } toolbar.$drawer.append( common.createButton('import', true, options, function (c, f) { if (async) { fi(c, f, function (content) { contentUpdate(content); onLocal(); }); return; } contentUpdate(fi(c, f)); onLocal(); }) ); }; var feedback = function (action, force) { if (state === STATE.DISCONNECTED || state === STATE.INITIALIZING) { return; } Feedback.send(action, force); }; var createFilePicker = function () { if (!common.isLoggedIn()) { return; } common.initFilePicker({ onSelect: function (data) { if (data.type !== 'file') { console.log("Unexpected data type picked " + data.type); return; } if (!mediaTagEmbedder) { console.log('mediaTagEmbedder missing'); return; } if (data.type !== 'file') { console.log('unhandled embed type ' + data.type); return; } var privateDat = cpNfInner.metadataMgr.getPrivateData(); var origin = privateDat.fileHost || privateDat.origin; var src = origin + data.src; mediaTagEmbedder($('')); } }); $embedButton = $('