From 18b5b20d2788e48e895a0c92a2d84d24afba34f0 Mon Sep 17 00:00:00 2001 From: yflory Date: Tue, 24 Oct 2017 14:31:42 +0200 Subject: [PATCH 01/46] Add thumbnails for PDFs --- www/common/common-thumbnail.js | 36 +++++++++++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/www/common/common-thumbnail.js b/www/common/common-thumbnail.js index ea0ce1d54..25cf92bc0 100644 --- a/www/common/common-thumbnail.js +++ b/www/common/common-thumbnail.js @@ -11,7 +11,8 @@ define([ 'image/jpeg', 'image/jpg', 'image/gif', - 'video/' + 'video/', + 'application/pdf' ]; Thumb.isSupportedType = function (type) { @@ -122,10 +123,43 @@ 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) { + canvas.toBlob(function (blob) { + cb(void 0, blob); + }); + }); + }).catch(function (err) { + 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); }; From bac10472f367b46f7696e578f13f62fbc709f2c4 Mon Sep 17 00:00:00 2001 From: yflory Date: Tue, 24 Oct 2017 14:32:47 +0200 Subject: [PATCH 02/46] lint compliance --- www/common/common-thumbnail.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/www/common/common-thumbnail.js b/www/common/common-thumbnail.js index 9bd97e8aa..18e963a4e 100644 --- a/www/common/common-thumbnail.js +++ b/www/common/common-thumbnail.js @@ -149,7 +149,7 @@ define([ cb(void 0, blob); }); }); - }).catch(function (err) { + }).catch(function () { cb('ERROR'); }); }); From dc908110901f0ad6e8c18ccaec053c3ca8db5c5e Mon Sep 17 00:00:00 2001 From: ansuz Date: Tue, 24 Oct 2017 16:56:08 +0200 Subject: [PATCH 03/46] only call onReady once in sframe-listmap --- www/common/sframe-chainpad-listmap.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/www/common/sframe-chainpad-listmap.js b/www/common/sframe-chainpad-listmap.js index 22b7e6216..a4c63641d 100644 --- a/www/common/sframe-chainpad-listmap.js +++ b/www/common/sframe-chainpad-listmap.js @@ -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) { From aa37997aa32295420290d70b3b1b552ace5040b2 Mon Sep 17 00:00:00 2001 From: Caleb James DeLisle Date: Tue, 24 Oct 2017 18:02:03 +0300 Subject: [PATCH 04/46] Enable JSON-OT again because it is working now that the arguments are passed in the right order --- www/common/sframe-app-framework.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/www/common/sframe-app-framework.js b/www/common/sframe-app-framework.js index 2bb6090aa..ea7c81175 100644 --- a/www/common/sframe-app-framework.js +++ b/www/common/sframe-app-framework.js @@ -364,8 +364,7 @@ define([ // really basic operational transform transformFunction: options.transformFunction || JsonOT.validate, - // This one causes a big mess. - //patchTransformer: options.patchTransformer || JsonOT.patchTransformer, + patchTransformer: options.patchTransformer || JsonOT.patchTransformer, // cryptpad debug logging (default is 1) // logLevel: 0, From 69f9a7ebf395bb5b4545cff331b11e8ba1626ec1 Mon Sep 17 00:00:00 2001 From: ansuz Date: Tue, 24 Oct 2017 17:29:58 +0200 Subject: [PATCH 05/46] make userlist change notifications configurable --- customize.dist/application_config.js | 1 + www/common/toolbar3.js | 1 + 2 files changed, 2 insertions(+) diff --git a/customize.dist/application_config.js b/customize.dist/application_config.js index 9cf29d565..079c36eab 100644 --- a/customize.dist/application_config.js +++ b/customize.dist/application_config.js @@ -12,6 +12,7 @@ define(function() { * You can change their duration here (measured in milliseconds) */ config.notificationTimeout = 5000; + config.disableUserlistNotifications = false; config.enablePinning = true; diff --git a/www/common/toolbar3.js b/www/common/toolbar3.js index fdebae133..29d78e91c 100644 --- a/www/common/toolbar3.js +++ b/www/common/toolbar3.js @@ -886,6 +886,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])); From 8359902f6a6b5cf154ee238096a4bcbc19a7bf81 Mon Sep 17 00:00:00 2001 From: ansuz Date: Tue, 24 Oct 2017 17:42:08 +0200 Subject: [PATCH 06/46] fix typo in poll --- www/poll/inner.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/www/poll/inner.js b/www/poll/inner.js index 767817ddd..eb62cddcd 100644 --- a/www/poll/inner.js +++ b/www/poll/inner.js @@ -651,7 +651,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) From 6157c57a4be24e4fecc66839bd843837202d8966 Mon Sep 17 00:00:00 2001 From: ansuz Date: Tue, 24 Oct 2017 17:59:19 +0200 Subject: [PATCH 07/46] disable color palette when interface is not editable --- www/whiteboard/inner.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/www/whiteboard/inner.js b/www/whiteboard/inner.js index f5ccd22b9..e71b7fe69 100644 --- a/www/whiteboard/inner.js +++ b/www/whiteboard/inner.js @@ -208,6 +208,7 @@ define([ }); var setEditable = function (bool) { + APP.editable = bool; if (readOnly && bool) { return; } if (bool) { $controls.css('display', 'flex'); } else { $controls.hide(); } @@ -287,6 +288,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, From ba97aa7ad2343d712c86f03673816d75bedb2cbd Mon Sep 17 00:00:00 2001 From: ansuz Date: Tue, 24 Oct 2017 18:09:38 +0200 Subject: [PATCH 08/46] allow file upload handler to create thumbnails for whiteboard --- www/whiteboard/inner.js | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/www/whiteboard/inner.js b/www/whiteboard/inner.js index e71b7fe69..626ad1a90 100644 --- a/www/whiteboard/inner.js +++ b/www/whiteboard/inner.js @@ -238,17 +238,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); + canvas.toBlob(function (blob) { + blob.name = title; + APP.FM.handleFile(blob); }); }; From f4adbd980e585f4cdd7b4bf2247d97b38e423d2f Mon Sep 17 00:00:00 2001 From: yflory Date: Tue, 24 Oct 2017 18:49:58 +0200 Subject: [PATCH 09/46] Thumbnails for the code app --- www/code/inner.html | 1 + www/code/inner.js | 23 +++++++++++++-- www/common/common-file.js | 15 +++------- www/common/common-thumbnail.js | 41 +++++++++++++++++---------- www/common/sframe-app-framework.js | 27 ++++++++++++++++++ www/common/sframe-common-file.js | 15 +++------- www/common/sframe-common-interface.js | 14 +++++++-- 7 files changed, 94 insertions(+), 42 deletions(-) diff --git a/www/code/inner.html b/www/code/inner.html index 5d914b6c8..327363fd0 100644 --- a/www/code/inner.html +++ b/www/code/inner.html @@ -6,6 +6,7 @@ diff --git a/www/code/inner.js b/www/code/inner.js index d5d7edfdd..7fd66721e 100644 --- a/www/code/inner.js +++ b/www/code/inner.js @@ -7,6 +7,7 @@ define([ '/common/sframe-common.js', '/common/sframe-app-framework.js', '/common/common-util.js', + '/common/common-thumbnail.js', '/common/modes.js', 'cm/lib/codemirror', @@ -45,6 +46,7 @@ define([ SFCommon, Framework, Util, + Thumb, Modes, CMeditor) { @@ -145,6 +147,10 @@ define([ $codeMirror.addClass('cp-app-code-fullpage'); }; + var isVisible = function () { + return $previewContainer.is(':visible'); + }; + framework.onReady(function () { // add the splitter var splitter = $('
', { @@ -184,7 +190,8 @@ define([ return { forceDraw: forceDrawPreview, draw: drawPreview, - modeChange: modeChange + modeChange: modeChange, + isVisible: isVisible }; }; @@ -317,6 +324,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 +345,8 @@ define([ Framework.create({ toolbarContainer: '#cme_toolbox', - contentContainer: '#cp-app-code-editor' + contentContainer: '#cp-app-code-editor', + getThumbnailContainer: getThumbnailContainer }, waitFor(function (fw) { framework = fw; })); nThen(function (waitFor) { diff --git a/www/common/common-file.js b/www/common/common-file.js index 73a58cc50..4501882b3 100644 --- a/www/common/common-file.js +++ b/www/common/common-file.js @@ -296,18 +296,11 @@ 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); - finish(); - }); + if (!thumb64) { return finish(); } + thumb = thumb64; + finish(); }); }); }; diff --git a/www/common/common-thumbnail.js b/www/common/common-thumbnail.js index 18e963a4e..a2deea346 100644 --- a/www/common/common-thumbnail.js +++ b/www/common/common-thumbnail.js @@ -48,7 +48,14 @@ define([ var dim = 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 @@ -77,18 +84,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; 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) { @@ -97,10 +102,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'); @@ -145,9 +147,7 @@ define([ PDFJS.getDocument(url).promise .then(function (doc) { return doc.getPage(1).then(makeThumb).then(function (canvas) { - canvas.toBlob(function (blob) { - cb(void 0, blob); - }); + cb(void 0, canvas.toDataURL()); }); }).catch(function () { cb('ERROR'); @@ -164,8 +164,19 @@ define([ Thumb.fromImageBlob(blob, cb); }; - Thumb.fromVideo = function (video, cb) { - cb = cb; // WIP + window.html2canvas = undefined; + Thumb.fromDOM = function (element, cb) { + var todo = function () { + html2canvas(element, { + allowTaint: true, + onrendered: function (canvas) { + var D = getResizedDimensions(canvas, 'image'); + Thumb.fromCanvas(canvas, D, cb); + } + }); + }; + if (html2canvas) { return void todo(); } + require(['/bower_components/html2canvas/build/html2canvas.min.js'], todo); }; return Thumb; diff --git a/www/common/sframe-app-framework.js b/www/common/sframe-app-framework.js index 2bb6090aa..8295560ad 100644 --- a/www/common/sframe-app-framework.js +++ b/www/common/sframe-app-framework.js @@ -8,8 +8,10 @@ define([ '/common/cryptpad-common.js', '/bower_components/nthen/index.js', '/common/sframe-common.js', + '/common/sframe-common-interface.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', @@ -25,8 +27,10 @@ define([ Cryptpad, nThen, SFCommon, + SFUI, Messages, Util, + Thumb, AppConfig) { var SaveAs = window.saveAs; @@ -264,6 +268,29 @@ define([ Cryptpad.removeLoadingScreen(emitResize); + if (options.getThumbnailContainer) { + var oldThumbnailState; + var privateDat = cpNfInner.metadataMgr.getPrivateData(); + var hash = privateDat.availableHashes.editHash || privateDat.availableHashes.viewHash; + var href = privateDat.pathname + '#' + hash; + var mkThumbnail = function () { + if (!hash) { return; } + if (state !== STATE.READY) { return; } + if (!cpNfInner.chainpad) { return; } + var content = cpNfInner.chainpad.getUserDoc(); + if (content === oldThumbnailState) { return; } + var el = options.getThumbnailContainer(); + if (!el) { return; } + $(el).parents().css('overflow', 'visible'); + Thumb.fromDOM(el, function (err, b64) { + oldThumbnailState = content; + $(el).parents().css('overflow', ''); + SFUI.setPadThumbnail(href, b64) + }); + }; + window.setInterval(mkThumbnail, 5000); + } + if (newPad) { common.openTemplatePicker(); } diff --git a/www/common/sframe-common-file.js b/www/common/sframe-common-file.js index 724c13349..aebec38e6 100644 --- a/www/common/sframe-common-file.js +++ b/www/common/sframe-common-file.js @@ -247,18 +247,11 @@ 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); - finish(); - }); + if (!thumb64) { return finish(); } + thumb = thumb64; + finish(); }); }); }; diff --git a/www/common/sframe-common-interface.js b/www/common/sframe-common-interface.js index e53db847d..6835039cb 100644 --- a/www/common/sframe-common-interface.js +++ b/www/common/sframe-common-interface.js @@ -35,15 +35,20 @@ define([ var addThumbnail = function (err, thumb, $span, cb) { var img = new Image(); - img.src = 'data:;base64,'+thumb; + img.src = thumb.slice(0,5) === 'data:' ? thumb : 'data:;base64,'+thumb; $span.find('.cp-icon').hide(); $span.prepend(img); cb($(img)); }; + UI.setPadThumbnail = function (href, b64, cb) { + cb = cb || $.noop; + var k ='thumbnail-' + href; + localForage.setItem(k, b64, cb); + }; + localForage.removeItem('thumbnail-/1/edit/lqg6RRnynI76LV0sR8F0YA/Nh1SNXxB5U2UjaADvODfvI5l/'); 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); @@ -61,7 +66,10 @@ define([ }); }; localForage.getItem(k, function (err, v) { - if (!v) { return void whenNewThumb(); } + 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 === 'EMPTY') { return; } addThumbnail(err, v, $container, cb); }); From f52a46c5aa3cc1819d264de8d2f14ab6c25557a1 Mon Sep 17 00:00:00 2001 From: ansuz Date: Tue, 24 Oct 2017 18:49:34 +0200 Subject: [PATCH 10/46] add html2canvas dependency --- bower.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bower.json b/bower.json index 0cabd2b5f..2b9c05687 100644 --- a/bower.json +++ b/bower.json @@ -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" From f031af4e9dd350be2aa61bab387436aa58669634 Mon Sep 17 00:00:00 2001 From: ansuz Date: Wed, 25 Oct 2017 10:50:17 +0200 Subject: [PATCH 11/46] fix broken media-tags in contacts --- www/contacts/messenger-ui.js | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/www/contacts/messenger-ui.js b/www/contacts/messenger-ui.js index fddad4497..b5c2f0c06 100644 --- a/www/contacts/messenger-ui.js +++ b/www/contacts/messenger-ui.js @@ -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); From bf817f20eec423aef58a296b27e5ba0cb472b59e Mon Sep 17 00:00:00 2001 From: yflory Date: Wed, 25 Oct 2017 12:31:22 +0200 Subject: [PATCH 12/46] Fix file upload in code and slide --- www/common/common-file.js | 6 +++--- www/common/sframe-common-file.js | 2 ++ www/slide/inner.js | 18 ++++++++++++++++++ 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/www/common/common-file.js b/www/common/common-file.js index 4501882b3..df2f94ee7 100644 --- a/www/common/common-file.js +++ b/www/common/common-file.js @@ -28,8 +28,8 @@ define([ var u8 = file.blob; // This is not a blob but a uint8array var metadata = file.metadata; - // if it exists, dropEvent contains the new pad location in the drive - var dropEvent = file.dropEvent; + // if it exists, path contains the new pad location in the drive + var path = file.path; var key = Nacl.randomBytes(32); var next = FileCrypto.encrypt(u8, metadata, key); @@ -76,7 +76,7 @@ define([ if (noStore) { return void onComplete(href); } - common.initialPath = dropEvent && dropEvent.path; + common.initialPath = path; common.renamePad(title || "", href, function (err) { if (err) { return void console.error(err); } onComplete(href); diff --git a/www/common/sframe-common-file.js b/www/common/sframe-common-file.js index aebec38e6..c8b1b7d7c 100644 --- a/www/common/sframe-common-file.js +++ b/www/common/sframe-common-file.js @@ -93,6 +93,8 @@ define([ var metadata = file.metadata; var id = file.id; var dropEvent = file.dropEvent; + delete file.dropEvent; + if (dropEvent.path) { file.path = dropEvent.path; } if (queue.inProgress) { return; } queue.inProgress = true; diff --git a/www/slide/inner.js b/www/slide/inner.js index bcfdc60f0..2dd47105f 100644 --- a/www/slide/inner.js +++ b/www/slide/inner.js @@ -376,6 +376,8 @@ define([ var andThen2 = function (editor, CodeMirror, framework, isPresentMode) { + var common = framework._.sfCommon; + var $contentContainer = $('#cp-app-slide-editor'); var $modal = $('#cp-app-slide-modal'); var $content = $('#cp-app-slide-modal-content'); @@ -427,6 +429,22 @@ define([ framework._.sfCommon.setTabTitle('{title}' + slideNumber); }); Slide.update(editor.getValue()); + + var fmConfig = { + dropArea: $('.CodeMirror'), + body: $('body'), + onUploaded: function (ev, data) { + //var cursor = editor.getCursor(); + //var cleanName = data.name.replace(/[\[\]]/g, ''); + //var text = '!['+cleanName+']('+data.url+')'; + var parsed = Cryptpad.parsePadUrl(data.url); + var hexFileName = Cryptpad.base64ToHex(parsed.hashData.channel); + var src = '/blob/' + hexFileName.slice(0,2) + '/' + hexFileName; + var mt = ''; + editor.replaceSelection(mt); + } + }; + common.createFileManager(fmConfig); }); framework.onDefaultContentNeeded(function () { From 6f020b67ca1a0f4f18197983e5da18282158d20f Mon Sep 17 00:00:00 2001 From: yflory Date: Thu, 26 Oct 2017 12:31:16 +0200 Subject: [PATCH 13/46] Add thumbnails to framework apps --- .../src/less2/include/icon-colors.less | 13 +++++++++ www/code/inner.js | 13 ++++++++- www/common/common-thumbnail.js | 29 ++++++++++++------- www/common/sframe-app-framework.js | 13 ++++----- www/common/sframe-common-interface.js | 1 + www/drive/app-drive.less | 23 ++++++++++----- www/drive/inner.js | 10 +++++-- www/pad/inner.js | 20 ++++++++++++- www/slide/inner.js | 26 +++++++++++++++-- 9 files changed, 116 insertions(+), 32 deletions(-) diff --git a/customize.dist/src/less2/include/icon-colors.less b/customize.dist/src/less2/include/icon-colors.less index 345a48a9a..db9de2a14 100644 --- a/customize.dist/src/less2/include/icon-colors.less +++ b/customize.dist/src/less2/include/icon-colors.less @@ -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; } } diff --git a/www/code/inner.js b/www/code/inner.js index 7fd66721e..7296d07d3 100644 --- a/www/code/inner.js +++ b/www/code/inner.js @@ -346,7 +346,18 @@ define([ Framework.create({ toolbarContainer: '#cme_toolbox', contentContainer: '#cp-app-code-editor', - getThumbnailContainer: getThumbnailContainer + 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) { diff --git a/www/common/common-thumbnail.js b/www/common/common-thumbnail.js index a2deea346..2ab07cb75 100644 --- a/www/common/common-thumbnail.js +++ b/www/common/common-thumbnail.js @@ -4,6 +4,7 @@ define([ var Nacl = window.nacl; var Thumb = { dimension: 100, + padDimension: 200 }; var supportedTypes = [ @@ -46,25 +47,28 @@ define([ 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) { + /*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, @@ -74,6 +78,7 @@ define([ var newH = Math.floor(h*r); d = Math.floor((dim - newH) / 2); return { + dim: dim, x: 0, w: dim, y: d, @@ -88,8 +93,8 @@ define([ var c2 = document.createElement('canvas'); 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); @@ -147,7 +152,8 @@ define([ PDFJS.getDocument(url).promise .then(function (doc) { return doc.getPage(1).then(makeThumb).then(function (canvas) { - cb(void 0, canvas.toDataURL()); + var D = getResizedDimensions(canvas, 'pdf'); + Thumb.fromCanvas(canvas, D, cb); }); }).catch(function () { cb('ERROR'); @@ -165,17 +171,20 @@ define([ }; window.html2canvas = undefined; - Thumb.fromDOM = function (element, cb) { + Thumb.fromDOM = function (opts, cb) { + var element = opts.getContainer(); var todo = function () { - html2canvas(element, { + if (opts.filter) { opts.filter(element, true); } + window.html2canvas(element, { allowTaint: true, onrendered: function (canvas) { - var D = getResizedDimensions(canvas, 'image'); + if (opts.filter) { opts.filter(element, false); } + var D = getResizedDimensions(canvas, 'pad'); Thumb.fromCanvas(canvas, D, cb); } }); }; - if (html2canvas) { return void todo(); } + if (window.html2canvas) { return void todo(); } require(['/bower_components/html2canvas/build/html2canvas.min.js'], todo); }; diff --git a/www/common/sframe-app-framework.js b/www/common/sframe-app-framework.js index 3f4772514..b6dfb6e58 100644 --- a/www/common/sframe-app-framework.js +++ b/www/common/sframe-app-framework.js @@ -268,10 +268,11 @@ define([ Cryptpad.removeLoadingScreen(emitResize); - if (options.getThumbnailContainer) { + if (options.thumbnail) { var oldThumbnailState; var privateDat = cpNfInner.metadataMgr.getPrivateData(); - var hash = privateDat.availableHashes.editHash || privateDat.availableHashes.viewHash; + var hash = privateDat.availableHashes.editHash || + privateDat.availableHashes.viewHash; var href = privateDat.pathname + '#' + hash; var mkThumbnail = function () { if (!hash) { return; } @@ -279,13 +280,9 @@ define([ if (!cpNfInner.chainpad) { return; } var content = cpNfInner.chainpad.getUserDoc(); if (content === oldThumbnailState) { return; } - var el = options.getThumbnailContainer(); - if (!el) { return; } - $(el).parents().css('overflow', 'visible'); - Thumb.fromDOM(el, function (err, b64) { + Thumb.fromDOM(options.thumbnail, function (err, b64) { oldThumbnailState = content; - $(el).parents().css('overflow', ''); - SFUI.setPadThumbnail(href, b64) + SFUI.setPadThumbnail(href, b64); }); }; window.setInterval(mkThumbnail, 5000); diff --git a/www/common/sframe-common-interface.js b/www/common/sframe-common-interface.js index 6835039cb..e023e8f11 100644 --- a/www/common/sframe-common-interface.js +++ b/www/common/sframe-common-interface.js @@ -70,6 +70,7 @@ define([ // 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); }); diff --git a/www/drive/app-drive.less b/www/drive/app-drive.less index 8def8763a..2cc04be9c 100644 --- a/www/drive/app-drive.less +++ b/www/drive/app-drive.less @@ -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; } } } diff --git a/www/drive/inner.js b/www/drive/inner.js index fe06dca69..5bcc70f45 100644 --- a/www/drive/inner.js +++ b/www/drive/inner.js @@ -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 = $('', {'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 = $('', {'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 = $('', { '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); }; @@ -2197,7 +2203,7 @@ define([ } $content.append($info).append($dirContent); - var $truncated = $('', {'class': 'cp-app-drive-element-truncated'}).text('...'); + /*var $truncated = $('', {'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); diff --git a/www/pad/inner.js b/www/pad/inner.js index edbb3c25a..34340c0a8 100644 --- a/www/pad/inner.js +++ b/www/pad/inner.js @@ -552,7 +552,25 @@ define([ nThen(function (waitFor) { Framework.create({ toolbarContainer: '#cke_1_toolbox', - contentContainer: '#cke_1_contents' + contentContainer: '#cke_1_contents', + thumbnail: { + getContainer: function () { return $('iframe').contents().find('html')[0]; }, + filter: function (el, before) { + if (before) { + $(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'); + } + } }, waitFor(function (fw) { window.APP.framework = framework = fw; })); nThen(function (waitFor) { diff --git a/www/slide/inner.js b/www/slide/inner.js index 2dd47105f..7c706ede2 100644 --- a/www/slide/inner.js +++ b/www/slide/inner.js @@ -467,6 +467,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 +485,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) { From 83da9cf752b996cdd60f9b5e7c74f53c35821d49 Mon Sep 17 00:00:00 2001 From: Evilham Date: Thu, 26 Oct 2017 20:29:14 +0200 Subject: [PATCH 14/46] Moved colours to colortheme.less to enable theming --- customize.dist/src/less2/include/colortheme.less | 6 +++++- www/pad/app-pad.less | 2 +- www/poll/app-poll.less | 6 +++--- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/customize.dist/src/less2/include/colortheme.less b/customize.dist/src/less2/include/colortheme.less index e5e57cc74..d3db9e7cd 100644 --- a/customize.dist/src/less2/include/colortheme.less +++ b/customize.dist/src/less2/include/colortheme.less @@ -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; diff --git a/www/pad/app-pad.less b/www/pad/app-pad.less index 258f4f66a..086b27f81 100644 --- a/www/pad/app-pad.less +++ b/www/pad/app-pad.less @@ -16,7 +16,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; diff --git a/www/poll/app-poll.less b/www/poll/app-poll.less index 7d2cf1216..51976b925 100644 --- a/www/poll/app-poll.less +++ b/www/poll/app-poll.less @@ -14,15 +14,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%); From 23c305f71fc7b8bca051307a7096a20847fa0497 Mon Sep 17 00:00:00 2001 From: ansuz Date: Fri, 27 Oct 2017 10:37:44 +0200 Subject: [PATCH 15/46] implement removeItem so localForage doesn't complain --- www/common/sframe-boot2.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/www/common/sframe-boot2.js b/www/common/sframe-boot2.js index dd0370ca8..9b0f055b3 100644 --- a/www/common/sframe-boot2.js +++ b/www/common/sframe-boot2.js @@ -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; }; From 69890ebd8fd5e2afa512186036a2e32833bd8e38 Mon Sep 17 00:00:00 2001 From: ansuz Date: Fri, 27 Oct 2017 10:43:44 +0200 Subject: [PATCH 16/46] prototype alternate datastructure for trees in listmap --- www/common/flat-dom.js | 79 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 www/common/flat-dom.js diff --git a/www/common/flat-dom.js b/www/common/flat-dom.js new file mode 100644 index 000000000..c6b2bc4d9 --- /dev/null +++ b/www/common/flat-dom.js @@ -0,0 +1,79 @@ +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; + }; + + 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) { + if (!el || el.attributes) { return void console.error(el); } + var id = uid(); + if (!el.tagName && el.nodeType === Node.TEXT_NODE) { + data.map[id] = el.textContent; + return id; + } + data.map[id] = [ + el.tagName, + getAttrs(el), + slice(el.childNodes).map(function (e) { + return process(e); + }) + ]; + return id; + }; + + data.root = process(dom); + return data; + }; + + Flat.toDOM = function (data) { + var visited = {}; + var process = function (key) { + 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; +}); From df1a700cb20e560722d7c8f553ada73287857fc7 Mon Sep 17 00:00:00 2001 From: ansuz Date: Fri, 27 Oct 2017 11:01:22 +0200 Subject: [PATCH 17/46] disable thumbnail test. add test for flat dom --- www/assert/main.js | 16 +++++++++++++++- www/common/flat-dom.js | 10 +++++++--- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/www/assert/main.js b/www/assert/main.js index 0913674ae..ad7883de3 100644 --- a/www/assert/main.js +++ b/www/assert/main.js @@ -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; @@ -241,6 +242,7 @@ define([ return cb(true); }, "version 2 hash failed to parse correctly"); +/* assert(function (cb) { var getBlob = function (url, cb) { var xhr = new XMLHttpRequest(); @@ -266,9 +268,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(//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; diff --git a/www/common/flat-dom.js b/www/common/flat-dom.js index c6b2bc4d9..51c3d07b3 100644 --- a/www/common/flat-dom.js +++ b/www/common/flat-dom.js @@ -17,6 +17,7 @@ define([], function () { return data; }; + var identity = function (x) { return x; }; Flat.fromDOM = function (dom) { var data = { map: {}, @@ -26,18 +27,20 @@ define([], function () { var uid = function () { return i++; }; var process = function (el) { - if (!el || el.attributes) { return void console.error(el); } - var id = uid(); + 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; }; @@ -49,6 +52,7 @@ define([], function () { 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'); From d644054e3f4e2aaff5c5ba972abb2abe197c4c80 Mon Sep 17 00:00:00 2001 From: ansuz Date: Fri, 27 Oct 2017 11:13:21 +0200 Subject: [PATCH 18/46] lint compliance --- www/assert/main.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/www/assert/main.js b/www/assert/main.js index ad7883de3..0b759c99b 100644 --- a/www/assert/main.js +++ b/www/assert/main.js @@ -278,7 +278,7 @@ define([ // recreate a _mostly_ equivalent DOM var dom = Flat.toDOM(flat); // assume we don't care about comments - var bodyText = document.body.outerHTML.replace(//g, '') + var bodyText = document.body.outerHTML.replace(//g, ''); // check for equality cb(dom.outerHTML === bodyText); }); From 1245b4d2448d4227ce97e070f807564037975ef2 Mon Sep 17 00:00:00 2001 From: yflory Date: Fri, 27 Oct 2017 13:31:41 +0200 Subject: [PATCH 19/46] Enable thumbnails in poll and whiteboard --- www/common/common-thumbnail.js | 5 +-- www/common/sframe-app-framework.js | 2 +- www/poll/inner.js | 50 ++++++++++++++++++++++++++++++ www/whiteboard/inner.js | 27 ++++++++++++++++ 4 files changed, 81 insertions(+), 3 deletions(-) diff --git a/www/common/common-thumbnail.js b/www/common/common-thumbnail.js index 2ab07cb75..fb7edafc4 100644 --- a/www/common/common-thumbnail.js +++ b/www/common/common-thumbnail.js @@ -4,7 +4,8 @@ define([ var Nacl = window.nacl; var Thumb = { dimension: 100, - padDimension: 200 + padDimension: 200, + UPDATE_INTERVAL: 5000 }; var supportedTypes = [ @@ -43,7 +44,7 @@ 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; diff --git a/www/common/sframe-app-framework.js b/www/common/sframe-app-framework.js index b6dfb6e58..87ba6ee2c 100644 --- a/www/common/sframe-app-framework.js +++ b/www/common/sframe-app-framework.js @@ -285,7 +285,7 @@ define([ SFUI.setPadThumbnail(href, b64); }); }; - window.setInterval(mkThumbnail, 5000); + window.setInterval(mkThumbnail, Thumb.UPDATE_INTERVAL); } if (newPad) { diff --git a/www/poll/inner.js b/www/poll/inner.js index eb62cddcd..3f66a965d 100644 --- a/www/poll/inner.js +++ b/www/poll/inner.js @@ -14,6 +14,8 @@ 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', @@ -41,6 +43,8 @@ define([ Renderer, DiffMd, SframeCM, + SFUI, + Thumb, CMeditor) { var Messages = Cryptpad.Messages; @@ -790,6 +794,51 @@ define([ updateComments(); }; + var initThumbnails = function () { + var oldThumbnailState; + var privateDat = metadataMgr.getPrivateData(); + var hash = privateDat.availableHashes.editHash || + privateDat.availableHashes.viewHash; + var href = privateDat.pathname + '#' + hash; + var $el = $('.cp-app-poll-realtime'); + //var $el = $('#cp-app-poll-table'); + var options = { + getContainer: function () { return $el[0]; }, + filter: function (el, before) { + if (before) { + $el.parents().css('overflow', 'visible'); + $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', ''); + } + }; + var mkThumbnail = function () { + if (!hash) { return; } + if (!APP.proxy) { return; } + var content = JSON.stringify(APP.proxy.content); + if (content === oldThumbnailState) { return; } + Thumb.fromDOM(options, function (err, b64) { + oldThumbnailState = content; + SFUI.setPadThumbnail(href, b64); + }); + }; + window.setInterval(mkThumbnail, Thumb.UPDATE_INTERVAL); + }; + var checkDeletedCells = function () { // faster than forEach? var c; @@ -938,6 +987,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 diff --git a/www/whiteboard/inner.js b/www/whiteboard/inner.js index 626ad1a90..efa45233e 100644 --- a/www/whiteboard/inner.js +++ b/www/whiteboard/inner.js @@ -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, @@ -372,6 +374,27 @@ define([ onLocal(); }; + var initThumbnails = function () { + var oldThumbnailState; + var privateDat = metadataMgr.getPrivateData(); + 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; + SFUI.setPadThumbnail(href, b64); + }); + }; + window.setInterval(mkThumbnail, Thumb.UPDATE_INTERVAL); + }; + config.onInit = function (info) { updateLocalPalette(palette); readOnly = metadataMgr.getPrivateData().readOnly; @@ -532,6 +555,10 @@ define([ initializing = false; config.onLocal(); Cryptpad.removeLoadingScreen(); + + initThumbnails(); + + if (readOnly) { return; } if (isNew) { common.openTemplatePicker(); From 1cbf1aec92cdc202e4cd83f8e4adedc448afdbab Mon Sep 17 00:00:00 2001 From: ansuz Date: Fri, 27 Oct 2017 14:14:19 +0200 Subject: [PATCH 20/46] prevent undefined access in non-sframe apps --- www/common/common-interface.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/www/common/common-interface.js b/www/common/common-interface.js index 644efded2..f03b4b481 100644 --- a/www/common/common-interface.js +++ b/www/common/common-interface.js @@ -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(); } From 4c0049ad5553f602e4076e40cf692512ea1d36a3 Mon Sep 17 00:00:00 2001 From: ansuz Date: Fri, 27 Oct 2017 14:20:31 +0200 Subject: [PATCH 21/46] don't log presence of other users as 'joins' when you have first joined --- www/common/toolbar3.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/www/common/toolbar3.js b/www/common/toolbar3.js index 29d78e91c..be8e6399f 100644 --- a/www/common/toolbar3.js +++ b/www/common/toolbar3.js @@ -921,6 +921,7 @@ define([ return count; }; + var joined = false; metadataMgr.onChange(function () { var newdata = metadataMgr.getMetadata().users; var netfluxIds = Object.keys(newdata); @@ -949,7 +950,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)) { @@ -960,6 +961,7 @@ define([ } } } + joined = true; oldUserData = JSON.parse(JSON.stringify(newdata)); }); } From 4933aafbc8cd8f83339469df7c6fa4bdbc64a1c0 Mon Sep 17 00:00:00 2001 From: Evilham Date: Fri, 27 Oct 2017 18:31:52 +0200 Subject: [PATCH 22/46] Added default values to avoid breaking existing themes. --- www/pad/app-pad.less | 4 ++++ www/poll/app-poll.less | 6 ++++++ 2 files changed, 10 insertions(+) diff --git a/www/pad/app-pad.less b/www/pad/app-pad.less index 086b27f81..55a50e28e 100644 --- a/www/pad/app-pad.less +++ b/www/pad/app-pad.less @@ -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'; diff --git a/www/poll/app-poll.less b/www/poll/app-poll.less index 51976b925..a2b3838f9 100644 --- a/www/poll/app-poll.less +++ b/www/poll/app-poll.less @@ -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"; From 0a14c715ad0188001a7b7008782afbed1f4c8b25 Mon Sep 17 00:00:00 2001 From: ansuz Date: Mon, 30 Oct 2017 14:40:43 +0100 Subject: [PATCH 23/46] add test for support of invite urls --- www/assert/main.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/www/assert/main.js b/www/assert/main.js index 0b759c99b..3eb9e5892 100644 --- a/www/assert/main.js +++ b/www/assert/main.js @@ -237,6 +237,14 @@ 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); From 68accaf653d6facaaa8e4d459ccd804be9a05a4b Mon Sep 17 00:00:00 2001 From: yflory Date: Mon, 30 Oct 2017 15:12:15 +0100 Subject: [PATCH 24/46] Todo in sframe with less2 --- customize.dist/src/less2/main.less | 1 + .../example/assets/todomvc-app-css/index.css | 378 ++++++++++++++++++ .../example/assets/todomvc-common/base.css | 141 +++++++ .../example/assets/todomvc-common/base.js | 249 ++++++++++++ www/oldtodo/example/index.html | 49 +++ www/oldtodo/example/js/app.js | 25 ++ www/oldtodo/example/js/controller.js | 270 +++++++++++++ www/oldtodo/example/js/helpers.js | 52 +++ www/oldtodo/example/js/model.js | 120 ++++++ www/oldtodo/example/js/store.js | 141 +++++++ www/oldtodo/example/js/template.js | 114 ++++++ www/oldtodo/example/js/view.js | 219 ++++++++++ www/oldtodo/index.html | 30 ++ www/oldtodo/inner.html | 20 + www/oldtodo/inner.js | 15 + www/oldtodo/main.js | 229 +++++++++++ www/oldtodo/todo.js | 83 ++++ www/{todo => oldtodo}/todo.less | 0 www/todo/app-todo.less | 130 ++++++ www/todo/index.html | 18 +- www/todo/inner.html | 22 +- www/todo/inner.js | 236 ++++++++++- www/todo/main.js | 270 +++---------- www/todo/todo.js | 5 +- 24 files changed, 2561 insertions(+), 256 deletions(-) create mode 100644 www/oldtodo/example/assets/todomvc-app-css/index.css create mode 100644 www/oldtodo/example/assets/todomvc-common/base.css create mode 100644 www/oldtodo/example/assets/todomvc-common/base.js create mode 100644 www/oldtodo/example/index.html create mode 100644 www/oldtodo/example/js/app.js create mode 100644 www/oldtodo/example/js/controller.js create mode 100644 www/oldtodo/example/js/helpers.js create mode 100644 www/oldtodo/example/js/model.js create mode 100644 www/oldtodo/example/js/store.js create mode 100644 www/oldtodo/example/js/template.js create mode 100644 www/oldtodo/example/js/view.js create mode 100644 www/oldtodo/index.html create mode 100644 www/oldtodo/inner.html create mode 100644 www/oldtodo/inner.js create mode 100644 www/oldtodo/main.js create mode 100644 www/oldtodo/todo.js rename www/{todo => oldtodo}/todo.less (100%) create mode 100644 www/todo/app-todo.less diff --git a/customize.dist/src/less2/main.less b/customize.dist/src/less2/main.less index 85a906902..c000248ba 100644 --- a/customize.dist/src/less2/main.less +++ b/customize.dist/src/less2/main.less @@ -33,4 +33,5 @@ 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"; } diff --git a/www/oldtodo/example/assets/todomvc-app-css/index.css b/www/oldtodo/example/assets/todomvc-app-css/index.css new file mode 100644 index 000000000..e6e089cbf --- /dev/null +++ b/www/oldtodo/example/assets/todomvc-app-css/index.css @@ -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,'); +} + +.todo-list li .toggle:checked:after { + content: url('data:image/svg+xml;utf8,'); +} + +.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; + } +} diff --git a/www/oldtodo/example/assets/todomvc-common/base.css b/www/oldtodo/example/assets/todomvc-common/base.css new file mode 100644 index 000000000..da65968a7 --- /dev/null +++ b/www/oldtodo/example/assets/todomvc-common/base.css @@ -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; + } +} diff --git a/www/oldtodo/example/assets/todomvc-common/base.js b/www/oldtodo/example/assets/todomvc-common/base.js new file mode 100644 index 000000000..3c6723f39 --- /dev/null +++ b/www/oldtodo/example/assets/todomvc-common/base.js @@ -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); +})(); diff --git a/www/oldtodo/example/index.html b/www/oldtodo/example/index.html new file mode 100644 index 000000000..09070b71d --- /dev/null +++ b/www/oldtodo/example/index.html @@ -0,0 +1,49 @@ + + + + + Crypt Todo + + + + +
+
+

todos

+ +
+
+ + +
    +
    + +
    +
    + +
    + + + + + + + + + + + diff --git a/www/oldtodo/example/js/app.js b/www/oldtodo/example/js/app.js new file mode 100644 index 000000000..c37e2e6a2 --- /dev/null +++ b/www/oldtodo/example/js/app.js @@ -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); +})(); diff --git a/www/oldtodo/example/js/controller.js b/www/oldtodo/example/js/controller.js new file mode 100644 index 000000000..0a3fb1d83 --- /dev/null +++ b/www/oldtodo/example/js/controller.js @@ -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); diff --git a/www/oldtodo/example/js/helpers.js b/www/oldtodo/example/js/helpers.js new file mode 100644 index 000000000..d59a72eff --- /dev/null +++ b/www/oldtodo/example/js/helpers.js @@ -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); diff --git a/www/oldtodo/example/js/model.js b/www/oldtodo/example/js/model.js new file mode 100644 index 000000000..9da766abc --- /dev/null +++ b/www/oldtodo/example/js/model.js @@ -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); diff --git a/www/oldtodo/example/js/store.js b/www/oldtodo/example/js/store.js new file mode 100644 index 000000000..ea09816c8 --- /dev/null +++ b/www/oldtodo/example/js/store.js @@ -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); diff --git a/www/oldtodo/example/js/template.js b/www/oldtodo/example/js/template.js new file mode 100644 index 000000000..a5587731f --- /dev/null +++ b/www/oldtodo/example/js/template.js @@ -0,0 +1,114 @@ +/*jshint laxbreak:true */ +(function (window) { + 'use strict'; + + var htmlEscapes = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + '\'': ''', + '`': '`' + }; + + 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 + = '
  • ' + + '
    ' + + '' + + '' + + '' + + '
    ' + + '
  • '; + } + + /** + * Creates an
  • 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
  • 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 '' + activeTodos + ' 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); diff --git a/www/oldtodo/example/js/view.js b/www/oldtodo/example/js/view.js new file mode 100644 index 000000000..d9f59611e --- /dev/null +++ b/www/oldtodo/example/js/view.js @@ -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)); diff --git a/www/oldtodo/index.html b/www/oldtodo/index.html new file mode 100644 index 000000000..a72a3c60b --- /dev/null +++ b/www/oldtodo/index.html @@ -0,0 +1,30 @@ + + + + CryptPad + + + + + + + + diff --git a/www/oldtodo/inner.html b/www/oldtodo/inner.html new file mode 100644 index 000000000..d147387ac --- /dev/null +++ b/www/oldtodo/inner.html @@ -0,0 +1,20 @@ + + + + + + + + + +
    +
    +
    + + +
    +
    +
    + + + diff --git a/www/oldtodo/inner.js b/www/oldtodo/inner.js new file mode 100644 index 000000000..79ef5783d --- /dev/null +++ b/www/oldtodo/inner.js @@ -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);*/ +}); diff --git a/www/oldtodo/main.js b/www/oldtodo/main.js new file mode 100644 index 000000000..abb732661 --- /dev/null +++ b/www/oldtodo/main.js @@ -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 $('', { + '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 = $('
    ', { + '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'); + } + + $('', { 'class': 'cp-task-text' }) + .text(entry.task) + .appendTo($taskDiv); + /*$('', { 'class': 'cp-task-date' }) + .text(new Date(entry.ctime).toLocaleString()) + .appendTo($taskDiv);*/ + $('