Merge branch 'soon'

pull/1/head
ansuz 7 years ago
commit 3c9a025a4a

@ -36,7 +36,10 @@ var nt = nThen(function (waitFor) {
}).nThen;
[
// login test must happen after register test
['/register/', {}],
['/login/', {}],
['/assert/', {}],
['/auth/', {}],
@ -49,6 +52,9 @@ var nt = nThen(function (waitFor) {
['/slide/#/1/edit/uwKqgj8Ezh2dRaFUWSlrRQ/JkJtAb-hNzfESZEHreAeULU1/', {}],
['/slide/#/1/view/uwKqgj8Ezh2dRaFUWSlrRQ/Xa8jXl+jWMpwep41mlrhkqbRuVKGxlueH80Pbgeu5Go/', {}],
['/poll/#/1/edit/lHhnKHSs0HBsl2UGfSJoLw/ZXSsAq4BORIixuFaLVBFcxoq/', {}],
['/poll/#/1/view/lHhnKHSs0HBsl2UGfSJoLw/TGul8PhswwLh1klHpBto6yEntWtKES2+tetYrrYec4M/', {}]
].forEach(function (x) {
if (failed) { return; }
var url = 'http://localhost:3000' + x[0];

@ -68,5 +68,7 @@ define(function() {
config.displayCreationScreen = false;
config.disableAnonymousStore = false;
return config;
});

@ -4,7 +4,7 @@ define([
'/bower_components/chainpad-crypto/crypto.js',
'/common/common-util.js',
'/common/outer/network-config.js',
'/common/credential.js',
'/customize/credential.js',
'/bower_components/chainpad/chainpad.dist.js',
'/bower_components/tweetnacl/nacl-fast.min.js',

@ -1,25 +1,12 @@
define([
'jquery',
'/customize/application_config.js',
'/common/cryptpad-common.js',
'/common/common-interface.js',
'/common/common-realtime.js',
'/common/common-constants.js',
'/common/outer/local-store.js',
'/customize/messages.js',
], function ($, Config, Cryptpad, UI, Realtime, Constants, LocalStore, Messages) {
window.APP = {
Cryptpad: Cryptpad,
};
], function ($, LocalStore, Messages) {
$(function () {
var $main = $('#mainBlock');
$(window).click(function () {
$('.cp-dropdown-content').hide();
});
// main block is hidden in case javascript is disabled
$main.removeClass('hidden');
@ -34,113 +21,9 @@ define([
$main.find('a[href="/drive/"] div.pad-button-text h4')
.text(Messages.main_yourCryptDrive);
var name = localStorage[Constants.userNameKey] || sessionStorage[Constants.userNameKey];
var $loggedInBlock = $main.find('#loggedIn');
var $hello = $loggedInBlock.find('#loggedInHello');
var $logout = $loggedInBlock.find('#loggedInLogOut');
if (name) {
$hello.text(Messages._getKey('login_hello', [name]));
} else {
$hello.text(Messages.login_helloNoName);
}
$('#buttons').find('.nologin').hide();
$logout.click(function () {
LocalStore.logout(function () {
window.location.reload();
});
});
$loggedInBlock.removeClass('hidden');
}
else {
$main.find('#userForm').removeClass('hidden');
$('#name').focus();
}
/* Log in UI */
var Login;
// deferred execution to avoid unnecessary asset loading
var loginReady = function (cb) {
if (Login) {
if (typeof(cb) === 'function') { cb(); }
return;
}
require([
'/common/login.js',
], function (_Login) {
Login = Login || _Login;
if (typeof(cb) === 'function') { cb(); }
});
};
var $uname = $('#name').on('focus', loginReady);
var $passwd = $('#password')
// background loading of login assets
.on('focus', loginReady)
// enter key while on password field clicks signup
.on('keyup', function (e) {
if (e.which !== 13) { return; } // enter
$('button.login').click();
});
$('button.login').click(function () {
// setTimeout 100ms to remove the keyboard on mobile devices before the loading screen pops up
window.setTimeout(function () {
UI.addLoadingScreen({loadingText: Messages.login_hashing});
// We need a setTimeout(cb, 0) otherwise the loading screen is only displayed after hashing the password
window.setTimeout(function () {
loginReady(function () {
var uname = $uname.val();
var passwd = $passwd.val();
Login.loginOrRegister(uname, passwd, false, function (err, result) {
if (!err) {
var proxy = result.proxy;
// successful validation and user already exists
// set user hash in localStorage and redirect to drive
if (proxy && !proxy.login_name) {
proxy.login_name = result.userName;
}
proxy.edPrivate = result.edPrivate;
proxy.edPublic = result.edPublic;
Realtime.whenRealtimeSyncs(result.realtime, function () {
LocalStore.login(result.userHash, result.userName, function () {
document.location.href = '/drive/';
});
});
return;
}
switch (err) {
case 'NO_SUCH_USER':
UI.removeLoadingScreen(function () {
UI.alert(Messages.login_noSuchUser);
});
break;
case 'INVAL_USER':
UI.removeLoadingScreen(function () {
UI.alert(Messages.login_invalUser);
});
break;
case 'INVAL_PASS':
UI.removeLoadingScreen(function () {
UI.alert(Messages.login_invalPass);
});
break;
default: // UNHANDLED ERROR
UI.errorLoadingScreen(Messages.login_unhandledError);
}
});
});
}, 0);
}, 100);
$(window).click(function () {
$('.cp-dropdown-content').hide();
});
/* End Log in UI */
console.log("ready");
});
});

@ -2,8 +2,9 @@ define([
'/api/config',
'/common/hyperscript.js',
'/customize/messages.js',
'jquery'
], function (Config, h, Msg, $) {
'jquery',
'/customize/application_config.js',
], function (Config, h, Msg, $, AppConfig) {
var Pages = {};
var urlArgs = Config.requireConf.urlArgs;
@ -71,7 +72,7 @@ define([
])
])
]),
h('div.cp-version-footer', "CryptPad v1.23.0 (Xenomorph)")
h('div.cp-version-footer', "CryptPad v1.24.0 (Yeti)")
]);
};
@ -374,8 +375,61 @@ define([
]);
};
var isAvailableType = function (x) {
if (!Array.isArray(AppConfig.availablePadTypes)) { return true; }
return AppConfig.availablePadTypes.some(function (type) {
return x.indexOf(type) > -1;
});
};
Pages['/'] = Pages['/index.html'] = function () {
var showingMore = false;
var icons = [
[ 'pad', '/pad/', Msg.main_richTextPad, 'fa-file-word-o' ],
[ 'code', '/code/', Msg.main_codePad, 'fa-file-code-o' ],
[ 'slide', '/slide/', Msg.main_slidePad, 'fa-file-powerpoint-o' ],
[ 'poll', '/poll/', Msg.main_pollPad, 'fa-calendar' ],
[ 'whiteboard', '/whiteboard/', Msg.main_whiteboardPad, 'fa-paint-brush' ],
[ 'recent', '/drive/', Msg.main_localPads, 'fa-hdd-o' ]
].filter(function (x) {
return isAvailableType(x[1]);
})
.map(function (x, i) {
var s = 'div.bs-callout.cp-callout-' + x[0];
if (i > 2) { s += '.cp-more.cp-hidden'; }
return h('a', [
{ href: x[1] },
h(s, [
h('i.fa.' + x[3]),
h('div.pad-button-text', [ h('h4', x[2]) ])
])
]);
});
var more = icons.length < 4? undefined: h('div.bs-callout.cp-callout-more', [
h('div.cp-callout-more-lessmsg.cp-hidden', [
"see less ",
h('i.fa.fa-caret-up')
]),
h('div.cp-callout-more-moremsg', [
"see more ",
h('i.fa.fa-caret-down')
]),
{
onclick: function () {
if (showingMore) {
$('.cp-more, .cp-callout-more-lessmsg').addClass('cp-hidden');
$('.cp-callout-more-moremsg').removeClass('cp-hidden');
} else {
$('.cp-more, .cp-callout-more-lessmsg').removeClass('cp-hidden');
$('.cp-callout-more-moremsg').addClass('cp-hidden');
}
showingMore = !showingMore;
}
}
]);
return [
h('div#cp-main', [
infopageTopbar(),
@ -387,44 +441,8 @@ define([
h('p', Msg.main_catch_phrase)
]),
h('div.col-12.col-sm-6', [
[
[ 'pad', '/pad/', Msg.main_richTextPad, 'fa-file-word-o' ],
[ 'code', '/code/', Msg.main_codePad, 'fa-file-code-o' ],
[ 'slide', '/slide/', Msg.main_slidePad, 'fa-file-powerpoint-o' ],
[ 'poll.cp-more.cp-hidden', '/poll/', Msg.main_pollPad, 'fa-calendar' ],
[ 'whiteboard.cp-more.cp-hidden', '/whiteboard/', Msg.main_whiteboardPad, 'fa-paint-brush' ],
[ 'recent.cp-more.cp-hidden', '/drive/', Msg.main_localPads, 'fa-hdd-o' ]
].map(function (x) {
return h('a', [
{ href: x[1] },
h('div.bs-callout.cp-callout-' + x[0], [
h('i.fa.' + x[3]),
h('div.pad-button-text', [ h('h4', x[2]) ])
])
]);
}),
h('div.bs-callout.cp-callout-more', [
h('div.cp-callout-more-lessmsg.cp-hidden', [
"see less ",
h('i.fa.fa-caret-up')
]),
h('div.cp-callout-more-moremsg', [
"see more ",
h('i.fa.fa-caret-down')
]),
{
onclick: function () {
if (showingMore) {
$('.cp-more, .cp-callout-more-lessmsg').addClass('cp-hidden');
$('.cp-callout-more-moremsg').removeClass('cp-hidden');
} else {
$('.cp-more, .cp-callout-more-lessmsg').removeClass('cp-hidden');
$('.cp-callout-more-moremsg').addClass('cp-hidden');
}
showingMore = !showingMore;
}
}
])
icons,
more
])
])
]),
@ -552,6 +570,19 @@ define([
'name': 'password',
placeholder: Msg.login_password,
}),
h('div.checkbox-container', [
h('input#import-recent', {
name: 'import-recent',
type: 'checkbox',
checked: true
}),
// hscript doesn't generate for on label for some
// reason... use jquery as a temporary fallback
setHTML($('<label for="import-recent"></label>')[0], Msg.register_importRecent)
/*h('label', {
'for': 'import-recent',
}, Msg.register_importRecent),*/
]),
h('div.extra', [
h('button.login.first.btn', Msg.login_login)
])

@ -125,6 +125,8 @@
width: 100%;
}
}
display: flex;
flex-flow: column;
}
width: 100%;
@ -132,6 +134,8 @@
position: relative;
top: 50%;
transform: translateY(-50%);
max-height: 100%;
display: flex;
> * {
width: 100%;
@ -148,6 +152,42 @@
padding: @alertify_padding-base;
margin-bottom: @alertify_padding-base;
margin: 0;
overflow: auto;
.alertify-tabs {
.alertify-tabs-titles {
height: 30px;
display: flex;
border-bottom: 1px solid @alertify-fore;
margin-bottom: 20px;
box-sizing: content-box;
span {
font-size: 20px;
height: 30px;
line-height: 30px;
box-sizing: border-box;
padding: 0 15px;
border-left: 1px solid lighten(@alertify-base, 10%);
border-right: 1px solid lighten(@alertify-base, 10%);
cursor: pointer;
}
span.alertify-tabs-active {
background-color: @alertify-fore;
border-left: 1px solid @alertify-fore;
border-right: 1px solid @alertify-fore;
color: @alertify-base;
font-weight: bold;
cursor: default;
}
}
.alertify-tabs-contents {
& > div {
display: none;
}
& > div.alertify-tabs-content-active {
display: block;
}
}
}
}
input:not(.form-control), textarea {

@ -14,7 +14,6 @@
}
.cke_toolbox_main {
display: inline-block;
margin-bottom: -3px;
}
#cke_1_contents {
flex: 1;

@ -0,0 +1,39 @@
.icons_main() {
li {
display: inline-block;
margin: 10px 10px;
width: 140px;
height: 140px;
text-align: center;
vertical-align: top;
overflow: hidden;
text-overflow: ellipsis;
padding-top: 5px;
padding-bottom: 5px;
border: 1px solid white;
.cp-icons-name {
width: 100%;
height: 24px;
margin: 0;
display: inline-block;
font-size: 14px;
//align-items: center;
//justify-content: center;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
word-wrap: break-word;
}
.fa {
display: block;
font-size: 64px;
margin: 18px 0;
text-align: center;
&.listonly {
display: none;
}
}
}
}

@ -48,6 +48,9 @@
display: block;
color: @description-color;
margin-bottom: 5px;
p {
margin-bottom: 0;
}
}
margin-bottom: 20px;
}

@ -6,6 +6,8 @@
@import (once) "./toolbar-history.less";
@import (once) "./icon-colors.less";
@import (once) "./tools.less";
@import (once) "./icons.less";
@import (once) "./modal.less";
.toolbar_main (
@color: @colortheme_default-color, // Color of the text for the toolbar
@ -173,6 +175,68 @@
}
}
#cp-app-toolbar-creation-dialog.cp-modal-container {
.icons_main();
li:hover {
border: 1px solid white;
}
.cp-modal {
display: flex;
flex-flow: column;
li, li .fa {
cursor: pointer;
}
&> p {
margin: 50px;
}
&> div {
display: flex;
flex-wrap: wrap;
justify-content: center;
align-content: center;
overflow-y: auto;
}
}
.cp-creation-icons-name {
white-space: nowrap;
}
#cp-app-toolbar-creation-advanced {
width: auto;
margin: 0;
padding: 0;
}
label[for="cp-app-toolbar-creation-advanced"] {
margin: 0;
margin-left: 5px;
}
@media screen and (max-height: @browser_media-not-big) {
.cp-modal {
& > p {
display: none;
}
& > div {
align-content: unset;
li {
height: 40px;
width: 200px;
display: flex;
align-items: center;
.fa {
font-size: 32px;
}
.cp-icons-name {
height: auto;
}
}
}
}
}
}
// TODO(cjd) This ought to be in a less file for markdown-based editors
.cp-markdown-toolbar {
height: @toolbar_line-height;

@ -1,97 +0,0 @@
define(function () {
/*
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 = {};
// Store uses nodebacks...
Store.set = function (key, val, cb) {
localStorage.setItem(key, JSON.stringify(val));
cb();
};
// implement in alternative store
Store.setBatch = function (map, cb) {
Object.keys(map).forEach(function (key) {
localStorage.setItem(key, JSON.stringify(map[key]));
});
cb(void 0, map);
};
var safeGet = window.safeGet = function (key) {
var val = localStorage.getItem(key);
try {
return JSON.parse(val);
} catch (err) {
console.log(val);
console.error(err);
return val;
}
};
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);
};
Store.remove = function (key, cb) {
localStorage.removeItem(key);
cb();
};
// implement in alternative store
Store.removeBatch = function (keys, cb) {
keys.forEach(function (key) {
localStorage.removeItem(key);
});
cb();
};
Store.keys = function (cb) {
cb(void 0, Object.keys(localStorage));
};
Store.ready = function (f) {
if (typeof(f) === 'function') {
f(void 0, Store);
}
};
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
window.addEventListener('storage', function (e) {
changeHandlers.forEach(function (f) {
f({
key: e.key,
oldValue: e.oldValue,
newValue: e.newValue,
});
});
});
}
};
return Store;
});

@ -29,6 +29,8 @@ define(function () {
out.typeError = "Ce pad n'est pas compatible avec l'application sélectionnée";
out.onLogout = 'Vous êtes déconnecté de votre compte utilisateur, <a href="/" target="_blank">cliquez ici</a> pour vous authentifier<br>ou appuyez sur <em>Échap</em> pour accéder au pad en mode lecture seule.';
out.wrongApp = "Impossible d'afficher le contenu de ce document temps-réel dans votre navigateur. Vous pouvez essayer de recharger la page.";
out.padNotPinned = 'Ce pad va expirer dans 3 mois, {0}connectez-vous{1} ou {2}enregistrez-vous{3} pour le préserver.';
out.anonymousStoreDisabled = "L'administrateur de cette instance de CryptPad a désactivé le drive pour les utilisateurs non enregistrés. Vous devez vous connecter pour pouvoir utiliser CryptDrive.";
out.loading = "Chargement...";
out.error = "Erreur";
@ -144,6 +146,9 @@ define(function () {
out.backgroundButtonTitle = 'Changer la couleur de fond de la présentation';
out.colorButtonTitle = 'Changer la couleur du texte en mode présentation';
out.propertiesButton = "Propriétés";
out.propertiesButtonTitle = 'Voir les propriétés de ce pad';
out.printText = "Imprimer";
out.printButton = "Imprimer (Entrée)";
out.printButtonTitle = "Imprimer votre présentation ou l'enregistrer au format PDF";
@ -153,6 +158,11 @@ define(function () {
out.printTitle = "Afficher le titre du pad";
out.printCSS = "Personnaliser l'apparence (CSS):";
out.printTransition = "Activer les animations de transition";
out.printBackground = "Utiliser une image d'arrière-plan";
out.printBackgroundButton = "Choisir une image";
out.printBackgroundValue = "<b>Arrière-plan actuel:</b> <em>{0}</em>";
out.printBackgroundNoValue = "<em>Aucun arrière-plan affiché</em>";
out.printBackgroundRemove = "Supprimer cet arrière-plan";
out.filePickerButton = "Intégrer un fichier stocké dans CryptDrive";
out.filePicker_close = "Fermer";
@ -345,6 +355,7 @@ define(function () {
out.fm_templateName = "Modèles";
out.fm_searchName = "Recherche";
out.fm_recentPadsName = "Pads récents";
out.fm_ownedPadsName = "Possédés";
out.fm_searchPlaceholder = "Rechercher...";
out.fm_newButton = "Nouveau";
out.fm_newButtonTitle = "Créer un nouveau pad ou un dossier, importer un fichier dans le dossier courant";
@ -366,6 +377,7 @@ define(function () {
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 votre CryptDrive de manière permanente ?";
out.fm_removePermanentlyDialog = "Êtes-vous sûr de vouloir supprimer cet élément de votre CryptDrive de manière permanente ?";
out.fm_deleteOwnedPads = "Êtes-vous sûr de vouloir supprimer définitivement ce pad du serveur ?";
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 ?";
@ -382,6 +394,7 @@ define(function () {
out.fm_info_allFiles = 'Contient tous les fichiers de "Documents", "Fichiers non triés" et "Corbeille". Vous ne pouvez pas supprimer ou déplacer des fichiers depuis cet endroit.'; // Same here
out.fm_info_anonymous = 'Vous n\'êtes pas connecté, ces pads risquent donc d\'être supprimés (<a href="https://blog.cryptpad.fr/2017/05/17/You-gotta-log-in/" target="_blank">découvrez pourquoi</a>). ' +
'<a href="/register/">Inscrivez-vous</a> ou <a href="/login/">connectez-vous</a> pour les maintenir en vie.';
out.fm_info_owned = "Vous êtes propriétaire des pads affichés dans cette catégorie. Cela signifie que vous pouvez choisir de les supprimer définitivement du serveur à n'importe quel moment. Ils seront alors inaccessibles pour tous les autres utilisateurs.";
out.fm_alert_backupUrl = "Lien de secours pour ce CryptDrive.<br>" +
"Il est <strong>fortement recommandé</strong> de garder ce lien pour vous-même.<br>" +
"Il vous servira en cas de perte des données de votre navigateur afin de retrouver vos fichiers.<br>" +
@ -400,12 +413,15 @@ define(function () {
out.fm_burnThisDriveButton = "Effacer toutes les informations stockées par CryptPad dans votre navigateur";
out.fm_burnThisDrive = "Êtes-vous sûr de vouloir supprimmer tout ce qui est stocké par CryptPad dans votre navigateur ?<br>" +
"Cette action supprimera votre CryptDrive et son historique de votre navigateur, mais les pads existeront toujours (de manière chiffrée) sur notre serveur.";
out.fm_padIsOwned = "Vous êtes le propriétaire de ce pad";
out.fm_padIsOwnedOther = "Ce pad est la propriété d'un autre utilisateur";
// File - Context menu
out.fc_newfolder = "Nouveau dossier";
out.fc_rename = "Renommer";
out.fc_open = "Ouvrir";
out.fc_open_ro = "Ouvrir (lecture seule)";
out.fc_delete = "Déplacer vers la corbeille";
out.fc_delete_owned = "Supprimer du serveur";
out.fc_restore = "Restaurer";
out.fc_remove = "Supprimer de votre CryptDrive";
out.fc_empty = "Vider la corbeille";
@ -473,6 +489,7 @@ define(function () {
out.settings_cat_drive = "CryptDrive";
out.settings_cat_code = "Code";
out.settings_cat_pad = "Documents texte";
out.settings_cat_creation = "Nouveau pad";
out.settings_title = "Préférences";
out.settings_save = "Sauver";
@ -533,6 +550,14 @@ define(function () {
out.settings_padWidthHint = "L'éditeur de documents texte occupe toute la largeur de l'écran disponible par défaut, ce qui peut rendre le texte difficile à lire. Vous pouvez ici réduire la largeur de l'éditeur.";
out.settings_padWidthLabel = "Réduire la largeur de l'éditeur";
out.settings_creationSkip = "Passer l'écran de création de pad";
out.settings_creationSkipHint = "L'écran de création de pad offre de nouvelles options pour créer un pad, permettant d'avoir plus de contrôle et de sécurité concernant vos données. Toutefois, il peut ralentir votre travail en ajoutant une étape supplémentaire et donc, ici, vous avez la possibilité de choisir de passer cet écran et d'utiliser les paramètres par défaut choisis au-dessus.";
out.settings_creationSkipTrue = "Passer";
out.settings_creationSkipFalse = "Afficher";
out.settings_templateSkip = "Passer la fenêtre de choix d'un modèle";
out.settings_templateSkipHint = "Quand vous créez un nouveau pad, et si vous possédez des modèles pour ce type de pad, une fenêtre peut apparaître pour demander si vous souhaitez importer un modèle. Ici vous pouvez choisir de ne jamais montrer cette fenêtre et donc de ne jamais utiliser de modèle.";
out.upload_title = "Hébergement de fichiers";
out.upload_rename = "Souhaitez-vous renommer <b>{0}</b> avant son stockage en ligne ?<br>" +
"<em>L'extension du fichier ({1}) sera ajoutée automatiquement. "+
@ -776,5 +801,16 @@ define(function () {
out.feedback_privacy = "Nous prenons au sérieux le respect de votre vie privée, et en même temps nous souhaitons rendre CryptPad très simple à utiliser. Nous utilisons cette page pour comprendre quelles fonctionnalités dans l'interface comptent le plus pour les utilisateurs, en l'appelant avec un paramètre spécifiant quelle action a été réalisée.";
out.feedback_optout = "Si vous le souhaitez, vous pouvez désactiver ces requêtes en vous rendant dans <a href='/settings/'>votre page de préférences</a>, où vous trouverez une case à cocher pour désactiver le retour d'expérience.";
// Creation page
// Properties about creation data
out.creation_owners = "Propriétaires";
out.creation_ownedByOther = "Possédé par un autre utilisateur";
out.creation_noOwner = "Pas de propriétaire";
out.creation_expiration = "Date d'expiration";
out.creation_propertiesTitle = "Disponibilité";
out.creation_appMenuName = "Mode avancé (Ctrl + E)";
out.creation_newPadModalDescription = "Cliquez sur un type de pad pour le créer. Vous pouvez cocher la case pour afficher l'écran de création de pads";
out.creation_newPadModalAdvanced = "Afficher l'écran de création de pads";
return out;
});

@ -32,6 +32,7 @@ define(function () {
out.onLogout = 'You are logged out, <a href="/" target="_blank">click here</a> to log in<br>or press <em>Escape</em> to access your pad in read-only mode.';
out.wrongApp = "Unable to display the content of that realtime session in your browser. Please try to reload that page.";
out.padNotPinned = 'This pad will expire in 3 months, {0}login{1} or {2}register{3} to preserve it.';
out.anonymousStoreDisabled = "The webmaster of this CryptPad instance has disabled the store for anonymous users. You have to log in to be able to use CryptDrive.";
out.loading = "Loading...";
out.error = "Error";
@ -147,6 +148,9 @@ define(function () {
out.backgroundButtonTitle = 'Change the background color in the presentation';
out.colorButtonTitle = 'Change the text color in presentation mode';
out.propertiesButton = "Properties";
out.propertiesButtonTitle = 'Get pad properties';
out.printText = "Print";
out.printButton = "Print (enter)";
out.printButtonTitle = "Print your slides or export them as a PDF file";
@ -156,6 +160,11 @@ define(function () {
out.printTitle = "Display the pad title";
out.printCSS = "Custom style rules (CSS):";
out.printTransition = "Enable transition animations";
out.printBackground = "Use a background image";
out.printBackgroundButton = "Pick an image";
out.printBackgroundValue = "<b>Current background:</b> <em>{0}</em>";
out.printBackgroundNoValue = "<em>No background image displayed</em>";
out.printBackgroundRemove = "Remove this background image";
out.filePickerButton = "Embed a file stored in CryptDrive";
out.filePicker_close = "Close";
@ -348,6 +357,7 @@ define(function () {
out.fm_templateName = "Templates";
out.fm_searchName = "Search";
out.fm_recentPadsName = "Recent pads";
out.fm_ownedPadsName = "Owned";
out.fm_searchPlaceholder = "Search...";
out.fm_newButton = "New";
out.fm_newButtonTitle = "Create a new pad or folder, import a file in the current folder";
@ -371,6 +381,7 @@ define(function () {
out.fm_removePermanentlyDialog = "Are you sure you want to remove that element from your CryptDrive 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_deleteOwnedPads = "Are you sure you want to remove permanently this pad from the server?";
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.";
@ -385,6 +396,7 @@ define(function () {
out.fm_info_allFiles = 'Contains all the files from "Documents", "Unsorted" and "Trash". You can\'t move or remove files from here.'; // Same here
out.fm_info_anonymous = 'You are not logged in so your pads will expire after 3 months (<a href="https://blog.cryptpad.fr/2017/05/17/You-gotta-log-in/" target="_blank">find out more</a>). ' +
'<a href="/register/">Sign up</a> or <a href="/login/">Log in</a> to keep them alive.';
out.fm_info_owned = "You are the owner of the pads displayed here. This means you can remove them permanently from the server whenever you want. If you do so, other users won't be able to access them anymore.";
out.fm_alert_backupUrl = "Backup link for this drive.<br>" +
"It is <strong>highly recommended</strong> that you keep it secret.<br>" +
"You can use it to retrieve all your files in case your browser memory got erased.<br>" +
@ -403,12 +415,15 @@ define(function () {
out.fm_burnThisDriveButton = "Erase all information stored by CryptPad in your browser";
out.fm_burnThisDrive = "Are you sure you want to remove everything stored by CryptPad in your browser?<br>" +
"This will remove your CryptDrive and its history from your browser, but your pads will still exist (encrypted) on our server.";
out.fm_padIsOwned = "You are the owner of this pad";
out.fm_padIsOwnedOther = "This pad is owned by another user";
// File - Context menu
out.fc_newfolder = "New folder";
out.fc_rename = "Rename";
out.fc_open = "Open";
out.fc_open_ro = "Open (read-only)";
out.fc_delete = "Move to trash";
out.fc_delete_owned = "Delete from the server";
out.fc_restore = "Restore";
out.fc_remove = "Remove from your CryptDrive";
out.fc_empty = "Empty the trash";
@ -479,6 +494,7 @@ define(function () {
out.settings_cat_drive = "CryptDrive";
out.settings_cat_code = "Code";
out.settings_cat_pad = "Rich text";
out.settings_cat_creation = "New pad";
out.settings_title = "Settings";
out.settings_save = "Save";
@ -539,6 +555,14 @@ define(function () {
out.settings_padWidthHint = "Rich text pads use by default the maximum available width on your screen and it can be difficult to read. You can reduce the editor's width here.";
out.settings_padWidthLabel = "Reduce the editor's width";
out.settings_creationSkip = "Skip the pad creation screen";
out.settings_creationSkipHint = "The pad creation screen offers new options to create a pad, providing you more control and security over your data. However, it may slow down your workflow by adding one additionnal step so, here, you have the option to skip this screen and use the default settings selected above.";
out.settings_creationSkipTrue = "Skip";
out.settings_creationSkipFalse = "Display";
out.settings_templateSkip = "Skip the template selection modal";
out.settings_templateSkipHint = "When you create a new empty pad, if you have stored templates for this type of pad, a modal appears to ask if you want to use a template. Here you can choose to never show this modal and so to never use a template.";
out.upload_title = "File upload";
out.upload_rename = "Do you want to rename <b>{0}</b> before uploading it to the server?<br>" +
"<em>The file extension ({1}) will be added automatically. "+
@ -805,11 +829,20 @@ define(function () {
out.creation_expireHours = "Hours";
out.creation_expireDays = "Days";
out.creation_expireMonths = "Months";
out.creation_expire1 = "By default, a pad stored by a registered users will never be removed from the server, unless it is requested by its owner.";
out.creation_expire1 = "By default, a pad stored by a registered user will never be removed from the server, unless it is requested by its owner.";
out.creation_expire2 = "If you prefer, you can set a life time to make sure the pad will be permanently deleted from the server and unavailable after the specified date.";
out.creation_createTitle = "Create a pad";
out.creation_createFromTemplate = "From template";
out.creation_createFromScratch = "From scratch";
// Properties about creation data
out.creation_owners = "Owners";
out.creation_ownedByOther = "Owned by another user";
out.creation_noOwner = "No owner";
out.creation_expiration = "Expiration time";
out.creation_propertiesTitle = "Availability";
out.creation_appMenuName = "Advanced mode (Ctrl + E)";
out.creation_newPadModalDescription = "Click on a pad type to create it. You can check the box if you want to display the pad creation screen (for owned pad, expiration pad, etc.).";
out.creation_newPadModalAdvanced = "Display the pad creation screen";
return out;
});

@ -1,7 +1,7 @@
{
"name": "cryptpad",
"description": "realtime collaborative visual editor with zero knowlege server",
"version": "1.23.0",
"version": "1.24.0",
"dependencies": {
"chainpad-server": "^1.0.1",
"express": "~4.10.1",

@ -70,6 +70,8 @@ run('npm', ['install'], () => {
' | awk \'{print $2}\' | while read x; do kill $x; done'
], waitFor());
run('bash', ['-c', 'rm -rf ./blob ./blobstage ./datastore'], waitFor());
run('bash', ['-c', 'caffeinate -u -t 2'], waitFor());
}
}).nThen((waitFor) => {

@ -45,7 +45,7 @@ var getChannelMetadata = function (Env, channelId, cb) {
};
var closeChannel = function (env, channelName, cb) {
if (!env.channels[channelName]) { return; }
if (!env.channels[channelName]) { return void cb(); }
try {
env.channels[channelName].writeStream.close();
delete env.channels[channelName];
@ -177,13 +177,13 @@ var getChannel = function (env, id, callback) {
}, env.channelExpirationMs / 2);
});
}
var path = mkPath(env, id);
var channel = env.channels[id] = {
atime: +new Date(),
messages: [],
writeStream: undefined,
whenLoaded: [ callback ],
onError: [ ]
onError: [ ],
path: path
};
var complete = function (err) {
var whenLoaded = channel.whenLoaded;
@ -195,7 +195,6 @@ var getChannel = function (env, id, callback) {
}
whenLoaded.forEach(function (wl) { wl(err, (err) ? undefined : channel); });
};
var path = mkPath(env, id);
var fileExists;
var errorState;
nThen(function (waitFor) {
@ -207,17 +206,6 @@ var getChannel = function (env, id, callback) {
}
fileExists = exists;
}));
}).nThen(function (waitFor) {
if (errorState) { return; }
if (!fileExists) { return; }
readMessages(path, function (msg) {
channel.messages.push(msg);
}, waitFor(function (err) {
if (err) {
errorState = true;
complete(err);
}
}));
}).nThen(function (waitFor) {
if (errorState) { return; }
var stream = channel.writeStream = Fs.createWriteStream(path, { flags: 'a' });
@ -255,7 +243,7 @@ var message = function (env, chanName, msg, cb) {
chan.writeStream.write(msg + '\n', function () {
chan.onError.splice(chan.onError.indexOf(complete) - 1, 1);
if (!cb) { return; }
chan.messages.push(msg);
//chan.messages.push(msg);
chan.atime = +new Date();
complete();
});
@ -268,19 +256,25 @@ var getMessages = function (env, chanName, handler, cb) {
cb(err);
return;
}
var errorState = false;
try {
chan.messages
.forEach(function (message) {
if (!message) { return; }
handler(message);
});
readMessages(chan.path, function (msg) {
if (!msg || errorState) { return; }
//console.log(msg);
handler(msg);
}, function (err) {
if (err) {
errorState = true;
return void cb(err);
}
chan.atime = +new Date();
cb();
});
} catch (err2) {
console.error(err2);
cb(err2);
return;
}
chan.atime = +new Date();
cb();
});
};

@ -134,12 +134,12 @@ define([
// check that old hashes parse correctly
assert(function (cb) {
if (1) { return cb(true); } // TODO(cjd): This is a test failure which is a known bug
//if (1) { return cb(true); } // TODO(cjd): This is a test failure which is a known bug
var secret = Hash.parsePadUrl('/pad/#67b8385b07352be53e40746d2be6ccd7XAYSuJYYqa9NfmInyHci7LNy');
return cb(secret.hashData.channel === "67b8385b07352be53e40746d2be6ccd7" &&
secret.hashData.key === "XAYSuJYYqa9NfmInyHci7LNy" &&
secret.hashData.version === 0 &&
typeof(secret.hashData.getURL) === 'function');
typeof(secret.getUrl) === 'function');
}, "Old hash failed to parse");
// make sure version 1 hashes parse correctly

@ -268,11 +268,11 @@ define([
////
framework.onContentUpdate(function (newContent) {
CodeMirror.contentUpdate(newContent);
var highlightMode = newContent.highlightMode;
if (highlightMode && highlightMode !== CodeMirror.highlightMode) {
CodeMirror.setMode(highlightMode, evModeChange.fire);
}
CodeMirror.contentUpdate(newContent);
previewPane.draw();
});
@ -357,7 +357,7 @@ define([
getContainer: getThumbnailContainer,
filter: function (el, before) {
if (before) {
$(el).parents().css('overflow', 'visible');
//$(el).parents().css('overflow', 'visible');
$(el).css('max-height', Math.max(600, $(el).width()) + 'px');
return;
}

@ -11,5 +11,6 @@ define(function () {
oldStorageKey: 'CryptPad_RECENTPADS',
storageKey: 'filesData',
tokenKey: 'loginToken',
displayPadCreationScreen: 'displayPadCreationScreen'
};
});

@ -128,6 +128,38 @@ define([
]);
};
/**
* tabs is an array containing objects
* each object must have the following attributes:
* - title: String
* - content: DOMElement
*/
dialog.tabs = function (tabs) {
var contents = [];
var titles = [];
tabs.forEach(function (tab) {
if (!tab.content || !tab.title) { return; }
var content = tab.content;
var title = h('span.alertify-tabs-title', tab.title);
$(title).click(function () {
titles.forEach(function (t) { $(t).removeClass('alertify-tabs-active'); });
contents.forEach(function (c) { $(c).removeClass('alertify-tabs-content-active'); });
$(title).addClass('alertify-tabs-active');
$(content).addClass('alertify-tabs-content-active');
});
titles.push(title);
contents.push(content);
});
if (contents.length) {
$(contents[0]).addClass('alertify-tabs-content-active');
$(titles[0]).addClass('alertify-tabs-active');
}
return h('div.alertify-tabs', [
h('div.alertify-tabs-titles', titles),
h('div.alertify-tabs-contents', contents),
]);
};
UI.tokenField = function (target) {
var t = {
element: target || h('input'),
@ -464,6 +496,7 @@ define([
UI.removeLoadingScreen = function (cb) {
// Release the test blocker, hopefully every test has been registered.
// This test is created in sframe-boot2.js
cb = cb || function () {};
if (Test.__ASYNC_BLOCKER__) { Test.__ASYNC_BLOCKER__.pass(); }
$('#' + LOADING).addClass("cp-loading-hidden");
@ -476,9 +509,9 @@ define([
'opacity': 0,
'pointer-events': 'none',
});
setTimeout(function () {
$tip.remove();
}, 3750);
window.setTimeout(function () {
$tip.remove();
}, 3750);
// jquery.fadeout can get stuck
};
UI.errorLoadingScreen = function (error, transparent) {

@ -183,8 +183,10 @@ define([
allowTaint: true,
onrendered: function (canvas) {
if (opts.filter) { opts.filter(element, false); }
var D = getResizedDimensions(canvas, 'pad');
Thumb.fromCanvas(canvas, D, cb);
setTimeout(function () {
var D = getResizedDimensions(canvas, 'pad');
Thumb.fromCanvas(canvas, D, cb);
}, 10);
}
});
};

@ -5,13 +5,17 @@ define([
'/common/common-hash.js',
'/common/common-language.js',
'/common/common-interface.js',
'/common/common-constants.js',
'/common/common-feedback.js',
'/common/hyperscript.js',
'/common/media-tag.js',
'/customize/messages.js',
'/customize/application_config.js',
'/bower_components/nthen/index.js',
'css!/common/tippy.css',
], function ($, Config, Util, Hash, Language, UI, Feedback, h, MediaTag, Messages) {
], function ($, Config, Util, Hash, Language, UI, Constants, Feedback, h, MediaTag, Messages,
AppConfig, NThen) {
var UIElements = {};
// Configure MediaTags to use our local viewer
@ -54,6 +58,174 @@ define([
};
};
var getPropertiesData = function (common, cb) {
var data = {};
NThen(function (waitFor) {
common.getPadAttribute('href', waitFor(function (err, val) {
var base = common.getMetadataMgr().getPrivateData().origin;
var parsed = Hash.parsePadUrl(val);
if (parsed.hashData.mode === "view") {
data.roHref = base + val;
return;
}
// We're not in a read-only pad
data.href = base + val;
// Get Read-only href
if (parsed.hashData.type !== "pad") { return; }
var i = data.href.indexOf('#') + 1;
var hBase = data.href.slice(0, i);
var hrefsecret = Hash.getSecrets(parsed.type, parsed.hash);
if (!hrefsecret.keys) { return; }
var viewHash = Hash.getViewHashFromKeys(hrefsecret.channel, hrefsecret.keys);
data.roHref = hBase + viewHash;
}));
common.getPadAttribute('atime', waitFor(function (err, val) {
data.atime = val;
}));
common.getPadAttribute('ctime', waitFor(function (err, val) {
data.ctime = val;
}));
common.getPadAttribute('tags', waitFor(function (err, val) {
data.tags = val;
}));
common.getPadAttribute('owners', waitFor(function (err, val) {
data.owners = val;
}));
common.getPadAttribute('expire', waitFor(function (err, val) {
data.expire = val;
}));
}).nThen(function () {
cb(void 0, data);
});
};
var getRightsProperties = function (common, data, cb) {
var $d = $('<div>');
if (!data) { return void cb(void 0, $d); }
$('<label>', {'for': 'cp-app-prop-owners'}).text(Messages.creation_owners)
.appendTo($d);
var owners = Messages.creation_noOwner;
var edPublic = common.getMetadataMgr().getPrivateData().edPublic;
var owned = false;
if (data.owners && data.owners.length) {
if (data.owners.indexOf(edPublic) !== -1) {
owners = Messages.yourself;
owned = true;
} else {
owners = Messages.creation_ownedByOther;
}
}
$d.append(UI.dialog.selectable(owners, {
id: 'cp-app-prop-owners',
}));
/* TODO
if (owned) {
var $deleteOwned = $('button').text(Messages.fc_delete_owned).click(function () {
});
$d.append($deleteOwned);
}*/
var expire = Messages.creation_expireFalse;
if (data.expire && typeof (data.expire) === "number") {
expire = new Date(data.expire).toLocaleString();
}
$('<label>', {'for': 'cp-app-prop-expire'}).text(Messages.creation_expiration)
.appendTo($d);
$d.append(UI.dialog.selectable(expire, {
id: 'cp-app-prop-expire',
}));
cb(void 0, $d);
};
var getPadProperties = function (common, data, cb) {
var $d = $('<div>');
if (!data || !data.href) { return void cb(void 0, $d); }
if (data.href) {
$('<label>', {'for': 'cp-app-prop-link'}).text(Messages.editShare).appendTo($d);
$d.append(UI.dialog.selectable(data.href, {
id: 'cp-app-prop-link',
}));
}
if (data.roHref) {
$('<label>', {'for': 'cp-app-prop-rolink'}).text(Messages.viewShare).appendTo($d);
$d.append(UI.dialog.selectable(data.roHref, {
id: 'cp-app-prop-rolink',
}));
}
if (data.tags && Array.isArray(data.tags)) {
$('<label>', {'for': 'cp-app-prop-tags'}).text(Messages.fm_prop_tagsList).appendTo($d);
$d.append(UI.dialog.selectable(data.tags.join(', '), {
id: 'cp-app-prop-tags',
}));
}
$('<label>', {'for': 'cp-app-prop-ctime'}).text(Messages.fm_creation)
.appendTo($d);
$d.append(UI.dialog.selectable(new Date(data.ctime).toLocaleString(), {
id: 'cp-app-prop-ctime',
}));
$('<label>', {'for': 'cp-app-prop-atime'}).text(Messages.fm_lastAccess)
.appendTo($d);
$d.append(UI.dialog.selectable(new Date(data.atime).toLocaleString(), {
id: 'cp-app-prop-atime',
}));
if (common.isLoggedIn() && AppConfig.enablePinning) {
// check the size of this file...
common.getFileSize(data.href, function (e, bytes) {
if (e) {
// there was a problem with the RPC
console.error(e);
// but we don't want to break the interface.
// continue as if there was no RPC
return void cb(void 0, $d);
}
var KB = Util.bytesToKilobytes(bytes);
var formatted = Messages._getKey('formattedKB', [KB]);
$('<br>').appendTo($d);
$('<label>', {
'for': 'cp-app-prop-size'
}).text(Messages.fc_sizeInKilobytes).appendTo($d);
$d.append(UI.dialog.selectable(formatted, {
id: 'cp-app-prop-size',
}));
cb(void 0, $d);
});
} else {
cb(void 0, $d);
}
};
UIElements.getProperties = function (common, data, cb) {
var c1;
var c2;
NThen(function (waitFor) {
getPadProperties(common, data, waitFor(function (e, c) {
c1 = c[0];
}));
getRightsProperties(common, data, waitFor(function (e, c) {
c2 = c[0];
}));
}).nThen(function () {
var tabs = UI.dialog.tabs([{
title: Messages.fc_prop,
content: c1
}, {
title: Messages.creation_propertiesTitle,
content: c2
}]);
cb (void 0, $(tabs));
});
};
UIElements.createButton = function (common, type, rightside, data, callback) {
var AppConfig = common.getAppConfig();
var button;
@ -255,6 +427,23 @@ define([
});
updateIcon(data.element.is(':visible'));
break;
case 'properties':
button = $('<button>', {
'class': 'fa fa-info-circle',
title: Messages.propertiesButtonTitle,
}).append($('<span>', {'class': 'cp-toolbar-drawer-element'})
.text(Messages.propertiesButton))
.click(common.prepareFeedback(type))
.click(function () {
getPropertiesData(common, function (e, data) {
if (e) { return void console.error(e); }
UIElements.getProperties(common, data, function (e, $prop) {
if (e) { return void console.error(e); }
UI.alert($prop[0], undefined, true);
});
});
});
break;
default:
button = $('<button>', {
'class': "fa fa-question",
@ -583,6 +772,7 @@ define([
*/
var LIMIT_REFRESH_RATE = 30000; // milliseconds
UIElements.createUsageBar = function (common, cb) {
if (AppConfig.hideUsageBar) { return cb('USAGE_BAR_HIDDEN'); }
if (!common.isLoggedIn()) { return cb("NOT_LOGGED_IN"); }
// getPinnedUsage updates common.account.usage, and other values
// so we can just use those and only check for errors
@ -679,9 +869,14 @@ define([
if (typeof config !== "object" || !Array.isArray(config.options)) { return; }
if (config.feedback && !config.common) { return void console.error("feedback in a dropdown requires sframe-common"); }
var isElement = function (o) {
return /HTML/.test(Object.prototype.toString.call(o)) &&
typeof(o.tagName) === 'string';
};
var allowedTags = ['a', 'p', 'hr'];
var isValidOption = function (o) {
if (typeof o !== "object") { return false; }
if (isElement(o)) { return true; }
if (!o.tag || allowedTags.indexOf(o.tag) === -1) { return false; }
return true;
};
@ -713,6 +908,7 @@ define([
config.options.forEach(function (o) {
if (!isValidOption(o)) { return; }
if (isElement(o)) { return $innerblock.append($(o)); }
$('<' + o.tag + '>', o.attributes || {}).html(o.content || '').appendTo($innerblock);
});
@ -1068,6 +1264,64 @@ define([
return $blockContainer;
};
UIElements.createNewPadModal = function (common) {
var $modal = UIElements.createModal({
id: 'cp-app-toolbar-creation-dialog',
$body: $('body')
});
var $title = $('<h3>').text(Messages.fm_newFile);
var $description = $('<p>').text(Messages.creation_newPadModalDescription);
$modal.find('.cp-modal').append($title);
$modal.find('.cp-modal').append($description);
var $advanced;
var $advancedContainer = $('<div>');
if (common.isLoggedIn()) {
$advanced = $('<input>', {
type: 'checkbox',
checked: 'checked',
id: 'cp-app-toolbar-creation-advanced'
}).appendTo($advancedContainer);
$('<label>', {
for: 'cp-app-toolbar-creation-advanced'
}).text(Messages.creation_newPadModalAdvanced).appendTo($advancedContainer);
}
var $container = $('<div>');
AppConfig.availablePadTypes.forEach(function (p) {
if (p === 'drive') { return; }
if (p === 'contacts') { return; }
if (p === 'todo') { return; }
if (p === 'file') { return; }
if (!common.isLoggedIn() && AppConfig.registeredOnlyTypes &&
AppConfig.registeredOnlyTypes.indexOf(p) !== -1) { return; }
var $element = $('<li>', {
'class': 'cp-icons-element'
}).prepend(UI.getIcon(p)).appendTo($container);
$element.append($('<span>', {'class': 'cp-icons-name'})
.text(Messages.type[p]));
$element.attr('data-type', p);
$element.click(function () {
$modal.hide();
if ($advanced && $advanced.is(':checked')) {
common.sessionStorage.put(Constants.displayPadCreationScreen, true, function () {
common.openURL('/' + p + '/');
});
return;
}
common.sessionStorage.put(Constants.displayPadCreationScreen, "", function () {
common.openURL('/' + p + '/');
});
});
});
/*var $content = createNewPadIcons($modal, isInRoot);*/
$modal.find('.cp-modal').append($container).append($advancedContainer);
window.setTimeout(function () { $modal.show(); });
//addNewPadHandlers($modal, isInRoot);
};
UIElements.initFilePicker = function (common, cfg) {
var onSelect = cfg.onSelect || $.noop;
@ -1129,12 +1383,36 @@ define([
});
};
UIElements.getPadCreationScreen = function (common, cb) {
UIElements.setExpirationValue = function (val, $expire) {
if (val && typeof (val) === "number") {
$expire.find('#cp-creation-expire-true').attr('checked', true);
if (val % (3600 * 24 * 30) === 0) {
$expire.find('#cp-creation-expire-unit').val("month");
$expire.find('#cp-creation-expire-val').val(val / (3600 * 24 * 30));
return;
}
if (val % (3600 * 24) === 0) {
$expire.find('#cp-creation-expire-unit').val("day");
$expire.find('#cp-creation-expire-val').val(val / (3600 * 24));
return;
}
if (val % 3600 === 0) {
$expire.find('#cp-creation-expire-unit').val("hour");
$expire.find('#cp-creation-expire-val').val(val / 3600);
return;
}
// if we're here, it means we don't have a valid value so we should check unlimited
$expire.find('#cp-creation-expire-false').attr('checked', true);
}
};
UIElements.getPadCreationScreen = function (common, cfg, cb) {
if (!common.isLoggedIn()) { return void cb(); }
var sframeChan = common.getSframeChannel();
var metadataMgr = common.getMetadataMgr();
var type = metadataMgr.getMetadataLazy().type;
// XXX check text for pad creation screen + translate it in French
var $body = $('body');
var $creationContainer = $('<div>', { id: 'cp-creation-container' }).appendTo($body);
var $creation = $('<div>', { id: 'cp-creation' }).appendTo($creationContainer);
@ -1182,6 +1460,11 @@ define([
]);
$creation.append(owned);
// If set to "open pad" or not set, check "open pad"
if (!cfg.owned && typeof cfg.owned !== "undefined") {
$creation.find('#cp-creation-owned-false').attr('checked', true);
}
// Life time
var expire = h('div.cp-creation-expire', [
h('h2', [
@ -1223,6 +1506,8 @@ define([
]);
$creation.append(expire);
UIElements.setExpirationValue(cfg.expire, $creation);
// Create the pad
var create = function (template) {
// Type of pad
@ -1240,11 +1525,7 @@ define([
expireVal = ($('#cp-creation-expire-val').val() || 0) * unit;
}
// XXX TODO remove these lines
ownedVal = undefined;
expire = undefined;
sframeChan.query("Q_CREATE_PAD", {
common.createPad({
owned: ownedVal,
expire: expireVal,
template: template

@ -276,6 +276,7 @@ define([
};
common.setPadAttribute = function (attr, value, cb, href) {
cb = cb || function () {};
href = Hash.getRelativeHref(href || window.location.href);
postMessage("SET_PAD_ATTRIBUTE", {
href: href,
@ -297,6 +298,7 @@ define([
});
};
common.setAttribute = function (attr, value, cb) {
cb = cb || function () {};
postMessage("SET_ATTRIBUTE", {
attr: attr,
value: value

@ -97,6 +97,8 @@ define(['json.sortify'], function (Sortify) {
change(true);
});
sframeChan.on('EV_RT_JOIN', function (ev) {
var idx = members.indexOf(ev);
if (idx !== -1) { console.log('Error: ' + ev + ' is already in members'); return; }
members.push(ev);
if (!meta.user) { return; }
change(false);

@ -10,12 +10,13 @@ define([
'/common/common-messenger.js',
'/common/outer/chainpad-netflux-worker.js',
'/common/outer/network-config.js',
'/customize/application_config.js',
'/bower_components/chainpad-crypto/crypto.js?v=0.1.5',
'/bower_components/chainpad/chainpad.dist.js',
'/bower_components/chainpad-listmap/chainpad-listmap.js',
], function (UserObject, Migrate, Hash, Util, Constants, Feedback, Realtime, Messaging, Messenger,
CpNfWorker, NetConfig,
CpNfWorker, NetConfig, AppConfig,
Crypto, ChainPad, Listmap) {
var Store = {};
@ -361,6 +362,8 @@ define([
Store.addPad = function (data, cb) {
if (!data.href) { return void cb({error:'NO_HREF'}); }
var pad = makePad(data.href, data.title);
if (data.owners) { pad.owners = data.owners; }
if (data.expire) { pad.expire = data.expire; }
store.userObject.pushData(pad, function (e, id) {
if (e) { return void cb({error: "Error while adding a template:"+ e}); }
var path = data.path || ['root'];
@ -522,6 +525,17 @@ define([
var p = Hash.parsePadUrl(href);
var h = p.hashData;
if (AppConfig.disableAnonymousStore && !store.loggedIn) { return void cb(); }
var owners;
if (Store.channel && Store.channel.wc && Util.base64ToHex(h.channel) === Store.channel.wc.id) {
owners = Store.channel.data.owners || undefined;
}
var expire;
if (Store.channel && Store.channel.wc && Util.base64ToHex(h.channel) === Store.channel.wc.id) {
expire = +Store.channel.data.expire || undefined;
}
var allPads = Util.find(store.proxy, ['drive', 'filesData']) || {};
var isStronger;
@ -583,6 +597,8 @@ define([
Store.addPad({
href: href,
title: title,
owners: owners,
expire: expire,
path: data.path || (store.data && store.data.initialPath)
}, cb);
return;
@ -735,12 +751,14 @@ define([
// TODO with sharedworker
// channel will be an object storing the webchannel associated to each browser tab
var channel = {
queue: []
var channel = Store.channel = {
queue: [],
data: {}
};
Store.joinPad = function (data, cb) {
var conf = {
onReady: function () {
onReady: function (padData) {
channel.data = padData || {};
postMessage("PAD_READY");
}, // post EV_PAD_READY
onMessage: function (m) {

@ -36,6 +36,7 @@ define([], function () {
var owners = conf.owners;
var password = conf.password;
var expire = conf.expire;
var padData;
conf = undefined;
var initializing = true;
@ -43,11 +44,11 @@ define([], function () {
var messageFromOuter = function () {};
var onRdy = function () {
var onRdy = function (padData) {
// Trigger onReady only if not ready yet. This is important because the history keeper sends a direct
// message through "network" when it is synced, and it triggers onReady for each channel joined.
if (!initializing) { return; }
onReady();
onReady(padData);
//sframeChan.event('EV_RT_READY', null);
// we're fully synced
initializing = false;
@ -92,13 +93,14 @@ define([], function () {
if (parsed.channel === wc.id && !validateKey) {
validateKey = parsed.validateKey;
}
padData = parsed;
// We have to return even if it is not the current channel:
// we don't want to continue with other channels messages here
return;
}
if (parsed.state && parsed.state === 1 && parsed.channel) {
if (parsed.channel === wc.id) {
onRdy();
onRdy(padData);
}
// We have to return even if it is not the current channel:
// we don't want to continue with other channels messages here

@ -2,7 +2,8 @@ define([
'/common/common-constants.js',
'/common/common-hash.js',
'/bower_components/localforage/dist/localforage.min.js',
], function (Constants, Hash, localForage) {
'/customize/application_config.js',
], function (Constants, Hash, localForage, AppConfig) {
var LocalStore = {};
LocalStore.setThumbnail = function (key, value, cb) {
@ -115,6 +116,10 @@ define([
if (typeof (h) === "function") { h(); }
});
if (typeof(AppConfig.customizeLogout) === 'function') {
return void AppConfig.customizeLogout(cb);
}
if (cb) { cb(); }
};
LocalStore.onLogout = function (h) {
@ -125,6 +130,5 @@ define([
return LocalStore;
});

@ -300,7 +300,9 @@ define([
}
}
if (newPad && !AppConfig.displayCreationScreen) {
var skipTemp = Util.find(privateDat, ['settings', 'general', 'creation', 'noTemplate']);
var skipCreation = Util.find(privateDat, ['settings', 'general', 'creation', 'skip']);
if (newPad && (!skipTemp && skipCreation)) {
common.openTemplatePicker();
}
};
@ -368,9 +370,9 @@ define([
if (data.type !== 'file') { console.log('unhandled embed type ' + data.type); return; }
var privateDat = cpNfInner.metadataMgr.getPrivateData();
var origin = privateDat.fileHost || privateDat.origin;
var src = origin + data.src;
var src = data.src = origin + data.src;
mediaTagEmbedder($('<media-tag src="' + src +
'" data-crypto-key="cryptpad:' + data.key + '"></media-tag>'));
'" data-crypto-key="cryptpad:' + data.key + '"></media-tag>'), data);
}
});
$embedButton = $('<button>', {
@ -402,8 +404,11 @@ define([
}).nThen(function (waitFor) {
Test.registerInner(common.getSframeChannel());
if (!AppConfig.displayCreationScreen) { return; }
if (common.getMetadataMgr().getPrivateData().isNewFile) {
common.getPadCreationScreen(waitFor());
var priv = common.getMetadataMgr().getPrivateData();
if (priv.isNewFile) {
var c = (priv.settings.general && priv.settings.general.creation) || {};
if (c.skip && !priv.forceCreationScreen) { return void common.createPad(c, waitFor()); }
common.getPadCreationScreen(c, waitFor());
}
}).nThen(function (waitFor) {
cpNfInner = common.startRealtime({
@ -548,6 +553,9 @@ define([
toolbar.$rightside.append($tags);
}
var $properties = common.createButton('properties', true);
toolbar.$drawer.append($properties);
createFilePicker();
cb(Object.freeze({

@ -35,6 +35,8 @@ define([
};
window.addEventListener('message', onMsg);
}).nThen(function (/*waitFor*/) {
SFCommonO.start();
SFCommonO.start({
useCreationScreen: true
});
});
});

@ -13,6 +13,11 @@ define([
};
}
// RPC breaks if you don't support Number.MAX_SAFE_INTEGER
if (Number && !Number.MAX_SAFE_INTEGER) {
Number.MAX_SAFE_INTEGER = 9007199254740991;
}
var mkFakeStore = function () {
var fakeStorage = {
getItem: function (k) { return fakeStorage[k]; },

@ -81,6 +81,7 @@ define([], function () {
sframeChan.query('Q_RT_MESSAGE', message, function () { });
};
var firstConnection = true;
var onOpen = function(data) {
// Add the existing peers in the userList
onConnect(data.id);
@ -88,10 +89,13 @@ define([], function () {
sframeChan.event('EV_RT_CONNECT', { myID: data.myID, members: data.members, readOnly: readOnly });
// Add the handlers to the WebChannel
padRpc.onMessageEvent.reg(function (msg) { onMessage(msg); });
padRpc.onJoinEvent.reg(function (m) { sframeChan.event('EV_RT_JOIN', m); });
padRpc.onLeaveEvent.reg(function (m) { sframeChan.event('EV_RT_LEAVE', m); });
if (firstConnection) {
firstConnection = false;
// Add the handlers to the WebChannel
padRpc.onMessageEvent.reg(function (msg) { onMessage(msg); });
padRpc.onJoinEvent.reg(function (m) { sframeChan.event('EV_RT_JOIN', m); });
padRpc.onLeaveEvent.reg(function (m) { sframeChan.event('EV_RT_LEAVE', m); });
}
};
padRpc.onDisconnectEvent.reg(function () {

@ -313,6 +313,9 @@ define([
exp.contentUpdate = function (newContent) {
var oldDoc = canonicalize($textarea.val());
var remoteDoc = newContent.content;
// setValueAndCursor triggers onLocal, even if we don't make any change to the content
// and it may revert other changes (metadata)
if (oldDoc === remoteDoc) { return; }
exp.setValueAndCursor(oldDoc, remoteDoc);
};

@ -240,7 +240,7 @@ define([
inProgress: false
};
var handleFile = File.handleFile = function (file, e) {
//if (handleFileState.inProgress) { return void handleFileState.queue.push(file); }
if (handleFileState.inProgress) { return void handleFileState.queue.push([file, e]); }
handleFileState.inProgress = true;
var thumb;
@ -258,7 +258,10 @@ define([
dropEvent: e
});
handleFileState.inProgress = false;
if (handleFileState.queue.length) { handleFile(handleFileState.queue.shift()); }
if (handleFileState.queue.length) {
var next = handleFileState.queue.shift();
handleFile(next[0], next[1]);
}
};
var getName = function () {
if (!showNamePrompt) { return void finish(); }

@ -9,7 +9,6 @@ define([
common.start = function (cfg) {
cfg = cfg || {};
var realtime = !cfg.noRealtime;
var network;
var secret;
var hashes;
var isNewFile;
@ -44,12 +43,10 @@ define([
'/common/common-feedback.js',
'/common/outer/local-store.js',
'/customize/application_config.js',
'/common/outer/network-config.js',
'/bower_components/netflux-websocket/netflux-client.js',
'/common/test.js',
], waitFor(function (_CpNfOuter, _Cryptpad, _Crypto, _Cryptget, _SFrameChannel,
_FilePicker, _Messaging, _Notifier, _Hash, _Util, _Realtime,
_Constants, _Feedback, _LocalStore, _AppConfig, NetConfig, Netflux, _Test) {
_Constants, _Feedback, _LocalStore, _AppConfig, _Test) {
CpNfOuter = _CpNfOuter;
Cryptpad = _Cryptpad;
Crypto = _Crypto;
@ -95,12 +92,6 @@ define([
messenger: cfg.messaging,
driveEvents: cfg.driveEvents
});
if (!cfg.newNetwork) {
Netflux.connect(NetConfig.getWebsocketURL()).then(waitFor(function (nw) {
network = nw;
}));
}
}));
}).nThen(function (waitFor) {
$('#sbox-iframe').focus();
@ -157,6 +148,9 @@ define([
if (!parsed.type) { throw new Error(); }
var defaultTitle = Utils.Hash.getDefaultName(parsed);
var edPublic;
var forceCreationScreen = cfg.useCreationScreen &&
sessionStorage[Utils.Constants.displayPadCreationScreen];
delete sessionStorage[Utils.Constants.displayPadCreationScreen];
var updateMeta = function () {
//console.log('EV_METADATA_UPDATE');
var metaObj, isTemplate;
@ -191,7 +185,8 @@ define([
upgradeURL: Cryptpad.upgradeURL
},
isNewFile: isNewFile,
isDeleted: window.location.hash.length > 0
isDeleted: window.location.hash.length > 0,
forceCreationScreen: forceCreationScreen
};
for (var k in additionalPriv) { metaObj.priv[k] = additionalPriv[k]; }
@ -503,6 +498,10 @@ define([
Cryptpad.setLanguage(data, cb);
});
sframeChan.on('Q_CONTACTS_CLEAR_OWNED_CHANNEL', function (channel, cb) {
Cryptpad.clearOwnedChannel(channel, cb);
});
if (cfg.addRpc) {
cfg.addRpc(sframeChan, Cryptpad, Utils);
}
@ -539,9 +538,6 @@ define([
sframeChan.on('Q_CONTACTS_SET_CHANNEL_HEAD', function (opt, cb) {
Cryptpad.messenger.setChannelHead(opt, cb);
});
sframeChan.on('Q_CONTACTS_CLEAR_OWNED_CHANNEL', function (channel, cb) {
Cryptpad.clearOwnedChannel(channel, cb);
});
Cryptpad.messenger.onMessageEvent.reg(function (data) {
sframeChan.event('EV_CONTACTS_MESSAGE', data);
@ -567,7 +563,8 @@ define([
// Join the netflux channel
var rtStarted = false;
var startRealtime = function () {
var startRealtime = function (rtConfig) {
rtConfig = rtConfig || {};
rtStarted = true;
var replaceHash = function (hash) {
if (window.history && window.history.replaceState) {
@ -581,7 +578,7 @@ define([
window.location.hash = hash;
};
CpNfOuter.start({
var cpNfCfg = {
sframeChan: sframeChan,
channel: secret.channel,
padRpc: Cryptpad.padRpc,
@ -600,7 +597,11 @@ define([
if (readOnly || cfg.noHash) { return; }
replaceHash(Utils.Hash.getEditHashFromKeys(wc, secret.keys));
}
};
Object.keys(rtConfig).forEach(function (k) {
cpNfCfg[k] = rtConfig[k];
});
CpNfOuter.start(cpNfCfg);
};
sframeChan.on('Q_CREATE_PAD', function (data, cb) {
@ -624,12 +625,11 @@ define([
var rtConfig = {};
if (data.owned) {
//rtConfig.owners = [edPublic];
rtConfig.owners = [edPublic];
}
if (data.expire) {
//rtConfig.expire = data.expire;
rtConfig.expire = data.expire;
}
if (data.template) {
// Pass rtConfig to useTemplate because Cryptput will create the file and
// we need to have the owners and expiration time in the first line on the
@ -651,7 +651,7 @@ define([
if (!realtime) { return; }
if (isNewFile && Utils.LocalStore.isLoggedIn()
&& AppConfig.displayCreationScreen) { return; }
&& AppConfig.displayCreationScreen && cfg.useCreationScreen) { return; }
startRealtime();
});

@ -91,6 +91,7 @@ define([
funcs.createLanguageSelector = callWithCommon(UIElements.createLanguageSelector);
funcs.createMarkdownToolbar = callWithCommon(UIElements.createMarkdownToolbar);
funcs.getPadCreationScreen = callWithCommon(UIElements.getPadCreationScreen);
funcs.createNewPadModal = callWithCommon(UIElements.createNewPadModal);
// Thumb
funcs.displayThumbnail = callWithCommon(Thumb.displayThumbnail);
@ -165,6 +166,14 @@ define([
};
// Store
funcs.createPad = function (cfg, cb) {
ctx.sframeChan.query("Q_CREATE_PAD", {
owned: cfg.owned,
expire: cfg.expire,
template: cfg.template
}, cb);
};
funcs.sendAnonRpcMsg = function (msg, content, cb) {
ctx.sframeChan.query('Q_ANON_RPC_MESSAGE', {
msg: msg,

@ -34,6 +34,11 @@ define([], function () {
if (locks.length === 1) {
runLock(locks.shift());
}
},
assert: function (expr) {
if (expr || failed) { return; }
failed = true;
out.failed("Failed assertion");
}
});
};
@ -131,10 +136,12 @@ define([], function () {
};
var enableManual = function () {
out.testing = 'manual';
console.log('manual testing enabled');
out.passed = function () {
window.alert("Test passed");
};
out.failed = function (reason) {
try { throw new Error(reason); } catch (err) { console.log(err.stack); }
window.alert("Test failed [" + reason + "]");
};
out.registerInner = function () { };
@ -146,18 +153,18 @@ define([], function () {
out.registerInner = function () { };
out.registerOuter = function () { };
if (window.location.hash.indexOf("test=auto") > -1) {
enableAuto();
} else if (window.location.hash.indexOf("test=manual") > -1) {
enableManual();
} else if (document.cookie.indexOf('test=') === 0) {
if (document.cookie.indexOf('test=') === 0) {
try {
var x = JSON.parse(decodeURIComponent(document.cookie.replace('test=', '')));
if (x.test === 'auto') {
out.options = x.opts;
enableAuto('auto');
console.log("Enable auto testing " + window.origin);
} else if (x.test === 'manual') {
out.options = x.opts;
enableManual();
console.log("Enable manual testing " + window.origin);
}
console.log("Enable auto testing " + window.origin);
} catch (e) { }
}

@ -496,6 +496,13 @@ define([
content: '<span class="fa fa-eye"></span> ' + Messages.getEmbedCode
});
}
if (typeof(Config.customizeShareOptions) === 'function') {
Config.customizeShareOptions(hashes, options, {
type: 'DEFAULT',
origin: origin,
pathname: pathname
});
}
var dropdownConfigShare = {
text: $('<div>').append($shareIcon).html(),
options: options,
@ -582,6 +589,15 @@ define([
attributes: {title: Messages.fileEmbedTitle, 'class': 'cp-toolbar-share-file-embed'},
content: '<span class="fa fa-file"></span> ' + Messages.getEmbedCode
});
if (typeof(Config.customizeShareOptions) === 'function') {
Config.customizeShareOptions(hashes, options, {
type: 'FILE',
origin: origin,
pathname: pathname
});
}
var dropdownConfigShare = {
text: $('<div>').append($shareIcon).html(),
options: options,
@ -917,6 +933,21 @@ define([
content: $('<div>').append(UI.getIcon(p)).html() + Messages.type[p]
});
});
if (Config.displayCreationScreen) {
pads_options.push({
tag: 'a',
attributes: {
id: 'cp-app-toolbar-creation-advanced',
href: origin
},
content: '<span class="fa fa-plus-circle"></span> ' + Messages.creation_appMenuName
});
$(window).keydown(function (e) {
if (e.which === 69 && e.ctrlKey) {
Common.createNewPadModal();
}
});
}
var dropdownConfig = {
text: '', // Button initial text
options: pads_options, // Entries displayed in the menu
@ -929,6 +960,10 @@ define([
var $newPadBlock = UIElements.createDropdown(dropdownConfig);
$newPadBlock.find('button').attr('title', Messages.newButtonTitle);
$newPadBlock.find('button').addClass('fa fa-th');
$newPadBlock.find('#cp-app-toolbar-creation-advanced').click(function (e) {
e.preventDefault();
Common.createNewPadModal();
});
return $newPadBlock;
};

@ -453,6 +453,12 @@ define([
.map(function (str) { return Number(str); });
return sorted;
};
exp.getOwnedPads = function (edPub) {
var allFiles = files[FILES_DATA];
return Object.keys(allFiles).filter(function (id) {
return allFiles[id].owners && allFiles[id].owners.indexOf(edPub) !== -1;
}).map(function (k) { return Number(k); });
};
/**
* OPERATIONS

@ -664,6 +664,10 @@ span {
}
}
.cp-app-drive-new-name {
white-space: nowrap;
}
@media screen and (max-height: @browser_media-not-big) {
.cp-modal {
& > p {

@ -24,6 +24,7 @@
<li><a tabindex="-1" data-icon="fa-eye" class="cp-app-drive-context-openro dropdown-item" data-localization="fc_open_ro">Open (read-only)</a></li>
<li><a tabindex="-1" data-icon="fa-pencil" class="cp-app-drive-context-rename cp-app-drive-context-editable dropdown-item" data-localization="fc_rename">Rename</a></li>
<li><a tabindex="-1" data-icon="fa-trash" class="cp-app-drive-context-delete cp-app-drive-context-editable dropdown-item" data-localization="fc_delete">Delete</a></li>
<li><a tabindex="-1" data-icon="fa-eraser" class="cp-app-drive-context-deleteowned dropdown-item" data-localization="fc_delete_owned">Delete permanently</a></li>
<li><a tabindex="-1" data-icon="fa-folder" class="cp-app-drive-context-newfolder cp-app-drive-context-editable dropdown-item" data-localization="fc_newfolder">New folder</a></li>
<li><a tabindex="-1" data-icon="fa-database" class="cp-app-drive-context-properties dropdown-item" data-localization="fc_prop">Properties</a></li>
<li><a tabindex="-1" data-icon="fa-hashtag" class="cp-app-drive-context-hashtag dropdown-item" data-localization="fc_hashtag">Tags</a></li>
@ -44,6 +45,7 @@
<li><a tabindex="-1" data-icon="fa-folder-open" class="cp-app-drive-context-open dropdown-item" data-localization="fc_open">Open</a></li>
<li><a tabindex="-1" data-icon="fa-eye" class="cp-app-drive-context-openro dropdown-item" data-localization="fc_open_ro">Open (read-only)</a></li>
<li><a tabindex="-1" data-icon="fa-trash" class="cp-app-drive-context-delete dropdown-item" data-localization="fc_delete">Delete</a></li>
<li><a tabindex="-1" data-icon="fa-eraser" class="cp-app-drive-context-deleteowned dropdown-item" data-localization="fc_delete_owned">Delete permanently</a></li>
<li><a tabindex="-1" data-icon="fa-database" class="cp-app-drive-context-properties dropdown-item" data-localization="fc_prop">Properties</a></li>
<li><a tabindex="-1" data-icon="fa-hashtag" class="cp-app-drive-context-hashtag dropdown-item" data-localization="fc_hashtag">Tags</a></li>
</ul>

@ -60,6 +60,8 @@ define([
var TRASH_NAME = Messages.fm_trashName;
var RECENT = "recent";
var RECENT_NAME = Messages.fm_recentPadsName;
var OWNED = "owned";
var OWNED_NAME = Messages.fm_ownedPadsName;
var LS_LAST = "app-drive-lastOpened";
var LS_OPENED = "app-drive-openedFolders";
@ -180,6 +182,8 @@ define([
var $addIcon = $('<span>', {"class": "fa fa-plus"});
var $renamedIcon = $('<span>', {"class": "fa fa-flag"});
var $readonlyIcon = $('<span>', {"class": "fa fa-eye"});
var $ownedIcon = $('<span>', {"class": "fa fa-id-card-o"});
var $ownerIcon = $('<span>', {"class": "fa fa-id-card"});
var history = {
isHistoryMode: false,
@ -202,6 +206,7 @@ define([
var sframeChan = common.getSframeChannel();
var priv = metadataMgr.getPrivateData();
var user = metadataMgr.getUserData();
var edPublic = priv.edPublic;
APP.origin = priv.origin;
var isOwnDrive = function () {
@ -255,9 +260,10 @@ define([
// Categories dislayed in the menu
// _WORKGROUP_ : do not display unsorted
var displayedCategories = [ROOT, TRASH, SEARCH, RECENT];
if (AppConfig.displayCreationScreen) { displayedCategories.push(OWNED); }
if (AppConfig.enableTemplates) { displayedCategories.push(TEMPLATE); }
if (isWorkgroup()) { displayedCategories = [ROOT, TRASH, SEARCH]; }
var virtualCategories = [SEARCH, RECENT];
var virtualCategories = [SEARCH, RECENT, OWNED];
if (!APP.loggedIn) {
displayedCategories = [FILES_DATA];
@ -652,13 +658,19 @@ define([
if (!isOwnDrive()) {
hide.push($menu.find('a.cp-app-drive-context-own'));
}
if (!$element.is('.cp-app-drive-element-owned')) {
hide.push($menu.find('a.cp-app-drive-context-deleteowned'));
}
if ($element.is('.cp-app-drive-element-notrash')) {
hide.push($menu.find('a.cp-app-drive-context-delete'));
}
if ($element.is('.cp-app-drive-element-file')) {
// No folder in files
hide.push($menu.find('a.cp-app-drive-context-newfolder'));
if ($element.is('.cp-app-drive-readonly')) {
if ($element.is('.cp-app-drive-element-readonly')) {
// Keep only open readonly
hide.push($menu.find('a.cp-app-drive-context-open'));
} else if ($element.is('.cp-app-drive-noreadonly')) {
} else if ($element.is('.cp-app-drive-element-noreadonly')) {
// Keep only open readonly
hide.push($menu.find('a.cp-app-drive-context-openro'));
}
@ -1169,6 +1181,14 @@ define([
var $renamed = $renamedIcon.clone().appendTo($state);
$renamed.attr('title', Messages._getKey('fm_renamedPad', [data.title]));
}
if (data.owners && data.owners.indexOf(edPublic) !== -1) {
var $owned = $ownedIcon.clone().appendTo($state);
$owned.attr('title', Messages.fm_padIsOwned);
$span.addClass('cp-app-drive-element-owned');
} else if (data.owners && data.owners.length) {
var $owner = $ownerIcon.clone().appendTo($state);
$owner.attr('title', Messages.fm_padIsOwnedOther);
}
var name = filesOp.getTitle(element);
@ -1391,6 +1411,9 @@ define([
case RECENT:
msg = Messages.fm_info_recent;
break;
case OWNED:
msg = Messages.fm_info_owned;
break;
default:
msg = undefined;
}
@ -1702,10 +1725,10 @@ define([
};
var sortElements = function (folder, path, oldkeys, prop, asc, useId) {
var root = filesOp.find(path);
var root = path && filesOp.find(path);
var test = folder ? filesOp.isFolder : filesOp.isFile;
var keys = oldkeys.filter(function (e) {
return useId ? test(e) : test(root[e]);
return useId ? test(e) : (path && test(root[e]));
});
if (keys.length < 2) { return keys; }
var mult = asc ? 1 : -1;
@ -1899,6 +1922,10 @@ define([
};
var displayAllFiles = function ($container) {
if (AppConfig.disableAnonymousStore && !APP.loggedIn) {
$container.append(Messages.anonymousStoreDisabled);
return;
}
var allfiles = files[FILES_DATA];
if (allfiles.length === 0) { return; }
var $fileHeader = getFileListHeader(false);
@ -2044,7 +2071,7 @@ define([
var roClass = typeof(ro) === 'undefined' ? ' cp-app-drive-element-noreadonly' :
ro ? ' cp-app-drive-element-readonly' : '';
var $element = $('<li>', {
'class': 'cp-app-drive-element cp-app-drive-element-file cp-app-drive-element-row' + roClass,
'class': 'cp-app-drive-element cp-app-drive-element-notrash cp-app-drive-element-file cp-app-drive-element-row' + roClass,
});
$element.prepend($icon).dblclick(function () {
openFile(id);
@ -2065,6 +2092,42 @@ define([
});
};
// Owned pads category
var displayOwned = function ($container) {
var list = filesOp.getOwnedPads(edPublic);
if (list.length === 0) { return; }
var $fileHeader = getFileListHeader(false);
$container.append($fileHeader);
var sortedFiles = sortElements(false, false, list, APP.store[SORT_FILE_BY], !getSortFileDesc(), true);
sortedFiles.forEach(function (id) {
var paths = filesOp.findFile(id);
if (!paths.length) { return; }
var path = paths[0];
var $icon = getFileIcon(id);
var ro = filesOp.isReadOnlyFile(id);
// ro undefined maens it's an old hash which doesn't support read-only
var roClass = typeof(ro) === 'undefined' ? ' cp-app-drive-element-noreadonly' :
ro ? ' cp-app-drive-element-readonly' : '';
var $element = $('<li>', {
'class': 'cp-app-drive-element cp-app-drive-element-notrash ' +
'cp-app-drive-element-file cp-app-drive-element-row' + roClass
});
$element.prepend($icon).dblclick(function () {
openFile(id);
});
addFileData(id, $element);
$element.data('path', path);
$element.data('element', id);
$element.click(function(e) {
e.stopPropagation();
onElementClick(e, $element);
});
$element.contextmenu(openDefaultContextMenu);
$element.data('context', $defaultContextMenu);
$container.append($element);
});
};
// Display the selected directory into the content part (rightside)
// NOTE: Elements in the trash are not using the same storage structure as the others
// _WORKGROUP_ : do not change the lastOpenedFolder value in localStorage
@ -2096,6 +2159,7 @@ define([
var isAllFiles = filesOp.comparePath(path, [FILES_DATA]);
var isSearch = path[0] === SEARCH;
var isRecent = path[0] === RECENT;
var isOwned = path[0] === OWNED;
var isVirtual = virtualCategories.indexOf(path[0]) !== -1;
var root = isVirtual ? undefined : filesOp.find(path);
@ -2194,6 +2258,8 @@ define([
displaySearch($list, path[1]);
} else if (isRecent) {
displayRecent($list);
} else if (isOwned) {
displayOwned($list);
} else {
$dirContent.contextmenu(openContentContextMenu);
if (filesOp.hasSubfolder(root)) { $list.append($folderHeader); }
@ -2374,6 +2440,15 @@ define([
$container.append($list);
};
var createOwned = function ($container, path) {
var $icon = $ownedIcon.clone(); // TODO
var isOpened = filesOp.comparePath(path, currentPath);
var $element = createTreeElement(OWNED_NAME, $icon, [OWNED], false, false, false, isOpened);
$element.addClass('root');
var $list = $('<ul>', { 'class': 'cp-app-drive-tree-category' }).append($element);
$container.append($list);
};
var search = APP.Search = {};
var createSearch = function ($container) {
var isInSearch = currentPath[0] === SEARCH;
@ -2437,6 +2512,7 @@ define([
var $div = $('<div>', {'class': 'cp-app-drive-tree-categories-container'})
.appendTo($tree);
if (displayedCategories.indexOf(RECENT) !== -1) { createRecent($div, [RECENT]); }
if (displayedCategories.indexOf(OWNED) !== -1) { createOwned($div, [OWNED]); }
if (displayedCategories.indexOf(ROOT) !== -1) { createTree($div, [ROOT]); }
if (displayedCategories.indexOf(TEMPLATE) !== -1) { createTemplate($div, [TEMPLATE]); }
if (displayedCategories.indexOf(FILES_DATA) !== -1) { createAllFiles($div, [FILES_DATA]); }
@ -2499,86 +2575,22 @@ define([
}
var ro = filesOp.isReadOnlyFile(el);
var base = APP.origin;
var $d = $('<div>');
$('<strong>').text(Messages.fc_prop).appendTo($d);
var data = filesOp.getFileData(el);
if (!data || !data.href) { return void cb(void 0, $d); }
$('<br>').appendTo($d);
if (!ro) {
$('<label>', {'for': 'cp-app-drive-prop-link'}).text(Messages.editShare).appendTo($d);
$d.append(UI.dialog.selectable(base + data.href, {
id: 'cp-app-drive-prop-link',
}));
}
var parsed = Hash.parsePadUrl(data.href);
if (parsed.hashData && parsed.hashData.type === 'pad') {
var roLink = ro ? base + data.href : base + getReadOnlyUrl(el);
if (roLink) {
$('<label>', {'for': 'cp-app-drive-prop-rolink'}).text(Messages.viewShare).appendTo($d);
$d.append(UI.dialog.selectable(roLink, {
id: 'cp-app-drive-prop-rolink',
}));
}
}
if (data.tags && Array.isArray(data.tags)) {
$('<label>', {'for': 'cp-app-drive-prop-tags'}).text(Messages.fm_prop_tagsList).appendTo($d);
$d.append(UI.dialog.selectable(data.tags.join(', '), {
id: 'cp-app-drive-prop-tags',
}));
}
$('<label>', {'for': 'cp-app-drive-prop-ctime'}).text(Messages.fm_creation)
.appendTo($d);
$d.append(UI.dialog.selectable(new Date(data.ctime).toLocaleString(), {
id: 'cp-app-drive-prop-ctime',
}));
$('<label>', {'for': 'cp-app-drive-prop-atime'}).text(Messages.fm_lastAccess)
.appendTo($d);
$d.append(UI.dialog.selectable(new Date(data.atime).toLocaleString(), {
id: 'cp-app-drive-prop-atime',
}));
if (APP.loggedIn && AppConfig.enablePinning) {
// check the size of this file...
common.getFileSize(data.href, function (e, bytes) {
if (e) {
// there was a problem with the RPC
logError(e);
// but we don't want to break the interface.
// continue as if there was no RPC
return void cb(void 0, $d);
}
var KB = Util.bytesToKilobytes(bytes);
var formatted = Messages._getKey('formattedKB', [KB]);
$('<br>').appendTo($d);
$('<label>', {
'for': 'cp-app-drive-prop-size'
}).text(Messages.fc_sizeInKilobytes).appendTo($d);
$d.append(UI.dialog.selectable(formatted, {
id: 'cp-app-drive-prop-size',
}));
cb(void 0, $d);
});
var data = JSON.parse(JSON.stringify(filesOp.getFileData(el)));
if (!data || !data.href) { return void cb('INVALID_FILE'); }
data.href = base + data.href;
if (ro) {
data.roHref = data.href;
delete data.href;
} else {
cb(void 0, $d);
data.roHref = base + getReadOnlyUrl(el);
}
UIElements.getProperties(common, data, cb);
};
$contextMenu.on("click", "a", function(e) {
e.stopPropagation();
var paths = $(this).data('paths');
//var path = $(this).data('path');
//var $element = $(this).data('element');
var el;
if (paths.length === 0) {
@ -2595,6 +2607,29 @@ define([
paths.forEach(function (p) { pathsList.push(p.path); });
moveElements(pathsList, [TRASH], false, refresh);
}
else if ($(this).hasClass('cp-app-drive-context-deleteowned')) {
var msgD = Messages.fm_deleteOwnedPads;
UI.confirm(msgD, function(res) {
$(window).focus();
if (!res) { return; }
// Try to delete each selected pad from server, and delete from drive if no error
var n = nThen(function () {});
paths.forEach(function (p) {
var el = filesOp.find(p.path);
var data = filesOp.getFileData(el);
var parsed = Hash.parsePadUrl(data.href);
var channel = Util.base64ToHex(parsed.hashData.channel);
n = n.nThen(function (waitFor) {
sframeChan.query('Q_CONTACTS_CLEAR_OWNED_CHANNEL', channel,
waitFor(function (e) {
if (e) { return void console.error(e); }
filesOp.delete([p.path], refresh);
}));
});
});
});
return;
}
else if ($(this).hasClass('cp-app-drive-context-open')) {
paths.forEach(function (p) {
var $element = p.element;
@ -2683,6 +2718,29 @@ define([
}
moveElements(pathsList, [TRASH], false, refresh);
}
else if ($(this).hasClass('cp-app-drive-context-deleteowned')) {
var msgD = Messages.fm_deleteOwnedPads;
UI.confirm(msgD, function(res) {
$(window).focus();
if (!res) { return; }
// Try to delete each selected pad from server, and delete from drive if no error
var n = nThen(function () {});
paths.forEach(function (p) {
var el = filesOp.find(p.path);
var data = filesOp.getFileData(el);
var parsed = Hash.parsePadUrl(data.href);
var channel = Util.base64ToHex(parsed.hashData.channel);
n = n.nThen(function (waitFor) {
sframeChan.query('Q_CONTACTS_CLEAR_OWNED_CHANNEL', channel,
waitFor(function (e) {
if (e) { return void console.error(e); }
filesOp.delete([p.path], refresh);
}));
});
});
});
return;
}
else if ($(this).hasClass("cp-app-drive-context-properties")) {
if (paths.length !== 1) { return; }
el = filesOp.find(paths[0].path);

@ -45,6 +45,7 @@ define([
sframeChan.event("EV_FILE_PICKED", {
type: parsed.type,
src: src,
name: data.name,
key: parsed.hashData.key
});
return;
@ -52,6 +53,7 @@ define([
sframeChan.event("EV_FILE_PICKED", {
type: parsed.type,
href: data.url,
name: data.name
});
};
@ -66,8 +68,8 @@ define([
APP.FM = common.createFileManager(fmConfig);
// Create file picker
var onSelect = function (url) {
onFilePicked({url: url});
var onSelect = function (url, name) {
onFilePicked({url: url, name: name});
};
var data = {
FM: APP.FM
@ -132,7 +134,7 @@ define([
$('<span>', {'class': 'cp-filepicker-content-element-name'}).text(name)
.appendTo($span);
$span.click(function () {
if (typeof onSelect === "function") { onSelect(data.href); }
if (typeof onSelect === "function") { onSelect(data.href, name); }
});
// Add thumbnail if it exists

@ -74,7 +74,6 @@ define([
config.addCommonRpc(sframeChan);
sframeChan.on('Q_GET_FILES_LIST', function (types, cb) {
console.error("TODO: make sure Q_GET_FILES_LIST is only available from filepicker");
Cryptpad.getSecureFilesList(types, function (err, data) {
cb({
error: err,

@ -1,16 +1,18 @@
define([
'jquery',
'/common/cryptpad-common.js',
'/common/login.js',
'/customize/login.js',
'/common/common-interface.js',
'/common/common-realtime.js',
'/common/common-feedback.js',
'/common/outer/local-store.js',
'/common/test.js',
'less!/bower_components/components-font-awesome/css/font-awesome.min.css',
], function ($, Cryptpad, Login, UI, Realtime, Feedback, LocalStore) {
], function ($, Cryptpad, Login, UI, Realtime, Feedback, LocalStore, Test) {
$(function () {
var $main = $('#mainBlock');
var $checkImport = $('#import-recent');
var Messages = Cryptpad.Messages;
// main block is hidden in case javascript is disabled
@ -53,10 +55,12 @@ define([
});
var hashing = false;
var test;
$('button.login').click(function () {
if (hashing) { return void console.log("hashing is already in progress"); }
hashing = true;
var shouldImport = $checkImport[0].checked;
// setTimeout 100ms to remove the keyboard on mobile devices before the loading screen pops up
window.setTimeout(function () {
@ -89,6 +93,14 @@ define([
Realtime.whenRealtimeSyncs(result.realtime, function() {
LocalStore.login(result.userHash, result.userName, function () {
hashing = false;
if (test) {
localStorage.clear();
test.pass();
return;
}
if (shouldImport) {
sessionStorage.migrateAnonDrive = 1;
}
if (sessionStorage.redirectTo) {
var h = sessionStorage.redirectTo;
var parser = document.createElement('a');
@ -145,5 +157,12 @@ define([
}
window.location.href = '/register/';
});
Test(function (t) {
$uname.val('testuser');
$passwd.val('testtest');
test = t;
$('button.login').click();
});
});
});

@ -16,8 +16,9 @@
display: flex;
}
#cke_1_toolbox {
display: inline-block;
display: inline-flex;
width: 100%;
flex-flow: column;
background-color: @colortheme_pad-toolbar-bg;
}
#cke_1_toolbox .cke_toolbar {

@ -13,10 +13,10 @@ define([
'/common/diffMarked.js',
'/common/sframe-common-codemirror.js',
'/common/common-thumbnail.js',
'/bower_components/chainpad/chainpad.dist.js',
'/common/common-interface.js',
'/customize/messages.js',
'cm/lib/codemirror',
'/common/test.js',
'cm/addon/display/placeholder',
'cm/mode/markdown/markdown',
@ -42,10 +42,10 @@ define([
DiffMd,
SframeCM,
Thumb,
ChainPad,
UI,
Messages,
CMeditor)
CMeditor,
Test)
{
var saveAs = window.saveAs;
@ -751,7 +751,7 @@ define([
}).appendTo($msg);
// Actions
if (!c.profile || c.profile === profile) {
if (!APP.readOnly && (!c.profile || c.profile === profile)) {
$('<button>', {
'class': 'btn btn-secondary fa fa-times',
'title': Messages.poll_comment_remove,
@ -1045,6 +1045,79 @@ define([
publish(true);
}
var passIfOk = function (t) {
t.assert($('#cp-app-poll-description-published').text().indexOf(
"Content for the description") === 0);
t.assert($('.cp-app-poll-comments-list-data-name').text().indexOf(
"Mr.Me") === 0);
t.assert($('.cp-app-poll-comments-list-msg-text').text().indexOf(
"Example comment yay") === 0);
t.assert($('input[value="Candy"]').length === 1);
t.assert($('input[value="IceCream"]').length === 1);
t.assert($('input[value="Soda"]').length === 1);
t.assert($('input[value="Meeee"]').length === 1);
t.pass();
};
if (!APP.readOnly) {
console.log("Here is the test");
Test(function (t) {
if ($('input[value="Candy"]').length) {
t.fail("Test has already been performed");
return;
}
nThen(function (waitFor) {
console.log("Here is the test1");
APP.editor.setValue("Content for the description");
$('.cp-app-poll-table-editing .cp-app-poll-table-text-cell input').val(
'Candy').keyup();
$('#cp-app-poll-create-option').click();
// TODO(cjd): Need to click outside to lock the first option we create.. bug?
$(window).trigger({ type: "click", which: 1 });
setTimeout(waitFor());
}).nThen(function (waitFor) {
$('.cp-app-poll-table-editing .cp-app-poll-table-text-cell input').val(
'IceCream').keyup();
$('#cp-app-poll-create-option').click();
setTimeout(waitFor());
}).nThen(function (waitFor) {
$('.cp-app-poll-table-editing .cp-app-poll-table-text-cell input').val(
'Soda').keyup();
$('#cp-app-poll-create-option').click();
setTimeout(waitFor());
}).nThen(function (waitFor) {
// Switch to non-admin mode
$('.cp-toolbar-rightside-button.fa-check').click();
setTimeout(waitFor());
}).nThen(function (waitFor) {
$('.cp-app-poll-comments-add-name').val("Mr.Me").keyup();
$('.cp-app-poll-comments-add-msg').val("Example comment yay").keyup();
setTimeout(waitFor());
}).nThen(function (waitFor) {
$('.cp-app-poll-comments-add-submit').click();
setTimeout(waitFor());
}).nThen(function (waitFor) {
$('#cp-app-poll-create-user').parent().find('input').val('Meeee').keyup();
[1,3,2].forEach(function (num, i) {
var x = $($('.cp-app-poll-table-checkbox-contain label')[i]);
for (var ii = 0; ii < num; ii++) {
x.trigger({ type: 'click', which: 1 });
}
});
setTimeout(waitFor());
}).nThen(function (waitFor) {
$('#cp-app-poll-create-user').click();
setTimeout(waitFor());
}).nThen(function (waitFor) {
APP.rt.realtime.onSettle(waitFor());
}).nThen(function (/*waitFor*/) {
passIfOk(t);
});
});
} else {
Test(passIfOk);
}
UI.removeLoadingScreen();
if (isNew) {
common.openTemplatePicker();
@ -1183,6 +1256,7 @@ define([
}).nThen(function (waitFor) {
common.getSframeChannel().onReady(waitFor());
}).nThen(function (/* waitFor */) {
Test.registerInner(common.getSframeChannel());
var metadataMgr = common.getMetadataMgr();
APP.locked = APP.readOnly = metadataMgr.getPrivateData().readOnly;
APP.loggedIn = common.isLoggedIn();

@ -1,9 +1,9 @@
define([
'jquery',
'/common/login.js',
'/customize/login.js',
'/common/cryptpad-common.js',
'/common/test.js',
'/common/credential.js', // preloaded for login.js
'/customize/credential.js', // preloaded for login.js
'/common/common-interface.js',
'/common/common-util.js',
'/common/common-realtime.js',
@ -54,11 +54,8 @@ define([
var $register = $('button#register');
var registering = false;
var test;
var logMeIn = function (result) {
if (Test.testing) {
Test.passed();
return;
}
LocalStore.setUserHash(result.userHash);
var proxy = result.proxy;
@ -72,6 +69,11 @@ define([
Realtime.whenRealtimeSyncs(result.realtime, function () {
LocalStore.login(result.userHash, result.userName, function () {
registering = false;
if (test) {
localStorage.clear();
test.pass();
return;
}
if (sessionStorage.redirectTo) {
var h = sessionStorage.redirectTo;
var parser = document.createElement('a');
@ -236,8 +238,9 @@ define([
}
});
Test(function () {
$uname.val('test' + Math.random());
Test(function (t) {
test = t;
$uname.val('testuser');
$passwd.val('testtest');
$confirm.val('testtest');
$checkImport[0].checked = true;

@ -5,6 +5,7 @@
@import (once) '../../customize/src/less2/include/alertify.less';
@import (once) '../../customize/src/less2/include/sidebar-layout.less';
@import (once) "../../customize/src/less2/include/limit-bar.less";
@import (once) "../../customize/src/less2/include/creation.less";
.toolbar_main(
@bg-color: @colortheme_settings-bg,
@ -14,6 +15,7 @@
.alertify_main();
.sidebar-layout_main();
.limit-bar_main();
.creation_main();
// body
&.cp-app-settings {
@ -55,7 +57,7 @@
width: @sidebar_button-width;
}
}
.cp-settings-backup-drive {
.cp-settings-drive-backup {
button {
span.fa {
margin-right: 5px;
@ -63,6 +65,77 @@
margin-right: 5px;
}
}
.cp-settings-creation-owned, .cp-settings-creation-expire,
.cp-settings-creation-skip, .cp-settings-creation-template {
input[type="radio"] {
display: none;
&:checked {
& + label {
font-weight: bold;
background-color: lighten(@colortheme_loading-bg, 20%);
cursor: default;
border: 1px solid #c1158e;
color: @colortheme_loading-color;
&:hover {
background-color: lighten(@colortheme_loading-bg, 20%);
}
}
}
}
input[type="radio"] + label {
.tools_unselectable();
display: inline-flex;
align-items: center;
justify-content: center;
width: 200px;
height: 50px;
padding: 5px;
margin: 0 20px;
border: 1px solid black;
cursor: pointer;
&:hover {
background-color: lighten(@colortheme_loading-bg, 10%);
}
}
.fa {
margin-left: 50px;
}
}
.cp-settings-creation-skipped {
display: none !important; // we have to override an inline style attribute
}
.cp-settings-creation-expire {
#cp-creation-expire-true {
display: none;
&:checked {
& + label {
height: 100px;
.cp-creation-expire-picker {
display: inline;
}
}
}
}
label[for="cp-creation-expire-true"] {
flex-wrap: wrap;
.cp-creation-expire-picker {
display: none;
}
input {
width: 70px;
}
select {
width: 100px;
}
input, select {
border: none;
height: 30px;
background: @colortheme_loading-bg;
color: @colortheme_loading-color;
border-radius: 3px;
}
}
}
}
}
}

@ -4,9 +4,12 @@ define([
'/bower_components/nthen/index.js',
'/common/sframe-common.js',
'/common/common-interface.js',
'/common/common-ui-elements.js',
'/common/common-util.js',
'/common/common-hash.js',
'/customize/messages.js',
'/common/hyperscript.js',
'/customize/application_config.js',
'/bower_components/file-saver/FileSaver.min.js',
'css!/bower_components/bootstrap/dist/css/bootstrap.min.css',
@ -18,9 +21,12 @@ define([
nThen,
SFCommon,
UI,
UIElements,
Util,
Hash,
Messages
Messages,
h,
AppConfig
)
{
var saveAs = window.saveAs;
@ -41,21 +47,35 @@ define([
'cp-settings-thumbnails',
'cp-settings-userfeedback'
],
'creation': [
'cp-settings-creation-owned',
'cp-settings-creation-expire',
'cp-settings-creation-skip',
'cp-settings-creation-template'
],
'drive': [
'cp-settings-backup-drive',
'cp-settings-import-local-pads',
'cp-settings-reset-drive'
'cp-settings-drive-backup',
'cp-settings-drive-import-local',
'cp-settings-drive-reset'
],
'pad': [
'cp-settings-pad-width',
],
'code': [
'cp-settings-indent-unit',
'cp-settings-indent-type'
'cp-settings-code-indent-unit',
'cp-settings-code-indent-type'
]
};
var createInfoBlock = function () {
if (!AppConfig.dislayCreationScreen) {
delete categories.creation;
}
var create = {};
// Account settings
create['info-block'] = function () {
var $div = $('<div>', {'class': 'cp-settings-info-block'});
var $account = $('<div>', {'class': 'cp-sidebarlayout-element'}).appendTo($div);
@ -81,7 +101,7 @@ define([
};
// Create the block containing the display name field
var createDisplayNameInput = function () {
create['displayname'] = function () {
var $div = $('<div>', {'class': 'cp-settings-displayname cp-sidebarlayout-element'});
$('<label>', {'for' : 'cp-settings-displayname'}).text(Messages.user_displayName).appendTo($div);
var $inputBlock = $('<div>', {'class': 'cp-sidebarlayout-input-block'}).appendTo($div);
@ -124,111 +144,49 @@ define([
return $div;
};
var createIndentUnitSelector = function () {
var $div = $('<div>', {
'class': 'cp-settings-indent-unit cp-sidebarlayout-element'
});
$('<label>').text(Messages.settings_codeIndentation).appendTo($div);
var $inputBlock = $('<div>', {
'class': 'cp-sidebarlayout-input-block',
}).appendTo($div);
var $input = $('<input>', {
'min': 1,
'max': 8,
type: 'number',
}).on('change', function () {
var val = parseInt($input.val());
if (typeof(val) !== 'number') { return; }
common.setAttribute(['codemirror', 'indentUnit'], val);
}).appendTo($inputBlock);
common.getAttribute(['codemirror', 'indentUnit'], function (e, val) {
if (e) { return void console.error(e); }
if (typeof(val) !== 'number') {
$input.val(2);
} else {
$input.val(val);
}
});
return $div;
};
var createIndentTypeSelector = function () {
var key = 'indentWithTabs';
var $div = $('<div>', {
'class': 'cp-settings-indent-type cp-sidebarlayout-element'
});
$('<label>').text(Messages.settings_codeUseTabs).appendTo($div);
var $inputBlock = $('<div>', {
'class': 'cp-sidebarlayout-input-block',
}).css('flex-flow', 'column')
.appendTo($div);
var $input = $('<input>', {
type: 'checkbox',
}).on('change', function () {
var val = $input.is(':checked');
if (typeof(val) !== 'boolean') { return; }
common.setAttribute(['codemirror', key], val);
}).appendTo($inputBlock);
/*proxy.on('change', ['settings', 'codemirror', key], function (o, n) {
$input[0].checked = !!n;
});*/
common.getAttribute(['codemirror', key], function (e, val) {
if (e) { return void console.error(e); }
$input[0].checked = !!val;
});
create['language-selector'] = function () {
var $div = $('<div>', {'class': 'cp-settings-language-selector cp-sidebarlayout-element'});
$('<label>').text(Messages.language).appendTo($div);
var $b = common.createLanguageSelector($div);
$b.find('button').addClass('btn btn-secondary');
return $div;
};
var createPadWidthSelector = function () {
var $div = $('<div>', {
'class': 'cp-settings-pad-width cp-sidebarlayout-element'
});
$('<span>', {'class': 'label'}).text(Messages.settings_padWidth).appendTo($div);
create['logout-everywhere'] = function () {
if (!common.isLoggedIn()) { return; }
var $div = $('<div>', { 'class': 'cp-settings-logout-everywhere cp-sidebarlayout-element'});
$('<label>', { 'for': 'cp-settings-logout-everywhere'})
.text(Messages.settings_logoutEverywhereTitle).appendTo($div);
$('<span>', {'class': 'cp-sidebarlayout-description'})
.text(Messages.settings_padWidthHint).appendTo($div);
var $ok = $('<span>', {'class': 'fa fa-check', title: Messages.saved});
var $spinner = $('<span>', {'class': 'fa fa-spinner fa-pulse'});
var $label = $('<label>', { 'for': 'cp-settings-padwidth', 'class': 'noTitle' })
.text(Messages.settings_padWidthLabel);
var $input = $('<input>', {
type: 'checkbox',
id: 'cp-settings-padwidth'
}).on('change', function () {
$spinner.show();
$ok.hide();
var val = $input.is(':checked');
common.setAttribute(['pad', 'width'], val, function () {
$spinner.hide();
$ok.show();
});
}).appendTo($div);
$label.appendTo($div);
.text(Messages.settings_logoutEverywhere).appendTo($div);
var $button = $('<button>', {
id: 'cp-settings-logout-everywhere',
'class': 'btn btn-primary'
}).text(Messages.settings_logoutEverywhereButton)
.appendTo($div);
var $ok = $('<span>', {'class': 'fa fa-check', title: Messages.saved}).hide().appendTo($div);
var $spinner = $('<span>', {'class': 'fa fa-spinner fa-pulse'}).hide().appendTo($div);
$ok.hide().appendTo($div);
$spinner.hide().appendTo($div);
$button.click(function () {
UI.confirm(Messages.settings_logoutEverywhereConfirm, function (yes) {
if (!yes) { return; }
$spinner.show();
$ok.hide();
common.getAttribute(['pad', 'width'], function (e, val) {
if (e) { return void console.error(e); }
if (val) {
$input.attr('checked', 'checked');
}
sframeChan.query('Q_SETTINGS_LOGOUT', null, function () {
$spinner.hide();
$ok.show();
window.setTimeout(function () {
$ok.fadeOut(1500);
}, 2500);
});
});
});
return $div;
};
var createResetTips = function () {
create['resettips'] = function () {
var $div = $('<div>', {'class': 'cp-settings-resettips cp-sidebarlayout-element'});
$('<label>').text(Messages.settings_resetTips).appendTo($div);
$('<span>', {'class': 'cp-sidebarlayout-description'})
@ -249,7 +207,7 @@ define([
return $div;
};
var createThumbnails = function () {
create['thumbnails'] = function () {
var $div = $('<div>', {'class': 'cp-settings-thumbnails cp-sidebarlayout-element'});
$('<label>').text(Messages.settings_thumbnails).appendTo($div);
@ -301,8 +259,286 @@ define([
return $div;
};
var createBackupDrive = function () {
var $div = $('<div>', {'class': 'cp-settings-backup-drive cp-sidebarlayout-element'});
create['userfeedback'] = function () {
var $div = $('<div>', { 'class': 'cp-settings-userfeedback cp-sidebarlayout-element'});
$('<span>', {'class': 'label'}).text(Messages.settings_userFeedbackTitle).appendTo($div);
var $label = $('<label>', { 'for': 'cp-settings-userfeedback', 'class': 'noTitle' })
.text(Messages.settings_userFeedback);
$('<span>', {'class': 'cp-sidebarlayout-description'})
.append(Messages.settings_userFeedbackHint1)
.append(Messages.settings_userFeedbackHint2).appendTo($div);
var $ok = $('<span>', {'class': 'fa fa-check', title: Messages.saved});
var $spinner = $('<span>', {'class': 'fa fa-spinner fa-pulse'});
var $checkbox = $('<input>', {
'type': 'checkbox',
id: 'cp-settings-userfeedback'
}).on('change', function () {
$spinner.show();
$ok.hide();
var val = $checkbox.is(':checked') || false;
common.setAttribute(['general', 'allowUserFeedback'], val, function () {
$spinner.hide();
$ok.show();
});
});
$checkbox.appendTo($div);
$label.appendTo($div);
$ok.hide().appendTo($div);
$spinner.hide().appendTo($div);
if (privateData.feedbackAllowed) {
$checkbox[0].checked = true;
}
return $div;
};
// Pad Creation settings
var setHTML = function (e, html) {
e.innerHTML = html;
return e;
};
create['creation-owned'] = function () {
if (!common.isLoggedIn()) { return; }
var owned = h('div.cp-settings-creation-owned.cp-sidebarlayout-element', [
h('label', [
Messages.creation_ownedTitle
]),
setHTML(h('p.cp-sidebarlayout-description'),
Messages.creation_owned1 + '<br>' + Messages.creation_owned2),
h('input#cp-creation-owned-true.cp-creation-owned-value', {
type: 'radio',
name: 'cp-creation-owned',
value: 1,
}),
h('label', { 'for': 'cp-creation-owned-true' }, Messages.creation_ownedTrue),
h('input#cp-creation-owned-false.cp-creation-owned-value', {
type: 'radio',
name: 'cp-creation-owned',
value: 0,
checked: 'checked'
}),
h('label', { 'for': 'cp-creation-owned-false' }, Messages.creation_ownedFalse),
h('span.fa.fa-check', {title: Messages.saved}),
h('span.fa.fa-spinner.fa-pulse'),
]);
var $owned = $(owned);
var $ok = $owned.find('.fa-check').hide();
var $spinner = $owned.find('.fa-spinner').hide();
$owned.find('input').change(function () {
$spinner.show();
$ok.hide();
var val = parseInt($owned.find('[name="cp-creation-owned"]:checked').val());
common.setAttribute(['general', 'creation', 'owned'], val, function (e) {
if (e) { return void console.error(e); }
$spinner.hide();
$ok.show();
});
});
common.getAttribute(['general', 'creation', 'owned'], function (e, val) {
if (val) {
$owned.find('#cp-creation-owned-true').attr('checked', true);
}
});
return $owned;
};
create['creation-expire'] = function () {
if (!common.isLoggedIn()) { return; }
var expire = h('div.cp-settings-creation-expire.cp-sidebarlayout-element', [
h('label', [
Messages.creation_expireTitle
]),
setHTML(h('p.cp-sidebarlayout-description'),
Messages.creation_expire1 + '<br>' + Messages.creation_expire2),
h('input#cp-creation-expire-false.cp-creation-expire-value', {
type: 'radio',
name: 'cp-creation-expire',
value: 0,
checked: 'checked'
}),
h('label', { 'for': 'cp-creation-expire-false' }, Messages.creation_expireFalse),
h('input#cp-creation-expire-true.cp-creation-expire-value', {
type: 'radio',
name: 'cp-creation-expire',
value: 1
}),
h('label', { 'for': 'cp-creation-expire-true' }, [
Messages.creation_expireTrue,
h('span.cp-creation-expire-picker', [
h('input#cp-creation-expire-val', {
type: "number",
min: 1,
max: 100,
value: 3
}),
h('select#cp-creation-expire-unit', [
h('option', { value: 'hour' }, Messages.creation_expireHours),
h('option', { value: 'day' }, Messages.creation_expireDays),
h('option', {
value: 'month',
selected: 'selected'
}, Messages.creation_expireMonths)
])
])
]),
h('span.fa.fa-check', {title: Messages.saved}),
h('span.fa.fa-spinner.fa-pulse'),
]);
var $expire = $(expire);
var $ok = $expire.find('.fa-check').hide();
var $spinner = $expire.find('.fa-spinner').hide();
var getValue = function () {
if(!parseInt($expire.find('[name="cp-creation-expire"]:checked').val())) { return 0; }
var unit = 0;
switch ($expire.find('#cp-creation-expire-unit').val()) {
case "hour" : unit = 3600; break;
case "day" : unit = 3600 * 24; break;
case "month": unit = 3600 * 24 * 30; break;
default: unit = 0;
}
return ($expire.find('#cp-creation-expire-val').val() || 0) * unit;
};
$expire.find('input, select').change(function () {
$spinner.show();
$ok.hide();
common.setAttribute(['general', 'creation', 'expire'], getValue(), function (e) {
if (e) { return void console.error(e); }
$spinner.hide();
$ok.show();
});
});
common.getAttribute(['general', 'creation', 'expire'], function (e, val) {
UIElements.setExpirationValue(val, $expire);
});
return $expire;
};
create['creation-skip'] = function () {
if (!common.isLoggedIn()) { return; }
var skip = h('div.cp-settings-creation-skip.cp-sidebarlayout-element', [
h('label', [
Messages.settings_creationSkip
]),
setHTML(h('p.cp-sidebarlayout-description'), Messages.settings_creationSkipHint),
h('input#cp-creation-skip-true.cp-creation-skip-value', {
type: 'radio',
name: 'cp-creation-skip',
value: 1,
}),
h('label', { 'for': 'cp-creation-skip-true' }, Messages.settings_creationSkipTrue),
h('input#cp-creation-skip-false.cp-creation-skip-value', {
type: 'radio',
name: 'cp-creation-skip',
value: 0,
checked: 'checked'
}),
h('label', { 'for': 'cp-creation-skip-false' }, Messages.settings_creationSkipFalse),
h('span.fa.fa-check', {title: Messages.saved}),
h('span.fa.fa-spinner.fa-pulse'),
]);
var $div = $(skip);
var $ok = $div.find('.fa-check').hide();
var $spinner = $div.find('.fa-spinner').hide();
$div.find('input').change(function () {
$spinner.show();
$ok.hide();
var val = parseInt($div.find('[name="cp-creation-skip"]:checked').val());
// If we don't skip the pad creation screen, we dont' need settings to hide the templates
// modal
if (!val) {
$('.cp-settings-creation-template').addClass('cp-settings-creation-skipped');
} else {
$('.cp-settings-creation-template').removeClass('cp-settings-creation-skipped');
}
common.setAttribute(['general', 'creation', 'skip'], val, function (e) {
if (e) { return void console.error(e); }
$spinner.hide();
$ok.show();
});
});
common.getAttribute(['general', 'creation', 'skip'], function (e, val) {
if (val) {
$div.find('#cp-creation-skip-true').attr('checked', true);
return;
}
// If we don't skip the pad creation screen, we dont' need settings to hide the templates
// modal
$('.cp-settings-creation-template').addClass('cp-settings-creation-skipped');
});
return $div;
};
create['creation-template'] = function () {
var skip = h('div.cp-settings-creation-template.cp-sidebarlayout-element', [
h('label', [
Messages.settings_templateSkip
]),
setHTML(h('p.cp-sidebarlayout-description'), Messages.settings_templateSkipHint),
h('input#cp-creation-template-true.cp-creation-template-value', {
type: 'radio',
name: 'cp-creation-template',
value: 1,
}),
h('label', { 'for': 'cp-creation-template-true' }, Messages.settings_creationSkipTrue),
h('input#cp-creation-template-false.cp-creation-template-value', {
type: 'radio',
name: 'cp-creation-template',
value: 0,
checked: 'checked'
}),
h('label', { 'for': 'cp-creation-template-false' }, Messages.settings_creationSkipFalse),
h('span.fa.fa-check', {title: Messages.saved}),
h('span.fa.fa-spinner.fa-pulse'),
]);
var $div = $(skip);
var $ok = $div.find('.fa-check').hide();
var $spinner = $div.find('.fa-spinner').hide();
$div.find('input').change(function () {
$spinner.show();
$ok.hide();
var val = parseInt($div.find('[name="cp-creation-template"]:checked').val());
common.setAttribute(['general', 'creation', 'noTemplate'], val, function (e) {
if (e) { return void console.error(e); }
$spinner.hide();
$ok.show();
});
});
common.getAttribute(['general', 'creation', 'noTemplate'], function (e, val) {
if (val) {
$div.find('#cp-creation-template-true').attr('checked', true);
}
});
return $div;
};
// Drive settings
create['drive-backup'] = function () {
var $div = $('<div>', {'class': 'cp-settings-drive-backup cp-sidebarlayout-element'});
var accountName = privateData.accountName;
var displayName = metadataMgr.getUserData().name || '';
@ -350,8 +586,38 @@ define([
return $div;
};
var createResetDrive = function () {
var $div = $('<div>', {'class': 'cp-settings-reset-drive cp-sidebarlayout-element'});
create['drive-import-local'] = function () {
if (!common.isLoggedIn()) { return; }
var $div = $('<div>', {'class': 'cp-settings-drive-import-local cp-sidebarlayout-element'});
$('<label>', {'for' : 'cp-settings-import-local-pads'})
.text(Messages.settings_import).appendTo($div);
$('<span>', {'class': 'cp-sidebarlayout-description'})
.text(Messages.settings_importTitle).appendTo($div);
var $button = $('<button>', {
'id': 'cp-settings-import-local-pads',
'class': 'btn btn-primary'
}).text(Messages.settings_import).appendTo($div);
var $ok = $('<span>', {'class': 'fa fa-check', title: Messages.saved}).hide().appendTo($div);
var $spinner = $('<span>', {'class': 'fa fa-spinner fa-pulse'}).hide().appendTo($div);
$button.click(function () {
UI.confirm(Messages.settings_importConfirm, function (yes) {
if (!yes) { return; }
$spinner.show();
$ok.hide();
sframeChan.query('Q_SETTINGS_IMPORT_LOCAL', null, function () {
$spinner.hide();
$ok.show();
UI.alert(Messages.settings_importDone);
});
}, undefined, true);
});
return $div;
};
create['drive-reset'] = function () {
var $div = $('<div>', {'class': 'cp-settings-drive-reset cp-sidebarlayout-element'});
$('<label>').text(Messages.settings_resetNewTitle).appendTo($div);
$('<span>', {'class': 'cp-sidebarlayout-description'})
.text(Messages.settings_reset).appendTo($div);
@ -374,124 +640,122 @@ define([
return $div;
};
var createUserFeedbackToggle = function () {
var $div = $('<div>', { 'class': 'cp-settings-userfeedback cp-sidebarlayout-element'});
// Rich text pads settings
$('<span>', {'class': 'label'}).text(Messages.settings_userFeedbackTitle).appendTo($div);
var $label = $('<label>', { 'for': 'cp-settings-userfeedback', 'class': 'noTitle' })
.text(Messages.settings_userFeedback);
create['pad-width'] = function () {
var $div = $('<div>', {
'class': 'cp-settings-pad-width cp-sidebarlayout-element'
});
$('<span>', {'class': 'label'}).text(Messages.settings_padWidth).appendTo($div);
$('<span>', {'class': 'cp-sidebarlayout-description'})
.append(Messages.settings_userFeedbackHint1)
.append(Messages.settings_userFeedbackHint2).appendTo($div);
.text(Messages.settings_padWidthHint).appendTo($div);
var $ok = $('<span>', {'class': 'fa fa-check', title: Messages.saved});
var $spinner = $('<span>', {'class': 'fa fa-spinner fa-pulse'});
var $checkbox = $('<input>', {
'type': 'checkbox',
id: 'cp-settings-userfeedback'
var $label = $('<label>', { 'for': 'cp-settings-padwidth', 'class': 'noTitle' })
.text(Messages.settings_padWidthLabel);
var $input = $('<input>', {
type: 'checkbox',
id: 'cp-settings-padwidth'
}).on('change', function () {
$spinner.show();
$ok.hide();
var val = $checkbox.is(':checked') || false;
common.setAttribute(['general', 'allowUserFeedback'], val, function () {
var val = $input.is(':checked');
common.setAttribute(['pad', 'width'], val, function () {
$spinner.hide();
$ok.show();
});
});
$checkbox.appendTo($div);
}).appendTo($div);
$label.appendTo($div);
$ok.hide().appendTo($div);
$spinner.hide().appendTo($div);
if (privateData.feedbackAllowed) {
$checkbox[0].checked = true;
}
common.getAttribute(['pad', 'width'], function (e, val) {
if (e) { return void console.error(e); }
if (val) {
$input.attr('checked', 'checked');
}
});
return $div;
};
var createUsageButton = function () {
common.createUsageBar(function (err, $bar) {
if (err) { return void console.error(err); }
APP.$usage.html('').append($bar);
}, true);
};
// Code settings
var createLogoutEverywhere = function () {
var $div = $('<div>', { 'class': 'cp-settings-logout-everywhere cp-sidebarlayout-element'});
$('<label>', { 'for': 'cp-settings-logout-everywhere'})
.text(Messages.settings_logoutEverywhereTitle).appendTo($div);
$('<span>', {'class': 'cp-sidebarlayout-description'})
.text(Messages.settings_logoutEverywhere).appendTo($div);
var $button = $('<button>', {
id: 'cp-settings-logout-everywhere',
'class': 'btn btn-primary'
}).text(Messages.settings_logoutEverywhereButton)
.appendTo($div);
var $ok = $('<span>', {'class': 'fa fa-check', title: Messages.saved}).hide().appendTo($div);
var $spinner = $('<span>', {'class': 'fa fa-spinner fa-pulse'}).hide().appendTo($div);
create['code-indent-unit'] = function () {
var $div = $('<div>', {
'class': 'cp-settings-code-indent-unit cp-sidebarlayout-element'
});
$('<label>').text(Messages.settings_codeIndentation).appendTo($div);
$button.click(function () {
var $inputBlock = $('<div>', {
'class': 'cp-sidebarlayout-input-block',
}).appendTo($div);
UI.confirm(Messages.settings_logoutEverywhereConfirm, function (yes) {
if (!yes) { return; }
$spinner.show();
$ok.hide();
var $input = $('<input>', {
'min': 1,
'max': 8,
type: 'number',
}).on('change', function () {
var val = parseInt($input.val());
if (typeof(val) !== 'number') { return; }
common.setAttribute(['codemirror', 'indentUnit'], val);
}).appendTo($inputBlock);
sframeChan.query('Q_SETTINGS_LOGOUT', null, function () {
$spinner.hide();
$ok.show();
window.setTimeout(function () {
$ok.fadeOut(1500);
}, 2500);
});
});
common.getAttribute(['codemirror', 'indentUnit'], function (e, val) {
if (e) { return void console.error(e); }
if (typeof(val) !== 'number') {
$input.val(2);
} else {
$input.val(val);
}
});
return $div;
};
var createImportLocalPads = function () {
if (!common.isLoggedIn()) { return; }
var $div = $('<div>', {'class': 'cp-settings-import-local-pads cp-sidebarlayout-element'});
$('<label>', {'for' : 'cp-settings-import-local-pads'})
.text(Messages.settings_import).appendTo($div);
$('<span>', {'class': 'cp-sidebarlayout-description'})
.text(Messages.settings_importTitle).appendTo($div);
var $button = $('<button>', {
'id': 'cp-settings-import-local-pads',
'class': 'btn btn-primary'
}).text(Messages.settings_import).appendTo($div);
var $ok = $('<span>', {'class': 'fa fa-check', title: Messages.saved}).hide().appendTo($div);
var $spinner = $('<span>', {'class': 'fa fa-spinner fa-pulse'}).hide().appendTo($div);
create['code-indent-type'] = function () {
var key = 'indentWithTabs';
$button.click(function () {
UI.confirm(Messages.settings_importConfirm, function (yes) {
if (!yes) { return; }
$spinner.show();
$ok.hide();
sframeChan.query('Q_SETTINGS_IMPORT_LOCAL', null, function () {
$spinner.hide();
$ok.show();
UI.alert(Messages.settings_importDone);
});
}, undefined, true);
var $div = $('<div>', {
'class': 'cp-settings-code-indent-type cp-sidebarlayout-element'
});
$('<label>').text(Messages.settings_codeUseTabs).appendTo($div);
return $div;
};
var $inputBlock = $('<div>', {
'class': 'cp-sidebarlayout-input-block',
}).css('flex-flow', 'column')
.appendTo($div);
var createLanguageSelector = function () {
var $div = $('<div>', {'class': 'cp-settings-language-selector cp-sidebarlayout-element'});
$('<label>').text(Messages.language).appendTo($div);
var $b = common.createLanguageSelector($div);
$b.find('button').addClass('btn btn-secondary');
var $input = $('<input>', {
type: 'checkbox',
}).on('change', function () {
var val = $input.is(':checked');
if (typeof(val) !== 'boolean') { return; }
common.setAttribute(['codemirror', key], val);
}).appendTo($inputBlock);
/*proxy.on('change', ['settings', 'codemirror', key], function (o, n) {
$input[0].checked = !!n;
});*/
common.getAttribute(['codemirror', key], function (e, val) {
if (e) { return void console.error(e); }
$input[0].checked = !!val;
});
return $div;
};
// Settings app
var createUsageButton = function () {
common.createUsageBar(function (err, $bar) {
if (err) { return void console.error(err); }
APP.$usage.html('').append($bar);
}, true);
};
var hideCategories = function () {
APP.$rightside.find('> div').hide();
@ -514,6 +778,7 @@ define([
if (key === 'drive') { $category.append($('<span>', {'class': 'fa fa-hdd-o'})); }
if (key === 'code') { $category.append($('<span>', {'class': 'fa fa-file-code-o' })); }
if (key === 'pad') { $category.append($('<span>', {'class': 'fa fa-file-word-o' })); }
if (key === 'creation') { $category.append($('<span>', {'class': 'fa fa-plus-circle' })); }
if (key === active) {
$category.addClass('cp-leftside-active');
@ -561,22 +826,10 @@ define([
// Content
var $rightside = APP.$rightside;
$rightside.append(createInfoBlock());
$rightside.append(createDisplayNameInput());
$rightside.append(createLanguageSelector());
$rightside.append(createIndentUnitSelector());
$rightside.append(createIndentTypeSelector());
if (common.isLoggedIn()) {
$rightside.append(createLogoutEverywhere());
for (var f in create) {
if (typeof create[f] !== "function") { continue; }
$rightside.append(create[f]());
}
$rightside.append(createResetTips());
$rightside.append(createThumbnails());
$rightside.append(createBackupDrive());
$rightside.append(createImportLocalPads());
$rightside.append(createResetDrive());
$rightside.append(createUserFeedbackToggle());
$rightside.append(createPadWidthSelector());
// TODO RPC
//obj.proxy.on('change', [], refresh);

@ -79,6 +79,28 @@ h6 { font-size: 24px; }
}
}
}
.cp-app-slide-shown {
.cp-app-slide-container {
position: relative;
&> media-tag {
position: absolute;
top:0; right: 0; bottom: 0; left: 0;
z-index: -1;
&> img {
width: 100vw;
height: 56.25vw; // height:width ratio = 9/16 = .5625
max-height: 100vh;
max-width: 177.78vh; // 16/9 = 1.778
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
margin: auto;
}
}
}
}
.cp-app-slide-preview {
.cp-app-slide-viewer {
width: 50vw;
@ -91,6 +113,26 @@ h6 { font-size: 24px; }
display: block;
height: 100%;
#cp-app-slide-modal-content {
.cp-app-slide-container {
position: relative;
&> media-tag {
position: absolute;
top:0; right: 0; bottom: 0; left: 0;
z-index: -1;
&> img {
width: 50vw;
height: 28.125vw;
max-height: ~"calc(100vh - 96px)";
max-width: ~"calc(16 / 9 * (100vh - 96px))";
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
margin: auto;
}
}
}
.cp-app-slide-container {
width: 100%;
}
@ -115,6 +157,11 @@ h6 { font-size: 24px; }
/* Slide position (print mode) */
@ratio:0.9;
@media print {
#cp-app-slide-editor-container {
display: none;
}
}
#cp-app-slide-print {
position: relative;
display: none;
@ -139,6 +186,24 @@ h6 { font-size: 24px; }
}
}
.cp-app-slide-container {
position: relative;
&> media-tag {
position: absolute;
top:0; right: 0; bottom: 0; left: 0;
z-index: -1;
&> img {
width: 90vw;
height: 50.625vw;
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
margin: auto;
}
}
}
.cp-app-slide-container {
width: 90vw;
height: 100vh;

@ -113,11 +113,16 @@ define([
$toolbarDrawer.append($printButton);
};
// Flag to check if a file from the filepicker is a mediatag for the slides or a background image
var Background = {
isBackground: false
};
var mkSlideOptionsButton = function (framework, slideOptions, $toolbarDrawer) {
var metadataMgr = framework._.cpNfInner.metadataMgr;
var updateSlideOptions = function (newOpt) {
if (JSONSortify(newOpt) !== JSONSortify(slideOptions)) {
$.extend(slideOptions, newOpt);
$.extend(true, slideOptions, newOpt);
// TODO: manage realtime + cursor in the "options" modal ??
Slide.updateOptions();
}
@ -129,17 +134,19 @@ define([
metadataMgr.updateMetadata(metadata);
framework.localChange();
};
var common = framework._.sfCommon;
var createPrintDialog = function (invalidStyle) {
var slideOptionsTmp = {
title: false,
slide: false,
date: false,
background: false,
transition: true,
style: '',
styleLess: ''
};
$.extend(slideOptionsTmp, slideOptions);
$.extend(true, slideOptionsTmp, slideOptions);
var $container = $('<div class="alertify">');
var $container2 = $('<div class="dialog">').appendTo($container);
var $div = $('<div id="printOptions">').appendTo($container2);
@ -194,6 +201,55 @@ define([
$('<label>', {'for': 'cp-app-slide-options-transition'}).text(Messages.printTransition)
.appendTo($p);
$p.append($('<br>'));
$p.append($('<br>'));
// Background image
$('<label>', {'for': 'cp-app-slide-options-bg'}).text(Messages.printBackground)
.appendTo($p);
if (common.isLoggedIn()) {
$p.append($('<br>'));
$('<button>', {
title: Messages.filePickerButton,
'class': '',
style: 'font-size: 17px',
id: 'cp-app-slide-options-bg'
}).click(function () {
Background.isBackground = true;
var pickerCfg = {
types: ['file'],
where: ['root'],
filter: {
fileType: ['image/']
}
};
common.openFilePicker(pickerCfg);
}).text(Messages.printBackgroundButton).appendTo($p);
}
$p.append($('<br>'));
var $bgValue = $('<div>').appendTo($p);
var refreshValue = function () {
$bgValue.html('');
if (slideOptionsTmp.background && slideOptionsTmp.background.name) {
$bgValue.append(Messages._getKey("printBackgroundValue", [slideOptionsTmp.background.name]));
$('<button>', {
'class': 'fa fa-times',
title: Messages.printBackgroundRemove
}).click(function () {
slideOptionsTmp.background = false;
refreshValue();
}).appendTo($bgValue);
} else {
$bgValue.append(Messages.printBackgroundNoValue);
}
};
refreshValue();
if (common.isLoggedIn()) {
Background.todo = function (newData) {
slideOptionsTmp.background = newData;
refreshValue();
};
}
$p.append($('<br>'));
$p.append($('<br>'));
// CSS
$('<label>', {'for': 'cp-app-slide-options-css'}).text(Messages.printCSS).appendTo($p);
$p.append($('<br>'));
@ -348,7 +404,15 @@ define([
};
var mkFilePicker = function (framework, editor) {
framework.setMediaTagEmbedder(function (mt) {
framework.setMediaTagEmbedder(function (mt, data) {
if (Background.isBackground) {
if (data.type === 'file') {
data.mt = mt[0].outerHTML;
Background.todo(data);
}
Background.isBackground = false;
return;
}
editor.replaceSelection($(mt)[0].outerHTML);
});
};

@ -75,7 +75,11 @@ define([
if (typeof(Slide.content) !== 'string') { return; }
var c = Slide.content;
var m = '<span class="cp-app-slide-container"><span class="'+slideClass+'">'+DiffMd.render(c).replace(separatorReg, '</span></span><span class="cp-app-slide-container"><span class="'+slideClass+'">')+'</span></span>';
var mediatagBg = '';
if (options.background && options.background.mt) {
mediatagBg = options.background.mt;
}
var m = '<span class="cp-app-slide-container">' + mediatagBg + '<span class="'+slideClass+'">'+DiffMd.render(c).replace(separatorReg, '</span></span><span class="cp-app-slide-container">' + mediatagBg + '<span class="'+slideClass+'">')+'</span></span>';
try { DiffMd.apply(m, $content); } catch (e) { return console.error(e); }

@ -1,378 +0,0 @@
html,
body {
margin: 0;
padding: 0;
}
button {
margin: 0;
padding: 0;
border: 0;
background: none;
font-size: 100%;
vertical-align: baseline;
font-family: inherit;
font-weight: inherit;
color: inherit;
-webkit-appearance: none;
appearance: none;
-webkit-font-smoothing: antialiased;
-moz-font-smoothing: antialiased;
font-smoothing: antialiased;
}
body {
font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif;
line-height: 1.4em;
background: #f5f5f5;
color: #4d4d4d;
min-width: 230px;
max-width: 550px;
margin: 0 auto;
-webkit-font-smoothing: antialiased;
-moz-font-smoothing: antialiased;
font-smoothing: antialiased;
font-weight: 300;
}
button,
input[type="checkbox"] {
outline: none;
}
.hidden {
display: none;
}
.todoapp {
background: #fff;
margin: 130px 0 40px 0;
position: relative;
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2),
0 25px 50px 0 rgba(0, 0, 0, 0.1);
}
.todoapp input::-webkit-input-placeholder {
font-style: italic;
font-weight: 300;
color: #e6e6e6;
}
.todoapp input::-moz-placeholder {
font-style: italic;
font-weight: 300;
color: #e6e6e6;
}
.todoapp input::input-placeholder {
font-style: italic;
font-weight: 300;
color: #e6e6e6;
}
.todoapp h1 {
position: absolute;
top: -155px;
width: 100%;
font-size: 100px;
font-weight: 100;
text-align: center;
color: rgba(175, 47, 47, 0.15);
-webkit-text-rendering: optimizeLegibility;
-moz-text-rendering: optimizeLegibility;
text-rendering: optimizeLegibility;
}
.new-todo,
.edit {
position: relative;
margin: 0;
width: 100%;
font-size: 24px;
font-family: inherit;
font-weight: inherit;
line-height: 1.4em;
border: 0;
outline: none;
color: inherit;
padding: 6px;
border: 1px solid #999;
box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2);
box-sizing: border-box;
-webkit-font-smoothing: antialiased;
-moz-font-smoothing: antialiased;
font-smoothing: antialiased;
}
.new-todo {
padding: 16px 16px 16px 60px;
border: none;
background: rgba(0, 0, 0, 0.003);
box-shadow: inset 0 -2px 1px rgba(0,0,0,0.03);
}
.main {
position: relative;
z-index: 2;
border-top: 1px solid #e6e6e6;
}
label[for='toggle-all'] {
display: none;
}
.toggle-all {
position: absolute;
top: -55px;
left: -12px;
width: 60px;
height: 34px;
text-align: center;
border: none; /* Mobile Safari */
}
.toggle-all:before {
content: '';
font-size: 22px;
color: #e6e6e6;
padding: 10px 27px 10px 27px;
}
.toggle-all:checked:before {
color: #737373;
}
.todo-list {
margin: 0;
padding: 0;
list-style: none;
}
.todo-list li {
position: relative;
font-size: 24px;
border-bottom: 1px solid #ededed;
}
.todo-list li:last-child {
border-bottom: none;
}
.todo-list li.editing {
border-bottom: none;
padding: 0;
}
.todo-list li.editing .edit {
display: block;
width: 506px;
padding: 13px 17px 12px 17px;
margin: 0 0 0 43px;
}
.todo-list li.editing .view {
display: none;
}
.todo-list li .toggle {
text-align: center;
width: 40px;
/* auto, since non-WebKit browsers doesn't support input styling */
height: auto;
position: absolute;
top: 0;
bottom: 0;
margin: auto 0;
border: none; /* Mobile Safari */
-webkit-appearance: none;
appearance: none;
}
.todo-list li .toggle:after {
content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="-10 -18 100 135"><circle cx="50" cy="50" r="50" fill="none" stroke="#ededed" stroke-width="3"/></svg>');
}
.todo-list li .toggle:checked:after {
content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="-10 -18 100 135"><circle cx="50" cy="50" r="50" fill="none" stroke="#bddad5" stroke-width="3"/><path fill="#5dc2af" d="M72 25L42 71 27 56l-4 4 20 20 34-52z"/></svg>');
}
.todo-list li label {
white-space: pre-line;
word-break: break-all;
padding: 15px 60px 15px 15px;
margin-left: 45px;
display: block;
line-height: 1.2;
transition: color 0.4s;
}
.todo-list li.completed label {
color: #d9d9d9;
text-decoration: line-through;
}
.todo-list li .destroy {
display: none;
position: absolute;
top: 0;
right: 10px;
bottom: 0;
width: 40px;
height: 40px;
margin: auto 0;
font-size: 30px;
color: #cc9a9a;
margin-bottom: 11px;
transition: color 0.2s ease-out;
}
.todo-list li .destroy:hover {
color: #af5b5e;
}
.todo-list li .destroy:after {
content: '×';
}
.todo-list li:hover .destroy {
display: block;
}
.todo-list li .edit {
display: none;
}
.todo-list li.editing:last-child {
margin-bottom: -1px;
}
.footer {
color: #777;
padding: 10px 15px;
height: 20px;
text-align: center;
border-top: 1px solid #e6e6e6;
}
.footer:before {
content: '';
position: absolute;
right: 0;
bottom: 0;
left: 0;
height: 50px;
overflow: hidden;
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2),
0 8px 0 -3px #f6f6f6,
0 9px 1px -3px rgba(0, 0, 0, 0.2),
0 16px 0 -6px #f6f6f6,
0 17px 2px -6px rgba(0, 0, 0, 0.2);
}
.todo-count {
float: left;
text-align: left;
}
.todo-count strong {
font-weight: 300;
}
.filters {
margin: 0;
padding: 0;
list-style: none;
position: absolute;
right: 0;
left: 0;
}
.filters li {
display: inline;
}
.filters li a {
color: inherit;
margin: 3px;
padding: 3px 7px;
text-decoration: none;
border: 1px solid transparent;
border-radius: 3px;
}
.filters li a.selected,
.filters li a:hover {
border-color: rgba(175, 47, 47, 0.1);
}
.filters li a.selected {
border-color: rgba(175, 47, 47, 0.2);
}
.clear-completed,
html .clear-completed:active {
float: right;
position: relative;
line-height: 20px;
text-decoration: none;
cursor: pointer;
position: relative;
}
.clear-completed:hover {
text-decoration: underline;
}
.info {
margin: 65px auto 0;
color: #bfbfbf;
font-size: 10px;
text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5);
text-align: center;
}
.info p {
line-height: 1;
}
.info a {
color: inherit;
text-decoration: none;
font-weight: 400;
}
.info a:hover {
text-decoration: underline;
}
/*
Hack to remove background from Mobile Safari.
Can't use it globally since it destroys checkboxes in Firefox
*/
@media screen and (-webkit-min-device-pixel-ratio:0) {
.toggle-all,
.todo-list li .toggle {
background: none;
}
.todo-list li .toggle {
height: 40px;
}
.toggle-all {
-webkit-transform: rotate(90deg);
transform: rotate(90deg);
-webkit-appearance: none;
appearance: none;
}
}
@media (max-width: 430px) {
.footer {
height: 50px;
}
.filters {
bottom: 10px;
}
}

@ -1,141 +0,0 @@
hr {
margin: 20px 0;
border: 0;
border-top: 1px dashed #c5c5c5;
border-bottom: 1px dashed #f7f7f7;
}
.learn a {
font-weight: normal;
text-decoration: none;
color: #b83f45;
}
.learn a:hover {
text-decoration: underline;
color: #787e7e;
}
.learn h3,
.learn h4,
.learn h5 {
margin: 10px 0;
font-weight: 500;
line-height: 1.2;
color: #000;
}
.learn h3 {
font-size: 24px;
}
.learn h4 {
font-size: 18px;
}
.learn h5 {
margin-bottom: 0;
font-size: 14px;
}
.learn ul {
padding: 0;
margin: 0 0 30px 25px;
}
.learn li {
line-height: 20px;
}
.learn p {
font-size: 15px;
font-weight: 300;
line-height: 1.3;
margin-top: 0;
margin-bottom: 0;
}
#issue-count {
display: none;
}
.quote {
border: none;
margin: 20px 0 60px 0;
}
.quote p {
font-style: italic;
}
.quote p:before {
content: '“';
font-size: 50px;
opacity: .15;
position: absolute;
top: -20px;
left: 3px;
}
.quote p:after {
content: '”';
font-size: 50px;
opacity: .15;
position: absolute;
bottom: -42px;
right: 3px;
}
.quote footer {
position: absolute;
bottom: -40px;
right: 0;
}
.quote footer img {
border-radius: 3px;
}
.quote footer a {
margin-left: 5px;
vertical-align: middle;
}
.speech-bubble {
position: relative;
padding: 10px;
background: rgba(0, 0, 0, .04);
border-radius: 5px;
}
.speech-bubble:after {
content: '';
position: absolute;
top: 100%;
right: 30px;
border: 13px solid transparent;
border-top-color: rgba(0, 0, 0, .04);
}
.learn-bar > .learn {
position: absolute;
width: 272px;
top: 8px;
left: -300px;
padding: 10px;
border-radius: 5px;
background-color: rgba(255, 255, 255, .6);
transition-property: left;
transition-duration: 500ms;
}
@media (min-width: 899px) {
.learn-bar {
width: auto;
padding-left: 300px;
}
.learn-bar > .learn {
left: 8px;
}
}

@ -1,249 +0,0 @@
/* global _ */
(function () {
'use strict';
/* jshint ignore:start */
// Underscore's Template Module
// Courtesy of underscorejs.org
var _ = (function (_) {
_.defaults = function (object) {
if (!object) {
return object;
}
for (var argsIndex = 1, argsLength = arguments.length; argsIndex < argsLength; argsIndex++) {
var iterable = arguments[argsIndex];
if (iterable) {
for (var key in iterable) {
if (object[key] == null) {
object[key] = iterable[key];
}
}
}
}
return object;
}
// By default, Underscore uses ERB-style template delimiters, change the
// following template settings to use alternative delimiters.
_.templateSettings = {
evaluate : /<%([\s\S]+?)%>/g,
interpolate : /<%=([\s\S]+?)%>/g,
escape : /<%-([\s\S]+?)%>/g
};
// When customizing `templateSettings`, if you don't want to define an
// interpolation, evaluation or escaping regex, we need one that is
// guaranteed not to match.
var noMatch = /(.)^/;
// Certain characters need to be escaped so that they can be put into a
// string literal.
var escapes = {
"'": "'",
'\\': '\\',
'\r': 'r',
'\n': 'n',
'\t': 't',
'\u2028': 'u2028',
'\u2029': 'u2029'
};
var escaper = /\\|'|\r|\n|\t|\u2028|\u2029/g;
// JavaScript micro-templating, similar to John Resig's implementation.
// Underscore templating handles arbitrary delimiters, preserves whitespace,
// and correctly escapes quotes within interpolated code.
_.template = function(text, data, settings) {
var render;
settings = _.defaults({}, settings, _.templateSettings);
// Combine delimiters into one regular expression via alternation.
var matcher = new RegExp([
(settings.escape || noMatch).source,
(settings.interpolate || noMatch).source,
(settings.evaluate || noMatch).source
].join('|') + '|$', 'g');
// Compile the template source, escaping string literals appropriately.
var index = 0;
var source = "__p+='";
text.replace(matcher, function(match, escape, interpolate, evaluate, offset) {
source += text.slice(index, offset)
.replace(escaper, function(match) { return '\\' + escapes[match]; });
if (escape) {
source += "'+\n((__t=(" + escape + "))==null?'':_.escape(__t))+\n'";
}
if (interpolate) {
source += "'+\n((__t=(" + interpolate + "))==null?'':__t)+\n'";
}
if (evaluate) {
source += "';\n" + evaluate + "\n__p+='";
}
index = offset + match.length;
return match;
});
source += "';\n";
// If a variable is not specified, place data values in local scope.
if (!settings.variable) source = 'with(obj||{}){\n' + source + '}\n';
source = "var __t,__p='',__j=Array.prototype.join," +
"print=function(){__p+=__j.call(arguments,'');};\n" +
source + "return __p;\n";
try {
render = new Function(settings.variable || 'obj', '_', source);
} catch (e) {
e.source = source;
throw e;
}
if (data) return render(data, _);
var template = function(data) {
return render.call(this, data, _);
};
// Provide the compiled function source as a convenience for precompilation.
template.source = 'function(' + (settings.variable || 'obj') + '){\n' + source + '}';
return template;
};
return _;
})({});
if (location.hostname === 'todomvc.com') {
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','https://www.google-analytics.com/analytics.js','ga');
ga('create', 'UA-31081062-1', 'auto');
ga('send', 'pageview');
}
/* jshint ignore:end */
function redirect() {
if (location.hostname === 'tastejs.github.io') {
location.href = location.href.replace('tastejs.github.io/todomvc', 'todomvc.com');
}
}
function findRoot() {
var base = location.href.indexOf('examples/');
return location.href.substr(0, base);
}
function getFile(file, callback) {
if (!location.host) {
return console.info('Miss the info bar? Run TodoMVC from a server to avoid a cross-origin error.');
}
var xhr = new XMLHttpRequest();
xhr.open('GET', findRoot() + file, true);
xhr.send();
xhr.onload = function () {
if (xhr.status === 200 && callback) {
callback(xhr.responseText);
}
};
}
function Learn(learnJSON, config) {
if (!(this instanceof Learn)) {
return new Learn(learnJSON, config);
}
var template, framework;
if (typeof learnJSON !== 'object') {
try {
learnJSON = JSON.parse(learnJSON);
} catch (e) {
return;
}
}
if (config) {
template = config.template;
framework = config.framework;
}
if (!template && learnJSON.templates) {
template = learnJSON.templates.todomvc;
}
if (!framework && document.querySelector('[data-framework]')) {
framework = document.querySelector('[data-framework]').dataset.framework;
}
this.template = template;
if (learnJSON.backend) {
this.frameworkJSON = learnJSON.backend;
this.frameworkJSON.issueLabel = framework;
this.append({
backend: true
});
} else if (learnJSON[framework]) {
this.frameworkJSON = learnJSON[framework];
this.frameworkJSON.issueLabel = framework;
this.append();
}
this.fetchIssueCount();
}
Learn.prototype.append = function (opts) {
var aside = document.createElement('aside');
aside.innerHTML = _.template(this.template, this.frameworkJSON);
aside.className = 'learn';
if (opts && opts.backend) {
// Remove demo link
var sourceLinks = aside.querySelector('.source-links');
var heading = sourceLinks.firstElementChild;
var sourceLink = sourceLinks.lastElementChild;
// Correct link path
var href = sourceLink.getAttribute('href');
sourceLink.setAttribute('href', href.substr(href.lastIndexOf('http')));
sourceLinks.innerHTML = heading.outerHTML + sourceLink.outerHTML;
} else {
// Localize demo links
var demoLinks = aside.querySelectorAll('.demo-link');
Array.prototype.forEach.call(demoLinks, function (demoLink) {
if (demoLink.getAttribute('href').substr(0, 4) !== 'http') {
demoLink.setAttribute('href', findRoot() + demoLink.getAttribute('href'));
}
});
}
document.body.className = (document.body.className + ' learn-bar').trim();
document.body.insertAdjacentHTML('afterBegin', aside.outerHTML);
};
Learn.prototype.fetchIssueCount = function () {
var issueLink = document.getElementById('issue-count-link');
if (issueLink) {
var url = issueLink.href.replace('https://github.com', 'https://api.github.com/repos');
var xhr = new XMLHttpRequest();
xhr.open('GET', url, true);
xhr.onload = function (e) {
var parsedResponse = JSON.parse(e.target.responseText);
if (parsedResponse instanceof Array) {
var count = parsedResponse.length;
if (count !== 0) {
issueLink.innerHTML = 'This app has ' + count + ' open issues';
document.getElementById('issue-count').style.display = 'inline';
}
}
};
xhr.send();
}
};
redirect();
getFile('learn.json', Learn);
})();

@ -1,49 +0,0 @@
<!doctype html>
<html lang="en" data-framework="javascript">
<head>
<meta charset="utf-8">
<title>Crypt Todo</title>
<link rel="stylesheet" href="assets/todomvc-common/base.css">
<link rel="stylesheet" href="assets/todomvc-app-css/index.css">
</head>
<body>
<section class="todoapp">
<header class="header">
<h1>todos</h1>
<input class="new-todo" placeholder="What needs to be done?" autofocus>
</header>
<section class="main">
<input class="toggle-all" type="checkbox">
<label for="toggle-all">Mark all as complete</label>
<ul class="todo-list"></ul>
</section>
<footer class="footer">
<span class="todo-count"></span>
<ul class="filters">
<li>
<a href="#/" class="selected">All</a>
</li>
<li>
<a href="#/active">Active</a>
</li>
<li>
<a href="#/completed">Completed</a>
</li>
</ul>
<button class="clear-completed">Clear completed</button>
</footer>
</section>
<footer class="info">
</footer>
<script src="assets/todomvc-common/base.js"></script>
<script src="js/helpers.js"></script>
<script src="js/store.js"></script>
<script src="js/model.js"></script>
<script src="js/template.js"></script>
<script src="js/view.js"></script>
<script src="js/controller.js"></script>
<script src="js/app.js"></script>
</body>
</html>

@ -1,25 +0,0 @@
/*global app, $on */
(function () {
'use strict';
/**
* Sets up a brand new Todo list.
*
* @param {string} name The name of your new to do list.
*/
function Todo(name) {
this.storage = new app.Store(name);
this.model = new app.Model(this.storage);
this.template = new app.Template();
this.view = new app.View(this.template);
this.controller = new app.Controller(this.model, this.view);
}
var todo = new Todo('todos-vanillajs');
function setView() {
todo.controller.setView(document.location.hash);
}
$on(window, 'load', setView);
$on(window, 'hashchange', setView);
})();

@ -1,270 +0,0 @@
(function (window) {
'use strict';
/**
* Takes a model and view and acts as the controller between them
*
* @constructor
* @param {object} model The model instance
* @param {object} view The view instance
*/
function Controller(model, view) {
var self = this;
self.model = model;
self.view = view;
self.view.bind('newTodo', function (title) {
self.addItem(title);
});
self.view.bind('itemEdit', function (item) {
self.editItem(item.id);
});
self.view.bind('itemEditDone', function (item) {
self.editItemSave(item.id, item.title);
});
self.view.bind('itemEditCancel', function (item) {
self.editItemCancel(item.id);
});
self.view.bind('itemRemove', function (item) {
self.removeItem(item.id);
});
self.view.bind('itemToggle', function (item) {
self.toggleComplete(item.id, item.completed);
});
self.view.bind('removeCompleted', function () {
self.removeCompletedItems();
});
self.view.bind('toggleAll', function (status) {
self.toggleAll(status.completed);
});
}
/**
* Loads and initialises the view
*
* @param {string} '' | 'active' | 'completed'
*/
Controller.prototype.setView = function (locationHash) {
var route = locationHash.split('/')[1];
var page = route || '';
this._updateFilterState(page);
};
/**
* An event to fire on load. Will get all items and display them in the
* todo-list
*/
Controller.prototype.showAll = function () {
var self = this;
self.model.read(function (data) {
self.view.render('showEntries', data);
});
};
/**
* Renders all active tasks
*/
Controller.prototype.showActive = function () {
var self = this;
self.model.read({ completed: false }, function (data) {
self.view.render('showEntries', data);
});
};
/**
* Renders all completed tasks
*/
Controller.prototype.showCompleted = function () {
var self = this;
self.model.read({ completed: true }, function (data) {
self.view.render('showEntries', data);
});
};
/**
* An event to fire whenever you want to add an item. Simply pass in the event
* object and it'll handle the DOM insertion and saving of the new item.
*/
Controller.prototype.addItem = function (title) {
var self = this;
if (title.trim() === '') {
return;
}
self.model.create(title, function () {
self.view.render('clearNewTodo');
self._filter(true);
});
};
/*
* Triggers the item editing mode.
*/
Controller.prototype.editItem = function (id) {
var self = this;
self.model.read(id, function (data) {
self.view.render('editItem', {id: id, title: data[0].title});
});
};
/*
* Finishes the item editing mode successfully.
*/
Controller.prototype.editItemSave = function (id, title) {
var self = this;
title = title.trim();
if (title.length !== 0) {
self.model.update(id, {title: title}, function () {
self.view.render('editItemDone', {id: id, title: title});
});
} else {
self.removeItem(id);
}
};
/*
* Cancels the item editing mode.
*/
Controller.prototype.editItemCancel = function (id) {
var self = this;
self.model.read(id, function (data) {
self.view.render('editItemDone', {id: id, title: data[0].title});
});
};
/**
* By giving it an ID it'll find the DOM element matching that ID,
* remove it from the DOM and also remove it from storage.
*
* @param {number} id The ID of the item to remove from the DOM and
* storage
*/
Controller.prototype.removeItem = function (id) {
var self = this;
self.model.remove(id, function () {
self.view.render('removeItem', id);
});
self._filter();
};
/**
* Will remove all completed items from the DOM and storage.
*/
Controller.prototype.removeCompletedItems = function () {
var self = this;
self.model.read({ completed: true }, function (data) {
data.forEach(function (item) {
self.removeItem(item.id);
});
});
self._filter();
};
/**
* Give it an ID of a model and a checkbox and it will update the item
* in storage based on the checkbox's state.
*
* @param {number} id The ID of the element to complete or uncomplete
* @param {object} checkbox The checkbox to check the state of complete
* or not
* @param {boolean|undefined} silent Prevent re-filtering the todo items
*/
Controller.prototype.toggleComplete = function (id, completed, silent) {
var self = this;
self.model.update(id, { completed: completed }, function () {
self.view.render('elementComplete', {
id: id,
completed: completed
});
});
if (!silent) {
self._filter();
}
};
/**
* Will toggle ALL checkboxes' on/off state and completeness of models.
* Just pass in the event object.
*/
Controller.prototype.toggleAll = function (completed) {
var self = this;
self.model.read({ completed: !completed }, function (data) {
data.forEach(function (item) {
self.toggleComplete(item.id, completed, true);
});
});
self._filter();
};
/**
* Updates the pieces of the page which change depending on the remaining
* number of todos.
*/
Controller.prototype._updateCount = function () {
var self = this;
self.model.getCount(function (todos) {
self.view.render('updateElementCount', todos.active);
self.view.render('clearCompletedButton', {
completed: todos.completed,
visible: todos.completed > 0
});
self.view.render('toggleAll', {checked: todos.completed === todos.total});
self.view.render('contentBlockVisibility', {visible: todos.total > 0});
});
};
/**
* Re-filters the todo items, based on the active route.
* @param {boolean|undefined} force forces a re-painting of todo items.
*/
Controller.prototype._filter = function (force) {
var activeRoute = this._activeRoute.charAt(0).toUpperCase() + this._activeRoute.substr(1);
// Update the elements on the page, which change with each completed todo
this._updateCount();
// If the last active route isn't "All", or we're switching routes, we
// re-create the todo item elements, calling:
// this.show[All|Active|Completed]();
if (force || this._lastActiveRoute !== 'All' || this._lastActiveRoute !== activeRoute) {
this['show' + activeRoute]();
}
this._lastActiveRoute = activeRoute;
};
/**
* Simply updates the filter nav's selected states
*/
Controller.prototype._updateFilterState = function (currentPage) {
// Store a reference to the active route, allowing us to re-filter todo
// items as they are marked complete or incomplete.
this._activeRoute = currentPage;
if (currentPage === '') {
this._activeRoute = 'All';
}
this._filter();
this.view.render('setFilter', currentPage);
};
// Export to window
window.app = window.app || {};
window.app.Controller = Controller;
})(window);

@ -1,52 +0,0 @@
/*global NodeList */
(function (window) {
'use strict';
// Get element(s) by CSS selector:
window.qs = function (selector, scope) {
return (scope || document).querySelector(selector);
};
window.qsa = function (selector, scope) {
return (scope || document).querySelectorAll(selector);
};
// addEventListener wrapper:
window.$on = function (target, type, callback, useCapture) {
target.addEventListener(type, callback, !!useCapture);
};
// Attach a handler to event for all elements that match the selector,
// now or in the future, based on a root element
window.$delegate = function (target, selector, type, handler) {
function dispatchEvent(event) {
var targetElement = event.target;
var potentialElements = window.qsa(selector, target);
var hasMatch = Array.prototype.indexOf.call(potentialElements, targetElement) >= 0;
if (hasMatch) {
handler.call(targetElement, event);
}
}
// https://developer.mozilla.org/en-US/docs/Web/Events/blur
var useCapture = type === 'blur' || type === 'focus';
window.$on(target, type, dispatchEvent, useCapture);
};
// Find the element's parent with the given tag name:
// $parent(qs('a'), 'div');
window.$parent = function (element, tagName) {
if (!element.parentNode) {
return;
}
if (element.parentNode.tagName.toLowerCase() === tagName.toLowerCase()) {
return element.parentNode;
}
return window.$parent(element.parentNode, tagName);
};
// Allow for looping on nodes by chaining:
// qsa('.foo').forEach(function () {})
NodeList.prototype.forEach = Array.prototype.forEach;
})(window);

@ -1,120 +0,0 @@
(function (window) {
'use strict';
/**
* Creates a new Model instance and hooks up the storage.
*
* @constructor
* @param {object} storage A reference to the client side storage class
*/
function Model(storage) {
this.storage = storage;
}
/**
* Creates a new todo model
*
* @param {string} [title] The title of the task
* @param {function} [callback] The callback to fire after the model is created
*/
Model.prototype.create = function (title, callback) {
title = title || '';
callback = callback || function () {};
var newItem = {
title: title.trim(),
completed: false
};
this.storage.save(newItem, callback);
};
/**
* Finds and returns a model in storage. If no query is given it'll simply
* return everything. If you pass in a string or number it'll look that up as
* the ID of the model to find. Lastly, you can pass it an object to match
* against.
*
* @param {string|number|object} [query] A query to match models against
* @param {function} [callback] The callback to fire after the model is found
*
* @example
* model.read(1, func); // Will find the model with an ID of 1
* model.read('1'); // Same as above
* //Below will find a model with foo equalling bar and hello equalling world.
* model.read({ foo: 'bar', hello: 'world' });
*/
Model.prototype.read = function (query, callback) {
var queryType = typeof query;
callback = callback || function () {};
if (queryType === 'function') {
callback = query;
return this.storage.findAll(callback);
} else if (queryType === 'string' || queryType === 'number') {
query = parseInt(query, 10);
this.storage.find({ id: query }, callback);
} else {
this.storage.find(query, callback);
}
};
/**
* Updates a model by giving it an ID, data to update, and a callback to fire when
* the update is complete.
*
* @param {number} id The id of the model to update
* @param {object} data The properties to update and their new value
* @param {function} callback The callback to fire when the update is complete.
*/
Model.prototype.update = function (id, data, callback) {
this.storage.save(data, callback, id);
};
/**
* Removes a model from storage
*
* @param {number} id The ID of the model to remove
* @param {function} callback The callback to fire when the removal is complete.
*/
Model.prototype.remove = function (id, callback) {
this.storage.remove(id, callback);
};
/**
* WARNING: Will remove ALL data from storage.
*
* @param {function} callback The callback to fire when the storage is wiped.
*/
Model.prototype.removeAll = function (callback) {
this.storage.drop(callback);
};
/**
* Returns a count of all todos
*/
Model.prototype.getCount = function (callback) {
var todos = {
active: 0,
completed: 0,
total: 0
};
this.storage.findAll(function (data) {
data.forEach(function (todo) {
if (todo.completed) {
todos.completed++;
} else {
todos.active++;
}
todos.total++;
});
callback(todos);
});
};
// Export to window
window.app = window.app || {};
window.app.Model = Model;
})(window);

@ -1,141 +0,0 @@
/*jshint eqeqeq:false */
(function (window) {
'use strict';
/**
* Creates a new client side storage object and will create an empty
* collection if no collection already exists.
*
* @param {string} name The name of our DB we want to use
* @param {function} callback Our fake DB uses callbacks because in
* real life you probably would be making AJAX calls
*/
function Store(name, callback) {
callback = callback || function () {};
this._dbName = name;
if (!localStorage[name]) {
var data = {
todos: []
};
localStorage[name] = JSON.stringify(data);
}
callback.call(this, JSON.parse(localStorage[name]));
}
/**
* Finds items based on a query given as a JS object
*
* @param {object} query The query to match against (i.e. {foo: 'bar'})
* @param {function} callback The callback to fire when the query has
* completed running
*
* @example
* db.find({foo: 'bar', hello: 'world'}, function (data) {
* // data will return any items that have foo: bar and
* // hello: world in their properties
* });
*/
Store.prototype.find = function (query, callback) {
if (!callback) {
return;
}
var todos = JSON.parse(localStorage[this._dbName]).todos;
callback.call(this, todos.filter(function (todo) {
for (var q in query) {
if (query[q] !== todo[q]) {
return false;
}
}
return true;
}));
};
/**
* Will retrieve all data from the collection
*
* @param {function} callback The callback to fire upon retrieving data
*/
Store.prototype.findAll = function (callback) {
callback = callback || function () {};
callback.call(this, JSON.parse(localStorage[this._dbName]).todos);
};
/**
* Will save the given data to the DB. If no item exists it will create a new
* item, otherwise it'll simply update an existing item's properties
*
* @param {object} updateData The data to save back into the DB
* @param {function} callback The callback to fire after saving
* @param {number} id An optional param to enter an ID of an item to update
*/
Store.prototype.save = function (updateData, callback, id) {
var data = JSON.parse(localStorage[this._dbName]);
var todos = data.todos;
callback = callback || function () {};
// If an ID was actually given, find the item and update each property
if (id) {
for (var i = 0; i < todos.length; i++) {
if (todos[i].id === id) {
for (var key in updateData) {
todos[i][key] = updateData[key];
}
break;
}
}
localStorage[this._dbName] = JSON.stringify(data);
callback.call(this, todos);
} else {
// Generate an ID
updateData.id = new Date().getTime();
todos.push(updateData);
localStorage[this._dbName] = JSON.stringify(data);
callback.call(this, [updateData]);
}
};
/**
* Will remove an item from the Store based on its ID
*
* @param {number} id The ID of the item you want to remove
* @param {function} callback The callback to fire after saving
*/
Store.prototype.remove = function (id, callback) {
var data = JSON.parse(localStorage[this._dbName]);
var todos = data.todos;
for (var i = 0; i < todos.length; i++) {
if (todos[i].id == id) {
todos.splice(i, 1);
break;
}
}
localStorage[this._dbName] = JSON.stringify(data);
callback.call(this, todos);
};
/**
* Will drop all storage and start fresh
*
* @param {function} callback The callback to fire after dropping the data
*/
Store.prototype.drop = function (callback) {
var data = {todos: []};
localStorage[this._dbName] = JSON.stringify(data);
callback.call(this, data.todos);
};
// Export to window
window.app = window.app || {};
window.app.Store = Store;
})(window);

@ -1,114 +0,0 @@
/*jshint laxbreak:true */
(function (window) {
'use strict';
var htmlEscapes = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
'\'': '&#x27;',
'`': '&#x60;'
};
var escapeHtmlChar = function (chr) {
return htmlEscapes[chr];
};
var reUnescapedHtml = /[&<>"'`]/g;
var reHasUnescapedHtml = new RegExp(reUnescapedHtml.source);
var escape = function (string) {
return (string && reHasUnescapedHtml.test(string))
? string.replace(reUnescapedHtml, escapeHtmlChar)
: string;
};
/**
* Sets up defaults for all the Template methods such as a default template
*
* @constructor
*/
function Template() {
this.defaultTemplate
= '<li data-id="{{id}}" class="{{completed}}">'
+ '<div class="view">'
+ '<input class="toggle" type="checkbox" {{checked}}>'
+ '<label>{{title}}</label>'
+ '<button class="destroy"></button>'
+ '</div>'
+ '</li>';
}
/**
* Creates an <li> HTML string and returns it for placement in your app.
*
* NOTE: In real life you should be using a templating engine such as Mustache
* or Handlebars, however, this is a vanilla JS example.
*
* @param {object} data The object containing keys you want to find in the
* template to replace.
* @returns {string} HTML String of an <li> element
*
* @example
* view.show({
* id: 1,
* title: "Hello World",
* completed: 0,
* });
*/
Template.prototype.show = function (data) {
var i = 0, l = data.length;
var view = '';
for (; i < l; i++) {
var template = this.defaultTemplate;
var completed = '';
var checked = '';
if (data[i].completed) {
completed = 'completed';
checked = 'checked';
}
template = template.replace('{{id}}', data[i].id);
template = template.replace('{{title}}', escape(data[i].title));
template = template.replace('{{completed}}', completed);
template = template.replace('{{checked}}', checked);
view = view + template;
}
return view;
};
/**
* Displays a counter of how many to dos are left to complete
*
* @param {number} activeTodos The number of active todos.
* @returns {string} String containing the count
*/
Template.prototype.itemCounter = function (activeTodos) {
var plural = activeTodos === 1 ? '' : 's';
return '<strong>' + activeTodos + '</strong> item' + plural + ' left';
};
/**
* Updates the text within the "Clear completed" button
*
* @param {[type]} completedTodos The number of completed todos.
* @returns {string} String containing the count
*/
Template.prototype.clearCompletedButton = function (completedTodos) {
if (completedTodos > 0) {
return 'Clear completed';
} else {
return '';
}
};
// Export to window
window.app = window.app || {};
window.app.Template = Template;
})(window);

@ -1,219 +0,0 @@
/*global qs, qsa, $on, $parent, $delegate */
(function (window) {
'use strict';
/**
* View that abstracts away the browser's DOM completely.
* It has two simple entry points:
*
* - bind(eventName, handler)
* Takes a todo application event and registers the handler
* - render(command, parameterObject)
* Renders the given command with the options
*/
function View(template) {
this.template = template;
this.ENTER_KEY = 13;
this.ESCAPE_KEY = 27;
this.$todoList = qs('.todo-list');
this.$todoItemCounter = qs('.todo-count');
this.$clearCompleted = qs('.clear-completed');
this.$main = qs('.main');
this.$footer = qs('.footer');
this.$toggleAll = qs('.toggle-all');
this.$newTodo = qs('.new-todo');
}
View.prototype._removeItem = function (id) {
var elem = qs('[data-id="' + id + '"]');
if (elem) {
this.$todoList.removeChild(elem);
}
};
View.prototype._clearCompletedButton = function (completedCount, visible) {
this.$clearCompleted.innerHTML = this.template.clearCompletedButton(completedCount);
this.$clearCompleted.style.display = visible ? 'block' : 'none';
};
View.prototype._setFilter = function (currentPage) {
qs('.filters .selected').className = '';
qs('.filters [href="#/' + currentPage + '"]').className = 'selected';
};
View.prototype._elementComplete = function (id, completed) {
var listItem = qs('[data-id="' + id + '"]');
if (!listItem) {
return;
}
listItem.className = completed ? 'completed' : '';
// In case it was toggled from an event and not by clicking the checkbox
qs('input', listItem).checked = completed;
};
View.prototype._editItem = function (id, title) {
var listItem = qs('[data-id="' + id + '"]');
if (!listItem) {
return;
}
listItem.className = listItem.className + ' editing';
var input = document.createElement('input');
input.className = 'edit';
listItem.appendChild(input);
input.focus();
input.value = title;
};
View.prototype._editItemDone = function (id, title) {
var listItem = qs('[data-id="' + id + '"]');
if (!listItem) {
return;
}
var input = qs('input.edit', listItem);
listItem.removeChild(input);
listItem.className = listItem.className.replace('editing', '');
qsa('label', listItem).forEach(function (label) {
label.textContent = title;
});
};
View.prototype.render = function (viewCmd, parameter) {
var self = this;
var viewCommands = {
showEntries: function () {
self.$todoList.innerHTML = self.template.show(parameter);
},
removeItem: function () {
self._removeItem(parameter);
},
updateElementCount: function () {
self.$todoItemCounter.innerHTML = self.template.itemCounter(parameter);
},
clearCompletedButton: function () {
self._clearCompletedButton(parameter.completed, parameter.visible);
},
contentBlockVisibility: function () {
self.$main.style.display = self.$footer.style.display = parameter.visible ? 'block' : 'none';
},
toggleAll: function () {
self.$toggleAll.checked = parameter.checked;
},
setFilter: function () {
self._setFilter(parameter);
},
clearNewTodo: function () {
self.$newTodo.value = '';
},
elementComplete: function () {
self._elementComplete(parameter.id, parameter.completed);
},
editItem: function () {
self._editItem(parameter.id, parameter.title);
},
editItemDone: function () {
self._editItemDone(parameter.id, parameter.title);
}
};
viewCommands[viewCmd]();
};
View.prototype._itemId = function (element) {
var li = $parent(element, 'li');
return parseInt(li.dataset.id, 10);
};
View.prototype._bindItemEditDone = function (handler) {
var self = this;
$delegate(self.$todoList, 'li .edit', 'blur', function () {
if (!this.dataset.iscanceled) {
handler({
id: self._itemId(this),
title: this.value
});
}
});
$delegate(self.$todoList, 'li .edit', 'keypress', function (event) {
if (event.keyCode === self.ENTER_KEY) {
// Remove the cursor from the input when you hit enter just like if it
// were a real form
this.blur();
}
});
};
View.prototype._bindItemEditCancel = function (handler) {
var self = this;
$delegate(self.$todoList, 'li .edit', 'keyup', function (event) {
if (event.keyCode === self.ESCAPE_KEY) {
this.dataset.iscanceled = true;
this.blur();
handler({id: self._itemId(this)});
}
});
};
View.prototype.bind = function (event, handler) {
var self = this;
if (event === 'newTodo') {
$on(self.$newTodo, 'change', function () {
handler(self.$newTodo.value);
});
} else if (event === 'removeCompleted') {
$on(self.$clearCompleted, 'click', function () {
handler();
});
} else if (event === 'toggleAll') {
$on(self.$toggleAll, 'click', function () {
handler({completed: this.checked});
});
} else if (event === 'itemEdit') {
$delegate(self.$todoList, 'li label', 'dblclick', function () {
handler({id: self._itemId(this)});
});
} else if (event === 'itemRemove') {
$delegate(self.$todoList, '.destroy', 'click', function () {
handler({id: self._itemId(this)});
});
} else if (event === 'itemToggle') {
$delegate(self.$todoList, '.toggle', 'click', function () {
handler({
id: self._itemId(this),
completed: this.checked
});
});
} else if (event === 'itemEditDone') {
self._bindItemEditDone(handler);
} else if (event === 'itemEditCancel') {
self._bindItemEditCancel(handler);
}
};
// Export to window
window.app = window.app || {};
window.app.View = View;
}(window));
Loading…
Cancel
Save