├── Makefile ├── README.md ├── bin └── thinglerd.sh ├── etc └── thingler.cron ├── pub ├── css │ └── thingler.css ├── error.html ├── favicon.gif ├── images │ └── lock-24.png ├── index.html ├── js │ ├── domdom-tokenizing.js │ ├── domdom.js │ ├── index.json │ ├── pilgrim.js │ └── thingler.js ├── less │ └── thingler.less └── upgrade.html └── src ├── changes.js ├── db.js ├── index.js ├── md5.js ├── routes.js ├── session ├── index.js └── session.js ├── todo ├── index.js └── todo.js └── uuid.js /Makefile: -------------------------------------------------------------------------------- 1 | css: 2 | lessc pub/less/thingler.less > pub/css/thingler.css -x 3 | 4 | crontab: 5 | sudo crontab etc/thingler.cron -u couchdb 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | thingler 2 | ======== 3 | 4 | > Hello, I am [Thingler](http://thingler.com), and this is my source code. Feel free to browse. 5 | 6 | License 7 | ------- 8 | 9 | Thingler is licensed under the following license: 10 | -------------------------------------------------------------------------------- /bin/thinglerd.sh: -------------------------------------------------------------------------------- 1 | #! /bin/sh 2 | 3 | PID_FILE="/tmp/thinglerd.pid" 4 | SERVER_PATH="$PWD/src/index.js" 5 | PORT=80 6 | 7 | if [ "$1" = "start" ]; then 8 | if [ -e $PID_FILE ]; then 9 | echo "~ thinglerd is already running." 10 | else 11 | echo "~ starting thinglerd." 12 | nohup node $SERVER_PATH $PORT > /dev/null 2>&1 & 13 | fi 14 | elif [ "$1" = "stop" ]; then 15 | if [ ! -e $PID_FILE ]; then 16 | echo "~ thinglerd is not running." 17 | else 18 | echo "~ stopping thinglerd." 19 | kill `cat $PID_FILE` 20 | rm -f $PID_FILE 21 | fi 22 | elif [ "$1" = "status" ]; then 23 | if [ -e $PID_FILE ]; then 24 | echo "~ thinglerd is running as process "`cat $PID_FILE` 25 | else 26 | echo "~ thinglerd is not running." 27 | fi 28 | fi 29 | -------------------------------------------------------------------------------- /etc/thingler.cron: -------------------------------------------------------------------------------- 1 | # 2 | # crontab for couchdb user 3 | # 4 | # install with `sudo crontab thingler.cron -u couchdb` 5 | # 6 | 7 | # Hourly & Daily backups of the thingler database 8 | 00 * * * * cp /var/lib/couchdb/thingler.couch /usr/local/var/couchdb/thingler-backup.hourly.`date +%H`.couch 9 | 00 7 * * * cp /var/lib/couchdb/thingler.couch /usr/local/var/couchdb/thingler-backup.daily.`date +%a`.couch 10 | 11 | # Compact database every day at 6AM 12 | 00 */3 * * * curl -H "Content-Type: application/json" -X POST http://0.0.0.0:5984/thingler/_compact 13 | -------------------------------------------------------------------------------- /pub/css/thingler.css: -------------------------------------------------------------------------------- 1 | *{margin:0;padding:0;} 2 | body{padding:60px 30px;font-family:'Arial',sans-serif;font-size:24px;width:800px;margin:0 auto;} 3 | body>div{display:none;} 4 | a{text-decoration:none;color:#cccccc;} 5 | #not-found{padding:60px 0;}#not-found h1{text-align:center;font-size:48px;} 6 | #not-found p{text-align:center;margin-top:30px;} 7 | #not-found a{border-bottom:1px solid #eeeeee;font-family:Arial,sans-serif;color:#aaaaaa;}#not-found a:hover{color:#cc7766;border-bottom:1px solid #f9eeec;} 8 | #about{margin-top:-30px;font-size:14px;float:left;} 9 | h1{color:#3d4a5c;font-size:42px;} 10 | ul,li{list-style-type:none;padding:0;} 11 | body>header{position:absolute;width:800px;top:15px;color:#dddddd;display:block;padding:15px 0;margin:0;font-size:16px;} 12 | body>footer{visibility:hidden;margin:30px 0;text-align:right;font-size:14px;color:#eaeaea;}body>footer p{margin:10px 0;} 13 | body>footer p:first-child{color:#dddddd;font-size:18px;}body>footer p:first-child:hover,body>footer p:first-child:hover a{color:#c9c9c9;}body>footer p:first-child:hover a:hover,body>footer p:first-child:hover a a:hover{color:#cc7766;border-bottom:1px solid #f9eeec;} 14 | body>footer a{color:#dddddd;border-bottom:1px solid #eee;}body>footer a:hover{color:#cc7766;border-bottom:1px solid #f9eeec;} 15 | ul{-webkit-padding-start:0;} 16 | #page{width:800px;}#page #title{border:0;font-size:48px;font-weight:bold;border:1px dashed transparent;color:#3d4a5c;font-family:'Arial',sans-serif;margin-bottom:15px;margin-left:-1px;padding:0.25em 1px;width:796px;}#page #title:focus{outline:none;border-color:#dddddd;} 17 | #page a#show-everything{font-size:14px;margin:0;padding:0;float:right;text-align:right;margin-top:15px;margin-right:95px;border-bottom:1px solid #eeeeee;font-family:Arial,sans-serif;color:#cccccc;}#page a#show-everything:hover{color:#cc7766;border-bottom:1px solid #f9eeec;} 18 | #page ul#list.unselectable li{cursor:default;user-select:none;-moz-user-select:none;-webkit-user-select:none;} 19 | #page label,#page label a,#page label a:visited{color:#3d4a5c;text-decoration:none;display:inline-block;line-height:48px;position:relative;} 20 | #page label{max-width:670px;} 21 | #page li.flashing label:after{visibility:hidden;} 22 | #page ul#list{margin:30px 0;width:800px;padding:0;} 23 | #page #list>li{overflow:hidden;font-size:22px;width:790px;font-family:Georgia,'Times New Roman',serif;padding:0 5px;cursor:move;background-color:white;height:auto;border-bottom:1px dotted #dddddd;}#page #list>li:hover{background-color:#fefef1;}#page #list>li:hover .actions{visibility:visible;} 24 | #page #list>li:hover label:after{background-color:#fefef1;-webkit-box-shadow:-15px 0px 30px #fefef1;-moz-box-shadow:-15px 0px 30px #fefef1;box-shadow:-15px 0px 30px #fefef1;} 25 | #page #list>li input{vertical-align:middle;height:48px;line-height:48px;float:left;display:block;margin-right:25px;} 26 | #page #list>li:hover .tags li a{color:#bcbc9a;background-color:white;} 27 | #page #list>li .tags{float:right;display:inline;line-height:48px;height:48px;font-size:16px;margin:0 15px;}#page #list>li .tags li{display:inline-block;margin-left:5px;}#page #list>li .tags li a{padding:0;margin:0;line-height:1em;margin:auto 0;padding:4px 8px;border-width:1px;border-style:solid;border-color:#f6f6df;border-bottom-color:#fcfcf3;background-color:#fefef1;display:inline-block;color:#c6c6a9;-webkit-box-shadow:0 1px 2px #eeeeee;-moz-box-shadow:0 1px 2px #eeeeee;box-shadow:0 1px 2px #eeeeee;-webkit-border-radius:2px;-moz-border-radius:2px;border-radius:2px;}#page #list>li .tags li a:hover{color:#b1b18b;border-color:#ededc0;} 28 | #page #list>li .tags li.active a{border-color:#ededc0;color:#b1b18b;} 29 | #page #list>li .tags li:first-child{margin-left:0;} 30 | #page #list>li .actions{visibility:hidden;float:right;font-size:14px;height:48px;line-height:48px;}#page #list>li .actions a{border-bottom:1px solid #f4f4f4;font-family:Arial,sans-serif;margin-right:15px;text-decoration:none;color:#cccccc;}#page #list>li .actions a:hover{color:#cc7766;border-bottom:1px solid #f9eeec;} 31 | #page #list>li:last-child{border-bottom:0;} 32 | #page .completed label,#page .completed label a{color:#ccc;text-decoration:line-through;} 33 | input#new{width:782px;-webkit-box-shadow:0px 1px 2px #dddddd;-moz-box-shadow:0px 1px 2px #dddddd;box-shadow:0px 1px 2px #dddddd;}input#new:focus{-webkit-box-shadow:0px 2px 8px #dddddd;-moz-box-shadow:0px 2px 8px #dddddd;box-shadow:0px 2px 8px #dddddd;} 34 | input#new,input[type="password"]{font-size:24px;padding:8px;outline:none;border:1px solid #dddddd;} 35 | input[type="checkbox"]{font-size:22px;} 36 | a[data-action="remove"]{padding:8px;border:0 !important;}a[data-action="remove"]:hover{border:0 !important;} 37 | #about{display:none;margin-top:0px;text-align:right;}#about p{font-size:14px !important;color:#aaaaaa;line-height:22px;}#about p a{border-bottom:1px dashed #dddddd;font-family:Arial,sans-serif;color:#bebebe;}#about p a:hover{color:#cc7766;border-bottom:1px solid #f9eeec;} 38 | .dragging{position:absolute;z-index:10;background-color:rgba(255, 255, 229, 0.7) !important;width:800px;border:1px dashed #dddddd !important;-webkit-box-shadow:0px 2px 16px #dddddd;-moz-box-shadow:0px 2px 16px #dddddd;box-shadow:0px 2px 16px #dddddd;}.dragging div{cursor:move;} 39 | .dragging .actions{visibility:hidden;} 40 | .ghost{margin-top:-1px;border-bottom:1px dashed #dddddd !important;border-top:1px dashed #dddddd !important;}.ghost label{color:#dddddd !important;} 41 | .ghost .tags{color:#dddddd !important;}.ghost .tags li{background-color:#fefefe !important;border-color:#eeeeee !important;} 42 | div.password{position:absolute;top:90px;left:150px;width:600px;padding:60px;background-color:white;-webkit-box-shadow:0 0px 30px #dddddd;-moz-box-shadow:0 0px 30px #dddddd;box-shadow:0 0px 30px #dddddd;-webkit-border-radius:8px;-moz-border-radius:8px;border-radius:8px;}div.password label{font-weight:bold;color:#52637a;font-size:32px;display:block;padding-bottom:30px;} 43 | div.password input{width:570px;-webkit-border-radius:8px;-moz-border-radius:8px;border-radius:8px;} 44 | .overlay{position:fixed;top:0;left:0;width:100%;height:100%;background-color:rgba(255, 255, 255, 0.8);} 45 | -------------------------------------------------------------------------------- /pub/error.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Thingler 5 | 11 | 12 | 13 |

Hmm… something weird just happened.

14 |

We've been notified, but I suggest you don't try that again, until it's fixed.

15 | 16 | 17 | -------------------------------------------------------------------------------- /pub/favicon.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudhead/thingler/321286cb9d988a77f27753ed4d83246a23d575e0/pub/favicon.gif -------------------------------------------------------------------------------- /pub/images/lock-24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudhead/thingler/321286cb9d988a77f27753ed4d83246a23d575e0/pub/images/lock-24.png -------------------------------------------------------------------------------- /pub/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Thingler 5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | unlock this room 14 | lock this room 15 |
16 |
17 |
18 | 19 | 27 | 28 | 32 | 33 | 41 | 42 | 48 | 49 | 67 | 68 | 81 | 82 | 94 | 95 | 96 | -------------------------------------------------------------------------------- /pub/js/domdom-tokenizing.js: -------------------------------------------------------------------------------- 1 | // 2 | // ~ domdom - tokenizing input behaviour ~ 3 | // 4 | dom.tokenizing = function (element, container, pattern) { 5 | var tokens = container.querySelector('.tokens'); 6 | 7 | element.tokenizer = { 8 | pattern: pattern, 9 | element: element, 10 | container: container, 11 | tokens: tokens 12 | }; 13 | 14 | // 15 | // When the user clicks in the input container, where there 16 | // is no input field, it should focus the right-most input, 17 | // to make it seem like the container is the input. 18 | // 19 | container.on('mousedown', function (e) { 20 | if (e.target.nodeName === 'INPUT') { return } 21 | else if (! e.target.isDescendantOf(container)) { return } 22 | 23 | container.emit('focus'); 24 | 25 | if (tokens.children.length > 0) { tokens.lastChild.lastChild.focus() } 26 | else { element.focus(), element.setCursor(element.value.length) } 27 | e.preventDefault(); 28 | }); 29 | 30 | dom.autosizing(element); 31 | // 32 | // Parse main input for tokens 33 | // 34 | element.on('keydown', function (e) { 35 | if (e.keyCode === KEY.RETURN) { 36 | this.emit('new', { tokens: dom.tokenizing.parseTokens(tokens) }); 37 | } else if (e.keyCode === KEY.RIGHT) { 38 | if (e.target.selectionStart === this.value.length) { 39 | tokens.firstChild && tokens.firstChild.lastChild.focus(); 40 | } 41 | } 42 | }).on('keypress', function (e) { 43 | if (e.charCode) { 44 | var value = this.value + String.fromCharCode(e.charCode), match; 45 | if (match = value.match(pattern)) { 46 | this.value = value.replace(match[0], '').trim(); 47 | this.autosize(); 48 | dom.tokenizing.createToken.call(element.tokenizer, match[0].trim()); 49 | e.preventDefault(); 50 | } 51 | } 52 | }); 53 | 54 | document.on('mousedown', function (e) { 55 | if (! e.target.isDescendantOf(container)) { 56 | container.emit('blur'); 57 | element.emit('blurall', { tokens: dom.tokenizing.parseTokens(tokens) }); 58 | } 59 | }); 60 | 61 | return element; 62 | }; 63 | dom.tokenizing.removeToken = function (elem) { 64 | if (this.tokens.firstChild === elem.parentNode) { 65 | this.element.focus(); 66 | } else { 67 | elem.parentNode.previousElementSibling.lastChild.focus(); 68 | } 69 | this.tokens.removeChild(elem.parentNode); 70 | }; 71 | dom.tokenizing.parseTokens = function (tokens) { 72 | return tokens.children.map(function (li) { return li.firstChild.value }) 73 | .filter(function (t) { return !!t }); 74 | }; 75 | dom.tokenizing.createTokens = function (tokens) { 76 | var that = this; 77 | this.tokens.innerHTML = ''; 78 | tokens.forEach(function (t) { 79 | dom.tokenizing.createToken.call(that, t); 80 | }); 81 | }; 82 | dom.tokenizing.createToken = function (str, ref) { 83 | var li = dom.createElement('li'), 84 | token = dom.createElement('input', { className: 'token' }), 85 | that = this; 86 | 87 | li.appendChild(token); 88 | 89 | if (ref) { this.tokens.insertBefore(li, ref) } 90 | else { this.tokens.appendChild(li) } 91 | 92 | token.value = str; 93 | token.focus(); 94 | 95 | var input = dom.tokenizing.createInput.call(this, li); 96 | 97 | token.on('keydown', function (e) { 98 | var value = this.value[0] === '#' ? this.value : '#' + this.value; 99 | if ((e.keyCode === KEY.RETURN || 100 | e.keyCode === KEY.SPACE || 101 | e.keyCode === KEY.TAB) && this.value) { 102 | input.focus(); 103 | e.preventDefault(); 104 | } else if (e.keyCode === KEY.BACKSPACE) { 105 | if (! this.value) { 106 | dom.tokenizing.removeToken.call(that, token); 107 | e.preventDefault(); 108 | } 109 | } 110 | }); 111 | dom.autosizing(token); 112 | return token; 113 | }; 114 | dom.tokenizing.createInput = function (li) { 115 | var input = dom.createElement('input', { className: 'token-input' }), 116 | that = this; 117 | 118 | li.appendChild(input); 119 | 120 | dom.autosizing(input); 121 | input.on('keydown', function (e) { 122 | var length = that.element.value.length; 123 | if (e.keyCode === KEY.LEFT) { 124 | if (input.parentNode === that.tokens.firstChild) { 125 | that.element.focus(); 126 | that.element.setCursor(length) 127 | that.element.autosize(); 128 | e.preventDefault(); 129 | } else { 130 | input.parentNode.previousSibling.lastChild.focus(); 131 | } 132 | } else if (e.keyCode === KEY.RIGHT && input.parentNode.nextSibling) { 133 | input.parentNode.nextSibling.lastChild.focus(); 134 | } else if (e.keyCode === KEY.BACKSPACE) { 135 | if (! input.value) { 136 | dom.tokenizing.removeToken.call(that, input); 137 | e.preventDefault(); 138 | } 139 | } else if (e.keyCode === KEY.ESC) { 140 | that.element.emit('blurall', { tokens: dom.tokenizing.parseTokens(that.tokens) }); 141 | } else if (e.keyCode === KEY.RETURN && !input.value) { 142 | that.element.emit('new', { tokens: dom.tokenizing.parseTokens(that.tokens) }); 143 | that.element.style.width = ''; 144 | } 145 | }).on('keypress', function (e) { 146 | if (e.charCode) { 147 | var value = input.value + String.fromCharCode(e.charCode); 148 | var match = value.match(that.pattern); 149 | 150 | if (e.keyCode === KEY.SPACE) { e.preventDefault() } 151 | else if (match) { 152 | input.value = ''; 153 | input.autosize(); 154 | dom.tokenizing.createToken.call(that, value, input.parentNode.nextSibling); 155 | e.preventDefault(); 156 | } 157 | } 158 | }); 159 | return input; 160 | }; 161 | -------------------------------------------------------------------------------- /pub/js/domdom.js: -------------------------------------------------------------------------------- 1 | window.dom = {}; 2 | 3 | dom.dragging = { 4 | element: null, 5 | offset: null, 6 | index: null, 7 | target: null 8 | }; 9 | 10 | dom.sorting = { 11 | element: null, 12 | count: null, 13 | positions: [] 14 | }; 15 | 16 | var KEY = { 17 | BACKSPACE: 8, 18 | TAB: 9, 19 | RETURN: 13, 20 | ESC: 27, 21 | SPACE: 32, 22 | LEFT: 37, 23 | UP: 38, 24 | RIGHT: 39, 25 | DOWN: 40, 26 | }; 27 | 28 | dom.sortable = function (list, callback) { 29 | dom.sorting.element = list; 30 | dom.sorting.callback = callback; 31 | dom.sorting.count = list.childElementCount; 32 | 33 | list.children.forEach(dom.draggable); 34 | }; 35 | 36 | document.onmouseup = function () { 37 | var list = dom.sorting.element; 38 | 39 | if (dom.dragging.element) { 40 | list.removeChild(dom.dragging.element); 41 | dom.dragging.target.removeClass('ghost'); 42 | dom.sorting.element.removeClass('unselectable'); 43 | 44 | dom.sorting.callback(dom.dragging.element.firstChild.getAttribute('data-id'), dom.dragging.index); 45 | 46 | dom.dragging.element = null; 47 | dom.dragging.offset = null; 48 | 49 | list.children.forEach(dom.enableSelection); 50 | } 51 | }; 52 | document.onmousemove = function (e) { 53 | var offset, element, list, position, prev, next; 54 | 55 | dom.mouse = { x: e.pageX, y: e.pageY }; 56 | 57 | if (dom.dragging.element) { 58 | dom.dragging.element.style.top = (dom.mouse.y - dom.dragging.offset.y) + 'px'; 59 | 60 | list = dom.sorting.element; 61 | 62 | if (dom.dragging.index > 0) { 63 | prev = list.children[dom.dragging.index - 1]; 64 | 65 | if (dom.mouse.y < dom.getPosition(prev).y + dom.dragging.element.clientHeight) { 66 | dom.dragging.index --; 67 | list.insertBefore(dom.dragging.target, prev); 68 | return dom.refreshPositions(list); 69 | } 70 | } 71 | 72 | if (dom.dragging.index < dom.sorting.count) { 73 | next = list.children[dom.dragging.index + 1]; 74 | 75 | if (dom.mouse.y > dom.getPosition(next).y) { 76 | dom.dragging.index ++; 77 | list.insertBefore(dom.dragging.target, next.nextSibling); 78 | return dom.refreshPositions(list); 79 | } 80 | } 81 | return false; 82 | } 83 | }; 84 | 85 | dom.refreshPositions = function (elem) { 86 | dom.sorting.positions = elem.children.map(dom.getPosition); 87 | return false; 88 | }; 89 | 90 | dom.draggable = function (elem) { 91 | elem.onmousedown = function (e) { 92 | var source = e.srcElement || e.target; 93 | 94 | if (source.nodeName === 'LI' || source.nodeName === 'DIV') { 95 | while (source !== elem) { 96 | if (source === document.body) { return true } 97 | else { source = source.parentNode } 98 | } 99 | } else { return true } 100 | 101 | if (this.hasClass('editing')) { return true } 102 | 103 | var pos = dom.getPosition(this); 104 | var clone = this.cloneNode(true); 105 | 106 | this.parentNode.appendChild(clone); 107 | 108 | this.addClass('ghost'); 109 | clone.addClass('dragging'); 110 | dom.sorting.element.addClass('unselectable'); 111 | 112 | dom.dragging.index = list.children.indexOf(this); 113 | dom.dragging.offset = { x: e.pageX - pos.x, y: e.pageY - pos.y }; 114 | dom.dragging.element = clone; 115 | dom.dragging.target = this; 116 | 117 | // Disable text selection while dragging 118 | dom.sorting.element.children.forEach(dom.disableSelection); 119 | document.onmousemove(e); 120 | }; 121 | }; 122 | 123 | dom.removeClass = function (e, name) { 124 | if (! e.className) { return } 125 | 126 | var classes = e.className.split(' '), 127 | index = classes.indexOf(name); 128 | 129 | if (index !== -1) { 130 | classes.splice(index, 1); 131 | e.setAttribute('class', classes.join(' ')); 132 | } 133 | }; 134 | dom.addClass = function (e, name) { 135 | var classes = e.className ? e.className.split(' ') : []; 136 | 137 | if (classes.indexOf(name) === -1) { 138 | e.setAttribute('class', classes.concat(name).join(' ').trim()); 139 | } 140 | }; 141 | dom.hasClass = function (e, name) { 142 | return new(RegExp)('\\b' + name + '\\b').test(e.className); 143 | }; 144 | dom.getPosition = function (e) { 145 | var left = 0; 146 | var top = 0; 147 | 148 | while (e.offsetParent) { 149 | left += e.offsetLeft; 150 | top += e.offsetTop; 151 | e = e.offsetParent; 152 | } 153 | 154 | left += e.offsetLeft; 155 | top += e.offsetTop; 156 | 157 | return { x: left, y: top }; 158 | }; 159 | 160 | dom.createElement = function (name, attrs, html) { 161 | var e = document.createElement(name); 162 | 163 | for (var a in (attrs || {})) { 164 | e[a] = attrs[a]; 165 | } 166 | html && (e.innerHTML = html); 167 | 168 | return e; 169 | }; 170 | 171 | dom.hsla = function (h, s, l, a) { 172 | return 'hsla(' + [h, s + '%', l + '%', a].join(',') + ')'; 173 | }; 174 | 175 | dom.flash = function (element) { 176 | var alpha = 80, inc = 1; 177 | 178 | var timer = setInterval(function () { 179 | element.style.backgroundColor = dom.hsla(60, 90, 95, alpha / 100); 180 | alpha += inc; 181 | 182 | if (alpha === 100) { inc = -0.3 } 183 | if (alpha <= 0) { clearInterval(timer), element.style.backgroundColor = '' } 184 | }, 5); 185 | }; 186 | 187 | dom.disableSelection = function (element) { 188 | element.onselectstart = function () { return false }; 189 | element.unselectable = "on"; 190 | }; 191 | dom.enableSelection = function (element) { 192 | element.onselectstart = null; 193 | element.unselectable = "off"; 194 | }; 195 | dom.show = function (e) { 196 | return e.style.display = ''; 197 | }; 198 | dom.hide = function (e) { 199 | return e.style.display = 'none'; 200 | }; 201 | 202 | dom.contentWidth = function (element, str) { 203 | var span = dom.createElement('span'), width, parent, style; 204 | 205 | var css = function (property) { 206 | return window.getComputedStyle(element, null).getPropertyValue(property); 207 | }; 208 | 209 | span.style.fontFamily = css('font-family'); 210 | span.style.fontSize = css('font-size'); 211 | span.style.letterSpacing = css('letter-spacing'); 212 | span.style.wordSpacing = css('word-spacing'); 213 | span.style.padding = css('padding'); 214 | span.style.margin = css('margin'); 215 | span.style.height = 'auto'; 216 | span.style.width = 'auto'; 217 | span.style.visibility = 'hidden'; 218 | span.style.position = 'absolute'; 219 | span.style.top = '-1000px'; 220 | 221 | span.innerHTML = (str || element.value).replace(/&/g, '&') 222 | .replace(//g, '>') 224 | .replace(/ /g, ' '); 225 | document.body.appendChild(span); 226 | width = span.offsetWidth; 227 | document.body.removeChild(span); 228 | 229 | return Math.max(width + 1, 2); 230 | }; 231 | dom.autosizing = function (input) { 232 | if (input.AUTOSIZING) { return input } 233 | 234 | var resize = function (element, str) { 235 | return element.style.width = dom.contentWidth(element, str) + 'px'; 236 | }; 237 | input.addEventListener('keypress', function (e) { 238 | if (e.charCode) { // Make sure it's a printable character. (Firefox) 239 | resize(this, this.value + String.fromCharCode(e.charCode)); 240 | } 241 | }, false); 242 | input.addEventListener('keydown', function (e) { 243 | var position = e.target.selectionStart, string = this.value.split(''); 244 | if (e.keyCode === KEY.BACKSPACE && position > 0) { 245 | string.splice(position - 1, 1); 246 | resize(this, string.join('')); 247 | } 248 | }, false); 249 | input.autosize = function (str) { 250 | return resize(this, str || this.value); 251 | }; 252 | resize(input, input.value); 253 | 254 | input.AUTOSIZING = true; 255 | 256 | return input; 257 | }; 258 | 259 | // 260 | // DOM Prototype Extensions 261 | // 262 | Node.prototype.emit = function (type, data) { 263 | var event = document.createEvent('Events'); 264 | event.initEvent(type, true, true); 265 | for (var k in data) { event[k] = data[k] } 266 | return this.dispatchEvent(event); 267 | }; 268 | Node.prototype.on = function (event, listener, c) { 269 | this.addEventListener(event, listener, c || false); 270 | return this; 271 | }; 272 | NodeList.prototype.forEach = function (fun) { 273 | return Array.prototype.forEach.call(this, fun); 274 | }; 275 | NodeList.prototype.map = function (fun) { 276 | return Array.prototype.map.call(this, fun); 277 | }; 278 | NodeList.prototype.indexOf = function (obj) { 279 | return Array.prototype.indexOf.call(this, obj); 280 | }; 281 | HTMLCollection.prototype.forEach = function (fun) { 282 | return Array.prototype.forEach.call(this, fun); 283 | }; 284 | HTMLCollection.prototype.map = function (fun) { 285 | return Array.prototype.map.call(this, fun); 286 | }; 287 | HTMLCollection.prototype.indexOf = function (obj) { 288 | return Array.prototype.indexOf.call(this, obj); 289 | }; 290 | HTMLElement.prototype.addClass = function (name) { 291 | return dom.addClass(this, name); 292 | }; 293 | HTMLElement.prototype.removeClass = function (name) { 294 | return dom.removeClass(this, name); 295 | }; 296 | HTMLElement.prototype.hasClass = function (name) { 297 | return dom.hasClass(this, name); 298 | }; 299 | HTMLElement.prototype.insertAfter = function (element, ref) { 300 | this.insertBefore(element, ref.nextElementSibling); 301 | }; 302 | HTMLElement.prototype.isDescendantOf = function (element) { 303 | var source = this; 304 | while (source !== document) { 305 | if (source === element) { return true } 306 | else { source = source.parentNode } 307 | } 308 | return false; 309 | }; 310 | HTMLElement.prototype.getComputedStyle = function (pseudo) { 311 | return window.getComputedStyle(this, pseudo); 312 | }; 313 | HTMLElement.prototype.setCursor = function (x) { 314 | this.setSelectionRange(x, x); 315 | return this; 316 | }; 317 | 318 | // 319 | // Object & Array ECMA 5 methods 320 | // 321 | if (! Array.isArray) { 322 | Array.isArray = function (obj) { 323 | return obj instanceof Array; 324 | }; 325 | } 326 | -------------------------------------------------------------------------------- /pub/js/index.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": ["pilgrim.js", "domdom.js", "domdom-tokenizing.js", "thingler.js"], 3 | "minify": false 4 | } 5 | -------------------------------------------------------------------------------- /pub/js/pilgrim.js: -------------------------------------------------------------------------------- 1 | // 2 | // ~ pilgrim.js ~ 3 | // 4 | // stateful xhr client 5 | // 6 | var pilgrim = (function () { 7 | // 8 | // Client 9 | // 10 | this.Client = function Client(host, options) { 11 | if (host && (typeof(host) === 'object')) { options = host, host = null } 12 | 13 | options = options || {}; 14 | 15 | this.headers = options.headers || {}; 16 | this.extension = options.extension || ''; 17 | this.host = host ? 'http://' + host.replace('http://', '') : ''; 18 | }; 19 | this.Client.prototype.resource = function (path) { 20 | return this.path(path + this.extension); 21 | }; 22 | this.Client.prototype.path = function (path) { 23 | var that = this; 24 | 25 | return { 26 | path: function (p) { return that.path([path, p].join('/')) }, 27 | 28 | get: function (data, callback) { this.request(true, 'get', data, callback) }, 29 | put: function (data, callback) { this.request(true, 'put', data, callback) }, 30 | post: function (data, callback) { this.request(true, 'post', data, callback) }, 31 | del: function (data, callback) { this.request(true, 'delete', data, callback) }, 32 | head: function (data, callback) { this.request(true, 'head', data, callback) }, 33 | 34 | getSync: function (data, callback) { this.request(false, 'get', data, callback) }, 35 | putSync: function (data, callback) { this.request(false, 'put', data, callback) }, 36 | postSync: function (data, callback) { this.request(false, 'post', data, callback) }, 37 | delSync: function (data, callback) { this.request(false, 'delete', data, callback) }, 38 | 39 | request: function (async, method /* [data], [callback] */) { 40 | var query = [], args = Array.prototype.slice.call(arguments, 2) 41 | .filter(function (a) { return a }); 42 | 43 | var callback = args.pop() || function () {}, 44 | data = args.shift(); 45 | 46 | path = (that.host + '/' + path).replace('//', '/'); 47 | 48 | if (method === 'get' && data) { 49 | for (var k in data) { 50 | query.push(k + '=' + encodeURIComponent(data[k])); 51 | } 52 | path += '?' + query.join('&'); 53 | data = null; 54 | } 55 | return new(pilgrim.XHR) 56 | (method, path, data, that.headers, async).send(callback); 57 | } 58 | }; 59 | }; 60 | // 61 | // XHR 62 | // 63 | this.XHR = function XHR(method, url, data, headers, async) { 64 | this.method = method.toLowerCase(); 65 | this.url = url; 66 | this.data = data || {}; 67 | this.async = async; 68 | 69 | if (window.XMLHttpRequest) { 70 | this.xhr = new(XMLHttpRequest); 71 | } else { 72 | this.xhr = new(ActiveXObject)("MSXML2.XMLHTTP.3.0"); 73 | } 74 | 75 | this.headers = { 76 | 'X-Requested-With': 'XMLHttpRequest', 77 | 'Accept': 'application/json' 78 | }; 79 | for (var k in headers) { this.headers[k] = headers[k] } 80 | }; 81 | this.XHR.prototype.send = function (callback) { 82 | this.data = JSON.stringify(this.data); 83 | 84 | this.xhr.open(this.method, this.url, this.async); 85 | this.xhr.onreadystatechange = function () { 86 | if (this.readyState != 4) { return } 87 | 88 | var body = this.responseText ? JSON.parse(this.responseText) : {}; 89 | 90 | if (this.status >= 200 && this.status < 300) { // Success 91 | callback(null, body); 92 | } else { // Error 93 | callback({ status: this.status, body: body, xhr: this }); 94 | } 95 | }; 96 | 97 | // Set content headers 98 | if (this.method === 'post' || this.method === 'put') { 99 | this.headers['Content-Type'] = 'application/json'; 100 | } 101 | 102 | // Set user headers 103 | for (k in this.headers) { 104 | this.xhr.setRequestHeader(k, this.headers[k]); 105 | } 106 | 107 | // Dispatch request 108 | this.xhr.send(this.method === 'get' ? null : this.data); 109 | 110 | return this; 111 | }; 112 | return this; 113 | }).call({}); 114 | -------------------------------------------------------------------------------- /pub/js/thingler.js: -------------------------------------------------------------------------------- 1 | // 2 | // ~ thingler.js ~ 3 | // 4 | var path = window.location.pathname; 5 | var id = path.slice(1); 6 | var xhr = new(pilgrim.Client)({ extension: '.json' }); 7 | var input = document.getElementById('new'); 8 | var title = document.getElementById('title'); 9 | var list = document.getElementById('list'); 10 | var about = document.getElementById('about'); 11 | var create = document.getElementById('create'); 12 | var header = document.querySelector('header'); 13 | var footer = document.querySelector('footer'); 14 | var hash = window.location.hash; 15 | 16 | var lock = document.getElementById('lock'); 17 | var passwordProtect = document.getElementById('password-protect'); 18 | var authenticate = document.getElementById('password-authenticate'); 19 | 20 | var room = { 21 | rev: null, 22 | locked: false, 23 | doc: null, 24 | changes: { 25 | data: [], 26 | rollback: function (changes) { 27 | this.data = changes.concat(this.data); 28 | }, 29 | push: function (type, id, change, callback) { 30 | change = change || {}; 31 | change.type = type; 32 | change.id = id && parseInt(id); 33 | change.ctime = Date.now() - new(Date)(room.doc.timestamp); 34 | change.callback = callback; 35 | 36 | this.data.push(change); 37 | 38 | // If we're inserting, sync the change right away. 39 | // Else, let it happen on the next tick. 40 | if (type === 'insert' || type === 'lock') { 41 | clock.tick(); 42 | } 43 | }, 44 | commit: function () { 45 | var commit = this.data; 46 | this.data = []; 47 | return commit; 48 | } 49 | }, 50 | initialize: function (doc) { 51 | // Initialize title and revision number 52 | room.rev = doc._rev && parseInt(doc._rev.match(/^(\d+)/)[1]); 53 | room.doc = doc; 54 | setTitle(doc.title); 55 | 56 | if (doc.locked) { 57 | lock.addClass('locked'); 58 | room.locked = true; 59 | } 60 | header.style.display = 'block'; 61 | 62 | // Initialize list 63 | doc.items && doc.items.forEach(function (item) { 64 | list.appendChild(createItem(item)); 65 | }); 66 | 67 | handleTagFilter(hash); 68 | dom.sortable(list, handleSort); 69 | 70 | footer.style.visibility = 'visible' 71 | 72 | // Before shutdown, do one last sync 73 | window.onbeforeunload = function (e) { 74 | clock.tick(true); 75 | }; 76 | 77 | // 78 | // Start the Clock 79 | // 80 | clock.init(function (clock, last) { 81 | var changes = room.changes.commit(); 82 | xhr.resource(id)[last ? 'postSync' : 'post']({ 83 | rev: room.rev || 0, 84 | changes: changes, 85 | last: last 86 | }, function (err, doc) { 87 | if (err) { 88 | if (err.status !== 404) { console.log(err) } 89 | room.changes.rollback(changes); 90 | } else if (doc && doc.commits) { 91 | room.rev = doc.rev || 0; 92 | 93 | if (doc.commits.length > 0) { 94 | doc.commits.forEach(function (commit) { 95 | commit.changes.forEach(function (change) { 96 | handlers[change.type](change); 97 | }); 98 | }); 99 | clock.activity(); 100 | } 101 | } 102 | clock.synchronised(); 103 | changes.forEach(function (change) { 104 | change.callback && change.callback(); 105 | }); 106 | }); 107 | }); 108 | } 109 | }; 110 | 111 | // 112 | // The Great Synchronization Clock 113 | // 114 | var clock = { 115 | timer: null, 116 | interval: 1000, 117 | synchronising: false, 118 | init: function (callback) { 119 | this.callback = callback; 120 | this.reset(1000); 121 | }, 122 | // 123 | // Creates a new timer based the interval 124 | // passed. 125 | // 126 | reset: function (interval) { 127 | // One hour maximum interval 128 | this.interval = Math.min(interval, 3600000); 129 | 130 | if (this.timer) { clearInterval(this.timer) } 131 | if (this.timeout) { clearTimeout(this.timeout) } 132 | 133 | this.timer = setInterval(function () { 134 | clock.tick(); 135 | }, this.interval); 136 | 137 | // In `this.interval * 4` milliseconds, 138 | // double the interval. 139 | // Note that this could never happen, 140 | // if a `reset` is executed within that time. 141 | this.timeout = setTimeout(function () { 142 | clock.reset(clock.interval * 2); 143 | }, this.interval * 4); 144 | }, 145 | // 146 | // Called on every interval tick. 147 | // 148 | tick: function (arg) { 149 | if (! this.synchronising) { 150 | this.synchronising = true; 151 | this.callback(this, arg); 152 | } 153 | }, 154 | 155 | synchronised: function () { 156 | this.synchronising = false; 157 | }, 158 | // 159 | // Called on inbound & outbound activity. 160 | // We either preserve the current interval length, 161 | // if it's the shortest possible, or divide it by four. 162 | // 163 | activity: function () { 164 | if (this.interval < 4000) { 165 | this.reset(1000); 166 | } else { 167 | this.reset(this.interval / 4); 168 | } 169 | return true; 170 | } 171 | }; 172 | 173 | var titleHasFocus = false; 174 | var tagPattern = /\B#[a-zA-Z0-9_-]+\b/g; 175 | 176 | dom.tokenizing(input, input.parentNode, tagPattern).on('new', function (e) { 177 | var tokens = this.parentNode.querySelector('.tokens'), 178 | title = this.value, 179 | item = { title: title, tags: e.tokens.concat(hash.length > 1 ? [hash] : []) }, 180 | element = handlers.insert(item), 181 | id = parseInt(element.firstChild.getAttribute('data-id')); 182 | 183 | tokens.innerHTML = ''; 184 | this.value = ''; 185 | this.focus(); 186 | 187 | room.changes.push('insert', id, item); 188 | }, false); 189 | 190 | input.parentNode.on('focus', function () { this.addClass('focused') }) 191 | .on('blur', function () { this.removeClass('focused') }); 192 | 193 | function parseTitle(str) { 194 | return str.replace(/&/, '&').replace(//g, '>').trim(); 195 | } 196 | 197 | // 198 | // Handle title changes 199 | // 200 | title.onfocus = function (e) { 201 | titleHasFocus = true; 202 | }; 203 | title.onkeydown = function (e) { 204 | if (e.keyCode === 13) { 205 | title.blur(); 206 | return false; 207 | } 208 | }; 209 | title.onblur = function (e) { 210 | titleHasFocus = false; 211 | setTitle(title.value); 212 | room.changes.push('title', null, { value: title.value }); 213 | }; 214 | 215 | xhr.resource(id).get(function (err, doc) { 216 | var password = authenticate.querySelector('input'); 217 | if (err && err.status === 404) { 218 | go('not-found'); 219 | if (id.match(/^[a-zA-Z0-9-]+$/)) { 220 | create.onclick = function () { 221 | xhr.resource(id).put(function (e, doc) { 222 | if (e) { 223 | 224 | } else { 225 | go('page'); 226 | dom.hide(document.getElementById('not-found')); 227 | room.initialize(doc); 228 | } 229 | }); 230 | return false; 231 | }; 232 | } else { 233 | dom.hide(create); 234 | } 235 | } else if (err && err.status === 401) { 236 | authenticate.style.display = 'block'; 237 | password.focus(); 238 | password.onkeydown = function (e) { 239 | var that = this; 240 | if (e.keyCode === 13) { 241 | password.addClass('disabled'); 242 | password.disabled = true; 243 | xhr.resource(id).path('session') 244 | .post({ password: this.value }, function (e, doc) { 245 | if (e) { 246 | that.addClass('error'); 247 | password.removeClass('disabled'); 248 | password.disabled = false; 249 | } else { 250 | xhr.resource(id).get(function (e, doc) { 251 | go('page'); 252 | room.initialize(doc); 253 | dom.hide(authenticate); 254 | }); 255 | } 256 | }); 257 | } 258 | }; 259 | } else { 260 | go('page'); 261 | room.initialize(doc); 262 | } 263 | 264 | function go(page) { 265 | document.getElementById(page).style.display = 'block'; 266 | if (page === 'page') { input.focus() } 267 | } 268 | }); 269 | 270 | var handlers = { 271 | insert: function (change) { 272 | var item = createItem(change); 273 | list.insertBefore(item, list.firstChild); 274 | dom.sortable(list, handleSort); 275 | dom.flash(item); 276 | return item; 277 | }, 278 | title: function (change) { 279 | setTitle(change.value); 280 | dom.flash(title); 281 | }, 282 | edit: function (change) { 283 | var element = find(change.id); 284 | refreshItem(element, { title: change.title, tags: change.tags }); 285 | dom.flash(element); 286 | }, 287 | check: function (change) { 288 | var element = find(change.id); 289 | element.querySelector('[type="checkbox"]').checked = true; 290 | element.setAttribute('class', 'completed'); 291 | dom.flash(element); 292 | }, 293 | uncheck: function (change) { 294 | var element = find(change.id), 295 | checkbox = element.querySelector('[type="checkbox"]'); 296 | 297 | checkbox.checked = false; 298 | checkbox.disabled = false; 299 | element.setAttribute('class', ''); 300 | dom.flash(element); 301 | }, 302 | remove: function (change) { 303 | var element = find(change.id); 304 | element && list.removeChild(element.parentNode); 305 | }, 306 | sort: function (change) { 307 | var elem = find(change.id).parentNode, 308 | index = elem.parentNode.children.indexOf(elem), 309 | ref = list.children[change.to]; 310 | 311 | if (change.to > index) { 312 | if (!ref || change.to === list.children.length - 1) { 313 | list.appendChild(elem); 314 | } else { 315 | list.insertBefore(elem, ref.nextSibling); 316 | } 317 | } else { 318 | list.insertBefore(elem, ref); 319 | } 320 | dom.flash(elem); 321 | }, 322 | lock: function (change) { 323 | lock.addClass('locked'); 324 | room.locked = true; 325 | }, 326 | unlock: function (change) { 327 | lock.removeClass('locked'); 328 | room.locked = false; 329 | } 330 | }; 331 | 332 | document.querySelector('[data-action="about"]').onclick = function () { 333 | if (about.style.display !== 'block') { 334 | about.style.display = 'block'; 335 | } else { 336 | dom.hide(about); 337 | } 338 | return false; 339 | }; 340 | 341 | document.querySelector('[data-action="close"]').onclick = function () { 342 | dom.hide(this.parentNode.parentNode); 343 | return false; 344 | }; 345 | 346 | lock.onclick = function () { 347 | var input = passwordProtect.querySelector('input'); 348 | 349 | if (room.locked) { 350 | room.changes.push('unlock', null, {}); 351 | handlers.unlock(); 352 | } else { 353 | input.disabled = false; 354 | input.removeClass('disabled'); 355 | passwordProtect.style.display = 'block'; 356 | input.focus(); 357 | input.onkeydown = function (e) { 358 | if (e.keyCode === 13 && input.value) { 359 | input.disabled = true; 360 | input.addClass('disabled'); 361 | room.changes.push('lock', null, { password: input.value }, function () { 362 | dom.hide(passwordProtect); 363 | }); 364 | handlers.lock(); 365 | return false; 366 | } 367 | }; 368 | } 369 | return false; 370 | }; 371 | 372 | // 373 | // Find an item by `title` 374 | // 375 | function find(id) { 376 | return list.querySelector('[data-id="' + id + '"]'); 377 | } 378 | 379 | function createItem(item) { 380 | var template = document.getElementById('todo-template'); 381 | var e = dom.createElement('li'), 382 | clone = template.cloneNode(true), 383 | remove = clone.querySelector('a[data-action="remove"]'), 384 | edit = clone.querySelector('a[data-action="edit"]'), 385 | checkbox = clone.querySelector('input[type="checkbox"]'), 386 | input = clone.querySelector('input[type=text]'); 387 | 388 | if (item.completed) { 389 | checkbox.checked = true; 390 | checkbox.disabled = true; 391 | clone.setAttribute('class', 'completed'); 392 | } 393 | 394 | clone.id = ''; 395 | clone.setAttribute('style', ''); 396 | 397 | if (! ('id' in item)) { 398 | item.id = Date.now() - new(Date)(room.doc.timestamp); 399 | } 400 | 401 | refreshItem(clone, item); 402 | 403 | e.appendChild(clone); 404 | 405 | // Remove Item 406 | remove.onclick = function () { 407 | list.removeChild(e); 408 | room.changes.push('remove', item.id); 409 | return false; 410 | }; 411 | 412 | // 413 | // Edit Item 414 | // 415 | // We return `false` from `onmousedown` to stop 416 | // `onblur` from triggering. 417 | // 418 | edit.onmousedown = function (e) { return false }; 419 | edit.onclick = function (e) { 420 | handleEdit(clone); 421 | return false; 422 | }; 423 | 424 | dom.tokenizing(input, e, tagPattern); 425 | 426 | input.addEventListener('new', handleEditSave, false); 427 | input.addEventListener('blurall', handleEditSave, false); 428 | 429 | // Check Item 430 | checkbox.addEventListener('click', function () { 431 | if (this.checked) { 432 | room.changes.push('check', item.id); 433 | clone.setAttribute('class', 'completed'); 434 | } else { 435 | room.changes.push('uncheck', item.id); 436 | clone.removeAttribute('data-completed'); 437 | clone.setAttribute('class', ''); 438 | } 439 | }, false); 440 | 441 | return e; 442 | } 443 | 444 | function refreshItem(element, item) { 445 | var tagList = element.querySelector('ul.tags'); 446 | var label = element.querySelector('label'); 447 | 448 | tagList.innerHTML = ''; 449 | 450 | for (var k in item) { 451 | element.setAttribute('data-' + k, Array.isArray(item[k]) ? 452 | item[k].join(' ') : item[k]); 453 | } 454 | 455 | if (item.tags) { 456 | item.tags.forEach(function (tag) { 457 | var a = dom.createElement('a', { href: tag }, tag), 458 | li = dom.createElement('li'); 459 | 460 | if (! tagList.querySelector('[data-tag="' + tag + '"]')) { 461 | li.setAttribute('data-tag', tag); 462 | li.appendChild(a); 463 | a.onclick = function () { 464 | if (window.location.hash === tag) { 465 | window.location.hash = ''; 466 | return false; 467 | } 468 | }; 469 | tagList.appendChild(li); 470 | } 471 | }); 472 | } 473 | label.innerHTML = markup(parseTitle(item.title)); 474 | } 475 | function markup(str) { 476 | return str.replace(/\*\*((?:\\\*|\*[^*]|[^*])+)\*\*/g, function (_, match) { 477 | return '' + match.replace(/\\\*\*/g, '*') + ''; 478 | }).replace(/\*((?:\\\*|[^*])+)\*/g, function (_, match) { 479 | return '' + match.replace(/\\\*/g, '*') + ''; 480 | }).replace(/\b_((?:\\\_|[^_])+)_\b/g, function (_, match) { 481 | return '' + match.replace(/\\_/g, '_') + ''; 482 | }).replace(/`((?:\\`|[^`])+)`/g, function (_, match) { 483 | return '' + match.replace(/\\`/g, '`') + ''; 484 | }).replace(/(https?:\/\/[^\s]+)/g, '$1'); 485 | } 486 | 487 | function handleSort(id, to) { 488 | return room.changes.push('sort', id, { to: to }); 489 | } 490 | function handleTagFilter(filter) { 491 | var child, tag, tags; 492 | 493 | list.querySelectorAll('li.active').forEach(function (e) { 494 | e.removeClass('active'); 495 | }); 496 | 497 | list.children.forEach(function (child) { 498 | if (filter) { 499 | tag = child.querySelector('[data-tag="' + filter + '"]'); 500 | tags = child.firstChild.getAttribute('data-tags'); 501 | 502 | if (tags && (tags.split(' ').indexOf(filter) !== -1)) { 503 | tag.addClass('active'); 504 | dom.show(child); 505 | } else { 506 | dom.hide(child); 507 | } 508 | } else { 509 | tag && tag.removeClass('active'); 510 | dom.show(child); 511 | } 512 | }); 513 | } 514 | function handleEdit(element) { 515 | var label = element.querySelector('label'), 516 | tags = element.getAttribute('data-tags'), 517 | field = element.querySelector('input[type="text"]'), 518 | li = element.parentNode, 519 | check = element.querySelector('input[type="checkbox"]'), 520 | tokens = element.querySelector('.tokens'); 521 | 522 | li.style.cursor = 'text'; 523 | 524 | if (li.hasClass('editing')) { 525 | handleEditSave.call(field, { tokens: dom.tokenizing.parseTokens(tokens) }); 526 | } else { 527 | check.disabled = false; 528 | li.addClass('editing'); 529 | dom.show(tokens), dom.hide(label), dom.show(field); 530 | if (tags) { 531 | dom.tokenizing.createTokens.call(field.tokenizer, tags.split(' ')); 532 | tokens.lastChild.lastChild.focus(); 533 | } else { 534 | field.focus(); 535 | } 536 | field.value = element.getAttribute('data-title'); 537 | field.autosize(); 538 | } 539 | } 540 | function handleEditSave(e) { 541 | if (! this.parentNode.parentNode.hasClass('editing')) { return } 542 | 543 | var div = this.parentNode, 544 | tokens = div.querySelector('.tokens'), 545 | tags = div.getAttribute('data-tags'), 546 | id = div.getAttribute('data-id'), 547 | check = div.querySelector('input[type="checkbox"]'), 548 | label = div.querySelector('label'); 549 | 550 | var item = { 551 | title: this.value, 552 | tags: e.tokens 553 | }; 554 | 555 | div.parentNode.style.cursor = ''; 556 | 557 | var old = div.getAttribute('data-title'); 558 | 559 | div.getAttribute('data-completed') && (check.disabled = true); 560 | 561 | div.parentNode.removeClass('editing'); 562 | dom.hide(this), dom.show(label); 563 | 564 | // Only push a change if something actually changed. 565 | if (item.title !== old || item.tags.join(' ') !== tags) { 566 | room.changes.push('edit', id, item); 567 | refreshItem(div, item); 568 | } 569 | dom.hide(tokens); 570 | }; 571 | 572 | function setTitle(str) { 573 | title.value = str; 574 | document.title = 'Thingler · ' + str; 575 | } 576 | // 577 | // Check the hashtag every 10ms, for changes 578 | // 579 | setInterval(function () { 580 | if (window.location.hash !== hash) { 581 | hash = window.location.hash; 582 | handleTagFilter(hash); 583 | } 584 | }, 10); 585 | 586 | -------------------------------------------------------------------------------- /pub/less/thingler.less: -------------------------------------------------------------------------------- 1 | // 2 | // thingler.less 3 | // 4 | @yellow: hsl(60, 85%, 97%); 5 | @dark-yellow: hsl(hue(@yellow), 55%, 92%); 6 | @red: hsl(10, 50%, 60%); 7 | @light-red: #ee8167; 8 | @black: hsl(215, 20%, 30%); 9 | @dark-blue: hsl(225, 50%, 40%); 10 | 11 | @white: #fafafa; 12 | @light-grey: #ddd; 13 | @medium-grey: #ccc; 14 | @dark-grey: #aaa; 15 | 16 | @page-width: 800px; 17 | 18 | // 19 | // Mixins 20 | // 21 | .box-shadow(@x, @y, @blur, @color) { 22 | -webkit-box-shadow: @x @y @blur @color; 23 | -moz-box-shadow: @x @y @blur @color; 24 | box-shadow: @x @y @blur @color; 25 | } 26 | .border-radius(@r) { 27 | -webkit-border-radius: @r; 28 | -moz-border-radius: @r; 29 | border-radius: @r; 30 | } 31 | .border-radius-bottom(@r) { 32 | -webkit-border-bottom-left-radius: @r; 33 | -moz-border-bottom-left-radius: @r; 34 | border-bottom-left-radius: @r; 35 | -webkit-border-bottom-right-radius: @r; 36 | -moz-border-bottom-right-radius: @r; 37 | border-bottom-right-radius: @r; 38 | } 39 | .link (@color: inherit, @border-color: #eee, @border-style: solid) { 40 | border-bottom: 1px @border-style @border-color; 41 | font-family: Arial, sans-serif; 42 | color: @color; 43 | &:hover { 44 | color: hsl(10, 50%, 60%); 45 | border-bottom: 1px solid hsl(10, 50%, 95%); 46 | } 47 | } 48 | .tokenize (@color, @padding-v, @padding-h: 8px) { 49 | color: @color; 50 | padding: 0; 51 | margin: auto 0; 52 | line-height: 1em; 53 | padding: @padding-v @padding-h; 54 | border-width: 1px; 55 | border-style: solid; 56 | border-color: darken(@dark-yellow, 2%); 57 | background-color: @yellow; 58 | display: inline-block; 59 | .box-shadow(0, 1px, 2px, #eee); 60 | .border-radius(2px); 61 | &:hover { 62 | text-decoration: none; 63 | color: darken(@color, 10%); 64 | border-color: darken(@dark-yellow, 8%); 65 | } 66 | } 67 | 68 | // 69 | // General 70 | // 71 | * { margin: 0; padding: 0 } 72 | 73 | body { 74 | font-family: 'Arial', sans-serif; 75 | font-size: 24px; 76 | width: @page-width; 77 | padding: 60px 30px; 78 | margin: 0 auto; 79 | background-color: @white; 80 | } 81 | body > header { 82 | position: absolute; 83 | width: @page-width; 84 | top: 22px; 85 | color: @light-grey; 86 | display: block; 87 | margin: 0; 88 | font-size: 16px; 89 | } 90 | body > footer { 91 | visibility: hidden; 92 | p { margin: 10px 0 } 93 | margin: 30px 0; 94 | text-align: right; 95 | font-size: 14px; 96 | color: lighten(@light-grey, 5%); 97 | p:first-child { 98 | color: @light-grey; 99 | font-size: 18px; 100 | &:hover, &:hover a { 101 | color: darken(@light-grey, 8%); 102 | a:hover { 103 | color: @light-red; 104 | border-bottom: 1px solid hsl(10, 50%, 95%); 105 | } 106 | } 107 | } 108 | a { 109 | color: @light-grey; 110 | border-bottom: 1px solid #eee; 111 | &:hover { 112 | color: @light-red; 113 | border-bottom: 1px solid hsl(10, 50%, 95%); 114 | } 115 | } 116 | } 117 | 118 | h1 { color: @black; font-size: 42px; } 119 | ul, li { list-style-type: none; padding: 0; } 120 | ul { -webkit-padding-start: 0; } 121 | a { text-decoration: none; color: @medium-grey; } 122 | input[type="text"], input[type="password"] { outline: none; } 123 | input[type="checkbox"] { font-size: 22px; } 124 | 125 | // 126 | // Todo List 127 | // 128 | #page { 129 | width: @page-width; 130 | @item-height: 48px; 131 | #title { 132 | font-size: 48px; 133 | font-weight: bold; 134 | font-family: 'Arial', sans-serif; 135 | border: 0; 136 | border: 1px dashed transparent; 137 | color: @black; 138 | &:focus { outline: none; border-color: @light-grey } 139 | margin-bottom: 15px; 140 | margin-left: -1px; 141 | padding: 0.25em 1px; 142 | width: @page-width - 4px; 143 | background-color: @white; 144 | &:focus { background-color: white; } 145 | } 146 | ul#list.unselectable li { 147 | cursor: default; 148 | user-select: none; 149 | -moz-user-select: none; 150 | -webkit-user-select: none; 151 | } 152 | label, label a, label a:visited { 153 | color: @black; 154 | text-decoration: none; 155 | display: inline-block; 156 | line-height: @item-height; 157 | //overflow: hidden; 158 | position: relative; 159 | //white-space: wrap; 160 | } 161 | label { max-width: 670px; } 162 | li.flashing label:after { visibility: hidden; } 163 | //label:after { 164 | // content: " "; 165 | // padding: 1px; 166 | // position: absolute; 167 | // top: 0; 168 | // right: -15px; 169 | // width: 30px; 170 | // height: @item-height; 171 | // display: block; 172 | // line-height: @item-height; 173 | // background-color: white; 174 | // .box-shadow(-15px, 0px, 30px, white); 175 | //} 176 | #list { margin: 30px 0; width: @page-width; padding: 0; } 177 | #list > li { 178 | overflow: hidden; 179 | font-size: 22px; 180 | font-family: Georgia, 'Times New Roman', serif; 181 | width: @page-width - 12px; 182 | padding: 0 5px; 183 | cursor: move; 184 | border-color: transparent; 185 | border-style: solid; 186 | border-width: 0 1px 0px 1px; 187 | border-bottom: 1px dotted @light-grey; 188 | background-color: @white; 189 | a { 190 | color: @black + #333; 191 | &:hover { text-decoration: underline; } 192 | } 193 | &:hover { 194 | background-color: @yellow; 195 | .actions { visibility: visible; } 196 | label:after { 197 | background-color: @yellow; 198 | .box-shadow(-15px, 0px, 30px, @yellow); 199 | } 200 | } 201 | &.editing { 202 | .tags { display: none } 203 | .actions { visibility: visible } 204 | [data-action="edit"] { color: @light-red } 205 | background-color: white; 206 | border: 1px dashed @light-grey !important; 207 | margin-top: -1px; 208 | } 209 | &.editing:last-child { margin-bottom: -1px; } 210 | input[type="text"] { 211 | font-family: Georgia, 'Times New Roman', serif; 212 | font-size: 22px; 213 | margin-left: -1px; 214 | border: 0; 215 | background-color: transparent; 216 | height: @item-height; 217 | width: 650px; 218 | } 219 | input[type="checkbox"] { 220 | vertical-align: middle; 221 | height: @item-height; 222 | line-height: @item-height; 223 | float: left; 224 | display: block; 225 | margin-right: 25px; 226 | } 227 | @color: desaturate(darken(@dark-yellow, 20%), 35%); 228 | height: auto; 229 | &:hover .tags li a { 230 | color: darken(@color, 5%); 231 | background-color: @white; 232 | } 233 | &.editing .token { 234 | color: darken(@color, 15%) !important; 235 | } 236 | .tags { 237 | @padding-v: 4px; 238 | float: right; 239 | display: inline; 240 | line-height: @item-height; 241 | height: @item-height; 242 | margin: 0 15px; 243 | li { 244 | display: inline-block; 245 | margin-left: 5px; 246 | 247 | a { 248 | .tokenize(@color, @padding-v); 249 | } 250 | &.active a { 251 | border-color: darken(@dark-yellow, 8%); 252 | color: darken(@color, 10%); 253 | } 254 | } 255 | li:first-child { margin-left: 0 } 256 | } 257 | // 258 | // Edit/Remove 259 | // 260 | .actions { 261 | visibility: hidden; 262 | float: right; 263 | font-size: 14px; 264 | height: @item-height; 265 | line-height: @item-height; 266 | a { 267 | padding: 8px; 268 | border: 0 !important; 269 | &:hover { border: 0 !important } 270 | border-bottom: 1px solid #f4f4f4; 271 | font-family: Arial, sans-serif; 272 | text-decoration: none; 273 | color: @medium-grey; 274 | &:last-child { margin-right: 10px } 275 | &:hover { color: @light-red; } 276 | } 277 | } 278 | &:last-child { border-bottom: 0; } 279 | } 280 | .completed { 281 | label, label a { 282 | color: #ccc !important; 283 | text-decoration: line-through; 284 | } 285 | .tags li a { 286 | color: #ccc !important; 287 | background-color: #fafafa !important; 288 | border-color: #eee !important; 289 | &:hover { 290 | color: #bbb !important; 291 | } 292 | } 293 | } 294 | } 295 | 296 | // 297 | // Input 298 | // 299 | input#new, input[type="password"], input.token-input { 300 | font-size: 24px; 301 | padding: 8px; 302 | border: 1px solid @light-grey; 303 | &.disabled { 304 | color: @medium-grey; 305 | background-color: #f0f0f0; 306 | } 307 | } 308 | input#new, span.string-input, input.token { 309 | font-family: 'Lucida Grande', Arial, sans-serif; 310 | } 311 | ul.tokens { margin: 0 } 312 | ul.tokens, ul.tokens li { display: inline-block; margin: 0; } 313 | 314 | span.string-input { 315 | font-size: 24px; 316 | margin-left: 1px; 317 | } 318 | ul.tokens li:last-child .token-input { 319 | padding: 0 5px !important; 320 | } 321 | input.token-input { 322 | margin: 0; 323 | padding: 0 3px 0 1px; 324 | display: inline-block; 325 | outline: none; 326 | border: 0; 327 | &.empty { 328 | margin: 0; 329 | width: 5px; 330 | } 331 | } 332 | 333 | #list { 334 | .token { 335 | @color: desaturate(darken(@dark-yellow, 20%), 35%); 336 | .tokenize(@color, 2px, 1px); 337 | padding-right: 4px; 338 | margin-left: -2px; 339 | outline: none; 340 | font-size: inherit; 341 | font-family: inherit; 342 | } 343 | .token-input { 344 | padding: 0 3px 0 1px; 345 | margin: 0; 346 | font-family: inherit; 347 | font-size: inherit; 348 | } 349 | } 350 | #new-wrapper { 351 | font-size: 24px; 352 | padding: 8px; 353 | cursor: text; 354 | width: @page-width - 18px; 355 | .box-shadow(0px, 1px, 2px, @light-grey); 356 | &.focused { .box-shadow(0px, 2px, 8px, @light-grey); } 357 | border: 1px solid @light-grey; 358 | background-color: white; 359 | input#new { 360 | width: @page-width - 18px - 16px; 361 | padding: 0; 362 | border: 0 !important; 363 | display: inline-block; 364 | margin: 0; 365 | } 366 | .token { 367 | @color: desaturate(darken(@dark-yellow, 20%), 35%); 368 | .tokenize(@color, 2px, 1px); 369 | padding-right: 4px; 370 | margin: -6px 0; 371 | margin-left: -2px; // To compensate for the padding-left 372 | outline: none; 373 | font-size: 24px; 374 | } 375 | } 376 | 377 | // 378 | // Drag & Drop 379 | // 380 | .dragging { 381 | div { cursor: move; } 382 | position: absolute; 383 | z-index: 10; 384 | background-color: hsla(60, 100%, 95%, 0.7) !important; 385 | width: @page-width; 386 | border: 1px dashed @light-grey !important; 387 | .actions { visibility: hidden } 388 | .box-shadow(0px, 2px, 16px, @light-grey); 389 | } 390 | .ghost { 391 | label { color: @light-grey !important; } 392 | .tags { 393 | color: @light-grey !important; 394 | li a { background-color: #fefefe !important; border-color: #eee !important } 395 | } 396 | margin-top: -1px; 397 | border-bottom: 1px dashed @light-grey !important; 398 | border-top: 1px dashed @light-grey !important; 399 | } 400 | 401 | 402 | // 403 | // Room Locking 404 | // 405 | #password-protect div.password { 406 | .border-radius(8px); 407 | background-color: transparent; 408 | input { 409 | .box-shadow(0, 3px, 30px, #ddd); 410 | } 411 | } 412 | div.password { 413 | @width: 600px; 414 | @height: 116px; 415 | position: absolute; 416 | height: @height; 417 | top: 40%; 418 | left: 50%; 419 | width: @width; 420 | margin-left: ((@width + 120px) / 2) * -1; 421 | margin-top: ((@height + 120px) / 2) * -1; 422 | padding: 60px; 423 | 424 | label { 425 | font-weight: bold; 426 | color: lighten(@black, 10%); 427 | font-size: 32px; 428 | display: block; 429 | padding-bottom: 30px; 430 | } 431 | input { 432 | width: @width - 30px; 433 | .border-radius(8px); 434 | } 435 | input.error { 436 | border-color: hsl(10, 60%, 60%); 437 | } 438 | label { 439 | } 440 | .close { 441 | color: #ccc; 442 | float: right; 443 | margin-top: 30px; 444 | margin-right: 16px; 445 | font-size: 18px; 446 | text-shadow: 0px 0px 10px white; 447 | border-bottom: 1px solid #eee; 448 | &:hover { 449 | color: @light-red; 450 | } 451 | } 452 | } 453 | #lock { 454 | text-align: right; 455 | float: right; 456 | div { 457 | display: inline-block; 458 | width: 24px; 459 | height: 24px; 460 | background: url(/images/lock-24.png) 0 0 no-repeat; 461 | margin-left: 5px; 462 | } 463 | &.locked { 464 | display: block; 465 | &:hover { 466 | .locked-hint { display: inline } 467 | .unlocked-hint { display: none } 468 | } 469 | span { color: @light-red } 470 | div { background: url(/images/lock-24.png) -24px 0 no-repeat; } 471 | } 472 | &:hover { 473 | .locked-hint { display: none } 474 | .unlocked-hint { display: inline } 475 | div { background: url(/images/lock-24.png) -24px 0 no-repeat; } 476 | } 477 | span { 478 | color: @light-red; 479 | font-size: 14px; 480 | display: none; 481 | vertical-align: 1px; 482 | } 483 | } 484 | 485 | // 486 | // Overlays 487 | // 488 | .overlay { 489 | position: fixed; 490 | top: 0; 491 | left: 0; 492 | width: 100%; 493 | height: 100%; 494 | z-index: 10; 495 | } 496 | #password-authenticate.overlay { 497 | background-color: @white; 498 | } 499 | #password-protect.overlay { 500 | background-color: rgba(255, 255, 255, 0.9); 501 | } 502 | 503 | // 504 | // 404 505 | // 506 | #not-found { 507 | padding: 60px 0; 508 | h1 { 509 | text-align: center; 510 | font-size: 48px; 511 | } 512 | p { text-align: center; margin-top: 30px; } 513 | a { .link(@dark-grey) } 514 | } 515 | 516 | // 517 | // About 518 | // 519 | #about { 520 | font-size: 14px; 521 | float: left; 522 | display: none; 523 | margin-top: 0px; 524 | text-align: right; 525 | p { 526 | font-size: 14px !important; 527 | color: @dark-grey; 528 | line-height: 22px; 529 | a { .link(lighten(@dark-grey, 8%), @light-grey, dashed) } 530 | } 531 | } 532 | 533 | -------------------------------------------------------------------------------- /pub/upgrade.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Thingler 5 | 11 | 12 | 13 |

This does not compute.

14 |

Please upgrade your browser to something decent.

15 | 16 | 17 | -------------------------------------------------------------------------------- /src/changes.js: -------------------------------------------------------------------------------- 1 | var db = require('./db').database; 2 | var parseRev = require('./db').parseRev; 3 | var todo = require('./todo').resource; 4 | var md5 = require('./md5'); 5 | 6 | var cache = {}; 7 | 8 | var hour = 3600 * 1000; 9 | 10 | // Remove commits older than an hour. 11 | // The maximum client polling time is one hour, 12 | // so by this time, all connected clients should 13 | // be up-to-date. 14 | setInterval(function () { 15 | var now = Date.now(); 16 | Object.keys(cache).forEach(function (k) { 17 | if (now - cache[k].ctime > hour) { 18 | delete(cache[k]); 19 | } 20 | }); 21 | }, hour); 22 | 23 | this.post = function (res, id, params, session) { 24 | todo.get(id, function (err, doc) { 25 | if (err) { return res.send(doc.headers.status, {}, err) } 26 | 27 | var changes = params.changes; 28 | 29 | // Apply all the changes to the document 30 | changes.forEach(function (change) { 31 | if (validate(change)) { 32 | exports.handlers[change.type](doc, change, session); 33 | } 34 | }); 35 | 36 | if (changes.length > 0) { 37 | db.put(id, doc, function (err, doc) { 38 | if (err) { 39 | return res.send(doc.headers.status, {}, err); 40 | } 41 | reply(doc.rev); 42 | todo.clear(id); 43 | }); 44 | } else { 45 | reply(doc._rev); 46 | } 47 | 48 | function reply(rev) { 49 | cache[id] = cache[id] || []; 50 | 51 | var dirty = cache[id].slice(0), status = 200; 52 | 53 | rev = rev ? parseRev(rev) : 0; 54 | 55 | if (changes.length > 0) { 56 | cache[id].push({ rev: rev, changes: changes, ctime: Date.now() }); 57 | status = 201; 58 | } 59 | 60 | // If it's a goodbye, don't send anything back, just an OK 61 | if (params.last) { 62 | res.send(status); 63 | } else { 64 | res.send(status, {}, { 65 | rev: rev, 66 | commits: dirty.filter(function (commit) { 67 | return commit.rev > params.rev; 68 | }) 69 | }); 70 | } 71 | } 72 | }); 73 | }; 74 | 75 | this.get = function (res, id, params) { 76 | res.send(200, {}, { changes: cache[id] }); 77 | }; 78 | 79 | this.handlers = { 80 | insert: function (doc, change) { 81 | if (doc.items.length < 256) { 82 | if (! Array.isArray(change.tags)) { return } 83 | doc.items.unshift({ 84 | id: change.id, 85 | title: sanitize(change.title), 86 | tags: change.tags 87 | }); 88 | } 89 | }, 90 | title: function (doc, change) { 91 | doc.title = sanitize(change.value); 92 | }, 93 | edit: function (doc, change) { 94 | var item = find(change.id, doc); 95 | if (item) { 96 | item.title = change.title; 97 | item.tags = change.tags; 98 | } 99 | }, 100 | sort: function (doc, change) { 101 | var index = indexOf(change.id, doc), item; 102 | if (index !== -1) { 103 | item = doc.items.splice(index, 1)[0]; 104 | doc.items.splice(change.to, 0, item); 105 | } 106 | }, 107 | check: function (doc, change) { 108 | var item = find(change.id, doc); 109 | if (item) { 110 | item.completed = Date.now(); 111 | } 112 | }, 113 | uncheck: function (doc, change) { 114 | var item = find(change.id, doc); 115 | if (item) { 116 | delete(item.completed); 117 | } 118 | }, 119 | remove: function (doc, change) { 120 | var index = indexOf(change.id, doc); 121 | if (index !== -1) { 122 | doc.items.splice(index, 1); 123 | } 124 | }, 125 | lock: function (doc, change, session) { 126 | if (session) { 127 | session.authenticated.push(doc._id); 128 | doc.password = md5.digest(change.password); 129 | } 130 | }, 131 | unlock: function (doc, change, session) { 132 | doc.password = null; 133 | } 134 | }; 135 | 136 | function validate(change) { 137 | if (change.type && (change.type in exports.handlers)) { 138 | if ('title' in change) { 139 | if ((typeof(change.title) !== 'string') || change.title.length > 256) { 140 | return false; 141 | } 142 | } 143 | } else { 144 | return false; 145 | } 146 | return true; 147 | } 148 | function sanitize(str) { 149 | return str.replace(//g, '>'); 150 | } 151 | 152 | function find(id, doc) { 153 | for (var i = 0; i < doc.items.length; i++) { 154 | if (doc.items[i].id === id) { 155 | return doc.items[i]; 156 | } 157 | } 158 | return null; 159 | } 160 | function indexOf(id, doc) { 161 | for (var i = 0; i < doc.items.length; i++) { 162 | if (doc.items[i].id === id) { 163 | return i; 164 | } 165 | } 166 | return -1; 167 | } 168 | -------------------------------------------------------------------------------- /src/db.js: -------------------------------------------------------------------------------- 1 | 2 | var cradle = require('cradle'); 3 | 4 | this.connection = new(cradle.Connection)({ 5 | host: '127.0.0.1', 6 | port: 5984 7 | }); 8 | 9 | this.database = this.connection.database('thingler'); 10 | 11 | this.parseRev = function (rev) { 12 | return parseInt(rev.match(/^(\d+)-/)[1]); 13 | } 14 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | 2 | var fs = require('fs'); 3 | var sys = require('sys'); 4 | var http = require('http'); 5 | var Buffer = require('buffer').Buffer; 6 | 7 | var journey = require('journey'), 8 | static = require('node-static'); 9 | 10 | var todo = require('./todo').resource, 11 | session = require('./session/session'), 12 | routes = require('./routes'); 13 | 14 | var options = { 15 | port: parseInt(process.argv[2]) || 8080, 16 | lock: '/tmp/thinglerd.pid' 17 | }; 18 | 19 | var env = (process.env['NODE_ENV'] === 'production' || 20 | options.port === 80) ? 'production' : 'development'; 21 | 22 | // 23 | // Create a Router object with an associated routing table 24 | // 25 | var router = new(journey.Router)(routes.map, { strict: true }); 26 | var file = new(static.Server)('./pub', { cache: env === 'production' ? 3600 : 0 }); 27 | 28 | this.server = http.createServer(function (request, response) { 29 | var body = [], log; 30 | 31 | request.addListener('data', function (chunk) { body.push(chunk) }); 32 | request.addListener('end', function () { 33 | log = [request.method, request.url, body.join('')]; 34 | 35 | // If the response hasn't completed within 5 seconds 36 | // of the request, send a 500 back. 37 | var timer = setTimeout(function () { 38 | if (! response.finished) { 39 | if (request.headers.accept.indexOf('application/json') !== -1) { 40 | response.writeHead(500, {}); 41 | response.end(JSON.stringify({error: 500})); 42 | } else { 43 | file.serveFile('/error.html', 500, {}, request, response); 44 | } 45 | } 46 | }, 5000); 47 | 48 | if (/MSIE [0-8]/.test(request.headers['user-agent'])) { // Block old IE 49 | file.serveFile('/upgrade.html', 200, {}, request, response); 50 | clearTimeout(timer); 51 | } else if (request.url === '/') { 52 | todo.create(function (id) { 53 | finish(303, { 'Location': '/' + id }); 54 | }); 55 | } else { 56 | // 57 | // Dispatch the request to the router 58 | // 59 | router.route(request, body.join(''), function (result) { 60 | if (result.status === 406) { // A request for non-json data 61 | file.serve(request, response, function (err, result) { 62 | if (err) { 63 | file.serveFile('/index.html', 200, {}, request, response); 64 | clearTimeout(timer); 65 | } 66 | }); 67 | } else { 68 | session.create(request, function (header) { 69 | if (header) { result.headers['Set-Cookie'] = header['Set-Cookie'] } 70 | finish(result.status, result.headers, result.body); 71 | }); 72 | } 73 | }); 74 | } 75 | function finish(status, headers, body) { 76 | response.writeHead(status, headers); 77 | body ? response.end(body) : response.end(); 78 | clearTimeout(timer); 79 | 80 | sys.puts([ 81 | new(Date)().toJSON(), 82 | log.join(' '), 83 | [status, http.STATUS_CODES[status], body].join(' ') 84 | ].join(' -- ')); 85 | } 86 | }); 87 | }); 88 | 89 | 90 | this.server.listen(options.port); 91 | 92 | if (env === 'production') { 93 | // Write lock file 94 | fs.writeFileSync(options.lock, process.pid.toString() + '\n', 'ascii'); 95 | } 96 | 97 | process.on('uncaughtException', function (err) { 98 | if (env === 'production') { 99 | fs.open('thinglerd.log', 'a+', 0666, function (e, fd) { 100 | var buffer = new(Buffer)(new(Date)().toUTCString() + ' -- ' + err.stack + '\n'); 101 | fs.write(fd, buffer, 0, buffer.length, null); 102 | }); 103 | } else { 104 | sys.error(err.stack); 105 | } 106 | }); 107 | process.on('exit', function () { 108 | (env === 'production') && fs.unlinkSync(options.lock); 109 | }); 110 | -------------------------------------------------------------------------------- /src/md5.js: -------------------------------------------------------------------------------- 1 | var crypto = require('crypto'); 2 | 3 | this.digest = function (str) { 4 | return crypto.createHash('md5').update(str).digest('hex'); 5 | }; 6 | -------------------------------------------------------------------------------- /src/routes.js: -------------------------------------------------------------------------------- 1 | var todo = require('./todo'), 2 | session = require('./session'), 3 | changes = require('./changes'); 4 | 5 | function auth(callback) { 6 | return function (res, id, params) { 7 | var sess = session.resource.retrieve(this.request); 8 | callback(res, id, params, sess); 9 | }; 10 | } 11 | // 12 | // Routing table 13 | // 14 | this.map = function () { 15 | // Create a new todo list 16 | this.post('/'); 17 | 18 | // List 19 | this.path(/^([a-zA-Z0-9-]+)(?:\.json)?/, function () { 20 | // Create/Destroy session 21 | this.post('/session').bind (auth(session.post)); 22 | this.del('/session').bind (auth(session.del)); 23 | 24 | // Retrieve the todo list 25 | this.get().bind (auth(todo.get)); 26 | 27 | // Update the todo list 28 | this.put().bind (auth(todo.put)); 29 | 30 | // Destroy the todo list 31 | this.del().bind (auth(todo.del)); 32 | 33 | // Create a change 34 | this.post().bind (auth(changes.post)); 35 | }); 36 | }; 37 | -------------------------------------------------------------------------------- /src/session/index.js: -------------------------------------------------------------------------------- 1 | 2 | var session = require('./session'); 3 | var todo = require('../todo').resource; 4 | var md5 = require('../md5'); 5 | 6 | this.resource = session; 7 | 8 | this.post = function (res, id, params, sess) { 9 | id = id.toString(); 10 | if (sess) { 11 | todo.get(id, function (e, doc) { 12 | if (! doc.password) { 13 | res.send(201); 14 | } else if (doc.password === md5.digest(params.password)) { 15 | session.authenticate(sess, id); 16 | res.send(201); 17 | } else { 18 | res.send(401, {}, { error: 'wrong password' }); 19 | } 20 | }); 21 | } else { 22 | res.send(401); 23 | } 24 | }; 25 | this.del = function (res, id, params) { 26 | 27 | }; 28 | -------------------------------------------------------------------------------- /src/session/session.js: -------------------------------------------------------------------------------- 1 | 2 | var uuid = require('../uuid'); 3 | 4 | // Session store 5 | var store = {}; 6 | 7 | this.store = store; 8 | 9 | // Prune un-used sessions every hour 10 | setInterval(function () { exports.prune() }, 3600); 11 | 12 | this.maxAge = 3600 * 24; // 1 day 13 | 14 | this.generate = function (callback) { 15 | uuid.generate(function (id) { 16 | store[id] = { 17 | id: id, 18 | ctime: Date.now(), 19 | atime: Date.now(), 20 | authenticated: [] 21 | }; 22 | callback(id); 23 | }); 24 | }; 25 | this.create = function (req, callback) { 26 | var id = this.extract(req); 27 | 28 | if (!id || !(id in store)) { 29 | this.generate(function (id) { 30 | callback({ 'Set-Cookie':'SESSID=' + id }); 31 | }); 32 | } else { 33 | callback(null); 34 | } 35 | }; 36 | 37 | this.extract = function (req) { 38 | if (req.headers['cookie']) { 39 | if (match = req.headers['cookie'].match(/SESSID=(\w{32})/)) { 40 | return match[1]; 41 | } else { 42 | return null; 43 | } 44 | } 45 | }; 46 | 47 | this.retrieve = function (req) { 48 | return this.get(this.extract(req)); 49 | }; 50 | 51 | this.get = function (id) { 52 | if (id in store) { 53 | store[id].atime = Date.now(); 54 | return store[id]; 55 | } else { 56 | return null; 57 | } 58 | }; 59 | 60 | this.authenticate = function (session, docId) { 61 | session.atime = Date.now(); 62 | return session.authenticated.push(docId); 63 | }; 64 | 65 | this.prune = function () { 66 | var keys = Object.keys(store); 67 | var now = Date.now(); 68 | 69 | for (var i = 0, key; i < keys.length; i++) { 70 | key = keys[i]; 71 | if (now - store[key].atime > this.maxAge) { 72 | delete(store[key]); 73 | } 74 | } 75 | }; 76 | -------------------------------------------------------------------------------- /src/todo/index.js: -------------------------------------------------------------------------------- 1 | 2 | var sys = require('sys'); 3 | 4 | var todo = require('./todo'); 5 | var db = require('../db'); 6 | 7 | this.resource = todo; 8 | 9 | // 10 | // Retrieve a list 11 | // 12 | this.get = function (res, id, params, session) { 13 | id = id.toString(); 14 | todo.get(id, function (e, doc) { 15 | if (e) { 16 | res.send(doc.headers.status, {}, e); 17 | } else { 18 | if (doc.password) { 19 | if (! session) { 20 | res.send(401, {}, { error: 'you must be logged in' }); 21 | } else if (session.authenticated.indexOf(id) === -1) { 22 | res.send(401, {}, { error: "you don't have permission to view this url" }); 23 | } else { 24 | doc.locked = true; 25 | res.send(200, {}, doc); 26 | } 27 | } else { 28 | res.send(200, {}, doc); 29 | } 30 | } 31 | }) 32 | }; 33 | 34 | // 35 | // Update a list, or create a named list 36 | // 37 | this.put = function (res, id, params) { 38 | todo.save(id.toString(), function (e, doc) { 39 | if (e) { 40 | res.send(doc.headers.status, {}, e); 41 | } else { 42 | res.send(doc.status, {}, doc); 43 | } 44 | }); 45 | }; 46 | 47 | -------------------------------------------------------------------------------- /src/todo/todo.js: -------------------------------------------------------------------------------- 1 | 2 | var uuid = require('../uuid'); 3 | 4 | require.paths.unshift(__dirname + '/..'); 5 | 6 | var db = require('db').database; 7 | 8 | var cache = {}; 9 | 10 | this.Todo = function (attributes) { 11 | this.title = "Hello, I'm a Todo List."; 12 | this.items = []; 13 | this.timestamp = new(Date)().toUTCString(); 14 | 15 | for (var k in attributes) { this[k] = attributes[k] } 16 | }; 17 | this.Todo.prototype = { 18 | get json () { 19 | var that = this; 20 | 21 | return Object.keys(this).reduce(function (json, k) { 22 | json[k] = that[k]; 23 | return json; 24 | }, {}); 25 | }, 26 | update: function (obj) { 27 | var that = this; 28 | Object.keys(obj).forEach(function (k) { 29 | that[k] = obj[k]; 30 | }) 31 | return this; 32 | }, 33 | save: function (callback) { 34 | db.put(this._id, this.json, function (e, res) { 35 | callback(e, res); 36 | }); 37 | } 38 | }; 39 | 40 | this.create = function (callback) { 41 | uuid.generate(function (id) { 42 | cache[id] = new(exports.Todo)({ _id: id }); 43 | callback(id); 44 | }); 45 | }; 46 | 47 | this.clear = function (id) { 48 | delete(cache[id]); 49 | }; 50 | 51 | this.get = function (id, callback) { 52 | process.nextTick(function () { 53 | if (id in cache) { 54 | callback(null, cache[id].json); 55 | } else { 56 | db.get(id, function (e, result) { 57 | if (e) { 58 | callback(e, result); 59 | } else { 60 | callback(null, result.json); 61 | } 62 | }); 63 | } 64 | }); 65 | }; 66 | 67 | this.save = function (id, callback) { 68 | db.get(id, function (e, doc) { 69 | var newDoc = new(exports.Todo)(); 70 | 71 | if (e && (e.error !== 'not_found')) { 72 | callback(e); 73 | } else { 74 | db.put(id, newDoc.json, function (e, doc) { 75 | if (e) { 76 | callback(e, doc); 77 | } else { 78 | callback(null, { 79 | title: newDoc.title, 80 | _rev: doc._rev, 81 | status: doc.headers.status 82 | }); 83 | } 84 | }); 85 | } 86 | }); 87 | }; 88 | -------------------------------------------------------------------------------- /src/uuid.js: -------------------------------------------------------------------------------- 1 | var db = require('./db').connection; 2 | 3 | var cache = []; 4 | 5 | this.generate = function (callback) { 6 | if (cache.length > 0) { 7 | callback(cache.pop()); 8 | } else { 9 | db.uuids(100, function (err, data) { 10 | Array.prototype.push.apply(cache, data); 11 | callback(cache.pop()); 12 | }); 13 | } 14 | }; 15 | --------------------------------------------------------------------------------