diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bd9241c --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +pub/application.manifest 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"' 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 ------- 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;} diff --git a/pub/index.html b/pub/index.html index dcb58de..62c176f 100644 --- a/pub/index.html +++ b/pub/index.html @@ -1,9 +1,16 @@ - + Thingler + + + + + + + diff --git a/pub/js/appcache.js b/pub/js/appcache.js new file mode 100644 index 0000000..6141b94 --- /dev/null +++ b/pub/js/appcache.js @@ -0,0 +1,84 @@ +// usage: log('inside coolFunc',this,arguments); +// paulirish.com/2009/log-a-lightweight-wrapper-for-consolelog/ +thingler_debug = false; + +window.log = function(){ + log.history = log.history || []; // store logs to an array for reference + if (thingler_debug) { + 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); + +//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; +} diff --git a/pub/js/index.json b/pub/js/index.json index 57b9977..59aba81 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", "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..6832cff --- /dev/null +++ b/pub/js/lawnchair.min.js @@ -0,0 +1,1507 @@ +/** + * LawnchairAdaptorHelpers + * ======================= + * Useful helpers for creating Lawnchair stores. Used as a mixin. + * + */ +var LawnchairAdaptorHelpers = { + // merging default properties with user defined args + merge: function(defaultOption, userOption) { + return (userOption == undefined || userOption == null) ? defaultOption: userOption; + }, + + // awesome shorthand callbacks as strings. this is shameless theft from dojo. + terseToVerboseCallback: function(callback) { + return (typeof arguments[0] == 'string') ? + function(r, i) { + eval(callback); + }: callback; + }, + + // Returns current datetime for timestamps. + now: function() { + return new Date().getTime(); + }, + + // Returns a unique identifier + uuid: function(len, radix) { + // based on Robert Kieffer's randomUUID.js at http://www.broofa.com + var chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'.split(''); + var uuid = []; + radix = radix || chars.length; + + if (len) { + for (var i = 0; i < len; i++) uuid[i] = chars[0 | Math.random() * radix]; + } else { + // rfc4122, version 4 form + var r; + + // rfc4122 requires these characters + uuid[8] = uuid[13] = uuid[18] = uuid[23] = '-'; + uuid[14] = '4'; + + // Fill in random data. At i==19 set the high bits of clock sequence as + // per rfc4122, sec. 4.1.5 + for (var i = 0; i < 36; i++) { + if (!uuid[i]) { + r = 0 | Math.random() * 16; + uuid[i] = chars[(i == 19) ? (r & 0x3) | 0x8: r]; + } + } + } + return uuid.join(''); + }, + + // Serialize a JSON object as a string. + serialize: function(obj) { + var r = ''; + r = JSON.stringify(obj); + return r; + }, + + // Deserialize JSON. + deserialize: function(json) { + return eval('(' + json + ')'); + } +}; + +// inline the AIR aliases file, edited to include only what we need + +/* AIRAliases.js - Revision: 2.0beta */ + +/* +ADOBE SYSTEMS INCORPORATED +Copyright 2007-2008 Adobe Systems Incorporated. All Rights Reserved. + +NOTICE: Adobe permits you to modify and distribute this file only in accordance with +the terms of Adobe AIR SDK license agreement. You may have received this file from a +source other than Adobe. Nonetheless, you may modify or +distribute this file only in accordance with such agreement. + +http://www.adobe.com/products/air/tools/sdk/eula/ + +@depend LawnchairAdaptorHelpers.js + +*/ + +var air; +if (window.runtime) +{ + if (!air) air = {}; + // functions + air.trace = window.runtime.trace; + + // file + air.File = window.runtime.flash.filesystem.File; + air.FileStream = window.runtime.flash.filesystem.FileStream; + air.FileMode = window.runtime.flash.filesystem.FileMode; + + // data + air.EncryptedLocalStore = window.runtime.flash.data.EncryptedLocalStore; + air.SQLCollationType = window.runtime.flash.data.SQLCollationType; + air.SQLColumnNameStyle = window.runtime.flash.data.SQLColumnNameStyle; + air.SQLColumnSchema = window.runtime.flash.data.SQLColumnSchema; + air.SQLConnection = window.runtime.flash.data.SQLConnection; + air.SQLError = window.runtime.flash.errors.SQLError; + air.SQLErrorEvent = window.runtime.flash.events.SQLErrorEvent; + air.SQLErrorOperation = window.runtime.flash.errors.SQLErrorOperation; + air.SQLEvent = window.runtime.flash.events.SQLEvent; + air.SQLIndexSchema = window.runtime.flash.data.SQLIndexSchema; + air.SQLMode = window.runtime.flash.data.SQLMode; + air.SQLResult = window.runtime.flash.data.SQLResult; + air.SQLSchema = window.runtime.flash.data.SQLSchema; + air.SQLSchemaResult = window.runtime.flash.data.SQLSchemaResult; + air.SQLStatement = window.runtime.flash.data.SQLStatement; + air.SQLTableSchema = window.runtime.flash.data.SQLTableSchema; + air.SQLTransactionLockType = window.runtime.flash.data.SQLTransactionLockType; + air.SQLTriggerSchema = window.runtime.flash.data.SQLTriggerSchema; + air.SQLUpdateEvent = window.runtime.flash.events.SQLUpdateEvent; + air.SQLViewSchema = window.runtime.flash.data.SQLViewSchema; + +} + + + + +/** + * AIRSQLiteAdaptor + * =================== + * AIR flavored SQLite implementation for Lawnchair. + * + * This uses synchronous connections to the DB. If this is available, + * I think this is the better option, but in single-threaded apps it + * may cause blocking. It might be reasonable to implement an alternative + * that uses async connections. + * + */ +var AIRSQLiteAdaptor = function(options) { + for (var i in LawnchairAdaptorHelpers) { + this[i] = LawnchairAdaptorHelpers[i]; + } + this.init(options); +}; + + +AIRSQLiteAdaptor.prototype = { + init:function(options) { + + var that = this; + var merge = that.merge; + var opts = (typeof arguments[0] == 'string') ? {table:options} : options; + + this.name = merge('Lawnchair', opts.name); + this.keyName = merge('key', opts.keyName); + this.table = merge('field', opts.table); + + this.conn = new air.SQLConnection(); + var appstoredir = air.File.applicationStorageDirectory; + this.dbFile = appstoredir.resolvePath(this.name + ".sqlite.db"); + + try { + this.conn.open(this.dbFile); + } catch(err) { + air.trace('Error msg:'+err.message); + air.trace('Error details:'+err.details); + } + + this._execSql('create table if not exists ' + this.table + ' (id NVARCHAR(32) UNIQUE PRIMARY KEY, value TEXT, timestamp REAL)'); + }, + + /* + + */ + save:function(obj, callback) { + var that = this; + + var insert = function(obj, callback) { + var id; + + if (obj[that.keyName] == undefined) { + id = that.uuid(); + } else { + id = obj[that.keyName]; + } + + delete(obj[that.keyName]); + + var rs = that._execSql("INSERT INTO " + that.table + " (id, value, timestamp) VALUES (:id,:value,:timestamp)", + { + ':id':id, + ':value':that.serialize(obj), + ':timestamp':that.now() + } + ); + + if (callback != undefined) { + obj[that.keyName] = id; + callback(obj); + } + }; + + var update = function(id, obj, callback) { + var rs = that._execSql("UPDATE " + that.table + " SET value=:value, timestamp=:timestamp WHERE id=:id", + { + ':id':id, + ':value':that.serialize(obj), + ':timestamp':that.now() + } + ); + + if (callback != undefined) { + obj[that.keyName] = id; + callback(obj); + } + }; + + + if (obj[that.keyName] == undefined) { + + insert(obj, callback); + } else { + + this.get(obj[that.keyName], function(r) { + var isUpdate = (r != null); + + if (isUpdate) { + var id = obj[that.keyName]; + delete(obj[that.keyName]); + update(id, obj, callback); + } else { + insert(obj, callback); + } + }); + } + + }, + + /* + + */ + get:function(key, callback) { + var rs = this._execSql("SELECT * FROM " + this.table + " WHERE id = :id", + { + ':id':key + } + ); + + if (rs.data && rs.data.length> 0) { + var o = this.deserialize(rs.data[0].value); + o[that.keyName] = key; + callback(o); + } else { + callback(null); + } + }, + + all:function(callback) { + + if (typeof callback === 'string') { + throw new Error("Callback was a string; strings are not supported for callback shorthand under AIR"); + } + + var cb = this.terseToVerboseCallback(callback); + var rs = this._execSql("SELECT * FROM " + this.table); + var r = []; + var o; + + + if (rs.data && rs.data.length > 0) { + var k = 0; + var numrows = rs.data.length; + + while (k < numrows) { + var thisdata = rs.data[k]; + o = this.deserialize(thisdata.value); + o[that.keyName] = thisdata.id; + r.push(o); + k++; + } + } else { + r = []; + } + + cb(r); + + + }, + + /* + + */ + remove:function(keyOrObj, callback) { + + var key = (typeof keyOrObj == 'string') ? keyOrObj : keyOrObj[that.keyName]; + var rs = this._execSql("DELETE FROM " + this.table + " WHERE id = :id", + { + ':id':key + }, + callback + ); + }, + + /* + + */ + nuke:function(callback) { + var rs = this._execSql("DELETE FROM " + this.table, {}, callback); + }, + + /* + this is a wrapper for the overly complex AIR SQL API method of executing + SQL statements + */ + _execSql:function(sql, params, onSuccess, onError) { + + var stmt = new air.SQLStatement(); + stmt.sqlConnection = this.conn; + stmt.text = sql; + if (params) { + for (var key in params) { + stmt.parameters[key] = params[key]; + } + } + + try { + stmt.execute(); + + var rs = stmt.getResult(); + if (onSuccess) { + onSuccess(rs.data); + } + + return rs; + } catch(err) { + air.trace('Error:' + err.message); + air.trace('Error details:' + err.details); + if (onError) { + onError(err); + } + return false; + } + } +}; + +// inline the AIR aliases file, edited to include only what we need + +/* AIRAliases.js - Revision: 2.0beta */ + +/* +ADOBE SYSTEMS INCORPORATED +Copyright 2007-2008 Adobe Systems Incorporated. All Rights Reserved. + +NOTICE: Adobe permits you to modify and distribute this file only in accordance with +the terms of Adobe AIR SDK license agreement. You may have received this file from a +source other than Adobe. Nonetheless, you may modify or +distribute this file only in accordance with such agreement. + +http://www.adobe.com/products/air/tools/sdk/eula/ + +@depend LawnchairAdaptorHelpers.js + +*/ + +var air; +if (window.runtime) +{ + if (!air) air = {}; + // functions + air.trace = window.runtime.trace; + + // file + air.File = window.runtime.flash.filesystem.File; + air.FileStream = window.runtime.flash.filesystem.FileStream; + air.FileMode = window.runtime.flash.filesystem.FileMode; + + // data + air.EncryptedLocalStore = window.runtime.flash.data.EncryptedLocalStore; + air.SQLCollationType = window.runtime.flash.data.SQLCollationType; + air.SQLColumnNameStyle = window.runtime.flash.data.SQLColumnNameStyle; + air.SQLColumnSchema = window.runtime.flash.data.SQLColumnSchema; + air.SQLConnection = window.runtime.flash.data.SQLConnection; + air.SQLError = window.runtime.flash.errors.SQLError; + air.SQLErrorEvent = window.runtime.flash.events.SQLErrorEvent; + air.SQLErrorOperation = window.runtime.flash.errors.SQLErrorOperation; + air.SQLEvent = window.runtime.flash.events.SQLEvent; + air.SQLIndexSchema = window.runtime.flash.data.SQLIndexSchema; + air.SQLMode = window.runtime.flash.data.SQLMode; + air.SQLResult = window.runtime.flash.data.SQLResult; + air.SQLSchema = window.runtime.flash.data.SQLSchema; + air.SQLSchemaResult = window.runtime.flash.data.SQLSchemaResult; + air.SQLStatement = window.runtime.flash.data.SQLStatement; + air.SQLTableSchema = window.runtime.flash.data.SQLTableSchema; + air.SQLTransactionLockType = window.runtime.flash.data.SQLTransactionLockType; + air.SQLTriggerSchema = window.runtime.flash.data.SQLTriggerSchema; + air.SQLUpdateEvent = window.runtime.flash.events.SQLUpdateEvent; + air.SQLViewSchema = window.runtime.flash.data.SQLViewSchema; + +} + + + + +/** + * AIRSQLiteAsyncAdaptor + * =================== + * AIR flavored SQLite implementation for Lawnchair. + * + * This uses asynchronous connections to the DB. + */ +var AIRSQLiteAsyncAdaptor = function(options) { + for (var i in LawnchairAdaptorHelpers) { + this[i] = LawnchairAdaptorHelpers[i]; + } + this.init(options); +}; + + +AIRSQLiteAsyncAdaptor.prototype = { + init:function(options) { + + var that = this; + var merge = that.merge; + var opts = (typeof arguments[0] == 'string') ? {table:options} : options; + + this.name = merge('Lawnchair', opts.name); + this.keyName = merge('key', opts.keyName); + this.table = merge('field', opts.table); + + this.conn = new air.SQLConnection(); + var appstoredir = air.File.applicationStorageDirectory; + this.dbFile = appstoredir.resolvePath(this.name + ".sqlite.db"); + + try { + this.conn.openAsync(this.dbFile); + } catch(err) { + air.trace('Error msg:'+err.message); + air.trace('Error details:'+err.details); + } + + this._execSql('create table if not exists ' + this.table + ' (id NVARCHAR(32) UNIQUE PRIMARY KEY, value TEXT, timestamp REAL)'); + }, + + /* + + */ + save:function(obj, callback) { + var that = this; + + var insert = function(obj, callback) { + var id; + + if (obj[that.keyName] == undefined) { + id = that.uuid(); + } else { + id = obj[that.keyName]; + } + + delete(obj[that.keyName]); + + that._execSql("INSERT INTO " + that.table + " (id, value, timestamp) VALUES (:id,:value,:timestamp)", + { + ':id':id, + ':value':that.serialize(obj), + ':timestamp':that.now() + }, + function(rs) { + if (callback != undefined) { + obj[that.keyName] = id; + callback(obj); + } + } + ); + }; + + var update = function(id, obj, callback) { + that._execSql("UPDATE " + that.table + " SET value=:value, timestamp=:timestamp WHERE id=:id", + { + ':id':id, + ':value':that.serialize(obj), + ':timestamp':that.now() + }, + function(rs) { + if (callback != undefined) { + obj[that.keyName] = id; + callback(obj); + } + } + ); + }; + + + if (obj[that.keyName] == undefined) { + + insert(obj, callback); + } else { + + this.get(obj[that.keyName], function(r) { + var isUpdate = (r != null); + + if (isUpdate) { + var id = obj[that.keyName]; + delete(obj[that.keyName]); + update(id, obj, callback); + } else { + insert(obj, callback); + } + }); + } + + }, + + /* + + */ + get:function(key, callback) { + var that = this; + this._execSql("SELECT * FROM " + this.table + " WHERE id = :id", + { + ':id':key + }, + function(rs) { + if (rs.data && rs.data.length> 0) { + var o = that.deserialize(rs.data[0].value); + o[that.keyName] = key; + callback(o); + } else { + callback(null); + } + } + ); + }, + + all:function(callback) { + var that = this; + + if (typeof callback === 'string') { + throw new Error("Callback was a string; strings are not supported for callback shorthand under AIR"); + } + + var cb = this.terseToVerboseCallback(callback); + this._execSql("SELECT * FROM " + this.table, null, function(rs) { + var r = []; + var o; + if (rs.data && rs.data.length > 0) { + var k = 0; + var numrows = rs.data.length; + + while (k < numrows) { + var thisdata = rs.data[k]; + o = that.deserialize(thisdata.value); + o[that.keyName] = thisdata.id; + r.push(o); + k++; + } + } else { + r = []; + } + + cb(r); + }); + + }, + + /* + + */ + remove:function(keyOrObj) { + + var key = (typeof keyOrObj == 'string') ? keyOrObj : keyOrObj[this.keyName]; + this._execSql("DELETE FROM " + this.table + " WHERE id = :id", + { + ':id':key + } + ); + }, + + /* + + */ + nuke:function() { + this._execSql("DELETE FROM " + this.table); + }, + + /* + this is a wrapper for the overly complex AIR SQL API method of executing + SQL statements + */ + _execSql:function(sql, params, onSuccess, onError) { + + var stmt = new air.SQLStatement(); + stmt.sqlConnection = this.conn; + stmt.text = sql; + if (params) { + for (var key in params) { + stmt.parameters[key] = params[key]; + } + } + + + function resultHandler(event) { + var rs = stmt.getResult(); + if (onSuccess) { + onSuccess(rs); + } + } + + function errorHandler(event) { + air.trace('Error:' + event.error.message); + air.trace('Error details:' + event.error.details); + if (onError) { + onError(event.error); + } + } + + stmt.addEventListener(air.SQLEvent.RESULT, resultHandler); + stmt.addEventListener(air.SQLErrorEvent.ERROR, errorHandler); + + try { + stmt.execute(); + } catch(err) { + air.trace('Error:' + err.message); + air.trace('Error details:' + err.details); + if (onError) { + onError(err); + } + } + } +}; + +/** + * BlackBerryPersistentStorageAdaptor + * =================== + * Implementation that uses the BlackBerry Persistent Storage mechanism. This is only available in PhoneGap BlackBerry projects + * See http://www.github.com/phonegap/phonegap-blackberry + * + * + * @depend LawnchairAdaptorHelpers.js + * + */ +var BlackBerryPersistentStorageAdaptor = function(options) { + for (var i in LawnchairAdaptorHelpers) { + this[i] = LawnchairAdaptorHelpers[i]; + } + this.init(options); +}; + +BlackBerryPersistentStorageAdaptor.prototype = { + init:function(options) { + this.keyName = merge('key', options.keyName); + + // Check for the existence of the phonegap blackberry persistent store API + if (!navigator.store) + throw('Lawnchair, "This browser does not support BlackBerry Persistent Storage; it is a PhoneGap-only implementation."'); + }, + get:function(key, callback) { + var that = this; + navigator.store.get(function(value) { // success cb + if (callback) { + // Check if BBPS returned a serialized JSON obj, if so eval it. + if (that.isObjectAsString(value)) { + eval('value = ' + value.substr(0,value.length-1) + ',key:\'' + key + '\'};'); + } + that.terseToVerboseCallback(callback)(value); + } + }, function() {}, // empty error cb + key); + }, + save:function(obj, callback) { + var id = obj[this.keyName] || this.uuid(); + delete obj[this.keyName]; + var that = this; + navigator.store.put(function(){ + if (callback) { + var cbObj = obj; + cbObj['key'] = id; + that.terseToVerboseCallback(callback)(cbObj); + } + }, function(){}, id, this.serialize(obj)); + }, + all:function(callback) { + var that = this; + navigator.store.getAll(function(json) { // success cb + if (callback) { + // BlackBerry store returns straight strings, so eval as necessary for values we deem as objects. + var arr = []; + for (var prop in json) { + if (that.isObjectAsString(json[prop])) { + eval('arr.push(' + json[prop].substr(0,json[prop].length-1) + ',key:\'' + prop + '\'});'); + } else { + eval('arr.push({\'' + prop + '\':\'' + json[prop] + '\'});'); + } + } + that.terseToVerboseCallback(callback)(arr); + } + }, function() {}); // empty error cb + }, + remove:function(keyOrObj, callback) { + var key = (typeof keyOrObj == 'string') ? keyOrObj : keyOrObj[this.keyName]; + var that = this; + navigator.store.remove(function() { + if (callback) + that.terseToVerboseCallback(callback)(); + }, function() {}, key); + }, + nuke:function(callback) { + var that = this; + navigator.store.nuke(function(){ + if (callback) + that.terseToVerboseCallback(callback)(); + },function(){}); + }, + // Private helper. + isObjectAsString:function(value) { + return (value != null && value[0] == '{' && value[value.length-1] == '}'); + } +}; + +/** + * CookieAdaptor + * =================== + * Cookie implementation for Lawnchair for older browsers. + * + * Based on ppk's http://www.quirksmode.org/js/cookies.html + * + * @depend LawnchairAdaptorHelpers.js + * + */ +var CookieAdaptor = function(options) { + for (var i in LawnchairAdaptorHelpers) { + this[i] = LawnchairAdaptorHelpers[i]; + } + this.init(options); +}; + +CookieAdaptor.prototype = { + init:function(options){ + this.keyName = this.merge('key', options.keyName); + + this.createCookie = function(name, value, days) { + if (days) { + var date = new Date(); + date.setTime(date.getTime()+(days*24*60*60*1000)); + var expires = "; expires="+date.toGMTString(); + } + else var expires = ""; + document.cookie = name+"="+value+expires+"; path=/"; + }; + }, + get:function(key, callback){ + var readCookie = function(name) { + var nameEQ = name + "="; + var ca = document.cookie.split(';'); + var len = ca.length; + for (var i=0; i < len; i++) { + var c = ca[i]; + while (c.charAt(0)==' ') c = c.substring(1,c.length); + if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length,c.length); + } + return null; + }; + var obj = this.deserialize(readCookie(key)) || null; + if (obj) { + obj[this.keyName] = key; + } + if (callback) + this.terseToVerboseCallback(callback)(obj); + }, + save:function(obj, callback){ + var id = obj[this.keyName] || this.uuid(); + delete obj[this.keyName]; + this.createCookie(id, this.serialize(obj), 365); + obj[this.keyName] = id; + if (callback) + this.terseToVerboseCallback(callback)(obj); + }, + all:function(callback){ + var cb = this.terseToVerboseCallback(callback); + var ca = document.cookie.split(';'); + var yar = []; + var c,k,v,o; + // yo ho yo ho a pirates life for me + for (var i = 0, l = ca.length; i < l; i++) { + c = ca[i].split('='); + k = c[0]; + v = c[1]; + o = this.deserialize(v); + if (o) { + o[this.keyName] = k; + yar.push(o); + } + } + if (cb) + cb(yar); + }, + remove:function(keyOrObj, callback) { + var key = (typeof keyOrObj == 'string') ? keyOrObj : keyOrObj[this.keyName]; + this.createCookie(key, '', -1); + if (callback) + this.terseToVerboseCallback(callback)(); + }, + nuke:function(callback) { + var that = this; + this.all(function(r){ + for (var i = 0, l = r.length; i < l; i++) { + if (r[i][that.keyName]) + that.remove(r[i][that.keyName]); + } + if (callback) { + callback = that.terseToVerboseCallback(callback); + callback(r); + } + }); + } +}; + +/** + * DOMStorageAdaptor + * =================== + * DOM Storage implementation for Lawnchair. + * + * - originally authored by Joseph Pecoraro + * - window.name code courtesy Remy Sharp: http://24ways.org/2009/breaking-out-the-edges-of-the-browser + * + * @depend LawnchairAdaptorHelpers.js + * + */ +var DOMStorageAdaptor = function(options) { + for (var i in LawnchairAdaptorHelpers) { + this[i] = LawnchairAdaptorHelpers[i]; + } + this.init(options); +}; + + +DOMStorageAdaptor.prototype = { + init:function(options) { + var self = this; + this.storage = this.merge(window.localStorage, options.storage); + this.table = this.merge('field', options.table); + this.keyName = this.merge('key', options.keyName); + + if (!window.Storage) { + this.storage = (function () { + // window.top.name ensures top level, and supports around 2Mb + var data = window.top.name ? self.deserialize(window.top.name) : {}; + return { + setItem: function (key, value) { + data[key] = value+""; // force to string + window.top.name = self.serialize(data); + }, + removeItem: function (key) { + delete data[key]; + window.top.name = self.serialize(data); + }, + getItem: function (key) { + return data[key] || null; + }, + clear: function () { + data = {}; + window.top.name = ''; + } + }; + })(); + }; + }, + + save:function(obj, callback) { + var id = this.table + '::' + (obj[this.keyName] || this.uuid()); + obj[this.keyName] = id.split('::')[1]; + this.storage.setItem(id, this.serialize(obj)); + if (callback) { + callback(obj); + } + }, + + get:function(key, callback) { + var obj = this.deserialize(this.storage.getItem(this.table + '::' + key)); + var cb = this.terseToVerboseCallback(callback); + + if (obj) { + obj[this.keyName] = key; + if (callback) cb(obj); + } else { + if (callback) cb(null); + } + }, + + all:function(callback) { + var cb = this.terseToVerboseCallback(callback); + var results = []; + for (var i = 0, l = this.storage.length; i < l; ++i) { + var id = this.storage.key(i); + var tbl = id.split('::')[0] + var key = id.split('::').slice(1).join("::"); + if (tbl == this.table) { + var obj = this.deserialize(this.storage.getItem(id)); + obj[this.keyName] = key; + results.push(obj); + } + } + if (cb) + cb(results); + }, + + remove:function(keyOrObj, callback) { + var key = this.table + '::' + (typeof keyOrObj === 'string' ? keyOrObj : keyOrObj[this.keyName]); + this.storage.removeItem(key); + if(callback) + callback(); + }, + + nuke:function(callback) { + var self = this; + this.all(function(r) { + for (var i = 0, l = r.length; i < l; i++) { + self.remove(r[i]); + } + if(callback) + callback(); + }); + } +}; + +// init.js directly included to save on include traffic +// +// @depend LawnchairAdaptorHelpers.js +// +// Copyright 2007, Google Inc. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// 3. Neither the name of Google Inc. nor the names of its contributors may be +// used to endorse or promote products derived from this software without +// specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED +// WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO +// EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +// +// Sets up google.gears.*, which is *the only* supported way to access Gears. +// +// Circumvent this file at your own risk! +// +// In the future, Gears may automatically define google.gears.* without this +// file. Gears may use these objects to transparently fix bugs and compatibility +// issues. Applications that use the code below will continue to work seamlessly +// when that happens. + +(function() { + // We are already defined. Hooray! + if (window.google && google.gears) { + return; + } + + var factory = null; + + // Firefox + if (typeof GearsFactory != 'undefined') { + factory = new GearsFactory(); + } else { + // IE + try { + factory = new ActiveXObject('Gears.Factory'); + // privateSetGlobalObject is only required and supported on IE Mobile on + // WinCE. + if (factory.getBuildInfo().indexOf('ie_mobile') != -1) { + factory.privateSetGlobalObject(this); + } + } catch (e) { + // Safari + if ((typeof navigator.mimeTypes != 'undefined') + && navigator.mimeTypes["application/x-googlegears"]) { + factory = document.createElement("object"); + factory.style.display = "none"; + factory.width = 0; + factory.height = 0; + factory.type = "application/x-googlegears"; + document.documentElement.appendChild(factory); + } + } + } + + // *Do not* define any objects if Gears is not installed. This mimics the + // behavior of Gears defining the objects in the future. + if (!factory) { + return; + } + + // Now set up the objects, being careful not to overwrite anything. + // + // Note: In Internet Explorer for Windows Mobile, you can't add properties to + // the window object. However, global objects are automatically added as + // properties of the window object in all browsers. + if (!window.google) { + google = {}; + } + + if (!google.gears) { + google.gears = {factory: factory}; + } +})(); + +/** + * GearsSQLiteAdaptor + * =================== + * Gears flavored SQLite implementation for Lawnchair. + * + */ +var GearsSQLiteAdaptor = function(options) { + for (var i in LawnchairAdaptorHelpers) { + this[i] = LawnchairAdaptorHelpers[i]; + } + this.init(options); +}; + + +GearsSQLiteAdaptor.prototype = { + init:function(options) { + var that = this; + var merge = that.merge; + var opts = (typeof arguments[0] == 'string') ? {table:options} : options; + this.name = merge('Lawnchair', opts.name); + this.keyName = merge('key', opts.keyName); + this.table = merge('field', opts.table); + this.db = google.gears.factory.create('beta.database'); + this.db.open(this.name); + this.db.execute('create table if not exists ' + this.table + ' (id NVARCHAR(32) UNIQUE PRIMARY KEY, value TEXT, timestamp REAL)'); + }, + save:function(obj, callback) { + var that = this; + + var insert = function(obj, callback) { + var id = (obj[that.keyName] == undefined) ? that.uuid() : obj[that.keyName]; + delete(obj[that.keyName]); + + var rs = that.db.execute( + "INSERT INTO " + that.table + " (id, value, timestamp) VALUES (?,?,?)", + [id, that.serialize(obj), that.now()] + ); + if (callback != undefined) { + obj[that.keyName] = id; + callback(obj); + } + }; + + var update = function(id, obj, callback) { + that.db.execute( + "UPDATE " + that.table + " SET value=?, timestamp=? WHERE id=?", + [that.serialize(obj), that.now(), id] + ); + if (callback != undefined) { + obj[that.keyName] = id; + callback(obj); + } + }; + + if (obj[that.keyName] == undefined) { + insert(obj, callback); + } else { + this.get(obj[that.keyName], function(r) { + var isUpdate = (r != null); + + if (isUpdate) { + var id = obj[that.keyName]; + delete(obj[that.keyName]); + update(id, obj, callback); + } else { + insert(obj, callback); + } + }); + } + + }, + get:function(key, callback) { + var rs = this.db.execute("SELECT * FROM " + this.table + " WHERE id = ?", [key]); + + if (rs.isValidRow()) { + // FIXME need to test null return / empty recordset + var o = this.deserialize(rs.field(1)); + o[this.keyName] = key; + callback(o); + } else { + callback(null); + } + rs.close(); + }, + all:function(callback) { + var cb = this.terseToVerboseCallback(callback); + var rs = this.db.execute("SELECT * FROM " + this.table); + var r = []; + var o; + + // FIXME need to add 0 len support + //if (results.rows.length == 0 ) { + // cb([]); + + while (rs.isValidRow()) { + o = this.deserialize(rs.field(1)); + o[this.keyName] = rs.field(0); + r.push(o); + rs.next(); + } + rs.close(); + cb(r); + }, + remove:function(keyOrObj, callback) { + this.db.execute( + "DELETE FROM " + this.table + " WHERE id = ?", + [(typeof keyOrObj == 'string') ? keyOrObj : keyOrObj[this.keyName]] + ); + if(callback) + callback(); + }, + nuke:function(callback) { + this.db.execute("DELETE FROM " + this.table); + if(callback) + callback(); + return this; + } +}; + +/** + * UserDataAdaptor + * =================== + * UserData implementation for Lawnchair for older IE browsers. + * + * @depend LawnchairAdaptorHelpers.js + * + */ +var UserDataAdaptor = function(options) { + for (var i in LawnchairAdaptorHelpers) { + this[i] = LawnchairAdaptorHelpers[i]; + } + this.init(options); +}; + +UserDataAdaptor.prototype = { + init:function(options){ + this.keyName = merge('key', options.keyName); + var s = document.createElement('span'); + s.style.behavior = 'url(\'#default#userData\')'; + s.style.position = 'absolute'; + s.style.left = 10000; + document.body.appendChild(s); + this.storage = s; + this.storage.load('lawnchair'); + }, + get:function(key, callback){ + + var obj = this.deserialize(this.storage.getAttribute(key)); + if (obj) { + obj[this.keyName] = key; + + } + if (callback) + callback(obj); + }, + save:function(obj, callback){ + var id = obj[this.keyName] || 'lc' + this.uuid(); + delete obj[this.keyName]; + this.storage.setAttribute(id, this.serialize(obj)); + this.storage.save('lawnchair'); + if (callback){ + obj[this.keyName] = id; + callback(obj); + } + }, + all:function(callback){ + var cb = this.terseToVerboseCallback(callback); + var ca = this.storage.XMLDocument.firstChild.attributes; + var yar = []; + var v,o; + // yo ho yo ho a pirates life for me + for (var i = 0, l = ca.length; i < l; i++) { + v = ca[i]; + o = this.deserialize(v.nodeValue); + if (o) { + o[this.keyName] = v.nodeName; + yar.push(o); + } + } + if (cb) + cb(yar); + }, + remove:function(keyOrObj,callback) { + var key = (typeof keyOrObj == 'string') ? keyOrObj : keyOrObj[this.keyName]; + this.storage.removeAttribute(key); + this.storage.save('lawnchair'); + if(callback) + callback(); + }, + nuke:function(callback) { + var that = this; + this.all(function(r){ + for (var i = 0, l = r.length; i < l; i++) { + if (r[i][that.keyName]) + that.remove(r[i][that.keyName]); + } + if(callback) + callback(); + }); + } +}; + +/** + * WebkitSQLiteAdaptor + * =================== + * Sqlite implementation for Lawnchair. + * + * @depend LawnchairAdaptorHelpers.js + * + */ +var WebkitSQLiteAdaptor = function(options) { + for (var i in LawnchairAdaptorHelpers) { + this[i] = LawnchairAdaptorHelpers[i]; + } + this.init(options); +}; + + +WebkitSQLiteAdaptor.prototype = { + init:function(options) { + var that = this; + var merge = that.merge; + var opts = (typeof arguments[0] == 'string') ? {table:options} : options; + + // default properties + this.name = merge('Lawnchair', opts.name); + this.keyName = merge('key', opts.keyName); + this.version = merge('1.0', opts.version); + this.table = merge('field', opts.table); + this.display = merge('shed', opts.display); + this.max = merge(65536, opts.max); + this.db = merge(null, opts.db); + this.perPage = merge(10, opts.perPage); + + // default sqlite callbacks + this.onError = function(){}; + this.onData = function(){}; + + if("onError" in opts) { + this.onError = opts.onError; + } + + // error out on shit browsers + if (!window.openDatabase) + throw('Lawnchair, "This browser does not support sqlite storage."'); + + // instantiate the store + this.db = openDatabase(this.name, this.version, this.display, this.max); + + // create a default database and table if one does not exist + this.db.transaction(function(tx) { + tx.executeSql("SELECT COUNT(*) FROM " + that.table, [], function(){}, function(tx, error) { + that.db.transaction(function(tx) { + tx.executeSql("CREATE TABLE "+ that.table + " (id NVARCHAR(32) UNIQUE PRIMARY KEY, value TEXT, timestamp REAL)", [], function(){}, that.onError); + }); + }); + }); + }, + save:function(obj, callback) { + var that = this; + + var update = function(id, obj, callback) { + that.db.transaction(function(t) { + t.executeSql( + "UPDATE " + that.table + " SET value=?, timestamp=? WHERE id=?", + [that.serialize(obj), that.now(), id], + function() { + if (callback != undefined) { + obj[that.keyName] = id; + that.terseToVerboseCallback(callback)(obj); + } + }, + that.onError + ); + }); + }; + var insert = function(obj, callback) { + that.db.transaction(function(t) { + var id = (obj[that.keyName] == undefined) ? that.uuid() : obj[that.keyName]; + delete(obj[that.keyName]); + t.executeSql( + "INSERT INTO " + that.table + " (id, value,timestamp) VALUES (?,?,?)", + [id, that.serialize(obj), that.now()], + function() { + if (callback != undefined) { + obj[that.keyName] = id; + that.terseToVerboseCallback(callback)(obj); + } + }, + that.onError + ); + }); + }; + if (obj[that.keyName] == undefined) { + insert(obj, callback); + } else { + this.get(obj[that.keyName], function(r) { + var isUpdate = (r != null); + + if (isUpdate) { + var id = obj[that.keyName]; + delete(obj[that.keyName]); + update(id, obj, callback); + } else { + insert(obj, callback); + } + }); + } + }, + get:function(key, callback) { + var that = this; + this.db.transaction(function(t) { + t.executeSql( + "SELECT value FROM " + that.table + " WHERE id = ?", + [key], + function(tx, results) { + if (results.rows.length == 0) { + that.terseToVerboseCallback(callback)(null); + } else { + var o = that.deserialize(results.rows.item(0).value); + o[that.keyName] = key; + that.terseToVerboseCallback(callback)(o); + } + }, + this.onError + ); + }); + }, + all:function(callback) { + var cb = this.terseToVerboseCallback(callback); + var that = this; + this.db.transaction(function(t) { + t.executeSql("SELECT * FROM " + that.table, [], function(tx, results) { + if (results.rows.length == 0 ) { + cb([]); + } else { + var r = []; + for (var i = 0, l = results.rows.length; i < l; i++) { + var raw = results.rows.item(i).value; + var obj = that.deserialize(raw); + obj[that.keyName] = results.rows.item(i).id; + r.push(obj); + } + cb(r); + } + }, + that.onError); + }); + }, + paged:function(page, callback) { + var cb = this.terseToVerboseCallback(callback); + var that = this; + this.db.transaction(function(t) { + var offset = that.perPage * (page - 1); // a little offset math magic so users don't have to be 0-based + var sql = "SELECT * FROM " + that.table + " ORDER BY timestamp ASC LIMIT ? OFFSET ?"; + t.executeSql(sql, [that.perPage, offset], function(tx, results) { + if (results.rows.length == 0 ) { + cb([]); + } else { + var r = []; + for (var i = 0, l = results.rows.length; i < l; i++) { + var raw = results.rows.item(i).value; + var obj = that.deserialize(raw); + obj[that.keyName] = results.rows.item(i).id; + r.push(obj); + } + cb(r); + } + }, + that.onError); + }); + }, + remove:function(keyOrObj, callback) { + var that = this; + if (callback) + callback = that.terseToVerboseCallback(callback); + this.db.transaction(function(t) { + t.executeSql( + "DELETE FROM " + that.table + " WHERE id = ?", + [(typeof keyOrObj == 'string') ? keyOrObj : keyOrObj[that.keyName]], + callback || that.onData, + that.onError + ); + }); + }, + nuke:function(callback) { + var that = this; + if (callback) + callback = that.terseToVerboseCallback(callback); + this.db.transaction(function(tx) { + tx.executeSql( + "DELETE FROM " + that.table, + [], + callback || that.onData, + that.onError + ); + }); + } +}; + +/** + * Lawnchair + * ========= + * A lightweight JSON document store. + * + * @depend adaptors/AIRSqliteAdaptor.js + * @depend adaptors/AIRSqliteAsyncAdaptor.js + * @depend adaptors/BlackBerryPersistentStorageAdaptor.js + * @depend adaptors/CookieAdaptor.js + * @depend adaptors/DOMStorageAdaptor.js + * @depend adaptors/GearsSQLiteAdaptor.js + * @depend adaptors/UserDataStorage.js + * @depend adaptors/WebkitSQLiteAdaptor.js + * + */ +var Lawnchair = function(opts) { + this.init(opts); +} + +Lawnchair.prototype = { + + init:function(opts) { + var adaptors = { + 'webkit':window.WebkitSQLiteAdaptor, + 'gears':window.GearsSQLiteAdaptor, + 'dom':window.DOMStorageAdaptor, + 'cookie':window.CookieAdaptor, + 'air':window.AIRSQLiteAdaptor, + 'userdata':window.UserDataAdaptor, + 'air-async':window.AIRSQLiteAsyncAdaptor, + 'blackberry':window.BlackBerryPersistentStorageAdaptor + }; + this.adaptor = opts.adaptor ? new adaptors[opts.adaptor](opts) : new WebkitSQLiteAdaptor(opts); + + // Check for native JSON functions. + if (!JSON || !JSON.stringify) throw "Native JSON functions unavailable - please include http://www.json.org/json2.js or run on a decent browser :P"; + }, + + // Save an object to the store. If a key is present then update. Otherwise create a new record. + save:function(obj, callback) {this.adaptor.save(obj, callback)}, + + // Invokes a callback on an object with the matching key. + get:function(key, callback) {this.adaptor.get(key, callback)}, + + // Returns whether a key exists to a callback. + exists:function(callback) {this.adaptor.exists(callback)}, + + // Returns all rows to a callback. + all:function(callback) {this.adaptor.all(callback)}, + + // Removes a json object from the store. + remove:function(keyOrObj, callback) {this.adaptor.remove(keyOrObj, callback)}, + + // Removes all documents from a store and returns self. + nuke:function(callback) {this.adaptor.nuke(callback);return this}, + + // Returns a page of results based on offset provided by user and perPage option + paged:function(page, callback) {this.adaptor.paged(page, callback)}, + + /** + * Iterator that accepts two paramters (methods or eval strings): + * + * - conditional test for a record + * - callback to invoke on matches + * + */ + find:function(condition, callback) { + var is = (typeof condition == 'string') ? function(r){return eval(condition)} : condition; + var cb = this.adaptor.terseToVerboseCallback(callback); + + this.each(function(record, index) { + if (is(record)) cb(record, index); // thats hot + }); + }, + + + /** + * Classic iterator. + * - Passes the record and the index as the second parameter to the callback. + * - Accepts a string for eval or a method to be invoked for each document in the collection. + */ + each:function(callback) { + var cb = this.adaptor.terseToVerboseCallback(callback); + this.all(function(results) { + var l = results.length; + for (var i = 0; i < l; i++) { + cb(results[i], i); + } + }); + } +// -- +}; + diff --git a/pub/js/pilgrim.js b/pub/js/pilgrim.js index 42a06fe..df779a6 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); @@ -74,26 +76,106 @@ var pilgrim = (function () { this.headers = { 'X-Requested-With': 'XMLHttpRequest', - 'Accept': 'application/json' + 'Accept': 'application/json', + 'Cache-Control': 'no-store, no-cache', + 'Pragma': 'no-cache' }; for (var k in headers) { this.headers[k] = headers[k] } }; this.XHR.prototype.send = function (callback) { 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 body = this.responseText ? JSON.parse(this.responseText) : {}; + 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 (responseText && typeof responseText === 'string' && responseText.search(/offline/i) != -1) { + status = 'error'; + } + } + + 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); + + 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 }); } }; + //Workaround occasional weird resetting of Accepts header in firefox + if (navigator && navigator.userAgent.search(/firefox/i) != -1) { + this.xhr.onload = onreadystatechange; + } + + 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'; diff --git a/pub/js/thingler.js b/pub/js/thingler.js index d6e7c22..3f0e981 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 || {}; @@ -35,6 +38,15 @@ var room = { this.data.push(change); + //replay onto doc + doc_handlers[change.type](room.doc, change); + + //save doc to localStorage + room.localStorage.save(room.doc); + + //save changes to localStorage + 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. if (type === 'insert' || type === 'lock') { @@ -44,67 +56,99 @@ var room = { commit: function () { var commit = this.data; 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 + if (data && data.changes) { + data.changes.forEach(function (change) { + ui_handlers[change.type](change); + //call change's callback if any + change.callback && change.callback(); + }); + } + }); } }, initialize: function (doc) { - // Initialize title and revision number - room.rev = doc._rev && parseInt(doc._rev.match(/^(\d+)/)[1]); - room.doc = doc; - setTitle(doc.title); - - if (doc.locked) { - lock.addClass('locked'); - room.locked = true; + if (doc && doc._rev) { + // 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); + + if (doc.locked) { + lock.addClass('locked'); + room.locked = true; + } + header.style.display = 'block'; + + // Initialize list + doc.items && doc.items.forEach(function (item) { + list.appendChild(createItem(item)); + }); + + handleTagFilter(hash); + dom.sortable(list, handleSort); + + footer.style.visibility = 'visible' + + // Before shutdown, do one last sync + window.onbeforeunload = function (e) { + 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) { + var changes = room.changes.commit(); + xhr.resource(id)[last ? 'postSync' : 'post']({ + rev: room.rev || 0, + changes: changes, + last: last + }, function (err, doc) { + if (err || (doc && !doc.commits)) { + if (err && 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 replay 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(); + }); + }); + }); } - header.style.display = 'block'; - - // Initialize list - doc.items && doc.items.forEach(function (item) { - list.appendChild(createItem(item)); - }); - handleTagFilter(hash); - dom.sortable(list, handleSort); - - footer.style.visibility = 'visible' - - // Before shutdown, do one last sync - window.onbeforeunload = function (e) { - clock.tick(true); - }; - - // - // 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) { console.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(); - }); - }); - }); } }; @@ -146,6 +190,8 @@ var clock = { // Called on every interval tick. // tick: function (arg) { + //cancel sync if offline (only for browsers that support this) + if (!navigator.onLine) return; if (! this.synchronising) { this.synchronising = true; this.callback(this, arg); @@ -177,7 +223,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 = ''; @@ -212,62 +258,14 @@ 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() } - } -}); -var handlers = { +var ui_handlers = { insert: function (change) { var item = createItem(change); list.insertBefore(item, list.firstChild); @@ -329,6 +327,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'; @@ -348,7 +401,7 @@ lock.onclick = function () { if (room.locked) { room.changes.push('unlock', null, {}); - handlers.unlock(); + ui_handlers.unlock(); } else { input.disabled = false; input.removeClass('disabled'); @@ -361,7 +414,7 @@ lock.onclick = function () { room.changes.push('lock', null, { password: input.value }, function () { dom.hide(passwordProtect); }); - handlers.lock(); + ui_handlers.lock(); return false; } }; @@ -376,6 +429,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'), @@ -490,7 +561,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'); }); @@ -573,6 +644,80 @@ 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.changes.replayLocalChanges(); + 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); + room.changes.replayLocalChanges(); + 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.changes.replayLocalChanges(); + 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 // diff --git a/pub/offline.html b/pub/offline.html new file mode 100644 index 0000000..f486740 --- /dev/null +++ b/pub/offline.html @@ -0,0 +1,16 @@ + + + + Thingler - Offline + + + +

You are offline!

+

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

+ + diff --git a/src/index.js b/src/index.js index 0673a88..c8673e1 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, @@ -36,7 +37,7 @@ this.server = http.createServer(function (request, response) { // of the request, send a 500 back. var timer = setTimeout(function () { if (! response.finished) { - if (request.headers.accept.indexOf('application/json') !== -1) { + if (request.headers.accept && request.headers.accept.indexOf('application/json') !== -1) { response.writeHead(500, {}); response.end(JSON.stringify({error: 500})); } else { @@ -48,6 +49,30 @@ 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(changed, key) { + var headers = { + "Content-Type": "text/cache-manifest", + "Cache-Control": "must-revalidate", + "ETag": key + }; + + if (changed) { + headers['Expires'] = new Date().toLocaleString(); + headers['Last-Modified'] = new Date().toLocaleString(); + } + + if (env === 'development') { + sys.puts([ + new(Date)().toJSON(), + '/application.manifest', + sys.inspect(headers), + ].join(' -- ')); + } + + file.serveFile('/application.manifest', 200, headers, request, response); + clearTimeout(timer); + }, ['/js'], ['*'],['/ /offline.html']); } else if (request.url === '/') { todo.create(function (id) { finish(303, { 'Location': '/' + id }); @@ -66,7 +91,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, no-cache'; + result.headers['Pragma'] = 'no-cache'; + result.headers['Expires'] = 'Sun, 19 Nov 1978 05:00:00 GMT'; finish(result.status, result.headers, result.body); }); } diff --git a/src/offline.js b/src/offline.js new file mode 100644 index 0000000..7f6e147 --- /dev/null +++ b/src/offline.js @@ -0,0 +1,146 @@ +var fs = require('fs'); +var sys = require('sys'); +var manifestPath = './pub/application.manifest'; + +this.cacheManifest = function(env, callback, cache, network, fallback) { + var config = { + cache: [], + 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++) { + 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 manifest doesn't exist or key has changed) + fs.stat(manifestPath, function(err, stat) { + var exists = true; + + if (err) {exists = false;} + + var filesToCache = listFiles(); + precache_key(filesToCache, function(key) { + hasKeyChanged(exists, key, function(changed) { + if (changed || !exists) { + config.cache = config.cache.concat(filesToCache); + generateManifest(key, callback); + } + else callback(false, key); + }); + }); + }); + + //check wether the manifest key has changed + var hasKeyChanged = function(manifestExists, newKey, cb) { + if (!manifestExists) { + cb(false); + } else { + fs.open(manifestPath, 'r+', function(err,fd) { + if (err) return; + var b = new Buffer(57); + fs.read(fd,b,0,57,0, function(err) { + if (err) return; + var oldKey = b.toString('utf8').match(/#(.*)$/)[1]; + if (oldKey.trim() != newKey.trim()) { + cb(true); + } else { + cb(false); + } + }); + }); + } + }; + + //calculate the hash 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('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]); + }; + + body.push(''); + + fs.writeFile(manifestPath, body.join("\n"), function(err) { + if (err) { throw err;} + cbk(true, key); + }); + }; + +}; + +//TODO: Find a way to make this totally async +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 != manifestPath) { + paths.push(newPath); + } + } + } + }; + recursivelylistFiles('.' + public_root); + return paths; +};