From f92d33beced54ca7f657566e95a0018a9687f3a4 Mon Sep 17 00:00:00 2001 From: Saimon Moore Date: Thu, 30 Sep 2010 12:16:24 +0200 Subject: [PATCH 01/29] Adding deps make target to install node deps via npm --- Makefile | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Makefile b/Makefile index 916af87..66e9ed3 100644 --- a/Makefile +++ b/Makefile @@ -3,3 +3,9 @@ css: crontab: sudo crontab etc/thingler.cron -u couchdb + +deps: + test -e $(which npm) && echo 'npm already installed.' || curl http://npmjs.org/install.sh | sh + test -e $(which npm) && npm install cradle || echo 'npm not installed. install via "curl http://npmjs.org/install.sh | sh"' + test -e $(which npm) && npm install journey || echo 'npm not installed. install via "curl http://npmjs.org/install.sh | sh"' + test -e $(which npm) && npm install node-static || echo 'npm not installed. install via "curl http://npmjs.org/install.sh | sh"' From c72ffbd6d9b8043b1d37a440296f1a22ab7b7937 Mon Sep 17 00:00:00 2001 From: Saimon Moore Date: Thu, 30 Sep 2010 12:19:08 +0200 Subject: [PATCH 02/29] Small blurb to readme for getting devs up to speed --- README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/README.md b/README.md index de9bf98..5fc283e 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,13 @@ thingler > Hello, I am [Thingler](http://thingler.com), and this is my source code. Feel free to browse. +forkers +======= + +If you want to hack on Thingler, install couchdb, node.js (I recommend following this gist: http://gist.github.com/579814) +Then run make deps. + + License ------- From 8f34aa428ef091787a3ae57db86423a7d151f7ac Mon Sep 17 00:00:00 2001 From: Saimon Moore Date: Thu, 30 Sep 2010 12:47:36 +0200 Subject: [PATCH 03/29] Updating css from production until I figurte out why lessc is complaining --- pub/css/thingler.css | 88 ++++++++++++++++++++++++++++++-------------- 1 file changed, 60 insertions(+), 28 deletions(-) diff --git a/pub/css/thingler.css b/pub/css/thingler.css index f8b3bac..47d888f 100644 --- a/pub/css/thingler.css +++ b/pub/css/thingler.css @@ -1,44 +1,76 @@ *{margin:0;padding:0;} -body{padding:60px 30px;font-family:'Arial',sans-serif;font-size:24px;width:800px;margin:0 auto;} -body>div{display:none;} -a{text-decoration:none;color:#cccccc;} -#not-found{padding:60px 0;}#not-found h1{text-align:center;font-size:48px;} -#not-found p{text-align:center;margin-top:30px;} -#not-found a{border-bottom:1px solid #eeeeee;font-family:Arial,sans-serif;color:#aaaaaa;}#not-found a:hover{color:#cc7766;border-bottom:1px solid #f9eeec;} -#about{margin-top:-30px;font-size:14px;float:left;} +body{font-family:'Arial',sans-serif;font-size:24px;width:800px;padding:60px 30px;margin:0 auto;background-color:#fafafa;} +body>header{position:absolute;width:800px;top:22px;color:#dddddd;display:block;margin:0;font-size:16px;} +body>footer{visibility:hidden;margin:30px 0;text-align:right;font-size:14px;color:#eaeaea;}body>footer p{margin:10px 0;} +body>footer p:first-child{color:#dddddd;font-size:18px;}body>footer p:first-child:hover,body>footer p:first-child:hover a{color:#c9c9c9;}body>footer p:first-child:hover a:hover,body>footer p:first-child:hover a a:hover{color:#ee8167;border-bottom:1px solid #f9eeec;} +body>footer a{color:#dddddd;border-bottom:1px solid #eee;}body>footer a:hover{color:#ee8167;border-bottom:1px solid #f9eeec;} h1{color:#3d4a5c;font-size:42px;} ul,li{list-style-type:none;padding:0;} -body>header{position:absolute;width:800px;top:15px;color:#dddddd;display:block;padding:15px 0;margin:0;font-size:16px;} -body>footer{visibility:hidden;margin:30px 0;text-align:right;font-size:14px;color:#eaeaea;}body>footer p{margin:10px 0;} -body>footer p:first-child{color:#dddddd;font-size:18px;}body>footer p:first-child:hover,body>footer p:first-child:hover a{color:#c9c9c9;}body>footer p:first-child:hover a:hover,body>footer p:first-child:hover a a:hover{color:#cc7766;border-bottom:1px solid #f9eeec;} -body>footer a{color:#dddddd;border-bottom:1px solid #eee;}body>footer a:hover{color:#cc7766;border-bottom:1px solid #f9eeec;} ul{-webkit-padding-start:0;} -#page{width:800px;}#page #title{border:0;font-size:48px;font-weight:bold;border:1px dashed transparent;color:#3d4a5c;font-family:'Arial',sans-serif;margin-bottom:15px;margin-left:-1px;padding:0.25em 1px;width:796px;}#page #title:focus{outline:none;border-color:#dddddd;} -#page a#show-everything{font-size:14px;margin:0;padding:0;float:right;text-align:right;margin-top:15px;margin-right:95px;border-bottom:1px solid #eeeeee;font-family:Arial,sans-serif;color:#cccccc;}#page a#show-everything:hover{color:#cc7766;border-bottom:1px solid #f9eeec;} +a{text-decoration:none;color:#cccccc;} +input[type="text"],input[type="password"]{outline:none;} +input[type="checkbox"]{font-size:22px;} +#page{width:800px;}#page #title{font-size:48px;font-weight:bold;font-family:'Arial',sans-serif;border:0;border:1px dashed transparent;color:#3d4a5c;margin-bottom:15px;margin-left:-1px;padding:0.25em 1px;width:796px;background-color:#fafafa;}#page #title:focus{outline:none;border-color:#dddddd;} +#page #title:focus{background-color:white;} #page ul#list.unselectable li{cursor:default;user-select:none;-moz-user-select:none;-webkit-user-select:none;} #page label,#page label a,#page label a:visited{color:#3d4a5c;text-decoration:none;display:inline-block;line-height:48px;position:relative;} #page label{max-width:670px;} #page li.flashing label:after{visibility:hidden;} -#page ul#list{margin:30px 0;width:800px;padding:0;} -#page #list>li{overflow:hidden;font-size:22px;width:790px;font-family:Georgia,'Times New Roman',serif;padding:0 5px;cursor:move;background-color:white;height:auto;border-bottom:1px dotted #dddddd;}#page #list>li:hover{background-color:#fefef1;}#page #list>li:hover .actions{visibility:visible;} +#page #list{margin:30px 0;width:800px;padding:0;} +#page #list>li{overflow:hidden;font-size:22px;font-family:Georgia,'Times New Roman',serif;width:788px;padding:0 5px;cursor:move;border-color:transparent;border-style:solid;border-width:0 1px 0px 1px;border-bottom:1px dotted #dddddd;background-color:#fafafa;height:auto;}#page #list>li a{color:#707d8f;}#page #list>li a:hover{text-decoration:underline;} +#page #list>li:hover{background-color:#fefef1;}#page #list>li:hover .actions{visibility:visible;} #page #list>li:hover label:after{background-color:#fefef1;-webkit-box-shadow:-15px 0px 30px #fefef1;-moz-box-shadow:-15px 0px 30px #fefef1;box-shadow:-15px 0px 30px #fefef1;} -#page #list>li input{vertical-align:middle;height:48px;line-height:48px;float:left;display:block;margin-right:25px;} -#page #list>li:hover .tags li a{color:#bcbc9a;background-color:white;} -#page #list>li .tags{float:right;display:inline;line-height:48px;height:48px;font-size:16px;margin:0 15px;}#page #list>li .tags li{display:inline-block;margin-left:5px;}#page #list>li .tags li a{padding:0;margin:0;line-height:1em;margin:auto 0;padding:4px 8px;border-width:1px;border-style:solid;border-color:#f6f6df;border-bottom-color:#fcfcf3;background-color:#fefef1;display:inline-block;color:#c6c6a9;-webkit-box-shadow:0 1px 2px #eeeeee;-moz-box-shadow:0 1px 2px #eeeeee;box-shadow:0 1px 2px #eeeeee;-webkit-border-radius:2px;-moz-border-radius:2px;border-radius:2px;}#page #list>li .tags li a:hover{color:#b1b18b;border-color:#ededc0;} +#page #list>li.editing{background-color:white;border:1px dashed #dddddd !important;margin-top:-1px;}#page #list>li.editing .tags{display:none;} +#page #list>li.editing .actions{visibility:visible;} +#page #list>li.editing [data-action="edit"]{color:#ee8167;} +#page #list>li.editing:last-child{margin-bottom:-1px;} +#page #list>li input[type="text"]{font-family:Georgia,'Times New Roman',serif;font-size:22px;margin-left:-1px;border:0;background-color:transparent;height:48px;width:650px;} +#page #list>li input[type="checkbox"]{vertical-align:middle;height:48px;line-height:48px;float:left;display:block;margin-right:25px;} +#page #list>li:hover .tags li a{color:#bcbc9a;background-color:#fafafa;} +#page #list>li.editing .token{color:#a7a77b !important;} +#page #list>li .tags{float:right;display:inline;line-height:48px;height:48px;margin:0 15px;}#page #list>li .tags li{display:inline-block;margin-left:5px;}#page #list>li .tags li a{color:#c6c6a9;padding:0;margin:auto 0;line-height:1em;padding:4px 8px;border-width:1px;border-style:solid;border-color:#f4f4d7;background-color:#fefef1;display:inline-block;-webkit-box-shadow:0 1px 2px #eeeeee;-moz-box-shadow:0 1px 2px #eeeeee;box-shadow:0 1px 2px #eeeeee;-webkit-border-radius:2px;-moz-border-radius:2px;border-radius:2px;}#page #list>li .tags li a:hover{text-decoration:none;color:#b1b18b;border-color:#ededc0;} #page #list>li .tags li.active a{border-color:#ededc0;color:#b1b18b;} #page #list>li .tags li:first-child{margin-left:0;} -#page #list>li .actions{visibility:hidden;float:right;font-size:14px;height:48px;line-height:48px;}#page #list>li .actions a{border-bottom:1px solid #f4f4f4;font-family:Arial,sans-serif;margin-right:15px;text-decoration:none;color:#cccccc;}#page #list>li .actions a:hover{color:#cc7766;border-bottom:1px solid #f9eeec;} +#page #list>li .actions{visibility:hidden;float:right;font-size:14px;height:48px;line-height:48px;}#page #list>li .actions a{padding:8px;border:0 !important;border-bottom:1px solid #f4f4f4;font-family:Arial,sans-serif;text-decoration:none;color:#cccccc;}#page #list>li .actions a:hover{border:0 !important;} +#page #list>li .actions a:last-child{margin-right:10px;} +#page #list>li .actions a:hover{color:#ee8167;} #page #list>li:last-child{border-bottom:0;} -#page .completed label,#page .completed label a{color:#ccc;text-decoration:line-through;} -input#new{width:782px;-webkit-box-shadow:0px 1px 2px #dddddd;-moz-box-shadow:0px 1px 2px #dddddd;box-shadow:0px 1px 2px #dddddd;}input#new:focus{-webkit-box-shadow:0px 2px 8px #dddddd;-moz-box-shadow:0px 2px 8px #dddddd;box-shadow:0px 2px 8px #dddddd;} -input#new,input[type="password"]{font-size:24px;padding:8px;outline:none;border:1px solid #dddddd;} -input[type="checkbox"]{font-size:22px;} -a[data-action="remove"]{padding:8px;border:0 !important;}a[data-action="remove"]:hover{border:0 !important;} -#about{display:none;margin-top:0px;text-align:right;}#about p{font-size:14px !important;color:#aaaaaa;line-height:22px;}#about p a{border-bottom:1px dashed #dddddd;font-family:Arial,sans-serif;color:#bebebe;}#about p a:hover{color:#cc7766;border-bottom:1px solid #f9eeec;} +#page .completed label,#page .completed label a{color:#ccc !important;text-decoration:line-through;} +#page .completed .tags li a{color:#ccc !important;background-color:#fafafa !important;border-color:#eee !important;}#page .completed .tags li a:hover{color:#bbb !important;} +input#new,input[type="password"],input.token-input{font-size:24px;padding:8px;border:1px solid #dddddd;}input#new.disabled,input[type="password"].disabled,input.token-input.disabled{color:#cccccc;background-color:#f0f0f0;} +input#new,span.string-input,input.token{font-family:'Lucida Grande',Arial,sans-serif;} +ul.tokens{margin:0;} +ul.tokens,ul.tokens li{display:inline-block;margin:0;} +span.string-input{font-size:24px;margin-left:1px;} +ul.tokens li:last-child .token-input{padding:0 5px !important;} +input.token-input{margin:0;padding:0 3px 0 1px;display:inline-block;outline:none;border:0;}input.token-input.empty{margin:0;width:5px;} +#list .token{color:#c6c6a9;padding:0;margin:auto 0;line-height:1em;padding:2px 1px;border-width:1px;border-style:solid;border-color:#f4f4d7;background-color:#fefef1;display:inline-block;-webkit-box-shadow:0 1px 2px #eeeeee;-moz-box-shadow:0 1px 2px #eeeeee;box-shadow:0 1px 2px #eeeeee;-webkit-border-radius:2px;-moz-border-radius:2px;border-radius:2px;padding-right:4px;margin-left:-2px;outline:none;font-size:inherit;font-family:inherit;}#list .token:hover{text-decoration:none;color:#b1b18b;border-color:#ededc0;} +#list .token-input{padding:0 3px 0 1px;margin:0;font-family:inherit;font-size:inherit;} +#new-wrapper{font-size:24px;padding:8px;cursor:text;width:782px;-webkit-box-shadow:0px 1px 2px #dddddd;-moz-box-shadow:0px 1px 2px #dddddd;box-shadow:0px 1px 2px #dddddd;border:1px solid #dddddd;background-color:white;}#new-wrapper.focused{-webkit-box-shadow:0px 2px 8px #dddddd;-moz-box-shadow:0px 2px 8px #dddddd;box-shadow:0px 2px 8px #dddddd;} +#new-wrapper input#new{width:766px;padding:0;border:0 !important;display:inline-block;margin:0;} +#new-wrapper .token{color:#c6c6a9;padding:0;margin:auto 0;line-height:1em;padding:2px 1px;border-width:1px;border-style:solid;border-color:#f4f4d7;background-color:#fefef1;display:inline-block;-webkit-box-shadow:0 1px 2px #eeeeee;-moz-box-shadow:0 1px 2px #eeeeee;box-shadow:0 1px 2px #eeeeee;-webkit-border-radius:2px;-moz-border-radius:2px;border-radius:2px;padding-right:4px;margin:-6px 0;margin-left:-2px;outline:none;font-size:24px;}#new-wrapper .token:hover{text-decoration:none;color:#b1b18b;border-color:#ededc0;} .dragging{position:absolute;z-index:10;background-color:rgba(255, 255, 229, 0.7) !important;width:800px;border:1px dashed #dddddd !important;-webkit-box-shadow:0px 2px 16px #dddddd;-moz-box-shadow:0px 2px 16px #dddddd;box-shadow:0px 2px 16px #dddddd;}.dragging div{cursor:move;} .dragging .actions{visibility:hidden;} .ghost{margin-top:-1px;border-bottom:1px dashed #dddddd !important;border-top:1px dashed #dddddd !important;}.ghost label{color:#dddddd !important;} -.ghost .tags{color:#dddddd !important;}.ghost .tags li{background-color:#fefefe !important;border-color:#eeeeee !important;} -div.password{position:absolute;top:90px;left:150px;width:600px;padding:60px;background-color:white;-webkit-box-shadow:0 0px 30px #dddddd;-moz-box-shadow:0 0px 30px #dddddd;box-shadow:0 0px 30px #dddddd;-webkit-border-radius:8px;-moz-border-radius:8px;border-radius:8px;}div.password label{font-weight:bold;color:#52637a;font-size:32px;display:block;padding-bottom:30px;} +.ghost .tags{color:#dddddd !important;}.ghost .tags li a{background-color:#fefefe !important;border-color:#eeeeee !important;} +#password-protect div.password{-webkit-border-radius:8px;-moz-border-radius:8px;border-radius:8px;background-color:transparent;}#password-protect div.password input{-webkit-box-shadow:0 3px 30px #dddddd;-moz-box-shadow:0 3px 30px #dddddd;box-shadow:0 3px 30px #dddddd;} +div.password{position:absolute;height:116px;top:40%;left:50%;width:600px;margin-left:-360px;margin-top:-118px;padding:60px;}div.password label{font-weight:bold;color:#52637a;font-size:32px;display:block;padding-bottom:30px;} div.password input{width:570px;-webkit-border-radius:8px;-moz-border-radius:8px;border-radius:8px;} -.overlay{position:fixed;top:0;left:0;width:100%;height:100%;background-color:rgba(255, 255, 255, 0.8);} +div.password input.error{border-color:#d6705c;} +div.password .close{color:#ccc;float:right;margin-top:30px;margin-right:16px;font-size:18px;text-shadow:0px 0px 10px white;border-bottom:1px solid #eee;}div.password .close:hover{color:#ee8167;} +#lock{text-align:right;float:right;}#lock div{display:inline-block;width:24px;height:24px;background:url(/images/lock-24.png) 0 0 no-repeat;margin-left:5px;} +#lock.locked{display:block;}#lock.locked:hover .locked-hint{display:inline;} +#lock.locked:hover .unlocked-hint{display:none;} +#lock.locked span{color:#ee8167;} +#lock.locked div{background:url(/images/lock-24.png) -24px 0 no-repeat;} +#lock:hover .locked-hint{display:none;} +#lock:hover .unlocked-hint{display:inline;} +#lock:hover div{background:url(/images/lock-24.png) -24px 0 no-repeat;} +#lock span{color:#ee8167;font-size:14px;display:none;vertical-align:1px;} +.overlay{position:fixed;top:0;left:0;width:100%;height:100%;z-index:10;} +#password-authenticate.overlay{background-color:#fafafa;} +#password-protect.overlay{background-color:rgba(255, 255, 255, 0.9);} +#not-found{padding:60px 0;}#not-found h1{text-align:center;font-size:48px;} +#not-found p{text-align:center;margin-top:30px;} +#not-found a{border-bottom:1px solid #eeeeee;font-family:Arial,sans-serif;color:#aaaaaa;}#not-found a:hover{color:#cc7766;border-bottom:1px solid #f9eeec;} +#about{font-size:14px;float:left;display:none;margin-top:0px;text-align:right;}#about p{font-size:14px !important;color:#aaaaaa;line-height:22px;}#about p a{border-bottom:1px dashed #dddddd;font-family:Arial,sans-serif;color:#bebebe;}#about p a:hover{color:#cc7766;border-bottom:1px solid #f9eeec;} From 81abaad39ea708e7a22fae701d7df569abb2775d Mon Sep 17 00:00:00 2001 From: Saimon Moore Date: Thu, 30 Sep 2010 16:45:33 +0200 Subject: [PATCH 04/29] Adding manifest attribute to public index file --- pub/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pub/index.html b/pub/index.html index dcb58de..76ad910 100644 --- a/pub/index.html +++ b/pub/index.html @@ -1,5 +1,5 @@ - + Thingler From 492458815d85cab8f5a062284ba53bf65cdaa16f Mon Sep 17 00:00:00 2001 From: Saimon Moore Date: Thu, 30 Sep 2010 17:33:02 +0200 Subject: [PATCH 05/29] Using absolute path for manifest file --- pub/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pub/index.html b/pub/index.html index 76ad910..1dc68f0 100644 --- a/pub/index.html +++ b/pub/index.html @@ -1,5 +1,5 @@ - + Thingler From d44a0183966e1a8a6f77d790e6505211d4104bda Mon Sep 17 00:00:00 2001 From: Saimon Moore Date: Thu, 30 Sep 2010 17:35:37 +0200 Subject: [PATCH 06/29] Adding ability to dynamically generate application manifest (will always regenerate in dev mode) --- src/index.js | 7 +++ src/offline.js | 117 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 124 insertions(+) create mode 100644 src/offline.js diff --git a/src/index.js b/src/index.js index 0673a88..50e83e5 100644 --- a/src/index.js +++ b/src/index.js @@ -10,6 +10,7 @@ var journey = require('journey'), var todo = require('./todo').resource, session = require('./session/session'), routes = require('./routes'); +var offline = require('./offline'); var options = { port: parseInt(process.argv[2]) || 8080, @@ -48,6 +49,12 @@ this.server = http.createServer(function (request, response) { if (/MSIE [0-7]/.test(request.headers['user-agent'])) { // Block old IE file.serveFile('/upgrade.html', 200, {}, request, response); clearTimeout(timer); + } else if (request.url === '/application.manifest') { + offline.cacheManifest(env, function(cached) { + //TODO return not-modified + file.serveFile('/application.manifest', 200, {"Content-Type": "text/cache-manifest"}, request, response); + clearTimeout(timer); + }); } else if (request.url === '/') { todo.create(function (id) { finish(303, { 'Location': '/' + id }); diff --git a/src/offline.js b/src/offline.js new file mode 100644 index 0000000..c73d3be --- /dev/null +++ b/src/offline.js @@ -0,0 +1,117 @@ +var fs = require('fs'); + +this.cacheManifest = function(env, callback, network, fallback) { + var config = { + cache: [], + network: ['/'], + fallback: [] + }; + + //augment network array ('/' is already present) + if (network && network.length > 0) { + for (var i = 0; i < network.length; i++) { + config.network.push(network[i]); + }; + } + + //augment fallback array (strings should be of the form: {namespace} {url}) + if (fallback && fallback.length > 0) { + for (var i = 0; i < fallback.length; i++) { + config.fallback.push(fallback[i]); + }; + } + + //check if we need to regenerate cache (if it doesn't exist or in dev mode) + fs.stat('./pub/application.manifest', function(err, stat) { + var regenerate = false; + + if (err) {regenerate = true;} + else { + regenerate = (env === 'development'); + } + + if (regenerate) { + config.cache = listFiles(); + + precache_key(config.cache, function(key) { + generateManifest(key, callback); + }); + } + else callback(true); + }); + + + //calculate the hask key from the files contents + var precache_key = function(paths, cb) { + var crypto = require('crypto'); + var hashes = []; + + for (var i = 0; i < paths.length; i++) { + var hash = crypto.createHash('sha1'); + hash.update(fs.readFileSync(paths[i])); + var fileHash = hash.digest('hex'); + hashes.push(fileHash); + + //Once we've hashed the last file, make a hash of all the hashes + if (i === (paths.length - 1)) { + hash = crypto.createHash('sha1'); + hash.update(hashes.join('')); + cb(hash.digest('hex')); + } + }; + }; + + //Generate the manifest and write it to disk + var generateManifest = function(key, cbk) { + var body = ["CACHE MANIFEST"]; + body.push("# " + key); + + for (var i = 0; i < config.cache.length; i++) { + body.push(config.cache[i].replace('./pub','')); + }; + + body.push(''); body.push('NETWORK:'); + + for (var j = 0; j < config.network.length; j++) { + body.push(config.network[j]); + }; + + body.push(''); body.push('FALLBACK:'); + + for (var k = 0; k < config.fallback.length; k++) { + body.push(config.fallback[k]); + }; + + fs.writeFile('./pub/application.manifest', body.join("\n"), function(err) { + if (err) { throw err;} + cbk(false); + }); + }; + +}; + +var listFiles = function() { + var paths = []; + var public_root = '/pub'; + + var recursivelylistFiles = function(root) { + var files = fs.readdirSync(root); + for (var i = 0; i < files.length; i++) { + var file = files[i]; + var newPath = root + '/' + file; + var stat = fs.statSync(newPath); + if (stat.isDirectory()) { + recursivelylistFiles(newPath); + } + else { + if (newPath != './pub/application.manifest') { + paths.push(newPath); + } + } + } + }; + recursivelylistFiles('.' + public_root); + return paths; +}; + + From c88890ffe53971e77cb84af21c22124ea764f95f Mon Sep 17 00:00:00 2001 From: Saimon Moore Date: Thu, 30 Sep 2010 17:36:50 +0200 Subject: [PATCH 07/29] Adding .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bd9241c --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +pub/application.manifest From a25f1e72532574839be8f4a220ed039207f35511 Mon Sep 17 00:00:00 2001 From: Saimon Moore Date: Fri, 1 Oct 2010 13:36:28 +0200 Subject: [PATCH 08/29] Allowing customization of cache section. Reenabling fallback session. Removing default '/' value for network session. Adding eof newline. --- src/offline.js | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/src/offline.js b/src/offline.js index c73d3be..f2cf0df 100644 --- a/src/offline.js +++ b/src/offline.js @@ -1,12 +1,19 @@ var fs = require('fs'); -this.cacheManifest = function(env, callback, network, fallback) { +this.cacheManifest = function(env, callback, cache, network, fallback) { var config = { cache: [], - network: ['/'], + network: [], fallback: [] }; + //augment cache array + if (cache && cache.length > 0) { + for (var i = 0; i < cache.length; i++) { + config.cache.push(cache[i]); + } + } + //augment network array ('/' is already present) if (network && network.length > 0) { for (var i = 0; i < network.length; i++) { @@ -31,9 +38,9 @@ this.cacheManifest = function(env, callback, network, fallback) { } if (regenerate) { - config.cache = listFiles(); - - precache_key(config.cache, function(key) { + var filesToCache = listFiles(); + precache_key(filesToCache, function(key) { + config.cache = config.cache.concat(filesToCache); generateManifest(key, callback); }); } @@ -70,7 +77,7 @@ this.cacheManifest = function(env, callback, network, fallback) { body.push(config.cache[i].replace('./pub','')); }; - body.push(''); body.push('NETWORK:'); + body.push('NETWORK:'); for (var j = 0; j < config.network.length; j++) { body.push(config.network[j]); @@ -82,6 +89,8 @@ this.cacheManifest = function(env, callback, network, fallback) { body.push(config.fallback[k]); }; + body.push(''); + fs.writeFile('./pub/application.manifest', body.join("\n"), function(err) { if (err) { throw err;} cbk(false); From dac06664aef7bef49aa339cf9d76eb2e2eefa0fd Mon Sep 17 00:00:00 2001 From: Saimon Moore Date: Fri, 1 Oct 2010 13:38:37 +0200 Subject: [PATCH 09/29] Adding wildcard character for appcache manifest network section and offline fallback page for accesses to new urls --- pub/offline.html | 16 ++++++++++++++++ src/index.js | 5 ++--- 2 files changed, 18 insertions(+), 3 deletions(-) create mode 100644 pub/offline.html diff --git a/pub/offline.html b/pub/offline.html new file mode 100644 index 0000000..23cf671 --- /dev/null +++ b/pub/offline.html @@ -0,0 +1,16 @@ + + + + Thingler + + + +

You are offline!

+

You cannot create a new list while offline (yet).

+ + diff --git a/src/index.js b/src/index.js index 50e83e5..e80f25e 100644 --- a/src/index.js +++ b/src/index.js @@ -51,10 +51,9 @@ this.server = http.createServer(function (request, response) { clearTimeout(timer); } else if (request.url === '/application.manifest') { offline.cacheManifest(env, function(cached) { - //TODO return not-modified - file.serveFile('/application.manifest', 200, {"Content-Type": "text/cache-manifest"}, request, response); + file.serveFile('/application.manifest', 200, {"Content-Type": "text/cache-manifest", "ExpiresActive": "On", "ExpiresDefault": "access plus 0 seconds"}, request, response); clearTimeout(timer); - }); + }, ['/js'], ['*'],['/ /offline.html']); } else if (request.url === '/') { todo.create(function (id) { finish(303, { 'Location': '/' + id }); From d552f0d6a5aeaa88857b9043f7b96d5ff6611693 Mon Sep 17 00:00:00 2001 From: Saimon Moore Date: Fri, 1 Oct 2010 13:40:33 +0200 Subject: [PATCH 10/29] Automatically swap to latest cache version when app cache updated. Adding appcache event logging for debugging. --- pub/js/appcache.js | 62 ++++++++++++++++++++++++++++++++++++++++++++++ pub/js/index.json | 2 +- pub/js/thingler.js | 2 +- 3 files changed, 64 insertions(+), 2 deletions(-) create mode 100644 pub/js/appcache.js diff --git a/pub/js/appcache.js b/pub/js/appcache.js new file mode 100644 index 0000000..715dce1 --- /dev/null +++ b/pub/js/appcache.js @@ -0,0 +1,62 @@ +// usage: log('inside coolFunc',this,arguments); +// paulirish.com/2009/log-a-lightweight-wrapper-for-consolelog/ +window.log = function(){ + log.history = log.history || []; // store logs to an array for reference + log.history.push(arguments); + if(this.console){ + console.log( Array.prototype.slice.call(arguments) ); + } +}; + +//app cache event handling +var cache = window.applicationCache; +var progressCount = 0; +var cacheStatusValues = { + 0: 'uncached', + 1: 'idle', + 2: 'checking', + 3: 'downloading', + 4: 'updateready', + 5: 'obsolete', + 6: 'error', + 7: 'cached', + 8: 'progress', + 9: 'noupdate' +}; + +function logAppCacheEvent(e) { + var online, status, type, message; + online = (navigator.onLine) ? 'yes' : 'no'; + status = cacheStatusValues[cache.status]; + type = e.type; + message = 'online: ' + online; + message+= ', event: ' + type; + message+= ', status: ' + status; + if (type == 'error' && navigator.onLine) { + message+= ' (probably a syntax error in the manifest)'; + } + log(message); +} + +for (var k in cacheStatusValues) { + if (['idle', 'uncached'].indexOf(cacheStatusValues[k]) == -1) { + cache.addEventListener(cacheStatusValues[k], logAppCacheEvent, false); + } +} + +cache.addEventListener('error', function(e) { + log( 'Error: Cache failed to update : ' + progressCount ); +}, false); +cache.addEventListener('progress', function(e) {progressCount++;}, false); +cache.addEventListener( + 'updateready', + function(){ + //automatically swap the cache to the latest version when new cache is ready + window.applicationCache.swapCache(); + log('swap cache has been called'); + }, + false +); + +// setInterval(function(){cache.update()}, 10000); + diff --git a/pub/js/index.json b/pub/js/index.json index 57b9977..d0a8abb 100644 --- a/pub/js/index.json +++ b/pub/js/index.json @@ -1,4 +1,4 @@ { - "files": ["pilgrim.js", "domdom.js", "domdom-tokenizing.js", "thingler.js"], + "files": ["pilgrim.js", "domdom.js", "domdom-tokenizing.js", "appcache.js", "thingler.js"], "minify": false } diff --git a/pub/js/thingler.js b/pub/js/thingler.js index d6e7c22..2d484d2 100644 --- a/pub/js/thingler.js +++ b/pub/js/thingler.js @@ -85,7 +85,7 @@ var room = { last: last }, function (err, doc) { if (err) { - if (err.status !== 404) { console.log(err) } + if (err.status !== 404) { log(err) } room.changes.rollback(changes); } else if (doc && doc.commits) { room.rev = doc.rev || 0; From 0fb77d29598c478066e329575a1178e0bb64df7f Mon Sep 17 00:00:00 2001 From: Saimon Moore Date: Sat, 2 Oct 2010 02:56:08 +0200 Subject: [PATCH 11/29] Adding lawnchair (cross-browser offline storage lib) --- pub/js/index.json | 2 +- pub/js/lawnchair.min.js | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 pub/js/lawnchair.min.js diff --git a/pub/js/index.json b/pub/js/index.json index d0a8abb..59aba81 100644 --- a/pub/js/index.json +++ b/pub/js/index.json @@ -1,4 +1,4 @@ { - "files": ["pilgrim.js", "domdom.js", "domdom-tokenizing.js", "appcache.js", "thingler.js"], + "files": ["pilgrim.js", "domdom.js", "domdom-tokenizing.js", "lawnchair.min.js", "appcache.js", "thingler.js"], "minify": false } diff --git a/pub/js/lawnchair.min.js b/pub/js/lawnchair.min.js new file mode 100644 index 0000000..f122625 --- /dev/null +++ b/pub/js/lawnchair.min.js @@ -0,0 +1 @@ +var LawnchairAdaptorHelpers={merge:function(a,b){return(b==undefined||b==null)?a:b},terseToVerboseCallback:function(callback){return(typeof arguments[0]=="string")?function(r,i){eval(callback)}:callback},now:function(){return new Date().getTime()},uuid:function(a,d){var f="0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz".split("");var c=[];d=d||f.length;if(a){for(var b=0;b0){var c=this.deserialize(a.data[0].value);c[that.keyName]=b;d(c)}else{d(null)}},all:function(h){if(typeof h==="string"){throw new Error("Callback was a string; strings are not supported for callback shorthand under AIR")}var b=this.terseToVerboseCallback(h);var d=this._execSql("SELECT * FROM "+this.table);var e=[];var g;if(d.data&&d.data.length>0){var c=0;var a=d.data.length;while(c0){var e=b.deserialize(d.data[0].value);e[b.keyName]=a;c(e)}else{c(null)}})},all:function(c){var b=this;if(typeof c==="string"){throw new Error("Callback was a string; strings are not supported for callback shorthand under AIR")}var a=this.terseToVerboseCallback(c);this._execSql("SELECT * FROM "+this.table,null,function(f){var g=[];var i;if(f.data&&f.data.length>0){var e=0;var d=f.data.length;while(e Date: Sat, 2 Oct 2010 02:56:33 +0200 Subject: [PATCH 12/29] Adding preliminary support for offline storage (untested) --- pub/js/thingler.js | 201 +++++++++++++++++++++++++++------------------ 1 file changed, 120 insertions(+), 81 deletions(-) diff --git a/pub/js/thingler.js b/pub/js/thingler.js index 2d484d2..8bf2e33 100644 --- a/pub/js/thingler.js +++ b/pub/js/thingler.js @@ -21,10 +21,13 @@ var room = { rev: null, locked: false, doc: null, + localStorage: new Lawnchair({adaptor:'dom', keyName: '_id'}), changes: { + localStorage: new Lawnchair({adaptor:'dom', keyName: 'id'}), data: [], rollback: function (changes) { this.data = changes.concat(this.data); + this.localStorage.save({id: 'changes', changes: this.data}); }, push: function (type, id, change, callback) { change = change || {}; @@ -34,6 +37,7 @@ var room = { change.callback = callback; this.data.push(change); + this.localStorage.save({id: 'changes', changes: this.data}); // If we're inserting, sync the change right away. // Else, let it happen on the next tick. @@ -41,10 +45,25 @@ var room = { clock.tick(); } }, - commit: function () { + commit: function (callback) { + var that = this; var commit = this.data; - this.data = []; - return commit; + //This is to handle the case where we went offline, made changes, + //closed app, opened app, came back online and need those changes synced + if (this.data.length == 0) { + this.localStorage.get('changes', function(data) { + data = data || {changes: []}; + commit = data.changes; + that.data = []; + that.localStorage.save({id: 'changes', changes: that.data}); + callback(commit); + }); + } + else { + this.data = []; + this.localStorage.save({id: 'changes', changes: this.data}); + callback(commit); + } } }, initialize: function (doc) { @@ -78,31 +97,34 @@ var room = { // Start the Clock // clock.init(function (clock, last) { - var changes = room.changes.commit(); - xhr.resource(id)[last ? 'postSync' : 'post']({ - rev: room.rev || 0, - changes: changes, - last: last - }, function (err, doc) { - if (err) { - if (err.status !== 404) { log(err) } - room.changes.rollback(changes); - } else if (doc && doc.commits) { - room.rev = doc.rev || 0; - - if (doc.commits.length > 0) { - doc.commits.forEach(function (commit) { - commit.changes.forEach(function (change) { - handlers[change.type](change); - }); - }); - clock.activity(); - } - } - clock.synchronised(); - changes.forEach(function (change) { - change.callback && change.callback(); - }); + room.changes.commit(function(changes) { + xhr.resource(id)[last ? 'postSync' : 'post']({ + rev: room.rev || 0, + changes: changes, + last: last + }, function (err, doc) { + if (err) { + if (err.status !== 404) { log(err) } + room.changes.rollback(changes); + } else if (doc && doc.commits) { + room.rev = doc.rev || 0; + + if (doc.commits.length > 0) { + doc.commits.forEach(function (commit) { + commit.changes.forEach(function (change) { + handlers[change.type](change); + }); + }); + clock.activity(); + //update localstorage version of doc + room.localStorage.save(doc); + } + } + clock.synchronised(); + changes.forEach(function (change) { + change.callback && change.callback(); + }); + }); }); }); } @@ -146,6 +168,8 @@ var clock = { // Called on every interval tick. // tick: function (arg) { + //cancel sync if offline + if (!navigator.onLine) return; if (! this.synchronising) { this.synchronising = true; this.callback(this, arg); @@ -212,60 +236,75 @@ title.onblur = function (e) { room.changes.push('title', null, { value: title.value }); }; -xhr.resource(id).get(function (err, doc) { - var password = authenticate.querySelector('input'); - if (err && err.status === 404) { - go('not-found'); - if (id.match(/^[a-zA-Z0-9-]+$/)) { - create.onclick = function () { - xhr.resource(id).put(function (e, doc) { - if (e) { - - } else { - go('page'); - dom.hide(document.getElementById('not-found')); - room.initialize(doc); - } - }); - return false; - }; - } else { - dom.hide(create); - } - } else if (err && err.status === 401) { - authenticate.style.display = 'block'; - password.focus(); - password.onkeydown = function (e) { - var that = this; - if (e.keyCode === 13) { - password.addClass('disabled'); - password.disabled = true; - xhr.resource(id).path('session') - .post({ password: this.value }, function (e, doc) { - if (e) { - that.addClass('error'); - password.removeClass('disabled'); - password.disabled = false; - } else { - xhr.resource(id).get(function (e, doc) { - go('page'); - room.initialize(doc); - dom.hide(authenticate); - }); - } - }); - } - }; - } else { - go('page'); - room.initialize(doc); - } +function go(page) { + document.getElementById(page).style.display = 'block'; + if (page === 'page') { input.focus() } +} - function go(page) { - document.getElementById(page).style.display = 'block'; - if (page === 'page') { input.focus() } - } -}); +if (navigator.onLine) { + //get doc from the server on page load + xhr.resource(id).get(function (err, doc) { + var password = authenticate.querySelector('input'); + if (err && err.status === 404) { + go('not-found'); + if (id.match(/^[a-zA-Z0-9-]+$/)) { + create.onclick = function () { + xhr.resource(id).put(function (e, doc) { + if (e) { + + } else { + go('page'); + dom.hide(document.getElementById('not-found')); + room.initialize(doc); + room.localStorage.save(doc); + } + }); + return false; + }; + } else { + dom.hide(create); + } + } else if (err && err.status === 401) { + authenticate.style.display = 'block'; + password.focus(); + password.onkeydown = function (e) { + var that = this; + if (e.keyCode === 13) { + password.addClass('disabled'); + password.disabled = true; + xhr.resource(id).path('session') + .post({ password: this.value }, function (e, doc) { + if (e) { + that.addClass('error'); + password.removeClass('disabled'); + password.disabled = false; + } else { + xhr.resource(id).get(function (e, doc) { + go('page'); + room.initialize(doc); + dom.hide(authenticate); + room.localStorage.save(doc); + }); + } + }); + } + }; + } else { + go('page'); + room.initialize(doc); + room.localStorage.save(doc); + } + + }); + +} +else { + //retrieve doc from local storage + room.localStorage.get(id, function(doc) { + go('page'); + room.initialize(doc); + }); +} var handlers = { insert: function (change) { From 74022dc4b196596871091d0259582bb5ac337413 Mon Sep 17 00:00:00 2001 From: Saimon Moore Date: Tue, 5 Oct 2010 14:14:31 +0200 Subject: [PATCH 13/29] Added offline support --- pub/js/thingler.js | 311 ++++++++++++++++++++++++++++----------------- 1 file changed, 197 insertions(+), 114 deletions(-) diff --git a/pub/js/thingler.js b/pub/js/thingler.js index 8bf2e33..4fa13a2 100644 --- a/pub/js/thingler.js +++ b/pub/js/thingler.js @@ -37,6 +37,9 @@ var room = { change.callback = callback; this.data.push(change); + doc_handlers[change.type](room.doc, change); + room.localStorage.save(room.doc); + this.localStorage.save({id: 'changes', changes: this.data}); // If we're inserting, sync the change right away. @@ -45,30 +48,17 @@ var room = { clock.tick(); } }, - commit: function (callback) { - var that = this; + commit: function () { var commit = this.data; - //This is to handle the case where we went offline, made changes, - //closed app, opened app, came back online and need those changes synced - if (this.data.length == 0) { - this.localStorage.get('changes', function(data) { - data = data || {changes: []}; - commit = data.changes; - that.data = []; - that.localStorage.save({id: 'changes', changes: that.data}); - callback(commit); - }); - } - else { - this.data = []; - this.localStorage.save({id: 'changes', changes: this.data}); - callback(commit); - } + this.data = []; + this.localStorage.save({id: 'changes', changes: this.data}); + return commit; } }, initialize: function (doc) { // Initialize title and revision number room.rev = doc._rev && parseInt(doc._rev.match(/^(\d+)/)[1]); + doc.rev = room.rev; room.doc = doc; setTitle(doc.title); @@ -93,38 +83,50 @@ var room = { clock.tick(true); }; + //restore any offline changes + room.changes.localStorage.get('changes', function(data) { + if (data && data.changes) { + room.changes.data = data.changes; + } + }); + // // Start the Clock // clock.init(function (clock, last) { - room.changes.commit(function(changes) { - xhr.resource(id)[last ? 'postSync' : 'post']({ - rev: room.rev || 0, - changes: changes, - last: last - }, function (err, doc) { - if (err) { - if (err.status !== 404) { log(err) } - room.changes.rollback(changes); - } else if (doc && doc.commits) { - room.rev = doc.rev || 0; - - if (doc.commits.length > 0) { - doc.commits.forEach(function (commit) { - commit.changes.forEach(function (change) { - handlers[change.type](change); - }); - }); - clock.activity(); - //update localstorage version of doc - room.localStorage.save(doc); - } - } - clock.synchronised(); - changes.forEach(function (change) { - change.callback && change.callback(); - }); - }); + var changes = room.changes.commit(); + xhr.resource(id)[last ? 'postSync' : 'post']({ + rev: room.rev || 0, + changes: changes, + last: last + }, function (err, doc) { + if (err) { + if (err.status !== 404) { log(err) } + room.changes.rollback(changes); + clock.synchronised(); + } else if (doc && doc.commits) { + room.rev = doc.rev || 0; + room.doc.rev = room.rev; + if (doc.commits.length > 0) { + doc.commits.forEach(function (commit) { + commit.changes.forEach(function (change) { + ui_handlers[change.type](change); + + //We don't get the actual doc back, just the changes, so we + //also need to replaty the changes to the local doc (and save to localStorage) + //so the next time the app is opened (while offline) we still have the right data + doc_handlers[change.type](room.doc, change); + }); + }); + + room.localStorage.save(room.doc); + clock.activity(); + } + } + clock.synchronised(); + changes.forEach(function (change) { + change.callback && change.callback(); + }); }); }); } @@ -168,7 +170,7 @@ var clock = { // Called on every interval tick. // tick: function (arg) { - //cancel sync if offline + //cancel sync if offline (only for browsers that support this) if (!navigator.onLine) return; if (! this.synchronising) { this.synchronising = true; @@ -201,7 +203,7 @@ dom.tokenizing(input, input.parentNode, tagPattern).on('new', function (e) { var tokens = this.parentNode.querySelector('.tokens'), title = parseTitle(this.value), item = { title: title, tags: e.tokens.concat(hash.length > 1 ? [hash] : []) }, - element = handlers.insert(item), + element = ui_handlers.insert(item), id = parseInt(element.firstChild.getAttribute('data-id')); tokens.innerHTML = ''; @@ -241,72 +243,9 @@ function go(page) { if (page === 'page') { input.focus() } } -if (navigator.onLine) { - //get doc from the server on page load - xhr.resource(id).get(function (err, doc) { - var password = authenticate.querySelector('input'); - if (err && err.status === 404) { - go('not-found'); - if (id.match(/^[a-zA-Z0-9-]+$/)) { - create.onclick = function () { - xhr.resource(id).put(function (e, doc) { - if (e) { - } else { - go('page'); - dom.hide(document.getElementById('not-found')); - room.initialize(doc); - room.localStorage.save(doc); - } - }); - return false; - }; - } else { - dom.hide(create); - } - } else if (err && err.status === 401) { - authenticate.style.display = 'block'; - password.focus(); - password.onkeydown = function (e) { - var that = this; - if (e.keyCode === 13) { - password.addClass('disabled'); - password.disabled = true; - xhr.resource(id).path('session') - .post({ password: this.value }, function (e, doc) { - if (e) { - that.addClass('error'); - password.removeClass('disabled'); - password.disabled = false; - } else { - xhr.resource(id).get(function (e, doc) { - go('page'); - room.initialize(doc); - dom.hide(authenticate); - room.localStorage.save(doc); - }); - } - }); - } - }; - } else { - go('page'); - room.initialize(doc); - room.localStorage.save(doc); - } - }); - -} -else { - //retrieve doc from local storage - room.localStorage.get(id, function(doc) { - go('page'); - room.initialize(doc); - }); -} - -var handlers = { +var ui_handlers = { insert: function (change) { var item = createItem(change); list.insertBefore(item, list.firstChild); @@ -368,6 +307,61 @@ var handlers = { } }; +var doc_handlers = { + insert: function (doc, change) { + if ((doc.items.length + 1) < 256) { + if (! Array.isArray(change.tags)) { return } + doc.items.unshift({ + id: change.id, + title: change.title, + tags: change.tags + }); + } + }, + title: function (doc, change) { + doc.title = change.value; + }, + edit: function (doc, change) { + var item = findInDoc(change.id, doc); + if (item) { + item.title = change.title; + item.tags = change.tags; + } + }, + sort: function (doc, change) { + var index = indexOf(change.id, doc), item; + if (index !== -1) { + item = doc.items.splice(index, 1)[0]; + doc.items.splice(change.to, 0, item); + } + }, + check: function (doc, change) { + var item = findInDoc(change.id, doc); + if (item) { + item.completed = ((typeof doc.timestamp === 'string') ? new Date(doc.timestamp) : doc.timestamp).getTime() + change.ctime; + } + }, + uncheck: function (doc, change) { + var item = findInDoc(change.id, doc); + if (item) { + delete(item.completed); + } + }, + remove: function (doc, change) { + var index = indexOf(change.id, doc); + if (index !== -1) { + doc.items.splice(index, 1); + } + }, + lock: function (doc, change) { + doc.password = change.password; + }, + unlock: function (doc, change) { + doc.password = null; + } +}; + + document.querySelector('[data-action="about"]').onclick = function () { if (about.style.display !== 'block') { about.style.display = 'block'; @@ -387,7 +381,7 @@ lock.onclick = function () { if (room.locked) { room.changes.push('unlock', null, {}); - handlers.unlock(); + ui_handlers.unlock(); } else { input.disabled = false; input.removeClass('disabled'); @@ -400,7 +394,7 @@ lock.onclick = function () { room.changes.push('lock', null, { password: input.value }, function () { dom.hide(passwordProtect); }); - handlers.lock(); + ui_handlers.lock(); return false; } }; @@ -415,6 +409,24 @@ function find(id) { return list.querySelector('[data-id="' + id + '"]'); } +function findInDoc(id, doc) { + for (var i = 0; i < doc.items.length; i++) { + if (doc.items[i].id === id) { + return doc.items[i]; + } + } + return null; +} + +function indexOf(id, doc) { + for (var i = 0; i < doc.items.length; i++) { + if (doc.items[i].id === id) { + return i; + } + } + return -1; +} + function createItem(item) { var template = document.getElementById('todo-template'); var e = dom.createElement('li'), @@ -612,6 +624,77 @@ function setTitle(str) { title.value = str; document.title = 'Thingler · ' + str; } + +if (navigator.onLine) { + //get doc from the server on page load + xhr.resource(id).get(function (err, doc) { + var password = authenticate.querySelector('input'); + if (err && err.status === 404) { + go('not-found'); + if (id.match(/^[a-zA-Z0-9-]+$/)) { + create.onclick = function () { + xhr.resource(id).put(function (e, doc) { + if (e) { + + } else { + go('page'); + dom.hide(document.getElementById('not-found')); + room.initialize(doc); + room.localStorage.save(doc); + } + }); + return false; + }; + } else { + dom.hide(create); + } + } else if (err && err.status === 401) { + authenticate.style.display = 'block'; + password.focus(); + password.onkeydown = function (e) { + var that = this; + if (e.keyCode === 13) { + password.addClass('disabled'); + password.disabled = true; + xhr.resource(id).path('session') + .post({ password: this.value }, function (e, doc) { + if (e) { + that.addClass('error'); + password.removeClass('disabled'); + password.disabled = false; + } else { + xhr.resource(id).get(function (e, doc) { + go('page'); + room.initialize(doc); + dom.hide(authenticate); + room.localStorage.save(doc); + }); + } + }); + } + }; + } else if (err || !doc._rev) { + //retrieve doc from local storage + room.localStorage.get(id, function(doc) { + go('page'); + room.initialize(doc); + }); + } else { + go('page'); + room.initialize(doc); + room.localStorage.save(doc); + } + }); +} +else { + //retrieve doc from local storage + room.localStorage.get(id, function(doc) { + go('page'); + room.initialize(doc); + }); +} + + // // Check the hashtag every 10ms, for changes // From b12b3dda55c23d5a493b1b3a379007b2af90bdda Mon Sep 17 00:00:00 2001 From: Saimon Moore Date: Tue, 5 Oct 2010 14:41:23 +0200 Subject: [PATCH 14/29] Ensure we replay local changes after initialising the room when online --- pub/js/thingler.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/pub/js/thingler.js b/pub/js/thingler.js index 4fa13a2..106adfa 100644 --- a/pub/js/thingler.js +++ b/pub/js/thingler.js @@ -53,6 +53,16 @@ var room = { this.data = []; this.localStorage.save({id: 'changes', changes: this.data}); return commit; + }, + replayLocalChanges: function() { + this.localStorage.get('changes', function(data) { + //replay changes to update UI + data.changes.forEach(function (change) { + ui_handlers[change.type](change); + //call change's callback if any + change.callback && change.callback(); + }); + }); } }, initialize: function (doc) { @@ -640,6 +650,7 @@ if (navigator.onLine) { go('page'); dom.hide(document.getElementById('not-found')); room.initialize(doc); + room.changes.replayLocalChanges(); room.localStorage.save(doc); } }); @@ -666,6 +677,7 @@ if (navigator.onLine) { xhr.resource(id).get(function (e, doc) { go('page'); room.initialize(doc); + room.changes.replayLocalChanges(); dom.hide(authenticate); room.localStorage.save(doc); }); @@ -682,6 +694,7 @@ if (navigator.onLine) { } else { go('page'); room.initialize(doc); + room.changes.replayLocalChanges(); room.localStorage.save(doc); } }); From 08d1206e3ce042747719692a6ab80b327561ef9f Mon Sep 17 00:00:00 2001 From: Saimon Moore Date: Tue, 5 Oct 2010 14:42:18 +0200 Subject: [PATCH 15/29] Updating lawnchair to not remove stored object's 'key' property (rather just update it) --- pub/js/lawnchair.min.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pub/js/lawnchair.min.js b/pub/js/lawnchair.min.js index f122625..29fbbff 100644 --- a/pub/js/lawnchair.min.js +++ b/pub/js/lawnchair.min.js @@ -1 +1 @@ -var LawnchairAdaptorHelpers={merge:function(a,b){return(b==undefined||b==null)?a:b},terseToVerboseCallback:function(callback){return(typeof arguments[0]=="string")?function(r,i){eval(callback)}:callback},now:function(){return new Date().getTime()},uuid:function(a,d){var f="0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz".split("");var c=[];d=d||f.length;if(a){for(var b=0;b0){var c=this.deserialize(a.data[0].value);c[that.keyName]=b;d(c)}else{d(null)}},all:function(h){if(typeof h==="string"){throw new Error("Callback was a string; strings are not supported for callback shorthand under AIR")}var b=this.terseToVerboseCallback(h);var d=this._execSql("SELECT * FROM "+this.table);var e=[];var g;if(d.data&&d.data.length>0){var c=0;var a=d.data.length;while(c0){var e=b.deserialize(d.data[0].value);e[b.keyName]=a;c(e)}else{c(null)}})},all:function(c){var b=this;if(typeof c==="string"){throw new Error("Callback was a string; strings are not supported for callback shorthand under AIR")}var a=this.terseToVerboseCallback(c);this._execSql("SELECT * FROM "+this.table,null,function(f){var g=[];var i;if(f.data&&f.data.length>0){var e=0;var d=f.data.length;while(e0){var c=this.deserialize(a.data[0].value);c[that.keyName]=b;d(c)}else{d(null)}},all:function(h){if(typeof h==="string"){throw new Error("Callback was a string; strings are not supported for callback shorthand under AIR")}var b=this.terseToVerboseCallback(h);var d=this._execSql("SELECT * FROM "+this.table);var e=[];var g;if(d.data&&d.data.length>0){var c=0;var a=d.data.length;while(c0){var e=b.deserialize(d.data[0].value);e[b.keyName]=a;c(e)}else{c(null)}})},all:function(c){var b=this;if(typeof c==="string"){throw new Error("Callback was a string; strings are not supported for callback shorthand under AIR")}var a=this.terseToVerboseCallback(c);this._execSql("SELECT * FROM "+this.table,null,function(f){var g=[];var i;if(f.data&&f.data.length>0){var e=0;var d=f.data.length;while(e Date: Tue, 5 Oct 2010 14:42:52 +0200 Subject: [PATCH 16/29] Adding logging for router requests --- src/index.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/index.js b/src/index.js index e80f25e..01e41a2 100644 --- a/src/index.js +++ b/src/index.js @@ -63,6 +63,13 @@ this.server = http.createServer(function (request, response) { // Dispatch the request to the router // router.route(request, body.join(''), function (result) { + + sys.puts([ + new(Date)().toJSON(), + log.join(' '), + [result.status, http.STATUS_CODES[result.status], result.body].join(' ') + ].join(' -- ')); + if (result.status === 406) { // A request for non-json data file.serve(request, response, function (err, result) { if (err) { From 40682e2c111b4709c2f2874165ac829cac9f5749 Mon Sep 17 00:00:00 2001 From: Saimon Moore Date: Tue, 5 Oct 2010 14:44:35 +0200 Subject: [PATCH 17/29] Adding dummy event listeners for testing online/offline events (not all browsers support these) --- pub/js/appcache.js | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/pub/js/appcache.js b/pub/js/appcache.js index 715dce1..d01b9d3 100644 --- a/pub/js/appcache.js +++ b/pub/js/appcache.js @@ -60,3 +60,20 @@ cache.addEventListener( // setInterval(function(){cache.update()}, 10000); +//onoffline/online events +var onoffline = function () { + log('Now offline...'); +}; + +var ononline = function () { + log('Now online...'); +}; + +if (window.addEventListener) { + document.body.addEventListener("offline", onoffline, false); + document.body.addEventListener("online", ononline, false); +} +else { + document.body.onoffline = onoffline; + document.body.ononline = ononline; +} From d3fa94115ec10b127976b2b7dba77a347c66b107 Mon Sep 17 00:00:00 2001 From: Saimon Moore Date: Tue, 5 Oct 2010 14:46:18 +0200 Subject: [PATCH 18/29] Adding timeout to pilgrim. TODO: all this may not be necessary. Need to find ff bug. --- pub/js/pilgrim.js | 87 ++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 79 insertions(+), 8 deletions(-) diff --git a/pub/js/pilgrim.js b/pub/js/pilgrim.js index 42a06fe..28103f6 100644 --- a/pub/js/pilgrim.js +++ b/pub/js/pilgrim.js @@ -60,11 +60,13 @@ var pilgrim = (function () { // // XHR // - this.XHR = function XHR(method, url, data, headers, async) { + this.XHR = function XHR(method, url, data, headers, async, timeout) { this.method = method.toLowerCase(); this.url = url; this.data = data || {}; this.async = async; + this.requestDone = false; + this.timeout = timeout || 0; if (window.XMLHttpRequest) { this.xhr = new(XMLHttpRequest); @@ -82,18 +84,86 @@ var pilgrim = (function () { this.data = JSON.stringify(this.data); this.xhr.open(this.method, this.url, this.async); - this.xhr.onreadystatechange = function () { - if (this.readyState != 4) { return } + var that = this; + var onreadystatechange = this.xhr.onreadystatechange = function (isTimeout) { + + if (that.xhr) { + var handleComplete = function(xhr, status, responseText) { + var body = {}; + try { + body = responseText ? JSON.parse(responseText) : {}; + } catch(parseError) {} + + if (['abort','timeout','error'].indexOf(status) != -1) { + callback({ status: status, body: body, xhr: xhr }); + } else if (status >= 200 && status < 300) { // Success + callback(null, body); + } else { // Error + callback({ status: status, body: body, xhr: xhr }); + } + }; + + // The request was aborted + if ( !that.xhr || that.xhr.readyState === 0 || isTimeout === "abort" ) { + + // Opera doesn't call onreadystatechange before this point + // so we simulate the call + if ( !that.requestDone ) { + var status = isTimeout === 'abort' ? 'abort' : 0; + handleComplete(that.xhr, status, that.xhr.responseText); + } + + that.requestDone = true; + + if ( that.xhr ) { + that.xhr.onreadystatechange = function() {}; + } + + } else if ( !that.requestDone && that.xhr && (that.xhr.readyState === 4 || isTimeout === "timeout") ) { + that.requestDone = true; + that.xhr.onreadystatechange = function() {}; + + + var status = isTimeout === "timeout" ? 'timeout' : ((that.xhr.status < 99) ? 'error' : that.xhr.status); - var body = this.responseText ? JSON.parse(this.responseText) : {}; + handleComplete(that.xhr, status, that.xhr.responseText); + + if ( isTimeout === "timeout" ) { + that.xhr.abort(); + } + + // Stop memory leaks + if ( that.async ) { + that.xhr = null; + } + + } - if (this.status >= 200 && this.status < 300) { // Success - callback(null, body); - } else { // Error - callback({ status: this.status, body: body, xhr: this }); } }; + if (this.timeout && this.timeout > 0) { + setTimeout(function() { + if (!this.requestDone) { + this.xhr.abort(); + } + },this.timeout); + } + + + // Override the abort handler, if we can (IE doesn't allow it, but that's OK) + // Opera doesn't fire onreadystatechange at all on abort + try { + var oldAbort = xhr.abort; + this.xhr.abort = function() { + if ( this.xhr ) { + oldAbort.call( this.xhr ); + } + + onreadystatechange( "abort" ); + }; + } catch( abortError ) {} + // Set content headers if (this.method === 'post' || this.method === 'put') { this.headers['Content-Type'] = 'application/json'; @@ -104,6 +174,7 @@ var pilgrim = (function () { this.xhr.setRequestHeader(k, this.headers[k]); } + // Dispatch request this.xhr.send(this.method === 'get' ? null : this.data); From ae6abd92e0dbfa1b5a14891c0970d25447eac5dc Mon Sep 17 00:00:00 2001 From: Saimon Moore Date: Tue, 5 Oct 2010 16:33:40 +0200 Subject: [PATCH 19/29] NodeList in opera doesn't expose an array-like interface. Convert to an array --- pub/js/thingler.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pub/js/thingler.js b/pub/js/thingler.js index 106adfa..67bdf7b 100644 --- a/pub/js/thingler.js +++ b/pub/js/thingler.js @@ -551,7 +551,7 @@ function handleSort(id, to) { function handleTagFilter(filter) { var child, tag, tags; - list.querySelectorAll('li.active').forEach(function (e) { + Array.prototype.slice.call(list.querySelectorAll('li.active')).forEach(function (e) { e.removeClass('active'); }); From 9a8b54b1a3188e58a98d1feecebf38c01925474f Mon Sep 17 00:00:00 2001 From: Saimon Moore Date: Wed, 6 Oct 2010 14:10:39 +0200 Subject: [PATCH 20/29] Adding headers to avoid caching of xhr requests in IE --- src/index.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/index.js b/src/index.js index 01e41a2..0bd98ce 100644 --- a/src/index.js +++ b/src/index.js @@ -51,7 +51,7 @@ this.server = http.createServer(function (request, response) { clearTimeout(timer); } else if (request.url === '/application.manifest') { offline.cacheManifest(env, function(cached) { - file.serveFile('/application.manifest', 200, {"Content-Type": "text/cache-manifest", "ExpiresActive": "On", "ExpiresDefault": "access plus 0 seconds"}, request, response); + file.serveFile('/application.manifest', 200, {"Content-Type": "text/cache-manifest"}, request, response); clearTimeout(timer); }, ['/js'], ['*'],['/ /offline.html']); } else if (request.url === '/') { @@ -79,7 +79,10 @@ this.server = http.createServer(function (request, response) { }); } else { session.create(request, function (header) { - if (header) { result.headers['Set-Cookie'] = header['Set-Cookie'] } + if (header) { result.headers['Set-Cookie'] = header['Set-Cookie']; } + result.headers['Cache-Control'] = 'no-store'; + result.headers['Pragma'] = 'no-cache'; + result.headers['Expires'] = 'Sun, 19 Nov 1978 05:00:00 GMT'; finish(result.status, result.headers, result.body); }); } From 394978fb0c606e69e9fd6cc0dec60038857632a1 Mon Sep 17 00:00:00 2001 From: Saimon Moore Date: Wed, 6 Oct 2010 14:11:36 +0200 Subject: [PATCH 21/29] Indicating thingler is offline in title --- pub/offline.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pub/offline.html b/pub/offline.html index 23cf671..f486740 100644 --- a/pub/offline.html +++ b/pub/offline.html @@ -1,7 +1,7 @@ - Thingler + Thingler - Offline