├── 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 += '';
12 | node.all('children').each(function(child) {
13 | content += '- '+child.get('name')+'
';
14 | });
15 | 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 = $('').hide().appendTo('body')[0].contentWindow;
694 | }
695 | if ('onhashchange' in window && !oldIE) {
696 | $(window).bind('hashchange', this.checkUrl);
697 | } else {
698 | setInterval(this.checkUrl, this.interval);
699 | }
700 | return this.loadUrl();
701 | },
702 |
703 | // Add a route to be tested when the hash changes. Routes are matched in the
704 | // order they are added.
705 | route : function(route, callback) {
706 | this.handlers.push({route : route, callback : callback});
707 | },
708 |
709 | // Checks the current URL to see if it has changed, and if it has,
710 | // calls `loadUrl`, normalizing across the hidden iframe.
711 | checkUrl : function() {
712 | var current = this.getFragment();
713 | if (current == this.fragment && this.iframe) {
714 | current = this.getFragment(this.iframe.location);
715 | }
716 | if (current == this.fragment ||
717 | current == decodeURIComponent(this.fragment)) return false;
718 | if (this.iframe) {
719 | window.location.hash = this.iframe.location.hash = current;
720 | }
721 | this.loadUrl();
722 | },
723 |
724 | // Attempt to load the current URL fragment. If a route succeeds with a
725 | // match, returns `true`. If no defined routes matches the fragment,
726 | // returns `false`.
727 | loadUrl : function() {
728 | var fragment = this.fragment = this.getFragment();
729 | var matched = _.any(this.handlers, function(handler) {
730 | if (handler.route.test(fragment)) {
731 | handler.callback(fragment);
732 | return true;
733 | }
734 | });
735 | return matched;
736 | },
737 |
738 | // Save a fragment into the hash history. You are responsible for properly
739 | // URL-encoding the fragment in advance. This does not trigger
740 | // a `hashchange` event.
741 | saveLocation : function(fragment) {
742 | fragment = (fragment || '').replace(hashStrip, '');
743 | if (this.fragment == fragment) return;
744 | window.location.hash = this.fragment = fragment;
745 | if (this.iframe && (fragment != this.getFragment(this.iframe.location))) {
746 | this.iframe.document.open().close();
747 | this.iframe.location.hash = fragment;
748 | }
749 | }
750 |
751 | });
752 |
753 | // Backbone.View
754 | // -------------
755 |
756 | // Creating a Backbone.View creates its initial element outside of the DOM,
757 | // if an existing element is not provided...
758 | Backbone.View = function(options) {
759 | this._configure(options || {});
760 | this._ensureElement();
761 | this.delegateEvents();
762 | this.initialize(options);
763 | };
764 |
765 | // Element lookup, scoped to DOM elements within the current view.
766 | // This should be prefered to global lookups, if you're dealing with
767 | // a specific view.
768 | var selectorDelegate = function(selector) {
769 | return $(selector, this.el);
770 | };
771 |
772 | // Cached regex to split keys for `delegate`.
773 | var eventSplitter = /^(\w+)\s*(.*)$/;
774 |
775 | // Set up all inheritable **Backbone.View** properties and methods.
776 | _.extend(Backbone.View.prototype, Backbone.Events, {
777 |
778 | // The default `tagName` of a View's element is `"div"`.
779 | tagName : 'div',
780 |
781 | // Attach the `selectorDelegate` function as the `$` property.
782 | $ : selectorDelegate,
783 |
784 | // Initialize is an empty function by default. Override it with your own
785 | // initialization logic.
786 | initialize : function(){},
787 |
788 | // **render** is the core function that your view should override, in order
789 | // to populate its element (`this.el`), with the appropriate HTML. The
790 | // convention is for **render** to always return `this`.
791 | render : function() {
792 | return this;
793 | },
794 |
795 | // Remove this view from the DOM. Note that the view isn't present in the
796 | // DOM by default, so calling this method may be a no-op.
797 | remove : function() {
798 | $(this.el).remove();
799 | return this;
800 | },
801 |
802 | // For small amounts of DOM Elements, where a full-blown template isn't
803 | // needed, use **make** to manufacture elements, one at a time.
804 | //
805 | // var el = this.make('li', {'class': 'row'}, this.model.escape('title'));
806 | //
807 | make : function(tagName, attributes, content) {
808 | var el = document.createElement(tagName);
809 | if (attributes) $(el).attr(attributes);
810 | if (content) $(el).html(content);
811 | return el;
812 | },
813 |
814 | // Set callbacks, where `this.callbacks` is a hash of
815 | //
816 | // *{"event selector": "callback"}*
817 | //
818 | // {
819 | // 'mousedown .title': 'edit',
820 | // 'click .button': 'save'
821 | // }
822 | //
823 | // pairs. Callbacks will be bound to the view, with `this` set properly.
824 | // Uses event delegation for efficiency.
825 | // Omitting the selector binds the event to `this.el`.
826 | // This only works for delegate-able events: not `focus`, `blur`, and
827 | // not `change`, `submit`, and `reset` in Internet Explorer.
828 | delegateEvents : function(events) {
829 | if (!(events || (events = this.events))) return;
830 | $(this.el).unbind();
831 | for (var key in events) {
832 | var methodName = events[key];
833 | var match = key.match(eventSplitter);
834 | var eventName = match[1], selector = match[2];
835 | var method = _.bind(this[methodName], this);
836 | if (selector === '') {
837 | $(this.el).bind(eventName, method);
838 | } else {
839 | $(this.el).delegate(selector, eventName, method);
840 | }
841 | }
842 | },
843 |
844 | // Performs the initial configuration of a View with a set of options.
845 | // Keys with special meaning *(model, collection, id, className)*, are
846 | // attached directly to the view.
847 | _configure : function(options) {
848 | if (this.options) options = _.extend({}, this.options, options);
849 | if (options.model) this.model = options.model;
850 | if (options.collection) this.collection = options.collection;
851 | if (options.el) this.el = options.el;
852 | if (options.id) this.id = options.id;
853 | if (options.className) this.className = options.className;
854 | if (options.tagName) this.tagName = options.tagName;
855 | this.options = options;
856 | },
857 |
858 | // Ensure that the View has a DOM element to render into.
859 | _ensureElement : function() {
860 | if (this.el) return;
861 | var attrs = {};
862 | if (this.id) attrs.id = this.id;
863 | if (this.className) attrs["class"] = this.className;
864 | this.el = this.make(this.tagName, attrs);
865 | }
866 |
867 | });
868 |
869 | // The self-propagating extend function that Backbone classes use.
870 | var extend = function (protoProps, classProps) {
871 | var child = inherits(this, protoProps, classProps);
872 | child.extend = extend;
873 | return child;
874 | };
875 |
876 | // Set up inheritance for the model, collection, and view.
877 | Backbone.Model.extend = Backbone.Collection.extend =
878 | Backbone.Controller.extend = Backbone.View.extend = extend;
879 |
880 | // Map from CRUD to HTTP for our default `Backbone.sync` implementation.
881 | var methodMap = {
882 | 'create': 'POST',
883 | 'update': 'PUT',
884 | 'delete': 'DELETE',
885 | 'read' : 'GET'
886 | };
887 |
888 | // Backbone.sync
889 | // -------------
890 |
891 | // Override this function to change the manner in which Backbone persists
892 | // models to the server. You will be passed the type of request, and the
893 | // model in question. By default, uses makes a RESTful Ajax request
894 | // to the model's `url()`. Some possible customizations could be:
895 | //
896 | // * Use `setTimeout` to batch rapid-fire updates into a single request.
897 | // * Send up the models as XML instead of JSON.
898 | // * Persist models via WebSockets instead of Ajax.
899 | //
900 | // Turn on `Backbone.emulateHTTP` in order to send `PUT` and `DELETE` requests
901 | // as `POST`, with a `_method` parameter containing the true HTTP method,
902 | // as well as all requests with the body as `application/x-www-form-urlencoded` instead of
903 | // `application/json` with the model in a param named `model`.
904 | // Useful when interfacing with server-side languages like **PHP** that make
905 | // it difficult to read the body of `PUT` requests.
906 | Backbone.sync = function(method, model, success, error) {
907 | var type = methodMap[method];
908 | var modelJSON = (method === 'create' || method === 'update') ?
909 | JSON.stringify(model.toJSON()) : null;
910 |
911 | // Default JSON-request options.
912 | var params = {
913 | url: getUrl(model),
914 | type: type,
915 | contentType: 'application/json',
916 | data: modelJSON,
917 | dataType: 'json',
918 | processData: false,
919 | success: success,
920 | error: error
921 | };
922 |
923 | // For older servers, emulate JSON by encoding the request into an HTML-form.
924 | if (Backbone.emulateJSON) {
925 | params.contentType = 'application/x-www-form-urlencoded';
926 | params.processData = true;
927 | params.data = modelJSON ? {model : modelJSON} : {};
928 | }
929 |
930 | // For older servers, emulate HTTP by mimicking the HTTP method with `_method`
931 | // And an `X-HTTP-Method-Override` header.
932 | if (Backbone.emulateHTTP) {
933 | if (type === 'PUT' || type === 'DELETE') {
934 | if (Backbone.emulateJSON) params.data._method = type;
935 | params.type = 'POST';
936 | params.beforeSend = function(xhr) {
937 | xhr.setRequestHeader("X-HTTP-Method-Override", type);
938 | };
939 | }
940 | }
941 |
942 | // Make the request.
943 | $.ajax(params);
944 | };
945 |
946 | // Helpers
947 | // -------
948 |
949 | // Shared empty constructor function to aid in prototype-chain creation.
950 | var ctor = function(){};
951 |
952 | // Helper function to correctly set up the prototype chain, for subclasses.
953 | // Similar to `goog.inherits`, but uses a hash of prototype properties and
954 | // class properties to be extended.
955 | var inherits = function(parent, protoProps, staticProps) {
956 | var child;
957 |
958 | // The constructor function for the new subclass is either defined by you
959 | // (the "constructor" property in your `extend` definition), or defaulted
960 | // by us to simply call `super()`.
961 | if (protoProps && protoProps.hasOwnProperty('constructor')) {
962 | child = protoProps.constructor;
963 | } else {
964 | child = function(){ return parent.apply(this, arguments); };
965 | }
966 |
967 | // Set the prototype chain to inherit from `parent`, without calling
968 | // `parent`'s constructor function.
969 | ctor.prototype = parent.prototype;
970 | child.prototype = new ctor();
971 |
972 | // Add prototype properties (instance properties) to the subclass,
973 | // if supplied.
974 | if (protoProps) _.extend(child.prototype, protoProps);
975 |
976 | // Add static properties to the constructor function, if supplied.
977 | if (staticProps) _.extend(child, staticProps);
978 |
979 | // Correctly set child's `prototype.constructor`, for `instanceof`.
980 | child.prototype.constructor = child;
981 |
982 | // Set a convenience property in case the parent's prototype is needed later.
983 | child.__super__ = parent.prototype;
984 |
985 | return child;
986 | };
987 |
988 | // Helper function to get a URL from a Model or Collection as a property
989 | // or as a function.
990 | var getUrl = function(object) {
991 | if (!(object && object.url)) throw new Error("A 'url' property or function must be specified");
992 | return _.isFunction(object.url) ? object.url() : object.url;
993 | };
994 |
995 | // Wrap an optional error callback with a fallback error event.
996 | var wrapError = function(onError, model, options) {
997 | return function(resp) {
998 | if (onError) {
999 | onError(model, resp);
1000 | } else {
1001 | model.trigger('error', model, resp, options);
1002 | }
1003 | };
1004 | };
1005 |
1006 | // Helper function to escape a string for HTML rendering.
1007 | var escapeHTML = function(string) {
1008 | return string.replace(/&(?!\w+;)/g, '&').replace(//g, '>').replace(/"/g, '"');
1009 | };
1010 |
1011 | })();
--------------------------------------------------------------------------------
/lib/data.js:
--------------------------------------------------------------------------------
1 | // (c) 2011 Michael Aufreiter
2 | // Data.js is freely distributable under the MIT license.
3 | // Portions of Data.js are inspired or borrowed from Underscore.js,
4 | // Backbone.js and Google's Visualization API.
5 | // For all details and documentation:
6 | // http://substance.io/#michael/data-js
7 |
8 | (function(){
9 |
10 | // Initial Setup
11 | // -------------
12 |
13 | // The top-level namespace. All public Data.js classes and modules will
14 | // be attached to this. Exported for both CommonJS and the browser.
15 | var Data;
16 | if (typeof exports !== 'undefined') {
17 | Data = exports;
18 | } else {
19 | Data = this.Data = {};
20 | }
21 |
22 | // Current version of the library. Keep in sync with `package.json`.
23 | Data.VERSION = '0.4.0-pre';
24 |
25 | // Require Underscore, if we're on the server, and it's not already present.
26 | var _ = this._;
27 | if (!_ && (typeof require !== 'undefined')) _ = require("underscore");
28 |
29 |
30 | // Top Level API
31 | // -------
32 |
33 | Data.VALUE_TYPES = [
34 | 'string',
35 | 'object',
36 | 'number',
37 | 'boolean',
38 | 'date'
39 | ];
40 |
41 | Data.isValueType = function (type) {
42 | return _.include(Data.VALUE_TYPES, type);
43 | };
44 |
45 | // Returns true if a certain object matches a particular query object
46 | // TODO: optimize!
47 | Data.matches = function(node, queries) {
48 | queries = _.isArray(queries) ? queries : [queries];
49 | var matched = false;
50 | // Matches at least one query
51 | _.each(queries, function(query) {
52 | if (matched) return;
53 | var rejected = false;
54 | _.each(query, function(value, key) {
55 | if (rejected) return;
56 | var condition;
57 | // Extract operator
58 | var matches = key.match(/^([a-z_]{1,30})(=|==|!=|>|>=|<|<=|\|=|&=)?$/),
59 | property = matches[1],
60 | operator = matches[2] || (property == "type" || _.isArray(value) ? "|=" : "=");
61 |
62 | if (operator === "|=") { // one of operator
63 | var values = _.isArray(value) ? value : [value];
64 | var objectValues = _.isArray(node[property]) ? node[property] : [node[property]];
65 | condition = false;
66 | _.each(values, function(val) {
67 | if (_.include(objectValues, val)) {
68 | condition = true;
69 | }
70 | });
71 | } else if (operator === "&=") {
72 | var values = _.isArray(value) ? value : [value];
73 | var objectValues = _.isArray(node[property]) ? node[property] : [node[property]];
74 | condition = _.intersect(objectValues, values).length === values.length;
75 | } else { // regular operators
76 | switch (operator) {
77 | case "!=": condition = !_.isEqual(node[property], value); break;
78 | case ">": condition = node[property] > value; break;
79 | case ">=": condition = node[property] >= value; break;
80 | case "<": condition = node[property] < value; break;
81 | case "<=": condition = node[property] <= value; break;
82 | default : condition = _.isEqual(node[property], value); break;
83 | }
84 | }
85 | // TODO: Make sure we exit the loop and return immediately when a condition is not met
86 | if (!condition) return rejected = true;
87 | });
88 | if (!rejected) return matched = true;
89 | });
90 | return matched;
91 | };
92 |
93 |
94 | /*!
95 | Math.uuid.js (v1.4)
96 | http://www.broofa.com
97 | mailto:robert@broofa.com
98 |
99 | Copyright (c) 2010 Robert Kieffer
100 | Dual licensed under the MIT and GPL licenses.
101 | */
102 |
103 | Data.uuid = function (prefix) {
104 | var chars = '0123456789abcdefghijklmnopqrstuvwxyz'.split(''),
105 | uuid = [],
106 | radix = 16,
107 | len = 32;
108 |
109 | if (len) {
110 | // Compact form
111 | for (var i = 0; i < len; i++) uuid[i] = chars[0 | Math.random()*radix];
112 | } else {
113 | // rfc4122, version 4 form
114 | var r;
115 |
116 | // rfc4122 requires these characters
117 | uuid[8] = uuid[13] = uuid[18] = uuid[23] = '-';
118 | uuid[14] = '4';
119 |
120 | // Fill in random data. At i==19 set the high bits of clock sequence as
121 | // per rfc4122, sec. 4.1.5
122 | for (var i = 0; i < 36; i++) {
123 | if (!uuid[i]) {
124 | r = 0 | Math.random()*16;
125 | uuid[i] = chars[(i == 19) ? (r & 0x3) | 0x8 : r];
126 | }
127 | }
128 | }
129 | return (prefix ? prefix : "") + uuid.join('');
130 | };
131 |
132 | // Helpers
133 | // -------
134 |
135 | // _.Events (borrowed from Backbone.js)
136 | // -----------------
137 |
138 | // A module that can be mixed in to *any object* in order to provide it with
139 | // custom events. You may `bind` or `unbind` a callback function to an event;
140 | // `trigger`-ing an event fires all callbacks in succession.
141 | //
142 | // var object = {};
143 | // _.extend(object, Backbone.Events);
144 | // object.bind('expand', function(){ alert('expanded'); });
145 | // object.trigger('expand');
146 | //
147 |
148 | _.Events = {
149 |
150 | // Bind an event, specified by a string name, `ev`, to a `callback` function.
151 | // Passing `"all"` will bind the callback to all events fired.
152 | bind : function(ev, callback) {
153 | var calls = this._callbacks || (this._callbacks = {});
154 | var list = this._callbacks[ev] || (this._callbacks[ev] = []);
155 | list.push(callback);
156 | return this;
157 | },
158 |
159 | // Remove one or many callbacks. If `callback` is null, removes all
160 | // callbacks for the event. If `ev` is null, removes all bound callbacks
161 | // for all events.
162 | unbind : function(ev, callback) {
163 | var calls;
164 | if (!ev) {
165 | this._callbacks = {};
166 | } else if (calls = this._callbacks) {
167 | if (!callback) {
168 | calls[ev] = [];
169 | } else {
170 | var list = calls[ev];
171 | if (!list) return this;
172 | for (var i = 0, l = list.length; i < l; i++) {
173 | if (callback === list[i]) {
174 | list.splice(i, 1);
175 | break;
176 | }
177 | }
178 | }
179 | }
180 | return this;
181 | },
182 |
183 | // Trigger an event, firing all bound callbacks. Callbacks are passed the
184 | // same arguments as `trigger` is, apart from the event name.
185 | // Listening for `"all"` passes the true event name as the first argument.
186 | trigger : function(ev) {
187 | var list, calls, i, l;
188 | if (!(calls = this._callbacks)) return this;
189 | if (list = calls[ev]) {
190 | for (i = 0, l = list.length; i < l; i++) {
191 | list[i].apply(this, Array.prototype.slice.call(arguments, 1));
192 | }
193 | }
194 | if (list = calls['all']) {
195 | for (i = 0, l = list.length; i < l; i++) {
196 | list[i].apply(this, arguments);
197 | }
198 | }
199 | return this;
200 | }
201 | };
202 |
203 | // Shared empty constructor function to aid in prototype-chain creation.
204 | var ctor = function(){};
205 |
206 | // Helper function to correctly set up the prototype chain, for subclasses.
207 | // Similar to `goog.inherits`, but uses a hash of prototype properties and
208 | // class properties to be extended.
209 | // Taken from Underscore.js (c) Jeremy Ashkenas
210 | _.inherits = function(parent, protoProps, staticProps) {
211 | var child;
212 |
213 | // The constructor function for the new subclass is either defined by you
214 | // (the "constructor" property in your `extend` definition), or defaulted
215 | // by us to simply call `super()`.
216 | if (protoProps && protoProps.hasOwnProperty('constructor')) {
217 | child = protoProps.constructor;
218 | } else {
219 | child = function(){ return parent.apply(this, arguments); };
220 | }
221 |
222 | // Set the prototype chain to inherit from `parent`, without calling
223 | // `parent`'s constructor function.
224 | ctor.prototype = parent.prototype;
225 | child.prototype = new ctor();
226 |
227 | // Add prototype properties (instance properties) to the subclass,
228 | // if supplied.
229 | if (protoProps) _.extend(child.prototype, protoProps);
230 |
231 | // Add static properties to the constructor function, if supplied.
232 | if (staticProps) _.extend(child, staticProps);
233 |
234 | // Correctly set child's `prototype.constructor`, for `instanceof`.
235 | child.prototype.constructor = child;
236 |
237 | // Set a convenience property in case the parent's prototype is needed later.
238 | child.__super__ = parent.prototype;
239 |
240 | return child;
241 | };
242 |
243 |
244 | // Data.Hash
245 | // --------------
246 |
247 | // A Hash data structure that provides a simple layer of abstraction for
248 | // managing a sortable data-structure with hash semantics. It's heavily
249 | // used throughout Data.js.
250 |
251 | Data.Hash = function(data) {
252 | var that = this;
253 | this.data = {};
254 | this.keyOrder = [];
255 | this.length = 0;
256 |
257 | if (data instanceof Array) {
258 | _.each(data, function(datum, index) {
259 | that.set(index, datum);
260 | });
261 | } else if (data instanceof Object) {
262 | _.each(data, function(datum, key) {
263 | that.set(key, datum);
264 | });
265 | }
266 |
267 | if (this.initialize) this.initialize(attributes, options);
268 | };
269 |
270 | _.extend(Data.Hash.prototype, _.Events, {
271 |
272 | // Returns a copy of the Hash
273 | // Used by transformation methods
274 | clone: function () {
275 | var copy = new Data.Hash();
276 | copy.length = this.length;
277 | _.each(this.data, function(value, key) {
278 | copy.data[key] = value;
279 | });
280 | copy.keyOrder = this.keyOrder.slice(0, this.keyOrder.length);
281 | return copy;
282 | },
283 |
284 | // Set a value at a given *key*
285 | set: function (key, value, targetIndex) {
286 | var index;
287 | if (key === undefined)
288 | return this;
289 |
290 | if (!this.data[key]) {
291 | if (targetIndex !== undefined) { // insert at a given index
292 | var front = this.select(function(item, key, i) {
293 | return i < targetIndex;
294 | });
295 |
296 | var back = this.select(function(item, key, i) {
297 | return i >= targetIndex;
298 | });
299 |
300 | this.keyOrder = [].concat(front.keyOrder);
301 | this.keyOrder.push(key);
302 | this.keyOrder = this.keyOrder.concat(back.keyOrder);
303 | } else {
304 | this.keyOrder.push(key);
305 | }
306 | index = this.length;
307 | this.length += 1;
308 | } else {
309 | index = this.index(key);
310 | }
311 | this.data[key] = value;
312 | this[index] = this.data[key];
313 |
314 | this.trigger('set', key);
315 | return this;
316 | },
317 |
318 | // Delete entry at given *key*
319 | del: function (key) {
320 | if (this.data[key]) {
321 | var l = this.length;
322 | var index = this.index(key);
323 | delete this.data[key];
324 | this.keyOrder.splice(index, 1);
325 | Array.prototype.splice.call(this, index, 1);
326 | this.length = l-1;
327 | this.trigger('del', key);
328 | }
329 | return this;
330 | },
331 |
332 | // Get value at given *key*
333 | get: function (key) {
334 | return this.data.hasOwnProperty(key) ? this.data[key] : undefined;
335 | },
336 |
337 | // Get value at given *index*
338 | at: function (index) {
339 | var key = this.keyOrder[index];
340 | return this.data[key];
341 | },
342 |
343 | // Get first item
344 | first: function () {
345 | return this.at(0);
346 | },
347 |
348 | // Returns a sub-range of the current *hash*
349 | range: function(start, end) {
350 | var result = new Data.Hash();
351 | for(var i=start; i<=end; i++) {
352 | result.set(this.key(i), this.at(i));
353 | }
354 | return result;
355 | },
356 |
357 | // Returns the rest of the elements.
358 | // Pass an index to return the items from that index onward.
359 | rest: function(index) {
360 | return this.range(index, this.length-1);
361 | },
362 |
363 | // Get last item
364 | last: function () {
365 | return this.at(this.length-1);
366 | },
367 |
368 | // Returns for an index the corresponding *key*
369 | key: function (index) {
370 | return this.keyOrder[index];
371 | },
372 |
373 | // Returns for a given *key* the corresponding *index*
374 | index: function(key) {
375 | return this.keyOrder.indexOf(key);
376 | },
377 |
378 | // Iterate over values contained in the `Data.Hash`
379 | each: function (fn) {
380 | var that = this;
381 | _.each(this.keyOrder, function(key, index) {
382 | fn.call(that, that.data[key], key, index);
383 | });
384 | return this;
385 | },
386 |
387 | // Convert to an ordinary JavaScript Array containing just the values
388 | values: function () {
389 | var result = [];
390 | this.each(function(value, key, index) {
391 | result.push(value);
392 | });
393 | return result;
394 | },
395 |
396 | // Returns all keys in current order
397 | keys: function () {
398 | return _.clone(this.keyOrder);
399 | },
400 |
401 | // Convert to an ordinary JavaScript Array containing
402 | // key value pairs. Used by `sort`.
403 | toArray: function () {
404 | var result = [];
405 |
406 | this.each(function(value, key) {
407 | result.push({key: key, value: value});
408 | });
409 |
410 | return result;
411 | },
412 |
413 | // Serialize
414 | toJSON: function() {
415 | var result = {};
416 |
417 | this.each(function(value, key) {
418 | result[key] = value.toJSON ? value.toJSON() : value;
419 | });
420 | return result;
421 | },
422 |
423 | // Map the `Data.Hash` to your needs
424 | map: function (fn) {
425 | var result = this.clone(),
426 | that = this;
427 | result.each(function(item, key, index) {
428 | result.data[that.key(index)] = fn.call(result, item);
429 | });
430 | return result;
431 | },
432 |
433 | // Select items that match some conditions expressed by a matcher function
434 | select: function (fn) {
435 | var result = new Data.Hash(),
436 | that = this;
437 |
438 | this.each(function(value, key, index) {
439 | if (fn.call(that, value, key, index)) {
440 | result.set(key, value);
441 | }
442 | });
443 | return result;
444 | },
445 |
446 | // Performs a sort
447 | sort: function (comparator) {
448 | var result = this.clone();
449 | sortedKeys = result.toArray().sort(comparator);
450 |
451 | // update keyOrder
452 | result.keyOrder = _.map(sortedKeys, function(k) {
453 | return k.key;
454 | });
455 | return result;
456 | },
457 |
458 | // Performs an intersection with the given *hash*
459 | intersect: function(hash) {
460 | var that = this,
461 | result = new Data.Hash();
462 |
463 | // Ensure that is the smaller one
464 | if (hash.length < that.length) {
465 | that = hash;
466 | hash = this;
467 | }
468 | that.each(function(value,key) {
469 | if (hash.get(key)) result.set(key, value);
470 | });
471 | return result;
472 | },
473 |
474 | // Performs an union with the given *hash*
475 | union: function(hash) {
476 | var that = this,
477 | result = new Data.Hash();
478 |
479 | this.each(function(value, key) {
480 | result.set(key, value);
481 | });
482 | hash.each(function(value, key) {
483 | if (!result.get(key)) result.set(key, value);
484 | });
485 | return result;
486 | },
487 |
488 | // Computes the difference between the current *hash* and a given *hash*
489 | difference: function(hash) {
490 | var that = this;
491 | result = new Data.Hash();
492 | this.each(function(value, key) {
493 | if (!hash.get(key)) result.set(key, value);
494 | });
495 | return result;
496 | }
497 | });
498 |
499 |
500 | // Data.Comparators
501 | // --------------
502 |
503 | Data.Comparators = {};
504 |
505 | Data.Comparators.ASC = function(item1, item2) {
506 | return item1.value === item2.value ? 0 : (item1.value < item2.value ? -1 : 1);
507 | };
508 |
509 | Data.Comparators.DESC = function(item1, item2) {
510 | return item1.value === item2.value ? 0 : (item1.value > item2.value ? -1 : 1);
511 | };
512 |
513 |
514 | // Data.Aggregators
515 | // --------------
516 |
517 | Data.Aggregators = {};
518 |
519 | Data.Aggregators.SUM = function (values) {
520 | var result = 0;
521 | values.each(function(value, key, index) {
522 | if (_.isNumber(value)) result += value;
523 | });
524 | return result;
525 | };
526 |
527 | Data.Aggregators.MIN = function (values) {
528 | var result = Infinity;
529 | values.each(function(value, key, index) {
530 | if (_.isNumber(value) && value < result) result = value;
531 | });
532 | return result;
533 | };
534 |
535 | Data.Aggregators.MAX = function (values) {
536 | var result = -Infinity;
537 | values.each(function(value, key, index) {
538 | if (_.isNumber(value) && value > result) result = value;
539 | });
540 | return result;
541 | };
542 |
543 | Data.Aggregators.AVG = function (values) {
544 | var sum = 0,
545 | count = 0;
546 | values.each(function(value, key, index) {
547 | if (_.isNumber(value)) {
548 | sum += value;
549 | count += 1;
550 | }
551 | });
552 | return count === 0 ? 0 : (sum / count);
553 | };
554 |
555 | Data.Aggregators.COUNT = function (values) {
556 | return values.length;
557 | };
558 |
559 |
560 | // Data.Modifiers
561 | // --------------
562 |
563 | Data.Modifiers = {};
564 |
565 | // The default modifier simply does nothing
566 | Data.Modifiers.DEFAULT = function (attribute) {
567 | return attribute;
568 | };
569 |
570 | Data.Modifiers.MONTH = function (attribute) {
571 | return attribute.getMonth();
572 | };
573 |
574 | Data.Modifiers.QUARTER = function (attribute) {
575 | return Math.floor(attribute.getMonth() / 3) + 1;
576 | };
577 |
578 | // Data.Transformers
579 | // --------------
580 |
581 | Data.Transformers = {
582 | group: function(g, type, keys, properties) {
583 | var gspec = {},
584 | type = g.get(type),
585 | groups = {},
586 | count = 0;
587 |
588 | gspec[type._id] = {"type": "/type/type", "properties": {}, indexes: type.indexes};
589 |
590 | // Include group keys to the output graph
591 | _.each(keys, function(key) {
592 | gspec[type._id].properties[key] = type.properties().get(key).toJSON();
593 | });
594 |
595 | // Include additional properties
596 | _.each(properties, function(options, key) {
597 | var p = type.properties().get(options.property || key).toJSON();
598 | if (options.name) p.name = options.name;
599 | gspec[type._id].properties[key] = p;
600 | });
601 |
602 | var groupedGraph = new Data.Graph(gspec);
603 |
604 | _.each(keys, function(key) {
605 | groups[key] = type.properties().get(key).all('values');
606 | });
607 |
608 | function aggregate(key) {
609 | var members = new Data.Hash();
610 |
611 | _.each(keys, function(k, index) {
612 | var objects = groups[keys[index]].get(key[index]).referencedObjects;
613 | members = index === 0 ? members.union(objects) : members.intersect(objects);
614 | });
615 |
616 | // Empty group key
617 | if (key.length === 0) members = g.objects();
618 | if (members.length === 0) return null;
619 |
620 | var res = {type: type._id};
621 | _.each(gspec[type._id].properties, function(p, pk) {
622 | if (_.include(keys, pk)) {
623 | res[pk] = key[_.indexOf(keys, pk)];
624 | } else {
625 | var numbers = members.map(function(obj) {
626 | return obj.get(properties[pk].property || pk);
627 | });
628 | var aggregator = properties[pk].aggregator || Data.Aggregators.SUM;
629 | res[pk] = aggregator(numbers);
630 | }
631 | });
632 | return res;
633 | }
634 |
635 | function extractGroups(keyIndex, key) {
636 | if (keyIndex === keys.length-1) {
637 | var aggregatedItem = aggregate(key);
638 | if (aggregatedItem) groupedGraph.set(key.join('::'), aggregatedItem);
639 | } else {
640 | keyIndex += 1;
641 | groups[keys[keyIndex]].each(function(grp, grpkey) {
642 | extractGroups(keyIndex, key.concat([grpkey]));
643 | });
644 | }
645 | }
646 | extractGroups(-1, []);
647 | return groupedGraph;
648 | }
649 | };
650 |
651 |
652 | // Data.Node
653 | // --------------
654 |
655 | // JavaScript Node implementation that hides graph complexity from
656 | // the interface. It introduces properties, which group types of edges
657 | // together. Therefore multi-partite graphs are possible without any hassle.
658 | // Every Node simply contains properties which conform to outgoing edges.
659 | // It makes heavy use of hashing through JavaScript object properties to
660 | // allow random access whenever possible. If I've got it right, it should
661 | // perform sufficiently fast, allowing speedy graph traversals.
662 |
663 | Data.Node = function(options) {
664 | this.nodeId = Data.Node.generateId();
665 | if (options) {
666 | this.val = options.value;
667 | }
668 | this._properties = {};
669 |
670 | if (this.initialize) this.initialize(options);
671 | };
672 |
673 | Data.Node.nodeCount = 0;
674 |
675 | // Generates a unique id for each node
676 | Data.Node.generateId = function () {
677 | return Data.Node.nodeCount += 1;
678 | };
679 |
680 | _.extend(Data.Node.prototype, _.Events, {
681 | // Node identity, which is simply the node's id
682 | identity: function() {
683 | return this.nodeId;
684 | },
685 |
686 | // Replace a property with a complete `Hash`
687 | replace: function(property, hash) {
688 | this._properties[property] = hash;
689 | },
690 |
691 | // Set a Node's property
692 | //
693 | // Takes a property key, a value key and value. Values that aren't
694 | // instances of `Data.Node` wrapped are automatically.
695 | set: function (property, key, value) {
696 | if (!this._properties[property]) {
697 | this._properties[property] = new Data.Hash();
698 | }
699 | this._properties[property].set(key, value instanceof Data.Node ? value : new Data.Node({value: value}));
700 | return this;
701 | },
702 |
703 | // Get node for given *property* at given *key*
704 | get: function (property, key) {
705 | if (key !== undefined && this._properties[property] !== undefined) {
706 | return this._properties[property].get(key);
707 | }
708 | },
709 |
710 | // Get all connected nodes at given *property*
711 | all: function(property) {
712 | return this._properties[property];
713 | },
714 |
715 | // Get first connected node at given *property*
716 | //
717 | // Useful if you want to mimic the behavior of unique properties.
718 | // That is, if you know that there's always just one associated node
719 | // at a given property.
720 | first: function(property) {
721 | var p = this._properties[property];
722 | return p ? p.first() : null;
723 | },
724 |
725 | // Value of first connected target node at given *property*
726 | value: function(property) {
727 | return this.values(property).first();
728 | },
729 |
730 | // Values of associated target nodes for non-unique properties
731 | values: function(property) {
732 | if (!this.all(property)) return new Data.Hash();
733 | return this.all(property).map(function(n) {
734 | return n.val;
735 | });
736 | }
737 | });
738 |
739 |
740 | // Data.Adapter
741 | // --------------
742 |
743 | // An abstract interface for writing and reading Data.Graphs.
744 |
745 | Data.Adapter = function(config) {
746 | // The config object is used to describe database credentials
747 | this.config = config;
748 | };
749 |
750 | // Namespace where Data.Adapters can register
751 | Data.Adapters = {};
752 |
753 | // Data.Property
754 | // --------------
755 |
756 | // Meta-data (data about data) is represented as a set of properties that
757 | // belongs to a certain `Data.Type`. A `Data.Property` holds a key, a name
758 | // and an expected type, telling whether the data is numeric or textual, etc.
759 |
760 | Data.Property = _.inherits(Data.Node, {
761 | constructor: function(type, id, options) {
762 | Data.Node.call(this);
763 | this.key = id;
764 | this._id = id;
765 | this.type = type;
766 | this.unique = options.unique;
767 | this.name = options.name;
768 | this.meta = options.meta || {};
769 | this.validator = options.validator;
770 | this.required = options["required"];
771 | this["default"] = options["default"];
772 |
773 | // TODO: ensure that object and value types are not mixed
774 | this.expectedTypes = _.isArray(options['type']) ? options['type'] : [options['type']];
775 | this.replace('values', new Data.Hash());
776 | },
777 |
778 | // TODO: this desctroys Data.Node#values
779 | // values: function() {
780 | // return this.all('values');
781 | // },
782 |
783 | isValueType: function() {
784 | return Data.isValueType(this.expectedTypes[0]);
785 | },
786 |
787 | isObjectType: function() {
788 | return !this.isValueType();
789 | },
790 |
791 | // Register values of a certain object
792 | registerValues: function(values, obj) {
793 | var that = this;
794 | var res = new Data.Hash();
795 |
796 | _.each(values, function(v, index) {
797 | if (v === undefined) return; // skip
798 | var val;
799 |
800 | // Skip registration for object type values
801 | // TODO: check edge cases!
802 | if (that.isValueType() && that.expectedTypes[0] === 'object') {
803 | val = new Data.Node({value: v});
804 | res.set(index, val);
805 | return;
806 | }
807 |
808 | // Check if we can recycle an old value of that object
809 | if (obj.all(that.key)) val = obj.all(that.key).get(v);
810 |
811 | if (!val) { // Can't recycle
812 | val = that.get('values', v);
813 | if (!val) {
814 | // Well, a new value needs to be created
815 | if (that.isObjectType()) {
816 | // Create on the fly if an object is passed as a value
817 | if (typeof v === 'object') v = that.type.g.set(null, v)._id;
818 | val = that.type.g.get('objects', v);
819 | if (!val) {
820 | // Register the object (even if not yet loaded)
821 | val = new Data.Object(that.type.g, v);
822 | that.type.g.set('objects', v, val);
823 | }
824 | } else {
825 | val = new Data.Node({value: v});
826 | val.referencedObjects = new Data.Hash();
827 | }
828 | // Register value on the property
829 | that.set('values', v, val);
830 | }
831 | val.referencedObjects.set(obj._id, obj);
832 | }
833 |
834 | res.set(v, val);
835 | });
836 |
837 | // Unregister values that are no longer used on the object
838 | if (obj.all(that.key)) {
839 | this.unregisterValues(obj.all(that.key).difference(res), obj);
840 | }
841 | return res;
842 | },
843 |
844 | // Unregister values from a certain object
845 | unregisterValues: function(values, obj) {
846 | var that = this;
847 |
848 | values.each(function(val, key) {
849 | if (val.referencedObjects && val.referencedObjects.length>1) {
850 | val.referencedObjects.del(obj._id);
851 | } else {
852 | that.all('values').del(key);
853 | }
854 | });
855 | },
856 |
857 | // Aggregates the property's values
858 | aggregate: function (fn) {
859 | return fn(this.values("values"));
860 | },
861 |
862 | // Serialize a propery definition
863 | toJSON: function() {
864 | return {
865 | name: this.name,
866 | type: this.expectedTypes,
867 | unique: this.unique,
868 | meta: this.meta,
869 | validator: this.validator,
870 | required: this.required,
871 | "default": this["default"]
872 | }
873 | }
874 | });
875 |
876 |
877 | // Data.Type
878 | // --------------
879 |
880 | // A `Data.Type` denotes an IS A relationship about a `Data.Object`.
881 | // For example, if you type the object 'Shakespear' with the type 'Person'
882 | // you are saying that Shakespeare IS A person. Types are also used to hold
883 | // collections of properties that belong to a certain group of objects.
884 |
885 | Data.Type = _.inherits(Data.Node, {
886 | constructor: function(g, id, type) {
887 | var that = this;
888 | Data.Node.call(this);
889 |
890 | this.g = g; // Belongs to the DataGraph
891 | this.key = id;
892 | this._id = id;
893 | this._rev = type._rev;
894 | this._conflicted = type._conflicted;
895 | this.type = type.type;
896 | this.name = type.name;
897 | this.meta = type.meta || {};
898 | this.indexes = type.indexes;
899 |
900 | that.replace('properties', new Data.Hash);
901 | // Extract properties
902 | _.each(type.properties, function(property, key) {
903 | that.set('properties', key, new Data.Property(that, key, property));
904 | });
905 | },
906 |
907 | // Convenience function for accessing properties
908 | properties: function() {
909 | return this.all('properties');
910 | },
911 |
912 | // Objects of this type
913 | objects: function() {
914 | return this.all('objects');
915 | },
916 |
917 | // Serialize a single type node
918 | toJSON: function() {
919 | var result = {
920 | _id: this._id,
921 | type: '/type/type',
922 | name: this.name,
923 | properties: {}
924 | };
925 |
926 | if (this._rev) result._rev = this._rev;
927 | if (this.meta && _.keys(this.meta).length > 0) result.meta = this.meta;
928 | if (this.indexes && _.keys(this.indexes).length > 0) result.indexes = this.indexes;
929 |
930 | this.all('properties').each(function(property) {
931 | var p = result.properties[property.key] = {
932 | name: property.name,
933 | unique: property.unique,
934 | type: property.expectedTypes,
935 | required: property.required ? true : false
936 | };
937 | if (property["default"]) p["default"] = property["default"];
938 | if (property.validator) p.validator = property.validator;
939 | if (property.meta && _.keys(property.meta).length > 0) p.meta = property.meta;
940 | });
941 | return result;
942 | }
943 | });
944 |
945 |
946 | // Data.Object
947 | // --------------
948 |
949 | // Represents a typed data object within a `Data.Graph`.
950 | // Provides access to properties, defined on the corresponding `Data.Type`.
951 |
952 | Data.Object = _.inherits(Data.Node, {
953 | constructor: function(g, id, data) {
954 | var that = this;
955 | Data.Node.call(this);
956 |
957 | this.g = g;
958 |
959 | // TODO: remove in favor of _id
960 | this.key = id;
961 | this._id = id;
962 | this.html_id = id.replace(/\//g, '_');
963 | this.dirty = true; // Every constructed node is dirty by default
964 |
965 | this.errors = []; // Stores validation errors
966 | this._types = new Data.Hash();
967 |
968 | // Associated Data.Objects
969 | this.referencedObjects = new Data.Hash();
970 |
971 | // Memoize raw data for the build process
972 | if (data) this.data = data;
973 | },
974 |
975 | // Convenience function for accessing all related types
976 | types: function() {
977 | return this._types;
978 | },
979 |
980 | toString: function() {
981 | return this.get('name') || this.val || this._id;
982 | },
983 |
984 | // Properties from all associated types
985 | properties: function() {
986 | var properties = new Data.Hash();
987 | // Prototypal inheritance in action: overriden properties belong to the last type specified
988 | this._types.each(function(type) {
989 | type.all('properties').each(function(property) {
990 | properties.set(property.key, property);
991 | });
992 | });
993 | return properties;
994 | },
995 |
996 | // After all nodes are recognized the object can be built
997 | build: function() {
998 | var that = this;
999 | var types = _.isArray(this.data.type) ? this.data.type : [this.data.type];
1000 |
1001 | if (!this.data) throw new Error('Object has no data, and cannot be built');
1002 |
1003 | // Pull off _id and _rev properties
1004 | // delete this.data._id;
1005 | this._rev = this.data._rev; // delete this.data._rev;
1006 | this._conflicted = this.data._conflicted;
1007 | this._deleted = this.data._deleted; // delete this.data._deleted;
1008 |
1009 | // Initialize primary type (backward compatibility)
1010 | this.type = this.g.get('objects', _.last(types));
1011 |
1012 | // Initialize types
1013 | _.each(types, function(type) {
1014 | that._types.set(type, that.g.get('objects', type));
1015 | // Register properties for all types
1016 | that._types.get(type).all('properties').each(function(property, key) {
1017 | function applyValue(value) {
1018 | var values = _.isArray(value) ? value : [value];
1019 | // Apply property values
1020 | that.replace(property.key, property.registerValues(values, that));
1021 | }
1022 |
1023 | if (that.data[key] !== undefined) {
1024 | applyValue(that.data[key]);
1025 | } else if (property["default"]) {
1026 | applyValue(property["default"]);
1027 | }
1028 | });
1029 | });
1030 | if (this.dirty) this.g.trigger('dirty');
1031 | },
1032 |
1033 | // Validates an object against its type (=schema)
1034 | validate: function() {
1035 | if (this.type.key === '/type/type') return true; // Skip type nodes
1036 |
1037 | var that = this;
1038 | this.errors = [];
1039 | this.properties().each(function(property, key) {
1040 | // Required property?
1041 | if ((that.get(key) === undefined || that.get(key) === null) || that.get(key) === "") {
1042 | if (property.required) {
1043 | that.errors.push({property: key, message: "Property \"" + property.name + "\" is required"});
1044 | }
1045 | } else {
1046 | // Correct type?
1047 | var types = property.expectedTypes;
1048 |
1049 | function validType(value, types) {
1050 | if (_.include(types, typeof value)) return true;
1051 | // FIXME: assumes that unloaded objects are valid properties
1052 | if (!value.data) return true;
1053 | if (value instanceof Data.Object && _.intersect(types, value.types().keys()).length>0) return true;
1054 | if (typeof value === 'object' && _.include(types, value.constructor.name.toLowerCase())) return true;
1055 | return false;
1056 | }
1057 |
1058 | // Unique properties
1059 | if (property.unique && !validType(that.get(key), types)) {
1060 | that.errors.push({property: key, message: "Invalid type for property \"" + property.name + "\""});
1061 | }
1062 |
1063 | // Non unique properties
1064 | if (!property.unique && !_.all(that.get(key).values(), function(v) { return validType(v, types); })) {
1065 | that.errors.push({property: key, message: "Invalid value type for property \"" + property.name + "\""});
1066 | }
1067 | }
1068 |
1069 | // Validator satisfied?
1070 | function validValue() {
1071 | return new RegExp(property.validator).test(that.get(key));
1072 | }
1073 |
1074 | if (property.validator) {
1075 | if (!validValue()) {
1076 | that.errors.push({property: key, message: "Invalid value for property \"" + property.name + "\""});
1077 | }
1078 | }
1079 | });
1080 | return this.errors.length === 0;
1081 | },
1082 |
1083 | // There are four different access scenarios for getting a certain property
1084 | //
1085 | // * Unique value types
1086 | // * Non-unique value types
1087 | // * Unique object types
1088 | // * Non-Unique object types
1089 | //
1090 | // For convenience there's a get method, which always returns the right
1091 | // result depending on the schema information. However, internally, every
1092 | // property of a resource is represented as a non-unique `Data.Hash`
1093 | // of `Data.Node` objects, even if it's a unique property. So if you want
1094 | // to be explicit you should use the native methods of `Data.Node`. If
1095 | // two arguments are provided `get` delegates to `Data.Node#get`.
1096 |
1097 | get: function(property, key) {
1098 | if (!this.data) return null;
1099 | var p = this.properties().get(property);
1100 | if (!p) return null;
1101 |
1102 | if (arguments.length === 1) {
1103 | if (p.isObjectType()) {
1104 | return p.unique ? this.first(property) : this.all(property);
1105 | } else {
1106 | return p.unique ? this.value(property) : this.values(property);
1107 | }
1108 | } else {
1109 | return Data.Node.prototype.get.call(this, property, key);
1110 | }
1111 | },
1112 |
1113 | // Sets properties on the object
1114 | // Existing properties are overridden / replaced
1115 | set: function(properties) {
1116 | var that = this;
1117 |
1118 | if (arguments.length === 1) {
1119 | _.each(properties, function(value, key) {
1120 | var p = that.properties().get(key);
1121 | if (!p) return; // Property not found on type
1122 |
1123 | // Setup values
1124 | that.replace(p.key, p.registerValues(_.isArray(value) ? value : [value], that));
1125 |
1126 | that.dirty = true;
1127 | that.g.trigger('dirty');
1128 | });
1129 | } else {
1130 | return Data.Node.prototype.set.call(this, arguments[0], arguments[1], arguments[2]);
1131 | }
1132 | },
1133 |
1134 | // Serialize an `Data.Object`'s properties
1135 | toJSON: function() {
1136 | var that = this;
1137 | result = {};
1138 | _.each(this._properties, function(value, key) {
1139 | var p = that.properties().get(key);
1140 | if (p.isObjectType()) {
1141 | result[key] = p.unique ? that.all(key).keys()[0] : that.all(key).keys()
1142 | } else {
1143 | result[key] = p.unique ? that.value(key) : that.values(key).values();
1144 | }
1145 | });
1146 | result['type'] = this.types().keys();
1147 | result['_id'] = this._id;
1148 | if (this._rev !== undefined) result['_rev'] = this._rev;
1149 | if (this._deleted) result['_deleted'] = this._deleted;
1150 | return result;
1151 | }
1152 | });
1153 |
1154 | _.extend(Data.Object.prototype, _.Events);
1155 |
1156 |
1157 | // Data.Graph
1158 | // --------------
1159 |
1160 | // A `Data.Graph` can be used for representing arbitrary complex object
1161 | // graphs. Relations between objects are expressed through links that
1162 | // point to referred objects. Data.Graphs can be traversed in various ways.
1163 | // See the testsuite for usage.
1164 |
1165 | // Set a new Data.Adapter and enable Persistence API
1166 |
1167 | Data.Graph = _.inherits(Data.Node, {
1168 | constructor: function(g, dirty) {
1169 | var that = this;
1170 | Data.Node.call(this);
1171 |
1172 | this.watchers = {};
1173 | this.replace('objects', new Data.Hash());
1174 | if (!g) return;
1175 | this.merge(g, dirty);
1176 | },
1177 |
1178 | connect: function(name, config) {
1179 | if (typeof exports !== 'undefined') {
1180 | var Adapter = require(__dirname + '/adapters/'+name+'_adapter');
1181 | this.adapter = new Adapter(this, config);
1182 | } else {
1183 | if (!Data.Adapters[name]) throw new Error('Adapter "'+name+'" not found');
1184 | this.adapter = new Data.Adapters[name](this, config);
1185 | }
1186 | return this;
1187 | },
1188 |
1189 | // Called when the Data.Adapter is ready
1190 | connected: function(callback) {
1191 | if (this.adapter.realtime) {
1192 | this.connectedCallback = callback;
1193 | } else {
1194 | callback();
1195 | }
1196 | },
1197 |
1198 | // Serve graph along with an httpServer instance
1199 | serve: function(server, options) {
1200 | require(__dirname + '/server').initialize(server, this);
1201 | },
1202 |
1203 | // Watch for graph updates
1204 | watch: function(channel, query, callback) {
1205 | this.watchers[channel] = callback;
1206 | this.adapter.watch(channel, query, function(err) {});
1207 | },
1208 |
1209 | // Stop watching that channel
1210 | unwatch: function(channel, callback) {
1211 | delete this.watchers[channel];
1212 | this.adapter.unwatch(channel, function() {});
1213 | },
1214 |
1215 | // Merges in another Graph
1216 | merge: function(g, dirty) {
1217 | var that = this;
1218 |
1219 | // Process schema nodes
1220 | var types = _.select(g, function(node, key) {
1221 | if (node.type === '/type/type' || node.type === 'type') {
1222 | if (!that.get('objects', key)) {
1223 | that.set('objects', key, new Data.Type(that, key, node));
1224 | that.get(key).dirty = dirty;
1225 | }
1226 | return true;
1227 | }
1228 | return false;
1229 | });
1230 |
1231 | // Process object nodes
1232 | var objects = _.select(g, function(node, key) {
1233 | if (node.type !== '/type/type' && node.type !== 'type') {
1234 | var res = that.get('objects', key);
1235 | var types = _.isArray(node.type) ? node.type : [node.type];
1236 | if (!res) {
1237 | res = new Data.Object(that, key, node);
1238 | that.set('objects', key, res);
1239 | } else {
1240 | // Populate existing node with data in order to be rebuilt
1241 | res.data = node;
1242 | }
1243 | // Check for type existence
1244 | _.each(types, function(type) {
1245 | if (!that.get('objects', type)) {
1246 | throw new Error("Type '"+type+"' not found for "+key+"...");
1247 | }
1248 | that.get('objects', type).set('objects', key, res);
1249 | });
1250 | that.get(key).dirty = dirty;
1251 | if (!node._id) node._id = key;
1252 | return true;
1253 | }
1254 | return false;
1255 | });
1256 |
1257 | // Now that all new objects are registered we can build them
1258 | _.each(objects, function(o) {
1259 | var obj = that.get(o._id);
1260 | if (obj.data) obj.build();
1261 | });
1262 | return this;
1263 | },
1264 |
1265 | set: function(node) {
1266 | var id, that = this;
1267 |
1268 | // Backward compatibility
1269 | if (arguments.length === 2) node = _.extend(arguments[1], {_id: arguments[0]});
1270 |
1271 | var types = _.isArray(node.type) ? node.type : [node.type];
1272 | if (arguments.length <= 2) {
1273 | node._id = node._id ? node._id : Data.uuid('/' + _.last(_.last(types).split('/')) + '/');
1274 | // Recycle existing object if there is one
1275 | var res = that.get(node._id) ? that.get(node._id) : new Data.Object(that, node._id, _.clone(node), true);
1276 | res.data = node;
1277 | res.dirty = true;
1278 | res.build();
1279 | this.set('objects', node._id, res);
1280 | return res;
1281 | } else { // Delegate to Data.Node#set
1282 | return Data.Node.prototype.set.call(this, arguments[0], arguments[1], arguments[2]);
1283 | }
1284 | },
1285 |
1286 | // API method for accessing objects in the graph space
1287 | get: function(id) {
1288 | if (arguments.length === 1) {
1289 | return this.get('objects', id);
1290 | } else {
1291 | return Data.Node.prototype.get.call(this, arguments[0], arguments[1]);
1292 | }
1293 | },
1294 |
1295 | // Delete node by id, referenced nodes remain untouched
1296 | del: function(id) {
1297 | var node = this.get(id);
1298 | if (!node) return;
1299 | node._deleted = true;
1300 | node.dirty = true;
1301 | // Remove registered values
1302 | node.properties().each(function(p, key) {
1303 | var values = node.all(key);
1304 | if (values) p.unregisterValues(values, node);
1305 | });
1306 | this.trigger('dirty');
1307 | },
1308 |
1309 | // Find objects that match a particular query
1310 | find: function(query) {
1311 | return this.objects().select(function(o) {
1312 | return Data.matches(o.toJSON(), query);
1313 | });
1314 | },
1315 |
1316 | // Fetches a new subgraph from the adapter and either merges the new nodes
1317 | // into the current set of nodes
1318 | fetch: function(query, options, callback) {
1319 | var that = this,
1320 | nodes = new Data.Hash(); // collects arrived nodes
1321 |
1322 | // Options are optional
1323 | if (typeof options === 'function' && typeof callback === 'undefined') {
1324 | callback = options;
1325 | options = {};
1326 | }
1327 |
1328 | this.adapter.read(query, options, function(err, graph) {
1329 | if (graph) {
1330 | that.merge(graph, false);
1331 | _.each(graph, function(node, key) {
1332 | nodes.set(key, that.get(key));
1333 | });
1334 | }
1335 | err ? callback(err) : callback(null, nodes);
1336 | });
1337 | },
1338 |
1339 | // Synchronize dirty nodes with the backend
1340 | sync: function(callback) {
1341 | callback = callback || function() {};
1342 | var that = this,
1343 | nodes = that.dirtyNodes();
1344 |
1345 | var validNodes = new Data.Hash();
1346 | var invalidNodes = nodes.select(function(node, key) {
1347 | if (!node.validate || (node.validate && node.validate())) {
1348 | validNodes.set(key, node);
1349 | return false;
1350 | } else {
1351 | return true;
1352 | }
1353 | });
1354 |
1355 | this.adapter.write(validNodes.toJSON(), function(err, g) {
1356 | if (err) {
1357 | callback(err);
1358 | } else {
1359 | that.merge(g, false);
1360 |
1361 | // Check for rejectedNodes
1362 | validNodes.each(function(n, key) {
1363 | if (g[key]) {
1364 | n.dirty = false;
1365 | n._rejected = false;
1366 | } else {
1367 | n._rejected = true;
1368 | }
1369 | });
1370 |
1371 | if (that.conflictedNodes().length > 0) that.trigger('conflicted');
1372 | if (that.rejectedNodes().length > 0) that.trigger('rejected');
1373 |
1374 | callback(invalidNodes.length > 0 ? 'Some invalid nodes' : null, invalidNodes);
1375 | }
1376 | });
1377 | },
1378 |
1379 | // Perform a group operation on a Data.Graph
1380 | group: function(type, keys, properties) {
1381 | var res = new Data.Collection();
1382 | res.g = Data.Transformers.group(this, type, keys, properties);
1383 | return res;
1384 | },
1385 |
1386 | // Type nodes
1387 | types: function() {
1388 | return this.all('objects').select(function(node, key) {
1389 | return node.type === '/type/type' || node.type === 'type';
1390 | });
1391 | },
1392 |
1393 | // Object nodes
1394 | objects: function() {
1395 | return this.all('objects').select(function(node, key) {
1396 | return node.type !== '/type/type' && node.type !== 'type' && node.data && !node._deleted;
1397 | });
1398 | },
1399 |
1400 | // Get dirty nodes
1401 | // Used by Data.Graph#sync
1402 | dirtyNodes: function() {
1403 | return this.all('objects').select(function(obj, key) {
1404 | return (obj.dirty && (obj.data || obj instanceof Data.Type));
1405 | });
1406 | },
1407 |
1408 | // Get invalid nodes
1409 | invalidNodes: function() {
1410 | return this.all('objects').select(function(obj, key) {
1411 | return (obj.errors && obj.errors.length > 0);
1412 | });
1413 | },
1414 |
1415 | // Get conflicting nodes
1416 | conflictedNodes: function() {
1417 | return this.all('objects').select(function(obj, key) {
1418 | return obj._conflicted;
1419 | });
1420 | },
1421 |
1422 | // Nodes that got rejected during sync
1423 | rejectedNodes: function() {
1424 | return this.all('objects').select(function(obj, key) {
1425 | return obj._rejected;
1426 | });
1427 | },
1428 |
1429 | // Serializes the graph to the JSON-based exchange format
1430 | toJSON: function() {
1431 | var result = {};
1432 |
1433 | // Serialize object nodes
1434 | this.all('objects').each(function(obj, key) {
1435 | // Only serialize fetched nodes
1436 | if (obj.data || obj instanceof Data.Type) {
1437 | result[key] = obj.toJSON();
1438 | }
1439 | });
1440 |
1441 | return result;
1442 | }
1443 | });
1444 |
1445 | _.extend(Data.Graph.prototype, _.Events);
1446 |
1447 |
1448 | // Data.Collection
1449 | // --------------
1450 |
1451 | // A Collection is a simple data abstraction format where a dataset under
1452 | // investigation conforms to a collection of data items that describes all
1453 | // facets of the underlying data in a simple and universal way. You can
1454 | // think of a Collection as a table of data, except it provides precise
1455 | // information about the data contained (meta-data). A Data.Collection
1456 | // just wraps a `Data.Graph` internally, in order to simplify the interface,
1457 | // for cases where you do not have to deal with linked data.
1458 |
1459 | Data.Collection = function(spec) {
1460 | var that = this,
1461 | gspec = { "/type/item": { "type": "/type/type", "properties": {}} };
1462 |
1463 | if (spec) gspec["/type/item"]["indexes"] = spec.indexes || {};
1464 |
1465 | // Convert to Data.Graph serialization format
1466 | if (spec) {
1467 | _.each(spec.properties, function(property, key) {
1468 | gspec["/type/item"].properties[key] = property;
1469 | });
1470 | this.g = new Data.Graph(gspec);
1471 | _.each(spec.items, function(item, key) {
1472 | that.set(key, item);
1473 | });
1474 | } else {
1475 | this.g = new Data.Graph();
1476 | }
1477 | };
1478 |
1479 | _.extend(Data.Collection.prototype, {
1480 |
1481 | // Get an object (item) from the collection
1482 | get: function(key) {
1483 | return this.g.get.apply(this.g, arguments);
1484 | },
1485 |
1486 | // Set (add) a new object to the collection
1487 | set: function(id, properties) {
1488 | this.g.set(id, _.extend(properties, {type: "/type/item"}));
1489 | },
1490 |
1491 | // Find objects that match a particular query
1492 | find: function(query) {
1493 | query["type|="] = "/type/item";
1494 | return this.g.find(query);
1495 | },
1496 |
1497 | // Returns a filtered collection containing only items that match a certain query
1498 | filter: function(query) {
1499 | return new Data.Collection({
1500 | properties: this.properties().toJSON(),
1501 | items: this.find(query).toJSON()
1502 | });
1503 | },
1504 |
1505 | // Perform a group operation on the collection
1506 | group: function(keys, properties) {
1507 | var res = new Data.Collection();
1508 | res.g = Data.Transformers.group(this.g, "/type/item", keys, properties);
1509 | return res;
1510 | },
1511 |
1512 | // Convenience function for accessing properties
1513 | properties: function() {
1514 | return this.g.get('objects', '/type/item').all('properties');
1515 | },
1516 |
1517 | // Convenience function for accessing items
1518 | items: function() {
1519 | return this.g.objects();
1520 | },
1521 |
1522 | // Convenience function for accessing indexes defined on the collection
1523 | indexes: function() {
1524 | return this.g.get('/type/item').indexes;
1525 | },
1526 |
1527 | // Serialize
1528 | toJSON: function() {
1529 | return {
1530 | properties: this.g.toJSON()["/type/item"].properties,
1531 | items: this.g.objects().toJSON()
1532 | }
1533 | }
1534 | });
1535 | })();
1536 |
1537 |
1538 |
1539 | Data.Adapters["ajax"] = function(graph, config) {
1540 |
1541 | config = config ? config : {url: '/graph/'};
1542 |
1543 | // write
1544 | // --------------
1545 |
1546 | // Takes a Data.Graph and calls a webservice to persist it
1547 |
1548 | self.write = function(graph, callback) {
1549 | $.ajax({
1550 | type: "PUT",
1551 | url: config.url+"write",
1552 | data: JSON.stringify(graph),
1553 | contentType: "application/json",
1554 | dataType: "json",
1555 | success: function(res) {
1556 | res.error ? callback(res.error) : callback(null, res.graph);
1557 | },
1558 | error: function(err) {
1559 | callback(err);
1560 | }
1561 | });
1562 | };
1563 |
1564 | // read
1565 | // --------------
1566 |
1567 | // Takes a query object and reads all matching nodes
1568 |
1569 | self.read = function(qry, options, callback) {
1570 | $.ajax({
1571 | type: "GET",
1572 | url: config.url+"read",
1573 | data: {
1574 | qry: JSON.stringify(qry),
1575 | options: JSON.stringify(options)
1576 | },
1577 | dataType: "jsonp",
1578 | success: function(res) {
1579 | res.error ? callback(res.error) : callback(null, res);
1580 | },
1581 | error: function(err) {
1582 | callback(err);
1583 | }
1584 | });
1585 | };
1586 |
1587 | self.watch = function() {
1588 | // no-op
1589 | };
1590 |
1591 | self.unwatch = function() {
1592 | // no-op
1593 | };
1594 |
1595 | // Expose Public API
1596 | return self;
1597 | };
--------------------------------------------------------------------------------
/lib/underscore.js:
--------------------------------------------------------------------------------
1 | // Underscore.js 1.1.6
2 | // (c) 2011 Jeremy Ashkenas, DocumentCloud Inc.
3 | // Underscore is freely distributable under the MIT license.
4 | // Portions of Underscore are inspired or borrowed from Prototype,
5 | // Oliver Steele's Functional, and John Resig's Micro-Templating.
6 | // For all details and documentation:
7 | // http://documentcloud.github.com/underscore
8 |
9 | (function() {
10 |
11 | // Baseline setup
12 | // --------------
13 |
14 | // Establish the root object, `window` in the browser, or `global` on the server.
15 | var root = this;
16 |
17 | // Save the previous value of the `_` variable.
18 | var previousUnderscore = root._;
19 |
20 | // Establish the object that gets returned to break out of a loop iteration.
21 | var breaker = {};
22 |
23 | // Save bytes in the minified (but not gzipped) version:
24 | var ArrayProto = Array.prototype, ObjProto = Object.prototype, FuncProto = Function.prototype;
25 |
26 | // Create quick reference variables for speed access to core prototypes.
27 | var slice = ArrayProto.slice,
28 | unshift = ArrayProto.unshift,
29 | toString = ObjProto.toString,
30 | hasOwnProperty = ObjProto.hasOwnProperty;
31 |
32 | // All **ECMAScript 5** native function implementations that we hope to use
33 | // are declared here.
34 | var
35 | nativeForEach = ArrayProto.forEach,
36 | nativeMap = ArrayProto.map,
37 | nativeReduce = ArrayProto.reduce,
38 | nativeReduceRight = ArrayProto.reduceRight,
39 | nativeFilter = ArrayProto.filter,
40 | nativeEvery = ArrayProto.every,
41 | nativeSome = ArrayProto.some,
42 | nativeIndexOf = ArrayProto.indexOf,
43 | nativeLastIndexOf = ArrayProto.lastIndexOf,
44 | nativeIsArray = Array.isArray,
45 | nativeKeys = Object.keys,
46 | nativeBind = FuncProto.bind;
47 |
48 | // Create a safe reference to the Underscore object for use below.
49 | var _ = function(obj) { return new wrapper(obj); };
50 |
51 | // Export the Underscore object for **CommonJS**, with backwards-compatibility
52 | // for the old `require()` API. If we're not in CommonJS, add `_` to the
53 | // global object.
54 | if (typeof module !== 'undefined' && module.exports) {
55 | module.exports = _;
56 | _._ = _;
57 | } else {
58 | root._ = _;
59 | }
60 |
61 | // Current version.
62 | _.VERSION = '1.1.6';
63 |
64 | // Collection Functions
65 | // --------------------
66 |
67 | // The cornerstone, an `each` implementation, aka `forEach`.
68 | // Handles objects implementing `forEach`, arrays, and raw objects.
69 | // Delegates to **ECMAScript 5**'s native `forEach` if available.
70 | var each = _.each = _.forEach = function(obj, iterator, context) {
71 | if (obj == null) return;
72 | if (nativeForEach && obj.forEach === nativeForEach) {
73 | obj.forEach(iterator, context);
74 | } else if (_.isNumber(obj.length)) {
75 | for (var i = 0, l = obj.length; i < l; i++) {
76 | if (iterator.call(context, obj[i], i, obj) === breaker) return;
77 | }
78 | } else {
79 | for (var key in obj) {
80 | if (hasOwnProperty.call(obj, key)) {
81 | if (iterator.call(context, obj[key], key, obj) === breaker) return;
82 | }
83 | }
84 | }
85 | };
86 |
87 | // Return the results of applying the iterator to each element.
88 | // Delegates to **ECMAScript 5**'s native `map` if available.
89 | _.map = function(obj, iterator, context) {
90 | var results = [];
91 | if (obj == null) return results;
92 | if (nativeMap && obj.map === nativeMap) return obj.map(iterator, context);
93 | each(obj, function(value, index, list) {
94 | results[results.length] = iterator.call(context, value, index, list);
95 | });
96 | return results;
97 | };
98 |
99 | // **Reduce** builds up a single result from a list of values, aka `inject`,
100 | // or `foldl`. Delegates to **ECMAScript 5**'s native `reduce` if available.
101 | _.reduce = _.foldl = _.inject = function(obj, iterator, memo, context) {
102 | var initial = memo !== void 0;
103 | if (obj == null) obj = [];
104 | if (nativeReduce && obj.reduce === nativeReduce) {
105 | if (context) iterator = _.bind(iterator, context);
106 | return initial ? obj.reduce(iterator, memo) : obj.reduce(iterator);
107 | }
108 | each(obj, function(value, index, list) {
109 | if (!initial && index === 0) {
110 | memo = value;
111 | initial = true;
112 | } else {
113 | memo = iterator.call(context, memo, value, index, list);
114 | }
115 | });
116 | if (!initial) throw new TypeError("Reduce of empty array with no initial value");
117 | return memo;
118 | };
119 |
120 | // The right-associative version of reduce, also known as `foldr`.
121 | // Delegates to **ECMAScript 5**'s native `reduceRight` if available.
122 | _.reduceRight = _.foldr = function(obj, iterator, memo, context) {
123 | if (obj == null) obj = [];
124 | if (nativeReduceRight && obj.reduceRight === nativeReduceRight) {
125 | if (context) iterator = _.bind(iterator, context);
126 | return memo !== void 0 ? obj.reduceRight(iterator, memo) : obj.reduceRight(iterator);
127 | }
128 | var reversed = (_.isArray(obj) ? obj.slice() : _.toArray(obj)).reverse();
129 | return _.reduce(reversed, iterator, memo, context);
130 | };
131 |
132 | // Return the first value which passes a truth test. Aliased as `detect`.
133 | _.find = _.detect = function(obj, iterator, context) {
134 | var result;
135 | any(obj, function(value, index, list) {
136 | if (iterator.call(context, value, index, list)) {
137 | result = value;
138 | return true;
139 | }
140 | });
141 | return result;
142 | };
143 |
144 | // Return all the elements that pass a truth test.
145 | // Delegates to **ECMAScript 5**'s native `filter` if available.
146 | // Aliased as `select`.
147 | _.filter = _.select = function(obj, iterator, context) {
148 | var results = [];
149 | if (obj == null) return results;
150 | if (nativeFilter && obj.filter === nativeFilter) return obj.filter(iterator, context);
151 | each(obj, function(value, index, list) {
152 | if (iterator.call(context, value, index, list)) results[results.length] = value;
153 | });
154 | return results;
155 | };
156 |
157 | // Return all the elements for which a truth test fails.
158 | _.reject = function(obj, iterator, context) {
159 | var results = [];
160 | if (obj == null) return results;
161 | each(obj, function(value, index, list) {
162 | if (!iterator.call(context, value, index, list)) results[results.length] = value;
163 | });
164 | return results;
165 | };
166 |
167 | // Determine whether all of the elements match a truth test.
168 | // Delegates to **ECMAScript 5**'s native `every` if available.
169 | // Aliased as `all`.
170 | _.every = _.all = function(obj, iterator, context) {
171 | var result = true;
172 | if (obj == null) return result;
173 | if (nativeEvery && obj.every === nativeEvery) return obj.every(iterator, context);
174 | each(obj, function(value, index, list) {
175 | if (!(result = result && iterator.call(context, value, index, list))) return breaker;
176 | });
177 | return result;
178 | };
179 |
180 | // Determine if at least one element in the object matches a truth test.
181 | // Delegates to **ECMAScript 5**'s native `some` if available.
182 | // Aliased as `any`.
183 | var any = _.some = _.any = function(obj, iterator, context) {
184 | iterator || (iterator = _.identity);
185 | var result = false;
186 | if (obj == null) return result;
187 | if (nativeSome && obj.some === nativeSome) return obj.some(iterator, context);
188 | each(obj, function(value, index, list) {
189 | if (result = iterator.call(context, value, index, list)) return breaker;
190 | });
191 | return result;
192 | };
193 |
194 | // Determine if a given value is included in the array or object using `===`.
195 | // Aliased as `contains`.
196 | _.include = _.contains = function(obj, target) {
197 | var found = false;
198 | if (obj == null) return found;
199 | if (nativeIndexOf && obj.indexOf === nativeIndexOf) return obj.indexOf(target) != -1;
200 | any(obj, function(value) {
201 | if (found = value === target) return true;
202 | });
203 | return found;
204 | };
205 |
206 | // Invoke a method (with arguments) on every item in a collection.
207 | _.invoke = function(obj, method) {
208 | var args = slice.call(arguments, 2);
209 | return _.map(obj, function(value) {
210 | return (method.call ? method || value : value[method]).apply(value, args);
211 | });
212 | };
213 |
214 | // Convenience version of a common use case of `map`: fetching a property.
215 | _.pluck = function(obj, key) {
216 | return _.map(obj, function(value){ return value[key]; });
217 | };
218 |
219 | // Return the maximum element or (element-based computation).
220 | _.max = function(obj, iterator, context) {
221 | if (!iterator && _.isArray(obj)) return Math.max.apply(Math, obj);
222 | var result = {computed : -Infinity};
223 | each(obj, function(value, index, list) {
224 | var computed = iterator ? iterator.call(context, value, index, list) : value;
225 | computed >= result.computed && (result = {value : value, computed : computed});
226 | });
227 | return result.value;
228 | };
229 |
230 | // Return the minimum element (or element-based computation).
231 | _.min = function(obj, iterator, context) {
232 | if (!iterator && _.isArray(obj)) return Math.min.apply(Math, obj);
233 | var result = {computed : Infinity};
234 | each(obj, function(value, index, list) {
235 | var computed = iterator ? iterator.call(context, value, index, list) : value;
236 | computed < result.computed && (result = {value : value, computed : computed});
237 | });
238 | return result.value;
239 | };
240 |
241 | // Sort the object's values by a criterion produced by an iterator.
242 | _.sortBy = function(obj, iterator, context) {
243 | return _.pluck(_.map(obj, function(value, index, list) {
244 | return {
245 | value : value,
246 | criteria : iterator.call(context, value, index, list)
247 | };
248 | }).sort(function(left, right) {
249 | var a = left.criteria, b = right.criteria;
250 | return a < b ? -1 : a > b ? 1 : 0;
251 | }), 'value');
252 | };
253 |
254 | // Use a comparator function to figure out at what index an object should
255 | // be inserted so as to maintain order. Uses binary search.
256 | _.sortedIndex = function(array, obj, iterator) {
257 | iterator || (iterator = _.identity);
258 | var low = 0, high = array.length;
259 | while (low < high) {
260 | var mid = (low + high) >> 1;
261 | iterator(array[mid]) < iterator(obj) ? low = mid + 1 : high = mid;
262 | }
263 | return low;
264 | };
265 |
266 | // Safely convert anything iterable into a real, live array.
267 | _.toArray = function(iterable) {
268 | if (!iterable) return [];
269 | if (iterable.toArray) return iterable.toArray();
270 | if (_.isArray(iterable)) return iterable;
271 | if (_.isArguments(iterable)) return slice.call(iterable);
272 | return _.values(iterable);
273 | };
274 |
275 | // Return the number of elements in an object.
276 | _.size = function(obj) {
277 | return _.toArray(obj).length;
278 | };
279 |
280 | // Array Functions
281 | // ---------------
282 |
283 | // Get the first element of an array. Passing **n** will return the first N
284 | // values in the array. Aliased as `head`. The **guard** check allows it to work
285 | // with `_.map`.
286 | _.first = _.head = function(array, n, guard) {
287 | return (n != null) && !guard ? slice.call(array, 0, n) : array[0];
288 | };
289 |
290 | // Returns everything but the first entry of the array. Aliased as `tail`.
291 | // Especially useful on the arguments object. Passing an **index** will return
292 | // the rest of the values in the array from that index onward. The **guard**
293 | // check allows it to work with `_.map`.
294 | _.rest = _.tail = function(array, index, guard) {
295 | return slice.call(array, (index == null) || guard ? 1 : index);
296 | };
297 |
298 | // Get the last element of an array.
299 | _.last = function(array) {
300 | return array[array.length - 1];
301 | };
302 |
303 | // Trim out all falsy values from an array.
304 | _.compact = function(array) {
305 | return _.filter(array, function(value){ return !!value; });
306 | };
307 |
308 | // Return a completely flattened version of an array.
309 | _.flatten = function(array) {
310 | return _.reduce(array, function(memo, value) {
311 | if (_.isArray(value)) return memo.concat(_.flatten(value));
312 | memo[memo.length] = value;
313 | return memo;
314 | }, []);
315 | };
316 |
317 | // Return a version of the array that does not contain the specified value(s).
318 | _.without = function(array) {
319 | var values = slice.call(arguments, 1);
320 | return _.filter(array, function(value){ return !_.include(values, value); });
321 | };
322 |
323 | // Produce a duplicate-free version of the array. If the array has already
324 | // been sorted, you have the option of using a faster algorithm.
325 | // Aliased as `unique`.
326 | _.uniq = _.unique = function(array, isSorted) {
327 | return _.reduce(array, function(memo, el, i) {
328 | if (0 == i || (isSorted === true ? _.last(memo) != el : !_.include(memo, el))) memo[memo.length] = el;
329 | return memo;
330 | }, []);
331 | };
332 |
333 | // Produce an array that contains every item shared between all the
334 | // passed-in arrays.
335 | _.intersect = function(array) {
336 | var rest = slice.call(arguments, 1);
337 | return _.filter(_.uniq(array), function(item) {
338 | return _.every(rest, function(other) {
339 | return _.indexOf(other, item) >= 0;
340 | });
341 | });
342 | };
343 |
344 | // Zip together multiple lists into a single array -- elements that share
345 | // an index go together.
346 | _.zip = function() {
347 | var args = slice.call(arguments);
348 | var length = _.max(_.pluck(args, 'length'));
349 | var results = new Array(length);
350 | for (var i = 0; i < length; i++) results[i] = _.pluck(args, "" + i);
351 | return results;
352 | };
353 |
354 | // If the browser doesn't supply us with indexOf (I'm looking at you, **MSIE**),
355 | // we need this function. Return the position of the first occurrence of an
356 | // item in an array, or -1 if the item is not included in the array.
357 | // Delegates to **ECMAScript 5**'s native `indexOf` if available.
358 | // If the array is large and already in sort order, pass `true`
359 | // for **isSorted** to use binary search.
360 | _.indexOf = function(array, item, isSorted) {
361 | if (array == null) return -1;
362 | var i, l;
363 | if (isSorted) {
364 | i = _.sortedIndex(array, item);
365 | return array[i] === item ? i : -1;
366 | }
367 | if (nativeIndexOf && array.indexOf === nativeIndexOf) return array.indexOf(item);
368 | for (i = 0, l = array.length; i < l; i++) if (array[i] === item) return i;
369 | return -1;
370 | };
371 |
372 |
373 | // Delegates to **ECMAScript 5**'s native `lastIndexOf` if available.
374 | _.lastIndexOf = function(array, item) {
375 | if (array == null) return -1;
376 | if (nativeLastIndexOf && array.lastIndexOf === nativeLastIndexOf) return array.lastIndexOf(item);
377 | var i = array.length;
378 | while (i--) if (array[i] === item) return i;
379 | return -1;
380 | };
381 |
382 | // Generate an integer Array containing an arithmetic progression. A port of
383 | // the native Python `range()` function. See
384 | // [the Python documentation](http://docs.python.org/library/functions.html#range).
385 | _.range = function(start, stop, step) {
386 | if (arguments.length <= 1) {
387 | stop = start || 0;
388 | start = 0;
389 | }
390 | step = arguments[2] || 1;
391 |
392 | var len = Math.max(Math.ceil((stop - start) / step), 0);
393 | var idx = 0;
394 | var range = new Array(len);
395 |
396 | while(idx < len) {
397 | range[idx++] = start;
398 | start += step;
399 | }
400 |
401 | return range;
402 | };
403 |
404 | // Function (ahem) Functions
405 | // ------------------
406 |
407 | // Create a function bound to a given object (assigning `this`, and arguments,
408 | // optionally). Binding with arguments is also known as `curry`.
409 | // Delegates to **ECMAScript 5**'s native `Function.bind` if available.
410 | // We check for `func.bind` first, to fail fast when `func` is undefined.
411 | _.bind = function(func, obj) {
412 | if (func.bind === nativeBind && nativeBind) return nativeBind.apply(func, slice.call(arguments, 1));
413 | var args = slice.call(arguments, 2);
414 | return function() {
415 | return func.apply(obj, args.concat(slice.call(arguments)));
416 | };
417 | };
418 |
419 | // Bind all of an object's methods to that object. Useful for ensuring that
420 | // all callbacks defined on an object belong to it.
421 | _.bindAll = function(obj) {
422 | var funcs = slice.call(arguments, 1);
423 | if (funcs.length == 0) funcs = _.functions(obj);
424 | each(funcs, function(f) { obj[f] = _.bind(obj[f], obj); });
425 | return obj;
426 | };
427 |
428 | // Memoize an expensive function by storing its results.
429 | _.memoize = function(func, hasher) {
430 | var memo = {};
431 | hasher || (hasher = _.identity);
432 | return function() {
433 | var key = hasher.apply(this, arguments);
434 | return hasOwnProperty.call(memo, key) ? memo[key] : (memo[key] = func.apply(this, arguments));
435 | };
436 | };
437 |
438 | // Delays a function for the given number of milliseconds, and then calls
439 | // it with the arguments supplied.
440 | _.delay = function(func, wait) {
441 | var args = slice.call(arguments, 2);
442 | return setTimeout(function(){ return func.apply(func, args); }, wait);
443 | };
444 |
445 | // Defers a function, scheduling it to run after the current call stack has
446 | // cleared.
447 | _.defer = function(func) {
448 | return _.delay.apply(_, [func, 1].concat(slice.call(arguments, 1)));
449 | };
450 |
451 | // Internal function used to implement `_.throttle` and `_.debounce`.
452 | var limit = function(func, wait, debounce) {
453 | var timeout;
454 | return function() {
455 | var context = this, args = arguments;
456 | var throttler = function() {
457 | timeout = null;
458 | func.apply(context, args);
459 | };
460 | if (debounce) clearTimeout(timeout);
461 | if (debounce || !timeout) timeout = setTimeout(throttler, wait);
462 | };
463 | };
464 |
465 | // Returns a function, that, when invoked, will only be triggered at most once
466 | // during a given window of time.
467 | _.throttle = function(func, wait) {
468 | return limit(func, wait, false);
469 | };
470 |
471 | // Returns a function, that, as long as it continues to be invoked, will not
472 | // be triggered. The function will be called after it stops being called for
473 | // N milliseconds.
474 | _.debounce = function(func, wait) {
475 | return limit(func, wait, true);
476 | };
477 |
478 | // Returns a function that will be executed at most one time, no matter how
479 | // often you call it. Useful for lazy initialization.
480 | _.once = function(func) {
481 | var ran = false, memo;
482 | return function() {
483 | if (ran) return memo;
484 | ran = true;
485 | return memo = func.apply(this, arguments);
486 | };
487 | };
488 |
489 | // Returns the first function passed as an argument to the second,
490 | // allowing you to adjust arguments, run code before and after, and
491 | // conditionally execute the original function.
492 | _.wrap = function(func, wrapper) {
493 | return function() {
494 | var args = [func].concat(slice.call(arguments));
495 | return wrapper.apply(this, args);
496 | };
497 | };
498 |
499 | // Returns a function that is the composition of a list of functions, each
500 | // consuming the return value of the function that follows.
501 | _.compose = function() {
502 | var funcs = slice.call(arguments);
503 | return function() {
504 | var args = slice.call(arguments);
505 | for (var i=funcs.length-1; i >= 0; i--) {
506 | args = [funcs[i].apply(this, args)];
507 | }
508 | return args[0];
509 | };
510 | };
511 |
512 | // Returns a function that will only be executed after being called N times.
513 | _.after = function(times, func) {
514 | return function() {
515 | if (--times < 1) { return func.apply(this, arguments); }
516 | };
517 | };
518 |
519 |
520 | // Object Functions
521 | // ----------------
522 |
523 | // Retrieve the names of an object's properties.
524 | // Delegates to **ECMAScript 5**'s native `Object.keys`
525 | _.keys = nativeKeys || function(obj) {
526 | if (obj !== Object(obj)) throw new TypeError('Invalid object');
527 | var keys = [];
528 | for (var key in obj) if (hasOwnProperty.call(obj, key)) keys[keys.length] = key;
529 | return keys;
530 | };
531 |
532 | // Retrieve the values of an object's properties.
533 | _.values = function(obj) {
534 | return _.map(obj, _.identity);
535 | };
536 |
537 | // Return a sorted list of the function names available on the object.
538 | // Aliased as `methods`
539 | _.functions = _.methods = function(obj) {
540 | return _.filter(_.keys(obj), function(key){ return _.isFunction(obj[key]); }).sort();
541 | };
542 |
543 | // Extend a given object with all the properties in passed-in object(s).
544 | _.extend = function(obj) {
545 | each(slice.call(arguments, 1), function(source) {
546 | for (var prop in source) {
547 | if (source[prop] !== void 0) obj[prop] = source[prop];
548 | }
549 | });
550 | return obj;
551 | };
552 |
553 | // Fill in a given object with default properties.
554 | _.defaults = function(obj) {
555 | each(slice.call(arguments, 1), function(source) {
556 | for (var prop in source) {
557 | if (obj[prop] == null) obj[prop] = source[prop];
558 | }
559 | });
560 | return obj;
561 | };
562 |
563 | // Create a (shallow-cloned) duplicate of an object.
564 | _.clone = function(obj) {
565 | return _.isArray(obj) ? obj.slice() : _.extend({}, obj);
566 | };
567 |
568 | // Invokes interceptor with the obj, and then returns obj.
569 | // The primary purpose of this method is to "tap into" a method chain, in
570 | // order to perform operations on intermediate results within the chain.
571 | _.tap = function(obj, interceptor) {
572 | interceptor(obj);
573 | return obj;
574 | };
575 |
576 | // Perform a deep comparison to check if two objects are equal.
577 | _.isEqual = function(a, b) {
578 | // Check object identity.
579 | if (a === b) return true;
580 | // Different types?
581 | var atype = typeof(a), btype = typeof(b);
582 | if (atype != btype) return false;
583 | // Basic equality test (watch out for coercions).
584 | if (a == b) return true;
585 | // One is falsy and the other truthy.
586 | if ((!a && b) || (a && !b)) return false;
587 | // Unwrap any wrapped objects.
588 | if (a._chain) a = a._wrapped;
589 | if (b._chain) b = b._wrapped;
590 | // One of them implements an isEqual()?
591 | if (a.isEqual) return a.isEqual(b);
592 | // Check dates' integer values.
593 | if (_.isDate(a) && _.isDate(b)) return a.getTime() === b.getTime();
594 | // Both are NaN?
595 | if (_.isNaN(a) && _.isNaN(b)) return false;
596 | // Compare regular expressions.
597 | if (_.isRegExp(a) && _.isRegExp(b))
598 | return a.source === b.source &&
599 | a.global === b.global &&
600 | a.ignoreCase === b.ignoreCase &&
601 | a.multiline === b.multiline;
602 | // If a is not an object by this point, we can't handle it.
603 | if (atype !== 'object') return false;
604 | // Check for different array lengths before comparing contents.
605 | if (a.length && (a.length !== b.length)) return false;
606 | // Nothing else worked, deep compare the contents.
607 | var aKeys = _.keys(a), bKeys = _.keys(b);
608 | // Different object sizes?
609 | if (aKeys.length != bKeys.length) return false;
610 | // Recursive comparison of contents.
611 | for (var key in a) if (!(key in b) || !_.isEqual(a[key], b[key])) return false;
612 | return true;
613 | };
614 |
615 | // Is a given array or object empty?
616 | _.isEmpty = function(obj) {
617 | if (_.isArray(obj) || _.isString(obj)) return obj.length === 0;
618 | for (var key in obj) if (hasOwnProperty.call(obj, key)) return false;
619 | return true;
620 | };
621 |
622 | // Is a given value a DOM element?
623 | _.isElement = function(obj) {
624 | return !!(obj && obj.nodeType == 1);
625 | };
626 |
627 | // Is a given value an array?
628 | // Delegates to ECMA5's native Array.isArray
629 | _.isArray = nativeIsArray || function(obj) {
630 | return toString.call(obj) === '[object Array]';
631 | };
632 |
633 | // Is a given variable an arguments object?
634 | _.isArguments = function(obj) {
635 | return !!(obj && hasOwnProperty.call(obj, 'callee'));
636 | };
637 |
638 | // Is a given value a function?
639 | _.isFunction = function(obj) {
640 | return !!(obj && obj.constructor && obj.call && obj.apply);
641 | };
642 |
643 | // Is a given value a string?
644 | _.isString = function(obj) {
645 | return !!(obj === '' || (obj && obj.charCodeAt && obj.substr));
646 | };
647 |
648 | // Is a given value a number?
649 | _.isNumber = function(obj) {
650 | return !!(obj === 0 || (obj && obj.toExponential && obj.toFixed));
651 | };
652 |
653 | // Is the given value `NaN`? `NaN` happens to be the only value in JavaScript
654 | // that does not equal itself.
655 | _.isNaN = function(obj) {
656 | return obj !== obj;
657 | };
658 |
659 | // Is a given value a boolean?
660 | _.isBoolean = function(obj) {
661 | return obj === true || obj === false;
662 | };
663 |
664 | // Is a given value a date?
665 | _.isDate = function(obj) {
666 | return !!(obj && obj.getTimezoneOffset && obj.setUTCFullYear);
667 | };
668 |
669 | // Is the given value a regular expression?
670 | _.isRegExp = function(obj) {
671 | return !!(obj && obj.test && obj.exec && (obj.ignoreCase || obj.ignoreCase === false));
672 | };
673 |
674 | // Is a given value equal to null?
675 | _.isNull = function(obj) {
676 | return obj === null;
677 | };
678 |
679 | // Is a given variable undefined?
680 | _.isUndefined = function(obj) {
681 | return obj === void 0;
682 | };
683 |
684 | // Utility Functions
685 | // -----------------
686 |
687 | // Run Underscore.js in *noConflict* mode, returning the `_` variable to its
688 | // previous owner. Returns a reference to the Underscore object.
689 | _.noConflict = function() {
690 | root._ = previousUnderscore;
691 | return this;
692 | };
693 |
694 | // Keep the identity function around for default iterators.
695 | _.identity = function(value) {
696 | return value;
697 | };
698 |
699 | // Run a function **n** times.
700 | _.times = function (n, iterator, context) {
701 | for (var i = 0; i < n; i++) iterator.call(context, i);
702 | };
703 |
704 | // Add your own custom functions to the Underscore object, ensuring that
705 | // they're correctly added to the OOP wrapper as well.
706 | _.mixin = function(obj) {
707 | each(_.functions(obj), function(name){
708 | addToWrapper(name, _[name] = obj[name]);
709 | });
710 | };
711 |
712 | // Generate a unique integer id (unique within the entire client session).
713 | // Useful for temporary DOM ids.
714 | var idCounter = 0;
715 | _.uniqueId = function(prefix) {
716 | var id = idCounter++;
717 | return prefix ? prefix + id : id;
718 | };
719 |
720 | // By default, Underscore uses ERB-style template delimiters, change the
721 | // following template settings to use alternative delimiters.
722 | _.templateSettings = {
723 | evaluate : /<%([\s\S]+?)%>/g,
724 | interpolate : /<%=([\s\S]+?)%>/g
725 | };
726 |
727 | // JavaScript micro-templating, similar to John Resig's implementation.
728 | // Underscore templating handles arbitrary delimiters, preserves whitespace,
729 | // and correctly escapes quotes within interpolated code.
730 | _.template = function(str, data) {
731 | var c = _.templateSettings;
732 | var tmpl = 'var __p=[],print=function(){__p.push.apply(__p,arguments);};' +
733 | 'with(obj||{}){__p.push(\'' +
734 | str.replace(/\\/g, '\\\\')
735 | .replace(/'/g, "\\'")
736 | .replace(c.interpolate, function(match, code) {
737 | return "'," + code.replace(/\\'/g, "'") + ",'";
738 | })
739 | .replace(c.evaluate || null, function(match, code) {
740 | return "');" + code.replace(/\\'/g, "'")
741 | .replace(/[\r\n\t]/g, ' ') + "__p.push('";
742 | })
743 | .replace(/\r/g, '\\r')
744 | .replace(/\n/g, '\\n')
745 | .replace(/\t/g, '\\t')
746 | + "');}return __p.join('');";
747 | var func = new Function('obj', tmpl);
748 | return data ? func(data) : func;
749 | };
750 |
751 | // The OOP Wrapper
752 | // ---------------
753 |
754 | // If Underscore is called as a function, it returns a wrapped object that
755 | // can be used OO-style. This wrapper holds altered versions of all the
756 | // underscore functions. Wrapped objects may be chained.
757 | var wrapper = function(obj) { this._wrapped = obj; };
758 |
759 | // Expose `wrapper.prototype` as `_.prototype`
760 | _.prototype = wrapper.prototype;
761 |
762 | // Helper function to continue chaining intermediate results.
763 | var result = function(obj, chain) {
764 | return chain ? _(obj).chain() : obj;
765 | };
766 |
767 | // A method to easily add functions to the OOP wrapper.
768 | var addToWrapper = function(name, func) {
769 | wrapper.prototype[name] = function() {
770 | var args = slice.call(arguments);
771 | unshift.call(args, this._wrapped);
772 | return result(func.apply(_, args), this._chain);
773 | };
774 | };
775 |
776 | // Add all of the Underscore functions to the wrapper object.
777 | _.mixin(_);
778 |
779 | // Add all mutator Array functions to the wrapper.
780 | each(['pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'], function(name) {
781 | var method = ArrayProto[name];
782 | wrapper.prototype[name] = function() {
783 | method.apply(this._wrapped, arguments);
784 | return result(this._wrapped, this._chain);
785 | };
786 | });
787 |
788 | // Add all accessor Array functions to the wrapper.
789 | each(['concat', 'join', 'slice'], function(name) {
790 | var method = ArrayProto[name];
791 | wrapper.prototype[name] = function() {
792 | return result(method.apply(this._wrapped, arguments), this._chain);
793 | };
794 | });
795 |
796 | // Start chaining a wrapped Underscore object.
797 | wrapper.prototype.chain = function() {
798 | this._chain = true;
799 | return this;
800 | };
801 |
802 | // Extracts the result from a wrapped and chained object.
803 | wrapper.prototype.value = function() {
804 | return this._wrapped;
805 | };
806 |
807 | })();
--------------------------------------------------------------------------------
/styles/reset.css:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/michael/reader/d6fbd8d90e66bab06cd0c8a77c7ebeee864a7b56/styles/reset.css
--------------------------------------------------------------------------------
/styles/styles.css:
--------------------------------------------------------------------------------
1 | /*
2 | Helpers
3 | ----------------------
4 | */
5 |
6 | * {
7 | box-sizing:border-box;
8 | -webkit-box-sizing:border-box;
9 | -moz-box-sizing:border-box;
10 | }
11 |
12 | .clear { clear: both; }
13 | .hidden { display: none; }
14 | .invisible { opacity: 0; }
15 | .right { float: right; }
16 | .left { float: left; }
17 | .nobr {white-space: nowrap}
18 | .right-align {text-align: right; }
19 |
20 | .serif {
21 | font-family: 'Palatino Linotype', 'Book Antiqua', Palatino, FreeSerif, serif;
22 | font-weight: lighter;
23 | }
24 |
25 | .italic {
26 | font-style: italic;
27 | }
28 |
29 | /* Common styles
30 | -------------------------------------------------------------------------------*/
31 |
32 | html {
33 | margin: 0; padding: 0;
34 | -webkit-font-smoothing: antialiased;
35 | }
36 |
37 | img { border: none; }
38 |
39 | body {
40 | margin: 0; padding: 0;
41 | background: #F0F1EB;
42 | color:#444;
43 | font:14px/26px "Helvetica Neue", Arial, Helvetica, Geneva, sans-serif; color:#111;
44 | line-height:1.5em;
45 | }
46 |
47 | a {
48 | text-decoration:none;
49 | color: #51483D;
50 | border-bottom: 1px solid #ccc;
51 | padding: 0 0 1px 0;
52 | }
53 |
54 | a:hover {
55 | color: #000;
56 | border-bottom: 1px solid #999;
57 | }
58 |
59 |
60 | p { margin: 0 0 10px 0; }
61 | p.info {
62 | font-style: italic;
63 | color: rgba(0, 0, 0, 0.7);
64 | }
65 |
66 |
67 | h1, h2, h3, h4, h5, h6 { padding: 10px 0 5px 0; }
68 | h3, h4, h5, h6 { padding-top: 20px; }
69 |
70 | /* Title */
71 | h1 {
72 | padding-top: 0px; margin-top: 0;
73 | font-family: 'Palatino Linotype', 'Book Antiqua', Palatino, FreeSerif, serif;
74 | font-size: 35px;
75 | line-height: 40px;
76 | }
77 | h2 { font-size: 120%; padding-bottom: 15px; }
78 |
79 | /* Common styles
80 | -------------------------------------------------------------------------------*/
81 |
82 | #container { width: 1100px; margin: auto; }
83 |
84 |
85 | div {
86 | -moz-transition-duration: 1s;
87 | -webkit-transition-duration: 1s;
88 | -o-transition-duration: 1s;
89 | transition-duration: 1s;
90 | }
91 |
92 | /* Header */
93 |
94 | #header {
95 | font-family: 'HelveticaNeue-Light';
96 | color: #383D3D;
97 | font-size: 24px;
98 | overflow: auto;
99 | padding: 40px 0;
100 | }
101 |
102 | #header .title { float: right; width: 720px; }
103 | #header .channel { float: left; }
104 |
105 |
106 | /* Main */
107 |
108 | #wrapper { }
109 |
110 | /* Browser */
111 |
112 | #browser { float: left; width: 330px; }
113 |
114 |
115 | #documents a { border: none; }
116 | #documents .document {
117 | padding: 10px;
118 | margin-bottom: 5px;
119 | }
120 |
121 | #documents .document:hover {
122 | background: rgba(54, 73,39, 0.2)
123 | }
124 |
125 | #documents .document .title {
126 | font-weight: bold;
127 | font-size: 17px;
128 | }
129 |
130 | #documents .document .teaser {
131 | margin-top: 5px;
132 | color: #7C7C7C;
133 | font-size: 12px;
134 | }
135 |
136 | #documents .document .published {
137 | color: #383D3D;
138 | font-size: 14px;
139 | }
140 |
141 |
142 | #documents .document.active .published { color: #CCDDB8; }
143 | #documents .document.active .title { color: #CCDDB8; }
144 | #documents .document.active .teaser { color: #719952; }
145 |
146 |
147 |
148 |
149 |
150 | #documents .document.active {
151 | background: #26351C;
152 | color: rgba(255,255,255, 0.8);
153 | }
154 |
155 | /* Documents */
156 |
157 | #document {
158 | font-family: 'Palatino Linotype', 'Book Antiqua', Palatino, FreeSerif, serif;
159 | float: right;
160 | width: 720px;
161 | line-height: 1.4em;
162 | font-size: 1.15em;
163 | counter-reset: section;
164 | }
165 |
166 | #document h2, h3, h4 {
167 | font-family: 'Helvetica Neue';
168 | font-weight: bold;
169 |
170 | }
171 |
172 | #document h2:before{
173 | counter-increment: section;
174 | content: counter(section) "";
175 |
176 | background: #364927;
177 | color: #D1E0E0;
178 |
179 | margin-right: 20px;
180 | padding: 2px 5px;
181 | font-weight: bold;
182 | }
183 |
184 | #document #lead {
185 | font-family: 'Helvetica Neue';
186 | font-size: 21px;
187 | line-height: 25px;
188 | color: #989998;
189 | margin-bottom: 20px;
190 | }
191 |
192 | #footer {
193 | margin-top: 50px;
194 | margin: 20px 0;
195 | margin-left: 380px;
196 | }
197 |
198 | /* Document Styles */
199 |
200 | code, pre {
201 | font-family: Monaco, Consolas, "Lucida Console", monospace;
202 | font-size: 12px;
203 | }
204 |
205 | code {
206 | padding: 0px 3px;
207 | background: white;
208 | border: 1px solid #DDD;
209 | zoom: 1;
210 | }
211 |
212 | pre {
213 | padding: 2px 10px 2px 15px;
214 | border-left: 4px solid #BBB;
215 | margin: 10px 0px;
216 | background: white;
217 | }
218 |
219 | pre code {
220 | background: none;
221 | border-color: transparent;
222 | }
223 |
224 |
--------------------------------------------------------------------------------