├── index.html ├── javascripts ├── application.js ├── helpers.js ├── html_renderer.js └── schema.js ├── lib ├── backbone.js ├── data.js ├── jquery-1.6.1.min.js └── underscore.js └── styles ├── reset.css └── styles.css /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Substance | Updates 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 27 | 28 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 |
63 | 67 | 68 |
69 |
70 | 71 |
72 |
73 |
74 |
75 |
76 | 77 | 80 |
81 | 82 | 83 | -------------------------------------------------------------------------------- /javascripts/application.js: -------------------------------------------------------------------------------- 1 | // DocumentBrowser 2 | // --------------- 3 | 4 | var DocumentBrowser = Backbone.View.extend({ 5 | initialize: function(options) { 6 | var that = this; 7 | 8 | }, 9 | 10 | load: function(username) { 11 | var that = this; 12 | graph.fetch({"type": "/type/document", "creator": "/user/"+username}, function(err, g) { 13 | if (err) return alert('An error occured during fetching the documents'); 14 | that.render(); 15 | }); 16 | }, 17 | 18 | render: function() { 19 | var that = this; 20 | 21 | // TODO: use this.options.query here 22 | var documents = graph.find({'type': "/type/document"}) 23 | 24 | var DESC_BY_UPDATED_AT = function(item1, item2) { 25 | var v1 = item1.value.get('updated_at'), 26 | v2 = item2.value.get('updated_at'); 27 | return v1 === v2 ? 0 : (v1 > v2 ? -1 : 1); 28 | }; 29 | 30 | documents = documents.sort(DESC_BY_UPDATED_AT); 31 | $(this.el).html(_.tpl('document_browser', { 32 | documents: documents, 33 | username: "substance" 34 | })); 35 | } 36 | }); 37 | 38 | 39 | // Document 40 | // --------------- 41 | 42 | var Document = Backbone.View.extend({ 43 | events: { 44 | 'click .toc-item': 'scrollTo' 45 | }, 46 | 47 | id: null, 48 | 49 | load: function(username, docname, node) { 50 | var that = this; 51 | 52 | // Already loaded? 53 | if (this.username === username && this.docname == docname) { 54 | that.scrollTo(node); 55 | } else { 56 | function getDocumentId() { 57 | var document = graph.find({"type": "/type/document", "creator": "/user/"+username, "name": docname}).first(); 58 | return document._id; 59 | }; 60 | 61 | graph.fetch({"type": "/type/document", "creator": "/user/"+username, "name": docname, "children": {"_recursive": true}}, function(err, nodes) { 62 | if (err) return alert('Document could not be found.'); 63 | that.id = getDocumentId(); 64 | if (that.id) { 65 | that.username = username; 66 | that.docname = docname; 67 | that.render(); 68 | app.browser.render(); // Re-render browser 69 | // Jump to node? 70 | that.scrollTo(node); 71 | } 72 | }); 73 | } 74 | }, 75 | 76 | scrollTo: function(arg) { 77 | if (!arg) return; 78 | var offset = arg.currentTarget ? $('#'+$(arg.currentTarget).attr('node')).offset() 79 | : $('#'+arg).offset(); 80 | 81 | offset ? $('html, body').animate({scrollTop: offset.top}, 'slow') : null; 82 | if (arg.currentTarget) controller.saveLocation($(arg.currentTarget).attr('href')); 83 | return false; 84 | }, 85 | 86 | initialize: function(options) { 87 | }, 88 | 89 | render: function() { 90 | if (this.id) { 91 | var doc = graph.get(this.id); 92 | 93 | $(this.el).html(_.tpl('document', { 94 | document: doc, 95 | })); 96 | // this.$('#toc').html(new TOCRenderer(doc).render()); 97 | this.$('#document_content').html(new HTMLRenderer(doc).render()); 98 | } 99 | } 100 | }); 101 | 102 | 103 | 104 | // Application 105 | // --------------- 106 | 107 | 108 | var Application = Backbone.View.extend({ 109 | 110 | events: { 111 | 'click a.load-document': 'loadDocument', 112 | 'click #browser_toggle': 'showBrowser', 113 | 'click #document_toggle': 'showDocument', 114 | 'click a.select-type': 'selectType' 115 | }, 116 | 117 | selectType: function(e) { 118 | var type = $(e.currentTarget).attr('type'); 119 | this.browser.documentType = type; 120 | this.browser.render(); 121 | return false; 122 | }, 123 | 124 | loadDocument: function(e) { 125 | app.document.load($(e.currentTarget).attr('user'), $(e.currentTarget).attr('name')); 126 | controller.saveLocation($(e.currentTarget).attr('href')); 127 | return false; 128 | }, 129 | 130 | showDocument: function() { 131 | this.toggleView('document'); 132 | }, 133 | 134 | showBrowser: function() { 135 | this.toggleView('browser'); 136 | }, 137 | 138 | initialize: function(options) { 139 | var that = this; 140 | 141 | this.view = 'browser'; 142 | 143 | that.browser = new DocumentBrowser({ 144 | el: '#browser', 145 | query: {'type': '/type/document', 'published_on!=': null} 146 | }); 147 | 148 | // Init document 149 | that.document = new Document({ 150 | el: '#document' 151 | }); 152 | }, 153 | 154 | render: function() { 155 | } 156 | }); 157 | 158 | 159 | // Application Controller 160 | // --------------- 161 | 162 | var ApplicationController = Backbone.Controller.extend({ 163 | routes: { 164 | ':username': 'load', 165 | ':username/:docname': 'load', 166 | ':username/:docname/:node': 'load' 167 | }, 168 | 169 | initialize: function() { 170 | 171 | }, 172 | 173 | load: function(username, docname, node) { 174 | if (!username) username = 'substance'; 175 | if (docname) { 176 | app.browser.load(username); 177 | app.document.load(username, docname, node); 178 | } else { 179 | // console.log('MEH'); 180 | app.browser.load(username); 181 | } 182 | } 183 | }); 184 | 185 | var app, 186 | controller, 187 | graph = new Data.Graph(schema).connect('ajax', {url: "http://substance.io/graph/"}); 188 | 189 | (function() { 190 | $(function() { 191 | // Start the browser 192 | app = new Application({el: $('#container')}); 193 | app.render(); 194 | 195 | // Register controller 196 | controller = new ApplicationController({app: app}); 197 | Backbone.history.start(); 198 | }); 199 | })(); -------------------------------------------------------------------------------- /javascripts/helpers.js: -------------------------------------------------------------------------------- 1 | // Helpers 2 | // --------------- 3 | 4 | _.tpl = function(tpl, ctx) { 5 | source = $("script[name="+tpl+"]").html(); 6 | return _.template(source, ctx); 7 | }; 8 | 9 | 10 | /** 11 | * Date.parse with progressive enhancement for ISO-8601, version 2 12 | * © 2010 Colin Snover 13 | * Released under MIT license. 14 | */ 15 | (function () { 16 | _.date = function (date) { 17 | var timestamp = Date.parse(date), minutesOffset = 0, struct; 18 | if (isNaN(timestamp) && (struct = /^(\d{4}|[+\-]\d{6})-(\d{2})-(\d{2})(?:[T ](\d{2}):(\d{2})(?::(\d{2})(?:\.(\d{3,}))?)?(?:(Z)|([+\-])(\d{2})(?::?(\d{2}))?))?/.exec(date))) { 19 | if (struct[8] !== 'Z') { 20 | minutesOffset = +struct[10] * 60 + (+struct[11]); 21 | 22 | if (struct[9] === '+') { 23 | minutesOffset = 0 - minutesOffset; 24 | } 25 | } 26 | 27 | timestamp = Date.UTC(+struct[1], +struct[2] - 1, +struct[3], +struct[4], +struct[5] + minutesOffset, +struct[6], +struct[7].substr(0, 3)); 28 | } 29 | 30 | return new Date(timestamp).toDateString(); 31 | }; 32 | }()); 33 | 34 | 35 | _.teaser = function(str) { 36 | if (!str) return ""; 37 | return str.length > 90 ? str.trim().substring(0, 89)+" ..." : str; 38 | } 39 | 40 | // _.prettyDate = function(time) { 41 | // return jQuery.timeago(time); 42 | // }; 43 | 44 | 45 | _.stripTags = function(input, allowed) { 46 | // Strips HTML and PHP tags from a string 47 | // 48 | // version: 1009.2513 49 | // discuss at: http://phpjs.org/functions/strip_tags 50 | // + original by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) 51 | // + improved by: Luke Godfrey 52 | // + input by: Pul 53 | // + bugfixed by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) 54 | // + bugfixed by: Onno Marsman 55 | // + input by: Alex 56 | // + bugfixed by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) 57 | // + input by: Marc Palau 58 | // + improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) 59 | // + input by: Brett Zamir (http://brett-zamir.me) 60 | // + bugfixed by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) 61 | // + bugfixed by: Eric Nagel 62 | // + input by: Bobby Drake 63 | // + bugfixed by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) 64 | // + bugfixed by: Tomasz Wesolowski 65 | // + input by: Evertjan Garretsen 66 | // + revised by: Rafał Kukawski (http://blog.kukawski.pl/) 67 | // * example 1: strip_tags('

Kevin

van Zonneveld', ''); 68 | // * returns 1: 'Kevin van Zonneveld' 69 | // * example 2: strip_tags('

Kevin van Zonneveld

', '

'); 70 | // * returns 2: '

Kevin van Zonneveld

' 71 | // * example 3: strip_tags("Kevin van Zonneveld", ""); 72 | // * returns 3: 'Kevin van Zonneveld' 73 | // * example 4: strip_tags('1 < 5 5 > 1'); 74 | // * returns 4: '1 < 5 5 > 1' 75 | // * example 5: strip_tags('1
1'); 76 | // * returns 5: '1 1' 77 | // * example 6: strip_tags('1
1', '
'); 78 | // * returns 6: '1 1' 79 | // * example 7: strip_tags('1
1', '

'); 80 | // * returns 7: '1
1' 81 | allowed = (((allowed || "") + "") 82 | .toLowerCase() 83 | .match(/<[a-z][a-z0-9]*>/g) || []) 84 | .join(''); // making sure the allowed arg is a string containing only tags in lowercase () 85 | var tags = /<\/?([a-z][a-z0-9]*)\b[^>]*>/gi, 86 | commentsAndPhpTags = /|<\?(?:php)?[\s\S]*?\?>/gi; 87 | return input.replace(commentsAndPhpTags, '').replace(tags, function($0, $1){ 88 | return allowed.indexOf('<' + $1.toLowerCase() + '>') > -1 ? $0 : ''; 89 | }); 90 | } -------------------------------------------------------------------------------- /javascripts/html_renderer.js: -------------------------------------------------------------------------------- 1 | // TOCRenderer 2 | // --------------- 3 | 4 | 5 | var TOCRenderer = function(root) { 6 | 7 | // Known node types 8 | var renderers = { 9 | "/type/document": function(node) { 10 | content = '

Table of contents

'; 11 | content += '
'; 16 | return content; 17 | }, 18 | 19 | "/type/article": function(node) { 20 | return renderers["/type/document"](node); 21 | }, 22 | 23 | "/type/manual": function(node) { 24 | return renderers["/type/document"](node); 25 | }, 26 | }; 27 | 28 | return { 29 | render: function() { 30 | // Traverse the document 31 | return renderers[root.type._id](root); 32 | } 33 | }; 34 | }; 35 | 36 | var HTMLRenderer = function(root) { 37 | 38 | // Implemented node types 39 | var renderers = { 40 | "/type/document": function(node) { 41 | var content = ''; 42 | 43 | node.all('children').each(function(child) { 44 | content += renderers[child.type._id](child); 45 | }); 46 | return content; 47 | }, 48 | 49 | "/type/article": function(node) { 50 | return renderers["/type/document"](node); 51 | }, 52 | 53 | "/type/manual": function(node) { 54 | return renderers["/type/document"](node); 55 | }, 56 | 57 | "/type/section": function(node) { 58 | var content = ''; 59 | content += '

' + node.get('name') + '

'; 60 | 61 | node.all('children').each(function(child) { 62 | content += renderers[child.type._id](child); 63 | }); 64 | 65 | return content; 66 | }, 67 | 68 | "/type/text": function(node) { 69 | return node.get('content'); 70 | }, 71 | 72 | "/type/question": function(node) { 73 | return '

'+node.get('content')+'

'; 74 | }, 75 | 76 | "/type/answer": function(node) { 77 | return '

'+node.get('content')+'

'; 78 | }, 79 | 80 | "/type/quote": function(node) { 81 | return ""+node.get('content')+""; 82 | }, 83 | 84 | "/type/code": function(node) { 85 | return '
'+node.get('content')+'
'; 86 | }, 87 | 88 | "/type/image": function(node) { 89 | return ''; 90 | }, 91 | "/type/resource": function(node) { 92 | return ''; 93 | } 94 | 95 | }; 96 | 97 | return { 98 | render: function() { 99 | // Traverse the document 100 | return renderers[root.type._id](root); 101 | } 102 | }; 103 | }; 104 | -------------------------------------------------------------------------------- /javascripts/schema.js: -------------------------------------------------------------------------------- 1 | var schema = { 2 | "/type/config": { 3 | "_id": "/type/config", 4 | "type": "/type/type", 5 | "name": "Configuration", 6 | "properties": { 7 | "allow_user_registration": { 8 | "name": "Allow User registration", 9 | "type": "boolean", 10 | "unique": true, 11 | "default": true 12 | }, 13 | "document_types": { 14 | "name": "Supported Document Types", 15 | "type": "string", 16 | "unique": false, 17 | "required": false 18 | } 19 | } 20 | }, 21 | 22 | "/config/substance": { 23 | "type": "/type/config", 24 | "document_types": ["/type/qaa", "/type/manual", "/type/article"], 25 | "allow_user_registration": true 26 | }, 27 | 28 | "/type/user": { 29 | "_id": "/type/user", 30 | "type": "/type/type", 31 | "name": "User", 32 | "properties": { 33 | "username": { 34 | "name": "Username", 35 | "unique": true, 36 | "type": "string", 37 | "required": true, 38 | "validator": "^[a-zA-Z_]{1}[a-zA-Z_0-9-]{2,20}$" 39 | }, 40 | "email": { 41 | "name": "Email", 42 | "unique": true, 43 | "type": "string", 44 | "required": true, 45 | "validator": "^(([^<>()[\\]\\\\.,;:\\s@\\\"]+(\\.[^<>()[\\]\\\\.,;:\\s@\\\"]+)*)|(\\\".+\\\"))@((\\[[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\])|(([a-zA-Z\\-0-9]+\\.)+[a-zA-Z]{2,}))$" 46 | }, 47 | "name": { 48 | "name": "Full Name", 49 | "unique": true, 50 | "type": "string", 51 | "required": true 52 | }, 53 | "website": { 54 | "name": "Website", 55 | "unique": true, 56 | "type": "string" 57 | }, 58 | "company": { 59 | "name": "Company", 60 | "unique": true, 61 | "type": "string" 62 | }, 63 | "location": { 64 | "name": "Location", 65 | "unique": true, 66 | "type": "string" 67 | }, 68 | "password": { 69 | "name": "Password", 70 | "unique": true, 71 | "type": "string", 72 | "required": true, 73 | "validator": "^\\w{4,}$" 74 | }, 75 | "created_at": { 76 | "name": "Created at", 77 | "unique": true, 78 | "type": "date" 79 | } 80 | } 81 | }, 82 | 83 | "/type/attribute": { 84 | "_id": "/type/attribute", 85 | "type": "/type/type", 86 | "name": "Attribute", 87 | "properties": { 88 | "name": { 89 | "name": "Attribute Value", 90 | "unique": true, 91 | "type": "string", 92 | "required": true 93 | }, 94 | "member_of": { 95 | "name": "Member of Property", 96 | "unique": true, 97 | "type": "string", 98 | "required": true 99 | } 100 | } 101 | }, 102 | 103 | "/type/event": { 104 | "_id": "/type/event", 105 | "type": "/type/type", 106 | "properties": { 107 | "event_type": { 108 | "name": "Event Type", 109 | "unique": true, 110 | "type": "string", 111 | "required": true 112 | }, 113 | "message": { 114 | "name": "Event message", 115 | "unique": true, 116 | "type": "string", 117 | "required": true 118 | }, 119 | "creator": { 120 | "name": "User causing the event", 121 | "unique": true, 122 | "type": "/type/user", 123 | "required": true 124 | }, 125 | "object": { 126 | "name": "Concerned Object", 127 | "unique": true, 128 | "type": "string", 129 | "required": true 130 | }, 131 | "link": { 132 | "name": "Link", 133 | "unique": true, 134 | "type": "string", 135 | "required": true 136 | }, 137 | "created_at": { 138 | "name": "Created at", 139 | "unique": true, 140 | "type": "date", 141 | "required": true 142 | } 143 | }, 144 | "indexes": { 145 | "by_date": ["created_at"], 146 | "by_event_type_and_date": ["event_type", "created_at"] 147 | } 148 | }, 149 | 150 | "/type/notification": { 151 | "_id": "/type/notification", 152 | "type": "/type/type", 153 | "properties": { 154 | "event": { 155 | "name": "Associated Event", 156 | "unique": true, 157 | "type": "/type/event", 158 | "required": true 159 | }, 160 | "recipient": { 161 | "name": "Recipient", 162 | "unique": true, 163 | "type": "/type/user", 164 | "required": true 165 | }, 166 | "read": { 167 | "name": "Read", 168 | "type": "boolean", 169 | "unique": true, 170 | "default": false 171 | }, 172 | "created_at": { 173 | "name": "Created at", 174 | "unique": true, 175 | "type": "date", 176 | "required": true 177 | }, 178 | "event_type": { 179 | "name": "Event Type", 180 | "unique": true, 181 | "type": "string" 182 | }, 183 | "message": { 184 | "name": "Event message", 185 | "unique": true, 186 | "type": "string" 187 | }, 188 | "link": { 189 | "name": "Link", 190 | "unique": true, 191 | "type": "string" 192 | } 193 | }, 194 | "indexes": { 195 | "by_recipient": ["recipient"], 196 | "by_date": ["created_at"], 197 | "by_recipient_and_date": ["recipient", "created_at"] 198 | } 199 | }, 200 | 201 | "/type/bookmark": { 202 | "_id": "/type/bookmark", 203 | "type": "/type/type", 204 | "properties": { 205 | "creator": { 206 | "name": "Creator", 207 | "unique": true, 208 | "type": "/type/user", 209 | "required": true, 210 | "meta": {} 211 | }, 212 | "document": { 213 | "name": "Document", 214 | "type": "/type/document", 215 | "unique": true, 216 | "required": true 217 | }, 218 | "node": { 219 | "name": "Referenced Node", 220 | "type": ["/type/text", "/type/section", "/type/quote", "/type/image", "/type/resource"], 221 | "unique": true, 222 | "required": true 223 | }, 224 | "created_at": { 225 | "name": "Created at", 226 | "unique": true, 227 | "type": "date", 228 | "required": true 229 | } 230 | }, 231 | "indexes": { 232 | "by_creator": ["creator"] 233 | } 234 | }, 235 | 236 | "/type/subscription": { 237 | "_id": "/type/subscription", 238 | "type": "/type/type", 239 | "properties": { 240 | "document": { 241 | "name": "Document", 242 | "type": "/type/document", 243 | "unique": true, 244 | "required": true 245 | }, 246 | "user": { 247 | "name": "User", 248 | "type": "/type/user", 249 | "unique": true, 250 | "required": true 251 | } 252 | }, 253 | "indexes": { 254 | "key": ["user", "document"], 255 | "by_user": ["user"] 256 | } 257 | }, 258 | 259 | "/type/comment": { 260 | "_id": "/type/comment", 261 | "type": "/type/type", 262 | "properties": { 263 | "node": { 264 | "name": "Node", 265 | "type": ["/type/section", "/type/text", "/type/image", "/type/resource", "/type/quote", "/type/code", "/type/question", "/type/answer"], 266 | "unique": true, 267 | "required": true 268 | }, 269 | "document": { 270 | "name": "Document", 271 | "type": ["/type/document"], 272 | "unique": true, 273 | "required": true 274 | }, 275 | "creator": { 276 | "name": "Creator", 277 | "type": "/type/user", 278 | "unique": true, 279 | "required": true 280 | }, 281 | "created_at": { 282 | "name": "Created at", 283 | "unique": true, 284 | "type": "date", 285 | "required": true 286 | }, 287 | "content": { 288 | "name": "Content", 289 | "type": "string", 290 | "unique": true, 291 | "required": true 292 | } 293 | }, 294 | "indexes": { 295 | "by_node": ["node"], 296 | "by_user": ["user"] 297 | } 298 | }, 299 | 300 | "/type/document": { 301 | "_id": "/type/document", 302 | "type": "/type/type", 303 | "name": "Document", 304 | "properties": { 305 | "name": { 306 | "name": "Internal name", 307 | "unique": true, 308 | "type": "string", 309 | "required": true, 310 | "validator": "^[a-zA-Z_0-9]{1}[a-zA-Z_0-9-]{2,40}$" 311 | }, 312 | "title": { 313 | "name": "Document Title", 314 | "unique": true, 315 | "type": "string", 316 | "default": "" 317 | }, 318 | "lead": { 319 | "name": "Lead", 320 | "unique": true, 321 | "type": "string", 322 | "default": "" 323 | }, 324 | "creator": { 325 | "name": "Creator", 326 | "unique": true, 327 | "type": "/type/user", 328 | "required": true, 329 | "meta": {} 330 | }, 331 | "created_at": { 332 | "name": "Created at", 333 | "unique": true, 334 | "type": "date", 335 | "required": true 336 | }, 337 | "updated_at": { 338 | "name": "Last modified", 339 | "unique": true, 340 | "type": "date", 341 | "required": true 342 | }, 343 | "published_on": { 344 | "name": "Publication Date", 345 | "unique": true, 346 | "type": "date" 347 | }, 348 | "settings": { 349 | "name": "Document Settings", 350 | "unique": true, 351 | "type": "object" 352 | }, 353 | "views": { 354 | "name": "View Count", 355 | "unique": true, 356 | "type": "number", 357 | "default": 0 358 | }, 359 | "subscribers": { 360 | "name": "Subscribers", 361 | "unique": true, 362 | "type": "number", 363 | "default": 0 364 | }, 365 | "subscribed": { 366 | "name": "Subscribed by current user (meta-attribute)", 367 | "unique": true, 368 | "type": "boolean" 369 | }, 370 | "subjects": { 371 | "type": ["/type/attribute"], 372 | "name": "Subjects", 373 | "unique": false, 374 | "default": [], 375 | "meta": { 376 | "facet": true 377 | } 378 | }, 379 | "entities": { 380 | "type": ["/type/attribute"], 381 | "name": "Entities mentioned", 382 | "unique": false, 383 | "default": [], 384 | "meta": { 385 | "facet": true 386 | } 387 | } 388 | }, 389 | "indexes": { 390 | "key": ["creator", "name"] 391 | } 392 | }, 393 | 394 | "/type/qaa": { 395 | "type": "/type/type", 396 | "name": "Q&A", 397 | "properties": { 398 | "children": { 399 | "name": "Children/Contents", 400 | "unique": false, 401 | "type": ["/type/question", "/type/answer"], 402 | "default": [] 403 | } 404 | }, 405 | "meta": { 406 | "template": { 407 | "type": ["/type/document", "/type/qaa"] 408 | } 409 | } 410 | }, 411 | 412 | "/type/manual": { 413 | "type": "/type/type", 414 | "name": "Manual", 415 | "properties": { 416 | "children": { 417 | "name": "Children/Contents", 418 | "unique": false, 419 | "type": ["/type/section"], 420 | "default": [] 421 | } 422 | }, 423 | "meta": { 424 | "template": { 425 | "type": ["/type/document", "/type/manual"] 426 | } 427 | } 428 | }, 429 | 430 | "/type/article": { 431 | "type": "/type/type", 432 | "name": "Article", 433 | "properties": { 434 | "children": { 435 | "name": "Children/Contents", 436 | "unique": false, 437 | "type": ["/type/section", "/type/text", "/type/image", "/type/resource", "/type/quote", "/type/code"], 438 | "default": [] 439 | } 440 | }, 441 | "meta": { 442 | "template": { 443 | "type": ["/type/document", "/type/article"] 444 | } 445 | } 446 | }, 447 | 448 | "/type/section": { 449 | "_id": "/type/section", 450 | "type": "/type/type", 451 | "name": "Section", 452 | "properties": { 453 | "name": { 454 | "name": "Name", 455 | "unique": true, 456 | "type": "string", 457 | "default": "" 458 | }, 459 | "document": { 460 | "name": "Document Membership", 461 | "unique": true, 462 | "required": true, 463 | "type": ["/type/document"] 464 | }, 465 | "children": { 466 | "name": "Children", 467 | "unique": false, 468 | "type": ["/type/text", "/type/image", "/type/resource", "/type/quote", "/type/code", "/type/section"], 469 | "default": [] 470 | }, 471 | "comments": { 472 | "name": "Comments", 473 | "unique": false, 474 | "type": ["/type/comment"], 475 | "default": [] 476 | } 477 | } 478 | }, 479 | 480 | "/type/text": { 481 | "_id": "/type/text", 482 | "type": "/type/type", 483 | "name": "Text", 484 | "properties": { 485 | "content": { 486 | "name": "Content", 487 | "unique": true, 488 | "type": "string", 489 | "default": "

" 490 | }, 491 | "comments": { 492 | "name": "Comments", 493 | "unique": false, 494 | "type": ["/type/comment"], 495 | "default": [] 496 | }, 497 | "document": { 498 | "name": "Document Membership", 499 | "unique": true, 500 | "required": true, 501 | "type": ["/type/document"] 502 | } 503 | } 504 | }, 505 | 506 | "/type/visualization": { 507 | "_id": "/type/visualization", 508 | "type": "/type/type", 509 | "name": "Visualization", 510 | "properties": { 511 | "data_source": { 512 | "name": "Data Source", 513 | "unique": true, 514 | "type": "string", 515 | "required": true, 516 | "default": "http://dejavis.org/files/linechart/data/countries.json" 517 | }, 518 | "visualization_type": { 519 | "name": "Visualization Type", 520 | "unique": true, 521 | "type": "string", 522 | "required": true, 523 | "default": "linechart" 524 | }, 525 | "comments": { 526 | "name": "Comments", 527 | "unique": false, 528 | "type": ["/type/comment"], 529 | "default": [] 530 | }, 531 | "document": { 532 | "name": "Document Membership", 533 | "unique": true, 534 | "required": true, 535 | "type": ["/type/document"] 536 | } 537 | } 538 | }, 539 | 540 | "/type/question": { 541 | "_id": "/type/question", 542 | "type": "/type/type", 543 | "name": "Question", 544 | "properties": { 545 | "content": { 546 | "name": "Content", 547 | "unique": true, 548 | "type": "string", 549 | "default": "" 550 | }, 551 | "document": { 552 | "name": "Document Membership", 553 | "unique": true, 554 | "required": true, 555 | "type": ["/type/document"] 556 | }, 557 | "comments": { 558 | "name": "Comments", 559 | "unique": false, 560 | "type": ["/type/comment"], 561 | "default": [] 562 | } 563 | } 564 | }, 565 | 566 | "/type/answer": { 567 | "_id": "/type/answer", 568 | "type": "/type/type", 569 | "name": "Answer", 570 | "properties": { 571 | "content": { 572 | "name": "Content", 573 | "unique": true, 574 | "type": "string", 575 | "default": "" 576 | }, 577 | "document": { 578 | "name": "Document Membership", 579 | "unique": true, 580 | "required": true, 581 | "type": ["/type/document"] 582 | }, 583 | "comments": { 584 | "name": "Comments", 585 | "unique": false, 586 | "type": ["/type/comment"], 587 | "default": [] 588 | } 589 | } 590 | }, 591 | 592 | "/type/quote": { 593 | "_id": "/type/quote", 594 | "type": "/type/type", 595 | "name": "Quote", 596 | "properties": { 597 | "author": { 598 | "name": "Quote Author", 599 | "unique": true, 600 | "type": "string", 601 | "default": "" 602 | }, 603 | "content": { 604 | "name": "Content", 605 | "unique": true, 606 | "type": "string", 607 | "default": "" 608 | }, 609 | "document": { 610 | "name": "Document Membership", 611 | "unique": true, 612 | "required": true, 613 | "type": ["/type/document"] 614 | }, 615 | "comments": { 616 | "name": "Comments", 617 | "unique": false, 618 | "type": ["/type/comment"], 619 | "default": [] 620 | } 621 | } 622 | }, 623 | 624 | "/type/code": { 625 | "_id": "/type/code", 626 | "type": "/type/type", 627 | "name": "Code", 628 | "properties": { 629 | "content": { 630 | "name": "Content", 631 | "unique": true, 632 | "type": "string", 633 | "default": "" 634 | }, 635 | "document": { 636 | "name": "Document Membership", 637 | "unique": true, 638 | "required": true, 639 | "type": ["/type/document"] 640 | }, 641 | "comments": { 642 | "name": "Comments", 643 | "unique": false, 644 | "type": ["/type/comment"], 645 | "default": [] 646 | } 647 | } 648 | }, 649 | 650 | "/type/image": { 651 | "_id": "/type/image", 652 | "type": "/type/type", 653 | "name": "Image", 654 | "properties": { 655 | "caption": { 656 | "name": "Image Caption", 657 | "unique": true, 658 | "type": "string" 659 | }, 660 | "url": { 661 | "name": "Image URL", 662 | "unique": true, 663 | "type": "string" 664 | }, 665 | "original_url": { 666 | "name": "Original Image URL", 667 | "unique": true, 668 | "type": "string" 669 | }, 670 | "document": { 671 | "name": "Document Membership", 672 | "unique": true, 673 | "required": true, 674 | "type": ["/type/document"] 675 | }, 676 | "comments": { 677 | "name": "Comments", 678 | "unique": false, 679 | "type": ["/type/comment"], 680 | "default": [] 681 | } 682 | } 683 | }, 684 | 685 | "/type/resource": { 686 | "_id": "/type/image", 687 | "type": "/type/type", 688 | "name": "Resource", 689 | "properties": { 690 | "caption": { 691 | "name": "Caption", 692 | "unique": true, 693 | "type": "string" 694 | }, 695 | "url": { 696 | "name": "Resource URL", 697 | "unique": true, 698 | "type": "string" 699 | }, 700 | "document": { 701 | "name": "Document Membership", 702 | "unique": true, 703 | "required": true, 704 | "type": ["/type/document"] 705 | }, 706 | "comments": { 707 | "name": "Comments", 708 | "unique": false, 709 | "type": ["/type/comment"], 710 | "default": [] 711 | } 712 | } 713 | } 714 | }; -------------------------------------------------------------------------------- /lib/backbone.js: -------------------------------------------------------------------------------- 1 | // Backbone.js 0.3.3 2 | // (c) 2010 Jeremy Ashkenas, DocumentCloud Inc. 3 | // Backbone may be freely distributed under the MIT license. 4 | // For all details and documentation: 5 | // http://documentcloud.github.com/backbone 6 | 7 | (function(){ 8 | 9 | // Initial Setup 10 | // ------------- 11 | 12 | // The top-level namespace. All public Backbone classes and modules will 13 | // be attached to this. Exported for both CommonJS and the browser. 14 | var Backbone; 15 | if (typeof exports !== 'undefined') { 16 | Backbone = exports; 17 | } else { 18 | Backbone = this.Backbone = {}; 19 | } 20 | 21 | // Current version of the library. Keep in sync with `package.json`. 22 | Backbone.VERSION = '0.3.3'; 23 | 24 | // Require Underscore, if we're on the server, and it's not already present. 25 | var _ = this._; 26 | if (!_ && (typeof require !== 'undefined')) _ = require("underscore")._; 27 | 28 | // For Backbone's purposes, either jQuery or Zepto owns the `$` variable. 29 | var $ = this.jQuery || this.Zepto; 30 | 31 | // Turn on `emulateHTTP` to use support legacy HTTP servers. Setting this option will 32 | // fake `"PUT"` and `"DELETE"` requests via the `_method` parameter and set a 33 | // `X-Http-Method-Override` header. 34 | Backbone.emulateHTTP = false; 35 | 36 | // Turn on `emulateJSON` to support legacy servers that can't deal with direct 37 | // `application/json` requests ... will encode the body as 38 | // `application/x-www-form-urlencoded` instead and will send the model in a 39 | // form param named `model`. 40 | Backbone.emulateJSON = false; 41 | 42 | // Backbone.Events 43 | // ----------------- 44 | 45 | // A module that can be mixed in to *any object* in order to provide it with 46 | // custom events. You may `bind` or `unbind` a callback function to an event; 47 | // `trigger`-ing an event fires all callbacks in succession. 48 | // 49 | // var object = {}; 50 | // _.extend(object, Backbone.Events); 51 | // object.bind('expand', function(){ alert('expanded'); }); 52 | // object.trigger('expand'); 53 | // 54 | Backbone.Events = { 55 | 56 | // Bind an event, specified by a string name, `ev`, to a `callback` function. 57 | // Passing `"all"` will bind the callback to all events fired. 58 | bind : function(ev, callback) { 59 | var calls = this._callbacks || (this._callbacks = {}); 60 | var list = this._callbacks[ev] || (this._callbacks[ev] = []); 61 | list.push(callback); 62 | return this; 63 | }, 64 | 65 | // Remove one or many callbacks. If `callback` is null, removes all 66 | // callbacks for the event. If `ev` is null, removes all bound callbacks 67 | // for all events. 68 | unbind : function(ev, callback) { 69 | var calls; 70 | if (!ev) { 71 | this._callbacks = {}; 72 | } else if (calls = this._callbacks) { 73 | if (!callback) { 74 | calls[ev] = []; 75 | } else { 76 | var list = calls[ev]; 77 | if (!list) return this; 78 | for (var i = 0, l = list.length; i < l; i++) { 79 | if (callback === list[i]) { 80 | list.splice(i, 1); 81 | break; 82 | } 83 | } 84 | } 85 | } 86 | return this; 87 | }, 88 | 89 | // Trigger an event, firing all bound callbacks. Callbacks are passed the 90 | // same arguments as `trigger` is, apart from the event name. 91 | // Listening for `"all"` passes the true event name as the first argument. 92 | trigger : function(ev) { 93 | var list, calls, i, l; 94 | if (!(calls = this._callbacks)) return this; 95 | if (list = calls[ev]) { 96 | for (i = 0, l = list.length; i < l; i++) { 97 | list[i].apply(this, Array.prototype.slice.call(arguments, 1)); 98 | } 99 | } 100 | if (list = calls['all']) { 101 | for (i = 0, l = list.length; i < l; i++) { 102 | list[i].apply(this, arguments); 103 | } 104 | } 105 | return this; 106 | } 107 | 108 | }; 109 | 110 | // Backbone.Model 111 | // -------------- 112 | 113 | // Create a new model, with defined attributes. A client id (`cid`) 114 | // is automatically generated and assigned for you. 115 | Backbone.Model = function(attributes, options) { 116 | attributes || (attributes = {}); 117 | if (this.defaults) attributes = _.extend({}, this.defaults, attributes); 118 | this.attributes = {}; 119 | this._escapedAttributes = {}; 120 | this.cid = _.uniqueId('c'); 121 | this.set(attributes, {silent : true}); 122 | this._previousAttributes = _.clone(this.attributes); 123 | if (options && options.collection) this.collection = options.collection; 124 | this.initialize(attributes, options); 125 | }; 126 | 127 | // Attach all inheritable methods to the Model prototype. 128 | _.extend(Backbone.Model.prototype, Backbone.Events, { 129 | 130 | // A snapshot of the model's previous attributes, taken immediately 131 | // after the last `"change"` event was fired. 132 | _previousAttributes : null, 133 | 134 | // Has the item been changed since the last `"change"` event? 135 | _changed : false, 136 | 137 | // Initialize is an empty function by default. Override it with your own 138 | // initialization logic. 139 | initialize : function(){}, 140 | 141 | // Return a copy of the model's `attributes` object. 142 | toJSON : function() { 143 | return _.clone(this.attributes); 144 | }, 145 | 146 | // Get the value of an attribute. 147 | get : function(attr) { 148 | return this.attributes[attr]; 149 | }, 150 | 151 | // Get the HTML-escaped value of an attribute. 152 | escape : function(attr) { 153 | var html; 154 | if (html = this._escapedAttributes[attr]) return html; 155 | var val = this.attributes[attr]; 156 | return this._escapedAttributes[attr] = escapeHTML(val == null ? '' : val); 157 | }, 158 | 159 | // Set a hash of model attributes on the object, firing `"change"` unless you 160 | // choose to silence it. 161 | set : function(attrs, options) { 162 | 163 | // Extract attributes and options. 164 | options || (options = {}); 165 | if (!attrs) return this; 166 | if (attrs.attributes) attrs = attrs.attributes; 167 | var now = this.attributes, escaped = this._escapedAttributes; 168 | 169 | // Run validation. 170 | if (!options.silent && this.validate && !this._performValidation(attrs, options)) return false; 171 | 172 | // Check for changes of `id`. 173 | if ('id' in attrs) this.id = attrs.id; 174 | 175 | // Update attributes. 176 | for (var attr in attrs) { 177 | var val = attrs[attr]; 178 | if (!_.isEqual(now[attr], val)) { 179 | now[attr] = val; 180 | delete escaped[attr]; 181 | if (!options.silent) { 182 | this._changed = true; 183 | this.trigger('change:' + attr, this, val, options); 184 | } 185 | } 186 | } 187 | 188 | // Fire the `"change"` event, if the model has been changed. 189 | if (!options.silent && this._changed) this.change(options); 190 | return this; 191 | }, 192 | 193 | // Remove an attribute from the model, firing `"change"` unless you choose 194 | // to silence it. 195 | unset : function(attr, options) { 196 | options || (options = {}); 197 | var value = this.attributes[attr]; 198 | 199 | // Run validation. 200 | var validObj = {}; 201 | validObj[attr] = void 0; 202 | if (!options.silent && this.validate && !this._performValidation(validObj, options)) return false; 203 | 204 | // Remove the attribute. 205 | delete this.attributes[attr]; 206 | delete this._escapedAttributes[attr]; 207 | if (!options.silent) { 208 | this._changed = true; 209 | this.trigger('change:' + attr, this, void 0, options); 210 | this.change(options); 211 | } 212 | return this; 213 | }, 214 | 215 | // Clear all attributes on the model, firing `"change"` unless you choose 216 | // to silence it. 217 | clear : function(options) { 218 | options || (options = {}); 219 | var old = this.attributes; 220 | 221 | // Run validation. 222 | var validObj = {}; 223 | for (attr in old) validObj[attr] = void 0; 224 | if (!options.silent && this.validate && !this._performValidation(validObj, options)) return false; 225 | 226 | this.attributes = {}; 227 | this._escapedAttributes = {}; 228 | if (!options.silent) { 229 | this._changed = true; 230 | for (attr in old) { 231 | this.trigger('change:' + attr, this, void 0, options); 232 | } 233 | this.change(options); 234 | } 235 | return this; 236 | }, 237 | 238 | // Fetch the model from the server. If the server's representation of the 239 | // model differs from its current attributes, they will be overriden, 240 | // triggering a `"change"` event. 241 | fetch : function(options) { 242 | options || (options = {}); 243 | var model = this; 244 | var success = function(resp) { 245 | if (!model.set(model.parse(resp), options)) return false; 246 | if (options.success) options.success(model, resp); 247 | }; 248 | var error = wrapError(options.error, model, options); 249 | (this.sync || Backbone.sync)('read', this, success, error); 250 | return this; 251 | }, 252 | 253 | // Set a hash of model attributes, and sync the model to the server. 254 | // If the server returns an attributes hash that differs, the model's 255 | // state will be `set` again. 256 | save : function(attrs, options) { 257 | options || (options = {}); 258 | if (attrs && !this.set(attrs, options)) return false; 259 | var model = this; 260 | var success = function(resp) { 261 | if (!model.set(model.parse(resp), options)) return false; 262 | if (options.success) options.success(model, resp); 263 | }; 264 | var error = wrapError(options.error, model, options); 265 | var method = this.isNew() ? 'create' : 'update'; 266 | (this.sync || Backbone.sync)(method, this, success, error); 267 | return this; 268 | }, 269 | 270 | // Destroy this model on the server. Upon success, the model is removed 271 | // from its collection, if it has one. 272 | destroy : function(options) { 273 | options || (options = {}); 274 | var model = this; 275 | var success = function(resp) { 276 | if (model.collection) model.collection.remove(model); 277 | if (options.success) options.success(model, resp); 278 | }; 279 | var error = wrapError(options.error, model, options); 280 | (this.sync || Backbone.sync)('delete', this, success, error); 281 | return this; 282 | }, 283 | 284 | // Default URL for the model's representation on the server -- if you're 285 | // using Backbone's restful methods, override this to change the endpoint 286 | // that will be called. 287 | url : function() { 288 | var base = getUrl(this.collection); 289 | if (this.isNew()) return base; 290 | return base + (base.charAt(base.length - 1) == '/' ? '' : '/') + this.id; 291 | }, 292 | 293 | // **parse** converts a response into the hash of attributes to be `set` on 294 | // the model. The default implementation is just to pass the response along. 295 | parse : function(resp) { 296 | return resp; 297 | }, 298 | 299 | // Create a new model with identical attributes to this one. 300 | clone : function() { 301 | return new this.constructor(this); 302 | }, 303 | 304 | // A model is new if it has never been saved to the server, and has a negative 305 | // ID. 306 | isNew : function() { 307 | return !this.id; 308 | }, 309 | 310 | // Call this method to manually fire a `change` event for this model. 311 | // Calling this will cause all objects observing the model to update. 312 | change : function(options) { 313 | this.trigger('change', this, options); 314 | this._previousAttributes = _.clone(this.attributes); 315 | this._changed = false; 316 | }, 317 | 318 | // Determine if the model has changed since the last `"change"` event. 319 | // If you specify an attribute name, determine if that attribute has changed. 320 | hasChanged : function(attr) { 321 | if (attr) return this._previousAttributes[attr] != this.attributes[attr]; 322 | return this._changed; 323 | }, 324 | 325 | // Return an object containing all the attributes that have changed, or false 326 | // if there are no changed attributes. Useful for determining what parts of a 327 | // view need to be updated and/or what attributes need to be persisted to 328 | // the server. 329 | changedAttributes : function(now) { 330 | now || (now = this.attributes); 331 | var old = this._previousAttributes; 332 | var changed = false; 333 | for (var attr in now) { 334 | if (!_.isEqual(old[attr], now[attr])) { 335 | changed = changed || {}; 336 | changed[attr] = now[attr]; 337 | } 338 | } 339 | return changed; 340 | }, 341 | 342 | // Get the previous value of an attribute, recorded at the time the last 343 | // `"change"` event was fired. 344 | previous : function(attr) { 345 | if (!attr || !this._previousAttributes) return null; 346 | return this._previousAttributes[attr]; 347 | }, 348 | 349 | // Get all of the attributes of the model at the time of the previous 350 | // `"change"` event. 351 | previousAttributes : function() { 352 | return _.clone(this._previousAttributes); 353 | }, 354 | 355 | // Run validation against a set of incoming attributes, returning `true` 356 | // if all is well. If a specific `error` callback has been passed, 357 | // call that instead of firing the general `"error"` event. 358 | _performValidation : function(attrs, options) { 359 | var error = this.validate(attrs); 360 | if (error) { 361 | if (options.error) { 362 | options.error(this, error); 363 | } else { 364 | this.trigger('error', this, error, options); 365 | } 366 | return false; 367 | } 368 | return true; 369 | } 370 | 371 | }); 372 | 373 | // Backbone.Collection 374 | // ------------------- 375 | 376 | // Provides a standard collection class for our sets of models, ordered 377 | // or unordered. If a `comparator` is specified, the Collection will maintain 378 | // its models in sort order, as they're added and removed. 379 | Backbone.Collection = function(models, options) { 380 | options || (options = {}); 381 | if (options.comparator) { 382 | this.comparator = options.comparator; 383 | delete options.comparator; 384 | } 385 | this._boundOnModelEvent = _.bind(this._onModelEvent, this); 386 | this._reset(); 387 | if (models) this.refresh(models, {silent: true}); 388 | this.initialize(models, options); 389 | }; 390 | 391 | // Define the Collection's inheritable methods. 392 | _.extend(Backbone.Collection.prototype, Backbone.Events, { 393 | 394 | // The default model for a collection is just a **Backbone.Model**. 395 | // This should be overridden in most cases. 396 | model : Backbone.Model, 397 | 398 | // Initialize is an empty function by default. Override it with your own 399 | // initialization logic. 400 | initialize : function(){}, 401 | 402 | // The JSON representation of a Collection is an array of the 403 | // models' attributes. 404 | toJSON : function() { 405 | return this.map(function(model){ return model.toJSON(); }); 406 | }, 407 | 408 | // Add a model, or list of models to the set. Pass **silent** to avoid 409 | // firing the `added` event for every new model. 410 | add : function(models, options) { 411 | if (_.isArray(models)) { 412 | for (var i = 0, l = models.length; i < l; i++) { 413 | this._add(models[i], options); 414 | } 415 | } else { 416 | this._add(models, options); 417 | } 418 | return this; 419 | }, 420 | 421 | // Remove a model, or a list of models from the set. Pass silent to avoid 422 | // firing the `removed` event for every model removed. 423 | remove : function(models, options) { 424 | if (_.isArray(models)) { 425 | for (var i = 0, l = models.length; i < l; i++) { 426 | this._remove(models[i], options); 427 | } 428 | } else { 429 | this._remove(models, options); 430 | } 431 | return this; 432 | }, 433 | 434 | // Get a model from the set by id. 435 | get : function(id) { 436 | if (id == null) return null; 437 | return this._byId[id.id != null ? id.id : id]; 438 | }, 439 | 440 | // Get a model from the set by client id. 441 | getByCid : function(cid) { 442 | return cid && this._byCid[cid.cid || cid]; 443 | }, 444 | 445 | // Get the model at the given index. 446 | at: function(index) { 447 | return this.models[index]; 448 | }, 449 | 450 | // Force the collection to re-sort itself. You don't need to call this under normal 451 | // circumstances, as the set will maintain sort order as each item is added. 452 | sort : function(options) { 453 | options || (options = {}); 454 | if (!this.comparator) throw new Error('Cannot sort a set without a comparator'); 455 | this.models = this.sortBy(this.comparator); 456 | if (!options.silent) this.trigger('refresh', this, options); 457 | return this; 458 | }, 459 | 460 | // Pluck an attribute from each model in the collection. 461 | pluck : function(attr) { 462 | return _.map(this.models, function(model){ return model.get(attr); }); 463 | }, 464 | 465 | // When you have more items than you want to add or remove individually, 466 | // you can refresh the entire set with a new list of models, without firing 467 | // any `added` or `removed` events. Fires `refresh` when finished. 468 | refresh : function(models, options) { 469 | models || (models = []); 470 | options || (options = {}); 471 | this._reset(); 472 | this.add(models, {silent: true}); 473 | if (!options.silent) this.trigger('refresh', this, options); 474 | return this; 475 | }, 476 | 477 | // Fetch the default set of models for this collection, refreshing the 478 | // collection when they arrive. 479 | fetch : function(options) { 480 | options || (options = {}); 481 | var collection = this; 482 | var success = function(resp) { 483 | collection.refresh(collection.parse(resp)); 484 | if (options.success) options.success(collection, resp); 485 | }; 486 | var error = wrapError(options.error, collection, options); 487 | (this.sync || Backbone.sync)('read', this, success, error); 488 | return this; 489 | }, 490 | 491 | // Create a new instance of a model in this collection. After the model 492 | // has been created on the server, it will be added to the collection. 493 | create : function(model, options) { 494 | var coll = this; 495 | options || (options = {}); 496 | if (!(model instanceof Backbone.Model)) { 497 | model = new this.model(model, {collection: coll}); 498 | } else { 499 | model.collection = coll; 500 | } 501 | var success = function(nextModel, resp) { 502 | coll.add(nextModel); 503 | if (options.success) options.success(nextModel, resp); 504 | }; 505 | return model.save(null, {success : success, error : options.error}); 506 | }, 507 | 508 | // **parse** converts a response into a list of models to be added to the 509 | // collection. The default implementation is just to pass it through. 510 | parse : function(resp) { 511 | return resp; 512 | }, 513 | 514 | // Proxy to _'s chain. Can't be proxied the same way the rest of the 515 | // underscore methods are proxied because it relies on the underscore 516 | // constructor. 517 | chain: function () { 518 | return _(this.models).chain(); 519 | }, 520 | 521 | // Reset all internal state. Called when the collection is refreshed. 522 | _reset : function(options) { 523 | this.length = 0; 524 | this.models = []; 525 | this._byId = {}; 526 | this._byCid = {}; 527 | }, 528 | 529 | // Internal implementation of adding a single model to the set, updating 530 | // hash indexes for `id` and `cid` lookups. 531 | _add : function(model, options) { 532 | options || (options = {}); 533 | if (!(model instanceof Backbone.Model)) { 534 | model = new this.model(model, {collection: this}); 535 | } 536 | var already = this.getByCid(model); 537 | if (already) throw new Error(["Can't add the same model to a set twice", already.id]); 538 | this._byId[model.id] = model; 539 | this._byCid[model.cid] = model; 540 | model.collection = this; 541 | var index = this.comparator ? this.sortedIndex(model, this.comparator) : this.length; 542 | this.models.splice(index, 0, model); 543 | model.bind('all', this._boundOnModelEvent); 544 | this.length++; 545 | if (!options.silent) model.trigger('add', model, this, options); 546 | return model; 547 | }, 548 | 549 | // Internal implementation of removing a single model from the set, updating 550 | // hash indexes for `id` and `cid` lookups. 551 | _remove : function(model, options) { 552 | options || (options = {}); 553 | model = this.getByCid(model) || this.get(model); 554 | if (!model) return null; 555 | delete this._byId[model.id]; 556 | delete this._byCid[model.cid]; 557 | delete model.collection; 558 | this.models.splice(this.indexOf(model), 1); 559 | this.length--; 560 | if (!options.silent) model.trigger('remove', model, this, options); 561 | model.unbind('all', this._boundOnModelEvent); 562 | return model; 563 | }, 564 | 565 | // Internal method called every time a model in the set fires an event. 566 | // Sets need to update their indexes when models change ids. All other 567 | // events simply proxy through. 568 | _onModelEvent : function(ev, model) { 569 | if (ev === 'change:id') { 570 | delete this._byId[model.previous('id')]; 571 | this._byId[model.id] = model; 572 | } 573 | this.trigger.apply(this, arguments); 574 | } 575 | 576 | }); 577 | 578 | // Underscore methods that we want to implement on the Collection. 579 | var methods = ['forEach', 'each', 'map', 'reduce', 'reduceRight', 'find', 'detect', 580 | 'filter', 'select', 'reject', 'every', 'all', 'some', 'any', 'include', 581 | 'invoke', 'max', 'min', 'sortBy', 'sortedIndex', 'toArray', 'size', 582 | 'first', 'rest', 'last', 'without', 'indexOf', 'lastIndexOf', 'isEmpty']; 583 | 584 | // Mix in each Underscore method as a proxy to `Collection#models`. 585 | _.each(methods, function(method) { 586 | Backbone.Collection.prototype[method] = function() { 587 | return _[method].apply(_, [this.models].concat(_.toArray(arguments))); 588 | }; 589 | }); 590 | 591 | // Backbone.Controller 592 | // ------------------- 593 | 594 | // Controllers map faux-URLs to actions, and fire events when routes are 595 | // matched. Creating a new one sets its `routes` hash, if not set statically. 596 | Backbone.Controller = function(options) { 597 | options || (options = {}); 598 | if (options.routes) this.routes = options.routes; 599 | this._bindRoutes(); 600 | this.initialize(options); 601 | }; 602 | 603 | // Cached regular expressions for matching named param parts and splatted 604 | // parts of route strings. 605 | var namedParam = /:([\w\d]+)/g; 606 | var splatParam = /\*([\w\d]+)/g; 607 | 608 | // Set up all inheritable **Backbone.Controller** properties and methods. 609 | _.extend(Backbone.Controller.prototype, Backbone.Events, { 610 | 611 | // Initialize is an empty function by default. Override it with your own 612 | // initialization logic. 613 | initialize : function(){}, 614 | 615 | // Manually bind a single named route to a callback. For example: 616 | // 617 | // this.route('search/:query/p:num', 'search', function(query, num) { 618 | // ... 619 | // }); 620 | // 621 | route : function(route, name, callback) { 622 | Backbone.history || (Backbone.history = new Backbone.History); 623 | if (!_.isRegExp(route)) route = this._routeToRegExp(route); 624 | Backbone.history.route(route, _.bind(function(fragment) { 625 | var args = this._extractParameters(route, fragment); 626 | callback.apply(this, args); 627 | this.trigger.apply(this, ['route:' + name].concat(args)); 628 | }, this)); 629 | }, 630 | 631 | // Simple proxy to `Backbone.history` to save a fragment into the history, 632 | // without triggering routes. 633 | saveLocation : function(fragment) { 634 | Backbone.history.saveLocation(fragment); 635 | }, 636 | 637 | // Bind all defined routes to `Backbone.history`. 638 | _bindRoutes : function() { 639 | if (!this.routes) return; 640 | for (var route in this.routes) { 641 | var name = this.routes[route]; 642 | this.route(route, name, this[name]); 643 | } 644 | }, 645 | 646 | // Convert a route string into a regular expression, suitable for matching 647 | // against the current location fragment. 648 | _routeToRegExp : function(route) { 649 | route = route.replace(namedParam, "([^\/]*)").replace(splatParam, "(.*?)"); 650 | return new RegExp('^' + route + '$'); 651 | }, 652 | 653 | // Given a route, and a URL fragment that it matches, return the array of 654 | // extracted parameters. 655 | _extractParameters : function(route, fragment) { 656 | return route.exec(fragment).slice(1); 657 | } 658 | 659 | }); 660 | 661 | // Backbone.History 662 | // ---------------- 663 | 664 | // Handles cross-browser history management, based on URL hashes. If the 665 | // browser does not support `onhashchange`, falls back to polling. 666 | Backbone.History = function() { 667 | this.handlers = []; 668 | this.fragment = this.getFragment(); 669 | _.bindAll(this, 'checkUrl'); 670 | }; 671 | 672 | // Cached regex for cleaning hashes. 673 | var hashStrip = /^#*/; 674 | 675 | // Set up all inheritable **Backbone.History** properties and methods. 676 | _.extend(Backbone.History.prototype, { 677 | 678 | // The default interval to poll for hash changes, if necessary, is 679 | // twenty times a second. 680 | interval: 50, 681 | 682 | // Get the cross-browser normalized URL fragment. 683 | getFragment : function(loc) { 684 | return (loc || window.location).hash.replace(hashStrip, ''); 685 | }, 686 | 687 | // Start the hash change handling, returning `true` if the current URL matches 688 | // an existing route, and `false` otherwise. 689 | start : function() { 690 | var docMode = document.documentMode; 691 | var oldIE = ($.browser.msie && (!docMode || docMode <= 7)); 692 | if (oldIE) { 693 | this.iframe = $('