├── .gitignore ├── _attachments ├── about.html ├── images │ └── icon.png ├── index.html ├── script │ ├── app.js │ ├── html.js │ ├── md5.js │ └── oldapp.js └── style │ ├── iphone.css │ ├── main.css │ └── old.css ├── evently ├── actions │ └── _init │ │ └── selectors │ │ ├── a[href=#allchans] │ │ └── click.js │ │ ├── a[href=#mychans] │ │ └── click.js │ │ └── a[href=#new] │ │ └── click.js ├── channels │ ├── _init.txt │ └── all │ │ ├── data.js │ │ ├── mustache.html │ │ └── query.json ├── chat │ ├── join │ │ ├── after.js │ │ ├── mustache.html │ │ ├── path.txt │ │ ├── query.js │ │ └── selectors │ │ │ └── ul │ │ │ └── _changes │ │ │ ├── data.js │ │ │ ├── mustache.html │ │ │ ├── query.js │ │ │ ├── render.txt │ │ │ └── selectors │ │ │ └── a[href=#delete] │ │ │ └── click.js │ └── newChannel │ │ ├── mustache.html │ │ └── selectors │ │ └── form │ │ └── submit.js └── profile │ ├── loggedOut │ └── mustache.html │ └── profileReady │ ├── mustache.html │ └── selectors │ └── form │ └── submit.js ├── options.json ├── validate_doc_update.js ├── vendor ├── couchapp │ ├── README.md │ ├── _attachments │ │ ├── docs.css │ │ ├── docs.html │ │ ├── docs.js │ │ ├── jquery.couch.app.js │ │ ├── jquery.couch.app.util.js │ │ ├── jquery.evently.js │ │ ├── jquery.mustache.js │ │ └── jquery.pathbinder.js │ ├── docs │ │ ├── account.md │ │ ├── couchapp.md │ │ ├── docs.md │ │ ├── evently.md │ │ ├── pathbinder.md │ │ └── profile.md │ ├── evently │ │ ├── account │ │ │ ├── _init.js │ │ │ ├── adminParty.js │ │ │ ├── doLogin.js │ │ │ ├── doLogout.js │ │ │ ├── doSignup.js │ │ │ ├── loggedIn │ │ │ │ ├── after.js │ │ │ │ ├── data.js │ │ │ │ ├── mustache.html │ │ │ │ └── selectors.json │ │ │ ├── loggedOut │ │ │ │ ├── mustache.html │ │ │ │ └── selectors.json │ │ │ ├── loginForm │ │ │ │ ├── after.js │ │ │ │ ├── mustache.html │ │ │ │ └── selectors │ │ │ │ │ ├── a[href=#signup].json │ │ │ │ │ └── form │ │ │ │ │ └── submit.js │ │ │ └── signupForm │ │ │ │ ├── after.js │ │ │ │ ├── mustache.html │ │ │ │ └── selectors │ │ │ │ ├── a[href=#login].json │ │ │ │ └── form │ │ │ │ └── submit.js │ │ ├── docs │ │ │ ├── index │ │ │ │ ├── data.js │ │ │ │ ├── mustache.html │ │ │ │ └── path.txt │ │ │ └── topic │ │ │ │ ├── after.js │ │ │ │ ├── data.js │ │ │ │ ├── edit │ │ │ │ └── _init │ │ │ │ │ ├── fun.js │ │ │ │ │ └── selectors │ │ │ │ │ ├── a.edit │ │ │ │ │ └── click.js │ │ │ │ │ └── a.run │ │ │ │ │ └── click.js │ │ │ │ ├── mustache.html │ │ │ │ └── path.txt │ │ └── profile │ │ │ ├── loggedIn.js │ │ │ ├── loggedOut │ │ │ ├── after.js │ │ │ └── mustache.html │ │ │ ├── noProfile │ │ │ ├── data.js │ │ │ ├── mustache.html │ │ │ └── selectors │ │ │ │ └── form │ │ │ │ └── submit.js │ │ │ └── profileReady │ │ │ ├── after.js │ │ │ ├── data.js │ │ │ └── mustache.html │ ├── lib │ │ ├── atom.js │ │ ├── docform.js │ │ ├── list.js │ │ ├── path.js │ │ └── redirect.js │ └── metadata.json └── mustache │ └── _attachments │ └── jquery.mustache.js └── views ├── authors └── map.js ├── channels └── map.js ├── messages ├── map.js └── reduce.txt └── userProfile └── map.js /.gitignore: -------------------------------------------------------------------------------- 1 | .couchapprc -------------------------------------------------------------------------------- /_attachments/about.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | About Toast 5 | 6 | 7 | 8 | 12 |
13 |

Go to the index to make new channels or find others. Download the Toast source code at Github.

14 |

Toast is 100% pure CouchApp. You are encouraged to install CouchDB on your computer and run your own Toast instances. You can even use replication to sync with other people's chats in near real-time.

15 |

Learn About CouchDB

16 |

O'Reilly is releasing a book on CouchDB. Jan, Noah and myself are the authors. The book is released under Creative Commons and available for free on the web.

17 |

About CouchApps:

18 |
19 |

Get Involved:

20 |

Join the CouchApp Mailing List. Also please replicate this database to your local instance of CouchDB, modify the application, pass it along, and show your friends. :)

21 |

If you're interested in improving or customizing Toast, join the Toasting channel.

22 |
23 |
24 | 25 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /_attachments/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jchris/toast/f57dddb70e0bd590d35967ec789488a9d5ea2bfa/_attachments/images/icon.png -------------------------------------------------------------------------------- /_attachments/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Toast CouchDB Chat 5 | 6 | 7 | 8 | 13 |
14 | 23 |
24 |
25 |
26 |
27 |
28 |
29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /_attachments/script/app.js: -------------------------------------------------------------------------------- 1 | $.couch.app(function(app) { 2 | // setup the account and profile widgets 3 | $("#account").evently(app.ddoc.vendor.couchapp.evently.account, app); 4 | 5 | // extend the vendor profile widget with Toast specific code 6 | var profile = $.extend(true, {}, 7 | app.ddoc.vendor.couchapp.evently.profile, 8 | app.ddoc.evently.profile); 9 | $("#profile").evently(profile, app); 10 | 11 | $.evently.connect($("#account"), $("#profile"), ["loggedIn", "loggedOut"]); 12 | 13 | $("#actions").evently(app.ddoc.evently.actions); 14 | $("#channels").evently(app.ddoc.evently.channels, app); 15 | $("#chat").evently(app.ddoc.evently.chat, app); 16 | $.pathbinder.begin("/"); 17 | }); -------------------------------------------------------------------------------- /_attachments/script/html.js: -------------------------------------------------------------------------------- 1 | function escapeHTML(st) { 2 | return( 3 | st && st.replace(/&/g,'&'). 4 | replace(/>/g,'>'). 5 | replace(/'+a+''; 17 | }).replace(/\@([\w\-]+)/g,function(user,name) { 18 | return ''+user+''; 19 | }).replace(/\#([\w\-]+)/g,function(word,term) { 20 | return ''+word+''; 21 | }); 22 | }; 23 | -------------------------------------------------------------------------------- /_attachments/script/md5.js: -------------------------------------------------------------------------------- 1 | /* 2 | * A JavaScript implementation of the RSA Data Security, Inc. MD5 Message 3 | * Digest Algorithm, as defined in RFC 1321. 4 | * Version 2.1 Copyright (C) Paul Johnston 1999 - 2002. 5 | * Other contributors: Greg Holt, Andrew Kepert, Ydnar, Lostinet 6 | * Distributed under the BSD License 7 | * See http://pajhome.org.uk/crypt/md5 for more info. 8 | */ 9 | 10 | /* 11 | * Configurable variables. You may need to tweak these to be compatible with 12 | * the server-side, but the defaults work in most cases. 13 | */ 14 | var hexcase = 0; /* hex output format. 0 - lowercase; 1 - uppercase */ 15 | var b64pad = ""; /* base-64 pad character. "=" for strict RFC compliance */ 16 | var chrsz = 8; /* bits per input character. 8 - ASCII; 16 - Unicode */ 17 | 18 | /* 19 | * These are the functions you'll usually want to call 20 | * They take string arguments and return either hex or base-64 encoded strings 21 | */ 22 | function hex_md5(s){ return binl2hex(core_md5(str2binl(s), s.length * chrsz));} 23 | function b64_md5(s){ return binl2b64(core_md5(str2binl(s), s.length * chrsz));} 24 | function str_md5(s){ return binl2str(core_md5(str2binl(s), s.length * chrsz));} 25 | function hex_hmac_md5(key, data) { return binl2hex(core_hmac_md5(key, data)); } 26 | function b64_hmac_md5(key, data) { return binl2b64(core_hmac_md5(key, data)); } 27 | function str_hmac_md5(key, data) { return binl2str(core_hmac_md5(key, data)); } 28 | 29 | /* 30 | * Perform a simple self-test to see if the VM is working 31 | */ 32 | function md5_vm_test() 33 | { 34 | return hex_md5("abc") == "900150983cd24fb0d6963f7d28e17f72"; 35 | } 36 | 37 | /* 38 | * Calculate the MD5 of an array of little-endian words, and a bit length 39 | keep 40 | */ 41 | function core_md5(x, len) 42 | { 43 | /* append padding */ 44 | x[len >> 5] |= 0x80 << ((len) % 32); 45 | x[(((len + 64) >>> 9) << 4) + 14] = len; 46 | 47 | var a = 1732584193; 48 | var b = -271733879; 49 | var c = -1732584194; 50 | var d = 271733878; 51 | 52 | for(var i = 0; i < x.length; i += 16) 53 | { 54 | var olda = a; 55 | var oldb = b; 56 | var oldc = c; 57 | var oldd = d; 58 | 59 | a = md5_ff(a, b, c, d, x[i+ 0], 7 , -680876936); 60 | d = md5_ff(d, a, b, c, x[i+ 1], 12, -389564586); 61 | c = md5_ff(c, d, a, b, x[i+ 2], 17, 606105819); 62 | b = md5_ff(b, c, d, a, x[i+ 3], 22, -1044525330); 63 | a = md5_ff(a, b, c, d, x[i+ 4], 7 , -176418897); 64 | d = md5_ff(d, a, b, c, x[i+ 5], 12, 1200080426); 65 | c = md5_ff(c, d, a, b, x[i+ 6], 17, -1473231341); 66 | b = md5_ff(b, c, d, a, x[i+ 7], 22, -45705983); 67 | a = md5_ff(a, b, c, d, x[i+ 8], 7 , 1770035416); 68 | d = md5_ff(d, a, b, c, x[i+ 9], 12, -1958414417); 69 | c = md5_ff(c, d, a, b, x[i+10], 17, -42063); 70 | b = md5_ff(b, c, d, a, x[i+11], 22, -1990404162); 71 | a = md5_ff(a, b, c, d, x[i+12], 7 , 1804603682); 72 | d = md5_ff(d, a, b, c, x[i+13], 12, -40341101); 73 | c = md5_ff(c, d, a, b, x[i+14], 17, -1502002290); 74 | b = md5_ff(b, c, d, a, x[i+15], 22, 1236535329); 75 | 76 | a = md5_gg(a, b, c, d, x[i+ 1], 5 , -165796510); 77 | d = md5_gg(d, a, b, c, x[i+ 6], 9 , -1069501632); 78 | c = md5_gg(c, d, a, b, x[i+11], 14, 643717713); 79 | b = md5_gg(b, c, d, a, x[i+ 0], 20, -373897302); 80 | a = md5_gg(a, b, c, d, x[i+ 5], 5 , -701558691); 81 | d = md5_gg(d, a, b, c, x[i+10], 9 , 38016083); 82 | c = md5_gg(c, d, a, b, x[i+15], 14, -660478335); 83 | b = md5_gg(b, c, d, a, x[i+ 4], 20, -405537848); 84 | a = md5_gg(a, b, c, d, x[i+ 9], 5 , 568446438); 85 | d = md5_gg(d, a, b, c, x[i+14], 9 , -1019803690); 86 | c = md5_gg(c, d, a, b, x[i+ 3], 14, -187363961); 87 | b = md5_gg(b, c, d, a, x[i+ 8], 20, 1163531501); 88 | a = md5_gg(a, b, c, d, x[i+13], 5 , -1444681467); 89 | d = md5_gg(d, a, b, c, x[i+ 2], 9 , -51403784); 90 | c = md5_gg(c, d, a, b, x[i+ 7], 14, 1735328473); 91 | b = md5_gg(b, c, d, a, x[i+12], 20, -1926607734); 92 | 93 | a = md5_hh(a, b, c, d, x[i+ 5], 4 , -378558); 94 | d = md5_hh(d, a, b, c, x[i+ 8], 11, -2022574463); 95 | c = md5_hh(c, d, a, b, x[i+11], 16, 1839030562); 96 | b = md5_hh(b, c, d, a, x[i+14], 23, -35309556); 97 | a = md5_hh(a, b, c, d, x[i+ 1], 4 , -1530992060); 98 | d = md5_hh(d, a, b, c, x[i+ 4], 11, 1272893353); 99 | c = md5_hh(c, d, a, b, x[i+ 7], 16, -155497632); 100 | b = md5_hh(b, c, d, a, x[i+10], 23, -1094730640); 101 | a = md5_hh(a, b, c, d, x[i+13], 4 , 681279174); 102 | d = md5_hh(d, a, b, c, x[i+ 0], 11, -358537222); 103 | c = md5_hh(c, d, a, b, x[i+ 3], 16, -722521979); 104 | b = md5_hh(b, c, d, a, x[i+ 6], 23, 76029189); 105 | a = md5_hh(a, b, c, d, x[i+ 9], 4 , -640364487); 106 | d = md5_hh(d, a, b, c, x[i+12], 11, -421815835); 107 | c = md5_hh(c, d, a, b, x[i+15], 16, 530742520); 108 | b = md5_hh(b, c, d, a, x[i+ 2], 23, -995338651); 109 | 110 | a = md5_ii(a, b, c, d, x[i+ 0], 6 , -198630844); 111 | d = md5_ii(d, a, b, c, x[i+ 7], 10, 1126891415); 112 | c = md5_ii(c, d, a, b, x[i+14], 15, -1416354905); 113 | b = md5_ii(b, c, d, a, x[i+ 5], 21, -57434055); 114 | a = md5_ii(a, b, c, d, x[i+12], 6 , 1700485571); 115 | d = md5_ii(d, a, b, c, x[i+ 3], 10, -1894986606); 116 | c = md5_ii(c, d, a, b, x[i+10], 15, -1051523); 117 | b = md5_ii(b, c, d, a, x[i+ 1], 21, -2054922799); 118 | a = md5_ii(a, b, c, d, x[i+ 8], 6 , 1873313359); 119 | d = md5_ii(d, a, b, c, x[i+15], 10, -30611744); 120 | c = md5_ii(c, d, a, b, x[i+ 6], 15, -1560198380); 121 | b = md5_ii(b, c, d, a, x[i+13], 21, 1309151649); 122 | a = md5_ii(a, b, c, d, x[i+ 4], 6 , -145523070); 123 | d = md5_ii(d, a, b, c, x[i+11], 10, -1120210379); 124 | c = md5_ii(c, d, a, b, x[i+ 2], 15, 718787259); 125 | b = md5_ii(b, c, d, a, x[i+ 9], 21, -343485551); 126 | 127 | a = safe_add(a, olda); 128 | b = safe_add(b, oldb); 129 | c = safe_add(c, oldc); 130 | d = safe_add(d, oldd); 131 | } 132 | return Array(a, b, c, d); 133 | 134 | } 135 | 136 | /* 137 | * These functions implement the four basic operations the algorithm uses. 138 | */ 139 | function md5_cmn(q, a, b, x, s, t) 140 | { 141 | return safe_add(bit_rol(safe_add(safe_add(a, q), safe_add(x, t)), s),b); 142 | } 143 | function md5_ff(a, b, c, d, x, s, t) 144 | { 145 | return md5_cmn((b & c) | ((~b) & d), a, b, x, s, t); 146 | } 147 | function md5_gg(a, b, c, d, x, s, t) 148 | { 149 | return md5_cmn((b & d) | (c & (~d)), a, b, x, s, t); 150 | } 151 | function md5_hh(a, b, c, d, x, s, t) 152 | { 153 | return md5_cmn(b ^ c ^ d, a, b, x, s, t); 154 | } 155 | function md5_ii(a, b, c, d, x, s, t) 156 | { 157 | return md5_cmn(c ^ (b | (~d)), a, b, x, s, t); 158 | } 159 | 160 | /* 161 | * Calculate the HMAC-MD5, of a key and some data 162 | */ 163 | function core_hmac_md5(key, data) 164 | { 165 | var bkey = str2binl(key); 166 | if(bkey.length > 16) bkey = core_md5(bkey, key.length * chrsz); 167 | 168 | var ipad = Array(16), opad = Array(16); 169 | for(var i = 0; i < 16; i++) 170 | { 171 | ipad[i] = bkey[i] ^ 0x36363636; 172 | opad[i] = bkey[i] ^ 0x5C5C5C5C; 173 | } 174 | 175 | var hash = core_md5(ipad.concat(str2binl(data)), 512 + data.length * chrsz); 176 | return core_md5(opad.concat(hash), 512 + 128); 177 | } 178 | 179 | /* 180 | * Add integers, wrapping at 2^32. This uses 16-bit operations internally 181 | * to work around bugs in some JS interpreters. 182 | */ 183 | function safe_add(x, y) 184 | { 185 | var lsw = (x & 0xFFFF) + (y & 0xFFFF); 186 | var msw = (x >> 16) + (y >> 16) + (lsw >> 16); 187 | return (msw << 16) | (lsw & 0xFFFF); 188 | } 189 | 190 | /* 191 | * Bitwise rotate a 32-bit number to the left. 192 | */ 193 | function bit_rol(num, cnt) 194 | { 195 | return (num << cnt) | (num >>> (32 - cnt)); 196 | } 197 | 198 | /* 199 | * Convert a string to an array of little-endian words 200 | * If chrsz is ASCII, characters >255 have their hi-byte silently ignored. 201 | keep 202 | */ 203 | function str2binl(str) 204 | { 205 | var bin = Array(); 206 | var mask = (1 << chrsz) - 1; 207 | for(var i = 0; i < str.length * chrsz; i += chrsz) 208 | bin[i>>5] |= (str.charCodeAt(i / chrsz) & mask) << (i%32); 209 | return bin; 210 | } 211 | 212 | /* 213 | * Convert an array of little-endian words to a string 214 | */ 215 | function binl2str(bin) 216 | { 217 | var str = ""; 218 | var mask = (1 << chrsz) - 1; 219 | for(var i = 0; i < bin.length * 32; i += chrsz) 220 | str += String.fromCharCode((bin[i>>5] >>> (i % 32)) & mask); 221 | return str; 222 | } 223 | 224 | /* 225 | * Convert an array of little-endian words to a hex string. 226 | keep 227 | */ 228 | function binl2hex(binarray) 229 | { 230 | var hex_tab = hexcase ? "0123456789ABCDEF" : "0123456789abcdef"; 231 | var str = ""; 232 | for(var i = 0; i < binarray.length * 4; i++) 233 | { 234 | str += hex_tab.charAt((binarray[i>>2] >> ((i%4)*8+4)) & 0xF) + 235 | hex_tab.charAt((binarray[i>>2] >> ((i%4)*8 )) & 0xF); 236 | } 237 | return str; 238 | } 239 | 240 | /* 241 | * Convert an array of little-endian words to a base-64 string 242 | */ 243 | function binl2b64(binarray) 244 | { 245 | var tab = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; 246 | var str = ""; 247 | for(var i = 0; i < binarray.length * 4; i += 3) 248 | { 249 | var triplet = (((binarray[i >> 2] >> 8 * ( i %4)) & 0xFF) << 16) 250 | | (((binarray[i+1 >> 2] >> 8 * ((i+1)%4)) & 0xFF) << 8 ) 251 | | ((binarray[i+2 >> 2] >> 8 * ((i+2)%4)) & 0xFF); 252 | for(var j = 0; j < 4; j++) 253 | { 254 | if(i * 8 + j * 6 > binarray.length * 32) str += b64pad; 255 | else str += tab.charAt((triplet >> 6*(3-j)) & 0x3F); 256 | } 257 | } 258 | return str; 259 | } -------------------------------------------------------------------------------- /_attachments/script/oldapp.js: -------------------------------------------------------------------------------- 1 | $.couch.app(function(app) { 2 | var userProfile = {}, currentChannel = null, since_seq = 0; 3 | 4 | // TODO package these logged in and logged as part of the userprofile code 5 | function loggedIn(e, resp) { 6 | // get the user profile doc 7 | // todo loggedIn should just trigger the user profile event 8 | app.view("userProfile", { 9 | key : resp.userCtx.name, success : function(view) { 10 | if (view.rows.length == 0) { 11 | // no profile yet 12 | userProfile = { 13 | type : "userProfile", 14 | authorRand : userProfile.authorRand || Math.random().toString(), 15 | name : resp.userCtx.name 16 | }; 17 | $("#new_message").trigger("newProfile"); 18 | } else if (view.rows.length == 1) { 19 | // populate the form with the profile 20 | userProfile = view.rows[0].value; 21 | $("#new_message").trigger("newProfile"); 22 | } else { 23 | // some kind of collision 24 | // todo create a ui for picking your profile 25 | // from the available list 26 | userProfile = {}; 27 | $("#new_message").trigger("newProfile"); 28 | alert("More than one profile for "+resp.userCtx.name+". Please resolve.") 29 | } 30 | }}); 31 | // todo 32 | // preview gravatar 33 | // copy gravatar to profile doc: 34 | // http://stackoverflow.com/questions/934012/get-image-data-in-javascript 35 | }; 36 | function loggedOut(e) { 37 | console.log("loggedout") 38 | console.log(userProfile) 39 | userProfile = { 40 | authorRand : Math.random().toString(), 41 | }; 42 | $("#new_message").trigger("newProfile"); 43 | }; 44 | 45 | // setup the account widget 46 | // first we customize a template for Toast 47 | $.couch.app.account.loggedIn.template = 'Toasty ' + $.couch.app.account.loggedIn.template; 48 | // now launch the evently widget. 49 | $.couch.app.account.loggedIn = [$.couch.app.account.loggedIn, loggedIn]; 50 | $.couch.app.account.loggedOut = [$.couch.app.account.loggedOut, loggedOut]; 51 | $("#userCtx").evently($.couch.app.account); 52 | 53 | // todo move this to an evently handler 54 | $("#new_channel").submit(function() { 55 | var cname = $('#name', this).val(); 56 | // return false; 57 | // $('body').append('redirect'); 58 | // var absurl = $('body a:last')[0].href; 59 | document.location = "#/channel/" + encodeURIComponent(cname); 60 | return false; 61 | }); 62 | 63 | $("#new_message").evently({ 64 | submit : [function(e) { 65 | e.preventDefault(); 66 | var name, email, url; 67 | name = $("#author-name").val(); 68 | email = $("#author-email").val(); 69 | url = $("#author-url").val(); 70 | if (name == userProfile.name && 71 | email == userProfile.email && 72 | url == userProfile.url) { 73 | // no changes, ignore 74 | } else { 75 | userProfile.name = name; 76 | userProfile.email = email; 77 | userProfile.url = url; 78 | app.db.saveDoc(userProfile); 79 | } 80 | return false; 81 | }, 82 | function() { 83 | // post new_message 84 | var name, email, url, body; 85 | name = $("#author-name").val(); 86 | email = $("#author-email").val(); 87 | url = $("#author-url").val(); 88 | body = $("#message").val(); 89 | if (body) { 90 | var message = { 91 | author: { 92 | name : name, 93 | email : email, 94 | url :url, 95 | rand : userProfile.authorRand 96 | }, 97 | date : new Date(), 98 | body : body 99 | }; 100 | app.db.saveDoc({ 101 | channel : currentChannel, 102 | message : message 103 | }, { 104 | success : function() { 105 | $("#message").val(''); 106 | } 107 | }); 108 | } 109 | return false; 110 | }], 111 | newProfile : function(e) { 112 | // update the form with the new profile info 113 | // setup the channel form based on the user profile 114 | $("#author-name", this).val(userProfile.name || ""); 115 | $("#author-email", this).val(userProfile.email || ""); 116 | $("#author-url", this).val(userProfile.url || ""); 117 | } 118 | }); 119 | 120 | 121 | function latestMessages(cname, fun) { 122 | app.view("channels",{ 123 | reduce: false, 124 | startkey : [cname,{}], 125 | endkey : [cname, since_seq+1], 126 | limit : 25, 127 | descending : true, 128 | success: function(json) { 129 | var new_rows = $.grep(json.rows, function(row) { 130 | return (row.key[1] > since_seq); 131 | }).reverse(); 132 | if (new_rows.length > 0) { 133 | since_seq = new_rows[new_rows.length - 1].key[1]; 134 | fun(new_rows); 135 | } 136 | } 137 | }); 138 | }; 139 | 140 | function prependMessages(messages) { 141 | messages.forEach(function(row) { 142 | // todo use mustache 143 | var m = row.value; 144 | var li = '
  • ' 145 | + '' 146 | + (m.author.url ? 147 | ''+ 150 | m.author.name 151 | +'' 152 | : m.author.name) 153 | + ": " 154 | + linkify(m.body) 155 | + '
    '+( m.date || 'perma')+'
  • '; 156 | $("#messages").prepend(li); 157 | }); 158 | }; 159 | 160 | var templates = { 161 | channel_list : '', // todo use partial 162 | li_channel : '
  • {{name}} {{count}} messages
  • ', 163 | index : { 164 | title : 165 | 'Toast Chat, powered by Apache CouchDB', 166 | title_tag : "Toast Chat powered by Apache CouchDB" 167 | 168 | }, 169 | channel : { 170 | title : 171 | 'Toast :: {{channel}}' 172 | } 173 | } 174 | 175 | var chatApp = $.sammy(function() { 176 | this.debug = true; 177 | this.element_selector = '#chat'; 178 | this.use(Sammy.Mustache, 'mustache'); 179 | 180 | 181 | // populate the default channel list 182 | // link to channels 183 | 184 | this.helpers({ 185 | listChannels : function(fun) { 186 | app.view("channels", {group_level: 1, success: function(json) { 187 | fun(json); 188 | }}); 189 | } 190 | }) 191 | 192 | this.get("#/", function(e) { 193 | $('h1').html($.mustache(templates.index.title)); 194 | document.title = templates.index.title_tag; 195 | 196 | this.listChannels(function(json) { 197 | e.channels = json.rows.map(function(row) { 198 | return { 199 | link : "#/channel/" + encodeURIComponent(row.key[0]), 200 | name : row.key[0], 201 | count : row.value 202 | }; 203 | }); 204 | 205 | e.partial('templates/channels.html.mustache', e, function(t) { 206 | this.app.swap(t); 207 | }); 208 | }); 209 | 210 | // setup footer 211 | $("#new_message").hide(); 212 | $("#new_channel").show(); 213 | }); 214 | 215 | this.get("#/channel/:channel", function(e) { 216 | var channel = this.params.channel; 217 | currentChannel = channel; 218 | since_seq = 0; 219 | 220 | $('h1').html($.mustache(templates.channel.title, { 221 | channel : channel 222 | })); 223 | document.title = "Toast :: "+channel; 224 | 225 | // setup the channel viewer 226 | e.partial('templates/channel.html.mustache', e, function(t) { 227 | this.app.swap(t); 228 | }); 229 | // view channel and append new junks 230 | // setup changes consumer to keep doing that 231 | 232 | latestMessages(currentChannel, prependMessages); 233 | connectToChanges(app, function() { 234 | latestMessages(currentChannel, prependMessages); 235 | }); 236 | 237 | // setup footer 238 | $("#new_channel").hide(); 239 | $("#new_message").show(); 240 | }); 241 | 242 | }); 243 | 244 | chatApp.run('#/'); 245 | 246 | }); 247 | 248 | 249 | 250 | 251 | function connectToChanges(app, fun) { 252 | function resetHXR(x) { 253 | x.abort(); 254 | connectToChanges(app, fun); 255 | }; 256 | app.db.info({success: function(db_info) { 257 | var c_xhr = jQuery.ajaxSettings.xhr(); 258 | c_xhr.open("GET", app.db.uri+"_changes?feed=continuous&since="+db_info.update_seq, true); 259 | c_xhr.send(""); 260 | c_xhr.onreadystatechange = fun; 261 | setTimeout(function() { 262 | resetHXR(c_xhr); 263 | }, 1000 * 60); 264 | }}); 265 | }; 266 | 267 | -------------------------------------------------------------------------------- /_attachments/style/iphone.css: -------------------------------------------------------------------------------- 1 | body { 2 | width:600px; 3 | } -------------------------------------------------------------------------------- /_attachments/style/main.css: -------------------------------------------------------------------------------- 1 | body { 2 | font:1em Helvetica, sans-serif; 3 | margin:0; 4 | padding:4px; 5 | } 6 | 7 | h1 { 8 | margin:0; 9 | } 10 | 11 | h2 { 12 | color:#222; 13 | } 14 | 15 | h3 { 16 | margin:4px; 17 | color:#222; 18 | font-style: italic; 19 | } 20 | 21 | #header { 22 | padding:4px; 23 | } 24 | 25 | #account { 26 | float:right; 27 | } 28 | 29 | #content { 30 | clear:both; 31 | } 32 | 33 | #activity { 34 | width:65%; 35 | } 36 | 37 | #sidebar { 38 | float:right; 39 | width:30%; 40 | } 41 | 42 | ul { 43 | list-style: none; 44 | margin:0; 45 | padding:4px; 46 | } 47 | 48 | .clear { 49 | clear:left; 50 | } 51 | 52 | #chat li, #profile { 53 | padding:4px; 54 | padding-bottom:0; 55 | margin:6px; 56 | margin-bottom:0; 57 | border: 1px solid #999; 58 | background-color:#ddd; 59 | -webkit-border-radius:14px; 60 | -moz-border-radius:14px; 61 | } 62 | 63 | #chat li a.delete { 64 | color:#ddd; 65 | -webkit-transition-property:color; 66 | -webkit-transition-duration:0.25s; 67 | -moz-transition-property:color; 68 | -moz-transition-duration:0.25s; 69 | } 70 | 71 | #chat li:hover a.delete { 72 | color:#00b; 73 | } 74 | 75 | #chat .meta { 76 | font-size: 0.78em; 77 | color:#333; 78 | padding:4px; 79 | } 80 | 81 | #chat .body { 82 | padding:2px; 83 | } 84 | 85 | form { 86 | background:#ddd; 87 | padding:4px; 88 | margin:6px; 89 | } 90 | 91 | input[name=message] { 92 | width:88%; 93 | } 94 | 95 | div.avatar { 96 | padding:2px; 97 | padding-bottom:0; 98 | margin-right:4px; 99 | float:left; 100 | font-size:0.78em; 101 | width : 60px; 102 | text-align: center; 103 | } 104 | 105 | div.avatar .name { 106 | padding-top:2px; 107 | } 108 | 109 | div.avatar img { 110 | margin:0 auto; 111 | padding:0; 112 | width : 40px; 113 | height : 40px; 114 | } 115 | -------------------------------------------------------------------------------- /_attachments/style/old.css: -------------------------------------------------------------------------------- 1 | 2 | 3 | #tagcloud, #usercloud { 4 | padding:24px 4px; 5 | } 6 | 7 | #tasks ul { 8 | list-style: none; 9 | margin:0; 10 | padding:0; 11 | } 12 | 13 | .clear { 14 | clear:left; 15 | } 16 | 17 | #tasks li, #profile { 18 | padding:4px; 19 | padding-bottom:0; 20 | margin:6px; 21 | margin-bottom:0; 22 | border: 1px solid #fff; 23 | background-color:#fff; 24 | -webkit-border-radius:14px; 25 | -webkit-transition-property:background-color; 26 | -webkit-transition-duration:0.5s; 27 | -moz-border-radius:14px; 28 | -moz-transition-property:background-color; 29 | -moz-transition-duration:0.5s; 30 | } 31 | 32 | div#profile { 33 | border: 1px solid #999; 34 | background-color:#ddd; 35 | } 36 | 37 | #profile textarea { 38 | width:80%; 39 | } 40 | 41 | #tasks li:hover { 42 | border: 1px solid #999; 43 | background-color:#aaa; 44 | } 45 | 46 | #tasks li.done, #tasks li.done:hover { 47 | background-color:#cfb; 48 | } 49 | 50 | #tasks li .body { 51 | padding:8px; 52 | padding-bottom:0; 53 | } 54 | 55 | #tasks li .react a { 56 | color:#fff; 57 | padding:0 4px; 58 | font-size:0.78em; 59 | } 60 | 61 | #tasks li div.replies li { 62 | font-size:0.84em; 63 | background:none; 64 | border:1px solid #fff; 65 | } 66 | 67 | #tasks li div.replies { 68 | margin:2px; 69 | } 70 | 71 | div.avatar { 72 | padding:2px; 73 | padding-bottom:0; 74 | margin-right:4px; 75 | float:left; 76 | font-size:0.78em; 77 | width : 60px; 78 | height : 60px; 79 | text-align: center; 80 | } 81 | 82 | div.avatar .name { 83 | padding-top:2px; 84 | } 85 | 86 | div.avatar img { 87 | margin:0 auto; 88 | padding:0; 89 | width : 40px; 90 | height : 40px; 91 | } 92 | 93 | -------------------------------------------------------------------------------- /evently/actions/_init/selectors/a[href=#allchans]/click.js: -------------------------------------------------------------------------------- 1 | function() { 2 | $("#channels").trigger("all"); 3 | return false; 4 | } -------------------------------------------------------------------------------- /evently/actions/_init/selectors/a[href=#mychans]/click.js: -------------------------------------------------------------------------------- 1 | function() { 2 | $("#channels").trigger("mine"); 3 | return false; 4 | } -------------------------------------------------------------------------------- /evently/actions/_init/selectors/a[href=#new]/click.js: -------------------------------------------------------------------------------- 1 | function() { 2 | $("#chat").trigger("newChannel"); 3 | return false; 4 | } -------------------------------------------------------------------------------- /evently/channels/_init.txt: -------------------------------------------------------------------------------- 1 | all -------------------------------------------------------------------------------- /evently/channels/all/data.js: -------------------------------------------------------------------------------- 1 | function(resp) { 2 | return { 3 | channels : resp.rows.map(function(r) { 4 | var v = r.value; 5 | v.channel_uri = encodeURIComponent(v.channel); 6 | return v; 7 | }) 8 | }; 9 | }; 10 | -------------------------------------------------------------------------------- /evently/channels/all/mustache.html: -------------------------------------------------------------------------------- 1 | {{#channels}} 2 |
  • {{channel}}
  • 3 | {{/channels}} -------------------------------------------------------------------------------- /evently/channels/all/query.json: -------------------------------------------------------------------------------- 1 | { 2 | "view" : "channels" 3 | } -------------------------------------------------------------------------------- /evently/chat/join/after.js: -------------------------------------------------------------------------------- 1 | function(resp) { 2 | var chan = resp.rows[0].value; 3 | $$(this).channel = chan.channel; 4 | $("h1").text("Toast :: "+chan.channel); 5 | $("#chandesc").text(chan.desc); 6 | }; -------------------------------------------------------------------------------- /evently/chat/join/mustache.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /evently/chat/join/path.txt: -------------------------------------------------------------------------------- 1 | /channel/:channel -------------------------------------------------------------------------------- /evently/chat/join/query.js: -------------------------------------------------------------------------------- 1 | function(e, p) { 2 | return { 3 | view : "channels", 4 | key : p.channel 5 | }; 6 | }; -------------------------------------------------------------------------------- /evently/chat/join/selectors/ul/_changes/data.js: -------------------------------------------------------------------------------- 1 | function(row) { 2 | var v = row.value; 3 | var d = { 4 | avatar_url : v.author && v.author.gravatar_url || "", 5 | body : $.linkify($.mustache.escape(v.body)), 6 | name : v.author && v.author.nickname || "", 7 | url : v.author && v.author.url || "", 8 | id : row.id, 9 | created_at : $.prettyDate(v.created_at) 10 | }; 11 | var p = $$("#profile").profile; 12 | if (p && v.author && v.author.rand && (v.author.rand == p.rand)) { 13 | // todo _admin owns everything... 14 | d.owned = true; 15 | } 16 | return d; 17 | }; -------------------------------------------------------------------------------- /evently/chat/join/selectors/ul/_changes/mustache.html: -------------------------------------------------------------------------------- 1 |
  • 2 |
    3 | {{#avatar_url}}{{/avatar_url}} 4 |
    5 | {{name}} 6 |
    7 |
    {{{body}}}
    8 |
    9 | {{created_at}} 10 | {{#owned}}[x]{{/owned}} 11 |
    12 |
    13 |
  • -------------------------------------------------------------------------------- /evently/chat/join/selectors/ul/_changes/query.js: -------------------------------------------------------------------------------- 1 | function(e) { 2 | var chan = e.data.args[0].rows[0].value; 3 | return { 4 | view : "messages", 5 | limit : 25, 6 | startkey : [chan.channel, {}], 7 | endkey : [chan.channel], 8 | reduce : false, 9 | descending : true, 10 | type : "newRows" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /evently/chat/join/selectors/ul/_changes/render.txt: -------------------------------------------------------------------------------- 1 | prepend -------------------------------------------------------------------------------- /evently/chat/join/selectors/ul/_changes/selectors/a[href=#delete]/click.js: -------------------------------------------------------------------------------- 1 | function() { 2 | var li = $(this).parents("li"); 3 | var app = $$(this).app; 4 | $.log("d",li); 5 | var message_id = li.attr("data-id"); 6 | app.db.openDoc(message_id, { 7 | success : function(doc) { 8 | $.log("delete", doc) 9 | app.db.removeDoc(doc, { 10 | success : function() { 11 | li.slideUp("slow"); 12 | } 13 | }); 14 | } 15 | }); 16 | 17 | return false; 18 | }; -------------------------------------------------------------------------------- /evently/chat/newChannel/mustache.html: -------------------------------------------------------------------------------- 1 |
    2 |

    Create a Channel

    3 |

    4 | 8 |

    9 |

    10 | 14 |

    15 |

    16 |
    -------------------------------------------------------------------------------- /evently/chat/newChannel/selectors/form/submit.js: -------------------------------------------------------------------------------- 1 | function() { 2 | var f = $(this); 3 | var app = $$(f).app; 4 | var name = $("input[name=name]", f).val(); 5 | var doc = { 6 | id : "toast.channel:"+name, 7 | channel : name, 8 | desc : $("input[name=desc]", f).val(), 9 | type : "channel", 10 | author : $$("#profile").profile 11 | }; 12 | app.db.saveDoc(doc, { 13 | success : function() { 14 | $("#chat").trigger("join", [{channel:name}]); 15 | $("#channels").trigger("all"); 16 | } 17 | }) 18 | return false; 19 | }; -------------------------------------------------------------------------------- /evently/profile/loggedOut/mustache.html: -------------------------------------------------------------------------------- 1 |

    Please log in to chat.

    -------------------------------------------------------------------------------- /evently/profile/profileReady/mustache.html: -------------------------------------------------------------------------------- 1 |
    2 | 6 | 7 |
    -------------------------------------------------------------------------------- /evently/profile/profileReady/selectors/form/submit.js: -------------------------------------------------------------------------------- 1 | function(e) { 2 | var f = $(this); 3 | var doc = { 4 | body : $("input[name=message]", f).val(), 5 | channel : $$("#chat").channel, 6 | author : $$("#profile").profile, 7 | created_at : new Date() 8 | }; 9 | $$(f).app.db.saveDoc(doc, { 10 | success : function() { 11 | $("input[name=message]", f).val(""); 12 | } 13 | }) 14 | return false; 15 | } 16 | -------------------------------------------------------------------------------- /options.json: -------------------------------------------------------------------------------- 1 | { 2 | "local_seq": true 3 | } -------------------------------------------------------------------------------- /validate_doc_update.js: -------------------------------------------------------------------------------- 1 | function (newDoc, oldDoc, userCtx) { 2 | function forbidden(message) { 3 | throw({forbidden : message}); 4 | }; 5 | 6 | function unauthorized(message) { 7 | throw({unauthorized : message}); 8 | }; 9 | 10 | if (userCtx.roles.indexOf('_admin') == -1) { 11 | // admin can edit anything, only check when not admin... 12 | if (!newDoc.author || !newDoc.author.name) { 13 | forbidden("Docs must have an author with a name"); 14 | } 15 | // you can't lie about authorship 16 | if (newDoc.author.name != userCtx.name) { 17 | forbidden("You may only update your own docs."); 18 | } 19 | // you can only update your own docs 20 | 21 | if (newDoc._deleted) 22 | forbidden("You may not delete a doc."); 23 | } 24 | }; 25 | -------------------------------------------------------------------------------- /vendor/couchapp/README.md: -------------------------------------------------------------------------------- 1 | ## CouchApp - more than just a filesystem mapper 2 | 3 | This is where documentation will go for the client and server JavaScript parts of CouchApp. -------------------------------------------------------------------------------- /vendor/couchapp/_attachments/docs.css: -------------------------------------------------------------------------------- 1 | body { 2 | font:1em Helvetica, sans-serif; 3 | margin:0; 4 | padding:4px; 5 | } 6 | 7 | h1 { 8 | margin:0.5em; 9 | } 10 | 11 | h2 { 12 | color:#222; 13 | } 14 | 15 | pre { 16 | padding:4px; 17 | margin:4px; 18 | background:#bbb; 19 | } 20 | 21 | #content { 22 | padding:4px; 23 | margin:2px; 24 | } 25 | 26 | #sidebar { 27 | float:right; 28 | width:34%; 29 | } 30 | 31 | #docs { 32 | -moz-box-shadow:0 0 2em #000; 33 | -webkit-box-shadow:0 0 2em #000; 34 | width:58%; 35 | padding:8px; 36 | margin:4px; 37 | } 38 | 39 | .example { 40 | background:#ffd; 41 | padding:4px; 42 | margin:4px; 43 | position:absolute; 44 | } 45 | 46 | textarea.code { 47 | width:100%; 48 | } 49 | 50 | .edit { 51 | float:right; 52 | font-size:0.8em; 53 | } 54 | -------------------------------------------------------------------------------- /vendor/couchapp/_attachments/docs.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Evently and CouchApp Docs 5 | 6 | 7 | 8 | 12 |
    13 | 16 |
    17 |
    18 |
    19 |
    20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /vendor/couchapp/_attachments/docs.js: -------------------------------------------------------------------------------- 1 | $.log = function() { 2 | // console.log(arguments) 3 | }; 4 | 5 | $.couch.app(function(app) { 6 | $("#docs").evently(app.ddoc.vendor.couchapp.evently.docs, app); 7 | $.pathbinder.begin("/"); 8 | }); -------------------------------------------------------------------------------- /vendor/couchapp/_attachments/jquery.couch.app.js: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not 2 | // use this file except in compliance with the License. You may obtain a copy 3 | // of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 9 | // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 10 | // License for the specific language governing permissions and limitations under 11 | // the License. 12 | 13 | // Usage: The passed in function is called when the page is ready. 14 | // CouchApp passes in the app object, which takes care of linking to 15 | // the proper database, and provides access to the CouchApp helpers. 16 | // $.couch.app(function(app) { 17 | // app.db.view(...) 18 | // ... 19 | // }); 20 | 21 | (function($) { 22 | 23 | function Design(db, name) { 24 | this.doc_id = "_design/"+name; 25 | this.view = function(view, opts) { 26 | db.view(name+'/'+view, opts); 27 | }; 28 | } 29 | 30 | $.couch.app = $.couch.app || function(appFun) { 31 | $(function() { 32 | var dbname = document.location.href.split('/')[3]; 33 | var dname = unescape(document.location.href).split('/')[5]; 34 | var db = $.couch.db(dbname); 35 | var design = new Design(db, dname); 36 | 37 | // docForm applies CouchDB behavior to HTML forms. 38 | // todo make this a couch.app plugin 39 | function docForm(formSelector, opts) { 40 | var localFormDoc = {}; 41 | opts = opts || {}; 42 | opts.fields = opts.fields || []; 43 | 44 | // turn the form into deep json 45 | // field names like 'author-email' get turned into json like 46 | // {"author":{"email":"quentin@example.com"}} 47 | function formToDeepJSON(form, fields, doc) { 48 | form = $(form); 49 | opts.fields.forEach(function(field) { 50 | var val = form.find("[name="+field+"]").val(); 51 | if (!val) {return;} 52 | var parts = field.split('-'); 53 | var frontObj = doc, frontName = parts.shift(); 54 | while (parts.length > 0) { 55 | frontObj[frontName] = frontObj[frontName] || {}; 56 | frontObj = frontObj[frontName]; 57 | frontName = parts.shift(); 58 | } 59 | frontObj[frontName] = val; 60 | }); 61 | } 62 | 63 | // Apply the behavior 64 | $(formSelector).submit(function(e) { 65 | e.preventDefault(); 66 | // formToDeepJSON acts on localFormDoc by reference 67 | formToDeepJSON(this, opts.fields, localFormDoc); 68 | if (opts.beforeSave) {opts.beforeSave(localFormDoc);} 69 | db.saveDoc(localFormDoc, { 70 | success : function(resp) { 71 | if (opts.success) {opts.success(resp, localFormDoc);} 72 | } 73 | }); 74 | 75 | return false; 76 | }); 77 | 78 | // populate form from an existing doc 79 | function docToForm(doc) { 80 | var form = $(formSelector); 81 | // fills in forms 82 | opts.fields.forEach(function(field) { 83 | var parts = field.split('-'); 84 | var run = true, frontObj = doc, frontName = parts.shift(); 85 | while (frontObj && parts.length > 0) { 86 | frontObj = frontObj[frontName]; 87 | frontName = parts.shift(); 88 | } 89 | if (frontObj && frontObj[frontName]) { 90 | form.find("[name="+field+"]").val(frontObj[frontName]); 91 | } 92 | }); 93 | } 94 | 95 | if (opts.id) { 96 | db.openDoc(opts.id, { 97 | success: function(doc) { 98 | if (opts.onLoad) {opts.onLoad(doc);} 99 | localFormDoc = doc; 100 | docToForm(doc); 101 | }}); 102 | } else if (opts.template) { 103 | if (opts.onLoad) {opts.onLoad(opts.template);} 104 | localFormDoc = opts.template; 105 | docToForm(localFormDoc); 106 | } 107 | var instance = { 108 | deleteDoc : function(opts) { 109 | opts = opts || {}; 110 | if (confirm("Really delete this document?")) { 111 | db.removeDoc(localFormDoc, opts); 112 | } 113 | }, 114 | localDoc : function() { 115 | formToDeepJSON(formSelector, opts.fields, localFormDoc); 116 | return localFormDoc; 117 | } 118 | }; 119 | return instance; 120 | } 121 | 122 | function resolveModule(names, parent, current) { 123 | if (names.length === 0) { 124 | if (typeof current != "string") { 125 | throw ["error","invalid_require_path", 126 | 'Must require a JavaScript string, not: '+(typeof current)]; 127 | } 128 | return [current, parent]; 129 | } 130 | // we need to traverse the path 131 | var n = names.shift(); 132 | if (n == '..') { 133 | if (!(parent && parent.parent)) { 134 | throw ["error", "invalid_require_path", 'Object has no parent '+JSON.stringify(current)]; 135 | } 136 | return resolveModule(names, parent.parent.parent, parent.parent); 137 | } else if (n == '.') { 138 | if (!parent) { 139 | throw ["error", "invalid_require_path", 'Object has no parent '+JSON.stringify(current)]; 140 | } 141 | return resolveModule(names, parent.parent, parent); 142 | } 143 | if (!current[n]) { 144 | throw ["error", "invalid_require_path", 'Object has no property "'+n+'". '+JSON.stringify(current)]; 145 | } 146 | var p = current; 147 | current = current[n]; 148 | current.parent = p; 149 | return resolveModule(names, p, current); 150 | } 151 | 152 | var p = document.location.pathname.split('/'); 153 | p.shift(); 154 | var qs = document.location.search.replace(/^\?/,'').split('&'); 155 | var q = {}; 156 | qs.forEach(function(param) { 157 | var ps = param.split('='); 158 | var k = decodeURIComponent(ps[0]); 159 | var v = decodeURIComponent(ps[1]); 160 | if (["startkey", "endkey", "key"].indexOf(k) != -1) { 161 | q[k] = JSON.parse(v); 162 | } else { 163 | q[k] = v; 164 | } 165 | }); 166 | var mockReq = { 167 | path : p, 168 | query : q 169 | }; 170 | 171 | var appExports = $.extend({ 172 | db : db, 173 | design : design, 174 | view : design.view, 175 | docForm : docForm, 176 | req : mockReq 177 | }, $.couch.app.app); 178 | 179 | function handleDDoc(ddoc) { 180 | if (ddoc) { 181 | var require = function(name, parent) { 182 | var exports = {}; 183 | var resolved = resolveModule(name.split('/'), parent, ddoc); 184 | var source = resolved[0]; 185 | parent = resolved[1]; 186 | var s = "var func = function (exports, require) { " + source + " };"; 187 | try { 188 | eval(s); 189 | func.apply(ddoc, [exports, function(name) {return require(name, parent, source)}]); 190 | } catch(e) { 191 | throw ["error","compilation_error","Module require('"+name+"') raised error "+e.toSource()]; 192 | } 193 | return exports; 194 | } 195 | appExports.ddoc = ddoc; 196 | appExports.require = require; 197 | } 198 | // todo make app-exports the this in the execution context? 199 | appFun.apply(appExports, [appExports]); 200 | } 201 | 202 | if ($.couch.app.ddocs[design.doc_id]) { 203 | handleDDoc($.couch.app.ddocs[design.doc_id]) 204 | } else { 205 | // only open 1 connection for this ddoc 206 | if ($.couch.app.ddoc_handlers[design.doc_id]) { 207 | // we are already fetching, just wait 208 | $.couch.app.ddoc_handlers[design.doc_id].push(handleDDoc); 209 | } else { 210 | $.couch.app.ddoc_handlers[design.doc_id] = [handleDDoc]; 211 | db.openDoc(design.doc_id, { 212 | success : function(doc) { 213 | $.couch.app.ddocs[design.doc_id] = doc; 214 | $.couch.app.ddoc_handlers[design.doc_id].forEach(function(h) { 215 | h(doc); 216 | }); 217 | $.couch.app.ddoc_handlers[design.doc_id] = null; 218 | }, 219 | error : function() { 220 | $.couch.app.ddoc_handlers[design.doc_id].forEach(function(h) { 221 | h(); 222 | }); 223 | $.couch.app.ddoc_handlers[design.doc_id] = null; 224 | } 225 | }); 226 | } 227 | } 228 | 229 | }); 230 | }; 231 | $.couch.app.ddocs = {}; 232 | $.couch.app.ddoc_handlers = {}; 233 | // legacy support. $.CouchApp is deprecated, please use $.couch.app 234 | $.CouchApp = $.couch.app; 235 | })(jQuery); 236 | -------------------------------------------------------------------------------- /vendor/couchapp/_attachments/jquery.couch.app.util.js: -------------------------------------------------------------------------------- 1 | $.log = function() { 2 | if (window && window.console && window.console.log) { 3 | window.console.log(arguments); 4 | } 5 | }; 6 | 7 | // todo remove this crap 8 | function escapeHTML(st) { 9 | return( 10 | st && st.replace(/&/g,'&'). 11 | replace(/>/g,'>'). 12 | replace(/'+a+''; 25 | }).replace(/\@([\w\-]+)/g,function(user,name) { 26 | return ''+user+''; 27 | }).replace(/\#([\w\-\.]+)/g,function(word,tag) { 28 | return ''+word+''; 29 | }); 30 | }; 31 | 32 | $.fn.prettyDate = function() { 33 | $(this).each(function() { 34 | $(this).text($.prettyDate($(this).text())); 35 | }); 36 | }; 37 | 38 | $.prettyDate = function(time){ 39 | 40 | var date = new Date(time.replace(/-/g,"/").replace("T", " ").replace("Z", " +0000").replace(/(\d*\:\d*:\d*)\.\d*/g,"$1")), 41 | diff = (((new Date()).getTime() - date.getTime()) / 1000), 42 | day_diff = Math.floor(diff / 86400); 43 | 44 | if (isNaN(day_diff)) return time; 45 | 46 | return day_diff < 1 && ( 47 | diff < 60 && "just now" || 48 | diff < 120 && "1 minute ago" || 49 | diff < 3600 && Math.floor( diff / 60 ) + " minutes ago" || 50 | diff < 7200 && "1 hour ago" || 51 | diff < 86400 && Math.floor( diff / 3600 ) + " hours ago") || 52 | day_diff == 1 && "yesterday" || 53 | day_diff < 21 && day_diff + " days ago" || 54 | day_diff < 45 && Math.ceil( day_diff / 7 ) + " weeks ago" || 55 | day_diff < 730 && Math.ceil( day_diff / 31 ) + " months ago" || 56 | Math.ceil( day_diff / 365 ) + " years ago"; 57 | }; 58 | -------------------------------------------------------------------------------- /vendor/couchapp/_attachments/jquery.evently.js: -------------------------------------------------------------------------------- 1 | // $$ inspired by @wycats: http://yehudakatz.com/2009/04/20/evented-programming-with-jquery/ 2 | function $$(node) { 3 | var data = $(node).data("$$"); 4 | if (data) { 5 | return data; 6 | } else { 7 | data = {}; 8 | $(node).data("$$", data); 9 | return data; 10 | } 11 | }; 12 | 13 | (function($) { 14 | // utility functions used in the implementation 15 | 16 | function forIn(obj, fun) { 17 | var name; 18 | for (name in obj) { 19 | if (obj.hasOwnProperty(name)) { 20 | fun(name, obj[name]); 21 | } 22 | } 23 | }; 24 | $.forIn = forIn; 25 | function funViaString(fun) { 26 | if (fun && fun.match && fun.match(/function/)) { 27 | eval("var f = "+fun); 28 | if (typeof f == "function") { 29 | return function() { 30 | try { 31 | return f.apply(this, arguments); 32 | } catch(e) { 33 | // IF YOU SEE AN ERROR HERE IT HAPPENED WHEN WE TRIED TO RUN YOUR FUNCTION 34 | // $.log({"message": "Error in evently function.", "error": e, "src" : fun}); 35 | throw(e); 36 | } 37 | }; 38 | } 39 | } 40 | return fun; 41 | }; 42 | 43 | function runIfFun(me, fun, args) { 44 | // if the field is a function, call it, bound to the widget 45 | var f = funViaString(fun); 46 | if (typeof f == "function") { 47 | return f.apply(me, args); 48 | } else { 49 | return fun; 50 | } 51 | } 52 | 53 | $.evently = { 54 | connect : function(source, target, events) { 55 | events.forEach(function(ev) { 56 | source.bind(ev, function() { 57 | var args = $.makeArray(arguments); 58 | // remove the original event to keep from stacking args extra deep 59 | // it would be nice if jquery had a way to pass the original 60 | // event to the trigger method. 61 | args.shift(); 62 | target.trigger(ev, args); 63 | return false; 64 | }); 65 | }); 66 | }, 67 | paths : [], 68 | changesDBs : {} 69 | }; 70 | 71 | $.fn.evently = function(events, app, args) { 72 | var elem = $(this); 73 | // store the app on the element for later use 74 | if (app) { 75 | $$(elem).app = app; 76 | } 77 | 78 | // setup the handlers onto elem 79 | forIn(events, function(name, h) { 80 | eventlyHandler(elem, name, h, args); 81 | }); 82 | 83 | if (events._init) { 84 | $.log("ev _init", elem); 85 | elem.trigger("_init", args); 86 | } 87 | 88 | if (app && events._changes) { 89 | $("body").bind("evently.changes."+app.db.name, function() { 90 | // we want to unbind this function when the element is deleted. 91 | // maybe jquery 1.4.2 has this covered? 92 | $.log('changes', elem); 93 | elem.trigger("_changes"); 94 | }); 95 | followChanges(app); 96 | elem.trigger("_changes"); 97 | } 98 | }; 99 | 100 | // eventlyHandler applies the user's handler (h) to the 101 | // elem, bound to trigger based on name. 102 | function eventlyHandler(elem, name, h, args) { 103 | if (h.path) { 104 | elem.pathbinder(name, h.path); 105 | } 106 | var f = funViaString(h); 107 | if (typeof f == "function") { 108 | elem.bind(name, {args:args}, f); 109 | } else if (typeof f == "string") { 110 | elem.bind(name, {args:args}, function() { 111 | $(this).trigger(f, arguments); 112 | return false; 113 | }); 114 | } else if ($.isArray(h)) { 115 | // handle arrays recursively 116 | for (var i=0; i < h.length; i++) { 117 | eventlyHandler(elem, name, h[i], args); 118 | } 119 | } else { 120 | // an object is using the evently / mustache template system 121 | if (h.fun) { 122 | elem.bind(name, {args:args}, funViaString(h.fun)); 123 | } 124 | // templates, selectors, etc are intepreted 125 | // when our named event is triggered. 126 | elem.bind(name, {args:args}, function() { 127 | renderElement($(this), h, arguments); 128 | return false; 129 | }); 130 | } 131 | }; 132 | 133 | $.fn.replace = function(elem) { 134 | $(this).empty().append(elem); 135 | }; 136 | 137 | // todo: ability to call this 138 | // to render and "prepend/append/etc" a new element to the host element (me) 139 | // as well as call this in a way that replaces the host elements content 140 | // this would be easy if there is a simple way to get at the element we just appended 141 | // (as html) so that we can attache the selectors 142 | function renderElement(me, h, args, qrun) { 143 | // if there's a query object we run the query, 144 | // and then call the data function with the response. 145 | if (h.query && !qrun) { 146 | // $.log("query before renderElement", arguments) 147 | runQuery(me, h, args) 148 | } else if (h.async && !qrun) { 149 | $.log("runAsync") 150 | runAsync(me, h, args) 151 | } else { 152 | // $.log("renderElement") 153 | // $.log(me, h, args, qrun) 154 | // otherwise we just render the template with the current args 155 | var selectors = runIfFun(me, h.selectors, args); 156 | var act = h.render || "replace"; 157 | var app = $$(me).app; 158 | if (h.mustache) { 159 | var newElem = mustachioed(me, h, args); 160 | me[act](newElem); 161 | } 162 | if (selectors) { 163 | if (act == "replace") { 164 | var s = me; 165 | } else { 166 | var s = newElem; 167 | } 168 | forIn(selectors, function(selector, handlers) { 169 | // $.log("selector", selector); 170 | // $.log("selected", $(selector, s)); 171 | $(selector, s).evently(handlers, app, args); 172 | // $.log("applied", selector); 173 | }); 174 | } 175 | if (h.after) { 176 | funViaString(h.after).apply(me, args); 177 | } 178 | } 179 | }; 180 | 181 | // todo this should return the new element 182 | function mustachioed(me, h, args) { 183 | return $($.mustache( 184 | runIfFun(me, h.mustache, args), 185 | runIfFun(me, h.data, args), 186 | runIfFun(me, h.partials, args))); 187 | }; 188 | 189 | function runAsync(me, h, args) { 190 | var app = $$(me).app; 191 | // the callback is the first argument 192 | funViaString(h.async).apply(me, [function() { 193 | renderElement(me, h, arguments, true); 194 | }].concat(args)); 195 | }; 196 | 197 | 198 | function runQuery(me, h, args) { 199 | // $.log("runQuery: args", args) 200 | var app = $$(me).app; 201 | var qu = runIfFun(me, h.query, args); 202 | var qType = qu.type; 203 | var viewName = qu.view; 204 | var userSuccess = qu.success; 205 | // $.log("qType", qType) 206 | 207 | var q = {}; 208 | forIn(qu, function(k, v) { 209 | q[k] = v; 210 | }); 211 | 212 | if (qType == "newRows") { 213 | q.success = function(resp) { 214 | $.log("runQuery newRows success", resp.rows.length, me, resp) 215 | resp.rows.reverse().forEach(function(row) { 216 | renderElement(me, h, [row], true) 217 | }); 218 | if (userSuccess) userSuccess(resp); 219 | }; 220 | newRows(me, app, viewName, q); 221 | } else { 222 | q.success = function(resp) { 223 | // $.log("runQuery success", resp) 224 | renderElement(me, h, [resp], true); 225 | userSuccess && userSuccess(resp); 226 | }; 227 | app.view(viewName, q); 228 | } 229 | } 230 | 231 | // this is for the items handler 232 | // var lastViewId, highKey, inFlight; 233 | // this needs to key per elem 234 | function newRows(elem, app, view, opts) { 235 | // $.log("newRows", arguments); 236 | // on success we'll set the top key 237 | var thisViewId, successCallback = opts.success, full = false; 238 | function successFun(resp) { 239 | // $.log("newRows success", resp) 240 | $$(elem).inFlight = false; 241 | var JSONhighKey = JSON.stringify($$(elem).highKey); 242 | resp.rows = resp.rows.filter(function(r) { 243 | return JSON.stringify(r.key) != JSONhighKey; 244 | }); 245 | if (resp.rows.length > 0) { 246 | if (opts.descending) { 247 | $$(elem).highKey = resp.rows[0].key; 248 | } else { 249 | $$(elem).highKey = resp.rows[resp.rows.length -1].key; 250 | } 251 | }; 252 | if (successCallback) {successCallback(resp, full)}; 253 | }; 254 | opts.success = successFun; 255 | 256 | if (opts.descending) { 257 | thisViewId = view + (opts.startkey ? JSON.stringify(opts.startkey) : ""); 258 | } else { 259 | thisViewId = view + (opts.endkey ? JSON.stringify(opts.endkey) : ""); 260 | } 261 | // $.log(["thisViewId",thisViewId]) 262 | // for query we'll set keys 263 | if (thisViewId == $$(elem).lastViewId) { 264 | // we only want the rows newer than changesKey 265 | var hk = $$(elem).highKey; 266 | if (hk !== undefined) { 267 | if (opts.descending) { 268 | opts.endkey = hk; 269 | // opts.inclusive_end = false; 270 | } else { 271 | opts.startkey = hk; 272 | } 273 | } 274 | // $.log("add view rows", opts) 275 | if (!$$(elem).inFlight) { 276 | $$(elem).inFlight = true; 277 | app.view(view, opts); 278 | } 279 | } else { 280 | // full refresh 281 | // $.log("new view stuff") 282 | full = true; 283 | $$(elem).lastViewId = thisViewId; 284 | $$(elem).highKey = undefined; 285 | $$(elem).inFlight = true; 286 | app.view(view, opts); 287 | } 288 | }; 289 | 290 | // only start one changes listener per db 291 | function followChanges(app) { 292 | var dbName = app.db.name; 293 | if (!$.evently.changesDBs[dbName]) { 294 | connectToChanges(app, function() { 295 | $("body").trigger("evently.changes."+dbName); 296 | }); 297 | $.evently.changesDBs[dbName] = true; 298 | } 299 | } 300 | 301 | function connectToChanges(app, fun) { 302 | function resetHXR(x) { 303 | x.abort(); 304 | connectToChanges(app, fun); 305 | }; 306 | app.db.info({success: function(db_info) { 307 | var c_xhr = jQuery.ajaxSettings.xhr(); 308 | c_xhr.open("GET", app.db.uri+"_changes?feed=continuous&since="+db_info.update_seq, true); 309 | c_xhr.send(""); 310 | // todo use a timeout to prevent rapid triggers 311 | var t; 312 | c_xhr.onreadystatechange = function() { 313 | clearTimeout(t); 314 | t = setTimeout(fun, 100); 315 | }; 316 | setTimeout(function() { 317 | resetHXR(c_xhr); 318 | }, 1000 * 60); 319 | }}); 320 | }; 321 | 322 | })(jQuery); 323 | -------------------------------------------------------------------------------- /vendor/couchapp/_attachments/jquery.mustache.js: -------------------------------------------------------------------------------- 1 | /* 2 | Shameless port of a shameless port 3 | @defunkt => @janl => @aq 4 | 5 | See http://github.com/defunkt/mustache for more info. 6 | */ 7 | 8 | ;(function($) { 9 | 10 | /* 11 | Shamless port of http://github.com/defunkt/mustache 12 | by Jan Lehnardt , Alexander Lang , 13 | Sebastian Cohnen 14 | 15 | Thanks @defunkt for the awesome code. 16 | 17 | See http://github.com/defunkt/mustache for more info. 18 | */ 19 | 20 | var Mustache = function() { 21 | var Renderer = function() {}; 22 | 23 | Renderer.prototype = { 24 | otag: "{{", 25 | ctag: "}}", 26 | pragmas: {}, 27 | 28 | render: function(template, context, partials) { 29 | // fail fast 30 | if(template.indexOf(this.otag) == -1) { 31 | return template; 32 | } 33 | 34 | template = this.render_pragmas(template); 35 | var html = this.render_section(template, context, partials); 36 | return this.render_tags(html, context, partials); 37 | }, 38 | 39 | /* 40 | Looks for %PRAGMAS 41 | */ 42 | render_pragmas: function(template) { 43 | // no pragmas 44 | if(template.indexOf(this.otag + "%") == -1) { 45 | return template; 46 | } 47 | 48 | var that = this; 49 | var regex = new RegExp(this.otag + "%(.+)" + this.ctag); 50 | return template.replace(regex, function(match, pragma) { 51 | that.pragmas[pragma] = true; 52 | return ""; 53 | // ignore unknown pragmas silently 54 | }); 55 | }, 56 | 57 | /* 58 | Tries to find a partial in the global scope and render it 59 | */ 60 | render_partial: function(name, context, partials) { 61 | if(typeof(context[name]) != "object") { 62 | throw({message: "subcontext for '" + name + "' is not an object"}); 63 | } 64 | if(!partials || !partials[name]) { 65 | throw({message: "unknown_partial"}); 66 | } 67 | return this.render(partials[name], context[name], partials); 68 | }, 69 | 70 | /* 71 | Renders boolean and enumerable sections 72 | */ 73 | render_section: function(template, context, partials) { 74 | if(template.indexOf(this.otag + "#") == -1) { 75 | return template; 76 | } 77 | var that = this; 78 | // CSW - Added "+?" so it finds the tighest bound, not the widest 79 | var regex = new RegExp(this.otag + "\\#(.+)" + this.ctag + 80 | "\\s*([\\s\\S]+?)" + this.otag + "\\/\\1" + this.ctag + "\\s*", "mg"); 81 | 82 | // for each {{#foo}}{{/foo}} section do... 83 | return template.replace(regex, function(match, name, content) { 84 | var value = that.find(name, context); 85 | if(that.is_array(value)) { // Enumerable, Let's loop! 86 | return that.map(value, function(row) { 87 | return that.render(content, that.merge(context, 88 | that.create_context(row)), partials); 89 | }).join(''); 90 | } else if(value) { // boolean section 91 | return that.render(content, context, partials); 92 | } else { 93 | return ""; 94 | } 95 | }); 96 | }, 97 | 98 | /* 99 | Replace {{foo}} and friends with values from our view 100 | */ 101 | render_tags: function(template, context, partials) { 102 | var lines = template.split("\n"); 103 | 104 | var new_regex = function() { 105 | return new RegExp(that.otag + "(=|!|>|\\{|%)?([^\/#]+?)\\1?" + 106 | that.ctag + "+", "g"); 107 | }; 108 | 109 | // tit for tat 110 | var that = this; 111 | 112 | var regex = new_regex(); 113 | for (var i=0; i < lines.length; i++) { 114 | lines[i] = lines[i].replace(regex, function (match,operator,name) { 115 | switch(operator) { 116 | case "!": // ignore comments 117 | return match; 118 | case "=": // set new delimiters, rebuild the replace regexp 119 | that.set_delimiters(name); 120 | regex = new_regex(); 121 | // redo the line in order to get tags with the new delimiters 122 | // on the same line 123 | i--; 124 | return ""; 125 | case ">": // render partial 126 | return that.render_partial(name, context, partials); 127 | case "{": // the triple mustache is unescaped 128 | return that.find(name, context); 129 | return ""; 130 | default: // escape the value 131 | return that.escape(that.find(name, context)); 132 | } 133 | },this); 134 | }; 135 | return lines.join("\n"); 136 | }, 137 | 138 | set_delimiters: function(delimiters) { 139 | var dels = delimiters.split(" "); 140 | this.otag = this.escape_regex(dels[0]); 141 | this.ctag = this.escape_regex(dels[1]); 142 | }, 143 | 144 | escape_regex: function(text) { 145 | // thank you Simon Willison 146 | if(!arguments.callee.sRE) { 147 | var specials = [ 148 | '/', '.', '*', '+', '?', '|', 149 | '(', ')', '[', ']', '{', '}', '\\' 150 | ]; 151 | arguments.callee.sRE = new RegExp( 152 | '(\\' + specials.join('|\\') + ')', 'g' 153 | ); 154 | } 155 | return text.replace(arguments.callee.sRE, '\\$1'); 156 | }, 157 | 158 | /* 159 | find `name` in current `context`. That is find me a value 160 | from the view object 161 | */ 162 | find: function(name, context) { 163 | name = this.trim(name); 164 | if(typeof context[name] === "function") { 165 | return context[name].apply(context); 166 | } 167 | if(context[name] !== undefined) { 168 | return context[name]; 169 | } 170 | // silently ignore unkown variables 171 | return ""; 172 | }, 173 | 174 | // Utility methods 175 | 176 | /* 177 | Does away with nasty characters 178 | */ 179 | escape: function(s) { 180 | return s.toString().replace(/[&"<>\\]/g, function(s) { 181 | switch(s) { 182 | case "&": return "&"; 183 | case "\\": return "\\\\";; 184 | case '"': return '\"';; 185 | case "<": return "<"; 186 | case ">": return ">"; 187 | default: return s; 188 | } 189 | }); 190 | }, 191 | 192 | /* 193 | Merges all properties of object `b` into object `a`. 194 | `b.property` overwrites a.property` 195 | */ 196 | merge: function(a, b) { 197 | var _new = {}; 198 | for(var name in a) { 199 | if(a.hasOwnProperty(name)) { 200 | _new[name] = a[name]; 201 | } 202 | }; 203 | for(var name in b) { 204 | if(b.hasOwnProperty(name)) { 205 | _new[name] = b[name]; 206 | } 207 | }; 208 | return _new; 209 | }, 210 | 211 | // by @langalex, support for arrays of strings 212 | create_context: function(_context) { 213 | if(this.is_object(_context)) { 214 | return _context; 215 | } else if(this.pragmas["JSTACHE-ENABLE-STRING-ARRAYS"]) { 216 | return {'.': _context}; 217 | } 218 | }, 219 | 220 | is_object: function(a) { 221 | return a && typeof a == 'object' 222 | }, 223 | 224 | /* 225 | Thanks Doug Crockford 226 | JavaScript — The Good Parts lists an alternative that works better with 227 | frames. Frames can suck it, we use the simple version. 228 | */ 229 | is_array: function(a) { 230 | return (a && 231 | typeof a === 'object' && 232 | a.constructor === Array); 233 | }, 234 | 235 | /* 236 | Gets rid of leading and trailing whitespace 237 | */ 238 | trim: function(s) { 239 | return s.replace(/^\s*|\s*$/g, ''); 240 | }, 241 | 242 | /* 243 | Why, why, why? Because IE. Cry, cry cry. 244 | */ 245 | map: function(array, fn) { 246 | if (typeof array.map == "function") { 247 | return array.map(fn) 248 | } else { 249 | var r = []; 250 | var l = array.length; 251 | for(i=0;i{{json}}", 23 | data : function() { 24 | var widget = app.ddoc.vendor.couchapp.evently.account; 25 | return { 26 | json : JSON.stringify(widget, null, 2) 27 | } 28 | } 29 | } 30 | }); 31 | }); 32 | 33 | The top level keys are the most important: `loggedIn`, `loggedOut`, `adminParty`, `signupForm`, `loginForm`, `doLogin`, `doSignup`, and `_init`. Each one of these corresponds to an event or state the system can be in. Some of them draw user interface elements, other directly trigger further events. 34 | 35 | ### _init 36 | 37 | The `_init` event is special, in that Evently will automatically trigger it when the widget is created. Here is the code for the account widget's `_init` event handler. 38 | 39 | function() { 40 | var elem = $(this); 41 | $.couch.session({ 42 | success : function(r) { 43 | var userCtx = r.userCtx; 44 | if (userCtx.name) { 45 | elem.trigger("loggedIn", [r]); 46 | } else if (userCtx.roles.indexOf("_admin") != -1) { 47 | elem.trigger("adminParty"); 48 | } else { 49 | elem.trigger("loggedOut"); 50 | }; 51 | } 52 | }); 53 | } 54 | 55 | This code does one query to CouchDB, to retrieve the session information for the current user. For this we use the `$.couch.session()` function which is part of the [jquery.couch.js](/_utils/script/jquery.couch.js) library which is part of the CouchDB distribution. 56 | 57 | The response is handled in one of three ways, depending on the user's session information. Either we trigger the `loggedIn` or `loggedOut` events, or in the special case where we detect that CouchDB's security is not properly configured, we trigger the `adminParty` event to warn the user. 58 | 59 | ### loggedOut 60 | 61 | Because most visitors start logged out, let's now turn our attention to the `loggedOut` event handler to see what will greet a new visitor: 62 | 63 | "loggedOut": { 64 | "mustache": "Signup or Login", 65 | "selectors": { 66 | "a[href=#login]": { 67 | "click": "loginForm" 68 | }, 69 | "a[href=#signup]": { 70 | "click": "signupForm" 71 | } 72 | } 73 | } 74 | 75 | There are two main components to this handler: `mustache` and `selectors`. `mustache` is a template file with two HTML links. `selectors` contains a set of CSS selectors with events bound to them. You can think of each selector as a nested Evently widget. In this case, clicking "Login" will trigger the `loginForm` event, while clicking "Signup" triggers the `signupForm` event. 76 | 77 | ### signupForm 78 | 79 | Let's see what happens during signup. We'll skip showing the whole handler (it should be in the sidebar anyway if you clicked "run" earlier.) 80 | 81 | When the `signupForm` event is triggered, a mustache template draws the form. Then the selectors are run, assigning this function to the form's submit event: 82 | 83 | function(e) { 84 | var name = $('input[name=name]', this).val(), 85 | pass = $('input[name=password]', this).val(); 86 | $(this).trigger('doSignup', [name, pass]); 87 | return false; 88 | } 89 | 90 | This handler is as simple as possible, all it does is use jQuery to pull the user data from the form, and send the name and password to the `doSignup` event. We could just use a function call here, but it's nice to keep our individual events as small as possible, as this makes customizing Evently widgets simpler. 91 | 92 | ### doSignup 93 | 94 | Here is the `doSignup` handler: 95 | 96 | function(e, name, pass) { 97 | var elem = $(this); 98 | $.couch.signup({ 99 | name : name 100 | }, pass, { 101 | success : function() { 102 | elem.trigger("doLogin", [name, pass]); 103 | } 104 | }); 105 | } 106 | 107 | Again, all the complex signup logic (encrypting passwords, etc) is pushed to the [jquery.couch.js](/_utils/script/jquery.couch.js) library (via the `$.couch.signup()` call), so our application code can stay as simple as possible. When signup is complete, we trigger the `doLogin` event, so new users don't have to go through another action. 108 | 109 | ### doLogin 110 | 111 | The code for `doLogin` isn't much different, just take the name and password, and call a jquery.couch.js library function with it. 112 | 113 | function(e, name, pass) { 114 | var elem = $(this); 115 | $.couch.login({ 116 | name : name, 117 | password : pass, 118 | success : function(r) { 119 | elem.trigger("_init") 120 | } 121 | }); 122 | } 123 | 124 | The last thing that `doLogin` does is trigger `_init`, so we come full circle! This time, `_init` will see that the user is logged in, and trigger the `loggedIn` event. You'll probably want to hook your application to this `loggedIn` event, to activate any features which are reserved for registered users. We'll cover linking events in a later section. 125 | 126 | ## Customizing the account widget 127 | 128 | Evently widgets are built out of JSON objects, which makes it easy to replace bits and pieces of them without having to mess with the entire widget. We'll start by customizing what users see when they are logged in. 129 | 130 | $.couch.app(function(app){ 131 | var customizedWidget = $.extend(true, {}, app.ddoc.vendor.couchapp.evently.account, { 132 | loggedIn : { 133 | mustache : 'Hello {{name}} you are logged in! ' + 134 | 'Would you like to logout?' 135 | } 136 | }); 137 | $("#customWelcome").evently(customizedWidget); 138 | }); 139 | 140 | Take a moment to run this example code and login to see how our custom template has replaced just one screen in the widget. The first time I did this I thought it was pretty cool. Hopefully you can think of a lot of powerful stuff you could do with it. The sky is the limit. 141 | 142 | Here's another quick one: 143 | 144 | $.couch.app(function(app){ 145 | var customizedWidget = $.extend(true, {}, app.ddoc.vendor.couchapp.evently.account, { 146 | loggedOut : { 147 | after : "function(){alert('Bye bye');}" 148 | } 149 | }); 150 | $("#afterAlert").evently(customizedWidget); 151 | }); 152 | 153 | For a deeper reference on what the various parts of an Evently widget are named, and how you can use them, see [the Evently docs page](#/topic/evently). 154 | 155 | ## Linking two widgets 156 | 157 | First, lets create a basic widget. This one just has an `_init` handler and a handler called `loggedIn`. There is nothing in this widget definition that will trigger `loggedIn`, unless something else triggers it, there's no way it will run. 158 | 159 | $("#link_target").evently({ 160 | _init : { 161 | mustache : "

    Not much to see here

    " 162 | }, 163 | loggedIn : { 164 | mustache : "

    loggedIn was triggered from another widget, {{name}}.

    ", 165 | data : function(e, r) { 166 | return { name : r.userCtx.name }; 167 | } 168 | } 169 | }); 170 | 171 | Be sure to run the above example code before the next one, otherwise there won't be anything to link to. 172 | 173 | This next block of code demonstrates how to link two widgets together. First we create a normal account widget on the `#link_source` element, then we tell Evently to connect it to the `#link_target` element. Now whenever the `loggedIn` evenr is triggered on the source, it will be triggered on the target. 174 | 175 | $.couch.app(function(app){ 176 | $("#link_source").evently(app.ddoc.vendor.couchapp.evently.account); 177 | // link the source to the target, for the loggedIn event 178 | $.evently.connect($("#link_source"), $("#link_target"), ["loggedIn"]); 179 | }); 180 | 181 | ## Conclusion 182 | 183 | If you are writing a CouchApp that will have users logging and and logging out, you'd do well to use the account widget. It's customizable and linkable. And what's more, it's code that's already written. 184 | 185 | Enjoy! 186 | 187 | -------------------------------------------------------------------------------- /vendor/couchapp/docs/couchapp.md: -------------------------------------------------------------------------------- 1 | # Docs for $.couch.app 2 | 3 | The simplest use of CouchApp in the browser is to get access to information about the database you are running in. 4 | 5 | $.couch.app(function(app) { 6 | $("#dbinfo").evently({ 7 | _init : { 8 | mustache : '

    The db name is {{name}}

    ', 9 | data : app.db 10 | } 11 | }); 12 | }); 13 | 14 | Yay couchapp. 15 | 16 | The `$.couch.app()` function also loads the current design document so that it is available for templates etc. That is how the words you are reading were loaded. This file is included in the CouchApp application library. Let's look at the design doc: 17 | 18 | $.couch.app(function(app) { 19 | $("#ddoc").evently({ 20 | _init : { 21 | mustache : '

    Click to show the full doc source:

    {{ddoc}}
    ', 22 | data : { 23 | ddoc : JSON.stringify(app.ddoc, null, 2).slice(0,100) + '...' 24 | } 25 | }, 26 | click : { 27 | mustache : '

    The full doc source (rerun to hide):

    {{ddoc}}
    ', 28 | data : { 29 | ddoc : JSON.stringify(app.ddoc, null, 2) 30 | } 31 | } 32 | }); 33 | }); 34 | 35 | -------------------------------------------------------------------------------- /vendor/couchapp/docs/docs.md: -------------------------------------------------------------------------------- 1 | # Docs for the docs system. 2 | 3 | You are encouraged to use the couchapp docs system to write documentation for your plugins and applications. Extra bonus points because it's fun. 4 | 5 | Docs automatically make divs based on `$("#foo")` pattern matching. That is, we regex the code looking for the first id we see referenced. Remember ids need to be unique on a page. For doc examples you only get one id. 6 | 7 | Example Code: 8 | 9 | $("#hide_foo").hide("slow"); 10 | 11 | That's all it takes. You only get one div in each example for now. Have fun! -------------------------------------------------------------------------------- /vendor/couchapp/docs/evently.md: -------------------------------------------------------------------------------- 1 | # Evently Docs 2 | 3 | Evently is an declarative framework for evented jQuery applications. You write your code as widgets made up of templates and callbacks, while Evently handles the busywork of linking them together. 4 | 5 | Evently has special handlers for CouchDB views and `_changes` feeds, and could be easily extended for other server-side frameworks. 6 | 7 | ## Hello World 8 | 9 | At it's simplest an Evently widget is a set of events connected to a single DOM element. 10 | 11 | JavaScript: 12 | 13 | $("#hello").evently({ 14 | _init : { 15 | mustache : "

    Hello world

    ", 16 | }, 17 | click : { 18 | mustache : "

    What a crazy world!

    ", 19 | } 20 | }); 21 | 22 | You can also do some more interesting things: 23 | 24 | $("#heyjane").evently({ 25 | _init : { 26 | mustache : '

    Hello Jane, Joan (pick one)

    ', 27 | selectors : { 28 | 'a[href=#joan]' : { 29 | click : 'hiJoan' 30 | }, 31 | 'a[href=#jane]' : { 32 | click : 'hiJane' 33 | } 34 | } 35 | }, 36 | hiJoan : { 37 | mustache : '

    Hello Joan!

    ' 38 | }, 39 | hiJane : { 40 | mustache : "

    Darn, it's Jane...

    ", 41 | after : function() { 42 | setTimeout(function() { 43 | // automatically trigger the "janeRocks" event after 2 seconds. 44 | $("#heyjane").trigger("janeRocks"); 45 | }, 2000); 46 | } 47 | }, 48 | janeRocks : { 49 | render : "append", 50 | mustache : "

    Actually Jane is awesome.

    " 51 | } 52 | }); 53 | 54 | 55 | The imporant thing about this is that the widget is defined by an JavaScript object. This means we can save it as files on our hard drive and `couchapp` will handle saving it as a JSON object for us. 56 | 57 | [screenshot of the above code in textmate's file drawer] 58 | 59 | When we let CouchApp package our evently apps we get to work on them in individual files, instead of as a great big giant mess of JavaScript. This means HTML is HTML, JSON is JSON, and JavaScript is JavaScript. Yay! 60 | 61 | ## Ajax Hello World 62 | 63 | Let's do a little Ajax. We'll just load the version of the CouchDB instance we happen to be serving our HTML from: 64 | 65 | $("#ajax").evently({ 66 | _init : { 67 | mustache : '

    Loading CouchDB server info.

    ', 68 | after : function() { 69 | var widget = $(this); 70 | $.ajax({ 71 | url : '/', 72 | complete : function(req) { 73 | var resp = $.httpData(req, "json"); 74 | widget.trigger("version", [resp]); 75 | } 76 | }) 77 | } 78 | }, 79 | version : { 80 | mustache : "

    Running CouchDB version {{version}}

    ", 81 | data : function(e, resp) { 82 | return resp; 83 | } 84 | } 85 | }); 86 | 87 | Explain `mustache` and `data` 88 | 89 | -- triggering other events 90 | -- selectors 91 | -- create a doc 92 | 93 | ## Evently and CouchApp together 94 | 95 | Evently makes it easy to write decoupled JavaScript code, but as the examples above show, Evently widgets can turn into a lot of JSON to look at all on one screen. Because Evently code is declarative, and each handler and callback stands on its own (instead of being wrapped in a common closure), it can be broken out into individual files. 96 | 97 | CouchApp provides a mechanism for mapping between individual files and JSON structures. In this model a directory structure is mapped to a JSON object. So if you have a directory structure like: 98 | 99 | _init/ 100 | mustache.html 101 | selectors/ 102 | form/ 103 | submit.js 104 | input.name/ 105 | change.js 106 | a.cancel/ 107 | click.txt 108 | cancelled/ 109 | mustache.html 110 | selectors/ 111 | a.continue/ 112 | click.txt 113 | 114 | It will appear within your CouchApp design document as: 115 | 116 | { 117 | _init : { 118 | mustache : "contents of mustache.html", 119 | selectors { 120 | form : { 121 | submit : "function() { ... }" 122 | }, 123 | "input.name" { 124 | change : "function() { ... }" 125 | }, 126 | "a.cancel" { 127 | click : "cancelled" 128 | } 129 | } 130 | }, 131 | cancelled : { 132 | mustache : "contents of mustache.html", 133 | selectors : { 134 | "a.continue" : { 135 | click : "_init" 136 | } 137 | } 138 | } 139 | } 140 | 141 | This makes Evently and CouchApp a natural fit for each other. I swear I didn't plan this when I started writing Evently, it just turned out to be an awesome side effect of trying to stay as close to JSON as possible. 142 | 143 | In the [account widget tutorial](#/topic/account) we see the details of the account widget. What isn't discussed much there, is how the code is edited on your filesystem. 144 | 145 | If you are writing an Evently CouchApp widget you can edit the individual pieces on your filesystem. This has the added advantage of giving you native syntax highlighting for all the code. Instead of editing everything as JSON or JavaScript, the templates can be treated as HTML, the paths as text, etc. 146 | 147 | ## Evently Queries 148 | 149 | Evently understands CouchDB in a couple of very simple ways. If you know CouchDB, you're probably familiar with its Map Reduce views. Evently lets you specify view queries in a declarative way, and even takes care of the Ajax request. All you have to do is write code to handle the returned data. 150 | 151 | -- new rows, etc 152 | 153 | -- run a query 154 | 155 | -- connect to changes 156 | 157 | -- links to example apps 158 | 159 | ## Freeform Asynchronous Actions 160 | 161 | Watch out, you're dangerous! Evently allows you to make any old asyncronous action you want, with the `widget.async` member. The callback is the first argument to the `async` function. Check it out: 162 | 163 | $("#async").evently({ 164 | _init : { 165 | mustache : "

    How many databases on the local host?

    Answer: {{number_of_dbs}}

    Other stuff: {{args}}

    More: {{allArgs}}

    ", 166 | async : function(cb) { 167 | var ag = Array.prototype.slice.call(arguments).map(function(a){return a.toSource ? a.toSource() : a}); 168 | $.couch.allDbs({ 169 | success : function(resp) { 170 | cb(resp.length, ag); 171 | } 172 | }) 173 | }, 174 | data : function(count, args) { 175 | return { 176 | number_of_dbs : count, 177 | args : JSON.stringify(args), 178 | allArgs : JSON.stringify(Array.prototype.slice.call(arguments)) 179 | }; 180 | } 181 | }, 182 | click : { 183 | mustache : "

    What a crazy world!

    ", 184 | } 185 | }); -------------------------------------------------------------------------------- /vendor/couchapp/docs/pathbinder.md: -------------------------------------------------------------------------------- 1 | # Docs about $.pathbinder 2 | 3 | Pathbinder is a tiny framework for triggering events based on paths in URL hash. For example, you might want to render one panel when the user clicks a link to `#/foo` and another when the URL hash changes to `#/bar`. If you've never used URL hashes for application state in an Ajax app before, prepare to be happy. 4 | 5 | There are two big advantages to having the state in the URL-hash. One is that users can bookmark screens they may have reached by navigating within your app. The other is that the back button will continue to work. 6 | 7 | The page you are on has a URL hash of `#/topic/pathbinder` right now. You can follow links to other "pages" within this application, and Pathbinder takes care of triggering the proper events. 8 | 9 | ## A simple example 10 | 11 | $("#basic_path").html('

    click for foo

    '); 12 | $("#basic_path").bind("foo", function() { 13 | $(this).html("

    you went to foo

    "); 14 | }); 15 | $("#basic_path").pathbinder("foo", "/foo"); 16 | 17 | This code sets up the `#basic_path` div with some initial content, including a link to `#/foo`. If you click the link to foo, you'll see the URL change. It is the changed URL which Pathbinder sees and uses to trigger any running code. You can experiment by manually entering the `#/foo` URL hash, instead of clicking the link, and you'll see that it also triggers the `foo` event. 18 | 19 | ## Using path parameters 20 | 21 | Pathbinder was inspired by the path handling in [Sammy.js](http://github.com/aq/sammy.js). Like Sammy, you can use it to pull parameters from the URL-hash. This page can be linked [using a path that has "pathbinder" as a parameter](#/topic/pathbinder). Let's explore how you can pull parameters out of a path. 22 | 23 | $("#param_path").html('

    click for super foo

    '); 24 | $("#param_path").bind("foo", function(e, params) { 25 | $(this).html("

    you went to foo - "+params.id+"

    "); 26 | }); 27 | $("#param_path").pathbinder("foo", "/foo/:id"); 28 | 29 | When you click the link to super foo, you'll see the param is passed through the event. You can also edit the URL to see that "super" is not hard coded and can be replaced with other values. 30 | 31 | ## Pathbinder with Evently 32 | 33 | It should be no suprise that Pathbinder and Evently play well together. The gist of it is that Evently looks for a key called `path` and if it finds it, uses Pathbinder to connect that event handler to the path. Let's try it out: 34 | 35 | $("#evently_path").evently({ 36 | _init : { 37 | path : '/index', 38 | mustache : '

    the index. more cowbell!

    ' 39 | }, 40 | cowbell : { 41 | path : '/cowbell', 42 | mustache : '

    Now that is a lot of cowbell. back to the index

    ' 43 | } 44 | }); 45 | 46 | Note that when you use an Evently path, Evently also takes care to visit the path when the corresponding event is triggered. So running the above example code (which automatically triggers the `_init` event) will set the hash to `#/index`. If you were to trigger the `cowbell` event through non-path means, you'd see that it changes the path to `#/cowbell` anyway. 47 | 48 | ### Too many widgets 49 | 50 | One thing worth noting: there is only one URL hash for any given page, so be aware that if you have multiple widgets competing for the real-estate, they could conflict with each other. Pathbinder won't do anything when presented with a path it doesn't care about (go ahead, try out some non-sense ones on this page). 51 | 52 | This means that if you have a few widgets all using the path, the page should still behave in a useful way. However, this breaks down if you intend people to be able to use the URL hash to link to page state. Since there can be only one URL hash, whichever action they took last will be reflected in the bookmarked URL. For this reason it makes sense to limit yourself to one path-based Evently widget per page. 53 | -------------------------------------------------------------------------------- /vendor/couchapp/docs/profile.md: -------------------------------------------------------------------------------- 1 | # docs for the profile evently widget 2 | 3 | This widget makes it easy to give users a profile for your application. -------------------------------------------------------------------------------- /vendor/couchapp/evently/account/_init.js: -------------------------------------------------------------------------------- 1 | function() { 2 | var elem = $(this); 3 | $.couch.session({ 4 | success : function(r) { 5 | var userCtx = r.userCtx; 6 | if (userCtx.name) { 7 | elem.trigger("loggedIn", [r]); 8 | } else if (userCtx.roles.indexOf("_admin") != -1) { 9 | elem.trigger("adminParty"); 10 | } else { 11 | elem.trigger("loggedOut"); 12 | }; 13 | } 14 | }); 15 | } -------------------------------------------------------------------------------- /vendor/couchapp/evently/account/adminParty.js: -------------------------------------------------------------------------------- 1 | function() { 2 | alert("Admin party! Fix this in Futon before proceeding."); 3 | } -------------------------------------------------------------------------------- /vendor/couchapp/evently/account/doLogin.js: -------------------------------------------------------------------------------- 1 | function(e, name, pass) { 2 | var elem = $(this); 3 | $.couch.login({ 4 | name : name, 5 | password : pass, 6 | success : function(r) { 7 | elem.trigger("_init") 8 | } 9 | }); 10 | } -------------------------------------------------------------------------------- /vendor/couchapp/evently/account/doLogout.js: -------------------------------------------------------------------------------- 1 | function() { 2 | var elem = $(this); 3 | $.couch.logout({ 4 | success : function() { 5 | elem.trigger("_init"); 6 | } 7 | }); 8 | } -------------------------------------------------------------------------------- /vendor/couchapp/evently/account/doSignup.js: -------------------------------------------------------------------------------- 1 | function(e, name, pass) { 2 | var elem = $(this); 3 | $.couch.signup({ 4 | name : name 5 | }, pass, { 6 | success : function() { 7 | elem.trigger("doLogin", [name, pass]); 8 | } 9 | }); 10 | } -------------------------------------------------------------------------------- /vendor/couchapp/evently/account/loggedIn/after.js: -------------------------------------------------------------------------------- 1 | // todo move to template 2 | function(e, r) { 3 | $(this).attr("data-name", r.userCtx.name); 4 | } -------------------------------------------------------------------------------- /vendor/couchapp/evently/account/loggedIn/data.js: -------------------------------------------------------------------------------- 1 | function(e, r) { 2 | return { 3 | name : r.userCtx.name, 4 | uri_name : encodeURIComponent(r.userCtx.name), 5 | auth_db : encodeURIComponent(r.info.authentication_db) 6 | }; 7 | } -------------------------------------------------------------------------------- /vendor/couchapp/evently/account/loggedIn/mustache.html: -------------------------------------------------------------------------------- 1 | Welcome 2 | {{name}}! 3 | Logout? 4 | -------------------------------------------------------------------------------- /vendor/couchapp/evently/account/loggedIn/selectors.json: -------------------------------------------------------------------------------- 1 | { 2 | "a[href=#logout]" : {"click" : ["doLogout"]} 3 | } -------------------------------------------------------------------------------- /vendor/couchapp/evently/account/loggedOut/mustache.html: -------------------------------------------------------------------------------- 1 | Signup or Login -------------------------------------------------------------------------------- /vendor/couchapp/evently/account/loggedOut/selectors.json: -------------------------------------------------------------------------------- 1 | { 2 | "a[href=#signup]" : {"click" : ["signupForm"]}, 3 | "a[href=#login]" : {"click" : ["loginForm"]} 4 | } -------------------------------------------------------------------------------- /vendor/couchapp/evently/account/loginForm/after.js: -------------------------------------------------------------------------------- 1 | function() { 2 | $("input[name=name]", this).focus(); 3 | } -------------------------------------------------------------------------------- /vendor/couchapp/evently/account/loginForm/mustache.html: -------------------------------------------------------------------------------- 1 |
    2 | 3 | 4 | 5 | or Signup 6 |
    7 | -------------------------------------------------------------------------------- /vendor/couchapp/evently/account/loginForm/selectors/a[href=#signup].json: -------------------------------------------------------------------------------- 1 | {"click" : ["signupForm"]} -------------------------------------------------------------------------------- /vendor/couchapp/evently/account/loginForm/selectors/form/submit.js: -------------------------------------------------------------------------------- 1 | function(e) { 2 | var name = $('input[name=name]', this).val(), 3 | pass = $('input[name=password]', this).val(); 4 | $(this).trigger('doLogin', [name, pass]); 5 | return false; 6 | } -------------------------------------------------------------------------------- /vendor/couchapp/evently/account/signupForm/after.js: -------------------------------------------------------------------------------- 1 | function() { 2 | $("input[name=name]", this).focus(); 3 | } -------------------------------------------------------------------------------- /vendor/couchapp/evently/account/signupForm/mustache.html: -------------------------------------------------------------------------------- 1 |
    2 | 3 | 4 | 5 | or Login 6 |
    7 | -------------------------------------------------------------------------------- /vendor/couchapp/evently/account/signupForm/selectors/a[href=#login].json: -------------------------------------------------------------------------------- 1 | {"click" : ["loginForm"]} -------------------------------------------------------------------------------- /vendor/couchapp/evently/account/signupForm/selectors/form/submit.js: -------------------------------------------------------------------------------- 1 | function(e) { 2 | var name = $('input[name=name]', this).val(), 3 | pass = $('input[name=password]', this).val(); 4 | $(this).trigger('doSignup', [name, pass]); 5 | return false; 6 | } -------------------------------------------------------------------------------- /vendor/couchapp/evently/docs/index/data.js: -------------------------------------------------------------------------------- 1 | function() { 2 | var docs = $$(this).app.ddoc.vendor.couchapp.docs; 3 | var dnames = []; 4 | $.forIn(docs, function(d) { 5 | dnames.push({ 6 | title: d, 7 | href : "#/topic/"+encodeURIComponent(d) 8 | }); 9 | }); 10 | return {docs:dnames}; 11 | }; -------------------------------------------------------------------------------- /vendor/couchapp/evently/docs/index/mustache.html: -------------------------------------------------------------------------------- 1 |
      2 | {{#docs}} 3 |
    • {{title}}
    • 4 | {{/docs}} 5 |
    -------------------------------------------------------------------------------- /vendor/couchapp/evently/docs/index/path.txt: -------------------------------------------------------------------------------- 1 | / -------------------------------------------------------------------------------- /vendor/couchapp/evently/docs/topic/after.js: -------------------------------------------------------------------------------- 1 | function() { 2 | var app = $$(this).app; 3 | var self = $(this); 4 | $("pre", self).each(function() { 5 | var pre = $(this); 6 | var js = pre.text(); 7 | var r = js.match(/\$\(\"\#([^\"]*)\"\)/); 8 | if (r) { 9 | var id = r[1]; 10 | var code_id = 'code-'+id; 11 | pre.wrap('
    '); 12 | $('#'+code_id).evently(app.ddoc.vendor.couchapp.evently.docs.topic.edit, app, [id]); 13 | } 14 | }); 15 | }; 16 | -------------------------------------------------------------------------------- /vendor/couchapp/evently/docs/topic/data.js: -------------------------------------------------------------------------------- 1 | function(e, p) { 2 | var doc = $$(this).app.ddoc.vendor.couchapp.docs[p.id]; 3 | var converter = new Showdown.converter(); 4 | var html = converter.makeHtml(doc); 5 | return { 6 | html : html 7 | }; 8 | }; -------------------------------------------------------------------------------- /vendor/couchapp/evently/docs/topic/edit/_init/fun.js: -------------------------------------------------------------------------------- 1 | function(e, id) { 2 | var editable = $(this); 3 | if ($$(editable)._init_ran) {return false;} 4 | // add edit link 5 | var edit = $('edit code'); 6 | editable.append(edit); 7 | 8 | // add run box 9 | var example = $('
    run #'+id+'
    #'+id+' output will be here
    '); 10 | var s = $("#sidebar"); 11 | var o = s.offset(); 12 | example.offset({ 13 | left: o.left 14 | }); 15 | example.width(s.width()*0.75); 16 | editable.prepend(example); 17 | $$(editable)._init_ran = true; 18 | return false; 19 | } -------------------------------------------------------------------------------- /vendor/couchapp/evently/docs/topic/edit/_init/selectors/a.edit/click.js: -------------------------------------------------------------------------------- 1 | function() { 2 | var pre = $(this).prev('pre'); 3 | var js = pre.text(); 4 | var lines = js.split('\n').length; 5 | var ta = $(''); 6 | ta.text(js); 7 | pre.replace(ta); 8 | return false; 9 | }; 10 | -------------------------------------------------------------------------------- /vendor/couchapp/evently/docs/topic/edit/_init/selectors/a.run/click.js: -------------------------------------------------------------------------------- 1 | function(e) { 2 | try { 3 | function err(y, id) { 4 | $('#'+id).html(['

    Error running #', id, 5 | ' code block:

    ',
     6 |       (y.toSource ? y.toSource() : JSON.stringify(y)),
     7 |       '

    '].join('')); 8 | } 9 | var id = e.data.args[1]; 10 | var example = $("#code-"+id); 11 | var js = $('textarea',example).val() || $('pre',example).text(); 12 | $('#'+id).unbind(); 13 | try { 14 | eval(js); 15 | } catch (y) { 16 | err(y, id); 17 | } 18 | } catch(x) { 19 | err(x, id); 20 | } 21 | return false; 22 | } -------------------------------------------------------------------------------- /vendor/couchapp/evently/docs/topic/mustache.html: -------------------------------------------------------------------------------- 1 |
    {{{html}}}
    -------------------------------------------------------------------------------- /vendor/couchapp/evently/docs/topic/path.txt: -------------------------------------------------------------------------------- 1 | /topic/:id -------------------------------------------------------------------------------- /vendor/couchapp/evently/profile/loggedIn.js: -------------------------------------------------------------------------------- 1 | function(e, r) { 2 | var userCtx = r.userCtx; 3 | var widget = $(this); 4 | // load the profile from the user doc 5 | $.couch.userDb(function(db) { 6 | var userDocId = "org.couchdb.user:"+userCtx.name; 7 | db.openDoc(userDocId, { 8 | success : function(userDoc) { 9 | var profile = userDoc["couch.app.profile"]; 10 | if (profile) { 11 | // we copy the name to the profile so it can be used later 12 | // without publishing the entire userdoc (roles, pass, etc) 13 | profile.name = userDoc.name; 14 | $$(widget).profile = profile; 15 | widget.trigger("profileReady", [profile]); 16 | } else { 17 | widget.trigger("noProfile", [userCtx]); 18 | } 19 | } 20 | }); 21 | }); 22 | } 23 | -------------------------------------------------------------------------------- /vendor/couchapp/evently/profile/loggedOut/after.js: -------------------------------------------------------------------------------- 1 | function() { 2 | $$(this).profile = null; 3 | }; -------------------------------------------------------------------------------- /vendor/couchapp/evently/profile/loggedOut/mustache.html: -------------------------------------------------------------------------------- 1 |

    Please log in to see your profile.

    -------------------------------------------------------------------------------- /vendor/couchapp/evently/profile/noProfile/data.js: -------------------------------------------------------------------------------- 1 | function(e, userCtx) { 2 | return userCtx; 3 | } -------------------------------------------------------------------------------- /vendor/couchapp/evently/profile/noProfile/mustache.html: -------------------------------------------------------------------------------- 1 |
    2 |

    Hello {{name}}, Please setup your user profile.

    3 | 5 | 7 | 9 | 10 | 11 |
    -------------------------------------------------------------------------------- /vendor/couchapp/evently/profile/noProfile/selectors/form/submit.js: -------------------------------------------------------------------------------- 1 | function() { 2 | // TODO this can be cleaned up with docForm? 3 | 4 | var name = $("input[name=userCtxName]",this).val(); 5 | var newProfile = { 6 | rand : Math.random().toString(), 7 | nickname : $("input[name=nickname]",this).val(), 8 | email : $("input[name=email]",this).val(), 9 | url : $("input[name=url]",this).val() 10 | }, widget = $(this); 11 | 12 | // setup gravatar_url 13 | if (typeof hex_md5 == "undefined") { 14 | alert("creating a profile requires md5.js to be loaded in the page"); 15 | return; 16 | } 17 | 18 | newProfile.gravatar_url = 'http://www.gravatar.com/avatar/'+hex_md5(newProfile.email || newProfile.rand)+'.jpg?s=40&d=identicon'; 19 | 20 | // store the user profile on the user account document 21 | $.couch.userDb(function(db) { 22 | var userDocId = "org.couchdb.user:"+name; 23 | db.openDoc(userDocId, { 24 | success : function(userDoc) { 25 | userDoc["couch.app.profile"] = newProfile; 26 | db.saveDoc(userDoc, { 27 | success : function() { 28 | newProfile.name = userDoc.name; 29 | $$(widget).profile = newProfile; 30 | widget.trigger("profileReady", [newProfile]); 31 | } 32 | }); 33 | } 34 | }); 35 | }); 36 | return false; 37 | } -------------------------------------------------------------------------------- /vendor/couchapp/evently/profile/profileReady/after.js: -------------------------------------------------------------------------------- 1 | function(e, p) { 2 | $$(this).profile = p; 3 | }; -------------------------------------------------------------------------------- /vendor/couchapp/evently/profile/profileReady/data.js: -------------------------------------------------------------------------------- 1 | function(e, p) { 2 | return { 3 | nickname : p.nickname, 4 | name : p.name, 5 | avatar_url : p.gravatar_url 6 | }; 7 | } 8 | -------------------------------------------------------------------------------- /vendor/couchapp/evently/profile/profileReady/mustache.html: -------------------------------------------------------------------------------- 1 |
    2 | {{#avatar_url}}{{/avatar_url}} 3 |
    4 | {{nickname}} 5 |
    6 |
    7 |

    Hello {{nickname}}!

    8 |
    -------------------------------------------------------------------------------- /vendor/couchapp/lib/atom.js: -------------------------------------------------------------------------------- 1 | // atom feed generator 2 | // requries E4X support. 3 | 4 | function f(n) { // Format integers to have at least two digits. 5 | return n < 10 ? '0' + n : n; 6 | } 7 | 8 | function rfc3339(date) { 9 | return date.getUTCFullYear() + '-' + 10 | f(date.getUTCMonth() + 1) + '-' + 11 | f(date.getUTCDate()) + 'T' + 12 | f(date.getUTCHours()) + ':' + 13 | f(date.getUTCMinutes()) + ':' + 14 | f(date.getUTCSeconds()) + 'Z'; 15 | }; 16 | 17 | exports.header = function(data) { 18 | var f = ; 19 | f.title = data.title; 20 | f.id = data.feed_id; 21 | f.link.@href = data.feed_link; 22 | f.link.@rel = "self"; 23 | f.generator = "CouchApp on CouchDB"; 24 | f.updated = rfc3339(data.updated); 25 | return f.toXMLString().replace(/\<\/feed\>/,''); 26 | }; 27 | 28 | exports.entry = function(data) { 29 | var entry = ; 30 | entry.id = data.entry_id; 31 | entry.title = data.title; 32 | entry.content = data.content; 33 | entry.content.@type = (data.content_type || 'html'); 34 | entry.updated = rfc3339(data.updated); 35 | entry.author = {data.author}; 36 | entry.link.@href = data.alternate; 37 | entry.link.@rel = "alternate"; 38 | return entry; 39 | } 40 | -------------------------------------------------------------------------------- /vendor/couchapp/lib/docform.js: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not 2 | // use this file except in compliance with the License. You may obtain a copy 3 | // of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 9 | // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 10 | // License for the specific language governing permissions and limitations under 11 | // the License. 12 | 13 | // turn the form into deep json 14 | // field names like 'author-email' get turned into json like 15 | // {"author":{"email":"quentin@example.com"}} 16 | // acts on doc by reference, so you can safely pass non-form fields through 17 | function formToDeepJSON(form, fields, doc) { 18 | form = $(form); 19 | fields.forEach(function(field) { 20 | var val = form.find("[name="+field+"]").val(); 21 | if (!val) {return;} 22 | var parts = field.split('-'); 23 | var frontObj = doc, frontName = parts.shift(); 24 | while (parts.length > 0) { 25 | frontObj[frontName] = frontObj[frontName] || {}; 26 | frontObj = frontObj[frontName]; 27 | frontName = parts.shift(); 28 | } 29 | frontObj[frontName] = val; 30 | }); 31 | } 32 | 33 | function onSubmit(form, db, doc, opts) { 34 | formToDeepJSON(form, opts.fields, doc); 35 | if (opts.beforeSave) {opts.beforeSave(doc);} 36 | db.saveDoc(localFormDoc, { 37 | success : function(resp) { 38 | if (opts.success) {opts.success(resp, doc);} 39 | } 40 | }); 41 | }; 42 | 43 | function applyFields(form, doc) { 44 | 45 | }; 46 | exports.applyFields = applyFields; 47 | 48 | // docForm applies CouchDB behavior to HTML forms. 49 | // todo make this a couch.app plugin 50 | function docForm(formSelector, opts) { 51 | var localFormDoc = {}; 52 | opts = opts || {}; 53 | opts.fields = opts.fields || []; 54 | 55 | // Apply the behavior 56 | $(formSelector).submit(function(e) { 57 | 58 | 59 | return false; 60 | }); 61 | 62 | // populate form from an existing doc 63 | function docToForm(doc) { 64 | var form = $(formSelector); 65 | // fills in forms 66 | opts.fields.forEach(function(field) { 67 | var parts = field.split('-'); 68 | var run = true, frontObj = doc, frontName = parts.shift(); 69 | while (frontObj && parts.length > 0) { 70 | frontObj = frontObj[frontName]; 71 | frontName = parts.shift(); 72 | } 73 | if (frontObj && frontObj[frontName]) { 74 | form.find("[name="+field+"]").val(frontObj[frontName]); 75 | } 76 | }); 77 | } 78 | 79 | if (opts.id) { 80 | db.openDoc(opts.id, { 81 | success: function(doc) { 82 | if (opts.onLoad) {opts.onLoad(doc);} 83 | localFormDoc = doc; 84 | docToForm(doc); 85 | }}); 86 | } else if (opts.template) { 87 | if (opts.onLoad) {opts.onLoad(opts.template);} 88 | localFormDoc = opts.template; 89 | docToForm(localFormDoc); 90 | } 91 | var instance = { 92 | deleteDoc : function(opts) { 93 | opts = opts || {}; 94 | if (confirm("Really delete this document?")) { 95 | db.removeDoc(localFormDoc, opts); 96 | } 97 | }, 98 | localDoc : function() { 99 | formToDeepJSON(formSelector, opts.fields, localFormDoc); 100 | return localFormDoc; 101 | } 102 | }; 103 | return instance; 104 | } 105 | 106 | 107 | 108 | 109 | -------------------------------------------------------------------------------- /vendor/couchapp/lib/list.js: -------------------------------------------------------------------------------- 1 | // Helpers for writing server-side _list functions in CouchDB 2 | exports.withRows = function(fun) { 3 | var f = function() { 4 | var row = getRow(); 5 | return row && fun(row); 6 | }; 7 | f.iterator = true; 8 | return f; 9 | } 10 | 11 | exports.send = function(chunk) { 12 | send(chunk + "\n") 13 | } -------------------------------------------------------------------------------- /vendor/couchapp/lib/path.js: -------------------------------------------------------------------------------- 1 | // from couch.js 2 | function encodeOptions(options) { 3 | var buf = []; 4 | if (typeof(options) == "object" && options !== null) { 5 | for (var name in options) { 6 | if (!options.hasOwnProperty(name)) {continue;} 7 | var value = options[name]; 8 | if (name == "key" || name == "startkey" || name == "endkey") { 9 | value = JSON.stringify(value); 10 | } 11 | buf.push(encodeURIComponent(name) + "=" + encodeURIComponent(value)); 12 | } 13 | } 14 | if (!buf.length) { 15 | return ""; 16 | } 17 | return "?" + buf.join("&"); 18 | } 19 | 20 | function concatArgs(array, args) { 21 | for (var i=0; i < args.length; i++) { 22 | array.push(args[i]); 23 | }; 24 | return array; 25 | }; 26 | 27 | function makePath(array) { 28 | var options, path; 29 | 30 | if (typeof array[array.length - 1] != "string") { 31 | // it's a params hash 32 | options = array.pop(); 33 | } 34 | path = array.map(function(item) {return encodeURIComponent(item)}).join('/'); 35 | if (options) { 36 | return path + encodeOptions(options); 37 | } else { 38 | return path; 39 | } 40 | }; 41 | 42 | exports.init = function(req) { 43 | return { 44 | asset : function() { 45 | var p = req.path, parts = ['', p[0], p[1] , p[2]]; 46 | return makePath(concatArgs(parts, arguments)); 47 | }, 48 | show : function() { 49 | var p = req.path, parts = ['', p[0], p[1] , p[2], '_show']; 50 | return makePath(concatArgs(parts, arguments)); 51 | }, 52 | list : function() { 53 | var p = req.path, parts = ['', p[0], p[1] , p[2], '_list']; 54 | return makePath(concatArgs(parts, arguments)); 55 | }, 56 | update : function() { 57 | var p = req.path, parts = ['', p[0], p[1] , p[2], '_update']; 58 | return makePath(concatArgs(parts, arguments)); 59 | }, 60 | limit : function(limit) { 61 | var query = req.query; 62 | var l = query.limit; 63 | query.limit = limit; 64 | var view = req.path[req.path.length - 1]; 65 | var list = req.path[req.path.length - 2]; 66 | var link = this.list(list, view, query); 67 | query.limit = l; 68 | return link; 69 | }, 70 | older : function(key) { 71 | if (!typeof key == "undefined") return null; 72 | var query = req.query; 73 | query.startkey = key; 74 | query.skip=1; 75 | var view = req.path[req.path.length - 1]; 76 | var list = req.path[req.path.length - 2]; 77 | return this.list(list, view, query); 78 | }, 79 | absolute : function(path) { 80 | return 'http://' + req.headers.Host + path; 81 | } 82 | } 83 | }; 84 | -------------------------------------------------------------------------------- /vendor/couchapp/lib/redirect.js: -------------------------------------------------------------------------------- 1 | exports.permanent = function(redirect) { 2 | return { 3 | code : 301, 4 | headers : { 5 | "Location" : redirect 6 | } 7 | }; 8 | }; -------------------------------------------------------------------------------- /vendor/couchapp/metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "couchapp", 3 | "description": "official couchapp vendor" 4 | } -------------------------------------------------------------------------------- /vendor/mustache/_attachments/jquery.mustache.js: -------------------------------------------------------------------------------- 1 | /* 2 | Shameless port of a shameless port 3 | @defunkt => @janl => @aq 4 | 5 | See http://github.com/defunkt/mustache for more info. 6 | */ 7 | 8 | ;(function($) { 9 | 10 | /* 11 | Shamless port of http://github.com/defunkt/mustache 12 | by Jan Lehnardt , Alexander Lang , 13 | Sebastian Cohnen 14 | 15 | Thanks @defunkt for the awesome code. 16 | 17 | See http://github.com/defunkt/mustache for more info. 18 | */ 19 | 20 | var Mustache = function() { 21 | var Renderer = function() {}; 22 | 23 | Renderer.prototype = { 24 | otag: "{{", 25 | ctag: "}}", 26 | pragmas: {}, 27 | 28 | render: function(template, context, partials) { 29 | // fail fast 30 | if(template.indexOf(this.otag) == -1) { 31 | return template; 32 | } 33 | 34 | template = this.render_pragmas(template); 35 | var html = this.render_section(template, context, partials); 36 | return this.render_tags(html, context, partials); 37 | }, 38 | 39 | /* 40 | Looks for %PRAGMAS 41 | */ 42 | render_pragmas: function(template) { 43 | // no pragmas 44 | if(template.indexOf(this.otag + "%") == -1) { 45 | return template; 46 | } 47 | 48 | var that = this; 49 | var regex = new RegExp(this.otag + "%(.+)" + this.ctag); 50 | return template.replace(regex, function(match, pragma) { 51 | that.pragmas[pragma] = true; 52 | return ""; 53 | // ignore unknown pragmas silently 54 | }); 55 | }, 56 | 57 | /* 58 | Tries to find a partial in the global scope and render it 59 | */ 60 | render_partial: function(name, context, partials) { 61 | if(typeof(context[name]) != "object") { 62 | throw({message: "subcontext for '" + name + "' is not an object"}); 63 | } 64 | if(!partials || !partials[name]) { 65 | throw({message: "unknown_partial"}); 66 | } 67 | return this.render(partials[name], context[name], partials); 68 | }, 69 | 70 | /* 71 | Renders boolean and enumerable sections 72 | */ 73 | render_section: function(template, context, partials) { 74 | if(template.indexOf(this.otag + "#") == -1) { 75 | return template; 76 | } 77 | var that = this; 78 | // CSW - Added "+?" so it finds the tighest bound, not the widest 79 | var regex = new RegExp(this.otag + "\\#(.+)" + this.ctag + 80 | "\\s*([\\s\\S]+?)" + this.otag + "\\/\\1" + this.ctag + "\\s*", "mg"); 81 | 82 | // for each {{#foo}}{{/foo}} section do... 83 | return template.replace(regex, function(match, name, content) { 84 | var value = that.find(name, context); 85 | if(that.is_array(value)) { // Enumerable, Let's loop! 86 | return that.map(value, function(row) { 87 | return that.render(content, that.merge(context, 88 | that.create_context(row)), partials); 89 | }).join(''); 90 | } else if(value) { // boolean section 91 | return that.render(content, context, partials); 92 | } else { 93 | return ""; 94 | } 95 | }); 96 | }, 97 | 98 | /* 99 | Replace {{foo}} and friends with values from our view 100 | */ 101 | render_tags: function(template, context, partials) { 102 | var lines = template.split("\n"); 103 | 104 | var new_regex = function() { 105 | return new RegExp(that.otag + "(=|!|>|\\{|%)?([^\/#]+?)\\1?" + 106 | that.ctag + "+", "g"); 107 | }; 108 | 109 | // tit for tat 110 | var that = this; 111 | 112 | var regex = new_regex(); 113 | for (var i=0; i < lines.length; i++) { 114 | lines[i] = lines[i].replace(regex, function (match,operator,name) { 115 | switch(operator) { 116 | case "!": // ignore comments 117 | return match; 118 | case "=": // set new delimiters, rebuild the replace regexp 119 | that.set_delimiters(name); 120 | regex = new_regex(); 121 | // redo the line in order to get tags with the new delimiters 122 | // on the same line 123 | i--; 124 | return ""; 125 | case ">": // render partial 126 | return that.render_partial(name, context, partials); 127 | case "{": // the triple mustache is unescaped 128 | return that.find(name, context); 129 | return ""; 130 | default: // escape the value 131 | return that.escape(that.find(name, context)); 132 | } 133 | },this); 134 | }; 135 | return lines.join("\n"); 136 | }, 137 | 138 | set_delimiters: function(delimiters) { 139 | var dels = delimiters.split(" "); 140 | this.otag = this.escape_regex(dels[0]); 141 | this.ctag = this.escape_regex(dels[1]); 142 | }, 143 | 144 | escape_regex: function(text) { 145 | // thank you Simon Willison 146 | if(!arguments.callee.sRE) { 147 | var specials = [ 148 | '/', '.', '*', '+', '?', '|', 149 | '(', ')', '[', ']', '{', '}', '\\' 150 | ]; 151 | arguments.callee.sRE = new RegExp( 152 | '(\\' + specials.join('|\\') + ')', 'g' 153 | ); 154 | } 155 | return text.replace(arguments.callee.sRE, '\\$1'); 156 | }, 157 | 158 | /* 159 | find `name` in current `context`. That is find me a value 160 | from the view object 161 | */ 162 | find: function(name, context) { 163 | name = this.trim(name); 164 | if(typeof context[name] === "function") { 165 | return context[name].apply(context); 166 | } 167 | if(context[name] !== undefined) { 168 | return context[name]; 169 | } 170 | // silently ignore unkown variables 171 | return ""; 172 | }, 173 | 174 | // Utility methods 175 | 176 | /* 177 | Does away with nasty characters 178 | */ 179 | escape: function(s) { 180 | return s.toString().replace(/[&"<>\\]/g, function(s) { 181 | switch(s) { 182 | case "&": return "&"; 183 | case "\\": return "\\\\";; 184 | case '"': return '\"';; 185 | case "<": return "<"; 186 | case ">": return ">"; 187 | default: return s; 188 | } 189 | }); 190 | }, 191 | 192 | /* 193 | Merges all properties of object `b` into object `a`. 194 | `b.property` overwrites a.property` 195 | */ 196 | merge: function(a, b) { 197 | var _new = {}; 198 | for(var name in a) { 199 | if(a.hasOwnProperty(name)) { 200 | _new[name] = a[name]; 201 | } 202 | }; 203 | for(var name in b) { 204 | if(b.hasOwnProperty(name)) { 205 | _new[name] = b[name]; 206 | } 207 | }; 208 | return _new; 209 | }, 210 | 211 | // by @langalex, support for arrays of strings 212 | create_context: function(_context) { 213 | if(this.is_object(_context)) { 214 | return _context; 215 | } else if(this.pragmas["JSTACHE-ENABLE-STRING-ARRAYS"]) { 216 | return {'.': _context}; 217 | } 218 | }, 219 | 220 | is_object: function(a) { 221 | return a && typeof a == 'object' 222 | }, 223 | 224 | /* 225 | Thanks Doug Crockford 226 | JavaScript — The Good Parts lists an alternative that works better with 227 | frames. Frames can suck it, we use the simple version. 228 | */ 229 | is_array: function(a) { 230 | return (a && 231 | typeof a === 'object' && 232 | a.constructor === Array); 233 | }, 234 | 235 | /* 236 | Gets rid of leading and trailing whitespace 237 | */ 238 | trim: function(s) { 239 | return s.replace(/^\s*|\s*$/g, ''); 240 | }, 241 | 242 | /* 243 | Why, why, why? Because IE. Cry, cry cry. 244 | */ 245 | map: function(array, fn) { 246 | if (typeof array.map == "function") { 247 | return array.map(fn) 248 | } else { 249 | var r = []; 250 | var l = array.length; 251 | for(i=0;i