require.config({ paths: { 'json.sortify': '/bower_components/json.sortify/dist/JSON.sortify'} }); define([ '/api/config?cb=' + Math.random().toString(16).substring(2), '/customize/messages.js?pad=slide', '/bower_components/chainpad-netflux/chainpad-netflux.js', '/bower_components/chainpad-crypto/crypto.js', '/bower_components/textpatcher/TextPatcher.amd.js', '/common/cryptpad-common.js', '/slide/slide.js', '/common/notify.js', '/common/visible.js', '/common/clipboard.js', 'json.sortify', '/bower_components/file-saver/FileSaver.min.js', '/bower_components/jquery/dist/jquery.min.js', '/customize/pad.js' ], function (Config, Messages, Realtime, Crypto, TextPatcher, Cryptpad, Slide, Notify, Visible, Clipboard, JSONSortify) { var $ = window.jQuery; var saveAs = window.saveAs; /* TODO * patch in changes using DiffDOM * predraw some things in case they use external assets * strip out script tags? * better CSS * use codemirror instead of a text editor * add ability to link to a rendered slide * ui hint for escaping presentation mode */ Cryptpad.styleAlerts(); var secret = Cryptpad.getSecrets(); var readOnly = secret.keys && !secret.keys.editKeyStr; Slide.readOnly = readOnly; if (!secret.keys) { secret.keys = secret.key; } var APP = window.APP = { TextPatcher: TextPatcher, Slide: Slide, }; var Stringify = APP.Stringify = JSONSortify; var initializing = true; var $textarea = $('textarea'); var suggestName = function () { var title = ''; var patt = /^#\s+(.*)\s*$/; $textarea.val().split("\n").some(function (line) { if (!patt.test(line)) { return; } line.replace(patt, function (a, b) { title = b; }); return true; }); return title; }; var unnotify = function () { if (!(APP.tabNofification && typeof(APP.tabNofification.cancel) === 'function')) { return; } APP.tabNofification.cancel(); }; var notify = function () { if (!(Visible.isSupported() && !Visible.currently())) { return; } unnotify(); APP.tabNofification = Notify.tab(1000, 10); }; var $modal = $('#modal'); var $content = $('#content'); Slide.setModal($modal, $content); var enterPresentationMode = function (shouldLog) { Slide.show(true, $textarea.val()); if (shouldLog) { Cryptpad.log(Messages.presentSuccess); } }; if (readOnly) { enterPresentationMode(false); } var config = APP.config = { initialState: '{}', websocketURL: Config.websocketURL, channel: secret.channel, crypto: Crypto.createEncryptor(secret.keys), validateKey: secret.keys.validateKey || undefined, readOnly: readOnly, }; var setEditable = function (bool) { if (readOnly && bool) { return; } $textarea.attr('disabled', !bool); }; var canonicalize = function (text) { return text.replace(/\r\n/g, '\n'); }; setEditable(false); var safelyParseContent = function (S, k, first) { if (!first) { return JSON.parse(S); } try { return JSON.parse(S); } catch (err) { console.log("Migrating text content to object form"); var O = {}; O[k] = S; return O; } }; var getUserObj = function (rt) { return safelyParseContent(rt.getUserDoc(), 'content'); }; var onLocal = config.onLocal = function () { if (initializing) { return; } if (readOnly) { return; } var textContent = canonicalize($textarea.val()); var userObj = getUserObj(APP.realtime); userObj.content = textContent; var content = Stringify(userObj); APP.patchText(content); Slide.update(textContent); }; var Button = function (opt) { return $('<button>', opt); }; var onInit = config.onInit = function (info) { var editHash; var viewHash = Cryptpad.getViewHashFromKeys(info.channel, secret.keys); if (!readOnly) { editHash = Cryptpad.getEditHashFromKeys(info.channel, secret.keys); window.location.hash = editHash; } $(window).on('hashchange', function() { window.location.reload(); }); Cryptpad.getPadTitle(function (err, title) { if (err) { console.error(err); console.log("Couldn't get pad title"); return; } document.title = APP.title = title || info.channel.slice(0, 8); Cryptpad.rememberPad(title, function (err, data) { if (err) { console.log("Couldn't remember pad"); console.error(err); } }); }); var $bar = $('#bar'); var $present = Button({ id: 'present', 'class': 'present button action', title: Messages.presentButtonTitle, }) .text(Messages.presentButton) .click(function () { enterPresentationMode(true); }); var $forget = Button({ id: 'forget', 'class': 'forget button action', title: Messages.forgetButtonTitle, }) .text(Messages.forgetButton) .click(function () { var href = window.location.href; Cryptpad.confirm(Messages.forgetPrompt, function (yes) { if (!yes) { return; } Cryptpad.forgetPad(href, function (err) { if (err) { console.log("unable to forget pad"); console.log(err); return; } var parsed = Cryptpad.parsePadUrl(href); document.title = APP.title = Cryptpad.getDefaultName(parsed, []); }); }); }); var $rename = Button({ id: 'rename', 'class': 'rename button action', title: Messages.renameButtonTitle, }) .text(Messages.renameButton) .click(function () { var suggestion = suggestName(); Cryptpad.prompt(Messages.renamePrompt, suggestion, function (title, ev) { if (title === null) { return; } Cryptpad.causesNamingConflict(title, function (err, conflicts) { if (conflicts) { Cryptpad.alert(Messages.renameConflict); return; } Cryptpad.setPadTitle(title, function (err) { if (err) { console.log("unable to set pad title"); console.error(err); return; } document.title = APP.title = title; }); }); }); }); var $import = Button({ id: 'import', 'class': 'import button action', title: Messages.importButtonTitle, }) .text(Messages.importButton) .click(Cryptpad.importContent('text/plain', function (content, file) { $textarea.val(content); onLocal(); })); var $export = Button({ id: 'export', 'class': 'export button action', title: Messages.exportButtonTitle, }) .text(Messages.exportButton) .click(function () { var text = $textarea.val(); var title = Cryptpad.fixFileName(suggestName()) + '.txt'; Cryptpad.prompt(Messages.exportPrompt, title, function (filename) { if (filename === null) { return; } var blob = new Blob([text], { type: 'text/plain;charset=utf-8', }); saveAs(blob, filename); }); }); var $share = Button({ id: 'share', 'class': 'share button action', title: Messages.shareButtonTitle, }) .text(Messages.shareButton) .click(function () { var text = window.location.href; var success = Clipboard.copy(text); if (success) { Cryptpad.log(Messages.shareSuccess); return; } Cryptpad.warn(Messages.shareFailed); }); /* add a 'links' button */ var $links = Button({ title: Messages.getViewButtonTitle, 'class': 'links button action', }) .text(Messages.getViewButton) .click(function () { var baseUrl = window.location.origin + window.location.pathname + '#'; var content = '<b>' + Messages.readonlyUrl + '</b><br><a>' + baseUrl + viewHash + '</a><br>'; Cryptpad.alert(content); }); if (readOnly) { $links = ''; $import = ''; $present = ''; } if (!viewHash) { $links = ''; } $bar .append($present) .append($forget) .append($rename) .append($import) .append($export) .append($share) .append($links); }; var onRemote = config.onRemote = function (info) { if (initializing) { return; } var userObj = getUserObj(APP.realtime); var userDoc = userObj.content; var content = canonicalize($textarea.val()); var op = TextPatcher.diff(content, userDoc); var elem = $textarea[0]; var selects = ['selectionStart', 'selectionEnd'].map(function (attr) { return TextPatcher.transformCursor(elem[attr], op); }); $textarea.val(userDoc); elem.selectionStart = selects[0]; elem.selectionEnd = selects[1]; Slide.update(userDoc); notify(); }; var onReady = config.onReady = function (info) { var realtime = APP.realtime = info.realtime; APP.patchText = TextPatcher.create({ realtime: realtime }); var userObj = getUserObj(APP.realtime); var content = canonicalize(userObj.content || ''); $textarea.val(content); Slide.update(content); if (Visible.isSupported()) { Visible.onChange(function (yes) { if (yes) { unnotify(); } }); } Slide.onChange(function (o, n, l) { if (n !== null) { document.title = APP.title + ' (' + (++n) + '/' + l + ')'; return; } console.log("Exiting presentation mode"); document.title = APP.title; }); setEditable(true); initializing = false; }; var onAbort = config.onAbort = function (info) { $textarea.attr('disabled', true); Cryptpad.alert(Messages.common_connectionLost); }; Cryptpad.ready(function () { var rt = Realtime.start(config); }); ['cut', 'paste', 'change', 'keyup', 'keydown', 'select', 'textInput'] .forEach(function (evt) { $textarea.on(evt, onLocal); }); });