Merge branch 'staging' into soon

pull/1/head
ansuz 7 years ago
commit 12ae657422

@ -43,7 +43,8 @@
"nthen": "^0.1.5",
"open-sans-fontface": "^1.4.2",
"bootstrap-tokenfield": "^0.12.1",
"localforage": "^1.5.2"
"localforage": "^1.5.2",
"html2canvas": "^0.4.1"
},
"resolutions": {
"bootstrap": "v4.0.0-alpha.6"

@ -12,6 +12,7 @@ define(function() {
* You can change their duration here (measured in milliseconds)
*/
config.notificationTimeout = 5000;
config.disableUserlistNotifications = false;
config.enablePinning = true;

@ -76,7 +76,7 @@ define([
])
])
]),
h('div.cp-version-footer', "CryptPad v1.18.0 (Snallygaster)")
h('div.cp-version-footer', "CryptPad v1.19.0 (Tarasque)")
]);
};
@ -728,7 +728,7 @@ define([
Pages['/profile/'] = Pages['/profile/index.html'] = function () {
return [
h('div#toolbar'),
h('div#cp-toolbar'),
h('div#container'),
loadingScreen()
];

@ -102,9 +102,9 @@
@category-bg: #f4f4f4;
@button-bg: #3066e5;
@button-alt-bg: #fff;
@button-red-bg: #e54e4e;
@button-bg: @colortheme_sidebar-button-bg;
@button-alt-bg: @colortheme_sidebar-button-alt-bg;
@button-red-bg: @colortheme_sidebar-button-red-bg;
.unselectable () {
-webkit-touch-callout: none;

@ -41,6 +41,7 @@
@colortheme_pad-bg: #1c4fa0;
@colortheme_pad-color: #fff;
@colortheme_pad-toolbar-bg: #c1e7ff;
@colortheme_slide-bg: #e57614;
@colortheme_slide-color: #fff;
@ -50,6 +51,9 @@
@colortheme_poll-bg: #006304;
@colortheme_poll-color: #fff;
@colortheme_poll-help-bg: #bbffbb;
@colortheme_poll-th-bg: #005bef;
@colortheme_poll-th-fg: #fff;
@colortheme_whiteboard-bg: #800080;
@colortheme_whiteboard-color: #fff;
@ -60,7 +64,7 @@
@colortheme_file-bg: #cd2532;
@colortheme_file-color: #fff;
@colortheme_friends-bg: #607B8D;
@colortheme_friends-bg: #607b8d;
@colortheme_friends-color: #fff;
@colortheme_default-bg: #ddd;
@ -75,7 +79,7 @@
@colortheme_todo-bg: #7bccd1;
@colortheme_todo-color: #000;
// Sidebar layout
// Sidebar layout (profile / settings)
@colortheme_sidebar-active: #fff;
@colortheme_sidebar-left-bg: #eee;
@colortheme_sidebar-left-fg: #000;
@ -83,7 +87,9 @@
@colortheme_sidebar-right-bg: #fff;
@colortheme_sidebar-right-fg: #000;
@colortheme_sidebar-description: #777;
@colortheme_sidebar-button-bg: #3066e5;
@colortheme_sidebar-button-red-bg: #e54e4e;
@colortheme_sidebar-button-alt-bg: #fff;
@cryptpad_color_blue: #4591C4;
@cryptpad_color_grey: #999999;

@ -13,5 +13,18 @@
.cp-icon-color-profile { color: @colortheme_settings-bg; }
.cp-icon-color-default { color: @colortheme_default-bg; }
.cp-icon-color-todo { color:@colortheme_todo-bg; }
.cp-border-color-pad { border-color: @colortheme_pad-bg !important; }
.cp-border-color-code { border-color: @colortheme_code-bg !important; }
.cp-border-color-slide { border-color: @colortheme_slide-bg !important; }
.cp-border-color-poll { border-color: @colortheme_poll-bg !important; }
.cp-border-color-file { border-color: @colortheme_file-bg !important; }
.cp-border-color-contacts { border-color: @colortheme_friends-bg !important; }
.cp-border-color-whiteboard { border-color: @colortheme_whiteboard-bg !important; }
.cp-border-color-drive { border-color: @colortheme_drive-bg !important; }
.cp-border-color-settings { border-color: @colortheme_settings-bg !important; }
.cp-border-color-profile { border-color: @colortheme_settings-bg !important; }
.cp-border-color-default { border-color: @colortheme_default-bg !important; }
.cp-border-color-todo { border-color:@colortheme_todo-bg !important; }
}

@ -0,0 +1,99 @@
@import (once) "/customize/src/less2/include/colortheme.less";
@import (once) "/customize/src/less2/include/leftside-menu.less";
@leftside-bg: @colortheme_sidebar-left-bg;
@leftside-color: @colortheme_sidebar-left-fg;
@rightside-color: @colortheme_sidebar-right-fg;
@description-color: @colortheme_sidebar-description;
@button-width: 400px;
.sidebar-layout_main() {
input[type="text"] {
padding-left: 10px;
}
#cp-sidebarlayout-container {
font-size: 16px;
display: flex;
flex: 1;
min-height: 0;
#cp-sidebarlayout-leftside {
color: @leftside-color;
width: 250px;
background: @leftside-bg;
display: flex;
flex-flow: column;
.cp-sidebarlayout-categories {
flex: 1;
.cp-sidebarlayout-category {
.leftside-menu-category_main();
}
}
}
#cp-sidebarlayout-rightside {
flex: 1;
padding: 5px 20px;
color: @rightside-color;
overflow: auto;
// Following rules are only in settings
.element {
label:not(.noTitle), .label {
display: block;
font-weight: bold;
margin-bottom: 0;
}
.description {
display: block;
color: @description-color;
margin-bottom: 5px;
}
margin-bottom: 20px;
}
[type="text"], button {
vertical-align: middle;
height: 40px;
box-sizing: border-box;
}
.inputBlock {
display: inline-flex;
width: @button-width;
input {
flex: 1;
border-radius: 0.25em 0 0 0.25em;
border: 1px solid #adadad;
border-right: 0px;
}
button {
border-radius: 0 0.25em 0.25em 0;
//border: 1px solid #adadad;
border-left: 0px;
}
}
&>div {
margin: 10px 0;
}
button.btn {
@button-bg: @colortheme_sidebar-button-bg;
@button-red-bg: @colortheme_sidebar-button-red-bg;
background-color: @button-bg;
border-color: darken(@button-bg, 10%);
color: white;
&:hover {
background-color: darken(@button-bg, 10%);
}
&.btn-danger {
background-color: @button-red-bg;
border-color: darken(@button-red-bg, 10%);
color: white;
&:hover {
background-color: darken(@button-red-bg, 10%);
}
}
}
}
}
}

@ -33,4 +33,6 @@ body.cp-app-filepicker { @import "../../../filepicker/app-filepicker.less"; }
body.cp-app-contacts { @import "../../../contacts/app-contacts.less"; }
body.cp-app-poll { @import "../../../poll/app-poll.less"; }
body.cp-app-whiteboard { @import "../../../whiteboard/app-whiteboard.less"; }
body.cp-app-todo { @import "../../../todo/app-todo.less"; }
body.cp-app-profile { @import "../../../profile/app-profile.less"; }

@ -44,6 +44,7 @@ define(function () {
out.typing = "Édition";
out.initializing = "Initialisation...";
out.forgotten = 'Déplacé vers la corbeille';
out.errorState = 'Erreur critique : {0}';
out.lag = 'Latence';
out.readonly = 'Lecture seule';
out.anonymous = "Anonyme";
@ -56,6 +57,7 @@ define(function () {
out.viewers = "lecteurs";
out.editor = "éditeur";
out.editors = "éditeurs";
out.userlist_offline = "Vous êtes actuellement hors-ligne, la liste des utilisateurs n'est pas disponible.";
out.language = "Langue";
@ -359,8 +361,8 @@ define(function () {
out.fm_openParent = "Montrer dans le dossier";
out.fm_noname = "Document sans titre";
out.fm_emptyTrashDialog = "Êtes-vous sûr de vouloir vider la corbeille ?";
out.fm_removeSeveralPermanentlyDialog = "Êtes-vous sûr de vouloir supprimer ces {0} éléments de manière permanente ?";
out.fm_removePermanentlyDialog = "Êtes-vous sûr de vouloir supprimer cet élément de manière permanente ?";
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_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 ?";
@ -397,9 +399,9 @@ define(function () {
out.fc_rename = "Renommer";
out.fc_open = "Ouvrir";
out.fc_open_ro = "Ouvrir (lecture seule)";
out.fc_delete = "Supprimer";
out.fc_delete = "Déplacer vers la corbeille";
out.fc_restore = "Restaurer";
out.fc_remove = "Supprimer définitivement";
out.fc_remove = "Supprimer de votre CryptDrive";
out.fc_empty = "Vider la corbeille";
out.fc_prop = "Propriétés";
out.fc_hashtag = "Mots-clés";
@ -486,6 +488,13 @@ define(function () {
out.settings_resetTipsButton = "Réinitialiser les astuces visibles dans CryptDrive";
out.settings_resetTipsDone = "Toutes les astuces sont de nouveau visibles.";
out.settings_thumbnails = "Vignettes";
out.settings_disableThumbnailsAction = "Désactiver la création de vignettes dans CryptDrive";
out.settings_disableThumbnailsDescription = "Des vignettes de vos pads sont automatiquement créées et stockées dans votre navigateur. Vous pouvez désactiver cette fonctionnalité.";
out.settings_resetThumbnailsAction = "Nettoyer";
out.settings_resetThumbnailsDescription = "Nettoyer toutes les vignettes stockées dans votre navigateur.";
out.settings_resetThumbnailsDone = "Toutes les vignettes ont été effacées.";
out.settings_importTitle = "Importer les pads récents de ce navigateur dans votre CryptDrive";
out.settings_import = "Importer";
out.settings_importConfirm = "Êtes-vous sûr de vouloir importer les pads récents de ce navigateur dans le CryptDrive de votre compte utilisateur ?";

@ -43,9 +43,10 @@ define(function () {
out.disconnected = 'Disconnected';
out.synchronizing = 'Synchronizing';
out.reconnecting = 'Reconnecting...';
out.typing = "Typing";
out.typing = "Editing";
out.initializing = "Initializing...";
out.forgotten = 'Moved to the trash';
out.errorState = 'Critical error: {0}';
out.lag = 'Lag';
out.readonly = 'Read only';
out.anonymous = "Anonymous";
@ -58,6 +59,7 @@ define(function () {
out.viewers = "viewers";
out.editor = "editor";
out.editors = "editors";
out.userlist_offline = "You're currently offline, the user list is not available.";
out.language = "Language";
@ -361,8 +363,8 @@ define(function () {
out.fm_openParent = "Show in folder";
out.fm_noname = "Untitled Document";
out.fm_emptyTrashDialog = "Are you sure you want to empty the trash?";
out.fm_removeSeveralPermanentlyDialog = "Are you sure you want to remove these {0} elements from the trash permanently?";
out.fm_removePermanentlyDialog = "Are you sure you want to remove that element permanently?";
out.fm_removeSeveralPermanentlyDialog = "Are you sure you want to remove these {0} elements from your CryptDrive permanently?";
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_restoreDialog = "Are you sure you want to restore {0} to its previous location?";
@ -399,9 +401,9 @@ define(function () {
out.fc_rename = "Rename";
out.fc_open = "Open";
out.fc_open_ro = "Open (read-only)";
out.fc_delete = "Delete";
out.fc_delete = "Move to trash";
out.fc_restore = "Restore";
out.fc_remove = "Delete permanently";
out.fc_remove = "Remove from your CryptDrive";
out.fc_empty = "Empty the trash";
out.fc_prop = "Properties";
out.fc_hashtag = "Tags";
@ -491,6 +493,13 @@ define(function () {
out.settings_resetTipsButton = "Reset the available tips in CryptDrive";
out.settings_resetTipsDone = "All the tips are now visible again.";
out.settings_thumbnails = "Thumbnails";
out.settings_disableThumbnailsAction = "Disable thumbnails creation in your CryptDrive";
out.settings_disableThumbnailsDescription = "Thumbnails are automatically created and stored in your browser when you visit a new pad. You can disable this feature here.";
out.settings_resetThumbnailsAction = "Clean";
out.settings_resetThumbnailsDescription = "Clean all the pads thumbnails stored in your browser.";
out.settings_resetThumbnailsDone = "All the thumbnails have been erased.";
out.settings_importTitle = "Import this browser's recent pads in your CryptDrive";
out.settings_import = "Import";
out.settings_importConfirm = "Are you sure you want to import recent pads from this browser to your user account's CryptDrive?";

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

@ -7,7 +7,8 @@ define([
'/drive/tests.js',
'/common/test.js',
'/common/common-thumbnail.js',
], function ($, Hyperjson, TextPatcher, Sortify, Cryptpad, Drive, Test, Thumb) {
'/common/flat-dom.js',
], function ($, Hyperjson, TextPatcher, Sortify, Cryptpad, Drive, Test, Thumb, Flat) {
window.Hyperjson = Hyperjson;
window.TextPatcher = TextPatcher;
window.Sortify = Sortify;
@ -236,11 +237,20 @@ define([
!secret.hashData.present);
}, "test support for trailing slashes in version 1 hash failed to parse");
assert(function (cb) {
var secret = Cryptpad.parsePadUrl('/invite/#/1/ilrOtygzDVoUSRpOOJrUuQ/e8jvf36S3chzkkcaMrLSW7PPrz7VDp85lIFNI26dTmr=/');
var hd = secret.hashData;
cb(hd.channel === "ilrOtygzDVoUSRpOOJrUuQ" &&
hd.pubkey === "e8jvf36S3chzkkcaMrLSW7PPrz7VDp85lIFNI26dTmr=" &&
hd.type === 'invite');
}, "test support for invite urls");
assert(function (cb) {
// TODO
return cb(true);
}, "version 2 hash failed to parse correctly");
/*
assert(function (cb) {
var getBlob = function (url, cb) {
var xhr = new XMLHttpRequest();
@ -266,9 +276,21 @@ define([
});
});
});
*/
Drive.test(assert);
assert(function (cb) {
// extract dom elements into a flattened JSON representation
var flat = Flat.fromDOM(document.body);
// recreate a _mostly_ equivalent DOM
var dom = Flat.toDOM(flat);
// assume we don't care about comments
var bodyText = document.body.outerHTML.replace(/<!\-\-[\s\S]*?\-\->/g, '');
// check for equality
cb(dom.outerHTML === bodyText);
});
var swap = function (str, dict) {
return str.replace(/\{\{(.*?)\}\}/g, function (all, key) {
return typeof dict[key] !== 'undefined'? dict[key] : all;

@ -6,6 +6,7 @@
<style>
.loading-hidden { display: none; }
#editor1 { display: none; }
.html2canvas-container { width: 3000px !important; height: 3000px !important; }
</style>
</head>
<body class="cp-app-code">

@ -145,6 +145,10 @@ define([
$codeMirror.addClass('cp-app-code-fullpage');
};
var isVisible = function () {
return $previewContainer.is(':visible');
};
framework.onReady(function () {
// add the splitter
var splitter = $('<div>', {
@ -184,7 +188,8 @@ define([
return {
forceDraw: forceDrawPreview,
draw: drawPreview,
modeChange: modeChange
modeChange: modeChange,
isVisible: isVisible
};
};
@ -317,6 +322,17 @@ define([
framework.start();
};
var getThumbnailContainer = function () {
var $preview = $('#cp-app-code-preview-content');
var $codeMirror = $('.CodeMirror');
if ($preview.length && $preview.is(':visible')) {
return $preview[0];
}
if ($codeMirror.length) {
return $codeMirror[0];
}
};
var main = function () {
var CodeMirror;
var editor;
@ -327,7 +343,19 @@ define([
Framework.create({
toolbarContainer: '#cme_toolbox',
contentContainer: '#cp-app-code-editor'
contentContainer: '#cp-app-code-editor',
thumbnail: {
getContainer: getThumbnailContainer,
filter: function (el, before) {
if (before) {
$(el).parents().css('overflow', 'visible');
$(el).css('max-height', Math.max(600, $(el).width()) + 'px');
return;
}
$(el).parents().css('overflow', '');
$(el).css('max-height', '');
}
}
}, waitFor(function (fw) { framework = fw; }));
nThen(function (waitFor) {

@ -296,20 +296,13 @@ define([
if (!Thumb.isSupportedType(file.type)) { return finish(); }
// make a resized thumbnail from the image..
Thumb.fromBlob(file, function (e, thumb_blob) {
Thumb.fromBlob(file, function (e, thumb64) {
if (e) { console.error(e); }
if (!thumb_blob) { return finish(); }
blobToArrayBuffer(thumb_blob, function (e, buffer) {
if (e) {
console.error(e);
return finish();
}
thumb = arrayBufferToString(buffer);
if (!thumb64) { return finish(); }
thumb = thumb64;
finish();
});
});
});
};
var onFileDrop = File.onFileDrop = function (file, e) {

@ -526,7 +526,8 @@ define([
// them.
var win;
$('.tippy-popper').each(function (i, el) {
win = win || $('#pad-iframe')[0].contentWindow;
win = win || $('#pad-iframe').length? $('#pad-iframe')[0].contentWindow: undefined;
if (!win) { return; }
if (win.$('[aria-describedby=' + el.getAttribute('id') + ']').length === 0) {
el.remove();
}

@ -1,9 +1,18 @@
define([
'jquery',
'/common/common-util.js',
'/common/visible.js',
'/common/common-hash.js',
'/file/file-crypto.js',
'/bower_components/localforage/dist/localforage.min.js',
'/bower_components/tweetnacl/nacl-fast.min.js',
], function () {
], function ($, Util, Visible, Hash, FileCrypto, localForage) {
var Nacl = window.nacl;
var Thumb = {
dimension: 100,
padDimension: 200,
UPDATE_INTERVAL: 60000,
UPDATE_FIRST: 5000
};
var supportedTypes = [
@ -11,7 +20,8 @@ define([
'image/jpeg',
'image/jpg',
'image/gif',
'video/'
'video/',
'application/pdf'
];
Thumb.isSupportedType = function (type) {
@ -41,22 +51,32 @@ define([
}
};
var getResizedDimensions = function (img, type) {
var getResizedDimensions = Thumb.getResizedDimensions = function (img, type) {
var h = type === 'video' ? img.videoHeight : img.height;
var w = type === 'video' ? img.videoWidth : img.width;
var dim = Thumb.dimension;
var dim = type === 'pad' ? Thumb.padDimension : Thumb.dimension;
// if the image is too small, don't bother making a thumbnail
if (h <= dim && w <= dim) { return null; }
/*if (h <= dim && w <= dim) {
return {
x: Math.floor((dim - w) / 2),
w: w,
y: Math.floor((dim - h) / 2),
h : h
};
}*/
// the image is taller than it is wide, so scale to that.
var r = dim / (h > w? h: w); // ratio
if (h <= dim && w <= dim) { r = 1; }
var d;
if (h > w) {
var newW = Math.floor(w*r);
d = Math.floor((dim - newW) / 2);
return {
dim: dim,
x: d,
w: newW,
y: 0,
@ -66,6 +86,7 @@ define([
var newH = Math.floor(h*r);
d = Math.floor((dim - newH) / 2);
return {
dim: dim,
x: 0,
w: dim,
y: d,
@ -76,18 +97,16 @@ define([
// assumes that your canvas is square
// nodeback returning blob
Thumb.fromCanvas = Thumb.fromImage = function (canvas, D, cb) {
Thumb.fromCanvas = function (canvas, D, cb) {
var c2 = document.createElement('canvas');
if (!D) { return void cb('TOO_SMALL'); }
if (!D) { return void cb('ERROR'); }
c2.width = Thumb.dimension;
c2.height = Thumb.dimension;
c2.width = D.dim;
c2.height = D.dim;
var ctx = c2.getContext('2d');
ctx.drawImage(canvas, D.x, D.y, D.w, D.h);
c2.toBlob(function (blob) {
cb(void 0, blob);
});
cb(void 0, c2.toDataURL());
};
Thumb.fromImageBlob = function (blob, cb) {
@ -96,10 +115,7 @@ define([
img.onload = function () {
var D = getResizedDimensions(img, 'image');
Thumb.fromImage(img, D, function (err, t) {
if (err === 'TOO_SMALL') { return void cb(void 0, blob); }
cb(err, t);
});
Thumb.fromCanvas(img, D, cb);
};
img.onerror = function () {
cb('ERROR');
@ -123,15 +139,138 @@ define([
cb('ERROR');
});
};
Thumb.fromPdfBlob = function (blob, cb) {
require.config({paths: {'pdfjs-dist': '/common/pdfjs'}});
require(['pdfjs-dist/build/pdf'], function (PDFJS) {
var url = URL.createObjectURL(blob);
var makeThumb = function (page) {
var vp = page.getViewport(1);
var canvas = document.createElement("canvas");
canvas.width = canvas.height = Thumb.dimension;
var scale = Math.min(canvas.width / vp.width, canvas.height / vp.height);
canvas.width = Math.floor(vp.width * scale);
canvas.height = Math.floor(vp.height * scale);
return page.render({
canvasContext: canvas.getContext("2d"),
viewport: page.getViewport(scale)
}).promise.then(function () {
return canvas;
});
};
PDFJS.getDocument(url).promise
.then(function (doc) {
return doc.getPage(1).then(makeThumb).then(function (canvas) {
var D = getResizedDimensions(canvas, 'pdf');
Thumb.fromCanvas(canvas, D, cb);
});
}).catch(function () {
cb('ERROR');
});
});
};
Thumb.fromBlob = function (blob, cb) {
if (blob.type.indexOf('video/') !== -1) {
return void Thumb.fromVideoBlob(blob, cb);
}
if (blob.type.indexOf('application/pdf') !== -1) {
return void Thumb.fromPdfBlob(blob, cb);
}
Thumb.fromImageBlob(blob, cb);
};
Thumb.fromVideo = function (video, cb) {
cb = cb; // WIP
window.html2canvas = undefined;
Thumb.fromDOM = function (opts, cb) {
var element = opts.getContainer();
var todo = function () {
if (opts.filter) { opts.filter(element, true); }
window.html2canvas(element, {
allowTaint: true,
onrendered: function (canvas) {
if (opts.filter) { opts.filter(element, false); }
var D = getResizedDimensions(canvas, 'pad');
Thumb.fromCanvas(canvas, D, cb);
}
});
};
if (window.html2canvas) { return void todo(); }
require(['/bower_components/html2canvas/build/html2canvas.min.js'], todo);
};
Thumb.initPadThumbnails = function (opts) {
if (!opts.href || !opts.getContent) {
throw new Error("href and getContent are needed for thumbnails");
}
var oldThumbnailState;
var mkThumbnail = function () {
var content = opts.getContent();
if (content === oldThumbnailState) { return; }
Thumb.fromDOM(opts, function (err, b64) {
oldThumbnailState = content;
Thumb.setPadThumbnail(opts.href, b64);
});
};
var nafa = Util.notAgainForAnother(mkThumbnail, Thumb.UPDATE_INTERVAL);
var to;
var tUntil;
var interval = function () {
tUntil = nafa();
if (tUntil) {
window.clearTimeout(to);
to = window.setTimeout(interval, tUntil+1);
return;
}
to = window.setTimeout(interval, Thumb.UPDATE_INTERVAL+1);
};
Visible.onChange(function (v) {
if (v) {
window.clearTimeout(to);
return;
}
interval();
});
if (!Visible.currently()) { to = window.setTimeout(interval, Thumb.UPDATE_FIRST); }
};
var addThumbnail = function (err, thumb, $span, cb) {
var img = new Image();
img.src = thumb.slice(0,5) === 'data:' ? thumb : 'data:;base64,'+thumb;
$span.find('.cp-icon').hide();
$span.prepend(img);
cb($(img));
};
Thumb.setPadThumbnail = function (href, b64, cb) {
cb = cb || function () {};
var k ='thumbnail-' + href;
localForage.setItem(k, b64, cb);
};
Thumb.displayThumbnail = function (href, $container, cb) {
cb = cb || function () {};
var parsed = Hash.parsePadUrl(href);
var k ='thumbnail-' + href;
var whenNewThumb = function () {
var secret = Hash.getSecrets('file', parsed.hash);
var hexFileName = Util.base64ToHex(secret.channel);
var src = Hash.getBlobPathFromHex(hexFileName);
var cryptKey = secret.keys && secret.keys.fileKeyStr;
var key = Nacl.util.decodeBase64(cryptKey);
FileCrypto.fetchDecryptedMetadata(src, key, function (e, metadata) {
if (!metadata.thumbnail) {
return void localForage.setItem(k, 'EMPTY');
}
localForage.setItem(k, metadata.thumbnail, function (err) {
addThumbnail(err, metadata.thumbnail, $container, cb);
});
});
};
localForage.getItem(k, function (err, v) {
if (!v && parsed.type === 'file') {
// We can only create thumbnails for files here since we can't easily decrypt pads
return void whenNewThumb();
}
if (!v) { return; }
if (v === 'EMPTY') { return; }
addThumbnail(err, v, $container, cb);
});
};
return Thumb;

@ -45,9 +45,7 @@ define([
var src = '/blob/' + hexFileName.slice(0,2) + '/' + hexFileName;
var mt = '<media-tag src="' + src + '" data-crypto-key="cryptpad:' + parsed.hashData.key + '">';
if (mediaMap[src]) {
mediaMap[src].forEach(function (n) {
mt += n.outerHTML;
});
mt += mediaMap[src];
}
mt += '</media-tag>';
return mt;
@ -129,6 +127,7 @@ define([
var domFromHTML = function (html) {
var Dom = new DOMParser().parseFromString(html, "text/html");
Dom.normalize();
removeForbiddenTags(Dom.body);
removeListeners(Dom.body);
return Dom;
@ -167,11 +166,7 @@ define([
var unsafe_newHtmlFixed = newHtml.replace(pattern, function (all, tag, src) {
var mt = tag;
if (mediaMap[src]) {
mediaMap[src].forEach(function (n) {
mt += n.outerHTML;
});
}
if (mediaMap[src]) { mt += mediaMap[src]; }
return mt + '</media-tag>';
});
@ -179,6 +174,7 @@ define([
var $div = $('<div>', {id: id}).append(safe_newHtmlFixed);
var Dom = domFromHTML($('<div>').append($div).html());
$content[0].normalize();
var oldDom = domFromHTML($content[0].outerHTML);
var patch = makeDiff(oldDom, Dom, id);
if (typeof(patch) === 'string') {
@ -191,9 +187,11 @@ define([
var observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
if (mutation.type === 'childList') {
//console.log(el.outerHTML);
var list_values = [].slice.call(el.children);
mediaMap[el.getAttribute('src')] = list_values;
var list_values = [].slice.call(mutation.target.children)
.map(function (el) { return el.outerHTML; })
.join('');
mediaMap[mutation.target.getAttribute('src')] = list_values;
observer.disconnect();
}
});
});

@ -0,0 +1,83 @@
define([], function () {
var Flat = {};
var slice = function (coll) {
return Array.prototype.slice.call(coll);
};
var getAttrs = function (el) {
var i = 0;
var l = el.attributes.length;
var attr;
var data = {};
for (;i < l;i++) {
attr = el.attributes[i];
if (attr.name && attr.value) { data[attr.name] = attr.value; }
}
return data;
};
var identity = function (x) { return x; };
Flat.fromDOM = function (dom) {
var data = {
map: {},
};
var i = 1; // start from 1 so we're always truthey
var uid = function () { return i++; };
var process = function (el) {
var id;
if (!el.tagName && el.nodeType === Node.TEXT_NODE) {
id = uid();
data.map[id] = el.textContent;
return id;
}
if (!el || !el.attributes) { return void console.error(el); }
id = uid();
data.map[id] = [
el.tagName,
getAttrs(el),
slice(el.childNodes).map(function (e) {
return process(e);
}).filter(identity)
];
return id;
};
data.root = process(dom);
return data;
};
Flat.toDOM = function (data) {
var visited = {};
var process = function (key) {
if (!key) { return; } // ignore falsey keys
if (visited[key]) {
// TODO handle this more gracefully.
throw new Error('duplicate id or loop detected');
}
visited[key] = true; // mark paths as visited.
var hj = data.map[key];
if (typeof(hj) === 'string') { return document.createTextNode(hj); }
if (typeof(hj) === 'undefined') { return; }
if (!Array.isArray(hj)) { console.error(hj); throw new Error('expected array'); }
var e = document.createElement(hj[0]);
for (var x in hj[1]) { e.setAttribute(x, hj[1][x]); }
var child;
for (var i = 0; i < hj[2].length; i++) {
child = process(hj[2][i]);
if (child) {
e.appendChild(child);
}
}
return e;
};
return process(data.root);
};
return Flat;
});

@ -10,6 +10,7 @@ define([
'/common/sframe-common.js',
'/customize/messages.js',
'/common/common-util.js',
'/common/common-thumbnail.js',
'/customize/application_config.js',
'css!/bower_components/bootstrap/dist/css/bootstrap.min.css',
@ -27,6 +28,7 @@ define([
SFCommon,
Messages,
Util,
Thumb,
AppConfig)
{
var SaveAs = window.saveAs;
@ -264,6 +266,20 @@ define([
Cryptpad.removeLoadingScreen(emitResize);
var privateDat = cpNfInner.metadataMgr.getPrivateData();
if (options.thumbnail && privateDat.thumbnails) {
var hash = privateDat.availableHashes.editHash ||
privateDat.availableHashes.viewHash;
if (hash) {
options.thumbnail.href = privateDat.pathname + '#' + hash;
options.thumbnail.getContent = function () {
if (!cpNfInner.chainpad) { return; }
return cpNfInner.chainpad.getUserDoc();
};
Thumb.initPadThumbnails(options.thumbnail);
}
}
if (newPad) {
common.openTemplatePicker();
}
@ -362,10 +378,7 @@ define([
}).nThen(function (waitFor) {
cpNfInner = common.startRealtime({
// really basic operational transform
transformFunction: options.transformFunction || JsonOT.validate,
// This one causes a big mess.
//patchTransformer: options.patchTransformer || JsonOT.patchTransformer,
transformFunction: options.transformFunction || JsonOT.transform,
// cryptpad debug logging (default is 1)
// logLevel: 0,
@ -399,6 +412,7 @@ define([
textPatcher = TextPatcher.create({ realtime: cpNfInner.chainpad });
var infiniteSpinnerModal = false;
window.setInterval(function () {
if (state === STATE.DISCONNECTED) { return; }
var l;
@ -409,13 +423,15 @@ define([
}
if (l.lag < badStateTimeout) { return; }
if (state === STATE.INFINITE_SPINNER) { return; }
if (infiniteSpinnerModal) { return; }
infiniteSpinnerModal = true;
stateChange(STATE.INFINITE_SPINNER);
Cryptpad.confirm(Messages.realtime_unrecoverableError, function (yes) {
if (!yes) { return; }
common.gotoURL();
});
cpNfInner.chainpad.onSettle(function () {
infiniteSpinnerModal = false;
Cryptpad.findCancelButton().click();
stateChange(STATE.READY);
onRemote();

@ -13,7 +13,8 @@ define(['/common/requireconfig.js'], function (RequireConfig) {
var mkFakeStore = function () {
var fakeStorage = {
getItem: function (k) { return fakeStorage[k]; },
setItem: function (k, v) { fakeStorage[k] = v; return v; }
setItem: function (k, v) { fakeStorage[k] = v; return v; },
removeItem: function (k) { delete fakeStorage[k]; }
};
return fakeStorage;
};

@ -685,7 +685,9 @@ define([
});
};
var ready = false;
realtimeOptions.onReady = function (info) {
if (ready) { return; }
// create your patcher
if (realtime !== info.realtime) {
realtime = rt.realtime = info.realtime;
@ -709,6 +711,7 @@ define([
DeepProxy.checkLocalChange(proxy, onLocal);
initializing = false;
ready = true;
};
realtimeOptions.onAbort = function (info) {

@ -249,20 +249,13 @@ define([
if (!Thumb.isSupportedType(file.type)) { return finish(); }
// make a resized thumbnail from the image..
Thumb.fromBlob(file, function (e, thumb_blob) {
Thumb.fromBlob(file, function (e, thumb64) {
if (e) { console.error(e); }
if (!thumb_blob) { return finish(); }
blobToArrayBuffer(thumb_blob, function (e, buffer) {
if (e) {
console.error(e);
return finish();
}
thumb = arrayBufferToString(buffer);
if (!thumb64) { return finish(); }
thumb = thumb64;
finish();
});
});
});
};
var onFileDrop = File.onFileDrop = function (file, e) {

@ -3,19 +3,14 @@ define([
'/api/config',
'/common/cryptpad-common.js',
'/common/common-util.js',
'/common/common-hash.js',
'/common/media-tag.js',
'/common/tippy.min.js',
'/customize/application_config.js',
'/file/file-crypto.js',
'/bower_components/localforage/dist/localforage.min.js',
'/bower_components/tweetnacl/nacl-fast.min.js',
'css!/common/tippy.css',
], function ($, Config, Cryptpad, Util, Hash, MediaTag, Tippy, AppConfig, FileCrypto, localForage) {
], function ($, Config, Cryptpad, Util, MediaTag, Tippy, AppConfig) {
var UI = {};
var Messages = Cryptpad.Messages;
var Nacl = window.nacl;
/**
* Requirements from cryptpad-common.js
@ -33,40 +28,6 @@ define([
* - createDropdown
*/
var addThumbnail = function (err, thumb, $span, cb) {
var img = new Image();
img.src = 'data:;base64,'+thumb;
$span.find('.cp-icon').hide();
$span.prepend(img);
cb($(img));
};
UI.displayThumbnail = function (href, $container, cb) {
cb = cb || $.noop;
var parsed = Hash.parsePadUrl(href);
if (parsed.type !== 'file') { return; }
var k ='thumbnail-' + href;
var whenNewThumb = function () {
var secret = Hash.getSecrets('file', parsed.hash);
var hexFileName = Util.base64ToHex(secret.channel);
var src = Hash.getBlobPathFromHex(hexFileName);
var cryptKey = secret.keys && secret.keys.fileKeyStr;
var key = Nacl.util.decodeBase64(cryptKey);
FileCrypto.fetchDecryptedMetadata(src, key, function (e, metadata) {
if (!metadata.thumbnail) {
return void localForage.setItem(k, 'EMPTY');
}
localForage.setItem(k, metadata.thumbnail, function (err) {
addThumbnail(err, metadata.thumbnail, $container, cb);
});
});
};
localForage.getItem(k, function (err, v) {
if (!v) { return void whenNewThumb(); }
if (v === 'EMPTY') { return; }
addThumbnail(err, v, $container, cb);
});
};
UI.updateTags = function (common, href) {
var sframeChan = common.getSframeChannel();
sframeChan.query('Q_TAGS_GET', href || null, function (err, res) {
@ -330,6 +291,7 @@ define([
$img.attr('src', src);
$img.attr('data-crypto-key', 'cryptpad:' + cryptKey);
UI.displayMediatagImage(Common, $img, function (err, $image, img) {
if (err) { return void console.error(err); }
var w = img.width;
var h = img.height;
if (w>h) {

@ -130,6 +130,7 @@ define([
settings: proxy.settings || {},
isPresent: parsed.hashData && parsed.hashData.present,
isEmbed: parsed.hashData && parsed.hashData.embed,
thumbnails: !((proxy.settings || {}).general || {}).disableThumbnails,
accounts: {
donateURL: Cryptpad.donateURL,
upgradeURL: Cryptpad.upgradeURL

@ -14,7 +14,8 @@ define([
'/customize/application_config.js',
'/common/cryptpad-common.js',
'/common/common-realtime.js',
'/common/common-util.js'
'/common/common-util.js',
'/common/common-thumbnail.js'
], function (
$,
nThen,
@ -30,7 +31,8 @@ define([
AppConfig,
Cryptpad,
CommonRealtime,
Util
Util,
Thumb
) {
// Chainpad Netflux Inner
@ -80,7 +82,9 @@ define([
funcs.createButton = callWithCommon(UI.createButton);
funcs.createUsageBar = callWithCommon(UI.createUsageBar);
funcs.updateTags = callWithCommon(UI.updateTags);
funcs.displayThumbnail = UI.displayThumbnail;
// Thumb
funcs.displayThumbnail = Thumb.displayThumbnail;
// History
funcs.getHistory = callWithCommon(History.create);

@ -168,4 +168,8 @@ define({
// in the drive at registration.
'Q_MERGE_ANON_DRIVE': true,
// Add or remove the avatar from the profile.
// We have to pin/unpin the avatar and store/remove the value from the user object
'Q_PROFILE_AVATAR_ADD': true,
'Q_PROFILE_AVATAR_REMOVE': true
});

@ -154,6 +154,7 @@ define([
var $userlistContent = toolbar.userlistContent;
var metadataMgr = config.metadataMgr;
var online = metadataMgr.isConnected();
var userData = metadataMgr.getMetadata().users;
var viewers = metadataMgr.getViewers();
var priv = metadataMgr.getPrivateData();
@ -184,7 +185,23 @@ define([
// Update the userlist
var $editUsers = $userlistContent.find('.' + USERLIST_CLS).html('');
var $editUsersList = $('<div>', {'class': 'cp-toolbar-userlist-others'});
var $editUsersList = $('<div>', {'class': 'cp-toolbar-userlist-others'})
.appendTo($editUsers);
if (!online) {
$('<em>').text(Messages.userlist_offline).appendTo($editUsersList);
numberOfEditUsers = '?';
numberOfViewUsers = '?';
}
// Update the buttons
var fa_editusers = '<span class="fa fa-users"></span>';
var fa_viewusers = '<span class="fa fa-eye"></span>';
var $spansmall = $('<span>').html(fa_editusers + ' ' + numberOfEditUsers + '&nbsp;&nbsp; ' + fa_viewusers + ' ' + numberOfViewUsers);
$userButtons.find('.cp-dropdown-button-title').html('').append($spansmall);
if (!online) { return; }
// Display the userlist
// Editors
var pendingFriends = Common.getPendingFriends();
@ -237,7 +254,6 @@ define([
$span.data('uid', data.uid);
$editUsersList.append($span);
});
$editUsers.append($editUsersList);
// Viewers
if (numberOfViewUsers > 0) {
@ -246,12 +262,6 @@ define([
viewText += numberOfViewUsers + ' ' + viewerText + '</div>';
$editUsers.append(viewText);
}
// Update the buttons
var fa_editusers = '<span class="fa fa-users"></span>';
var fa_viewusers = '<span class="fa fa-eye"></span>';
var $spansmall = $('<span>').html(fa_editusers + ' ' + numberOfEditUsers + '&nbsp;&nbsp; ' + fa_viewusers + ' ' + numberOfViewUsers);
$userButtons.find('.cp-dropdown-button-title').html('').append($spansmall);
};
var initUserList = function (toolbar, config) {
@ -691,6 +701,7 @@ define([
var typing = -1;
var kickSpinner = function (toolbar, config/*, local*/) {
if (!toolbar.spinner) { return; }
if (toolbar.isErrorState) { return; }
var $spin = toolbar.spinner;
if (typing === -1) {
@ -886,6 +897,7 @@ define([
// type : 1 (+1 user), 0 (rename existing user), -1 (-1 user)
if (typeof name === "undefined") { return; }
name = name || Messages.anonymous;
if (Config.disableUserlistNotifications) { return; }
switch(type) {
case 1:
Cryptpad.log(Messages._getKey("notifyJoined", [name]));
@ -920,6 +932,7 @@ define([
return count;
};
var joined = false;
metadataMgr.onChange(function () {
var newdata = metadataMgr.getMetadata().users;
var netfluxIds = Object.keys(newdata);
@ -948,7 +961,7 @@ define([
return;
}
for (var k in newdata) {
if (k !== userNetfluxId && netfluxIds.indexOf(k) !== -1) {
if (joined && k !== userNetfluxId && netfluxIds.indexOf(k) !== -1) {
if (typeof oldUserData[k] === "undefined") {
// if the same uid is already present in the userdata, don't notify
if (!userPresent(k, newdata[k], oldUserData)) {
@ -959,6 +972,7 @@ define([
}
}
}
joined = true;
oldUserData = JSON.parse(JSON.stringify(newdata));
});
}
@ -1047,6 +1061,17 @@ define([
}
};
toolbar.errorState = function (state, error) {
toolbar.isErrorState = state;
if (toolbar.spinner) {
if (!state) {
return void kickSpinner(toolbar, config);
}
var txt = Messages._getKey('errorState', [error]);
toolbar.spinner.text(txt);
}
};
// When the pad is moved to the trash (forget button)
toolbar.forgotten = function (/*userId*/) {
toolbar.connected = false;

@ -3,10 +3,9 @@ define([
'/common/cryptpad-common.js',
'/common/hyperscript.js',
'/bower_components/marked/marked.min.js',
], function ($, Cryptpad, h, Marked) {
'/common/media-tag.js',
], function ($, Cryptpad, h, Marked, MediaTag) {
'use strict';
// TODO use our fancy markdown and support media-tags
Marked.setOptions({ sanitize: true, });
var UI = {};
var Messages = Cryptpad.Messages;
@ -15,6 +14,12 @@ define([
var d = h('div.cp-app-contacts-content');
try {
d.innerHTML = Marked(md || '');
var $d = $(d);
// remove potentially malicious elements
$d.find('script, iframe, object, applet, video, audio').remove();
// activate media-tags
$d.find('media-tag').each(function (i, e) { MediaTag(e); });
} catch (e) {
console.error(md);
console.error(e);

@ -58,13 +58,15 @@ min-height: auto;
}
.cp-app-drive-element-name {
width: 100%;
height: 48px;
margin: 8px 0;
height: 24px;
margin: 0;
display: inline-block;
font-size: 14px;
//align-items: center;
//justify-content: center;
overflow: hidden;
//text-overflow: ellipsis;
white-space: nowrap;
text-overflow: ellipsis;
word-wrap: break-word;
}
.cp-app-drive-element-truncated {
@ -83,8 +85,8 @@ min-height: auto;
.fa {
display: block;
margin: auto;
font-size: 48px;
margin: 8px 0;
font-size: 64px;
margin: 18px 0;
text-align: center;
&.listonly {
display: none;
@ -518,10 +520,15 @@ span {
}
}
.cp-app-drive-element-thumbnail {
max-width: 64px;
max-height: 64px;
max-width: 100px;
max-height: 100px;
& ~ .fa {
display: none;
display: inline;
font-size: 17px;
position: absolute;
top: 3px;
left: 3px;
margin: 0;
}
}
}

@ -1145,6 +1145,10 @@ define([
if (!data) { return void logError("No data for the file", element); }
var hrefData = Cryptpad.parsePadUrl(data.href);
if (hrefData.type) {
$span.addClass('cp-border-color-'+hrefData.type);
}
var $state = $('<span>', {'class': 'cp-app-drive-element-state'});
if (hrefData.hashData && hrefData.hashData.mode === 'view') {
var $ro = $readonlyIcon.clone().appendTo($state);
@ -1161,6 +1165,7 @@ define([
var $name = $('<span>', {'class': 'cp-app-drive-element-name'}).text(name);
$span.append($name);
$span.append($state);
$span.attr('title', name);
var type = Messages.type[hrefData.type] || hrefData.type;
common.displayThumbnail(data.href, $span, function ($thumb) {
@ -1199,6 +1204,7 @@ define([
var $files = $('<span>', {
'class': 'cp-app-drive-element-files cp-app-drive-element-list'
}).text(files);
$span.attr('title', key);
$span.append($name).append($state).append($subfolders).append($files);
};
@ -1512,7 +1518,7 @@ define([
if (isInRoot) {
options.push({
tag: 'a',
attributes: {'class': 'cp-app-drive-new-older'},
attributes: {'class': 'cp-app-drive-new-folder'},
content: $('<div>').append($folderIcon.clone()).html() + Messages.fm_folder
});
options.push({tag: 'hr'});
@ -2197,7 +2203,7 @@ define([
}
$content.append($info).append($dirContent);
var $truncated = $('<span>', {'class': 'cp-app-drive-element-truncated'}).text('...');
/*var $truncated = $('<span>', {'class': 'cp-app-drive-element-truncated'}).text('...');
$content.find('.cp-app-drive-element').each(function (idx, el) {
var $name = $(el).find('.cp-app-drive-element-name');
if ($name.length === 0) { return; }
@ -2206,7 +2212,7 @@ define([
$tr.attr('title', $name.text());
$(el).append($tr);
}
});
});*/
$content.scrollTop(s);
appStatus.ready(true);

@ -47,6 +47,11 @@
align-items: center;
img {
max-width: 100px;
max-height: 100px;
}
.cp-filepicker-content-element-name {
overflow: hidden;
text-overflow: ellipsis;

@ -0,0 +1,20 @@
<!DOCTYPE html>
<html class="cp">
<!-- If this file is not called customize.dist/src/template.html, it is generated -->
<head>
<title data-localization="main_title">CryptPad: Zero Knowledge, Collaborative Real Time Editing</title>
<meta content="text/html; charset=utf-8" http-equiv="content-type"/>
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<link rel="icon" type="image/png" href="/customize/main-favicon.png" id="favicon"/>
<script async data-bootload="/customize/template.js" data-main="/common/boot.js?ver=1.0" src="/bower_components/requirejs/require.js?ver=2.3.5"></script>
<link rel="stylesheet" href="/bower_components/codemirror/lib/codemirror.css">
<link rel="stylesheet" href="/bower_components/codemirror/addon/dialog/dialog.css">
<link rel="stylesheet" href="/bower_components/codemirror/addon/fold/foldgutter.css" />
</head>
<body class="html">
<noscript>
<p><strong>OOPS</strong> In order to do encryption in your browser, Javascript is really <strong>really</strong> required.</p>
<p><strong>OUPS</strong> Afin de pouvoir réaliser le chiffrement dans votre navigateur, Javascript est <strong>vraiment</strong> nécessaire.</p>
</noscript>
</html>

@ -0,0 +1,532 @@
require.config({
paths: {
cm: '/bower_components/codemirror'
}
});
define([
'jquery',
'/common/cryptpad-common.js',
'/bower_components/chainpad-listmap/chainpad-listmap.js',
'/bower_components/chainpad-crypto/crypto.js',
'/bower_components/marked/marked.min.js',
'/common/toolbar2.js',
'cm/lib/codemirror',
'cm/mode/markdown/markdown',
'less!/profile/main.less',
'less!/customize/src/less/toolbar.less',
'less!/customize/src/less/cryptpad.less',
'css!/bower_components/bootstrap/dist/css/bootstrap.min.css',
], function ($, Cryptpad, Listmap, Crypto, Marked, Toolbar, CodeMirror) {
var APP = window.APP = {
Cryptpad: Cryptpad,
_onRefresh: []
};
$(window.document).on('decryption', function (e) {
var decrypted = e.originalEvent;
if (decrypted.callback) { decrypted.callback(); }
})
.on('decryptionError', function (e) {
var error = e.originalEvent;
Cryptpad.alert(error.message);
});
// Marked
var renderer = new Marked.Renderer();
Marked.setOptions({
renderer: renderer,
sanitize: true
});
// Tasks list
var checkedTaskItemPtn = /^\s*\[x\]\s*/;
var uncheckedTaskItemPtn = /^\s*\[ \]\s*/;
renderer.listitem = function (text) {
var isCheckedTaskItem = checkedTaskItemPtn.test(text);
var isUncheckedTaskItem = uncheckedTaskItemPtn.test(text);
if (isCheckedTaskItem) {
text = text.replace(checkedTaskItemPtn,
'<i class="fa fa-check-square" aria-hidden="true"></i>&nbsp;') + '\n';
}
if (isUncheckedTaskItem) {
text = text.replace(uncheckedTaskItemPtn,
'<i class="fa fa-square-o" aria-hidden="true"></i>&nbsp;') + '\n';
}
var cls = (isCheckedTaskItem || isUncheckedTaskItem) ? ' class="todo-list-item"' : '';
return '<li'+ cls + '>' + text + '</li>\n';
};
/*renderer.image = function (href, title, text) {
if (href.slice(0,6) === '/file/') {
var parsed = Cryptpad.parsePadUrl(href);
var hexFileName = Cryptpad.base64ToHex(parsed.hashData.channel);
var src = '/blob/' + hexFileName.slice(0,2) + '/' + hexFileName;
var mt = '<media-tag src="' + src + '" data-crypto-key="cryptpad:' + parsed.hashData.key + '">';
mt += '</media-tag>';
return mt;
}
var out = '<img src="' + href + '" alt="' + text + '"';
if (title) {
out += ' title="' + title + '"';
}
out += this.options.xhtml ? '/>' : '>';
return out;
};*/
var Messages = Cryptpad.Messages;
var DISPLAYNAME_ID = "displayName";
var LINK_ID = "link";
var AVATAR_ID = "avatar";
var DESCRIPTION_ID = "description";
var PUBKEY_ID = "pubKey";
var CREATE_ID = "createProfile";
var HEADER_ID = "header";
var HEADER_RIGHT_ID = "rightside";
var CREATE_INVITE_BUTTON = 'inviteButton'; /* jshint ignore: line */
var VIEW_PROFILE_BUTTON = 'viewProfileButton';
var createEditableInput = function ($block, name, ph, getValue, setValue, realtime, fallbackValue) {
fallbackValue = fallbackValue || ''; // don't ever display 'null' or 'undefined'
var lastVal;
getValue(function (value) {
lastVal = value;
var $input = $('<input>', {
'id': name+'Input',
placeholder: ph
}).val(value);
var $icon = $('<span>', {'class': 'fa fa-pencil edit'});
var editing = false;
var todo = function () {
if (editing) { return; }
editing = true;
var newVal = $input.val().trim();
if (newVal === lastVal) {
editing = false;
return;
}
setValue(newVal, function (err) {
if (err) { return void console.error(err); }
Cryptpad.whenRealtimeSyncs(realtime, function () {
lastVal = newVal;
Cryptpad.log(Messages._getKey('profile_fieldSaved', [newVal || fallbackValue]));
editing = false;
});
});
};
$input.on('keyup', function (e) {
if (e.which === 13) { return void todo(); }
if (e.which === 27) {
$input.val(lastVal);
}
});
$icon.click(function () { $input.focus(); });
$input.focus(function () {
$input.width('');
});
$input.focusout(todo);
$block.append($input).append($icon);
});
};
/* jshint ignore:start */
var isFriend = function (proxy, edKey) {
var friends = Cryptpad.find(proxy, ['friends']);
return typeof(edKey) === 'string' && friends && (edKey in friends);
};
var addCreateInviteLinkButton = function ($container) {
return;
var obj = APP.lm.proxy;
var proxy = Cryptpad.getProxy();
var userViewHash = Cryptpad.find(proxy, ['profile', 'view']);
var edKey = obj.edKey;
var curveKey = obj.curveKey;
if (!APP.readOnly || !curveKey || !edKey || userViewHash === window.location.hash.slice(1) || isFriend(proxy, edKey)) {
//console.log("edit mode or missing curve key, or you're viewing your own profile");
return;
}
// sanitize user inputs
var unsafeName = obj.name || '';
console.log(unsafeName);
var name = Cryptpad.fixHTML(unsafeName) || Messages.anonymous;
console.log(name);
console.log("Creating invite button");
$("<button>", {
id: CREATE_INVITE_BUTTON,
title: Messages.profile_inviteButtonTitle,
})
.addClass('btn btn-success')
.text(Messages.profile_inviteButton)
.click(function () {
Cryptpad.confirm(Messages._getKey('profile_inviteExplanation', [name]), function (yes) {
if (!yes) { return; }
console.log(obj.curveKey);
Cryptpad.alert("TODO");
// TODO create a listmap object using your curve keys
// TODO fill the listmap object with your invite data
// TODO generate link to invite object
// TODO copy invite link to clipboard
}, null, true);
})
.appendTo($container);
};
/* jshint ignore:end */
var addViewButton = function ($container) {
if (!Cryptpad.isLoggedIn() || window.location.hash) {
return;
}
var hash = Cryptpad.find(Cryptpad.getProxy(), ['profile', 'view']);
var url = '/profile/#' + hash;
var $button = $('<button>', {
'class': 'btn btn-success',
id: VIEW_PROFILE_BUTTON,
})
.text(Messages.profile_viewMyProfile)
.click(function () {
window.open(url, '_blank');
});
$container.append($button);
};
var addDisplayName = function ($container) {
var $block = $('<div>', {id: DISPLAYNAME_ID}).appendTo($container);
var getValue = function (cb) {
cb(APP.lm.proxy.name);
};
var placeholder = Messages.profile_namePlaceholder;
if (APP.readOnly) {
var $span = $('<span>', {'class': DISPLAYNAME_ID}).appendTo($block);
getValue(function (value) {
$span.text(value || Messages.anonymous);
});
//addCreateInviteLinkButton($block);
return;
}
var setValue = function (value, cb) {
APP.lm.proxy.name = value;
cb();
};
var rt = Cryptpad.getStore().getProxy().info.realtime;
createEditableInput($block, DISPLAYNAME_ID, placeholder, getValue, setValue, rt, Messages.anonymous);
};
var addLink = function ($container) {
var $block = $('<div>', {id: LINK_ID}).appendTo($container);
var getValue = function (cb) {
cb(APP.lm.proxy.url);
};
if (APP.readOnly) {
var $a = $('<a>', {
'class': LINK_ID,
target: '_blank',
rel: 'noreferrer noopener'
}).appendTo($block);
getValue(function (value) {
if (!value) {
return void $a.hide();
}
$a.attr('href', value).text(value);
});
return;
}
var setValue = function (value, cb) {
APP.lm.proxy.url = value;
cb();
};
var rt = APP.lm.realtime;
var placeholder = Messages.profile_urlPlaceholder;
createEditableInput($block, LINK_ID, placeholder, getValue, setValue, rt);
};
var addAvatar = function ($container) {
var $block = $('<div>', {id: AVATAR_ID}).appendTo($container);
var $span = $('<span>').appendTo($block);
var allowedMediaTypes = Cryptpad.avatarAllowedTypes;
var displayAvatar = function () {
$span.html('');
if (!APP.lm.proxy.avatar) {
$('<img>', {
src: '/customize/images/avatar.png',
title: Messages.profile_avatar,
alt: 'Avatar'
}).appendTo($span);
return;
}
Cryptpad.displayAvatar($span, APP.lm.proxy.avatar);
if (APP.readOnly) { return; }
var $delButton = $('<button>', {
'class': 'delete btn btn-danger fa fa-times',
title: Messages.fc_delete
});
$span.append($delButton);
$delButton.click(function () {
var oldChanId = Cryptpad.hrefToHexChannelId(APP.lm.proxy.avatar);
Cryptpad.unpinPads([oldChanId], function (e) {
if (e) { Cryptpad.log(e); }
delete APP.lm.proxy.avatar;
delete Cryptpad.getProxy().profile.avatar;
Cryptpad.whenRealtimeSyncs(APP.lm.realtime, function () {
var driveRt = Cryptpad.getStore().getProxy().info.realtime;
Cryptpad.whenRealtimeSyncs(driveRt, function () {
displayAvatar();
});
});
});
});
};
displayAvatar();
if (APP.readOnly) { return; }
var fmConfig = {
noHandlers: true,
noStore: true,
body: $('body'),
onUploaded: function (ev, data) {
var chanId = Cryptpad.hrefToHexChannelId(data.url);
var profile = Cryptpad.getProxy().profile;
var old = profile.avatar;
var todo = function () {
Cryptpad.pinPads([chanId], function (e) {
if (e) { return void Cryptpad.log(e); }
APP.lm.proxy.avatar = data.url;
Cryptpad.getProxy().profile.avatar = data.url;
Cryptpad.whenRealtimeSyncs(APP.lm.realtime, function () {
var driveRt = Cryptpad.getStore().getProxy().info.realtime;
Cryptpad.whenRealtimeSyncs(driveRt, function () {
displayAvatar();
});
});
});
};
if (old) {
var oldChanId = Cryptpad.hrefToHexChannelId(old);
Cryptpad.unpinPads([oldChanId], function (e) {
if (e) { Cryptpad.log(e); }
todo();
});
return;
}
todo();
}
};
APP.FM = Cryptpad.createFileManager(fmConfig);
var data = {
FM: APP.FM,
filter: function (file) {
var sizeMB = Cryptpad.bytesToMegabytes(file.size);
var type = file.type;
return sizeMB <= 0.5 && allowedMediaTypes.indexOf(type) !== -1;
},
accept: ".gif,.jpg,.jpeg,.png"
};
var $upButton = Cryptpad.createButton('upload', false, data);
$upButton.text(Messages.profile_upload);
$upButton.prepend($('<span>', {'class': 'fa fa-upload'}));
$block.append($upButton);
};
var addDescription = function ($container) {
var $block = $('<div>', {id: DESCRIPTION_ID}).appendTo($container);
if (APP.readOnly) {
if (!(APP.lm.proxy.description || "").trim()) { return void $block.hide(); }
var $div = $('<div>', {'class': 'rendered'}).appendTo($block);
var val = Marked(APP.lm.proxy.description);
$div.html(val);
return;
}
$('<h3>').text(Messages.profile_description).insertBefore($block);
var $ok = $('<span>', {'class': 'ok fa fa-check', title: Messages.saved}).appendTo($block);
var $spinner = $('<span>', {'class': 'spin fa fa-spinner fa-pulse'}).appendTo($block);
var $textarea = $('<textarea>').val(APP.lm.proxy.description || '');
$block.append($textarea);
var editor = APP.editor = CodeMirror.fromTextArea($textarea[0], {
lineNumbers: true,
lineWrapping: true,
styleActiveLine : true,
mode: "markdown",
});
var onLocal = function () {
$ok.hide();
$spinner.show();
var val = editor.getValue();
APP.lm.proxy.description = val;
Cryptpad.whenRealtimeSyncs(APP.lm.realtime, function () {
$ok.show();
$spinner.hide();
});
};
editor.on('change', onLocal);
};
var addPublicKey = function ($container) {
var $block = $('<div>', {id: PUBKEY_ID});
$container.append($block);
};
var createLeftside = function () {
var $categories = $('<div>', {'class': 'categories'}).appendTo(APP.$leftside);
APP.$usage = $('<div>', {'class': 'usage'}).appendTo(APP.$leftside);
var $category = $('<div>', {'class': 'category'}).appendTo($categories);
$category.append($('<span>', {'class': 'fa fa-user'}));
$category.addClass('active');
$category.append(Messages.profileButton);
};
var createToolbar = function () {
var displayed = ['useradmin', 'newpad', 'limit', 'upgrade', 'pageTitle'];
var configTb = {
displayed: displayed,
ifrw: window,
common: Cryptpad,
$container: APP.$toolbar,
pageTitle: Messages.profileButton
};
var toolbar = APP.toolbar = Toolbar.create(configTb);
toolbar.$rightside.html(''); // Remove the drawer if we don't use it to hide the toolbar
};
var onReady = function () {
APP.$container.find('#'+CREATE_ID).remove();
var obj = APP.lm && APP.lm.proxy;
if (!APP.readOnly) {
var pubKeys = Cryptpad.getPublicKeys();
if (pubKeys && pubKeys.curve) {
obj.curveKey = pubKeys.curve;
obj.edKey = pubKeys.ed;
}
}
if (!APP.initialized) {
var $header = $('<div>', {id: HEADER_ID}).appendTo(APP.$rightside);
addAvatar($header);
var $rightside = $('<div>', {id: HEADER_RIGHT_ID}).appendTo($header);
addDisplayName($rightside);
addLink($rightside);
addDescription(APP.$rightside);
addViewButton(APP.$rightside); //$rightside);
addPublicKey(APP.$rightside);
APP.initialized = true;
createLeftside();
}
Cryptpad.removeLoadingScreen();
};
var onInit = function () {
};
var onDisconnect = function () {};
var onChange = function () {};
var andThen = function (profileHash) {
var secret = Cryptpad.getSecrets('profile', profileHash);
var readOnly = APP.readOnly = secret.keys && !secret.keys.editKeyStr;
var listmapConfig = {
data: {},
websocketURL: Cryptpad.getWebsocketURL(),
channel: secret.channel,
readOnly: readOnly,
validateKey: secret.keys.validateKey || undefined,
crypto: Crypto.createEncryptor(secret.keys),
userName: 'profile',
logLevel: 1,
};
var lm = APP.lm = Listmap.create(listmapConfig);
lm.proxy.on('create', onInit)
.on('ready', onReady)
.on('disconnect', onDisconnect)
.on('change', [], onChange);
};
var getOrCreateProfile = function () {
var obj = Cryptpad.getStore().getProxy().proxy;
if (obj.profile && obj.profile.view && obj.profile.edit) {
return void andThen(obj.profile.edit);
}
// If the user doesn't have a public profile, ask them if they want to create one
var todo = function () {
var secret = Cryptpad.getSecrets();
obj.profile = {};
var channel = Cryptpad.createChannelId();
Cryptpad.pinPads([channel], function (e) {
if (e) {
if (e === 'E_OVER_LIMIT') {
Cryptpad.alert(Messages.pinLimitNotPinned, null, true);
}
return void Cryptpad.log(Messages._getKey('profile_error', [e]));
}
obj.profile.edit = Cryptpad.getEditHashFromKeys(channel, secret.keys);
obj.profile.view = Cryptpad.getViewHashFromKeys(channel, secret.keys);
andThen(obj.profile.edit);
});
};
Cryptpad.removeLoadingScreen();
if (!Cryptpad.isLoggedIn()) {
var $p = $('<p>', {id: CREATE_ID}).append(Messages.profile_register);
var $a = $('<a>', {
href: '/register/'
});
$('<button>', {
'class': 'btn btn-success',
}).text(Messages.login_register).appendTo($a);
$p.append($('<br>')).append($a);
APP.$rightside.append($p);
return;
}
// make an empty profile for the user on their first visit
todo();
};
var onCryptpadReady = function () {
APP.$leftside = $('<div>', {id: 'leftSide'}).appendTo(APP.$container);
APP.$rightside = $('<div>', {id: 'rightSide'}).appendTo(APP.$container);
createToolbar();
if (window.location.hash) {
return void andThen(window.location.hash.slice(1));
}
getOrCreateProfile();
};
$(function () {
$(window).click(function () {
$('.cp-dropdown-content').hide();
});
APP.$container = $('#container');
APP.$toolbar = $('#toolbar');
Cryptpad.ready(function () {
Cryptpad.reportAppUsage();
onCryptpadReady();
});
});
});

@ -0,0 +1,378 @@
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;
}
}

@ -0,0 +1,141 @@
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;
}
}

@ -0,0 +1,249 @@
/* 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);
})();

@ -0,0 +1,49 @@
<!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>

@ -0,0 +1,25 @@
/*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);
})();

@ -0,0 +1,270 @@
(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);

@ -0,0 +1,52 @@
/*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);

@ -0,0 +1,120 @@
(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);

@ -0,0 +1,141 @@
/*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);

@ -0,0 +1,114 @@
/*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);

@ -0,0 +1,219 @@
/*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));

@ -0,0 +1,30 @@
<!DOCTYPE html>
<html class="cp pad">
<head>
<title>CryptPad</title>
<meta content="text/html; charset=utf-8" http-equiv="content-type"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script async data-bootload="/customize/template.js" data-main="/common/boot.js?ver=1.0" src="/bower_components/requirejs/require.js?ver=2.3.5"></script>
<style>
html, body {
margin: 0px;
padding: 0px;
}
#pad-iframe {
position:fixed;
top:0px;
left:0px;
bottom:0px;
right:0px;
width:100%;
height:100%;
border:none;
margin:0;
padding:0;
overflow:hidden;
}
</style>
</head>
<body>
<iframe id="pad-iframe"></iframe><script src="/common/noscriptfix.js"></script>

@ -0,0 +1,20 @@
<!DOCTYPE html>
<html>
<head>
<meta content="text/html; charset=utf-8" http-equiv="content-type"/>
<script src="/bower_components/jquery/dist/jquery.min.js"></script>
<script async data-bootload="/todo/inner.js" data-main="/common/boot.js?ver=1.0" src="/bower_components/requirejs/require.js?ver=2.3.5"></script>
<style>.loading-hidden, .loading-hidden * {display: none !important;}</style>
</head>
<body class="loading-hidden">
<div id="toolbar" class="toolbar-container"></div>
<div id="container">
<div class="cp-create-form">
<input type="text" id="newTodoName" data-localization-placeholder="todo_newTodoNamePlaceholder" />
<button class="btn btn-success fa fa-plus" data-localization-title="todo_newTodoNameTitle"></button>
</div>
<div id="tasksList"></div>
</div>
</body>
</html>

@ -0,0 +1,15 @@
define([
'jquery',
'less!/bower_components/components-font-awesome/css/font-awesome.min.css',
'css!/bower_components/bootstrap/dist/css/bootstrap.min.css',
'less!/todo/todo.less',
//'less!/customize/src/less/cryptpad.less',
'less!/customize/src/less/toolbar.less',
], function ($) {
$('.loading-hidden').removeClass('loading-hidden');
// dirty hack to get rid the flash of the lock background
/*
setTimeout(function () {
$('#app').addClass('ready');
}, 100);*/
});

@ -0,0 +1,229 @@
define([
'jquery',
'/bower_components/chainpad-crypto/crypto.js',
'/bower_components/chainpad-listmap/chainpad-listmap.js',
'/common/toolbar2.js',
'/common/cryptpad-common.js',
'/todo/todo.js',
//'/common/media-tag.js',
//'/bower_components/file-saver/FileSaver.min.js',
'less!/bower_components/components-font-awesome/css/font-awesome.min.css',
'less!/customize/src/less/cryptpad.less',
], function ($, Crypto, Listmap, Toolbar, Cryptpad, Todo) {
var Messages = Cryptpad.Messages;
var APP = window.APP = {};
$(function () {
var $iframe = $('#pad-iframe').contents();
var $body = $iframe.find('body');
var ifrw = $('#pad-iframe')[0].contentWindow;
var $list = $iframe.find('#tasksList');
var removeTips = function () {
Cryptpad.clearTooltips();
};
var onReady = function () {
var todo = Todo.init(APP.lm.proxy, Cryptpad);
var deleteTask = function(id) {
todo.remove(id);
var $els = $list.find('.cp-task').filter(function (i, el) {
return $(el).data('id') === id;
});
$els.fadeOut(null, function () {
$els.remove();
removeTips();
});
//APP.display();
};
// TODO make this actually work, and scroll to bottom...
var scrollTo = function (t) {
var $list = $iframe.find('#tasksList');
$list.animate({
scrollTop: t,
});
};
scrollTo = scrollTo;
var makeCheckbox = function (id, cb) {
var entry = APP.lm.proxy.data[id];
var checked = entry.state === 1? 'cp-task-checkbox-checked fa-check-square-o': 'cp-task-checkbox-unchecked fa-square-o';
var title = entry.state === 1?
Messages.todo_markAsIncompleteTitle:
Messages.todo_markAsCompleteTitle;
title = title;
removeTips();
return $('<span>', {
'class': 'cp-task-checkbox fa ' + checked,
//title: title,
}).on('click', function () {
entry.state = (entry.state + 1) % 2;
if (typeof(cb) === 'function') {
cb(entry.state);
}
});
};
var addTaskUI = function (el, animate) {
var $taskDiv = $('<div>', {
'class': 'cp-task'
});
if (animate) {
$taskDiv.prependTo($list);
} else {
$taskDiv.appendTo($list);
}
$taskDiv.data('id', el);
makeCheckbox(el, function (/*state*/) {
APP.display();
})
.appendTo($taskDiv);
var entry = APP.lm.proxy.data[el];
if (entry.state) {
$taskDiv.addClass('cp-task-complete');
}
$('<span>', { 'class': 'cp-task-text' })
.text(entry.task)
.appendTo($taskDiv);
/*$('<span>', { 'class': 'cp-task-date' })
.text(new Date(entry.ctime).toLocaleString())
.appendTo($taskDiv);*/
$('<button>', {
'class': 'fa fa-times cp-task-remove btn btn-danger',
title: Messages.todo_removeTaskTitle,
}).appendTo($taskDiv).on('click', function() {
deleteTask(el);
});
if (animate) {
$taskDiv.hide();
window.setTimeout(function () {
// ???
$taskDiv.fadeIn();
}, 0);
}
removeTips();
};
var display = APP.display = function () {
$list.empty();
removeTips();
APP.lm.proxy.order.forEach(function (el) {
addTaskUI(el);
});
//scrollTo('300px');
};
var addTask = function () {
var $input = $iframe.find('#newTodoName');
// if the input is empty after removing leading and trailing spaces
// don't create a new entry
if (!$input.val().trim()) { return; }
var obj = {
"state": 0,
"task": $input.val(),
"ctime": +new Date(),
"mtime": +new Date()
};
var id = Cryptpad.createChannelId();
todo.add(id, obj);
$input.val("");
addTaskUI(id, true);
//display();
};
var $formSubmit = $iframe.find('.cp-create-form button').on('click', addTask);
$iframe.find('#newTodoName').on('keypress', function (e) {
switch (e.which) {
case 13:
$formSubmit.click();
break;
default:
console.log(e.which);
}
}).focus();
var editTask = function () {
};
editTask = editTask;
display();
Cryptpad.removeLoadingScreen();
};
var onInit = function () {
Cryptpad.addLoadingScreen();
$body.on('dragover', function (e) { e.preventDefault(); });
$body.on('drop', function (e) { e.preventDefault(); });
var Title;
var $bar = $iframe.find('.toolbar-container');
Title = Cryptpad.createTitle({}, function(){}, Cryptpad);
var configTb = {
displayed: ['useradmin', 'newpad', 'limit', 'upgrade', 'pageTitle'],
ifrw: ifrw,
common: Cryptpad,
//hideDisplayName: true,
$container: $bar,
pageTitle: Messages.todo_title
};
APP.toolbar = Toolbar.create(configTb);
APP.toolbar.$rightside.html(''); // Remove the drawer if we don't use it to hide the toolbar
};
var createTodo = function() {
var obj = Cryptpad.getProxy();
var hash = Cryptpad.createRandomHash();
if(obj.todo) {
hash = obj.todo;
} else {
obj.todo = hash;
}
var secret = Cryptpad.getSecrets('todo', hash);
var listmapConfig = {
data: {},
websocketURL: Cryptpad.getWebsocketURL(),
channel: secret.channel,
validateKey: secret.keys.validateKey || undefined,
crypto: Crypto.createEncryptor(secret.keys),
userName: 'todo',
logLevel: 1,
};
var lm = APP.lm = Listmap.create(listmapConfig);
lm.proxy.on('create', onInit)
.on('ready', onReady);
};
Cryptpad.ready(function () {
createTodo();
Cryptpad.reportAppUsage();
});
});
});

@ -0,0 +1,83 @@
define([
], function () {
var Todo = {};
var Cryptpad;
/* data model
{
"order": [
"123456789abcdef0",
"23456789abcdef01",
"0123456789abcedf"
],
"data": {
"0123456789abcedf": {
"state": 0, // used to sort completed elements
"task": "pewpewpew",
"ctime": +new Date(), // used to display chronologically
"mtime": +new Date(), // used to display recent actions
// "deadline": +new Date() + 1000 * 60 * 60 * 24 * 7
},
"123456789abcdef0": {},
"23456789abcdef01": {}
}
}
*/
var val = function (proxy, id, k, v) {
var el = proxy.data[id];
if (!el) {
throw new Error('expected an element');
}
if (typeof(v) === 'function') { el[k] = v(el[k]); }
else { el[k] = v; }
return el[k];
};
var initialize = function (proxy) {
// run migration
if (typeof(proxy.data) !== 'object') { proxy.data = {}; }
if (!Array.isArray(proxy.order)) { proxy.order = []; }
if (typeof(proxy.type) !== 'string') { proxy.type = 'todo'; }
};
/* add (id, obj) push id to order, add object to data */
var add = function (proxy, id, obj) {
if (!Array.isArray(proxy.order)) {
throw new Error('expected an array');
}
proxy.order.unshift(id);
proxy.data[id] = obj;
};
/* delete (id) remove id from order, delete id from data */
var remove = function (proxy, id) {
if (Array.isArray(proxy.order)) {
var i = proxy.order.indexOf(id);
proxy.order.splice(i, 1);
}
if (proxy.data[id]) { delete proxy.data[id]; }
};
Todo.init = function (proxy, common) {
Cryptpad = common;
var api = {};
initialize(proxy);
api.val = function (id, k, v) {
return val(proxy, id, k, v);
};
api.add = function (id, obj) {
return add(proxy, id, obj);
};
api.remove = function (id) {
return remove(proxy, id);
};
return api;
};
return Todo;
});

@ -1,3 +1,7 @@
// Defaults to avoid breaking existing themes
@colortheme_pad-toolbar-bg: #c1e7ff;
@import (once) "../../customize/src/less2/include/toolbar.less";
@import (once) '../../customize/src/less2/include/alertify.less';
@import (once) '../../customize/src/less2/include/tokenfield.less';
@ -16,7 +20,7 @@
#cke_1_toolbox {
display: inline-block;
width: 100%;
background-color: #c1e7ff;
background-color: @colortheme_pad-toolbar-bg;
}
#cke_1_toolbox .cke_toolbar {
height: 28px;

@ -30,6 +30,7 @@ define([
'/api/config',
'/common/common-hash.js',
'/common/common-util.js',
'/bower_components/chainpad-json-validator/json-ot.js',
'/bower_components/diff-dom/diffDOM.js',
@ -48,7 +49,8 @@ define([
MediaTag,
ApiConfig,
Hash,
Util)
Util,
JsonOT)
{
var DiffDom = window.diffDOM;
@ -552,7 +554,30 @@ define([
nThen(function (waitFor) {
Framework.create({
toolbarContainer: '#cke_1_toolbox',
contentContainer: '#cke_1_contents'
contentContainer: '#cke_1_contents',
transformFunction: JsonOT.validate,
thumbnail: {
getContainer: function () { return $('iframe').contents().find('html')[0]; },
filter: function (el, before) {
if (before) {
module.cursor.update();
$(el).parents().css('overflow', 'visible');
$(el).css('max-width', '1200px');
$(el).css('max-height', Math.max(600, $(el).width()) + 'px');
$(el).css('overflow', 'hidden');
$(el).find('body').css('background-color', 'transparent');
return;
}
$(el).parents().css('overflow', '');
$(el).css('max-width', '');
$(el).css('max-height', '');
$(el).css('overflow', '');
$(el).find('body').css('background-color', '#fff');
var sel = module.cursor.makeSelection();
var range = module.cursor.makeRange();
module.cursor.fixSelection(sel, range);
}
}
}, waitFor(function (fw) { window.APP.framework = framework = fw; }));
nThen(function (waitFor) {

@ -1,3 +1,9 @@
// Defaults to avoid breaking existing themes
@colortheme_poll-th-bg: #005bef;
@colortheme_poll-th-fg: #fff;
@colortheme_poll-help-bg: #bbffbb;
@import (once) "../../customize/src/less2/include/browser.less";
@import (once) "../../customize/src/less2/include/toolbar.less";
@import (once) "../../customize/src/less2/include/markdown.less";
@ -14,15 +20,15 @@
@poll-fore: #555;
@poll-th-bg: #005bef;
@poll-th-fg: #fff;
@poll-th-bg: @colortheme_poll-th-bg;
@poll-th-fg: @colortheme_poll-th-fg;
@poll-th-user-bg: darken(@poll-th-bg, 10%);
@poll-editing: lighten(@poll-th-bg, 10%);
@poll-winner: darken(@poll-th-bg, 15%);
@poll-td-bg: @poll-th-bg;
@poll-td-fg: @poll-th-fg;
@poll-help-bg: #bbffbb; // lightgreen
@poll-help-bg: @colortheme_poll-help-bg;
@poll-uncommitted-cell: #eee;
@poll-uncommitted-bg: #ddd; //lighten(@poll-th-bg, 50%);

@ -14,12 +14,15 @@ define([
'/poll/render.js',
'/common/diffMarked.js',
'/common/sframe-common-codemirror.js',
'/common/sframe-common-interface.js',
'/common/common-thumbnail.js',
'cm/lib/codemirror',
'cm/addon/display/placeholder',
'cm/mode/markdown/markdown',
'css!cm/lib/codemirror.css',
'/bower_components/file-saver/FileSaver.min.js',
'css!/bower_components/bootstrap/dist/css/bootstrap.min.css',
@ -41,6 +44,8 @@ define([
Renderer,
DiffMd,
SframeCM,
SFUI,
Thumb,
CMeditor)
{
var Messages = Cryptpad.Messages;
@ -651,7 +656,7 @@ define([
if (editable === false) {
// disable all the things
$('.icp-app-poll-realtime input, .cp-app-poll-realtime button, .cp-app-poll-upper button, .cp-app-poll-realtime textarea').attr('disabled', true);
$('.cp-app-poll-realtime input, .cp-app-poll-realtime button, .cp-app-poll-upper button, .cp-app-poll-realtime textarea').attr('disabled', true);
$('span.cp-app-poll-table-edit, span.cp-app-poll-table-remove').hide();
$('span.cp-app-poll-table-lock').addClass('fa-lock').removeClass('fa-unlock')
.attr('title', Messages.poll_locked)
@ -682,6 +687,8 @@ define([
var getCommentId = Render.Uid('c');
var removeComment = function (uid) {
var idx = APP.proxy.commentsOrder.indexOf(uid);
if (idx !== -1) { APP.proxy.commentsOrder.splice(idx, 1); }
delete APP.proxy.comments[uid];
APP.updateComments();
};
@ -691,6 +698,7 @@ define([
var avatars = {};
var updateComments = APP.updateComments = function () {
if (!APP.proxy.comments) { APP.proxy.comments = {}; }
if (!APP.proxy.commentsOrder) { APP.proxy.commentsOrder = []; }
var profile;
if (common.isLoggedIn()) {
@ -699,10 +707,9 @@ define([
var $comments = APP.$comments.html('');
var comments = APP.proxy.comments;
Object.keys(copyObject(comments)).sort(function (a, b) {
return comments[a].time > comments[b].time;
}).forEach(function (k) {
APP.proxy.commentsOrder.forEach(function (k) {
var c = comments[k];
if (!c) { return; }
var name = c.name || Messages.anonymous;
var $c = $('<div>', {
'class': 'cp-app-poll-comments-list-el'
@ -766,6 +773,7 @@ define([
};
var addComment = function () {
if (!APP.proxy.comments) { APP.proxy.comments = {}; }
if (!APP.proxy.commentsOrder) { APP.proxy.commentsOrder = []; }
var name = APP.$addComment.find('.cp-app-poll-comments-add-name').val().trim();
var msg = APP.$addComment.find('.cp-app-poll-comments-add-msg').val().trim();
var time = +new Date();
@ -779,6 +787,7 @@ define([
}
var uid = getCommentId();
APP.proxy.commentsOrder.push(uid);
APP.proxy.comments[uid] = {
msg: msg,
name: name,
@ -790,6 +799,47 @@ define([
updateComments();
};
var initThumbnails = function () {
var privateDat = metadataMgr.getPrivateData();
if (!privateDat.thumbnails) { return; } // Thumbnails are disabled
var hash = privateDat.availableHashes.editHash ||
privateDat.availableHashes.viewHash;
if (!hash) { return; }
var href = privateDat.pathname + '#' + hash;
var $el = $('.cp-app-poll-realtime');
//var $el = $('#cp-app-poll-table');
var scrollTop;
var options = {
getContainer: function () { return $el[0]; },
filter: function (el, before) {
if (before) {
$el.parents().css('overflow', 'visible');
scrollTop = $('#cp-app-poll-form').scrollTop();
$el.css('max-height', Math.max(600, $(el).width()) + 'px');
$el.find('tr td:first-child, tr td:last-child, tr td:nth-last-child(2)')
.css('position', 'static');
$el.find('#cp-app-poll-comments').css('display', 'none');
$el.find('#cp-app-poll-table-container').css('text-align', 'center');
$el.find('#cp-app-poll-table-scroll').css('margin', 'auto');
$el.find('#cp-app-poll-table-scroll').css('max-width', '100%');
return;
}
$el.parents().css('overflow', '');
$el.css('max-height', '');
$el.find('#cp-app-poll-comments').css('display', '');
$el.find('#cp-app-poll-table-container').css('text-align', '');
$el.find('#cp-app-poll-table-scroll').css('margin', '');
$el.find('#cp-app-poll-table-scroll').css('max-width', '');
$el.find('tr td:first-child, tr td:last-child, tr td:nth-last-child(2)')
.css('position', '');
$('#cp-app-poll-form').scrollTop(scrollTop);
},
href: href,
getContent: function () { return JSON.stringify(APP.proxy.content); }
};
Thumb.initPadThumbnails(options);
};
var checkDeletedCells = function () {
// faster than forEach?
var c;
@ -810,17 +860,23 @@ define([
if (!isNew) {
if (proxy.info) {
// Migration??
// Migration
proxy.metadata = proxy.info;
delete proxy.info;
}
if (proxy.table) {
// Migration??
// Migration
proxy.content = proxy.table;
delete proxy.table;
}
checkDeletedCells();
if (proxy.comments && !proxy.commentsOrder) { // Migration
proxy.commentsOrder = Object.keys(copyObject(proxy.comments)).sort(function (a, b) {
return proxy.comments[a].time > proxy.comments[b].time;
});
}
if (proxy && proxy.metadata) {
metadataMgr.updateMetadata(proxy.metadata);
}
@ -938,6 +994,7 @@ define([
var $table = APP.$table = $('#cp-app-poll-table-scroll').find('table');
updateDisplayedTable();
updateDescription(null, APP.proxy.description || '');
initThumbnails();
// Initialize author name for comments.
// Disable name modification for logged in users
@ -1149,6 +1206,17 @@ define([
mode: "markdown",
});
APP.$descriptionPublished.click(function (e) {
if (!e.target) { return; }
var $t = $(e.target);
if ($t.is('a') || $t.parents('a').length) {
e.preventDefault();
var $a = $t.is('a') ? $t : $t.parents('a').first();
var href = $a.attr('href');
if (!href) { return; }
window.open(href);
}
});
var listmapConfig = {
data: {},

@ -0,0 +1,160 @@
@import (once) "../../customize/src/less2/include/browser.less";
@import (once) "../../customize/src/less2/include/toolbar.less";
@import (once) "../../customize/src/less2/include/markdown.less";
@import (once) '../../customize/src/less2/include/fileupload.less';
@import (once) '../../customize/src/less2/include/alertify.less';
//@import (once) '../../customize/src/less/mixins.less';
//@import (once) '../../customize/src/less/variables.less";
@import (once) '../../customize/src/less2/include/avatar.less';
@import (once) '../../customize/src/less2/include/sidebar-layout.less';
.toolbar_main();
.fileupload_main();
.alertify_main();
.sidebar-layout_main();
// body
&.cp-app-profile {
display: flex;
flex-flow: column;
#cp-app-profile-header {
display: flex;
#cp-app-profile-rightside {
flex: 1;
display: flex;
flex-flow: column;
}
}
#cp-app-profile-avatar {
width: 300px;
//height: 350px;
margin: 10px;
margin-right: 20px;
text-align: center;
&> span {
display: inline-block;
text-align: center;
height: 300px;
width: 300px;
border: 1px solid black;
border-radius: 4px;
overflow: hidden;
position: relative;
.cp-app-profile-avatar-delete {
right: 0;
position: absolute;
opacity: 0.7;
&:hover {
opacity: 1;
}
}
}
img {
max-width: 100%;
max-height: 100%;
vertical-align: top;
}
media-tag {
height: 100%;
width: 100%;
display: inline-flex;
justify-content: center;
align-items: center;
img {
min-width: 100%;
min-height: 100%;
max-width: none;
max-height: none;
flex: 1;
}
}
button {
height: 40px;
margin: 5px;
}
}
#cp-app-profile-displayname, #cp-app-profile-link {
width: 100%;
height: 40px;
margin: 10px 0;
input {
width: 100%;
font-size: 20px;
box-sizing: border-box;
padding-right: 30px;
}
input:focus ~ .edit {
display: none;
}
.cp-app-profile-input-edit {
position: absolute;
margin-left: -25px;
margin-top: 8px;
}
.cp-app-profile-input-temp {
font-weight: 400;
font-family: sans-serif;
}
.cp-app-profile-displayname {
font-weight: bold;
font-size: 30px;
}
.cp-app-profile-link {
font-size: 25px;
}
.cp-app-profile-displayname, .cp-app-profile-link {
line-height: 40px;
}
}
// I tried using flexbox but messed with how the pencil icon was displayed
#cp-app-profile-invite-button {
float: right;
}
#cp-app-profile-viewprofile-button {
margin-bottom: 20px;
float: right;
}
#cp-app-profile-description {
position: relative;
font-size: 16px;
border: 1px solid #DDD;
margin-bottom: 20px;
.cp-app-profile-description-rendered {
padding: 0 15px;
}
.cp-app-profile-description-ok, .cp-app-profile-description-spin {
position: absolute;
top: 2px;
right: 2px;
display: none;
z-index: 1000;
}
textarea {
width: 100%;
height: 300px;
}
.CodeMirror {
border: 1px solid #DDD;
font-family: monospace;
font-size: 16px;
line-height: initial;
pre {
margin: 0;
font-family: inherit;
font-size: inherit;
line-height: inherit;
}
}
}
#cp-app-profile-create {
height: 100%;
display: flex;
flex-flow: column;
align-items: center;
justify-content: center;
}
}

@ -1,20 +1,38 @@
<!DOCTYPE html>
<html class="cp">
<!-- If this file is not called customize.dist/src/template.html, it is generated -->
<html>
<head>
<title data-localization="main_title">CryptPad: Zero Knowledge, Collaborative Real Time Editing</title>
<title>CryptPad</title>
<meta content="text/html; charset=utf-8" http-equiv="content-type"/>
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<link rel="icon" type="image/png" href="/customize/main-favicon.png" id="favicon"/>
<script async data-bootload="/customize/template.js" data-main="/common/boot.js?ver=1.0" src="/bower_components/requirejs/require.js?ver=2.3.5"></script>
<link rel="stylesheet" href="/bower_components/codemirror/lib/codemirror.css">
<link rel="stylesheet" href="/bower_components/codemirror/addon/dialog/dialog.css">
<link rel="stylesheet" href="/bower_components/codemirror/addon/fold/foldgutter.css" />
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="referrer" content="no-referrer" />
<script async data-bootload="main.js" data-main="/common/boot.js?ver=1.0" src="/bower_components/requirejs/require.js?ver=2.3.5"></script>
<style>
html, body {
margin: 0px;
padding: 0px;
}
#sbox-iframe {
position:fixed;
top:0px;
left:0px;
bottom:0px;
right:0px;
width:100%;
height:100%;
border:none;
margin:0;
padding:0;
overflow:hidden;
}
#sbox-filePicker-iframe {
position: fixed;
top:0; left:0;
bottom:0; right:0;
width:100%;
height: 100%;
border: 0;
}
</style>
</head>
<body class="html">
<noscript>
<p><strong>OOPS</strong> In order to do encryption in your browser, Javascript is really <strong>really</strong> required.</p>
<p><strong>OUPS</strong> Afin de pouvoir réaliser le chiffrement dans votre navigateur, Javascript est <strong>vraiment</strong> nécessaire.</p>
</noscript>
</html>
<body>
<iframe id="sbox-iframe">

@ -0,0 +1,18 @@
<!DOCTYPE html>
<html class="cp-app-noscroll">
<head>
<meta content="text/html; charset=utf-8" http-equiv="content-type"/>
<script async data-bootload="/profile/inner.js" data-main="/common/sframe-boot.js?ver=1.4" src="/bower_components/requirejs/require.js?ver=2.3.5"></script>
<style>
.loading-hidden { display: none; }
</style>
</head>
<body class="cp-app-profile">
<div id="cp-toolbar" class="cp-toolbar-container"></div>
<div id="cp-sidebarlayout-container"></div>
<noscript>
<p><strong>OOPS</strong> In order to do encryption in your browser, Javascript is really <strong>really</strong> required.</p>
<p><strong>OUPS</strong> Afin de pouvoir réaliser le chiffrement dans votre navigateur, Javascript est <strong>vraiment</strong> nécessaire.</p>
</noscript>
</body>

@ -0,0 +1,479 @@
define([
'jquery',
'/bower_components/chainpad-crypto/crypto.js',
'/common/sframe-chainpad-listmap.js',
'/common/toolbar3.js',
'/common/cryptpad-common.js',
'/bower_components/nthen/index.js',
'/common/sframe-common.js',
'/bower_components/marked/marked.min.js',
'cm/lib/codemirror',
'cm/mode/markdown/markdown',
'css!/bower_components/codemirror/lib/codemirror.css',
'css!/bower_components/codemirror/addon/dialog/dialog.css',
'css!/bower_components/codemirror/addon/fold/foldgutter.css',
'css!/bower_components/bootstrap/dist/css/bootstrap.min.css',
'less!/bower_components/components-font-awesome/css/font-awesome.min.css',
'less!/customize/src/less2/main.less',
], function (
$,
Crypto,
Listmap,
Toolbar,
Cryptpad,
nThen,
SFCommon,
Marked,
CodeMirror
)
{
var Messages = Cryptpad.Messages;
var APP = window.APP = {
Cryptpad: Cryptpad,
_onRefresh: []
};
var onConnectError = function () {
Cryptpad.errorLoadingScreen(Messages.websocketError);
};
// Decryption event for avatar mediatag (TODO not needed anymore?)
$(window.document).on('decryption', function (e) {
var decrypted = e.originalEvent;
if (decrypted.callback) { decrypted.callback(); }
})
.on('decryptionError', function (e) {
var error = e.originalEvent;
Cryptpad.alert(error.message);
});
$(window).click(function () {
$('.cp-dropdown-content').hide();
});
// Marked
var renderer = new Marked.Renderer();
Marked.setOptions({
renderer: renderer,
sanitize: true
});
// Tasks list
var checkedTaskItemPtn = /^\s*\[x\]\s*/;
var uncheckedTaskItemPtn = /^\s*\[ \]\s*/;
renderer.listitem = function (text) {
var isCheckedTaskItem = checkedTaskItemPtn.test(text);
var isUncheckedTaskItem = uncheckedTaskItemPtn.test(text);
if (isCheckedTaskItem) {
text = text.replace(checkedTaskItemPtn,
'<i class="fa fa-check-square" aria-hidden="true"></i>&nbsp;') + '\n';
}
if (isUncheckedTaskItem) {
text = text.replace(uncheckedTaskItemPtn,
'<i class="fa fa-square-o" aria-hidden="true"></i>&nbsp;') + '\n';
}
var cls = (isCheckedTaskItem || isUncheckedTaskItem) ? ' class="todo-list-item"' : '';
return '<li'+ cls + '>' + text + '</li>\n';
};
var DISPLAYNAME_ID = "cp-app-profile-displayname";
var LINK_ID = "cp-app-profile-link";
var AVATAR_ID = "cp-app-profile-avatar";
var DESCRIPTION_ID = "cp-app-profile-description";
var PUBKEY_ID = "cp-app-profile-pubkey";
var CREATE_ID = "cp-app-profile-create";
var HEADER_ID = "cp-app-profile-header";
var HEADER_RIGHT_ID = "cp-app-profile-rightside";
var CREATE_INVITE_BUTTON = 'cp-app-profile-invite-button'; /* jshint ignore: line */
var VIEW_PROFILE_BUTTON = 'cp-app-profile-viewprofile-button';
var common;
var sFrameChan;
var createEditableInput = function ($block, name, ph, getValue, setValue, fallbackValue) {
fallbackValue = fallbackValue || ''; // don't ever display 'null' or 'undefined'
var lastVal;
getValue(function (value) {
lastVal = value;
var $input = $('<input>', {
'id': name+'Input',
placeholder: ph
}).val(value);
var $icon = $('<span>', {'class': 'fa fa-pencil cp-app-profile-input-edit'});
var editing = false;
var todo = function () {
if (editing) { return; }
editing = true;
var newVal = $input.val().trim();
if (newVal === lastVal) {
editing = false;
return;
}
setValue(newVal, function (err) {
if (err) { return void console.error(err); }
lastVal = newVal;
Cryptpad.log(Messages._getKey('profile_fieldSaved', [newVal || fallbackValue]));
editing = false;
});
};
$input.on('keyup', function (e) {
if (e.which === 13) { return void todo(); }
if (e.which === 27) {
$input.val(lastVal);
}
});
$icon.click(function () { $input.focus(); });
$input.focus(function () {
$input.width('');
});
$input.focusout(todo);
$block.append($input).append($icon);
});
};
/* jshint ignore:start */
var isFriend = function (proxy, edKey) {
var friends = Cryptpad.find(proxy, ['friends']);
return typeof(edKey) === 'string' && friends && (edKey in friends);
};
var addCreateInviteLinkButton = function ($container) {
return;
var obj = APP.lm.proxy;
var proxy = Cryptpad.getProxy();
var userViewHash = Cryptpad.find(proxy, ['profile', 'view']);
var edKey = obj.edKey;
var curveKey = obj.curveKey;
if (!APP.readOnly || !curveKey || !edKey || userViewHash === window.location.hash.slice(1) || isFriend(proxy, edKey)) {
//console.log("edit mode or missing curve key, or you're viewing your own profile");
return;
}
// sanitize user inputs
var unsafeName = obj.name || '';
console.log(unsafeName);
var name = Cryptpad.fixHTML(unsafeName) || Messages.anonymous;
console.log(name);
console.log("Creating invite button");
$("<button>", {
id: CREATE_INVITE_BUTTON,
title: Messages.profile_inviteButtonTitle,
})
.addClass('btn btn-success')
.text(Messages.profile_inviteButton)
.click(function () {
Cryptpad.confirm(Messages._getKey('profile_inviteExplanation', [name]), function (yes) {
if (!yes) { return; }
console.log(obj.curveKey);
Cryptpad.alert("TODO");
// TODO create a listmap object using your curve keys
// TODO fill the listmap object with your invite data
// TODO generate link to invite object
// TODO copy invite link to clipboard
}, null, true);
})
.appendTo($container);
};
/* jshint ignore:end */
var addViewButton = function ($container) {
if (APP.readOnly) {
return;
}
var hash = common.getMetadataMgr().getPrivateData().availableHashes.viewHash;
var url = APP.origin + '/profile/#' + hash;
var $button = $('<button>', {
'class': 'btn btn-success',
id: VIEW_PROFILE_BUTTON,
})
.text(Messages.profile_viewMyProfile)
.click(function () {
window.open(url, '_blank');
});
$container.append($button);
};
var addDisplayName = function ($container) {
var $block = $('<div>', {id: DISPLAYNAME_ID}).appendTo($container);
var getValue = function (cb) {
cb(APP.lm.proxy.name);
};
var placeholder = Messages.profile_namePlaceholder;
if (APP.readOnly) {
var $span = $('<span>', {'class': DISPLAYNAME_ID}).appendTo($block);
getValue(function (value) {
$span.text(value || Messages.anonymous);
});
//addCreateInviteLinkButton($block);
return;
}
var setValue = function (value, cb) {
APP.lm.proxy.name = value;
Cryptpad.whenRealtimeSyncs(APP.lm.realtime, cb);
};
createEditableInput($block, DISPLAYNAME_ID, placeholder, getValue, setValue, Messages.anonymous);
};
var addLink = function ($container) {
var $block = $('<div>', {id: LINK_ID}).appendTo($container);
var getValue = function (cb) {
cb(APP.lm.proxy.url);
};
if (APP.readOnly) {
var $a = $('<a>', {
'class': LINK_ID,
target: '_blank',
rel: 'noreferrer noopener'
}).appendTo($block);
getValue(function (value) {
if (!value) {
return void $a.hide();
}
$a.attr('href', value).text(value);
});
return;
}
var setValue = function (value, cb) {
APP.lm.proxy.url = value;
Cryptpad.whenRealtimeSyncs(APP.lm.realtime, cb);
};
var placeholder = Messages.profile_urlPlaceholder;
createEditableInput($block, LINK_ID, placeholder, getValue, setValue);
};
var addAvatar = function ($container) {
var $block = $('<div>', {id: AVATAR_ID}).appendTo($container);
var $span = $('<span>').appendTo($block);
var allowedMediaTypes = Cryptpad.avatarAllowedTypes;
var sframeChan = common.getSframeChannel();
var displayAvatar = function () {
$span.html('');
if (!APP.lm.proxy.avatar) {
$('<img>', {
src: '/customize/images/avatar.png',
title: Messages.profile_avatar,
alt: 'Avatar'
}).appendTo($span);
return;
}
common.displayAvatar($span, APP.lm.proxy.avatar);
if (APP.readOnly) { return; }
var $delButton = $('<button>', {
'class': 'cp-app-profile-avatar-delete btn btn-danger fa fa-times',
title: Messages.fc_delete
});
$span.append($delButton);
$delButton.click(function () {
var old = common.getMetadataMgr().getUserData().avatar;
sframeChan.query("Q_PROFILE_AVATAR_REMOVE", old, function (err, err2) {
if (err || err2) { return void Cryptpad.log(err || err2); }
delete APP.lm.proxy.avatar;
displayAvatar();
});
});
};
displayAvatar();
if (APP.readOnly) { return; }
var fmConfig = {
noHandlers: true,
noStore: true,
body: $('body'),
onUploaded: function (ev, data) {
var old = common.getMetadataMgr().getUserData().avatar;
var todo = function () {
sframeChan.query("Q_PROFILE_AVATAR_ADD", data.url, function (err, err2) {
if (err || err2) { return void Cryptpad.log(err || err2); }
APP.lm.proxy.avatar = data.url;
displayAvatar();
});
};
if (old) {
sframeChan.query("Q_PROFILE_AVATAR_REMOVE", old, function (err, err2) {
if (err || err2) { return void Cryptpad.log(err || err2); }
todo();
});
return;
}
todo();
}
};
APP.FM = common.createFileManager(fmConfig);
var data = {
FM: APP.FM,
filter: function (file) {
var sizeMB = Cryptpad.bytesToMegabytes(file.size);
var type = file.type;
return sizeMB <= 0.5 && allowedMediaTypes.indexOf(type) !== -1;
},
accept: ".gif,.jpg,.jpeg,.png"
};
var $upButton = common.createButton('upload', false, data);
$upButton.text(Messages.profile_upload);
$upButton.prepend($('<span>', {'class': 'fa fa-upload'}));
$block.append($upButton);
};
var addDescription = function ($container) {
var $block = $('<div>', {id: DESCRIPTION_ID}).appendTo($container);
if (APP.readOnly) {
if (!(APP.lm.proxy.description || "").trim()) { return void $block.hide(); }
var $div = $('<div>', {'class': 'cp-app-profile-description-rendered'}).appendTo($block);
var val = Marked(APP.lm.proxy.description);
$div.html(val);
return;
}
$('<h3>').text(Messages.profile_description).insertBefore($block);
var $ok = $('<span>', {
'class': 'cp-app-profile-description-ok fa fa-check',
title: Messages.saved
}).appendTo($block);
var $spinner = $('<span>', {
'class': 'cp-app-profile-description-spin fa fa-spinner fa-pulse'
}).appendTo($block);
var $textarea = $('<textarea>').val(APP.lm.proxy.description || '');
$block.append($textarea);
var editor = APP.editor = CodeMirror.fromTextArea($textarea[0], {
lineNumbers: true,
lineWrapping: true,
styleActiveLine : true,
mode: "markdown",
});
var onLocal = function () {
$ok.hide();
$spinner.show();
var val = editor.getValue();
APP.lm.proxy.description = val;
Cryptpad.whenRealtimeSyncs(APP.lm.realtime, function () {
$ok.show();
$spinner.hide();
});
};
editor.on('change', onLocal);
};
var addPublicKey = function ($container) {
var $block = $('<div>', {id: PUBKEY_ID});
$container.append($block);
};
var createLeftside = function () {
var $categories = $('<div>', {'class': 'cp-sidebarlayout-categories'}).appendTo(APP.$leftside);
var $category = $('<div>', {'class': 'cp-sidebarlayout-category'}).appendTo($categories);
$category.append($('<span>', {'class': 'fa fa-user'}));
$category.addClass('cp-leftside-active');
$category.append(Messages.profileButton);
};
var onReady = function () {
APP.$container.find('#'+CREATE_ID).remove();
var obj = APP.lm && APP.lm.proxy;
if (!APP.readOnly) {
var pubKeys = Cryptpad.getPublicKeys();
if (pubKeys && pubKeys.curve) {
obj.curveKey = pubKeys.curve;
obj.edKey = pubKeys.ed;
}
}
if (!APP.initialized) {
var $header = $('<div>', {id: HEADER_ID}).appendTo(APP.$rightside);
addAvatar($header);
var $rightside = $('<div>', {id: HEADER_RIGHT_ID}).appendTo($header);
addDisplayName($rightside);
addLink($rightside);
addDescription(APP.$rightside);
addViewButton(APP.$rightside);
addPublicKey(APP.$rightside);
APP.initialized = true;
createLeftside();
}
Cryptpad.removeLoadingScreen();
};
var createToolbar = function () {
var displayed = ['useradmin', 'newpad', 'limit', 'pageTitle'];
var configTb = {
displayed: displayed,
common: Cryptpad,
sfCommon: common,
$container: APP.$toolbar,
pageTitle: Messages.profileButton,
metadataMgr: common.getMetadataMgr(),
};
APP.toolbar = Toolbar.create(configTb);
APP.toolbar.$rightside.hide();
};
nThen(function (waitFor) {
$(waitFor(Cryptpad.addLoadingScreen));
SFCommon.create(waitFor(function (c) { APP.common = common = c; }));
}).nThen(function (waitFor) {
APP.$container = $('#cp-sidebarlayout-container');
APP.$toolbar = $('#cp-toolbar');
APP.$leftside = $('<div>', {id: 'cp-sidebarlayout-leftside'}).appendTo(APP.$container);
APP.$rightside = $('<div>', {id: 'cp-sidebarlayout-rightside'}).appendTo(APP.$container);
sFrameChan = common.getSframeChannel();
sFrameChan.onReady(waitFor());
}).nThen(function (/*waitFor*/) {
Cryptpad.onError(function (info) {
if (info && info.type === "store") {
onConnectError();
}
});
createToolbar();
var metadataMgr = common.getMetadataMgr();
var privateData = metadataMgr.getPrivateData();
APP.origin = privateData.origin;
APP.readOnly = privateData.readOnly;
// If not logged in, you can only view other users's profile
if (!privateData.readOnly && !common.isLoggedIn()) {
Cryptpad.removeLoadingScreen();
var $p = $('<p>', {id: CREATE_ID}).append(Messages.profile_register);
var $a = $('<a>', {
href: APP.origin + '/register/'
});
$('<button>', {
'class': 'btn btn-success',
}).text(Messages.login_register).appendTo($a);
$p.append($('<br>')).append($a);
APP.$rightside.append($p);
return;
}
var listmapConfig = {
data: {},
common: common,
userName: 'profile',
logLevel: 1
};
var lm = APP.lm = Listmap.create(listmapConfig);
lm.proxy.on('ready', onReady);
});
});

@ -1,532 +1,95 @@
require.config({
paths: {
cm: '/bower_components/codemirror'
}
});
// Load #1, load as little as possible because we are in a race to get the loading screen up.
define([
'/bower_components/nthen/index.js',
'/api/config',
'jquery',
'/common/cryptpad-common.js',
'/bower_components/chainpad-listmap/chainpad-listmap.js',
'/bower_components/chainpad-crypto/crypto.js',
'/bower_components/marked/marked.min.js',
'/common/toolbar2.js',
'cm/lib/codemirror',
'cm/mode/markdown/markdown',
'less!/profile/main.less',
'less!/customize/src/less/toolbar.less',
'less!/customize/src/less/cryptpad.less',
'css!/bower_components/bootstrap/dist/css/bootstrap.min.css',
], function ($, Cryptpad, Listmap, Crypto, Marked, Toolbar, CodeMirror) {
var APP = window.APP = {
Cryptpad: Cryptpad,
_onRefresh: []
};
$(window.document).on('decryption', function (e) {
var decrypted = e.originalEvent;
if (decrypted.callback) { decrypted.callback(); }
})
.on('decryptionError', function (e) {
var error = e.originalEvent;
Cryptpad.alert(error.message);
});
// Marked
var renderer = new Marked.Renderer();
Marked.setOptions({
renderer: renderer,
sanitize: true
});
// Tasks list
var checkedTaskItemPtn = /^\s*\[x\]\s*/;
var uncheckedTaskItemPtn = /^\s*\[ \]\s*/;
renderer.listitem = function (text) {
var isCheckedTaskItem = checkedTaskItemPtn.test(text);
var isUncheckedTaskItem = uncheckedTaskItemPtn.test(text);
if (isCheckedTaskItem) {
text = text.replace(checkedTaskItemPtn,
'<i class="fa fa-check-square" aria-hidden="true"></i>&nbsp;') + '\n';
}
if (isUncheckedTaskItem) {
text = text.replace(uncheckedTaskItemPtn,
'<i class="fa fa-square-o" aria-hidden="true"></i>&nbsp;') + '\n';
}
var cls = (isCheckedTaskItem || isUncheckedTaskItem) ? ' class="todo-list-item"' : '';
return '<li'+ cls + '>' + text + '</li>\n';
};
/*renderer.image = function (href, title, text) {
if (href.slice(0,6) === '/file/') {
var parsed = Cryptpad.parsePadUrl(href);
var hexFileName = Cryptpad.base64ToHex(parsed.hashData.channel);
var src = '/blob/' + hexFileName.slice(0,2) + '/' + hexFileName;
var mt = '<media-tag src="' + src + '" data-crypto-key="cryptpad:' + parsed.hashData.key + '">';
mt += '</media-tag>';
return mt;
}
var out = '<img src="' + href + '" alt="' + text + '"';
if (title) {
out += ' title="' + title + '"';
}
out += this.options.xhtml ? '/>' : '>';
return out;
};*/
var Messages = Cryptpad.Messages;
var DISPLAYNAME_ID = "displayName";
var LINK_ID = "link";
var AVATAR_ID = "avatar";
var DESCRIPTION_ID = "description";
var PUBKEY_ID = "pubKey";
var CREATE_ID = "createProfile";
var HEADER_ID = "header";
var HEADER_RIGHT_ID = "rightside";
var CREATE_INVITE_BUTTON = 'inviteButton'; /* jshint ignore: line */
var VIEW_PROFILE_BUTTON = 'viewProfileButton';
var createEditableInput = function ($block, name, ph, getValue, setValue, realtime, fallbackValue) {
fallbackValue = fallbackValue || ''; // don't ever display 'null' or 'undefined'
var lastVal;
getValue(function (value) {
lastVal = value;
var $input = $('<input>', {
'id': name+'Input',
placeholder: ph
}).val(value);
var $icon = $('<span>', {'class': 'fa fa-pencil edit'});
var editing = false;
var todo = function () {
if (editing) { return; }
editing = true;
var newVal = $input.val().trim();
if (newVal === lastVal) {
editing = false;
return;
}
setValue(newVal, function (err) {
if (err) { return void console.error(err); }
Cryptpad.whenRealtimeSyncs(realtime, function () {
lastVal = newVal;
Cryptpad.log(Messages._getKey('profile_fieldSaved', [newVal || fallbackValue]));
editing = false;
});
});
};
$input.on('keyup', function (e) {
if (e.which === 13) { return void todo(); }
if (e.which === 27) {
$input.val(lastVal);
}
});
$icon.click(function () { $input.focus(); });
$input.focus(function () {
$input.width('');
});
$input.focusout(todo);
$block.append($input).append($icon);
});
};
/* jshint ignore:start */
var isFriend = function (proxy, edKey) {
var friends = Cryptpad.find(proxy, ['friends']);
return typeof(edKey) === 'string' && friends && (edKey in friends);
};
var addCreateInviteLinkButton = function ($container) {
return;
var obj = APP.lm.proxy;
var proxy = Cryptpad.getProxy();
var userViewHash = Cryptpad.find(proxy, ['profile', 'view']);
var edKey = obj.edKey;
var curveKey = obj.curveKey;
if (!APP.readOnly || !curveKey || !edKey || userViewHash === window.location.hash.slice(1) || isFriend(proxy, edKey)) {
//console.log("edit mode or missing curve key, or you're viewing your own profile");
return;
}
// sanitize user inputs
var unsafeName = obj.name || '';
console.log(unsafeName);
var name = Cryptpad.fixHTML(unsafeName) || Messages.anonymous;
console.log(name);
console.log("Creating invite button");
$("<button>", {
id: CREATE_INVITE_BUTTON,
title: Messages.profile_inviteButtonTitle,
})
.addClass('btn btn-success')
.text(Messages.profile_inviteButton)
.click(function () {
Cryptpad.confirm(Messages._getKey('profile_inviteExplanation', [name]), function (yes) {
if (!yes) { return; }
console.log(obj.curveKey);
Cryptpad.alert("TODO");
// TODO create a listmap object using your curve keys
// TODO fill the listmap object with your invite data
// TODO generate link to invite object
// TODO copy invite link to clipboard
}, null, true);
})
.appendTo($container);
};
/* jshint ignore:end */
var addViewButton = function ($container) {
if (!Cryptpad.isLoggedIn() || window.location.hash) {
return;
}
var hash = Cryptpad.find(Cryptpad.getProxy(), ['profile', 'view']);
var url = '/profile/#' + hash;
var $button = $('<button>', {
'class': 'btn btn-success',
id: VIEW_PROFILE_BUTTON,
})
.text(Messages.profile_viewMyProfile)
.click(function () {
window.open(url, '_blank');
});
$container.append($button);
};
var addDisplayName = function ($container) {
var $block = $('<div>', {id: DISPLAYNAME_ID}).appendTo($container);
var getValue = function (cb) {
cb(APP.lm.proxy.name);
};
var placeholder = Messages.profile_namePlaceholder;
if (APP.readOnly) {
var $span = $('<span>', {'class': DISPLAYNAME_ID}).appendTo($block);
getValue(function (value) {
$span.text(value || Messages.anonymous);
});
//addCreateInviteLinkButton($block);
return;
'/common/requireconfig.js',
'/common/sframe-common-outer.js'
], function (nThen, ApiConfig, $, RequireConfig, SFCommonO) {
var requireConfig = RequireConfig();
// Loaded in load #2
nThen(function (waitFor) {
$(waitFor());
}).nThen(function (waitFor) {
var req = {
cfg: requireConfig,
req: [ '/common/loading.js' ],
pfx: window.location.origin
};
window.rc = requireConfig;
window.apiconf = ApiConfig;
$('#sbox-iframe').attr('src',
ApiConfig.httpSafeOrigin + '/profile/inner.html?' + requireConfig.urlArgs +
'#' + encodeURIComponent(JSON.stringify(req)));
// This is a cheap trick to avoid loading sframe-channel in parallel with the
// loading screen setup.
var done = waitFor();
var onMsg = function (msg) {
var data = JSON.parse(msg.data);
if (data.q !== 'READY') { return; }
window.removeEventListener('message', onMsg);
var _done = done;
done = function () { };
_done();
};
window.addEventListener('message', onMsg);
}).nThen(function (/*waitFor*/) {
var getSecrets = function (Cryptpad) {
// 1st case: visiting someone else's profile with hash in the URL
if (window.location.hash) {
return Cryptpad.getSecrets('profile', window.location.hash.slice(1));
}
var setValue = function (value, cb) {
APP.lm.proxy.name = value;
cb();
};
var rt = Cryptpad.getStore().getProxy().info.realtime;
createEditableInput($block, DISPLAYNAME_ID, placeholder, getValue, setValue, rt, Messages.anonymous);
};
var addLink = function ($container) {
var $block = $('<div>', {id: LINK_ID}).appendTo($container);
var getValue = function (cb) {
cb(APP.lm.proxy.url);
};
if (APP.readOnly) {
var $a = $('<a>', {
'class': LINK_ID,
target: '_blank',
rel: 'noreferrer noopener'
}).appendTo($block);
getValue(function (value) {
if (!value) {
return void $a.hide();
// 2nd case: visiting our own existing profile
var obj = Cryptpad.getProxy();
if (obj.profile && obj.profile.view && obj.profile.edit) {
return Cryptpad.getSecrets('profile', obj.profile.edit);
}
$a.attr('href', value).text(value);
});
return;
// 3rd case: profile creation (create a new random hash, store it later if needed)
if (!Cryptpad.isLoggedIn()) { return; }
var hash = Cryptpad.createRandomHash();
var secret = Cryptpad.getSecrets('profile', hash);
Cryptpad.pinPads([secret.channel], function (e) {
if (e) {
if (e === 'E_OVER_LIMIT') {
// TODO
}
var setValue = function (value, cb) {
APP.lm.proxy.url = value;
cb();
};
var rt = APP.lm.realtime;
var placeholder = Messages.profile_urlPlaceholder;
createEditableInput($block, LINK_ID, placeholder, getValue, setValue, rt);
};
var addAvatar = function ($container) {
var $block = $('<div>', {id: AVATAR_ID}).appendTo($container);
var $span = $('<span>').appendTo($block);
var allowedMediaTypes = Cryptpad.avatarAllowedTypes;
var displayAvatar = function () {
$span.html('');
if (!APP.lm.proxy.avatar) {
$('<img>', {
src: '/customize/images/avatar.png',
title: Messages.profile_avatar,
alt: 'Avatar'
}).appendTo($span);
return;
//return void Cryptpad.log(Messages._getKey('profile_error', [e])) // TODO
}
Cryptpad.displayAvatar($span, APP.lm.proxy.avatar);
if (APP.readOnly) { return; }
var $delButton = $('<button>', {
'class': 'delete btn btn-danger fa fa-times',
title: Messages.fc_delete
});
$span.append($delButton);
$delButton.click(function () {
var oldChanId = Cryptpad.hrefToHexChannelId(APP.lm.proxy.avatar);
Cryptpad.unpinPads([oldChanId], function (e) {
if (e) { Cryptpad.log(e); }
delete APP.lm.proxy.avatar;
delete Cryptpad.getProxy().profile.avatar;
Cryptpad.whenRealtimeSyncs(APP.lm.realtime, function () {
var driveRt = Cryptpad.getStore().getProxy().info.realtime;
Cryptpad.whenRealtimeSyncs(driveRt, function () {
displayAvatar();
});
});
});
obj.profile = {};
obj.profile.edit = Cryptpad.getEditHashFromKeys(secret.channel, secret.keys);
obj.profile.view = Cryptpad.getViewHashFromKeys(secret.channel, secret.keys);
});
return secret;
};
displayAvatar();
if (APP.readOnly) { return; }
var fmConfig = {
noHandlers: true,
noStore: true,
body: $('body'),
onUploaded: function (ev, data) {
var chanId = Cryptpad.hrefToHexChannelId(data.url);
var profile = Cryptpad.getProxy().profile;
var old = profile.avatar;
var todo = function () {
var addRpc = function (sframeChan, Cryptpad) {
// Adding a new avatar from the profile: pin it and store it in the object
sframeChan.on('Q_PROFILE_AVATAR_ADD', function (data, cb) {
var chanId = Cryptpad.hrefToHexChannelId(data);
Cryptpad.pinPads([chanId], function (e) {
if (e) { return void Cryptpad.log(e); }
APP.lm.proxy.avatar = data.url;
Cryptpad.getProxy().profile.avatar = data.url;
Cryptpad.whenRealtimeSyncs(APP.lm.realtime, function () {
var driveRt = Cryptpad.getStore().getProxy().info.realtime;
Cryptpad.whenRealtimeSyncs(driveRt, function () {
displayAvatar();
if (e) { return void cb(e); }
Cryptpad.getProxy().profile.avatar = data;
Cryptpad.whenRealtimeSyncs(Cryptpad.getRealtime(), function () {
cb();
});
});
});
};
if (old) {
var oldChanId = Cryptpad.hrefToHexChannelId(old);
Cryptpad.unpinPads([oldChanId], function (e) {
if (e) { Cryptpad.log(e); }
todo();
});
return;
}
todo();
}
};
APP.FM = Cryptpad.createFileManager(fmConfig);
var data = {
FM: APP.FM,
filter: function (file) {
var sizeMB = Cryptpad.bytesToMegabytes(file.size);
var type = file.type;
return sizeMB <= 0.5 && allowedMediaTypes.indexOf(type) !== -1;
},
accept: ".gif,.jpg,.jpeg,.png"
};
var $upButton = Cryptpad.createButton('upload', false, data);
$upButton.text(Messages.profile_upload);
$upButton.prepend($('<span>', {'class': 'fa fa-upload'}));
$block.append($upButton);
};
var addDescription = function ($container) {
var $block = $('<div>', {id: DESCRIPTION_ID}).appendTo($container);
if (APP.readOnly) {
if (!(APP.lm.proxy.description || "").trim()) { return void $block.hide(); }
var $div = $('<div>', {'class': 'rendered'}).appendTo($block);
var val = Marked(APP.lm.proxy.description);
$div.html(val);
return;
}
$('<h3>').text(Messages.profile_description).insertBefore($block);
var $ok = $('<span>', {'class': 'ok fa fa-check', title: Messages.saved}).appendTo($block);
var $spinner = $('<span>', {'class': 'spin fa fa-spinner fa-pulse'}).appendTo($block);
var $textarea = $('<textarea>').val(APP.lm.proxy.description || '');
$block.append($textarea);
var editor = APP.editor = CodeMirror.fromTextArea($textarea[0], {
lineNumbers: true,
lineWrapping: true,
styleActiveLine : true,
mode: "markdown",
});
var onLocal = function () {
$ok.hide();
$spinner.show();
var val = editor.getValue();
APP.lm.proxy.description = val;
Cryptpad.whenRealtimeSyncs(APP.lm.realtime, function () {
$ok.show();
$spinner.hide();
});
};
editor.on('change', onLocal);
};
var addPublicKey = function ($container) {
var $block = $('<div>', {id: PUBKEY_ID});
$container.append($block);
};
var createLeftside = function () {
var $categories = $('<div>', {'class': 'categories'}).appendTo(APP.$leftside);
APP.$usage = $('<div>', {'class': 'usage'}).appendTo(APP.$leftside);
var $category = $('<div>', {'class': 'category'}).appendTo($categories);
$category.append($('<span>', {'class': 'fa fa-user'}));
$category.addClass('active');
$category.append(Messages.profileButton);
};
var createToolbar = function () {
var displayed = ['useradmin', 'newpad', 'limit', 'upgrade', 'pageTitle'];
var configTb = {
displayed: displayed,
ifrw: window,
common: Cryptpad,
$container: APP.$toolbar,
pageTitle: Messages.profileButton
};
var toolbar = APP.toolbar = Toolbar.create(configTb);
toolbar.$rightside.html(''); // Remove the drawer if we don't use it to hide the toolbar
};
var onReady = function () {
APP.$container.find('#'+CREATE_ID).remove();
var obj = APP.lm && APP.lm.proxy;
if (!APP.readOnly) {
var pubKeys = Cryptpad.getPublicKeys();
if (pubKeys && pubKeys.curve) {
obj.curveKey = pubKeys.curve;
obj.edKey = pubKeys.ed;
}
}
if (!APP.initialized) {
var $header = $('<div>', {id: HEADER_ID}).appendTo(APP.$rightside);
addAvatar($header);
var $rightside = $('<div>', {id: HEADER_RIGHT_ID}).appendTo($header);
addDisplayName($rightside);
addLink($rightside);
addDescription(APP.$rightside);
addViewButton(APP.$rightside); //$rightside);
addPublicKey(APP.$rightside);
APP.initialized = true;
createLeftside();
}
Cryptpad.removeLoadingScreen();
};
var onInit = function () {
};
var onDisconnect = function () {};
var onChange = function () {};
var andThen = function (profileHash) {
var secret = Cryptpad.getSecrets('profile', profileHash);
var readOnly = APP.readOnly = secret.keys && !secret.keys.editKeyStr;
var listmapConfig = {
data: {},
websocketURL: Cryptpad.getWebsocketURL(),
channel: secret.channel,
readOnly: readOnly,
validateKey: secret.keys.validateKey || undefined,
crypto: Crypto.createEncryptor(secret.keys),
userName: 'profile',
logLevel: 1,
};
var lm = APP.lm = Listmap.create(listmapConfig);
lm.proxy.on('create', onInit)
.on('ready', onReady)
.on('disconnect', onDisconnect)
.on('change', [], onChange);
};
var getOrCreateProfile = function () {
var obj = Cryptpad.getStore().getProxy().proxy;
if (obj.profile && obj.profile.view && obj.profile.edit) {
return void andThen(obj.profile.edit);
}
// If the user doesn't have a public profile, ask them if they want to create one
var todo = function () {
var secret = Cryptpad.getSecrets();
obj.profile = {};
var channel = Cryptpad.createChannelId();
Cryptpad.pinPads([channel], function (e) {
if (e) {
if (e === 'E_OVER_LIMIT') {
Cryptpad.alert(Messages.pinLimitNotPinned, null, true);
}
return void Cryptpad.log(Messages._getKey('profile_error', [e]));
}
obj.profile.edit = Cryptpad.getEditHashFromKeys(channel, secret.keys);
obj.profile.view = Cryptpad.getViewHashFromKeys(channel, secret.keys);
andThen(obj.profile.edit);
// Removing the avatar from the profile: unpin it
sframeChan.on('Q_PROFILE_AVATAR_REMOVE', function (data, cb) {
var chanId = Cryptpad.hrefToHexChannelId(data);
Cryptpad.unpinPads([chanId], function (e) {
delete Cryptpad.getProxy().profile.avatar;
cb(e);
});
};
Cryptpad.removeLoadingScreen();
if (!Cryptpad.isLoggedIn()) {
var $p = $('<p>', {id: CREATE_ID}).append(Messages.profile_register);
var $a = $('<a>', {
href: '/register/'
});
$('<button>', {
'class': 'btn btn-success',
}).text(Messages.login_register).appendTo($a);
$p.append($('<br>')).append($a);
APP.$rightside.append($p);
return;
}
// make an empty profile for the user on their first visit
todo();
};
var onCryptpadReady = function () {
APP.$leftside = $('<div>', {id: 'leftSide'}).appendTo(APP.$container);
APP.$rightside = $('<div>', {id: 'rightSide'}).appendTo(APP.$container);
createToolbar();
if (window.location.hash) {
return void andThen(window.location.hash.slice(1));
}
getOrCreateProfile();
};
$(function () {
$(window).click(function () {
$('.cp-dropdown-content').hide();
SFCommonO.start({
getSecrets: getSecrets,
noHash: true, // Don't add the hash in the URL if it doesn't already exist
addRpc: addRpc,
noRealtime: !localStorage.User_hash
});
APP.$container = $('#container');
APP.$toolbar = $('#toolbar');
Cryptpad.ready(function () {
Cryptpad.reportAppUsage();
onCryptpadReady();
});
});
});

@ -42,6 +42,7 @@ define([
'languageSelector',
'logoutEverywhere',
'resetTips',
'thumbnails',
'userFeedback'
],
'drive': [
@ -220,6 +221,60 @@ define([
return $div;
};
var createThumbnails = function (obj) {
var $div = $('<div>', {'class': 'thumbnails element'});
$('<label>').text(Messages.settings_thumbnails).appendTo($div);
// Disable
$('<span>', {'class': 'description'})
.text(Messages.settings_disableThumbnailsDescription).appendTo($div);
var $label = $('<label>', { 'for': 'disableThumbnails', 'class': 'noTitle' })
.text(Messages.settings_disableThumbnailsAction);
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: 'disableThumbnails'
}).on('change', function () {
$spinner.show();
$ok.hide();
if (!obj.proxy.settings) { obj.proxy.settings = {}; }
if (!obj.proxy.settings.general) { obj.proxy.settings.general = {}; }
obj.proxy.settings.general.disableThumbnails = $checkbox.is(':checked') || false;
Cryptpad.whenRealtimeSyncs(obj.info.realtime, function () {
$spinner.hide();
$ok.show();
});
});
$checkbox.appendTo($div);
$label.appendTo($div);
$ok.hide().appendTo($div);
$spinner.hide().appendTo($div);
if (obj.proxy.settings && obj.proxy.settings.general
&& obj.proxy.settings.general.disableThumbnails) {
$checkbox[0].checked = true;
}
// Reset
/*$('<span>', {'class': 'description'})
.text(Messages.settings_resetThumbnailsDescription).appendTo($div);
var $button = $('<button>', {'id': 'resetThumbnails', 'class': 'btn btn-primary'})
.text(Messages.settings_resetThumbnailsAction).appendTo($div);
$button.click(function () {
// TODO we need to have access to the sandbox localforage first...
Cryptpad.alert(Messages.settings_resetThumbnailsDone);
});*/
return $div;
};
var createBackupDrive = function (store) {
var obj = store.proxy;
var $div = $('<div>', {'class': 'backupDrive element'});
@ -467,6 +522,7 @@ define([
$rightside.append(createLogoutEverywhere(obj));
}
$rightside.append(createResetTips());
$rightside.append(createThumbnails(obj));
$rightside.append(createBackupDrive(obj));
$rightside.append(createImportLocalPads(obj));
$rightside.append(createResetDrive(obj));

@ -5,8 +5,8 @@
.cp {
#container {
#rightSide {
.userFeedback {
input {
.userFeedback, .thumbnails {
input[type="checkbox"] {
vertical-align: middle;
margin-right: 5px;
}

@ -402,7 +402,6 @@ define([
});
framework.setContentGetter(function () {
console.log(framework._.title.title);
var content = CodeMirror.getContent();
Slide.update(content.content);
return content;
@ -467,6 +466,17 @@ define([
framework.start();
};
var getThumbnailContainer = function () {
var $codeMirror = $('.CodeMirror');
var $c = $('#cp-app-slide-editor');
if ($c.hasClass('cp-app-slide-preview')) {
return $('.cp-app-slide-frame').first()[0];
}
if ($codeMirror.length) {
return $codeMirror[0];
}
};
var main = function () {
var CodeMirror;
var editor;
@ -474,10 +484,21 @@ define([
var framework;
nThen(function (waitFor) {
Framework.create({
toolbarContainer: '#cme_toolbox',
contentContainer: '#cp-app-slide-editor'
contentContainer: '#cp-app-slide-editor',
thumbnail: {
getContainer: getThumbnailContainer,
filter: function (el, before) {
var metadataMgr = framework._.cpNfInner.metadataMgr;
var metadata = metadataMgr.getMetadata();
if (before) {
$(el).css('background-color', metadata.backColor || '#000');
return;
}
$(el).css('background-color', '');
}
}
}, waitFor(function (fw) { framework = fw; }));
nThen(function (waitFor) {

@ -0,0 +1,130 @@
@import (once) "../../customize/src/less2/include/browser.less";
@import (once) "../../customize/src/less2/include/toolbar.less";
@import (once) "../../customize/src/less2/include/markdown.less";
@import (once) '../../customize/src/less2/include/fileupload.less';
@import (once) '../../customize/src/less2/include/alertify.less';
//@import (once) '../../customize/src/less/mixins.less';
//@import (once) '../../customize/src/less/variables.less";
@import (once) '../../customize/src/less2/include/avatar.less';
.toolbar_main();
.fileupload_main();
.alertify_main();
// body
&.cp-app-todo {
display: flex;
flex-flow: column;
@button-border: 2px;
#cp-toolbar {
display: flex; // We need this to remove a 3px border at the bottom of the toolbar
}
.cp-cryptpad-toolbar {
padding: 0px;
display: inline-block;
}
#cp-app-todo-container {
display: flex;
flex: 1;
flex-flow: column;
padding: 20px;
align-items: center;
background-color: lighten(@colortheme_todo-bg, 15%);
min-height: 0;
}
@spacing: 15px;
#cp-app-todo-taskslist {
flex: 1;
min-height: 0;
overflow-y: auto;
min-width: 40%;
max-width: 90%;
}
.cp-app-todo-create-form {
margin: @spacing;
min-width: 40%;
display: flex;
#cp-app-todo-newtodo {
flex: 1;
margin-right: 15px;
border-radius: 0;
border: 0;
background-color: darken(@colortheme_todo-bg, 10%);
color: #fff;
padding: 5px 10px;
font-weight: bold;
}
button {
cursor: pointer;
border-radius: 0;
background-color: darken(@colortheme_todo-bg, 20%);
border:0;
&:hover {
background-color: darken(@colortheme_todo-bg, 25%);
}
}
}
.cp-app-todo-task {
border: 1px solid black;
padding: @spacing;
display: flex;
align-items: center;
background-color: white;
&.cp-app-todo-task-complete {
background-color: #f0f0f0;
color: #777;
}
.cp-app-todo-task-text {
margin: @spacing;
flex: 1;
word-wrap: break-word;
min-width: 0;
font-weight: bold;
}
.cp-app-todo-task-date {
margin: @spacing;
}
.cp-app-todo-task-remove {
margin: @spacing;
cursor: pointer;
}
.cp-app-todo-task-checkbox {
font-size: 45px;
width: 45px;
cursor: pointer;
&:hover {
color: #999;
}
}
.cp-app-todo-task-checkbox-checked {
}
.cp-app-todo-task-checkbox-unchecked {
}
button {
border-radius: 0;
border:0;
}
}
display: flex;
flex-flow: column;
}

@ -1,16 +1,17 @@
<!DOCTYPE html>
<html class="cp pad">
<html>
<head>
<title>CryptPad</title>
<meta content="text/html; charset=utf-8" http-equiv="content-type"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script async data-bootload="/customize/template.js" data-main="/common/boot.js?ver=1.0" src="/bower_components/requirejs/require.js?ver=2.3.5"></script>
<meta name="referrer" content="no-referrer" />
<script async data-bootload="main.js" data-main="/common/boot.js?ver=1.0" src="/bower_components/requirejs/require.js?ver=2.3.5"></script>
<style>
html, body {
margin: 0px;
padding: 0px;
}
#pad-iframe {
#sbox-iframe {
position:fixed;
top:0px;
left:0px;
@ -23,8 +24,15 @@
padding:0;
overflow:hidden;
}
#sbox-filePicker-iframe {
position: fixed;
top:0; left:0;
bottom:0; right:0;
width:100%;
height: 100%;
border: 0;
}
</style>
</head>
<body>
<iframe id="pad-iframe"></iframe><script src="/common/noscriptfix.js"></script>
<iframe id="sbox-iframe">

@ -1,20 +1,20 @@
<!DOCTYPE html>
<html>
<html class="cp-app-noscroll">
<head>
<meta content="text/html; charset=utf-8" http-equiv="content-type"/>
<script src="/bower_components/jquery/dist/jquery.min.js"></script>
<script async data-bootload="/todo/inner.js" data-main="/common/boot.js?ver=1.0" src="/bower_components/requirejs/require.js?ver=2.3.5"></script>
<style>.loading-hidden, .loading-hidden * {display: none !important;}</style>
<script async data-bootload="/todo/inner.js" data-main="/common/sframe-boot.js?ver=1.4" src="/bower_components/requirejs/require.js?ver=2.3.5"></script>
<style>
.loading-hidden { display: none; }
</style>
</head>
<body class="loading-hidden">
<div id="toolbar" class="toolbar-container"></div>
<div id="container">
<div class="cp-create-form">
<input type="text" id="newTodoName" data-localization-placeholder="todo_newTodoNamePlaceholder" />
<body class="cp-app-todo">
<div id="cp-toolbar" class="cp-toolbar-container"></div>
<div id="cp-app-todo-container">
<div class="cp-app-todo-create-form">
<input type="text" id="cp-app-todo-newtodo" data-localization-placeholder="todo_newTodoNamePlaceholder" />
<button class="btn btn-success fa fa-plus" data-localization-title="todo_newTodoNameTitle"></button>
</div>
<div id="tasksList"></div>
<div id="cp-app-todo-taskslist"></div>
</div>
</body>
</html>

@ -1,15 +1,229 @@
define([
'jquery',
'less!/bower_components/components-font-awesome/css/font-awesome.min.css',
'/bower_components/chainpad-crypto/crypto.js',
'/common/sframe-chainpad-listmap.js',
'/common/toolbar3.js',
'/common/cryptpad-common.js',
'/bower_components/nthen/index.js',
'/common/sframe-common.js',
'/todo/todo.js',
'css!/bower_components/bootstrap/dist/css/bootstrap.min.css',
'less!/todo/todo.less',
//'less!/customize/src/less/cryptpad.less',
'less!/customize/src/less/toolbar.less',
], function ($) {
$('.loading-hidden').removeClass('loading-hidden');
// dirty hack to get rid the flash of the lock background
/*
setTimeout(function () {
$('#app').addClass('ready');
}, 100);*/
'less!/bower_components/components-font-awesome/css/font-awesome.min.css',
'less!/customize/src/less2/main.less',
], function (
$,
Crypto,
Listmap,
Toolbar,
Cryptpad,
nThen,
SFCommon,
Todo
)
{
var Messages = Cryptpad.Messages;
var APP = window.APP = {};
var onConnectError = function () {
Cryptpad.errorLoadingScreen(Messages.websocketError);
};
var common;
var sFrameChan;
nThen(function (waitFor) {
$(waitFor(Cryptpad.addLoadingScreen));
SFCommon.create(waitFor(function (c) { APP.common = common = c; }));
}).nThen(function (waitFor) {
sFrameChan = common.getSframeChannel();
sFrameChan.onReady(waitFor());
}).nThen(function (/*waitFor*/) {
Cryptpad.onError(function (info) {
if (info && info.type === "store") {
onConnectError();
}
});
var $body = $('body');
var $list = $('#cp-app-todo-taskslist');
var removeTips = function () {
Cryptpad.clearTooltips();
};
var onReady = function () {
var todo = Todo.init(APP.lm.proxy);
var deleteTask = function(id) {
todo.remove(id);
var $els = $list.find('.cp-app-todo-task').filter(function (i, el) {
return $(el).data('id') === id;
});
$els.fadeOut(null, function () {
$els.remove();
removeTips();
});
//APP.display();
};
// TODO make this actually work, and scroll to bottom...
var scrollTo = function (t) {
$list.animate({
scrollTop: t,
});
};
scrollTo = scrollTo;
var makeCheckbox = function (id, cb) {
var entry = APP.lm.proxy.data[id];
var checked = entry.state === 1 ?
'cp-app-todo-task-checkbox-checked fa-check-square-o':
'cp-app-todo-task-checkbox-unchecked fa-square-o';
var title = entry.state === 1?
Messages.todo_markAsIncompleteTitle:
Messages.todo_markAsCompleteTitle;
title = title;
removeTips();
return $('<span>', {
'class': 'cp-app-todo-task-checkbox fa ' + checked,
//title: title,
}).on('click', function () {
entry.state = (entry.state + 1) % 2;
if (typeof(cb) === 'function') {
cb(entry.state);
}
});
};
var addTaskUI = function (el, animate) {
var $taskDiv = $('<div>', {
'class': 'cp-app-todo-task'
});
if (animate) {
$taskDiv.prependTo($list);
} else {
$taskDiv.appendTo($list);
}
$taskDiv.data('id', el);
makeCheckbox(el, function (/*state*/) {
APP.display();
})
.appendTo($taskDiv);
var entry = APP.lm.proxy.data[el];
if (entry.state) {
$taskDiv.addClass('cp-app-todo-task-complete');
}
$('<span>', { 'class': 'cp-app-todo-task-text' })
.text(entry.task)
.appendTo($taskDiv);
/*$('<span>', { 'class': 'cp-app-todo-task-date' })
.text(new Date(entry.ctime).toLocaleString())
.appendTo($taskDiv);*/
$('<button>', {
'class': 'fa fa-times cp-app-todo-task-remove btn btn-danger',
title: Messages.todo_removeTaskTitle,
}).appendTo($taskDiv).on('click', function() {
deleteTask(el);
});
if (animate) {
$taskDiv.hide();
window.setTimeout(function () {
// ???
$taskDiv.fadeIn();
}, 0);
}
removeTips();
};
var display = APP.display = function () {
$list.empty();
removeTips();
APP.lm.proxy.order.forEach(function (el) {
addTaskUI(el);
});
//scrollTo('300px');
};
var addTask = function () {
var $input = $('#cp-app-todo-newtodo');
// if the input is empty after removing leading and trailing spaces
// don't create a new entry
if (!$input.val().trim()) { return; }
var obj = {
"state": 0,
"task": $input.val(),
"ctime": +new Date(),
"mtime": +new Date()
};
var id = Cryptpad.createChannelId();
todo.add(id, obj);
$input.val("");
addTaskUI(id, true);
//display();
};
var $formSubmit = $('.cp-app-todo-create-form button').on('click', addTask);
$('#cp-app-todo-newtodo').on('keypress', function (e) {
switch (e.which) {
case 13:
$formSubmit.click();
break;
default:
//console.log(e.which);
}
}).focus();
var editTask = function () {
};
editTask = editTask;
display();
Cryptpad.removeLoadingScreen();
};
var onInit = function () {
Cryptpad.addLoadingScreen();
$body.on('dragover', function (e) { e.preventDefault(); });
$body.on('drop', function (e) { e.preventDefault(); });
var $bar = $('.cp-toolbar-container');
var displayed = ['useradmin', 'newpad', 'limit', 'pageTitle'];
var configTb = {
displayed: displayed,
common: Cryptpad,
sfCommon: common,
$container: $bar,
pageTitle: Messages.todo_title,
metadataMgr: common.getMetadataMgr(),
};
APP.toolbar = Toolbar.create(configTb);
APP.toolbar.$rightside.hide();
};
var createTodo = function() {
var listmapConfig = {
data: {},
common: common,
userName: 'todo',
logLevel: 1
};
var lm = APP.lm = Listmap.create(listmapConfig);
lm.proxy.on('create', onInit)
.on('ready', onReady);
};
createTodo();
});
});

@ -1,229 +1,49 @@
// Load #1, load as little as possible because we are in a race to get the loading screen up.
define([
'/bower_components/nthen/index.js',
'/api/config',
'jquery',
'/bower_components/chainpad-crypto/crypto.js',
'/bower_components/chainpad-listmap/chainpad-listmap.js',
'/common/toolbar2.js',
'/common/cryptpad-common.js',
'/todo/todo.js',
//'/common/media-tag.js',
//'/bower_components/file-saver/FileSaver.min.js',
'less!/bower_components/components-font-awesome/css/font-awesome.min.css',
'less!/customize/src/less/cryptpad.less',
], function ($, Crypto, Listmap, Toolbar, Cryptpad, Todo) {
var Messages = Cryptpad.Messages;
var APP = window.APP = {};
$(function () {
var $iframe = $('#pad-iframe').contents();
var $body = $iframe.find('body');
var ifrw = $('#pad-iframe')[0].contentWindow;
var $list = $iframe.find('#tasksList');
var removeTips = function () {
Cryptpad.clearTooltips();
};
var onReady = function () {
var todo = Todo.init(APP.lm.proxy, Cryptpad);
var deleteTask = function(id) {
todo.remove(id);
var $els = $list.find('.cp-task').filter(function (i, el) {
return $(el).data('id') === id;
});
$els.fadeOut(null, function () {
$els.remove();
removeTips();
});
//APP.display();
};
// TODO make this actually work, and scroll to bottom...
var scrollTo = function (t) {
var $list = $iframe.find('#tasksList');
$list.animate({
scrollTop: t,
});
};
scrollTo = scrollTo;
var makeCheckbox = function (id, cb) {
var entry = APP.lm.proxy.data[id];
var checked = entry.state === 1? 'cp-task-checkbox-checked fa-check-square-o': 'cp-task-checkbox-unchecked fa-square-o';
var title = entry.state === 1?
Messages.todo_markAsIncompleteTitle:
Messages.todo_markAsCompleteTitle;
title = title;
removeTips();
return $('<span>', {
'class': 'cp-task-checkbox fa ' + checked,
//title: title,
}).on('click', function () {
entry.state = (entry.state + 1) % 2;
if (typeof(cb) === 'function') {
cb(entry.state);
}
});
};
var addTaskUI = function (el, animate) {
var $taskDiv = $('<div>', {
'class': 'cp-task'
});
if (animate) {
$taskDiv.prependTo($list);
} else {
$taskDiv.appendTo($list);
}
$taskDiv.data('id', el);
makeCheckbox(el, function (/*state*/) {
APP.display();
})
.appendTo($taskDiv);
var entry = APP.lm.proxy.data[el];
if (entry.state) {
$taskDiv.addClass('cp-task-complete');
}
$('<span>', { 'class': 'cp-task-text' })
.text(entry.task)
.appendTo($taskDiv);
/*$('<span>', { 'class': 'cp-task-date' })
.text(new Date(entry.ctime).toLocaleString())
.appendTo($taskDiv);*/
$('<button>', {
'class': 'fa fa-times cp-task-remove btn btn-danger',
title: Messages.todo_removeTaskTitle,
}).appendTo($taskDiv).on('click', function() {
deleteTask(el);
'/common/requireconfig.js',
'/common/sframe-common-outer.js'
], function (nThen, ApiConfig, $, RequireConfig, SFCommonO) {
var requireConfig = RequireConfig();
// Loaded in load #2
nThen(function (waitFor) {
$(waitFor());
}).nThen(function (waitFor) {
var req = {
cfg: requireConfig,
req: [ '/common/loading.js' ],
pfx: window.location.origin
};
window.rc = requireConfig;
window.apiconf = ApiConfig;
$('#sbox-iframe').attr('src',
ApiConfig.httpSafeOrigin + '/todo/inner.html?' + requireConfig.urlArgs +
'#' + encodeURIComponent(JSON.stringify(req)));
// This is a cheap trick to avoid loading sframe-channel in parallel with the
// loading screen setup.
var done = waitFor();
var onMsg = function (msg) {
var data = JSON.parse(msg.data);
if (data.q !== 'READY') { return; }
window.removeEventListener('message', onMsg);
var _done = done;
done = function () { };
_done();
};
window.addEventListener('message', onMsg);
}).nThen(function (/*waitFor*/) {
var getSecrets = function (Cryptpad) {
var proxy = Cryptpad.getProxy();
var hash = proxy.todo || Cryptpad.createRandomHash();
return Cryptpad.getSecrets('todo', hash);
};
SFCommonO.start({
getSecrets: getSecrets,
noHash: true,
});
if (animate) {
$taskDiv.hide();
window.setTimeout(function () {
// ???
$taskDiv.fadeIn();
}, 0);
}
removeTips();
};
var display = APP.display = function () {
$list.empty();
removeTips();
APP.lm.proxy.order.forEach(function (el) {
addTaskUI(el);
});
//scrollTo('300px');
};
var addTask = function () {
var $input = $iframe.find('#newTodoName');
// if the input is empty after removing leading and trailing spaces
// don't create a new entry
if (!$input.val().trim()) { return; }
var obj = {
"state": 0,
"task": $input.val(),
"ctime": +new Date(),
"mtime": +new Date()
};
var id = Cryptpad.createChannelId();
todo.add(id, obj);
$input.val("");
addTaskUI(id, true);
//display();
};
var $formSubmit = $iframe.find('.cp-create-form button').on('click', addTask);
$iframe.find('#newTodoName').on('keypress', function (e) {
switch (e.which) {
case 13:
$formSubmit.click();
break;
default:
console.log(e.which);
}
}).focus();
var editTask = function () {
};
editTask = editTask;
display();
Cryptpad.removeLoadingScreen();
};
var onInit = function () {
Cryptpad.addLoadingScreen();
$body.on('dragover', function (e) { e.preventDefault(); });
$body.on('drop', function (e) { e.preventDefault(); });
var Title;
var $bar = $iframe.find('.toolbar-container');
Title = Cryptpad.createTitle({}, function(){}, Cryptpad);
var configTb = {
displayed: ['useradmin', 'newpad', 'limit', 'upgrade', 'pageTitle'],
ifrw: ifrw,
common: Cryptpad,
//hideDisplayName: true,
$container: $bar,
pageTitle: Messages.todo_title
};
APP.toolbar = Toolbar.create(configTb);
APP.toolbar.$rightside.html(''); // Remove the drawer if we don't use it to hide the toolbar
};
var createTodo = function() {
var obj = Cryptpad.getProxy();
var hash = Cryptpad.createRandomHash();
if(obj.todo) {
hash = obj.todo;
} else {
obj.todo = hash;
}
var secret = Cryptpad.getSecrets('todo', hash);
var listmapConfig = {
data: {},
websocketURL: Cryptpad.getWebsocketURL(),
channel: secret.channel,
validateKey: secret.keys.validateKey || undefined,
crypto: Crypto.createEncryptor(secret.keys),
userName: 'todo',
logLevel: 1,
};
var lm = APP.lm = Listmap.create(listmapConfig);
lm.proxy.on('create', onInit)
.on('ready', onReady);
};
Cryptpad.ready(function () {
createTodo();
Cryptpad.reportAppUsage();
});
});
});

@ -2,7 +2,6 @@ define([
], function () {
var Todo = {};
var Cryptpad;
/* data model
{
@ -60,9 +59,7 @@ define([
if (proxy.data[id]) { delete proxy.data[id]; }
};
Todo.init = function (proxy, common) {
Cryptpad = common;
Todo.init = function (proxy) {
var api = {};
initialize(proxy);

@ -10,6 +10,7 @@ define([
'/common/cryptget.js',
'/bower_components/nthen/index.js',
'/common/sframe-common.js',
'/common/sframe-common-interface.js',
'/api/config',
'/common/common-realtime.js',
'/customize/pages.js',
@ -36,6 +37,7 @@ define([
Cryptget,
nThen,
SFCommon,
SFUI,
ApiConfig,
CommonRealtime,
Pages,
@ -208,6 +210,7 @@ define([
});
var setEditable = function (bool) {
APP.editable = bool;
if (readOnly && bool) { return; }
if (bool) { $controls.css('display', 'flex'); }
else { $controls.hide(); }
@ -237,17 +240,9 @@ define([
APP.upload = function (title) {
var canvas = $canvas[0];
APP.canvas.deactivateAll().renderAll();
var finish = function (thumb) {
canvas.toBlob(function (blob) {
blob.name = title;
APP.FM.handleFile(blob, void 0, thumb);
});
};
Thumb.fromCanvas(canvas, function (e, blob) {
// carry on even if you can't get a thumbnail
if (e) { console.error(e); }
finish(blob);
APP.FM.handleFile(blob);
});
};
@ -287,6 +282,7 @@ define([
})
.on('dblclick', function (e) {
e.preventDefault();
if (!APP.editable) { return; }
pickColor(Colors.rgb2hex($color.css('background-color')), function (c) {
$color.css({
'background-color': c,
@ -378,6 +374,29 @@ define([
onLocal();
};
var initThumbnails = function () {
var oldThumbnailState;
var privateDat = metadataMgr.getPrivateData();
if (!privateDat.thumbnails) { return; }
var hash = privateDat.availableHashes.editHash ||
privateDat.availableHashes.viewHash;
var href = privateDat.pathname + '#' + hash;
var mkThumbnail = function () {
if (!hash) { return; }
if (initializing) { return; }
if (!APP.realtime) { return; }
var content = APP.realtime.getUserDoc();
if (content === oldThumbnailState) { return; }
var D = Thumb.getResizedDimensions($canvas[0], 'pad');
Thumb.fromCanvas($canvas[0], D, function (err, b64) {
oldThumbnailState = content;
Thumb.setPadThumbnail(href, b64);
});
};
window.setInterval(mkThumbnail, Thumb.UPDATE_INTERVAL);
window.setTimeout(mkThumbnail, Thumb.UPDATE_FIRST);
};
config.onInit = function (info) {
updateLocalPalette(palette);
readOnly = metadataMgr.getPrivateData().readOnly;
@ -538,6 +557,10 @@ define([
initializing = false;
config.onLocal();
Cryptpad.removeLoadingScreen();
initThumbnails();
if (readOnly) { return; }
if (isNew) {
common.openTemplatePicker();

Loading…
Cancel
Save