diff --git a/bower.json b/bower.json index c7100f505..c92757dea 100644 --- a/bower.json +++ b/bower.json @@ -41,6 +41,7 @@ "diff-dom": "#gh-pages", "alertifyjs": "^1.0.11", "spin.js": "^2.3.2", - "scrypt-async": "^1.2.0" + "scrypt-async": "^1.2.0", + "bootstrap": "^3.3.7" } } diff --git a/customize.dist/DecorateToolbar.js b/customize.dist/DecorateToolbar.js index 49a30c79f..b0d5e967b 100644 --- a/customize.dist/DecorateToolbar.js +++ b/customize.dist/DecorateToolbar.js @@ -10,7 +10,7 @@ define([ var main = function () { var url = window.location.pathname; var isHtml = /\.html/.test(url) || url === '/' || url === ''; - var isPoll = /\/poll\//.test(url); + var isPoll = /\/poll\//.test(url) || /\/file\//.test(url); if (!isHtml && !isPoll) { Messages._applyTranslation(); return; diff --git a/customize.dist/fsStore.js b/customize.dist/fsStore.js new file mode 100644 index 000000000..215c515ab --- /dev/null +++ b/customize.dist/fsStore.js @@ -0,0 +1,187 @@ +define([ + '/api/config?cb=' + Math.random().toString().slice(2), + '/customize/messages.js?app=fs', + '/bower_components/chainpad-listmap/chainpad-listmap.js', + '/bower_components/chainpad-crypto/crypto.js', + '/bower_components/textpatcher/TextPatcher.amd.js', + '/file/fileObject.js' +], function (Config, Messages, Listmap, Crypto, TextPatcher, FO) { + /* + This module uses localStorage, which is synchronous, but exposes an + asyncronous API. This is so that we can substitute other storage + methods. + + To override these methods, create another file at: + /customize/storage.js + */ + + var Store = {}; + var storeObj; + var ready = false; + var filesOp; + + var safeSet = function (key, val) { + storeObj[key] = val; + }; + + // Store uses nodebacks... + Store.set = function (key, val, cb) { + safeSet(key, val); + cb(); + }; + + // implement in alternative store + Store.setBatch = function (map, cb) { + Object.keys(map).forEach(function (key) { + safeSet(key, val); + }); + cb(void 0, map); + }; + + var safeGet = window.safeGet = function (key) { + return storeObj[key]; + }; + + Store.get = function (key, cb) { + cb(void 0, safeGet(key)); + }; + + // implement in alternative store + Store.getBatch = function (keys, cb) { + var res = {}; + keys.forEach(function (key) { + res[key] = safeGet(key); + }); + cb(void 0, res); + }; + + var safeRemove = function (key) { + delete storeObj[key]; + }; + + Store.remove = function (key, cb) { + safeRemove(key); + cb(); + }; + + // implement in alternative store + Store.removeBatch = function (keys, cb) { + keys.forEach(function (key) { + safeRemove(key); + }); + cb(); + }; + + Store.keys = function (cb) { + cb(void 0, Object.keys(storeObj)); + }; + + Store.addPad = function (href, path, name) { + filesOp.addPad(href, path, name); + }; + + Store.forgetPad = function (href, cb) { + filesOp.forgetPad(href); + cb(); + }; + + var changeHandlers = Store.changeHandlers = []; + + Store.change = function (f) { + if (typeof(f) !== 'function') { + throw new Error('[Store.change] callback must be a function'); + } + changeHandlers.push(f); + + if (changeHandlers.length === 1) { + // start listening for changes +/* TODO: listen for changes in the proxy + window.addEventListener('storage', function (e) { + changeHandlers.forEach(function (f) { + f({ + key: e.key, + oldValue: e.oldValue, + newValue: e.newValue, + }); + }); + }); +*/ + } + }; + + var onReady = function (f, proxy, storageKey) { + filesOp = FO.init(proxy, { + storageKey: storageKey + }); + storeObj = proxy; + ready = true; + if (typeof(f) === 'function') { + f(void 0, Store); + } + }; + + var init = function (f, Cryptpad) { + if (!Cryptpad) { return; } + var hash = localStorage.FS_hash; + var secret = Cryptpad.getSecrets(hash); + var listmapConfig = { + data: {}, + websocketURL: Cryptpad.getWebsocketURL(), + channel: secret.channel, + readOnly: false, + validateKey: secret.keys.validateKey || undefined, + crypto: Crypto.createEncryptor(secret.keys), + }; + + var rt = window.rt = Listmap.create(listmapConfig); + rt.proxy.on('create', function (info) { + var realtime = info.realtime; + localStorage.FS_hash = Cryptpad.getEditHashFromKeys(info.channel, secret.keys); + window.patchText = TextPatcher.create({ + realtime: realtime, + logging: true, + }); + + }).on('ready', function () { + if (JSON.stringify(rt.proxy) === '{}') { + var oldStore = Cryptpad.getStore(true); + oldStore.get(Cryptpad.storageKey, function (err, s) { + rt.proxy.filesData = s; + onReady(f, rt.proxy, Cryptpad.storageKey); + }); + return; + } + onReady(f, rt.proxy, Cryptpad.storageKey); + }) + .on('disconnect', function (info) { + //setEditable(false); + if (info.error) { + //Cryptpad.alert(Messages.websocketError); + if (typeof Cryptpad.storeError === "function") { + Cryptpad.storeError(); + } + return; + } + Cryptpad.alert(Messages.common_connectionLost); + }); + + }; + + Store.ready = function (f, Cryptpad) { + if (Cryptpad.parsePadUrl(window.location.href).type === "file") { + if (typeof(f) === 'function') { + f(void 0, Cryptpad.getStore(true)); + } + return; + } + if (ready) { + if (typeof(f) === 'function') { + f(void 0, Store); + } + } else { + init(f, Cryptpad); + } + }; + + return Store; +}); diff --git a/customize.dist/main.css b/customize.dist/main.css index 2455aa465..1d3e3c347 100644 --- a/customize.dist/main.css +++ b/customize.dist/main.css @@ -318,47 +318,68 @@ tbody td:last-child { color: #FF0073; cursor: pointer !important; } -form.realtime { +form.realtime, +div.realtime { padding: 0px; margin: 0px; } -form.realtime > textarea { +form.realtime > textarea, +div.realtime > textarea { width: 50%; height: 15vh; } -form.realtime table { +form.realtime table, +div.realtime table { border-collapse: collapse; + width: calc(100% - 1px); } -form.realtime table tr td { +form.realtime table tr td:first-child, +div.realtime table tr td:first-child { + position: absolute; + left: 29px; + top: auto; + width: calc(30% - 50px); +} +form.realtime table tr td, +div.realtime table tr td { padding: 0px; margin: 0px; } -form.realtime table tr td div.text-cell { +form.realtime table tr td div.text-cell, +div.realtime table tr td div.text-cell { padding: 0px; margin: 0px; height: 100%; } -form.realtime table tr td div.text-cell input { +form.realtime table tr td div.text-cell input, +div.realtime table tr td div.text-cell input { width: 80%; + width: 90%; height: 100%; border: 0px; } -form.realtime table tr td div.text-cell input[disabled] { +form.realtime table tr td div.text-cell input[disabled], +div.realtime table tr td div.text-cell input[disabled] { background-color: transparent; color: #fafafa; font-weight: bold; } -form.realtime table tr td.checkbox-cell { +form.realtime table tr td.checkbox-cell, +div.realtime table tr td.checkbox-cell { margin: 0px; padding: 0px; + height: 100%; + min-width: 150px; } -form.realtime table tr td.checkbox-cell div.checkbox-contain { - display: block; +form.realtime table tr td.checkbox-cell div.checkbox-contain, +div.realtime table tr td.checkbox-cell div.checkbox-contain { + display: inline-block; height: 100%; width: 100%; position: relative; } -form.realtime table tr td.checkbox-cell div.checkbox-contain label { +form.realtime table tr td.checkbox-cell div.checkbox-contain label, +div.realtime table tr td.checkbox-cell div.checkbox-contain label { background-color: transparent; display: block; position: absolute; @@ -367,72 +388,138 @@ form.realtime table tr td.checkbox-cell div.checkbox-contain label { height: 100%; width: 100%; } -form.realtime table tr td.checkbox-cell div.checkbox-contain input[type="checkbox"]:not(.editable) { +form.realtime table tr td.checkbox-cell div.checkbox-contain input[type="checkbox"]:not(.editable), +div.realtime table tr td.checkbox-cell div.checkbox-contain input[type="checkbox"]:not(.editable) { display: none; } -form.realtime table tr td.checkbox-cell div.checkbox-contain input[type="checkbox"]:not(.editable) ~ .cover { +form.realtime table tr td.checkbox-cell div.checkbox-contain input[type="checkbox"]:not(.editable) ~ .cover, +div.realtime table tr td.checkbox-cell div.checkbox-contain input[type="checkbox"]:not(.editable) ~ .cover { font-weight: bold; background-color: #FF0073; color: #302B28; display: block; } -form.realtime table tr td.checkbox-cell div.checkbox-contain input[type="checkbox"]:not(.editable) ~ .cover:after { +form.realtime table tr td.checkbox-cell div.checkbox-contain input[type="checkbox"]:not(.editable) ~ .cover:after, +div.realtime table tr td.checkbox-cell div.checkbox-contain input[type="checkbox"]:not(.editable) ~ .cover:after { + height: 100%; +} +form.realtime table tr td.checkbox-cell div.checkbox-contain input[type="checkbox"]:not(.editable) ~ .cover:after, +div.realtime table tr td.checkbox-cell div.checkbox-contain input[type="checkbox"]:not(.editable) ~ .cover:after { content: "✖"; } -form.realtime table tr td.checkbox-cell div.checkbox-contain input[type="checkbox"]:not(.editable) ~ .cover.yes { +form.realtime table tr td.checkbox-cell div.checkbox-contain input[type="checkbox"]:not(.editable) ~ .cover.yes, +div.realtime table tr td.checkbox-cell div.checkbox-contain input[type="checkbox"]:not(.editable) ~ .cover.yes { background-color: #46E981; } -form.realtime table tr td.checkbox-cell div.checkbox-contain input[type="checkbox"]:not(.editable) ~ .cover.yes:after { +form.realtime table tr td.checkbox-cell div.checkbox-contain input[type="checkbox"]:not(.editable) ~ .cover.yes:after, +div.realtime table tr td.checkbox-cell div.checkbox-contain input[type="checkbox"]:not(.editable) ~ .cover.yes:after { content: "✔"; } -form.realtime table tr td.checkbox-cell div.checkbox-contain input[type="checkbox"]:not(.editable) ~ .cover.mine { +form.realtime table tr td.checkbox-cell div.checkbox-contain input[type="checkbox"]:not(.editable) ~ .cover.uncommitted, +div.realtime table tr td.checkbox-cell div.checkbox-contain input[type="checkbox"]:not(.editable) ~ .cover.uncommitted { + background: #ddd; +} +form.realtime table tr td.checkbox-cell div.checkbox-contain input[type="checkbox"]:not(.editable) ~ .cover.mine, +div.realtime table tr td.checkbox-cell div.checkbox-contain input[type="checkbox"]:not(.editable) ~ .cover.mine { display: none; } -form.realtime table input[type="text"] { +form.realtime table input[type="text"], +div.realtime table input[type="text"] { height: 100%; + border: 1px solid #302B28; width: 80%; - border: 3px solid #302B28; } -form.realtime table .edit { +form.realtime table thead td, +div.realtime table thead td { + padding: 0px 5px; + background: #4c443f; + border-radius: 20px 20px 0 0; + text-align: center; +} +form.realtime table thead td input[type="text"], +div.realtime table thead td input[type="text"] { + width: 100%; + box-sizing: border-box; +} +form.realtime table thead td input[type="text"][disabled], +div.realtime table thead td input[type="text"][disabled] { + color: white; + padding: 1px 5px; + border: none; +} +form.realtime table tbody .text-cell, +div.realtime table tbody .text-cell { + background: #4c443f; +} +form.realtime table tbody .text-cell input[type="text"], +div.realtime table tbody .text-cell input[type="text"] { + width: calc(100% - 50px); +} +form.realtime table tbody .text-cell .edit, +div.realtime table tbody .text-cell .edit { + float: right; + margin: 0 10px 0 0; +} +form.realtime table tbody .text-cell .remove, +div.realtime table tbody .text-cell .remove { + float: left; + margin: 0 0 0 10px; +} +form.realtime table .edit, +div.realtime table .edit { color: #46E981; cursor: pointer; - width: 10%; - font-size: 20px; + float: left; + margin-left: 10px; + /*&:after { content: '✐'; }*/ + /*&.editable { display: none; }*/ } -form.realtime table .edit:after { - content: '✐'; +form.realtime table .remove, +div.realtime table .remove { + float: right; + margin-right: 10px; } -form.realtime table .edit.editable { - display: none; -} -form.realtime table thead tr th input[type="text"][disabled] { +form.realtime table thead tr th input[type="text"][disabled], +div.realtime table thead tr th input[type="text"][disabled] { background-color: transparent; color: #fafafa; font-weight: bold; } -form.realtime table thead tr th .remove { +form.realtime table thead tr th .remove, +div.realtime table thead tr th .remove { cursor: pointer; font-size: 20px; } -form.realtime table tfoot tr td { +form.realtime table tfoot tr, +div.realtime table tfoot tr { + border: none; +} +form.realtime table tfoot tr td, +div.realtime table tfoot tr td { + border: none; text-align: center; } -form.realtime table tfoot tr td .save { +form.realtime table tfoot tr td .save, +div.realtime table tfoot tr td .save { padding: 15px; border-top-left-radius: 5px; border-top-right-radius: 5px; } form.realtime #adduser, -form.realtime #addoption { +div.realtime #adduser, +form.realtime #addoption, +div.realtime #addoption { color: #46E981; border: 1px solid #46E981; padding: 15px; cursor: pointer; } -form.realtime #adduser { +form.realtime #adduser, +div.realtime #adduser { border-top-left-radius: 5px; } -form.realtime #addoption { +form.realtime #addoption, +div.realtime #addoption { border-bottom-left-radius: 5px; } div.modal, diff --git a/customize.dist/messages.js b/customize.dist/messages.js index fdd11d3d6..8cb4e1918 100644 --- a/customize.dist/messages.js +++ b/customize.dist/messages.js @@ -69,17 +69,21 @@ define(['/customize/languageSelector.js', }); }; + var translateText = function (i, e) { + var $el = $(e); + var key = $el.data('localization'); + $el.html(messages[key]); + }; + var translateTitle = function (i, e) { + var $el = $(this); + var key = $el.data('localization-title'); + $el.attr('title', messages[key]); + }; messages._applyTranslation = function () { - $('[data-localization]').each(function (i, e) { - var $el = $(this); - var key = $el.data('localization'); - $el.html(messages[key]); - }); - $('[data-localization-title]').each(function (i, e) { - var $el = $(this); - var key = $el.data('localization-title'); - $el.attr('title', messages[key]); - }); + $('[data-localization]').each(translateText); + $('#pad-iframe').contents().find('[data-localization]').each(translateText); + $('[data-localization-title]').each(translateTitle); + $('#pad-iframe').contents().find('[data-localization-title]').each(translateTitle); }; // Non translatable keys diff --git a/customize.dist/src/cryptpad.less b/customize.dist/src/cryptpad.less index 7492114e9..494e7c369 100644 --- a/customize.dist/src/cryptpad.less +++ b/customize.dist/src/cryptpad.less @@ -359,7 +359,7 @@ tbody { cursor: pointer !important; } -form.realtime { +form.realtime, div.realtime { > input { &[type="text"] { @@ -375,7 +375,14 @@ form.realtime { table { border-collapse: collapse; + width: ~"calc(100% - 1px)"; tr { + td:first-child { + position:absolute; + left: 29px; + top: auto; + width: ~"calc(30% - 50px)"; + } td { padding: 0px; margin: 0px; @@ -383,9 +390,11 @@ form.realtime { div.text-cell { padding: 0px; margin: 0px; - height: 100%; + height: 100%; + input { width: 80%; + width: 90%; height: 100%; border: 0px; &[disabled] { @@ -399,9 +408,11 @@ form.realtime { &.checkbox-cell { margin: 0px; padding: 0px; + height: 100%; + min-width: 150px; div.checkbox-contain { - display: block; + display: inline-block; height: 100%; width: 100%; position: relative; @@ -427,6 +438,11 @@ form.realtime { background-color: @cp-red; color: @base; + + &:after { + height: 100%; + } + &:after { content: "✖"; } display: block; @@ -434,6 +450,12 @@ form.realtime { background-color: @cp-green; &:after { content: "✔"; } } + + &.uncommitted { + background: #ddd; + } + + &.mine { display: none; } @@ -449,17 +471,59 @@ form.realtime { input { &[type="text"] { height: 100%; + border: 1px solid @base; width: 80%; - border: 3px solid @base; + } + } + thead { + td { + padding: 0px 5px; + background: @less-light-base; + border-radius: 20px 20px 0 0; + text-align: center; + input { + &[type="text"] { + width: 100%; + box-sizing: border-box; + &[disabled] { + color: white; + padding: 1px 5px; + border: none; + } + } + } + } + } + + tbody { + .text-cell { + background: @less-light-base; + //border-radius: 20px 0 0 20px; + input[type="text"] { + width: ~"calc(100% - 50px)"; + } + .edit { + float:right; + margin: 0 10px 0 0; + } + .remove { + float: left; + margin: 0 0 0 10px; + } } } .edit { color: @cp-green; cursor: pointer; - width: 10%; - font-size: 20px; - &:after { content: '✐'; } - &.editable { display: none; } + float: left; + margin-left: 10px; + /*&:after { content: '✐'; }*/ + /*&.editable { display: none; }*/ + } + + .remove { + float: right; + margin-right: 10px } thead { @@ -486,7 +550,9 @@ form.realtime { } tfoot { tr { + border: none; td { + border: none; text-align: center; .save { padding: 15px; diff --git a/customize.dist/src/toolbar.less b/customize.dist/src/toolbar.less index d8f7b6a6c..c420bdae1 100644 --- a/customize.dist/src/toolbar.less +++ b/customize.dist/src/toolbar.less @@ -271,7 +271,7 @@ margin: 2px 4px 2px 0px; } .cryptpad-userbuttons-container { - display: none; + /*display: none;*/ } } .cryptpad-toolbar-rightside { diff --git a/customize.dist/src/variables.less b/customize.dist/src/variables.less index 14e6de373..c6d555219 100644 --- a/customize.dist/src/variables.less +++ b/customize.dist/src/variables.less @@ -1,5 +1,6 @@ @base: #302B28; @light-base: lighten(@base, 20%); +@less-light-base: lighten(@base, 10%); @fore: #fafafa; @cp-green: #46E981; diff --git a/customize.dist/toolbar.css b/customize.dist/toolbar.css index cfde957a3..02eeff7e7 100644 --- a/customize.dist/toolbar.css +++ b/customize.dist/toolbar.css @@ -279,7 +279,7 @@ margin: 2px 4px 2px 0px; } .cryptpad-toolbar-leftside .cryptpad-userbuttons-container { - display: none; + /*display: none;*/ } .cryptpad-toolbar-rightside { text-align: right; diff --git a/customize.dist/translations/messages.es.js b/customize.dist/translations/messages.es.js index a7b9740b9..0b6de909f 100644 --- a/customize.dist/translations/messages.es.js +++ b/customize.dist/translations/messages.es.js @@ -27,8 +27,8 @@ define(function () { out.readOnly = 'Solo lectura'; out.anonymous = 'Anónimo'; out.yourself = "tú mismo"; - out.anonymousUsers = "usuarios anónimos"; - out.anonymousUser = "usuario anónimo"; + out.anonymousUsers = "editores anónimos"; + out.anonymousUser = "editor anónimo"; out.shareView = "URL de sólo lectura"; out.shareEdit = "Editar URL"; out.users = "Usuarios"; diff --git a/customize.dist/translations/messages.fr.js b/customize.dist/translations/messages.fr.js index 94ca6050a..fbc2a3c25 100644 --- a/customize.dist/translations/messages.fr.js +++ b/customize.dist/translations/messages.fr.js @@ -27,8 +27,8 @@ define(function () { out.readonly = 'Lecture seule'; out.anonymous = "Anonyme"; out.yourself = "Vous-même"; - out.anonymousUsers = "utilisateurs anonymes"; - out.anonymousUser = "utilisateur anonyme"; + out.anonymousUsers = "éditeurs anonymes"; + out.anonymousUser = "éditeur anonyme"; out.shareView = "URL de lecture seule"; out.shareEdit = "URL d'édition"; out.users = "Utilisateurs"; @@ -129,6 +129,12 @@ define(function () { out.wizardTitle = "Utiliser l'assistant pour créer votre sondage"; out.wizardConfirm = "Êtes-vous vraiment prêt à ajouter ces options au sondage ?"; + out.poll_publish_button = "Publier"; + out.poll_admin_button = "Administrer"; + out.poll_create_user = "Ajouter un utilisateur"; + out.poll_create_option = "Ajouter une option"; + out.poll_commit = "Valider"; + out.poll_closeWizardButton = "Fermer l'assistant"; out.poll_closeWizardButtonTitle = "Fermer l'assistant"; out.poll_wizardComputeButton = "Générer les options"; @@ -157,6 +163,51 @@ define(function () { out.poll_titleHint = "Titre"; out.poll_descriptionHint = "Description"; + // File manager + + out.fm_rootName = "Mes documents"; + out.fm_trashName = "Corbeille"; + out.fm_unsortedName = "Fichiers non triés"; + out.fm_filesDataName = "Tous les fichiers"; + out.fm_newFolder = "Nouveau dossier"; + out.fm_newFolderButton = "NOUVEAU DOSSIER"; + out.fm_folderName = "Nom du dossier"; + out.fm_numberOfFolders = "# de dossiers"; + out.fm_numberOfFiles = "# de fichiers"; + out.fm_fileName = "Nom du fichier"; + out.fm_title = "Titre"; + out.fm_lastAccess = "Dernier accès"; + out.fm_creation = "Création"; + out.fm_forbidden = "Action interdite"; + out.fm_originalPath = "Chemin d'origine"; + out.fm_emptyTrashDialog = "Êtes-vous sûr de vouloir vider la corbeille ?"; + out.fm_removeSeveralPermanentlyDialog = "Êtes-vous sûr de vouloir supprimer ces {0} éléments de manière permanente ?"; + out.fm_removePermanentlyDialog = "Êtes-vous sûr de vouloir supprimer {0} de manière permanente ?"; + out.fm_restoreDialog = "Êtes-vous sûr de vouloir restaurer {0} à son emplacement précédent ?"; + out.fm_removeSeveralDialog = "Êtes-vous sûr de vouloir déplacer ces {0} éléments vers la corbeille ?"; + out.fm_removeDialog = "Êtes-vous sûr de vouloir déplacer {0} vers la corbeille ?"; + out.fm_unknownFolderError = "Le dossier sélectionné ou le dernier dossier visité n'existe plus. Ouverture du dossier parent..."; + out.fm_contextMenuError = "Impossible d'ouvrir le menu contextuel pour cet élément. Si le problème persiste, essayez de rechercher la page."; + out.fm_selectError = "Impossible de sélectionner l'élément ciblé. Si le problème persiste, essayez de recharger la page."; + // File - Context menu + out.fc_newfolder = "Nouveau dossier"; + out.fc_rename = "Renommer"; + out.fc_open = "Ouvrir"; + out.fc_delete = "Supprimer"; + out.fc_restore = "Restaurer"; + out.fc_remove = "Supprimer définitivement"; + out.fc_empty = "Vider la corbeille"; + out.fc_newpad = "Nouveau pad de texte"; + out.fc_newcode = "Nouveau pad de code"; + out.fc_newslide = "Nouvelle présentation"; + out.fc_newpoll = "Nouveau sondage"; + out.fc_prop = "Propriétés"; + // fileObject.js (logs) + out.fo_moveUnsortedError = "La liste des éléments non triés ne peut pas contenir de dossiers."; + out.fo_existingNameError = "Ce nom est déjà utilisé dans ce répertoire. Veuillez en choisir un autre."; + out.fo_moveFolderToChildError = "Vous ne pouvez pas déplacer un dossier dans un de ses descendants"; + out.fo_unableToRestore = "Impossible de restaurer ce fichier à son emplacement d'origine. Vous pouvez essayer de le déplacer à un nouvel emplacement."; + // index.html out.main_p1 = 'CryptPad est l\'éditeur collaboratif en temps réel zero knowledge. Le chiffrement est effectué depuis votre navigateur, ce qui protège les données contre le serveur, le cloud, et la NSA. La clé de chiffrement est stockée dans l\'identifieur de fragment de l\'URL qui n\'est jamais envoyée au serveur mais est accessible depuis javascript, de sorte qu\'en partageant l\'URL, vous donnez l\'accès au pad à ceux qui souhaitent participer.'; diff --git a/customize.dist/translations/messages.js b/customize.dist/translations/messages.js index 61880205a..81c0eb0f8 100644 --- a/customize.dist/translations/messages.js +++ b/customize.dist/translations/messages.js @@ -21,6 +21,7 @@ define(function () { ].join(''); out.common_connectionLost = 'Server Connection Lost'; + out.websocketError = 'Unable to connect to the websocket server...'; out.disconnected = 'Disconnected'; out.synchronizing = 'Synchronizing'; @@ -29,8 +30,8 @@ define(function () { out.readonly = 'Read only'; out.anonymous = "Anonymous"; out.yourself = "Yourself"; - out.anonymousUsers = "anonymous users"; - out.anonymousUser = "anonymous user"; + out.anonymousUsers = "anonymous editors"; + out.anonymousUser = "anonymous editor"; out.shareView = "Read-only URL"; out.shareEdit = "Edit URL"; out.users = "Users"; @@ -131,6 +132,12 @@ define(function () { out.wizardTitle = "Use the wizard to create your poll"; out.wizardConfirm = "Are you really ready to add these options to your poll?"; + out.poll_publish_button = "Publish"; + out.poll_admin_button = "Admin"; + out.poll_create_user = "Add a new user"; + out.poll_create_option = "Add a new option"; + out.poll_commit = "Commit"; + out.poll_closeWizardButton = "Close wizard"; out.poll_closeWizardButtonTitle = "Close wizard"; out.poll_wizardComputeButton = "Compute Options"; @@ -159,6 +166,51 @@ define(function () { out.poll_titleHint = "Title"; out.poll_descriptionHint = "Description"; + // File manager + + out.fm_rootName = "My documents"; + out.fm_trashName = "Trash"; + out.fm_unsortedName = "Unsorted files"; + out.fm_filesDataName = "All files"; + out.fm_newFolder = "New folder"; + out.fm_newFolderButton = "NEW FOLDER"; + out.fm_folderName = "Folder name"; + out.fm_numberOfFolders = "# of folders"; + out.fm_numberOfFiles = "# of files"; + out.fm_fileName = "File name"; + out.fm_title = "Title"; + out.fm_lastAccess = "Last access"; + out.fm_creation = "Creation"; + out.fm_forbidden = "Forbidden action"; + out.fm_originalPath = "Original path"; + out.fm_emptyTrashDialog = "Are you sure you want to empty the trash?"; + out.fm_removeSeveralPermanentlyDialog = "Are you sure you want to remove these {0} elements from the trash permanently?"; + out.fm_removePermanentlyDialog = "Are you sure you want to remove {0} permanently?"; + out.fm_removeSeveralDialog = "Are you sure you want to move these {0} elements to the trash?"; + out.fm_removeDialog = "Are you sure you want to move {0} to the trash?"; + out.fm_restoreDialog = "Are you sure you want to restore {0} to its previous location?"; + out.fm_unknownFolderError = "The selected or last visited directory no longer exist. Opening the parent folder..."; + out.fm_contextMenuError = "Unable to open the context menu for that element. If the problem persist, try to reload the page."; + out.fm_selectError = "Unable to select the targetted element. If the problem persist, try to reload the page."; + // File - Context menu + out.fc_newfolder = "New folder"; + out.fc_rename = "Rename"; + out.fc_open = "Open"; + out.fc_delete = "Delete"; + out.fc_restore = "Restore"; + out.fc_remove = "Delete permanently"; + out.fc_empty = "Empty the trash"; + out.fc_newpad = "New text pad"; + out.fc_newcode = "New code pad"; + out.fc_newslide = "New presentation"; + out.fc_newpoll = "New poll"; + out.fc_prop = "Properties"; + // fileObject.js (logs) + out.fo_moveUnsortedError = "You can't move a folder to the list of unsorted pads"; + out.fo_existingNameError = "Name already used in that directory. Please choose another one."; + out.fo_moveFolderToChildError = "You can't move a folder into one of its descendants"; + out.fo_unableToRestore = "Unable to restore that file to its original location. You can try to move it to a new location."; + // index.html out.main_p1 = 'CryptPad is the zero knowledge realtime collaborative editor. Encryption carried out in your web browser protects the data from the server, the cloud, and the NSA. The secret encryption key is stored in the URL fragment identifier which is never sent to the server but is available to javascript so by sharing the URL, you give authorization to others who want to participate.'; diff --git a/www/code/main.js b/www/code/main.js index 9646e6d44..54e166fed 100644 --- a/www/code/main.js +++ b/www/code/main.js @@ -42,6 +42,11 @@ define([ secret.keys = secret.key; } + var onConnectError = function (info) { + module.spinner.hide(); + Cryptpad.alert(Messages.websocketError); + }; + var andThen = function (CMeditor) { var CodeMirror = module.CodeMirror = CMeditor; CodeMirror.modeURL = "/bower_components/codemirror/mode/%N/%N.js"; @@ -687,6 +692,8 @@ define([ } }; + var onError = config.onError = onConnectError; + var realtime = module.realtime = Realtime.start(config); editor.on('change', onLocal); @@ -699,6 +706,11 @@ define([ // TODO handle error andThen(CM); }); + Cryptpad.onError(function (info) { + if (info && info.type === "store") { + onConnectError(); + } + }); }; var first = function () { diff --git a/www/common/cryptpad-common.js b/www/common/cryptpad-common.js index 1866fb34a..fdc212cec 100644 --- a/www/common/cryptpad-common.js +++ b/www/common/cryptpad-common.js @@ -7,10 +7,11 @@ define([ '/bower_components/spin.js/spin.min.js', '/common/clipboard.js', + '/customize/fsStore.js', '/customize/user.js', '/bower_components/jquery/dist/jquery.min.js', -], function (Config, Messages, Store, Crypto, Alertify, Spinner, Clipboard, User) { +], function (Config, Messages, Store, Crypto, Alertify, Spinner, Clipboard, FS, User) { /* This file exposes functionality which is specific to Cryptpad, but not to any particular pad type. This includes functions for committing metadata about pads to your local storage for future use and improved usability. @@ -19,11 +20,18 @@ define([ */ var $ = window.jQuery; + // When set to true, USE_FS_STORE becomes the default store, but the localStorage store is + // still loaded for migration purpose. When false, the localStorage is used. + var USE_FS_STORE = true; + + var storeToUse = USE_FS_STORE ? FS : Store; + var common = { User: User, Messages: Messages, }; var store; + var fsStore; var userProxy; var userStore; @@ -35,7 +43,8 @@ define([ var getStore = common.getStore = function (legacy) { if (!legacy && userStore) { return userStore; } - if (store) { return store; } + if ((!USE_FS_STORE || legacy) && store) { return store; } + if (USE_FS_STORE && !legacy && fsStore) { return fsStore; } throw new Error("Store is not ready!"); }; @@ -95,8 +104,8 @@ define([ store = Store; }); - - var isArray = function (o) { return Object.prototype.toString.call(o) === '[object Array]'; }; + // var isArray = function (o) { return Object.prototype.toString.call(o) === '[object Array]'; }; + var isArray = $.isArray; var fixHTML = common.fixHTML = function (html) { return html.replace(/', { title: Messages.editShareTitle, - 'class': "button action" }).text(Messages.editShare); if (data && data.editHash) { var editHash = data.editHash; @@ -778,7 +830,6 @@ define([ case 'viewshare': button = $(' + + + +
+
+
+
+
+ + + +
+
diff --git a/www/poll/test/main.js b/www/poll/test/main.js new file mode 100644 index 000000000..069406ee7 --- /dev/null +++ b/www/poll/test/main.js @@ -0,0 +1,719 @@ +define([ + '/api/config?cb=' + Math.random().toString(16).substring(2), + '/customize/messages.js?app=poll', + '/bower_components/textpatcher/TextPatcher.js', + '/bower_components/chainpad-listmap/chainpad-listmap.js', + '/bower_components/chainpad-crypto/crypto.js', + '/common/cryptpad-common.js', + '/bower_components/hyperjson/hyperjson.js', + '/poll/test/render.js', + '/common/toolbar.js', + '/common/visible.js', + '/common/notify.js', + '/bower_components/file-saver/FileSaver.min.js', + '/bower_components/jquery/dist/jquery.min.js', + //'/customize/pad.js' +], function (Config, Messages, TextPatcher, Listmap, Crypto, Cryptpad, Hyperjson, Render, Toolbar) { + var $ = window.jQuery; + + var HIDE_INTRODUCTION_TEXT = "hide_poll_text"; + var defaultName; + + var secret = Cryptpad.getSecrets(); + var readOnly = secret.keys && !secret.keys.editKeyStr; + + var APP = window.APP = { + Toolbar: Toolbar, + Hyperjson: Hyperjson, + Render: Render, + $bar: $('#toolbar').css({ border: '1px solid white', background: 'grey', 'margin-bottom': '1vh', }), + editable: { + row: [], + col: [] + } + }; + + var sortColumns = function (order, firstcol) { + var colsOrder = order.slice(); + colsOrder.sort(function (a, b) { + return (a === firstcol) ? -1 : + ((b === firstcol) ? 1 : 0); + }); + return colsOrder; + }; + + var isOwnColumnCommitted = function () { + return APP.proxy && APP.proxy.table.colsOrder.indexOf(APP.userid) !== -1; + }; + + var mergeUncommitted = function (proxy, uncommitted, commit) { + var newObj; + if (commit) { + newObj = proxy; + } else { + newObj = $.extend(true, {}, proxy); + } + // We have uncommitted data only if the user's column is not in the proxy + // If it is already is the proxy, nothing to merge + if (isOwnColumnCommitted()) { + return newObj; + } + // Merge uncommitted into the proxy + uncommitted.table.colsOrder.forEach(function (x) { + if (newObj.table.colsOrder.indexOf(x) !== -1) { return; } + newObj.table.colsOrder.push(x); + }); + for (var k in uncommitted.table.cols) { + if (!newObj.table.cols[k]) { + newObj.table.cols[k] = uncommitted.table.cols[k]; + } + } + for (var l in uncommitted.table.cells) { + if (!newObj.table.cells[l]) { + newObj.table.cells[l] = uncommitted.table.cells[l]; + } + } + return newObj; + }; + + var setColumnDisabled = function (id, state) { + if (!state) { + $('input[data-rt-id^="' + id + '"]').removeAttr('disabled'); + return; + } + $('input[data-rt-id^="' + id + '"]').attr('disabled', 'disabled'); + }; + + var styleUncommittedColumn = function () { + var id = APP.userid; + + // Enable the checkboxes for the user's column (committed or not) + $('input[disabled="disabled"][data-rt-id^="' + id + '"]').removeAttr('disabled'); + $('input[type="checkbox"][data-rt-id^="' + id + '"]').addClass('enabled'); + $('[data-rt-id="' + id + '"] ~ .edit').css('visibility', 'hidden'); + $('.lock[data-rt-id="' + id + '"]').html('🔓'); + + if (isOwnColumnCommitted()) { return; } + $('[data-rt-id^="' + id + '"]').closest('td').addClass("uncommitted"); + $('td.uncommitted .remove, td.uncommitted .edit').css('visibility', 'hidden'); + $('td.uncommitted .cover').addClass("uncommitted"); + $('.uncommitted input[type="text"]').attr("placeholder", Messages.poll_userPlaceholder); + }; + + var unlockElements = function () { + APP.editable.row.forEach(function (id) { + $('input[type="text"][disabled="disabled"][data-rt-id="' + id + '"]').removeAttr('disabled'); + $('span.edit[data-rt-id="' + id + '"]').css('visibility', 'hidden'); + }); + APP.editable.col.forEach(function (id) { + $('input[disabled="disabled"][data-rt-id^="' + id + '"]').removeAttr('disabled'); + $('input[type="checkbox"][data-rt-id^="' + id + '"]').addClass('enabled'); + $('span.edit[data-rt-id="' + id + '"]').css('visibility', 'hidden'); + $('.lock[data-rt-id="' + id + '"]').html('🔓'); + }); + }; + + var updateTableButtons = function () { + if (!isOwnColumnCommitted()) { + $('#commit').show(); + } + + var $createOption = APP.$table.find('tfoot tr td:first-child'); + var $commitCell = APP.$table.find('tfoot tr td:nth-child(2)'); + $createOption.append(APP.$createRow); + $commitCell.append(APP.$commit); + $('#create-user, #create-option').css('display', 'inline-block'); + if (!APP.proxy || !APP.proxy.table.rowsOrder || APP.proxy.table.rowsOrder.length === 0) { $('#create-user').hide(); } + var width = $('#table').outerWidth(); + if (width) { + //$('#create-user').css('left', width + 30 + 'px'); + } + }; + + var setTablePublished = function (bool) { + if (bool) { + if (APP.$publish) { APP.$publish.hide(); } + if (APP.$admin) { APP.$admin.show(); } + $('#create-option').hide(); + $('.remove[data-rt-id^="y"], .edit[data-rt-id^="y"]').hide(); + } else { + if (APP.$publish) { APP.$publish.show(); } + if (APP.$admin) { APP.$admin.hide(); } + $('#create-option').show(); + $('.remove[data-rt-id^="y"], .edit[data-rt-id^="y"]').show(); + } + }; + + var updateDisplayedTable = function () { + styleUncommittedColumn(); + unlockElements(); + updateTableButtons(); + setTablePublished(APP.proxy.published); + }; + + var unlockColumn = function (id, cb) { + if (APP.editable.col.indexOf(id) === -1) { + APP.editable.col.push(id); + } + if (typeof(cb) === "function") { + cb(); + } + }; + var unlockRow = function (id, cb) { + if (APP.editable.row.indexOf(id) === -1) { + APP.editable.row.push(id); + } + if (typeof(cb) === "function") { + cb(); + } + }; + + /* Any time the realtime object changes, call this function */ + var change = function (o, n, path) { + if (path && path.join) { + console.log("Change from [%s] to [%s] at [%s]", + o, n, path.join(', ')); + } + + var table = APP.$table[0]; + + var displayedObj = mergeUncommitted(APP.proxy, APP.uncommitted); + + var colsOrder = sortColumns(displayedObj.table.colsOrder, APP.userid); + var conf = { + cols: colsOrder, + readOnly: readOnly + }; + + //Render.updateTable(table, displayedObj, conf); + + /* FIXME browser autocomplete fills in new fields sometimes + calling updateTable twice removes the autofilled in values + setting autocomplete="off" is not reliable + + https://developer.mozilla.org/en-US/docs/Web/Security/Securing_your_site/Turning_off_form_autocompletion + */ + window.setTimeout(function () { + var displayedObj2 = mergeUncommitted(APP.proxy, APP.uncommitted); + Render.updateTable(table, displayedObj2, conf); + updateDisplayedTable(); + }); + }; + + var getRealtimeId = function (input) { + return input.getAttribute && input.getAttribute('data-rt-id'); + }; + + /* Called whenever an event is fired on an input element */ + var handleInput = function (input) { + var type = input.type.toLowerCase(); + var id = getRealtimeId(input); + + console.log(input); + + var object = APP.proxy; + + var x = Render.getCoordinates(id)[0]; + if (type !== "row" && x === APP.userid && APP.proxy.table.colsOrder.indexOf(x) === -1) { + object = APP.uncommitted; + } + + switch (type) { + case 'text': + console.log("text[rt-id='%s'] [%s]", id, input.value); + if (!input.value) { return void console.log("Hit enter?"); } + Render.setValue(object, id, input.value); + change(); + break; + case 'checkbox': + console.log("checkbox[tr-id='%s'] %s", id, input.checked); + if (APP.editable.col.indexOf(x) >= 0 || x === APP.userid) { + Render.setValue(object, id, input.checked); + change(); + } else { + console.log('checkbox locked'); + } + break; + default: + console.log("Input[type='%s']", type); + break; + } + }; + + /* Called whenever an event is fired on a span */ + var handleSpan = function (span) { + var id = span.getAttribute('data-rt-id'); + var type = Render.typeofId(id); + var isRemove = span.className && span.className.split(' ').indexOf('remove') !== -1; + var isEdit = span.className && span.className.split(' ').indexOf('edit') !== -1; + if (type === 'row') { + if (isRemove) { + Cryptpad.confirm(Messages.poll_removeOption, function (res) { + if (!res) { return; } + Render.removeRow(APP.proxy, id, function () { + change(); + }); + }); + } else if (isEdit) { + unlockRow(id, function () { + change(); + }); + } + } else if (type === 'col') { + if (isRemove) { + Cryptpad.confirm(Messages.poll_removeUser, function (res) { + if (!res) { return; } + Render.removeColumn(APP.proxy, id, function () { + change(); + }); + }); + } else if (isEdit) { + unlockColumn(id, function () { + change(); + }); + } + } else if (type === 'cell') { + change(); + } else { + console.log("UNHANDLED"); + } + }; + + var hideInputs = function (e) { + if ($(e.target).is('[type="text"]')) { + return; + } + $('.lock[data-rt-id!="' + APP.userid + '"]').html('🔒 '); + var $cells = APP.$table.find('thead td:not(.uncommitted), tbody td'); + $cells.find('[type="text"][data-rt-id!="' + APP.userid + '"]').attr('disabled', true); + $('.edit[data-rt-id!="' + APP.userid + '"]').css('visibility', 'visible'); + APP.editable.col = [APP.userid]; + APP.editable.row = []; + }; + + $(window).click(hideInputs); + + var handleClick = function (e, isKeyup) { + e.stopPropagation(); + + if (!APP.ready) { return; } + var target = e && e.target; + + if (isKeyup) { + console.log("Keyup!"); + } + + if (!target) { return void console.log("NO TARGET"); } + + var nodeName = target && target.nodeName; + + if (!$(target).parents('#table tbody').length || $(target).hasClass('edit')) { + hideInputs(e); + } + + switch (nodeName) { + case 'INPUT': + handleInput(target); + break; + case 'SPAN': + //case 'LABEL': + handleSpan(target); + break; + case undefined: + //console.error(new Error("C'est pas possible!")); + break; + default: + console.log(target, nodeName); + break; + } + }; + + /* + Make sure that the realtime data structure has all the required fields + */ + var prepareProxy = function (proxy, schema) { + if (proxy && proxy.version === 1) { return; } + console.log("Configuring proxy schema..."); + + proxy.info = schema.info; + proxy.table = schema.table; + proxy.version = 1; + }; + + + /* + + */ + var publish = APP.publish = function (bool) { + if (!APP.ready) { return; } + if (APP.proxy.published !== bool) { + APP.proxy.published = bool; + } + setTablePublished(bool); + ['textarea'].forEach(function (sel) { + $(sel).attr('disabled', bool); + }); + }; + + var userData = APP.userData = {}; // List of pretty names for all users (mapped with their ID) + var userList; // List of users still connected to the channel (server IDs) + var addToUserData = function(data) { + var users = userList ? userList.users : undefined; + //var userData = APP.proxy.info.userData; + for (var attrname in data) { userData[attrname] = data[attrname]; } + + if (users && users.length) { + for (var userKey in userData) { + if (users.indexOf(userKey) === -1) { delete userData[userKey]; } + } + } + + if(userList && typeof userList.onChange === "function") { + userList.onChange(userData); + } + + APP.proxy.info.userData = userData; + }; + + //var myData = {}; + var getLastName = function (cb) { + Cryptpad.getAttribute('username', function (err, userName) { + cb(err, userName || ''); + }); + }; + + var setName = APP.setName = function (newName) { + if (typeof(newName) !== 'string') { return; } + var myUserNameTemp = Cryptpad.fixHTML(newName.trim()); + if(myUserNameTemp.length > 32) { + myUserNameTemp = myUserNameTemp.substr(0, 32); + } + var myUserName = myUserNameTemp; + var myID = APP.myID; + var myData = {}; + myData[myID] = { + name: myUserName + }; + addToUserData(myData); + Cryptpad.setAttribute('username', newName, function (err, data) { + if (err) { + console.error("Couldn't set username"); + return; + } + APP.userName.lastName = myUserName; + //change(); + }); + }; + + var updateTitle = function (newTitle) { + if (newTitle === document.title) { return; } + // Change the title now, and set it back to the old value if there is an error + var oldTitle = document.title; + document.title = newTitle; + Cryptpad.renamePad(newTitle, function (err, data) { + if (err) { + console.log("Couldn't set pad title"); + console.error(err); + document.title = oldTitle; + return; + } + document.title = data; + APP.$bar.find('.' + Toolbar.constants.title).find('span.title').text(data); + APP.$bar.find('.' + Toolbar.constants.title).find('input').val(data); + }); + }; + + var updateDefaultTitle = function (defaultTitle) { + defaultName = defaultTitle; + APP.$bar.find('.' + Toolbar.constants.title).find('input').attr("placeholder", defaultName); + }; + var renameCb = function (err, title) { + if (err) { return; } + document.title = title; + APP.proxy.info.title = title; + }; + + var suggestName = function (fallback) { + return document.title || defaultName || ""; + }; + + + var copyObject = function (obj) { + return JSON.parse(JSON.stringify(obj)); + }; + + // special UI elements + //var $title = $('#title').attr('placeholder', Messages.poll_titleHint || 'title'); TODO + var $description = $('#description').attr('placeholder', Messages.poll_descriptionHint || 'description'); + + var ready = function (info, userid, readOnly) { + console.log("READY"); + console.log('userid: %s', userid); + + var proxy = APP.proxy; + var uncommitted = APP.uncommitted = {}; + prepareProxy(proxy, copyObject(Render.Example)); + prepareProxy(uncommitted, copyObject(Render.Example)); + if (!readOnly && proxy.table.colsOrder.indexOf(userid) === -1 && + uncommitted.table.colsOrder.indexOf(userid) === -1) { + uncommitted.table.colsOrder.unshift(userid); + } + + var displayedObj = mergeUncommitted(proxy, uncommitted, false); + + var colsOrder = sortColumns(displayedObj.table.colsOrder, userid); + + var $table = APP.$table = $(Render.asHTML(displayedObj, null, colsOrder, readOnly)); + var $createRow = APP.$createRow = $('#create-option').click(function () { + console.error("BUTTON CLICKED! LOL"); + Render.createRow(proxy, function () { + change(); + }); + }); + + var $createCol = APP.$createCol = $('#create-user').click(function () { + Render.createColumn(proxy, function () { + change(); + }); + }); + + // Commit button + var $commit = APP.$commit = $('#commit').click(function () { + var uncommittedCopy = JSON.parse(JSON.stringify(APP.uncommitted)); + APP.uncommitted = {}; + prepareProxy(APP.uncommitted, copyObject(Render.Example)); + mergeUncommitted(proxy, uncommittedCopy, true); + APP.$commit.hide(); + change(); + }); + + // #publish button is removed in readonly + var $publish = APP.$publish = $('#publish') + .click(function () { + publish(true); + }); + + // #publish button is removed in readonly + var $admin = APP.$admin = $('#admin') + .click(function () { + publish(false); + }); + + // Title + if (APP.proxy.info.defaultTitle) { + updateDefaultTitle(APP.proxy.info.defaultTitle); + } else { + APP.proxy.info.defaultTitle = defaultName; + } + updateTitle(APP.proxy.info.title || defaultName); + + // Description + var resize = function () { + var lineCount = $description.val().split('\n').length; + $description.css('height', lineCount + 'rem'); + }; + $description.on('change keyup', function () { + var val = $description.val(); + proxy.info.description = val; + resize(); + }); + resize(); + if (typeof(proxy.info.description) !== 'undefined') { + $description.val(proxy.info.description); + } + + $('#tableScroll').prepend($table); + updateDisplayedTable(); + + $table + .click(handleClick) + .on('keyup', function (e) { handleClick(e, true); }); + + proxy + .on('change', ['info'], function (o, n, p) { + if (p[1] === 'title') { + updateTitle(n); + } else if (p[1] === "userData") { + addToUserData(APP.proxy.info.userData); + } else if (p[1] === 'description') { + var op = TextPatcher.diff(o, n); + var el = $description[0]; + + var selects = ['selectionStart', 'selectionEnd'].map(function (attr) { + var before = el[attr]; + var after = TextPatcher.transformCursor(el[attr], op); + return after; + }); + $description.val(n); + if (op) { + el.selectionStart = selects[0]; + el.selectionEnd = selects[1]; + } + } + + console.log("change: (%s, %s, [%s])", o, n, p.join(', ')); + }) + .on('change', ['table'], change) + .on('remove', [], change); + + addToUserData(APP.proxy.info.userData); + + getLastName(function (err, lastName) { + APP.ready = true; + + if (!proxy.published) { + publish(false); + } else { + publish(true); + } + + // Update the toolbar list: + // Add the current user in the metadata if he has edit rights + if (readOnly) { return; } + if (typeof(lastName) === 'string' && lastName.length) { + setName(lastName); + } else { + var myData = {}; + myData[info.myId] = { + name: "" + }; + addToUserData(myData); + APP.$userNameButton.click(); + } + }); + }; + + var create = function (info) { + var realtime = APP.realtime = info.realtime; + var myID = APP.myID = info.myID; + + var editHash; + var viewHash = Cryptpad.getViewHashFromKeys(info.channel, secret.keys); + + if (!readOnly) { + editHash = Cryptpad.getEditHashFromKeys(info.channel, secret.keys); + } + + APP.patchText = TextPatcher.create({ + realtime: realtime, + logging: true, + }); + + userList = APP.userList = info.userList; + var config = { + userData: userData, + readOnly: readOnly, + title: { + onRename: renameCb, + defaultName: defaultName, + suggestName: suggestName + }, + ifrw: window, + common: Cryptpad + }; + var toolbar = info.realtime.toolbar = Toolbar.create(APP.$bar, info.myID, info.realtime, info.getLag, userList, config); + + var $bar = APP.$bar; + var $rightside = $bar.find('.' + Toolbar.constants.rightside); + var $userBlock = $bar.find('.' + Toolbar.constants.username); + var $editShare = $bar.find('.' + Toolbar.constants.editShare); + var $viewShare = $bar.find('.' + Toolbar.constants.viewShare); + + // Store the object sent for the "change username" button so that we can update the field value correctly + var userNameButtonObject = APP.userName = {}; + /* add a "change username" button */ + getLastName(function (err, lastName) { + userNameButtonObject.lastName = lastName; + var $username = APP.$userNameButton = Cryptpad.createButton('username', false, userNameButtonObject, setName).hide(); + $userBlock.append($username); + }); + + /* add a forget button */ + var forgetCb = function (err, title) { + if (err) { return; } + document.title = title; + }; + var $forgetPad = Cryptpad.createButton('forget', true, {}, forgetCb); + $rightside.append($forgetPad); + + if (!readOnly) { + $editShare.append(Cryptpad.createButton('editshare', false, {editHash: editHash})); + } + if (viewHash) { + /* add a 'links' button */ + $viewShare.append(Cryptpad.createButton('viewshare', false, {viewHash: viewHash})); + if (!readOnly) { + $viewShare.append(Cryptpad.createButton('viewopen', false, {viewHash: viewHash})); + } + } + + // set the hash + if (!readOnly) { Cryptpad.replaceHash(editHash); } + + Cryptpad.getPadTitle(function (err, title) { + if (err) { + console.error(err); + console.log("Couldn't get pad title"); + return; + } + updateTitle(title || defaultName); + }); + }; + + var disconnect = function () { + //setEditable(false); // TODO + if (info.error) { + Cryptpad.alert(Messages.websocketError); + return; + } + //Cryptpad.alert(Messages.common_connectionLost); // TODO + }; + + var config = { + websocketURL: Cryptpad.getWebsocketURL(), + channel: secret.channel, + readOnly: readOnly, + data: {}, + // our public key + validateKey: secret.keys.validateKey || undefined, + //readOnly: readOnly, + crypto: Crypto.createEncryptor(secret.keys), + }; + + // don't initialize until the store is ready. + Cryptpad.ready(function () { + if (readOnly) { + $('#commit, #create-user, #create-option, #publish, #admin').remove(); + } + + var parsedHash = Cryptpad.parsePadUrl(window.location.href); + defaultName = Cryptpad.getDefaultName(parsedHash); + var rt = window.rt = APP.rt = Listmap.create(config); + APP.proxy = rt.proxy; + rt.proxy + .on('create', create) + .on('ready', function (info) { + Cryptpad.getPadAttribute('userid', function (e, userid) { + if (e) { console.error(e); } + if (!userid) { userid = Render.coluid(); } + APP.userid = userid; + Cryptpad.setPadAttribute('userid', userid, function (e) { + ready(info, userid, readOnly); + }); + }); + }) + .on('disconnect', disconnect); + + Cryptpad.getAttribute(HIDE_INTRODUCTION_TEXT, function (e, value) { + if (e) { console.error(e); } + if (value === null) { + Cryptpad.setAttribute(HIDE_INTRODUCTION_TEXT, "1", function (e) { + if (e) { console.error(e); } + }); + } else if (value === "1") { + $('#howItWorks').hide(); + } + }); + }); +}); + diff --git a/www/poll/test/render.js b/www/poll/test/render.js new file mode 100644 index 000000000..b0b41369d --- /dev/null +++ b/www/poll/test/render.js @@ -0,0 +1,446 @@ +define([ + '/common/cryptpad-common.js', + '/bower_components/hyperjson/hyperjson.js', + '/bower_components/textpatcher/TextPatcher.js', + '/bower_components/diff-dom/diffDOM.js', +], function (Cryptpad, Hyperjson, TextPatcher) { + var DiffDOM = window.diffDOM; + + var Example = { + info: { + title: '', + description: '', + userData: {} + }, + table: { +/* TODO + +deprecate the practice of storing cells, cols, and rows separately. + +Instead, keep everything in one map, and iterate over columns and rows +by maintaining indexes in rowsOrder and colsOrder + +*/ + cells: {}, + cols: {}, + colsOrder: [], + rows: {}, + rowsOrder: [] + } + }; + + var Render = { + Example: Example + }; + + var Uid = Render.Uid = function (prefix, f) { + f = f || function () { + return Number(Math.random() * Number.MAX_SAFE_INTEGER) + .toString(32).replace(/\./g, ''); + }; + return function () { return prefix + '-' + f(); }; + }; + + var coluid = Render.coluid = Uid('x'); + var rowuid = Render.rowuid = Uid('y'); + + var isRow = Render.isRow = function (id) { return /^y\-[^_]*$/.test(id); }; + var isColumn = Render.isColumn = function (id) { return /^x\-[^_]*$/.test(id); }; + var isCell = Render.isCell = function (id) { return /^x\-[^_]*_y\-.*$/.test(id); }; + + var typeofId = Render.typeofId = function (id) { + if (isRow(id)) { return 'row'; } + if (isColumn(id)) { return 'col'; } + if (isCell(id)) { return 'cell'; } + return null; + }; + + var getCoordinates = Render.getCoordinates = function (id) { + return id.split('_'); + }; + + var getColumnValue = Render.getColumnValue = function (obj, colId) { + return Cryptpad.find(obj, ['table', 'cols'].concat([colId])); + }; + + var getRowValue = Render.getRowValue = function (obj, rowId) { + return Cryptpad.find(obj, ['table', 'rows'].concat([rowId])); + }; + + var getCellValue = Render.getCellValue = function (obj, cellId) { + return Cryptpad.find(obj, ['table', 'cells'].concat([cellId])); + }; + + var setRowValue = Render.setRowValue = function (obj, rowId, value) { + var parent = Cryptpad.find(obj, ['table', 'rows']); + if (typeof(parent) === 'object') { return (parent[rowId] = value); } + return null; + }; + + var setColumnValue = Render.setColumnValue = function (obj, colId, value) { + var parent = Cryptpad.find(obj, ['table', 'cols']); + if (typeof(parent) === 'object') { return (parent[colId] = value); } + return null; + }; + + var setCellValue = Render.setCellValue = function (obj, cellId, value) { + var parent = Cryptpad.find(obj, ['table', 'cells']); + if (typeof(parent) === 'object') { return (parent[cellId] = value); } + return null; + }; + + var createColumn = Render.createColumn = function (obj, cb, id, value) { + var order = Cryptpad.find(obj, ['table', 'colsOrder']); + if (!order) { throw new Error("Uninitialized realtime object!"); } + id = id || coluid(); + value = value || ""; + setColumnValue(obj, id, value); + order.push(id); + if (typeof(cb) === 'function') { cb(void 0, id); } + }; + + var removeColumn = Render.removeColumn = function (obj, id, cb) { + var order = Cryptpad.find(obj, ['table', 'colsOrder']); + var parent = Cryptpad.find(obj, ['table', 'cols']); + + if (!(order && parent)) { throw new Error("Uninitialized realtime object!"); } + + var idx = order.indexOf(id); + if (idx === -1) { + return void console + .error(new Error("Attempted to remove id which does not exist")); + } + + Object.keys(obj.table.cells).forEach(function (key) { + if (key.indexOf(id) === 0) { + delete obj.table.cells[key]; + } + }); + + order.splice(idx, 1); + if (parent[id]) { delete parent[id]; } + if (typeof(cb) === 'function') { + cb(); + } + }; + + var createRow = Render.createRow = function (obj, cb, id, value) { + console.error('new row!'); + var order = Cryptpad.find(obj, ['table', 'rowsOrder']); + if (!order) { throw new Error("Uninitialized realtime object!"); } + id = id || rowuid(); + value = value || ""; + setRowValue(obj, id, value); + order.push(id); + if (typeof(cb) === 'function') { cb(void 0, id); } + }; + + var removeRow = Render.removeRow = function (obj, id, cb) { + var order = Cryptpad.find(obj, ['table', 'rowsOrder']); + var parent = Cryptpad.find(obj, ['table', 'rows']); + + if (!(order && parent)) { throw new Error("Uninitialized realtime object!"); } + + var idx = order.indexOf(id); + if (idx === -1) { + return void console + .error(new Error("Attempted to remove id which does not exist")); + } + + order.splice(idx, 1); + if (parent[id]) { delete parent[id]; } + if (typeof(cb) === 'function') { cb(); } + }; + + var setValue = Render.setValue = function (obj, id, value) { + var type = typeofId(id); + + switch (type) { + case 'row': return setRowValue(obj, id, value); + case 'col': return setColumnValue(obj, id, value); + case 'cell': return setCellValue(obj, id, value); + case null: break; + default: + console.log("[%s] has type [%s]", id, type); + throw new Error("Unexpected type!"); + } + }; + + var getValue = Render.getValue = function (obj, id) { + switch (typeofId(id)) { + case 'row': return getRowValue(obj, id); + case 'col': return getColumnValue(obj, id); + case 'cell': return getCellValue(obj, id); + case null: break; + default: throw new Error("Unexpected type!"); + } + }; + + var getRowIds = Render.getRowIds = function (obj) { + return Cryptpad.find(obj, ['table', 'rowsOrder']); + }; + + var getColIds = Render.getColIds = function (obj) { + return Cryptpad.find(obj, ['table', 'colsOrder']); + }; + + var getCells = Render.getCells = function (obj) { + return Cryptpad.find(obj, ['table', 'cells']); + }; + + /* cellMatrix takes a proxy object, and optionally an alternate ordering + of row/column keys (as an array). + + it returns an array of arrays containing the relevant data for each + cell in table we wish to construct. + */ + var cellMatrix = Render.cellMatrix = function (obj, rows, cols, readOnly) { + if (typeof(obj) !== 'object') { + throw new Error('expected realtime-proxy object'); + } + + var cells = getCells(obj); + rows = rows || getRowIds(obj); + rows.push(''); + cols = cols || getColIds(obj); + + return [null].concat(rows).map(function (row, i) { + if (i === 0) { + return [null].concat(cols.map(function (col) { + var result = { + 'data-rt-id': col, + type: 'text', + value: getColumnValue(obj, col) || "", + placeholder: Cryptpad.Messages.poll_userPlaceholder, + disabled: 'disabled' + }; + return result; + })); + } + if (i === rows.length) { + return [null].concat(cols.map(function (col) { + return { + 'class': 'lastRow', + }; + })); + } + + return [{ + 'data-rt-id': row, + value: getRowValue(obj, row), + type: 'text', + placeholder: Cryptpad.Messages.poll_optionPlaceholder, + disabled: 'disabled' + }].concat(cols.map(function (col) { + var id = [col, rows[i-1]].join('_'); + var val = cells[id] || false; + var result = { + 'data-rt-id': id, + type: 'checkbox', + autocomplete: 'nope', + }; + if (readOnly) { + result.disabled = "disabled"; + } + if (val) { result.checked = true; } + return result; + })); + }); + }; + + var makeRemoveElement = Render.makeRemoveElement = function (id) { + return ['SPAN', { + 'data-rt-id': id, + class: 'remove', + }, ['✖']]; + }; + + var makeEditElement = Render.makeEditElement = function (id) { + return ['SPAN', { + 'data-rt-id': id, + class: 'edit', + }, ['✐']]; + }; + + var makeLockElement = Render.makeLockElement = function (id) { + return ['SPAN', { + 'data-rt-id': id, + class: 'lock', + }, ['🔒']]; + }; + + var makeHeadingCell = Render.makeHeadingCell = function (cell, readOnly) { + if (!cell) { return ['TD', {}, []]; } + if (cell.type === 'text') { + var removeElement = makeRemoveElement(cell['data-rt-id']); + var editElement = makeEditElement(cell['data-rt-id']); + var lockElement = makeLockElement(cell['data-rt-id']); + var elements = [['INPUT', cell, []]]; + if (!readOnly) { + elements.unshift(removeElement); + elements.unshift(lockElement); + elements.unshift(editElement); + } + return ['TD', {}, elements]; + } + return ['TD', cell, []]; + }; + + var clone = function (o) { + return JSON.parse(JSON.stringify(o)); + }; + + var makeCheckbox = Render.makeCheckbox = function (cell) { + var attrs = clone(cell); + + // FIXME + attrs.id = cell['data-rt-id']; + + var labelClass = 'cover'; + if (cell.checked) { + labelClass += ' yes'; + } + + return ['TD', {class:"checkbox-cell"}, [ + ['DIV', {class: 'checkbox-contain'}, [ + ['INPUT', attrs, []], + ['SPAN', {class: labelClass}, []], + ['LABEL', { + for: attrs.id, + 'data-rt-id': attrs.id, + }, []] + ]] + ]]; + }; + + var makeBodyCell = Render.makeBodyCell = function (cell, readOnly) { + if (cell && cell.type === 'text') { + var removeElement = makeRemoveElement(cell['data-rt-id']); + var editElement = makeEditElement(cell['data-rt-id']); + var elements = [['INPUT', cell, []]]; + if (!readOnly) { + elements.push(removeElement); + elements.push(editElement); + } + return ['TD', {}, [ + ['DIV', {class: 'text-cell'}, elements] + ]]; + } + + if (cell && cell.type === 'checkbox') { + return makeCheckbox(cell); + } + return ['TD', cell, []]; + }; + + var makeBodyRow = Render.makeBodyRow = function (row, readOnly) { + return ['TR', {}, row.map(function (cell) { + return makeBodyCell(cell, readOnly); + })]; + }; + + var toHyperjson = Render.toHyperjson = function (matrix, readOnly) { + if (!matrix || !matrix.length) { return; } + var head = ['THEAD', {}, [ ['TR', {}, matrix[0].map(function (cell) { + return makeHeadingCell(cell, readOnly); + })] ]]; + var foot = ['TFOOT', {}, matrix.slice(-1).map(function (row) { + return makeBodyRow(row, readOnly); + })]; + var body = ['TBODY', {}, matrix.slice(1, -1).map(function (row) { + return makeBodyRow(row, readOnly); + })]; + return ['TABLE', {id:'table'}, [head, foot, body]]; + }; + + var asHTML = Render.asHTML = function (obj, rows, cols, readOnly) { + return Hyperjson.toDOM(toHyperjson(cellMatrix(obj, rows, cols, readOnly), readOnly)); + }; + + var diffIsInput = Render.diffIsInput = function (info) { + var nodeName = Cryptpad.find(info, ['node', 'nodeName']); + if (nodeName !== 'INPUT') { return; } + return true; + }; + + var getInputType = Render.getInputType = function (info) { + return Cryptpad.find(info, ['node', 'type']); + }; + + var preserveCursor = Render.preserveCursor = function (info) { + if (['modifyValue', 'modifyAttribute'].indexOf(info.diff.action) !== -1) { + var element = info.node; + + if (typeof(element.selectionStart) !== 'number') { return; } + + var o = info.oldValue || ''; + var n = info.newValue || ''; + var op = TextPatcher.diff(o, n); + + info.selection = ['selectionStart', 'selectionEnd'].map(function (attr) { + var before = element[attr]; + var after = TextPatcher.transformCursor(element[attr], op); + return after; + }); + } + }; + + var recoverCursor = Render.recoverCursor = function (info) { + try { + if (info.selection && info.node) { + info.node.selectionStart = info.selection[0]; + info.node.selectionEnd = info.selection[1]; + } + } catch (err) { + //console.log(info.node); + //console.error(err); + } + }; + + var diffOptions = { + preDiffApply: function (info) { + if (!diffIsInput(info)) { return; } + switch (getInputType(info)) { + case 'checkbox': + //console.log('checkbox'); + //console.log("[preDiffApply]", info); + break; + case 'text': + preserveCursor(info); + break; + default: break; + } + }, + postDiffApply: function (info) { + if (info.selection) { recoverCursor(info); } + /* + if (!diffIsInput(info)) { return; } + switch (getInputType(info)) { + case 'checkbox': + console.log("[postDiffApply]", info); + break; + case 'text': break; + default: break; + }*/ + } + }; + + var updateTable = Render.updateTable = function (table, obj, conf) { + var DD = new DiffDOM(diffOptions); + + var rows = conf ? conf.rows : null; + var cols = conf ? conf.cols : null; + var readOnly = conf ? conf.readOnly : false; + var matrix = cellMatrix(obj, rows, cols, readOnly); + + var hj = toHyperjson(matrix, readOnly); + + if (!hj) { throw new Error("Expected Hyperjson!"); } + + var table2 = Hyperjson.toDOM(hj); + var patch = DD.diff(table, table2); + DD.apply(table, patch); + }; + + return Render; +}); diff --git a/www/slide/main.js b/www/slide/main.js index a291ae02c..d1dfe212e 100644 --- a/www/slide/main.js +++ b/www/slide/main.js @@ -59,6 +59,11 @@ define([ var presentMode = Slide.isPresentURL(); + var onConnectError = function (info) { + module.spinner.hide(); + Cryptpad.alert(Messages.websocketError); + }; + var andThen = function (CMeditor) { var CodeMirror = module.CodeMirror = CMeditor; CodeMirror.modeURL = "/bower_components/codemirror/mode/%N/%N.js"; @@ -786,6 +791,8 @@ define([ } }; + var onError = config.onError = onConnectError; + var realtime = module.realtime = Realtime.start(config); editor.on('change', onLocal); @@ -798,6 +805,11 @@ define([ // TODO handle error andThen(CM); }); + Cryptpad.onError(function (info) { + if (info && info.type === "store") { + onConnectError(); + } + }); }; var first = function () {