Merge branch 'fileManager' of github.com:xwiki-labs/cryptpad into test-filemanager

pull/1/head
ansuz 8 years ago
commit f36d15d0db

@ -41,6 +41,7 @@
"diff-dom": "#gh-pages", "diff-dom": "#gh-pages",
"alertifyjs": "^1.0.11", "alertifyjs": "^1.0.11",
"spin.js": "^2.3.2", "spin.js": "^2.3.2",
"scrypt-async": "^1.2.0" "scrypt-async": "^1.2.0",
"bootstrap": "^3.3.7"
} }
} }

@ -10,7 +10,7 @@ define([
var main = function () { var main = function () {
var url = window.location.pathname; var url = window.location.pathname;
var isHtml = /\.html/.test(url) || url === '/' || url === ''; var isHtml = /\.html/.test(url) || url === '/' || url === '';
var isPoll = /\/poll\//.test(url); var isPoll = /\/poll\//.test(url) || /\/file\//.test(url);
if (!isHtml && !isPoll) { if (!isHtml && !isPoll) {
Messages._applyTranslation(); Messages._applyTranslation();
return; return;

@ -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;
});

@ -318,47 +318,68 @@ tbody td:last-child {
color: #FF0073; color: #FF0073;
cursor: pointer !important; cursor: pointer !important;
} }
form.realtime { form.realtime,
div.realtime {
padding: 0px; padding: 0px;
margin: 0px; margin: 0px;
} }
form.realtime > textarea { form.realtime > textarea,
div.realtime > textarea {
width: 50%; width: 50%;
height: 15vh; height: 15vh;
} }
form.realtime table { form.realtime table,
div.realtime table {
border-collapse: collapse; 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; padding: 0px;
margin: 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; padding: 0px;
margin: 0px; margin: 0px;
height: 100%; 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: 80%;
width: 90%;
height: 100%; height: 100%;
border: 0px; 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; background-color: transparent;
color: #fafafa; color: #fafafa;
font-weight: bold; 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; margin: 0px;
padding: 0px; padding: 0px;
height: 100%;
min-width: 150px;
} }
form.realtime table tr td.checkbox-cell div.checkbox-contain { form.realtime table tr td.checkbox-cell div.checkbox-contain,
display: block; div.realtime table tr td.checkbox-cell div.checkbox-contain {
display: inline-block;
height: 100%; height: 100%;
width: 100%; width: 100%;
position: relative; 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; background-color: transparent;
display: block; display: block;
position: absolute; position: absolute;
@ -367,72 +388,138 @@ form.realtime table tr td.checkbox-cell div.checkbox-contain label {
height: 100%; height: 100%;
width: 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; 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; font-weight: bold;
background-color: #FF0073; background-color: #FF0073;
color: #302B28; color: #302B28;
display: block; 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: "✖"; 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; 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: "✔"; 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; display: none;
} }
form.realtime table input[type="text"] { form.realtime table input[type="text"],
div.realtime table input[type="text"] {
height: 100%; height: 100%;
border: 1px solid #302B28;
width: 80%; 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; color: #46E981;
cursor: pointer; cursor: pointer;
width: 10%; float: left;
font-size: 20px; margin-left: 10px;
} /*&:after { content: '✐'; }*/
form.realtime table .edit:after { /*&.editable { display: none; }*/
content: '✐';
} }
form.realtime table .edit.editable { form.realtime table .remove,
display: none; div.realtime table .remove {
float: right;
margin-right: 10px;
} }
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; background-color: transparent;
color: #fafafa; color: #fafafa;
font-weight: bold; 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; cursor: pointer;
font-size: 20px; 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; 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; padding: 15px;
border-top-left-radius: 5px; border-top-left-radius: 5px;
border-top-right-radius: 5px; border-top-right-radius: 5px;
} }
form.realtime #adduser, form.realtime #adduser,
form.realtime #addoption { div.realtime #adduser,
form.realtime #addoption,
div.realtime #addoption {
color: #46E981; color: #46E981;
border: 1px solid #46E981; border: 1px solid #46E981;
padding: 15px; padding: 15px;
cursor: pointer; cursor: pointer;
} }
form.realtime #adduser { form.realtime #adduser,
div.realtime #adduser {
border-top-left-radius: 5px; border-top-left-radius: 5px;
} }
form.realtime #addoption { form.realtime #addoption,
div.realtime #addoption {
border-bottom-left-radius: 5px; border-bottom-left-radius: 5px;
} }
div.modal, div.modal,

@ -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 () { messages._applyTranslation = function () {
$('[data-localization]').each(function (i, e) { $('[data-localization]').each(translateText);
var $el = $(this); $('#pad-iframe').contents().find('[data-localization]').each(translateText);
var key = $el.data('localization'); $('[data-localization-title]').each(translateTitle);
$el.html(messages[key]); $('#pad-iframe').contents().find('[data-localization-title]').each(translateTitle);
});
$('[data-localization-title]').each(function (i, e) {
var $el = $(this);
var key = $el.data('localization-title');
$el.attr('title', messages[key]);
});
}; };
// Non translatable keys // Non translatable keys

@ -359,7 +359,7 @@ tbody {
cursor: pointer !important; cursor: pointer !important;
} }
form.realtime { form.realtime, div.realtime {
> input { > input {
&[type="text"] { &[type="text"] {
@ -375,7 +375,14 @@ form.realtime {
table { table {
border-collapse: collapse; border-collapse: collapse;
width: ~"calc(100% - 1px)";
tr { tr {
td:first-child {
position:absolute;
left: 29px;
top: auto;
width: ~"calc(30% - 50px)";
}
td { td {
padding: 0px; padding: 0px;
margin: 0px; margin: 0px;
@ -383,9 +390,11 @@ form.realtime {
div.text-cell { div.text-cell {
padding: 0px; padding: 0px;
margin: 0px; margin: 0px;
height: 100%; height: 100%;
input { input {
width: 80%; width: 80%;
width: 90%;
height: 100%; height: 100%;
border: 0px; border: 0px;
&[disabled] { &[disabled] {
@ -399,9 +408,11 @@ form.realtime {
&.checkbox-cell { &.checkbox-cell {
margin: 0px; margin: 0px;
padding: 0px; padding: 0px;
height: 100%;
min-width: 150px;
div.checkbox-contain { div.checkbox-contain {
display: block; display: inline-block;
height: 100%; height: 100%;
width: 100%; width: 100%;
position: relative; position: relative;
@ -427,6 +438,11 @@ form.realtime {
background-color: @cp-red; background-color: @cp-red;
color: @base; color: @base;
&:after {
height: 100%;
}
&:after { content: "✖"; } &:after { content: "✖"; }
display: block; display: block;
@ -434,6 +450,12 @@ form.realtime {
background-color: @cp-green; background-color: @cp-green;
&:after { content: "✔"; } &:after { content: "✔"; }
} }
&.uncommitted {
background: #ddd;
}
&.mine { &.mine {
display: none; display: none;
} }
@ -449,17 +471,59 @@ form.realtime {
input { input {
&[type="text"] { &[type="text"] {
height: 100%; height: 100%;
border: 1px solid @base;
width: 80%; 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 { .edit {
color: @cp-green; color: @cp-green;
cursor: pointer; cursor: pointer;
width: 10%; float: left;
font-size: 20px; margin-left: 10px;
&:after { content: '✐'; } /*&:after { content: '✐'; }*/
&.editable { display: none; } /*&.editable { display: none; }*/
}
.remove {
float: right;
margin-right: 10px
} }
thead { thead {
@ -486,7 +550,9 @@ form.realtime {
} }
tfoot { tfoot {
tr { tr {
border: none;
td { td {
border: none;
text-align: center; text-align: center;
.save { .save {
padding: 15px; padding: 15px;

@ -271,7 +271,7 @@
margin: 2px 4px 2px 0px; margin: 2px 4px 2px 0px;
} }
.cryptpad-userbuttons-container { .cryptpad-userbuttons-container {
display: none; /*display: none;*/
} }
} }
.cryptpad-toolbar-rightside { .cryptpad-toolbar-rightside {

@ -1,5 +1,6 @@
@base: #302B28; @base: #302B28;
@light-base: lighten(@base, 20%); @light-base: lighten(@base, 20%);
@less-light-base: lighten(@base, 10%);
@fore: #fafafa; @fore: #fafafa;
@cp-green: #46E981; @cp-green: #46E981;

@ -279,7 +279,7 @@
margin: 2px 4px 2px 0px; margin: 2px 4px 2px 0px;
} }
.cryptpad-toolbar-leftside .cryptpad-userbuttons-container { .cryptpad-toolbar-leftside .cryptpad-userbuttons-container {
display: none; /*display: none;*/
} }
.cryptpad-toolbar-rightside { .cryptpad-toolbar-rightside {
text-align: right; text-align: right;

@ -27,8 +27,8 @@ define(function () {
out.readOnly = 'Solo lectura'; out.readOnly = 'Solo lectura';
out.anonymous = 'Anónimo'; out.anonymous = 'Anónimo';
out.yourself = "tú mismo"; out.yourself = "tú mismo";
out.anonymousUsers = "usuarios anónimos"; out.anonymousUsers = "editores anónimos";
out.anonymousUser = "usuario anónimo"; out.anonymousUser = "editor anónimo";
out.shareView = "URL de sólo lectura"; out.shareView = "URL de sólo lectura";
out.shareEdit = "Editar URL"; out.shareEdit = "Editar URL";
out.users = "Usuarios"; out.users = "Usuarios";

@ -27,8 +27,8 @@ define(function () {
out.readonly = 'Lecture seule'; out.readonly = 'Lecture seule';
out.anonymous = "Anonyme"; out.anonymous = "Anonyme";
out.yourself = "Vous-même"; out.yourself = "Vous-même";
out.anonymousUsers = "utilisateurs anonymes"; out.anonymousUsers = "éditeurs anonymes";
out.anonymousUser = "utilisateur anonyme"; out.anonymousUser = "éditeur anonyme";
out.shareView = "URL de lecture seule"; out.shareView = "URL de lecture seule";
out.shareEdit = "URL d'édition"; out.shareEdit = "URL d'édition";
out.users = "Utilisateurs"; out.users = "Utilisateurs";
@ -129,6 +129,12 @@ define(function () {
out.wizardTitle = "Utiliser l'assistant pour créer votre sondage"; out.wizardTitle = "Utiliser l'assistant pour créer votre sondage";
out.wizardConfirm = "Êtes-vous vraiment prêt à ajouter ces options au 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_closeWizardButton = "Fermer l'assistant";
out.poll_closeWizardButtonTitle = "Fermer l'assistant"; out.poll_closeWizardButtonTitle = "Fermer l'assistant";
out.poll_wizardComputeButton = "Générer les options"; out.poll_wizardComputeButton = "Générer les options";
@ -157,6 +163,51 @@ define(function () {
out.poll_titleHint = "Titre"; out.poll_titleHint = "Titre";
out.poll_descriptionHint = "Description"; 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 // index.html
out.main_p1 = 'CryptPad est l\'éditeur collaboratif en temps réel <strong>zero knowledge</strong>. 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\'<a href="https://fr.wikipedia.org/wiki/Identificateur_de_fragment">identifieur de fragment</a> 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.'; out.main_p1 = 'CryptPad est l\'éditeur collaboratif en temps réel <strong>zero knowledge</strong>. 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\'<a href="https://fr.wikipedia.org/wiki/Identificateur_de_fragment">identifieur de fragment</a> 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.';

@ -21,6 +21,7 @@ define(function () {
].join(''); ].join('');
out.common_connectionLost = 'Server Connection Lost'; out.common_connectionLost = 'Server Connection Lost';
out.websocketError = 'Unable to connect to the websocket server...';
out.disconnected = 'Disconnected'; out.disconnected = 'Disconnected';
out.synchronizing = 'Synchronizing'; out.synchronizing = 'Synchronizing';
@ -29,8 +30,8 @@ define(function () {
out.readonly = 'Read only'; out.readonly = 'Read only';
out.anonymous = "Anonymous"; out.anonymous = "Anonymous";
out.yourself = "Yourself"; out.yourself = "Yourself";
out.anonymousUsers = "anonymous users"; out.anonymousUsers = "anonymous editors";
out.anonymousUser = "anonymous user"; out.anonymousUser = "anonymous editor";
out.shareView = "Read-only URL"; out.shareView = "Read-only URL";
out.shareEdit = "Edit URL"; out.shareEdit = "Edit URL";
out.users = "Users"; out.users = "Users";
@ -131,6 +132,12 @@ define(function () {
out.wizardTitle = "Use the wizard to create your poll"; out.wizardTitle = "Use the wizard to create your poll";
out.wizardConfirm = "Are you really ready to add these options to 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_closeWizardButton = "Close wizard";
out.poll_closeWizardButtonTitle = "Close wizard"; out.poll_closeWizardButtonTitle = "Close wizard";
out.poll_wizardComputeButton = "Compute Options"; out.poll_wizardComputeButton = "Compute Options";
@ -159,6 +166,51 @@ define(function () {
out.poll_titleHint = "Title"; out.poll_titleHint = "Title";
out.poll_descriptionHint = "Description"; 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 // index.html
out.main_p1 = 'CryptPad is the <strong>zero knowledge</strong> 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 <a href="https://en.wikipedia.org/wiki/Fragment_identifier">fragment identifier</a> 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.'; out.main_p1 = 'CryptPad is the <strong>zero knowledge</strong> 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 <a href="https://en.wikipedia.org/wiki/Fragment_identifier">fragment identifier</a> 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.';

@ -42,6 +42,11 @@ define([
secret.keys = secret.key; secret.keys = secret.key;
} }
var onConnectError = function (info) {
module.spinner.hide();
Cryptpad.alert(Messages.websocketError);
};
var andThen = function (CMeditor) { var andThen = function (CMeditor) {
var CodeMirror = module.CodeMirror = CMeditor; var CodeMirror = module.CodeMirror = CMeditor;
CodeMirror.modeURL = "/bower_components/codemirror/mode/%N/%N.js"; 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); var realtime = module.realtime = Realtime.start(config);
editor.on('change', onLocal); editor.on('change', onLocal);
@ -699,6 +706,11 @@ define([
// TODO handle error // TODO handle error
andThen(CM); andThen(CM);
}); });
Cryptpad.onError(function (info) {
if (info && info.type === "store") {
onConnectError();
}
});
}; };
var first = function () { var first = function () {

@ -7,10 +7,11 @@ define([
'/bower_components/spin.js/spin.min.js', '/bower_components/spin.js/spin.min.js',
'/common/clipboard.js', '/common/clipboard.js',
'/customize/fsStore.js',
'/customize/user.js', '/customize/user.js',
'/bower_components/jquery/dist/jquery.min.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 /* This file exposes functionality which is specific to Cryptpad, but not to
any particular pad type. This includes functions for committing metadata any particular pad type. This includes functions for committing metadata
about pads to your local storage for future use and improved usability. about pads to your local storage for future use and improved usability.
@ -19,11 +20,18 @@ define([
*/ */
var $ = window.jQuery; 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 = { var common = {
User: User, User: User,
Messages: Messages, Messages: Messages,
}; };
var store; var store;
var fsStore;
var userProxy; var userProxy;
var userStore; var userStore;
@ -35,7 +43,8 @@ define([
var getStore = common.getStore = function (legacy) { var getStore = common.getStore = function (legacy) {
if (!legacy && userStore) { return userStore; } 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!"); throw new Error("Store is not ready!");
}; };
@ -95,8 +104,8 @@ define([
store = Store; 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) { var fixHTML = common.fixHTML = function (html) {
return html.replace(/</g, '&lt;'); return html.replace(/</g, '&lt;');
@ -164,13 +173,18 @@ define([
}; };
var getHashFromKeys = common.getHashFromKeys = getEditHashFromKeys; var getHashFromKeys = common.getHashFromKeys = getEditHashFromKeys;
var getSecrets = common.getSecrets = function () { var getSecrets = common.getSecrets = function (secretHash) {
var secret = {}; var secret = {};
if (!/#/.test(window.location.href)) { if (/#\?path=/.test(window.location.href)) {
var arr = window.location.hash.match(/\?path=(.+)/);
common.initialPath = arr[1] || undefined;
window.location.hash = '';
}
if (!secretHash && !/#/.test(window.location.href)) {
secret.keys = Crypto.createEditCryptor(); secret.keys = Crypto.createEditCryptor();
secret.key = Crypto.createEditCryptor().editKeyStr; secret.key = Crypto.createEditCryptor().editKeyStr;
} else { } else {
var hash = window.location.hash.slice(1); var hash = secretHash || window.location.hash.slice(1);
if (hash.length === 0) { if (hash.length === 0) {
secret.keys = Crypto.createEditCryptor(); secret.keys = Crypto.createEditCryptor();
secret.key = Crypto.createEditCryptor().editKeyStr; secret.key = Crypto.createEditCryptor().editKeyStr;
@ -386,9 +400,43 @@ define([
}; };
// STORAGE // STORAGE
var forgetFSPad = function (href, cb) {
getStore().forgetPad(href, cb);
};
var forgetPad = common.forgetPad = function (href, cb, legacy) { var forgetPad = common.forgetPad = function (href, cb, legacy) {
var parsed = parsePadUrl(href); var parsed = parsePadUrl(href);
var callback = function (err, data) {
if (err) {
cb(err);
return;
}
getStore(legacy).keys(function (err, keys) {
if (err) {
cb(err);
return;
}
var toRemove = keys.filter(function (k) {
return k.indexOf(parsed.hash) === 0;
});
if (!toRemove.length) {
cb();
return;
}
getStore(legacy).removeBatch(toRemove, function (err, data) {
cb(err, data);
});
});
};
if (USE_FS_STORE && !legacy) {
// TODO implement forgetPad in store.js
forgetFSPad(href, callback);
return;
}
getRecentPads(function (err, recentPads) { getRecentPads(function (err, recentPads) {
setRecentPads(recentPads.filter(function (pad) { setRecentPads(recentPads.filter(function (pad) {
var p = parsePadUrl(pad.href); var p = parsePadUrl(pad.href);
@ -398,31 +446,15 @@ define([
return; return;
} }
return true; return true;
}), function (err, data) { }), callback, legacy);
if (err) { }, legacy);
cb(err);
return;
}
getStore(legacy).keys(function (err, keys) {
if (err) {
cb(err);
return;
}
var toRemove = keys.filter(function (k) {
return k.indexOf(parsed.hash) === 0;
});
if (!toRemove.length) {
cb(); if (typeof(getStore(legacy).forgetPad) === "function") {
return; // TODO implement forgetPad in store.js
} getStore(legacy).forgetPad(href, callback);
getStore(legacy).removeBatch(toRemove, function (err, data) { }
cb(err, data);
});
});
}, legacy);
}, legacy);
}; };
// STORAGE // STORAGE
@ -471,7 +503,11 @@ define([
}); });
if (!contains) { if (!contains) {
renamed.push(makePad(href, name)); var data = makePad(href, name);
renamed.push(data);
if (USE_FS_STORE && typeof(getStore().addPad) === "function") {
getStore().addPad(href, common.initialPath, name);
}
} }
setRecentPads(renamed, function (err, data) { setRecentPads(renamed, function (err, data) {
@ -544,12 +580,16 @@ define([
f(void 0, env); f(void 0, env);
}; };
Store.ready(function (err, store) { storeToUse.ready(function (err, store) {
common.store = env.store = store; common.store = env.store = store;
if (USE_FS_STORE) {
fsStore = store;
}
$(function() { $(function() {
// Race condition : if document.body is undefined when alertify.js is loaded, Alertify // Race condition : if document.body is undefined when alertify.js is loaded, Alertify
// won't work. We have to reset it now to make sure it uses a correct "body" // won't work. We have to reset it now to make sure it uses a correct "body"
Alertify.reset(); Alertify.reset();
if($('#pad-iframe').length) { if($('#pad-iframe').length) {
var $iframe = $('#pad-iframe'); var $iframe = $('#pad-iframe');
@ -606,6 +646,19 @@ define([
cb(); cb();
}); */ }); */
}, common);
};
var errorHandlers = [];
common.onError = function (h) {
if (typeof h !== "function") { return; }
errorHandlers.push(h);
};
common.storeError = function () {
errorHandlers.forEach(function (h) {
if (typeof h === "function") {
h({type: "store"});
}
}); });
}; };
@ -759,7 +812,6 @@ define([
case 'editshare': case 'editshare':
button = $('<button>', { button = $('<button>', {
title: Messages.editShareTitle, title: Messages.editShareTitle,
'class': "button action"
}).text(Messages.editShare); }).text(Messages.editShare);
if (data && data.editHash) { if (data && data.editHash) {
var editHash = data.editHash; var editHash = data.editHash;
@ -778,7 +830,6 @@ define([
case 'viewshare': case 'viewshare':
button = $('<button>', { button = $('<button>', {
title: Messages.viewShareTitle, title: Messages.viewShareTitle,
'class': "button action"
}).text(Messages.viewShare); }).text(Messages.viewShare);
if (data && data.viewHash) { if (data && data.viewHash) {
button.click(function () { button.click(function () {
@ -796,7 +847,6 @@ define([
case 'viewopen': case 'viewopen':
button = $('<button>', { button = $('<button>', {
title: Messages.viewOpenTitle, title: Messages.viewOpenTitle,
'class': "button action"
}).text(Messages.viewOpen); }).text(Messages.viewOpen);
if (data && data.viewHash) { if (data && data.viewHash) {
button.click(function () { button.click(function () {
@ -839,9 +889,10 @@ define([
var styleAlerts = common.styleAlerts = function (href) { var styleAlerts = common.styleAlerts = function (href) {
var $link = $('link[href="/customize/alertify.css"]'); var $link = $('link[href="/customize/alertify.css"]');
if ($link.length) { if ($link.length) {
$link.attr('href', '');
$link.attr('href', '/customize/alertify.css');
return; return;
/*$link.attr('href', '');
$link.attr('href', '/customize/alertify.css');
return;*/
} }
href = href || '/customize/alertify.css'; href = href || '/customize/alertify.css';

@ -202,14 +202,14 @@ define([
var updateUserList = function (myUserName, userlistElement, userList, userData, readOnly, $stateElement, $userAdminElement) { var updateUserList = function (myUserName, userlistElement, userList, userData, readOnly, $stateElement, $userAdminElement) {
var meIdx = userList.indexOf(myUserName); var meIdx = userList.indexOf(myUserName);
if (meIdx === -1) { if (meIdx === -1) {
console.log('nok');
$stateElement.text(Messages.synchronizing); $stateElement.text(Messages.synchronizing);
return; return;
} }
$stateElement.text(''); $stateElement.text('');
// Make sure the elements are displayed // Make sure the elements are displayed
var $userButtons = $(userlistElement).find("#userButtons"); var $userButtons = $(userlistElement).find("#userButtons");
$userButtons.show(); $userButtons.attr('display', 'inline');
var numberOfUsers = userList.length; var numberOfUsers = userList.length;
@ -271,7 +271,7 @@ define([
if (readOnly === 1) { if (readOnly === 1) {
$userElement.html('<span class="' + READONLY_CLS + '">' + Messages.readonly + '</span>'); $userElement.html('<span class="' + READONLY_CLS + '">' + Messages.readonly + '</span>');
} }
else { else {
var name = userData[myUserName] && userData[myUserName].name; var name = userData[myUserName] && userData[myUserName].name;
var icon = '<span class="fa fa-user" style="font-family:FontAwesome;"></span>'; var icon = '<span class="fa fa-user" style="font-family:FontAwesome;"></span>';
if (!name) { if (!name) {
@ -313,12 +313,11 @@ define([
} }
} }
else if (!firstConnection) { else if (!firstConnection) {
lagErrors++;
// Display the red light at the 2nd failed attemp to get the lag // Display the red light at the 2nd failed attemp to get the lag
if (lagErrors > 1) { //if (lagErrors > 1) {
lagLight.addClass('lag-red'); lagLight.addClass('lag-red');
title = Messages.redLight; title = Messages.redLight;
} //}
} }
if (title) { if (title) {
lagLight.attr('title', title); lagLight.attr('title', title);

@ -0,0 +1,302 @@
/* PAGE */
html, body {
width: 100%;
height: 100%;
box-sizing: border-box;
padding: 0;
margin: 0;
position: relative;
font-size: 20px;
overflow: auto;
}
body {
display: flex;
flex-flow: row;
}
.fa {
/*min-width: 17px;*/
margin-right: 3px;
font-family: FontAwesome;
}
ul {
list-style: none;
padding-left: 10px;
}
li {
padding: 0px 5px;
cursor: pointer;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.folder, .file {
margin-right: 5px;
}
.contextMenu {
display: none;
position: absolute;
}
.droppable {
background-color: #FE9A2E;
color: #222;
}
.selected {
border: 1px dotted #bbb;
background: #666;
color: #eee;
margin: -1px;
}
/* TREE */
#tree {
border-right: 1px solid #ccc;
box-sizing: border-box;
background: white;
overflow: auto;
resize: horizontal;
width: 350px;
white-space: nowrap;
max-width: 500px;
min-width: 200px;
padding: 10px 0px;
}
#tree li {
cursor: auto;
}
#tree span.element {
cursor: pointer;
}
#tree li > span.element:hover {
text-decoration: underline;
}
#tree .active {
text-decoration: underline;
}
#tree #trashTree, #tree #unsortedTree, #tree #allfilesTree {
margin-top: 2em;
}
#tree .fa.expcol {
margin-left: -10px;
font-size: 14px;
position: absolute;
left: -20px;
top: 9px;
width: auto;
height: 11px;
padding: 0;
margin: 0;
background: white;
z-index: 10;
cursor: default;
}
#tree .fa.expcol:before {
position:relative;
top: -1px;
}
#tree li.collapsed ul {
display: none;
}
#tree li input {
width: calc(100% - 30px);
}
/* Tree lines */
#tree ul {
margin: 0px 0px 0px 10px;
list-style: none;
}
#tree ul li {
position: relative;
}
#tree ul li:before {
position: absolute;
left: -15px;
top: -0.25em;
content: '';
display: block;
border-left: 1px solid #888;
height: 1em;
border-bottom: 1px solid #888;
width: 17.5px;
}
#tree ul li:after {
position: absolute;
left: -15px;
bottom: -7px;
content: '';
display: block;
border-left: 1px solid #888;
height: 100%;
}
#tree ul li.root {
margin: 0px 0px 0px -10px;
}
#tree ul li.root:before {
display: none;
}
#tree ul li.root:after {
display: none;
}
#tree ul li:last-child:after {
display: none;
}
/* CONTENT */
#content {
box-sizing: border-box;
background: #eee;
overflow: auto;
flex: 1;
display: flex;
flex-flow: column;
}
#content h1 {
padding-left: 10px;
}
.topButtonContainer {
border: 1px solid #ccc;
float: right;
border-radius: 4px;
}
.parentFolder {
cursor: pointer;
margin-left: 10px;
}
.parentFolder:hover {
text-decoration: underline;
}
#folderContent {
padding-right: 10px;
flex: 1;
}
#content li:not(.header) * {
pointer-events: none;
}
#content li:hover:not(.header) .name {
text-decoration: underline;
}
#content .grid li {
display: inline-block;
margin: 10px 10px;
width: 140px;
text-align: center;
vertical-align: top;
}
#content .grid li .name {
width: 100%;
}
#content .grid li input {
width: 100%;
}
#content .grid li .fa {
display: block;
margin: auto;
font-size: 40px;
width: auto;
text-align: center;
}
#content .list li {
display: flex;
flex-flow: row;
align-items: center;
padding-right: 0px;
}
#content .list li .element {
display: inline-flex;
flex: 1;
}
#content .list li.file-header {
margin-top: 20px;
}
#content .list li.header {
cursor: default;
color: #008;
}
#content .list li.header .element span:not(.fa) {
border-right: 1px solid #CCC;
text-align: left;
}
#content .list li.header .element span.fa {
float: right;
}
#content .list li.header span.name {
padding-left: 0;
}
#content .list .element span {
padding: 0px 10px;
display: inline-block;
overflow: hidden;
white-space: nowrap;
box-sizing: border-box;
padding-right: 0px;
border-right: 10px solid rgba(0, 0, 0, 0);
}
#content .list .element span.name {
width: 478px;
}
#content .list .header span.name {
width: 500px;
}
#content .list .element span.type, #content .list .element span.atime, #content .list .element span.ctime {
width: 175px;
}
#content .list .element span.title {
width: 250px;
}
#content .list .element span.folders {
width: 150px;
}
#content .list .element span.files {
width: 150px;
}
#content div.grid .listElement {
display: none;
}
@media screen and (max-width: 1200px) {
#content .list .element span.title {
display: none;
}
}
@media screen and (min-width: 1201px) {
#content .list .element span.title {
display: inline;
}
}

@ -0,0 +1,698 @@
define([
'/customize/messages.js',
'/bower_components/jquery/dist/jquery.min.js',
], function (Messages) {
var $ = window.jQuery;
var module = {};
var ROOT = "root";
var UNSORTED = "unsorted";
var FILES_DATA = "filesData";
var TRASH = "trash";
var NEW_FOLDER_NAME = Messages.fm_newFolder;
var init = module.init = function (files, config) {
FILES_DATA = config.storageKey;
var DEBUG = config.DEBUG || false;
var logging = function () {
console.log.apply(console, arguments);
};
var log = config.log || logging;
var logError = config.logError || logging;
var debug = config.debug || logging;
var exp = {};
var error = exp.error = function() {
exp.fixFiles();
console.error.apply(console, arguments);
};
var comparePath = exp.comparePath = function (a, b) {
if (!a || !b || !$.isArray(a) || !$.isArray(b)) { return false; }
if (a.length !== b.length) { return false; }
var result = true;
var i = a.length - 1;
while (result && i >= 0) {
result = a[i] === b[i];
i--;
}
return result;
};
var isPathInRoot = exp.isPathInRoot = function (path) {
return path[0] && path[0] === ROOT;
};
var isPathInUnsorted = exp.isPathInUnsorted = function (path) {
return path[0] && path[0] === UNSORTED;
};
var isPathInTrash = exp.isPathInTrash = function (path) {
return path[0] && path[0] === TRASH;
};
var isFile = exp.isFile = function (element) {
return typeof(element) === "string";
};
var isFolder = exp.isFolder = function (element) {
return typeof(element) !== "string";
};
var isFolderEmpty = exp.isFolderEmpty = function (element) {
if (typeof(element) !== "object") { return false; }
return Object.keys(element).length === 0;
};
var hasSubfolder = exp.hasSubfolder = function (element, trashRoot) {
if (typeof(element) !== "object") { return false; }
var subfolder = 0;
var addSubfolder = function (el, idx) {
subfolder += isFolder(el.element) ? 1 : 0;
};
for (var f in element) {
if (trashRoot) {
if ($.isArray(element[f])) {
element[f].forEach(addSubfolder);
}
} else {
subfolder += isFolder(element[f]) ? 1 : 0;
}
}
return subfolder;
};
var hasFile = exp.hasFile = function (element, trashRoot) {
if (typeof(element) !== "object") { return false; }
var file = 0;
var addFile = function (el, idx) {
file += isFile(el.element) ? 1 : 0;
};
for (var f in element) {
if (trashRoot) {
if ($.isArray(element[f])) {
element[f].forEach(addFile);
}
} else {
file += isFile(element[f]) ? 1 : 0;
}
}
return file;
};
var isSubpath = exp.isSubpath = function (path, parentPath) {
var pathA = parentPath.slice();
var pathB = path.slice(0, pathA.length);
return comparePath(pathA, pathB);
};
var getAvailableName = function (parentEl, name) {
if (typeof(parentEl[name]) === "undefined") { return name; }
var newName = name;
var i = 1;
while (typeof(parentEl[newName]) !== "undefined") {
newName = name + "_" + i;
i++;
}
return newName;
};
var compareFiles = function (fileA, fileB) {
// Compare string, might change in the future
return fileA === fileB;
};
var isFileInTree = function (file, root) {
if (isFile(root)) {
return compareFiles(file, root);
}
var inTree = false;
for (var e in root) {
inTree = isFileInTree(file, root[e]);
if (inTree) { break; }
}
return inTree;
};
var isFileInTrash = function (file) {
var inTrash = false;
var root = files[TRASH];
var filter = function (trashEl, idx) {
inTrash = isFileInTree(file, trashEl.element);
return inTrash;
};
for (var e in root) {
if (!$.isArray(root[e])) {
error("Trash contains a non-array element");
return;
}
root[e].some(filter);
if (inTrash) { break; }
}
return inTrash;
};
var isFileInUnsorted = function (file) {
return files[UNSORTED].indexOf(file) !== -1;
};
var getUnsortedFiles = exp.getUnsortedFiles = function () {
if (!files[UNSORTED]) {
files[UNSORTED] = [];
}
return files[UNSORTED].slice();
};
var getFilesRecursively = function (root, arr) {
for (var e in root) {
if (isFile(root[e])) {
if(arr.indexOf(root[e]) === -1) { arr.push(root[e]); }
} else {
getFilesRecursively(root[e], arr);
}
}
};
var getRootFiles = function () {
var ret = [];
getFilesRecursively(files[ROOT], ret);
return ret;
};
var getTrashFiles = exp.getTrashFiles = function () {
var root = files[TRASH];
var ret = [];
var addFiles = function (el, idx) {
if (isFile(el.element)) {
if(ret.indexOf(el.element) === -1) { ret.push(el.element); }
} else {
getFilesRecursively(el.element, ret);
}
};
for (var e in root) {
if (!$.isArray(root[e])) {
error("Trash contains a non-array element");
return;
}
root[e].forEach(addFiles);
}
return ret;
};
var getFilesDataFiles = function () {
var ret = [];
for (var el in files[FILES_DATA]) {
if (el.href && ret.indexOf(el.href) === -1) {
ret.push(el.href);
}
}
return ret;
};
var removeFileFromRoot = function (root, href) {
if (isFile(root)) { return; }
for (var e in root) {
if (isFile(root[e])) {
if (compareFiles(href, root[e])) {
delete root[e];
}
} else {
removeFileFromRoot(root[e], href);
}
}
};
var isInTrashRoot = exp.isInTrashRoot = function (path) {
return path[0] === TRASH && path.length === 4;
};
var checkDeletedFiles = function () {
var rootFiles = getRootFiles();
var unsortedFiles = getUnsortedFiles();
var trashFiles = getTrashFiles();
var toRemove = [];
files[FILES_DATA].forEach(function (arr) {
var f = arr.href;
if (rootFiles.indexOf(f) === -1
&& unsortedFiles.indexOf(f) === -1
&& trashFiles.indexOf(f) === -1) {
toRemove.push(arr);
}
});
toRemove.forEach(function (f) {
var idx = files[FILES_DATA].indexOf(f);
if (idx !== -1) {
debug("Removing", f, "from filesData");
files[FILES_DATA].splice(idx, 1);
}
});
};
var deleteFromObject = exp.deletePathPermanently = function (path) {
var parentPath = path.slice();
var key = parentPath.pop();
var parentEl = exp.findElement(files, parentPath);
if (path.length === 4 && path[0] === TRASH) {
files[TRASH][path[1]].splice(path[2], 1);
} else if (path[0] === UNSORTED) {
parentEl.splice(key, 1);
} else {
delete parentEl[key];
}
checkDeletedFiles();
};
// Find an element in a object following a path, resursively
var findElement = exp.findElement = function (root, pathInput) {
if (!pathInput) {
error("Invalid path:\n", pathInput, "\nin root\n", root);
return;
}
if (pathInput.length === 0) { return root; }
var path = pathInput.slice();
var key = path.shift();
if (typeof root[key] === "undefined") {
debug("Unable to find the key '" + key + "' in the root object provided:", root);
return;
}
return findElement(root[key], path);
};
var getTrashElementData = exp.getTrashElementData = function (trashPath) {
if (!isInTrashRoot) {
debug("Called getTrashElementData on a element not in trash root: ", trashPath);
return;
}
var parentPath = trashPath.slice();
parentPath.pop();
return findElement(files, parentPath);
};
var getFileData = exp.getFileData = function (file) {
if (!file) { return; }
var res;
files[FILES_DATA].some(function(arr) {
var href = arr.href;
if (href === file) {
res = arr;
return true;
}
return false;
});
return res;
};
// Data from filesData
var getTitle = exp.getTitle = function (href) {
var data = getFileData(href);
if (!href || !data) {
error("getTitle called with a non-existing href: ", href);
return;
}
return data.title;
};
var pushToTrash = function (name, element, path) {
var trash = findElement(files, [TRASH]);
if (typeof(trash[name]) === "undefined") {
trash[name] = [];
}
var trashArray = trash[name];
var trashElement = {
element: element,
path: path
};
trashArray.push(trashElement);
};
// Move to trash
var removeElement = exp.removeElement = function (path, cb, keepOld) {
if (!path || path.length < 2 || path[0] === TRASH) {
debug("Calling removeElement from a wrong path: ", path);
return;
}
var element = findElement(files, path);
var key = path[path.length - 1];
var name = isPathInUnsorted(path) ? getTitle(element) : key;
var parentPath = path.slice();
parentPath.pop();
pushToTrash(name, element, parentPath);
if (!keepOld) { deleteFromObject(path); }
if (cb) { cb(); }
};
var moveElement = exp.moveElement = function (elementPath, newParentPath, cb, keepOld) {
if (comparePath(elementPath, newParentPath)) { return; } // Nothing to do...
if (isPathInTrash(newParentPath)) {
removeElement(elementPath, cb, keepOld);
return;
}
var element = findElement(files, elementPath);
var newParent = findElement(files, newParentPath);
if (isFolder(element) && isSubpath(newParentPath, elementPath)) {
log(Messages.fo_moveFolderToChildError);
return;
}
if (isPathInUnsorted(newParentPath)) {
if (isFolder(element)) {
log(Messages.fo_moveUnsortedError);
return;
} else {
if (isPathInUnsorted(elementPath)) { return; }
if (files[UNSORTED].indexOf(element) === -1) {
files[UNSORTED].push(element);
}
if (!keepOld) { deleteFromObject(elementPath); }
if(cb) { cb(); }
return;
}
}
var name;
if (isPathInUnsorted(elementPath)) {
name = getTitle(element);
} else if (isInTrashRoot(elementPath)) {
// Element from the trash root: elementPath = [TRASH, "{dirName}", 0, 'element']
name = elementPath[1];
} else {
name = elementPath[elementPath.length-1];
}
var newName = !isPathInRoot(elementPath) ? getAvailableName(newParent, name) : name;
if (typeof(newParent[newName]) !== "undefined") {
log("A file with the same name already exist at the new location. Rename the file and try again.");
return;
}
newParent[newName] = element;
if (!keepOld) { deleteFromObject(elementPath); }
if(cb) { cb(); }
};
// "Unsorted" is an array of href: we can't move several of them using "moveElement" in a
// loop because moveElement removes the href from the array and it changes the path for all
// the other elements. We have to move them all and then remove them from unsorted
var moveUnsortedElements = exp.moveUnsortedElements = function (paths, newParentPath, cb) {
if (!paths || paths.length === 0) { return; }
if (isPathInUnsorted(newParentPath)) { return; }
var elements = {};
// Get the elements
paths.forEach(function (p) {
if (!isPathInUnsorted(p)) { return; }
var el = findElement(files, p);
if (el) { elements[el] = p; }
});
// Copy the elements to their new location
Object.keys(elements).forEach(function (el) {
moveElement(elements[el], newParentPath, null, true);
});
// Remove the elements from their old location
Object.keys(elements).forEach(function (el) {
var idx = files[UNSORTED].indexOf(el);
if (idx !== -1) {
files[UNSORTED].splice(idx, 1);
}
});
if (cb) { cb(); }
};
var moveElements = exp.moveElements = function (paths, newParentPath, cb) {
var unsortedPaths = paths.filter(function (p) {
return p[0] === UNSORTED;
});
moveUnsortedElements(unsortedPaths, newParentPath);
// Copy the elements to their new location
paths.forEach(function (p) {
if (isPathInUnsorted(p)) { return; }
moveElement(p, newParentPath, null);
});
if(cb) { cb(); }
};
var createNewFolder = exp.createNewFolder = function (folderPath, name, cb) {
var parentEl = findElement(files, folderPath);
var folderName = getAvailableName(parentEl, name || NEW_FOLDER_NAME);
parentEl[folderName] = {};
var newPath = folderPath.slice();
newPath.push(folderName);
cb({
newPath: newPath
});
};
// Remove an element from the trash root
var removeFromTrashArray = function (element, name) {
var array = files[TRASH][name];
if (!array || !$.isArray(array)) { return; }
// Remove the element from the trash array
var index = array.indexOf(element);
if (index > -1) {
array.splice(index, 1);
}
// Remove the array is empty to have a cleaner object in chainpad
if (array.length === 0) {
delete files[TRASH][name];
}
};
// Restore an element (copy it elsewhere and remove from the trash root)
var restoreTrash = exp.restoreTrash = function (path, cb) {
if (!path || path.length !== 4 || path[0] !== TRASH) {
debug("restoreTrash was called from an element not in the trash root: ", path);
return;
}
var element = findElement(files, path);
var parentEl = getTrashElementData(path);
var newPath = parentEl.path;
if (isPathInUnsorted(newPath)) {
if (files[UNSORTED].indexOf(element) === -1) {
files[UNSORTED].push(element);
removeFromTrashArray(parentEl, path[1]);
cb();
}
return;
}
// Find the new parent element
var newParentEl = findElement(files, newPath);
while (newPath.length > 1 && !newParentEl) {
newPath.pop();
newParentEl = findElement(files, newPath);
}
if (!newParentEl) {
log(Messages.fo_unableToRestore);
}
var name = getAvailableName(newParentEl, path[1]);
// Move the element
newParentEl[name] = element;
removeFromTrashArray(parentEl, path[1]);
cb();
};
// Delete permanently (remove from the trash root and from filesData)
var removeFromTrash = exp.removeFromTrash = function (path, cb) {
if (!path || path.length < 4 || path[0] !== TRASH) { return; }
// Remove the last element from the path to get the parent path and the element name
var parentPath = path.slice();
var name;
var element = findElement(files, path);
if (path.length === 4) { // Trash root
name = path[1];
parentPath.pop();
var parentElement = findElement(files, parentPath);
removeFromTrashArray(parentElement, name);
} else {
name = parentPath.pop();
var parentEl = findElement(files, parentPath);
if (typeof(parentEl[name]) === "undefined") {
logError("Unable to locate the element to remove from trash: ", path);
return;
}
delete parentEl[name];
}
checkDeletedFiles();
if(cb) { cb(); }
};
var emptyTrash = exp.emptyTrash = function (cb) {
files[TRASH] = {};
if(cb) { cb(); }
};
var renameElement = exp.renameElement = function (path, newName, cb) {
if (path.length <= 1) {
logError('Renaming `root` is forbidden');
return;
}
if (!newName || newName.trim() === "") { return; }
// Copy the element path and remove the last value to have the parent path and the old name
var element = findElement(files, path);
var parentPath = path.slice();
var oldName = parentPath.pop();
if (oldName === newName) {
return;
}
var parentEl = findElement(files, parentPath);
if (typeof(parentEl[newName]) !== "undefined") {
log(Messages.fo_existingNameError);
return;
}
parentEl[newName] = element;
delete parentEl[oldName];
cb();
};
var forgetPad = exp.forgetPad = function (href) {
var rootFiles = getRootFiles().slice();
if (rootFiles.indexOf(href) !== -1) {
removeFileFromRoot(files[ROOT], href);
}
var unsortedIdx = getUnsortedFiles().indexOf(href);
if (unsortedIdx !== -1) {
files[UNSORTED].splice(unsortedIdx, 1);
}
var key = getTitle(href);
pushToTrash(key, href, [UNSORTED]);
};
var addUnsortedPad = exp.addPad = function (href, path, name) {
var unsortedFiles = getUnsortedFiles();
var rootFiles = getRootFiles();
var trashFiles = getTrashFiles();
if (path && name) {
var newPath = decodeURIComponent(path).split(',');
var parentEl = findElement(files, newPath);
if (parentEl) {
var newName = getAvailableName(parentEl, name);
parentEl[newName] = href;
return;
}
}
if (unsortedFiles.indexOf(href) === -1 && rootFiles.indexOf(href) === -1 && trashFiles.indexOf(href) === -1) {
files[UNSORTED].push(href);
}
};
var uniq = function (a) {
var seen = {};
return a.filter(function(item) {
return seen.hasOwnProperty(item) ? false : (seen[item] = true);
});
};
var fixFiles = exp.fixFiles = function () {
// Explore the tree and check that everything is correct:
// * 'root', 'trash' and 'filesData' exist and are objects
// * ROOT: Folders are objects, files are href
// * TRASH: Trash root contains only arrays, each element of the array is an object {element:.., path:..}
// * FILES_DATA: - Data (title, cdate, adte) are stored in filesData. filesData contains only href keys linking to object with title, cdate, adate.
// - Dates (adate, cdate) can be parsed/formatted
// - All files in filesData should be either in 'root', 'trash' or 'unsorted'. If that's not the case, copy the fily to 'unsorted'
// * UNSORTED: Contains only files (href), and does not contains files that are in ROOT
debug("Cleaning file system...");
// Create a backup
if (typeof(localStorage.oldFileSystem) === "undefined") {
localStorage.oldFileSystem = '[]';
}
var before = JSON.stringify(files);
if (typeof(files[ROOT]) !== "object") { debug("ROOT was not an object"); files[ROOT] = {}; }
if (typeof(files[TRASH]) !== "object") { debug("TRASH was not an object"); files[TRASH] = {}; }
if (!$.isArray(files[FILES_DATA])) { debug("FILES_DATA was not an array"); files[FILES_DATA] = []; }
if (!$.isArray(files[UNSORTED])) { debug("UNSORTED was not an array"); files[UNSORTED] = []; }
var fixRoot = function (element) {
for (var el in element) {
if (!isFile(element[el]) && !isFolder(element[el])) {
debug("An element in ROOT was not a folder nor a file. ", element[el]);
delete element[el];
} else if (isFolder(element[el])) {
fixRoot(element[el]);
}
}
};
fixRoot(files[ROOT]);
var fixTrashRoot = function (tr) {
var toClean;
var addToClean = function (obj, idx) {
if (typeof(obj) !== "object") { toClean.push(idx); return; }
if (!isFile(obj.element) && !isFolder(obj.element)) { toClean.push(idx); return; }
if (!$.isArray(obj.path)) { toClean.push(idx); return; }
};
for (var el in tr) {
if (!$.isArray(tr[el])) {
debug("An element in TRASH root is not an array. ", tr[el]);
delete tr[el];
} else {
toClean = [];
tr[el].forEach(addToClean);
for (var i = toClean.length-1; i>=0; i--) {
tr[el].splice(toClean[i], 1);
}
}
}
};
fixTrashRoot(files[TRASH]);
var fixUnsorted = function (us) {
var rootFiles = getRootFiles().slice();
var toClean = [];
us.forEach(function (el, idx) {
if (!isFile(el) || rootFiles.indexOf(el) !== -1) {
toClean.push(idx);
}
});
};
files[UNSORTED] = uniq(files[UNSORTED]);
fixUnsorted(files[UNSORTED]);
var fixFilesData = function (fd) {
var rootFiles = getRootFiles();
var unsortedFiles = getUnsortedFiles();
var trashFiles = getTrashFiles();
var toClean = [];
fd.forEach(function (el, idx) {
if (typeof(el) !== "object") {
debug("An element in filesData was not an object.", el);
toClean.push(el);
} else {
if (rootFiles.indexOf(el.href) === -1
&& unsortedFiles.indexOf(el.href) === -1
&& trashFiles.indexOf(el.href) === -1) {
debug("An element in filesData was not in ROOT, UNSORTED or TRASH.", el);
files[UNSORTED].push(el.href);
}
}
});
toClean.forEach(function (el) {
var idx = fd.indexOf(el);
if (idx !== -1) {
fd.splice(idx, 1);
}
});
};
fixFilesData(files[FILES_DATA]);
if (JSON.stringify(files) !== before) {
var backup = JSON.parse(localStorage.oldFileSystem);
backup.push(files);
localStorage.oldFileSystem = JSON.stringify(backup);
debug("Your file system was corrupted. It has been cleaned so that the pads you visit can be stored safely");
return;
}
debug("File system was clean");
};
return exp;
};
return module;
});

@ -0,0 +1,41 @@
<!DOCTYPE html>
<html>
<head>
<meta content="text/html; charset=utf-8" http-equiv="content-type"/>
<link rel="icon" type="image/png"
href="/customize/main-favicon.png"
data-main-favicon="/customize/main-favicon.png"
data-alt-favicon="/customize/alt-favicon.png"
id="favicon" />
<link rel="stylesheet" href="/customize/main.css" />
<script data-main="main" src="/bower_components/requirejs/require.js"></script>
<script>
require.config({
waitSeconds: 60,
});
</script>
<style>
html, body {
margin: 0px;
padding: 0px;
}
#pad-iframe {
position:fixed;
top:2.5em;
left:0px;
bottom:0px;
right:0px;
width:100%;
height:calc(100% - 2.5em);
border:none;
margin:0;
padding:0;
overflow:hidden;
}
</style>
</head>
<body>
<iframe id="pad-iframe" src="inner.html"></iframe>
</body>
</html>

@ -0,0 +1,46 @@
<!DOCTYPE html>
<html>
<head>
<meta content="text/html; charset=utf-8" http-equiv="content-type"/>
<link rel="stylesheet" href="/bower_components/components-font-awesome/css/font-awesome.min.css">
<script src="/bower_components/jquery/dist/jquery.min.js"></script>
<link rel="stylesheet" href="/bower_components/bootstrap/dist/css/bootstrap.min.css">
<link rel="stylesheet" href="file.css" />
</head>
<body>
<div id="tree">
</div>
<div id="content">
</div>
<div id="contextMenu" class="contextMenu dropdown clearfix" oncontextmenu="return false;">
<ul class="dropdown-menu" role="menu" aria-labelledby="dropdownMenu" style="display:block;position:static;margin-bottom:5px;">
<li><a tabindex="-1" href="#" class="open" data-localization="fc_open">Open</a></li>
<li><a tabindex="-1" href="#" class="rename" data-localization="fc_rename">Rename</a></li>
<li><a tabindex="-1" href="#" class="delete" data-localization="fc_delete">Delete</a></li>
<li><a tabindex="-1" href="#" class="newfolder" data-localization="fc_newfolder">New folder</a></li>
</ul>
</div>
<div id="contentContextMenu" class="contextMenu dropdown clearfix" oncontextmenu="return false;">
<ul class="dropdown-menu" role="menu" aria-labelledby="dropdownMenu" style="display:block;position:static;margin-bottom:5px;">
<li><a tabindex="-1" href="#" class="newfolder" data-localization="fc_newfolder">New folder</a></li>
<li><a tabindex="-1" href="#" class="newdoc" data-type="pad" data-localization="fc_newpad" target="_blank">New pad</a></li>
<li><a tabindex="-1" href="#" class="newdoc" data-type="code" data-localization="fc_newcode" target="_blank">New code</a></li>
<li><a tabindex="-1" href="#" class="newdoc" data-type="slide" data-localization="fc_newslide" target="_blank">New slide</a></li>
<li><a tabindex="-1" href="#" class="newdoc" data-type="poll" data-localization="fc_newpoll" target="_blank">New poll</a></li>
</ul>
</div>
<div id="trashTreeContextMenu" class="contextMenu dropdown clearfix" oncontextmenu="return false;">
<ul class="dropdown-menu" role="menu" aria-labelledby="dropdownMenu" style="display:block;position:static;margin-bottom:5px;">
<li><a tabindex="-1" href="#" class="empty" data-localization="fc_empty">Empty the trash</a></li>
</ul>
</div>
<div id="trashContextMenu" class="contextMenu dropdown clearfix" oncontextmenu="return false;">
<ul class="dropdown-menu" role="menu" aria-labelledby="dropdownMenu" style="display:block;position:static;margin-bottom:5px;">
<li><a tabindex="-1" href="#" class="remove" data-localization="fc_remove">Delete permanently</a></li>
<li><a tabindex="-1" href="#" class="restore" data-localization="fc_restore">Restore</a></li>
<li><a tabindex="-1" href="#" class="properties" data-localization="fc_prop">Properties</a></li>
</ul>
</div>
</body>
</html>

File diff suppressed because it is too large Load Diff

@ -77,6 +77,11 @@ define([
return hj; return hj;
}; };
var onConnectError = function (info) {
module.spinner.hide();
Cryptpad.alert(Messages.websocketError);
};
var andThen = function (Ckeditor) { var andThen = function (Ckeditor) {
var secret = Cryptpad.getSecrets(); var secret = Cryptpad.getSecrets();
var readOnly = secret.keys && !secret.keys.editKeyStr; var readOnly = secret.keys && !secret.keys.editKeyStr;
@ -691,6 +696,8 @@ define([
} }
}; };
var onError = realtimeOptions.onError = onConnectError;
var onLocal = realtimeOptions.onLocal = function () { var onLocal = realtimeOptions.onLocal = function () {
if (initializing) { return; } if (initializing) { return; }
if (readOnly) { return; } if (readOnly) { return; }
@ -736,6 +743,11 @@ define([
// TODO handle error // TODO handle error
andThen(Ckeditor); andThen(Ckeditor);
}); });
Cryptpad.onError(function (info) {
if (info && info.type === "store") {
onConnectError();
}
});
}; };
var first = function () { var first = function () {

@ -0,0 +1,121 @@
<!DOCTYPE html>
<html>
<head>
<meta content="text/html; charset=utf-8" http-equiv="content-type"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title data-localization="poll_title">Zero Knowledge Date Picker</title>
<link rel="stylesheet" href="/bower_components/components-font-awesome/css/font-awesome.min.css">
<link rel="stylesheet" href="/customize/main.css" />
<script data-main="main" src="/bower_components/requirejs/require.js"></script>
<script> require.config({ waitSeconds: 60, }); </script>
<style>
html, body {
width: 100%;
height: 100%;
margin: 0px;
padding: 0px;
border: 0px;
}
.cryptpad-toolbar h2 {
font: normal normal normal 12px Arial, Helvetica, Tahoma, Verdana, Sans-Serif;
color: #000;
line-height: auto;
}
.realtime {
display: block;
overflow: auto;
max-height: 100%;
max-width: 100%;
}
.realtime input[type="text"] {
height: 1em;
margin: 0px;
}
.text-cell input[type="text"] {
width: 400px;
}
/*input#title, textarea { wiidth: 50px; }*/
input[type="text"][disabled], textarea[disabled] {
background-color: transparent;
font: white;
border: 0px;
}
td label {
border: .5px solid black;
}
table#table {
margin: 0px;
}
#tableContainer {
position: relative;
padding: 29px;
padding-right: 79px;
}
#tableContainer button {
height: 2rem;
display: none;
}
#publish {
display: none;
}
#create-user {
position: absolute;
display: inline-block;
/*left: 0px;*/
top: 55px;
width: 50px;
overflow: hidden;
}
#create-option {
width: 50px;
}
#tableScroll {
overflow-x: auto;
margin-left: calc(30% - 50px + 29px);
max-width: 70%;
width: auto;
display: inline-block;
}
#description[disabled] {
resize: none;
}
#description {
padding: 5px;
}
#commit {
width: 100%;
}
</style>
</head>
<body>
<div id="toolbar"></div>
<div id="howItWorks">
<h1 id="mainTitle">CryptPoll</h1>
<h2 data-localization="poll_subtitle"></h2>
<p data-localization="poll_p_save"></p>
<p data-localization="poll_p_encryption"></p>
</div>
<button id="publish" data-localization-title="poll_publish_button" data-localization="poll_publish_button" style="display: none;">publish poll</button>
<button id="admin" data-localization-title="poll_admin_button" data-localization="poll_admin_button" style="display: none;">admin</button>
<div class="realtime">
<br />
<textarea rows=5 cols=50 disabled="disabled" id="description"></textarea><br />
<div id="tableContainer">
<div id="tableScroll"></div>
<button data-localization-title="poll_create_user" id="create-user"><span class="fa fa-plus"></span></button>
<button data-localization-title="poll_create_option" id="create-option"><span class="fa fa-plus"></span></button>
<button data-localization-title="poll_commit" id="commit"><span class="fa fa-check"></span></button>
</div>
</div>

@ -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();
}
});
});
});

@ -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;
});

@ -59,6 +59,11 @@ define([
var presentMode = Slide.isPresentURL(); var presentMode = Slide.isPresentURL();
var onConnectError = function (info) {
module.spinner.hide();
Cryptpad.alert(Messages.websocketError);
};
var andThen = function (CMeditor) { var andThen = function (CMeditor) {
var CodeMirror = module.CodeMirror = CMeditor; var CodeMirror = module.CodeMirror = CMeditor;
CodeMirror.modeURL = "/bower_components/codemirror/mode/%N/%N.js"; 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); var realtime = module.realtime = Realtime.start(config);
editor.on('change', onLocal); editor.on('change', onLocal);
@ -798,6 +805,11 @@ define([
// TODO handle error // TODO handle error
andThen(CM); andThen(CM);
}); });
Cryptpad.onError(function (info) {
if (info && info.type === "store") {
onConnectError();
}
});
}; };
var first = function () { var first = function () {

Loading…
Cancel
Save