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;
+};