├── app ├── js │ ├── templates │ │ ├── lists │ │ │ ├── menuitem.html │ │ │ └── form.html │ │ ├── auth.html │ │ ├── tasks │ │ │ ├── task.html │ │ │ ├── index.html │ │ │ └── edit.html │ │ └── app.html │ ├── models │ │ ├── tasklist.js │ │ └── task.js │ ├── collections │ │ ├── tasks.js │ │ └── tasklists.js │ ├── config.js │ ├── main.js │ ├── views │ │ ├── auth.js │ │ ├── lists │ │ │ ├── add.js │ │ │ ├── menu.js │ │ │ ├── edit.js │ │ │ └── menuitem.js │ │ ├── app.js │ │ └── tasks │ │ │ ├── edit.js │ │ │ ├── task.js │ │ │ └── index.js │ ├── app.js │ ├── gapi.js │ └── lib │ │ ├── underscore-min.js │ │ ├── text.js │ │ ├── backbone-min.js │ │ └── backbone.js ├── img │ ├── glyphicons-halflings.png │ └── glyphicons-halflings-white.png ├── index.html └── css │ └── app.css ├── test ├── app.test.js ├── setup.js ├── index.html ├── lists.test.js └── fixtures │ └── gapi.js ├── require-config.js ├── README.md ├── package.json ├── server.js └── grunt.js /app/js/templates/lists/menuitem.html: -------------------------------------------------------------------------------- 1 | {{title}} 2 | -------------------------------------------------------------------------------- /app/js/templates/auth.html: -------------------------------------------------------------------------------- 1 | Sign In with Google 2 | -------------------------------------------------------------------------------- /app/img/glyphicons-halflings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stereobooster/dailyjs-backbone-tutorial/master/app/img/glyphicons-halflings.png -------------------------------------------------------------------------------- /test/app.test.js: -------------------------------------------------------------------------------- 1 | suite('App', function() { 2 | test('Should be present', function() { 3 | assert.ok(window.bTask); 4 | }); 5 | }); 6 | -------------------------------------------------------------------------------- /test/setup.js: -------------------------------------------------------------------------------- 1 | var assert = chai.assert; 2 | 3 | mocha.setup({ 4 | ui: 'tdd' 5 | , globals: ['bTask', 'gapi', '___jsl', 'confirm'] 6 | }); 7 | -------------------------------------------------------------------------------- /app/img/glyphicons-halflings-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stereobooster/dailyjs-backbone-tutorial/master/app/img/glyphicons-halflings-white.png -------------------------------------------------------------------------------- /app/js/models/tasklist.js: -------------------------------------------------------------------------------- 1 | define(function() { 2 | var TaskList = Backbone.Model.extend({ 3 | url: 'tasklists' 4 | }); 5 | 6 | return TaskList; 7 | }); 8 | -------------------------------------------------------------------------------- /require-config.js: -------------------------------------------------------------------------------- 1 | ({ 2 | appDir: 'app/' 3 | , baseUrl: 'js' 4 | , paths: {} 5 | , dir: 'build/' 6 | , mainConfigFile : 'app/js/main.js' 7 | , modules: [{ name: 'main' }] 8 | }) 9 | -------------------------------------------------------------------------------- /app/js/models/task.js: -------------------------------------------------------------------------------- 1 | define(function() { 2 | var Task = Backbone.Model.extend({ 3 | url: 'tasks', 4 | defaults: { title: '', notes: '' } 5 | }); 6 | 7 | return Task; 8 | }); 9 | -------------------------------------------------------------------------------- /app/js/collections/tasks.js: -------------------------------------------------------------------------------- 1 | define(['models/task'], function(Task) { 2 | var Tasks = Backbone.Collection.extend({ 3 | model: Task, 4 | url: 'tasks' 5 | }); 6 | 7 | return Tasks; 8 | }); 9 | -------------------------------------------------------------------------------- /app/js/collections/tasklists.js: -------------------------------------------------------------------------------- 1 | define(['models/tasklist'], function(TaskList) { 2 | var TaskLists = Backbone.Collection.extend({ 3 | model: TaskList 4 | , url: 'tasklists' 5 | }); 6 | 7 | return TaskLists; 8 | }); 9 | -------------------------------------------------------------------------------- /app/js/templates/tasks/task.html: -------------------------------------------------------------------------------- 1 | 2 | {{title}} 3 | {{notes}} 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This repository is from a tutorial for http://dailyjs.com 2 | 3 | To use the code, you need to get an Google Tasks API key and Client ID. Set these values in `app/js/config.js`. See [DailyJS](http://dailyjs.com) for details. 4 | -------------------------------------------------------------------------------- /app/js/config.js: -------------------------------------------------------------------------------- 1 | define([], function() { 2 | var config = {}; 3 | config.apiKey = ''; 4 | config.scopes = 'https://www.googleapis.com/auth/tasks https://www.googleapis.com/auth/userinfo.profile'; 5 | config.clientId = ''; 6 | 7 | _.templateSettings = { 8 | interpolate: /\{\{(.+?)\}\}/g 9 | }; 10 | 11 | return config; 12 | }); 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "btask" 3 | , "version": "0.0.1" 4 | , "private": true 5 | , "dependencies": { 6 | "requirejs": "latest" 7 | , "connect": "2.7.0" 8 | } 9 | , "devDependencies": { 10 | "mocha": "latest" 11 | , "chai": "latest" 12 | , "grunt": "latest" 13 | , "grunt-exec": "latest" 14 | } 15 | , "scripts": { 16 | "grunt": "node_modules/.bin/grunt" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | var connect = require('connect') 2 | , http = require('http') 3 | , app 4 | ; 5 | 6 | app = connect() 7 | .use(connect.static('app')) 8 | .use('/js/lib/', connect.static('node_modules/requirejs/')) 9 | .use('/node_modules', connect.static('node_modules')) 10 | .use('/test', connect.static('test/')) 11 | .use('/test', connect.static('app')) 12 | ; 13 | 14 | http.createServer(app).listen(8080, function() { 15 | console.log('Running on http://localhost:8080'); 16 | }); 17 | -------------------------------------------------------------------------------- /app/js/main.js: -------------------------------------------------------------------------------- 1 | requirejs.config({ 2 | baseUrl: 'js', 3 | 4 | paths: { 5 | text: 'lib/text' 6 | }, 7 | 8 | shim: { 9 | 'lib/underscore-min': { 10 | exports: '_' 11 | }, 12 | 'lib/backbone': { 13 | deps: ['lib/underscore-min'] 14 | , exports: 'Backbone' 15 | }, 16 | 'app': { 17 | deps: ['lib/underscore-min', 'lib/backbone'] 18 | } 19 | } 20 | }); 21 | 22 | require([ 23 | 'app' 24 | ], 25 | 26 | function(App) { 27 | window.bTask = new App(); 28 | }); 29 | -------------------------------------------------------------------------------- /app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | bTask 5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /grunt.js: -------------------------------------------------------------------------------- 1 | module.exports = function(grunt) { 2 | grunt.loadNpmTasks('grunt-exec'); 3 | 4 | grunt.initConfig({ 5 | exec: { 6 | build: { 7 | command: 'node node_modules/requirejs/bin/r.js -o require-config.js' 8 | } 9 | } 10 | }); 11 | 12 | grunt.registerTask('copy-require', function() { 13 | grunt.file.mkdir('build/js/lib'); 14 | grunt.file.copy('node_modules/requirejs/require.js', 'build/js/lib/require.js'); 15 | }); 16 | 17 | grunt.registerTask('default', 'exec copy-require'); 18 | }; 19 | -------------------------------------------------------------------------------- /app/js/templates/tasks/index.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 5 | 6 |
7 |
8 | 9 |
10 |
11 |
12 |
13 | Note: Select a task to edit or delete it. 14 |
15 |
16 | -------------------------------------------------------------------------------- /app/js/views/auth.js: -------------------------------------------------------------------------------- 1 | define(['text!templates/auth.html'], function(template) { 2 | var AuthView = Backbone.View.extend({ 3 | el: '#sign-in-container', 4 | template: _.template(template), 5 | 6 | events: { 7 | 'click #authorize-button': 'auth' 8 | }, 9 | 10 | initialize: function(app) { 11 | this.app = app; 12 | }, 13 | 14 | render: function() { 15 | this.$el.html(this.template()); 16 | return this; 17 | }, 18 | 19 | auth: function() { 20 | this.app.apiManager.checkAuth(); 21 | return false; 22 | } 23 | }); 24 | 25 | return AuthView; 26 | }); 27 | -------------------------------------------------------------------------------- /app/js/views/lists/add.js: -------------------------------------------------------------------------------- 1 | define([ 2 | 'models/tasklist' 3 | , 'views/lists/edit' 4 | ], 5 | 6 | function(TaskList, EditListView) { 7 | var AddListView = EditListView.extend({ 8 | submit: function() { 9 | var self = this 10 | , title = this.$el.find('input[name="title"]').val() 11 | ; 12 | 13 | this.model.save({ title: title }, { success: function(model) { 14 | // Add the updated model to the collection 15 | bTask.collections.lists.add(model); 16 | self.remove(); 17 | }}); 18 | 19 | return false; 20 | } 21 | }); 22 | 23 | return AddListView; 24 | }); 25 | -------------------------------------------------------------------------------- /app/js/templates/lists/form.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | Edit List 4 | 5 | 6 |
7 | 8 |
9 | 10 |
11 |
12 |
13 |
14 | 15 | 16 |
17 | -------------------------------------------------------------------------------- /app/js/templates/app.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

bTask

4 | 5 |
6 |
7 | 8 |
9 |
10 |
11 | 20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 | -------------------------------------------------------------------------------- /test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | bTask Tests 5 | 6 | 9 | 10 | 11 |
12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /app/js/app.js: -------------------------------------------------------------------------------- 1 | define([ 2 | 'gapi' 3 | , 'views/app' 4 | , 'views/auth' 5 | , 'views/lists/menu' 6 | , 'collections/tasklists' 7 | , 'collections/tasks' 8 | ], 9 | 10 | function(ApiManager, AppView, AuthView, ListMenuView, TaskLists, Tasks) { 11 | var App = function() { 12 | this.collections.lists = new TaskLists(); 13 | this.views.app = new AppView(); 14 | this.views.app.render(); 15 | this.views.auth = new AuthView(this); 16 | this.views.auth.render(); 17 | this.views.listMenu = new ListMenuView({ collection: this.collections.lists }); 18 | 19 | this.connectGapi(); 20 | }; 21 | 22 | App.prototype = { 23 | views: {}, 24 | collections: {}, 25 | connectGapi: function() { 26 | var self = this; 27 | this.apiManager = new ApiManager(this); 28 | this.apiManager.on('ready', function() { 29 | self.collections.lists.fetch({ data: { userId: '@me' }, success: function(collection, res, req) { 30 | self.views.listMenu.render(); 31 | }}); 32 | }); 33 | } 34 | }; 35 | 36 | return App; 37 | }); 38 | -------------------------------------------------------------------------------- /app/js/views/lists/menu.js: -------------------------------------------------------------------------------- 1 | define(['views/lists/menuitem'], function(ListMenuItemView) { 2 | var ListMenuView = Backbone.View.extend({ 3 | el: '.left-nav', 4 | tagName: 'ul', 5 | className: 'nav nav-list lists-nav', 6 | 7 | events: { 8 | }, 9 | 10 | initialize: function() { 11 | this.collection.on('add', this.renderMenuItem, this); 12 | }, 13 | 14 | renderMenuItem: function(model) { 15 | var item = new ListMenuItemView({ model: model }); 16 | this.$el.append(item.render().el); 17 | 18 | if (!bTask.views.activeListMenuItem) { 19 | bTask.views.activeListMenuItem = item; 20 | } 21 | 22 | if (model.get('id') === bTask.views.activeListMenuItem.model.get('id')) { 23 | item.open(); 24 | } 25 | }, 26 | 27 | render: function() { 28 | var $el = $(this.el) 29 | , self = this; 30 | 31 | this.collection.each(function(list) { 32 | self.renderMenuItem(list); 33 | }); 34 | 35 | return this; 36 | } 37 | }); 38 | 39 | return ListMenuView; 40 | }); 41 | -------------------------------------------------------------------------------- /app/js/templates/tasks/edit.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | Task Properties 4 | 5 | 6 |
7 | 8 | 9 |
10 |
11 | 12 | 13 |
14 | 15 |
16 | 17 | 18 |
19 |
20 |
21 | 22 | 23 |
24 | -------------------------------------------------------------------------------- /app/js/views/lists/edit.js: -------------------------------------------------------------------------------- 1 | define(['text!templates/lists/form.html'], function(template) { 2 | var EditListView = Backbone.View.extend({ 3 | tagName: 'form', 4 | className: 'form-horizontal well edit-list', 5 | template: _.template(template), 6 | 7 | events: { 8 | 'submit': 'submit' 9 | , 'click .cancel': 'cancel' 10 | }, 11 | 12 | initialize: function() { 13 | this.model.on('change', this.render, this); 14 | }, 15 | 16 | render: function() { 17 | var $el = $(this.el); 18 | $el.html(this.template(this.model.toJSON())); 19 | 20 | if (!this.model.get('id')) { 21 | this.$el.find('legend').html('Add List'); 22 | } 23 | 24 | return this; 25 | }, 26 | 27 | submit: function() { 28 | var self = this 29 | , title = this.$el.find('input[name="title"]').val() 30 | ; 31 | 32 | this.model.save({ title: title }, { 33 | success: function() { 34 | self.remove(); 35 | } 36 | }); 37 | 38 | return false; 39 | }, 40 | 41 | cancel: function() { 42 | this.$el.hide(); 43 | return false; 44 | } 45 | }); 46 | 47 | return EditListView; 48 | }); 49 | -------------------------------------------------------------------------------- /app/css/app.css: -------------------------------------------------------------------------------- 1 | #main { padding-left: 0; height: 100% } 2 | #sign-in-container, #signed-in-container { display: none } 3 | h1 { margin: 0 0 10px 0; padding: 0; text-align: center } 4 | 5 | .main-left-col { height: 100%; position: fixed; padding-top: 10px; box-shadow: inset -1px 0 3px #000; overflow: auto; background-color: #999} 6 | 7 | .left-nav { list-style-type: none; margin: 0; padding: 0 } 8 | .left-nav li a { display: block; text-shadow: 1px 1px #000; color: #fff; padding: 10px; width: 100% } 9 | .left-nav li.active a { background-color: #fff; color: #333; text-shadow: none; font-weight: bold } 10 | .left-nav li a:hover { text-decoration: none; background-color: #fff; color: #333; text-shadow: none; font-weight: bold } 11 | 12 | .main-right-col { margin-left: 16.5%; margin-top: 10px } 13 | 14 | #top-nav { margin-bottom: 10px; padding-bottom: 10px } 15 | #list-editor { display: none; } 16 | 17 | #add-task form, .task { padding: 5px; margin: 0 0 5px 0 } 18 | #add-task input[name="title"] { width: 80% } 19 | 20 | #task-list { margin: 0; padding: 0; list-style-type: none } 21 | .task { vertical-align: middle } 22 | .task { padding: 8px; margin: 0 0 5px 0; } 23 | .task.active { border: 1px solid #049cdb } 24 | .task .notes { color: #888 } 25 | .task input { margin-right: 3px } 26 | 27 | .completed { text-decoration: line-through; } 28 | -------------------------------------------------------------------------------- /app/js/views/lists/menuitem.js: -------------------------------------------------------------------------------- 1 | define(['text!templates/lists/menuitem.html', 'views/tasks/index', 'collections/tasks'], function(template, TasksIndexView, Tasks) { 2 | var ListMenuItemView = Backbone.View.extend({ 3 | tagName: 'li', 4 | className: 'list-menu-item', 5 | 6 | template: _.template(template), 7 | 8 | events: { 9 | 'click': 'open' 10 | }, 11 | 12 | initialize: function() { 13 | this.model.on('change', this.render, this); 14 | this.model.on('destroy', this.remove, this); 15 | }, 16 | 17 | render: function() { 18 | var $el = $(this.el); 19 | $el.html(this.template(this.model.toJSON())); 20 | return this; 21 | }, 22 | 23 | open: function() { 24 | if (bTask.views.activeListMenuItem) { 25 | bTask.views.activeListMenuItem.$el.removeClass('active'); 26 | } 27 | 28 | bTask.views.activeListMenuItem = this; 29 | this.$el.addClass('active'); 30 | 31 | // Render the tasks 32 | if (bTask.views.tasksIndexView) { 33 | bTask.views.tasksIndexView.remove(); 34 | } 35 | 36 | bTask.views.tasksIndexView = new TasksIndexView({ collection: new Tasks({ tasklist: this.model.get('id') }), model: this.model }); 37 | bTask.views.app.$el.find('#tasks-container').html(bTask.views.tasksIndexView.render().el); 38 | 39 | return false; 40 | } 41 | }); 42 | 43 | return ListMenuItemView; 44 | }); 45 | -------------------------------------------------------------------------------- /app/js/views/app.js: -------------------------------------------------------------------------------- 1 | define([ 2 | 'text!templates/app.html' 3 | , 'views/lists/add' 4 | , 'views/lists/edit' 5 | ], 6 | 7 | function(template, AddListView, EditListView) { 8 | var AppView = Backbone.View.extend({ 9 | id: 'main', 10 | tagName: 'div', 11 | className: 'container-fluid', 12 | el: '#todo-app', 13 | template: _.template(template), 14 | 15 | events: { 16 | 'click #add-list-button': 'addList' 17 | , 'click #edit-list-button': 'editList' 18 | , 'click #delete-list-button': 'deleteList' 19 | }, 20 | 21 | initialize: function() { 22 | }, 23 | 24 | render: function() { 25 | this.$el.html(this.template()); 26 | return this; 27 | }, 28 | 29 | listForm: function(form) { 30 | this.$el.find('#list-editor').html(form.render().el).show(); 31 | form.$el.find('input:first').focus(); 32 | 33 | return false; 34 | }, 35 | 36 | addList: function() { 37 | return this.listForm(new AddListView({ model: new bTask.collections.lists.model({ title: '' }) })); 38 | }, 39 | 40 | editList: function() { 41 | return this.listForm(new EditListView({ model: bTask.views.activeListMenuItem.model })); 42 | }, 43 | 44 | deleteList: function() { 45 | if (confirm('Are you sure you want to delete that list?')) { 46 | bTask.views.activeListMenuItem.model.destroy(); 47 | } 48 | return false; 49 | } 50 | }); 51 | 52 | return AppView; 53 | }); 54 | -------------------------------------------------------------------------------- /app/js/views/tasks/edit.js: -------------------------------------------------------------------------------- 1 | define(['text!templates/tasks/edit.html'], function(template) { 2 | var TaskEditView = Backbone.View.extend({ 3 | tagName: 'form', 4 | className: 'well edit-task', 5 | template: _.template(template), 6 | 7 | events: { 8 | 'submit': 'submit' 9 | , 'click .cancel': 'cancel' 10 | , 'click .delete-task': 'destroy' 11 | }, 12 | 13 | initialize: function() { 14 | this.model.on('change', this.render, this); 15 | this.model.on('destroy', this.remove, this); 16 | }, 17 | 18 | render: function() { 19 | this.$el.html(this.template(this.model.toJSON())); 20 | return this; 21 | }, 22 | 23 | submit: function() { 24 | var title = this.$el.find('input[name="title"]').val() 25 | , notes = this.$el.find('textarea[name="notes"]').val() 26 | , status = this.$el.find('input[name="status"]:checked').val() 27 | ; 28 | 29 | this.model.set('title', title); 30 | this.model.set('notes', notes); 31 | 32 | if (status !== this.model.get('status')) { 33 | this.model.set('status', status); 34 | if (status === 'needsAction') { 35 | this.model.set('completed', null); 36 | } 37 | } 38 | 39 | this.model.save(null, { 40 | success: function() { 41 | // TODO: Show feedback 42 | } 43 | }); 44 | 45 | return false; 46 | }, 47 | 48 | cancel: function() { 49 | this.remove(); 50 | return false; 51 | }, 52 | 53 | destroy: function() { 54 | this.model.destroy(); 55 | return false; 56 | } 57 | }); 58 | 59 | return TaskEditView; 60 | }); 61 | -------------------------------------------------------------------------------- /app/js/views/tasks/task.js: -------------------------------------------------------------------------------- 1 | define(['text!templates/tasks/task.html', 'views/tasks/edit'], function(template, TaskEditView) { 2 | var TaskView = Backbone.View.extend({ 3 | tagName: 'li', 4 | className: 'controls well task row', 5 | 6 | template: _.template(template), 7 | 8 | events: { 9 | 'click': 'open' 10 | , 'change .check-task': 'toggle' 11 | }, 12 | 13 | initialize: function(options) { 14 | this.parentView = options.parentView; 15 | this.model.on('change', this.render, this); 16 | this.model.on('destroy', this.remove, this); 17 | }, 18 | 19 | render: function(e) { 20 | var $el = $(this.el); 21 | $el.data('taskId', this.model.get('id')); 22 | $el.html(this.template(this.model.toJSON())); 23 | $el.find('.check-task').attr('checked', this.model.get('status') === 'completed'); 24 | 25 | return this; 26 | }, 27 | 28 | open: function(e) { 29 | if (this.parentView.activeTaskView) { 30 | this.parentView.activeTaskView.close(); 31 | } 32 | this.$el.addClass('active'); 33 | this.parentView.activeTaskView = this; 34 | this.parentView.editTask(this.model); 35 | }, 36 | 37 | close: function(e) { 38 | this.$el.removeClass('active'); 39 | }, 40 | 41 | toggle: function() { 42 | var id = this.model.get('id') 43 | , $el = this.$el.find('.check-task') 44 | ; 45 | 46 | this.model.set('status', $el.attr('checked') ? 'completed' : 'needsAction'); 47 | if (this.model.get('status') === 'needsAction') { 48 | this.model.set('completed', null); 49 | } 50 | 51 | this.model.save(); 52 | return false; 53 | } 54 | }); 55 | 56 | return TaskView; 57 | }); 58 | -------------------------------------------------------------------------------- /test/lists.test.js: -------------------------------------------------------------------------------- 1 | suite('Lists', function() { 2 | var spyUpdate = sinon.spy(gapi.client.tasks.tasklists, 'update') 3 | , spyCreate = sinon.spy(gapi.client.tasks.tasklists, 'insert') 4 | , spyDelete = sinon.spy(gapi.client.tasks.tasklists, 'delete') 5 | ; 6 | 7 | setup(function() { 8 | spyUpdate.reset(); 9 | spyCreate.reset(); 10 | }); 11 | 12 | test('Creating a list', function() { 13 | var $el = bTask.views.app.$el 14 | , listName = 'Example list'; 15 | 16 | // Show the add list form 17 | $el.find('#add-list-button').click(); 18 | 19 | // Fill out a value for the new list's title 20 | $el.find('#list_title').val(listName); 21 | $el.find('#list_title').parents('form').first().submit(); 22 | 23 | // Make sure the spy has seen a call for a list being created 24 | assert.equal(1, spyCreate.callCount); 25 | 26 | // Ensure the expected UI element has been added 27 | assert.equal(listName, $('.list-menu-item:last').text().trim()); 28 | }); 29 | 30 | test('Editing a list', function() { 31 | var $el = bTask.views.app.$el; 32 | 33 | // Show the edit list form 34 | $el.find('.list-menu-item:first').click(); 35 | $el.find('#edit-list-button').click(); 36 | 37 | $el.find('#list_title').val('Edited list'); 38 | $el.find('#list_title').parents('form').first().submit(); 39 | 40 | assert.equal(1, spyUpdate.callCount); 41 | assert.equal('Edited list', $('.list-menu-item:first').text().trim()); 42 | }); 43 | 44 | test('Deleting a list', function() { 45 | var $el = bTask.views.app.$el; 46 | 47 | // Automatically accept the confirmation 48 | window.confirm = function() { return true; }; 49 | 50 | // Show the edit list form 51 | $el.find('#edit-list-button').click(); 52 | 53 | // Click the list delete button 54 | $el.find('.delete-list').click(); 55 | 56 | assert.equal(1, spyDelete.callCount); 57 | }); 58 | }); 59 | -------------------------------------------------------------------------------- /app/js/views/tasks/index.js: -------------------------------------------------------------------------------- 1 | define(['text!templates/tasks/index.html', 'views/tasks/task', 'views/tasks/edit', 'collections/tasks'], function(template, TaskView, TaskEditView, Tasks) { 2 | var TasksIndexView = Backbone.View.extend({ 3 | tagName: 'div', 4 | className: 'row-fluid', 5 | 6 | template: _.template(template), 7 | 8 | events: { 9 | 'submit .add-task': 'addTask' 10 | }, 11 | 12 | initialize: function() { 13 | this.children = []; 14 | this.collection.on('add', this.renderTask, this); 15 | }, 16 | 17 | addTask: function() { 18 | var $input = this.$el.find('input[name="title"]') 19 | , task = new this.collection.model({ tasklist: this.model.get('id') }) 20 | , self = this 21 | ; 22 | 23 | task.save({ title: $input.val() }, { 24 | success: function() { 25 | self.collection.add(task, { at: 0 }); 26 | } 27 | }); 28 | $input.val(''); 29 | 30 | return false; 31 | }, 32 | 33 | renderTask: function(task, list, options) { 34 | var item = new TaskView({ model: task, parentView: this }) 35 | , $el = this.$el.find('#task-list'); 36 | if (options && options.at === 0) { 37 | $el.prepend(item.render().el); 38 | } else { 39 | $el.append(item.render().el); 40 | } 41 | this.children.push(item); 42 | }, 43 | 44 | render: function() { 45 | this.$el.html(this.template()); 46 | 47 | var $el = this.$el.find('#task-list') 48 | , self = this; 49 | 50 | this.collection.fetch({ data: { tasklist: this.model.get('id') }, success: function() { 51 | self.collection.each(function(task) { 52 | task.set('tasklist', self.model.get('id')); 53 | self.renderTask(task); 54 | }); 55 | }}); 56 | 57 | return this; 58 | }, 59 | 60 | editTask: function(task) { 61 | if (this.taskEditView) { 62 | this.taskEditView.remove(); 63 | } 64 | this.taskEditView = new TaskEditView({ model: task }); 65 | this.$el.find('#selected-task').append(this.taskEditView.render().el); 66 | } 67 | }); 68 | 69 | return TasksIndexView; 70 | }); 71 | -------------------------------------------------------------------------------- /app/js/gapi.js: -------------------------------------------------------------------------------- 1 | define(['config'], function(config) { 2 | var app; 3 | 4 | function ApiManager(_app) { 5 | app = _app; 6 | this.loadGapi(); 7 | } 8 | 9 | _.extend(ApiManager.prototype, Backbone.Events); 10 | 11 | ApiManager.prototype.init = function() { 12 | var self = this; 13 | 14 | gapi.client.load('tasks', 'v1', function() { /* Loaded */ }); 15 | 16 | function handleClientLoad() { 17 | gapi.client.setApiKey(config.apiKey); 18 | window.setTimeout(checkAuth, 100); 19 | } 20 | 21 | function checkAuth() { 22 | gapi.auth.authorize({ client_id: config.clientId, scope: config.scopes, immediate: true }, handleAuthResult); 23 | } 24 | 25 | function handleAuthResult(authResult) { 26 | var authTimeout; 27 | 28 | if (authResult && !authResult.error) { 29 | // Schedule a check when the authentication token expires 30 | if (authResult.expires_in) { 31 | authTimeout = (authResult.expires_in - 5 * 60) * 1000; 32 | setTimeout(checkAuth, authTimeout); 33 | } 34 | 35 | app.views.auth.$el.hide(); 36 | $('#signed-in-container').show(); 37 | self.trigger('ready'); 38 | } else { 39 | if (authResult && authResult.error) { 40 | // TODO: Show error 41 | console.error('Unable to sign in:', authResult.error); 42 | } 43 | 44 | app.views.auth.$el.show(); 45 | } 46 | } 47 | 48 | this.checkAuth = function() { 49 | gapi.auth.authorize({ client_id: config.clientId, scope: config.scopes, immediate: false }, handleAuthResult); 50 | }; 51 | 52 | handleClientLoad(); 53 | }; 54 | 55 | ApiManager.prototype.loadGapi = function() { 56 | var self = this; 57 | 58 | // Don't load gapi if it's already present 59 | if (typeof gapi !== 'undefined') { 60 | return this.init(); 61 | } 62 | 63 | require(['https://apis.google.com/js/client.js?onload=define'], function() { 64 | // Poll until gapi is ready 65 | function checkGAPI() { 66 | if (gapi && gapi.client) { 67 | self.init(); 68 | } else { 69 | setTimeout(checkGAPI, 100); 70 | } 71 | } 72 | 73 | checkGAPI(); 74 | }); 75 | }; 76 | 77 | Backbone.sync = function(method, model, options) { 78 | var requestContent = {}, request; 79 | options || (options = {}); 80 | 81 | switch (model.url) { 82 | case 'tasks': 83 | requestContent.task = model.get('id'); 84 | requestContent.tasklist = model.get('tasklist'); 85 | break; 86 | 87 | case 'tasklists': 88 | requestContent.tasklist = model.get('id'); 89 | break; 90 | } 91 | 92 | switch (method) { 93 | case 'create': 94 | requestContent['resource'] = model.toJSON(); 95 | request = gapi.client.tasks[model.url].insert(requestContent); 96 | Backbone.gapiRequest(request, method, model, options); 97 | break; 98 | 99 | case 'update': 100 | requestContent['resource'] = model.toJSON(); 101 | request = gapi.client.tasks[model.url].update(requestContent); 102 | Backbone.gapiRequest(request, method, model, options); 103 | break; 104 | 105 | case 'delete': 106 | requestContent['resource'] = model.toJSON(); 107 | request = gapi.client.tasks[model.url].delete(requestContent); 108 | Backbone.gapiRequest(request, method, model, options); 109 | break; 110 | 111 | case 'read': 112 | request = gapi.client.tasks[model.url].list(options.data); 113 | Backbone.gapiRequest(request, method, model, options); 114 | break; 115 | } 116 | }; 117 | 118 | Backbone.gapiRequest = function(request, method, model, options) { 119 | var result; 120 | request.execute(function(res) { 121 | if (res.error) { 122 | if (options.error) options.error(res); 123 | } else if (options.success) { 124 | if (res.items) { 125 | result = res.items; 126 | } else { 127 | result = res; 128 | } 129 | options.success(model, result, request); 130 | } 131 | }); 132 | }; 133 | 134 | return ApiManager; 135 | }); 136 | -------------------------------------------------------------------------------- /test/fixtures/gapi.js: -------------------------------------------------------------------------------- 1 | gapi = {}; 2 | gapi.client = { 3 | load: function(path, version, cb) { 4 | cb(); 5 | }, 6 | oauth2: { 7 | userinfo: { 8 | get: function() { 9 | return { 10 | execute: function(cb) { 11 | return cb({"id":"1","name":"Sergey Brin","given_name":"Sergey","family_name":"Brin","link":"https://plus.google.com/1","picture":"","gender":"male","locale":"en","result":{"id":"1","name":"Sergey Brin","given_name":"Sergey","family_name":"Brin","link":"https://plus.google.com/1","picture":"","gender":"male","locale":"en"}}); 12 | } 13 | }; 14 | } 15 | } 16 | }, 17 | setApiKey: function() { 18 | }, 19 | tasks: { 20 | tasks: { 21 | update: function() { 22 | return { 23 | execute: function(cb) { 24 | cb({}); 25 | } 26 | }; 27 | }, 28 | delete: function() { 29 | return { 30 | execute: function(cb) { 31 | cb(); 32 | } 33 | }; 34 | }, 35 | insert: function() { 36 | return { 37 | execute: function(cb) { 38 | } 39 | }; 40 | }, 41 | list: function() { 42 | return { 43 | execute: function(cb) { 44 | return cb({ 45 | "kind": "tasks#tasks", 46 | "items": [ 47 | { 48 | "kind": "tasks#task", 49 | "id": "MDE5MzU2Njg4NDcyNjMxOTE4MzE6Njk1MzUzOTY2OjQ3NTg4MjQyMg", 50 | "title": "x1", 51 | "updated": "2012-08-10T22:07:22.000Z", 52 | "position": "00000000000000410311", 53 | "status": "needsAction" 54 | }, 55 | { 56 | "kind": "tasks#task", 57 | "id": "MDE5MzU2Njg4NDcyNjMxOTE4MzE6Njk1MzUzOTY2OjE0ODU0NTE1NDc", 58 | "title": "x2", 59 | "updated": "2012-08-10T22:07:25.000Z", 60 | "position": "00000000000000615467", 61 | "status": "needsAction" 62 | }, 63 | { 64 | "kind": "tasks#task", 65 | "id": "MDE5MzU2Njg4NDcyNjMxOTE4MzE6Njk1MzUzOTY2OjgxNTQ5MTA3Nw", 66 | "title": "x3", 67 | "updated": "2012-08-12T14:30:49.000Z", 68 | "position": "00000000000000820623", 69 | "status": "completed", 70 | "completed": "2012-08-12T14:30:49.000Z" 71 | } 72 | ], 73 | "result": { 74 | "kind": "tasks#tasks", 75 | "items": [ 76 | { 77 | "kind": "tasks#task", 78 | "id": "MDE5MzU2Njg4NDcyNjMxOTE4MzE6Njk1MzUzOTY2OjQ3NTg4MjQyMg", 79 | "title": "x1", 80 | "updated": "2012-08-10T22:07:22.000Z", 81 | "position": "00000000000000410311", 82 | "status": "needsAction" 83 | }, 84 | { 85 | "kind": "tasks#task", 86 | "id": "MDE5MzU2Njg4NDcyNjMxOTE4MzE6Njk1MzUzOTY2OjE0ODU0NTE1NDc", 87 | "title": "x2", 88 | "updated": "2012-08-10T22:07:25.000Z", 89 | "position": "00000000000000615467", 90 | "status": "needsAction" 91 | }, 92 | { 93 | "kind": "tasks#task", 94 | "id": "MDE5MzU2Njg4NDcyNjMxOTE4MzE6Njk1MzUzOTY2OjgxNTQ5MTA3Nw", 95 | "title": "x3", 96 | "updated": "2012-08-12T14:30:49.000Z", 97 | "position": "00000000000000820623", 98 | "status": "completed", 99 | "completed": "2012-08-12T14:30:49.000Z" 100 | } 101 | ] 102 | } 103 | }); 104 | } 105 | }; 106 | } 107 | }, 108 | tasklists: { 109 | update: function() { 110 | return { 111 | execute: function(cb) { 112 | cb({}); 113 | } 114 | }; 115 | }, 116 | delete: function() { 117 | return { 118 | execute: function(cb) { 119 | cb({}); 120 | } 121 | }; 122 | }, 123 | insert: function() { 124 | return { 125 | execute: function(cb) { 126 | // Used for the 'Creating a list' test 127 | cb({ 128 | "id": "1", 129 | "kind": "tasks#taskList", 130 | "title": "Example list", 131 | "updated": "2013-01-14T13:58:48.000Z" 132 | }); 133 | } 134 | }; 135 | }, 136 | list: function() { 137 | return { 138 | execute: function(cb) { 139 | cb({ 140 | "kind": "tasks#taskLists", 141 | "items": [ 142 | { 143 | "kind": "tasks#taskList", 144 | "id": "MDE5MzU2Njg4NDcyNjMxOTE4MzE6MDow", 145 | "title": "Default List", 146 | "updated": "2012-08-14T13:58:48.000Z", 147 | }, 148 | { 149 | "kind": "tasks#taskList", 150 | "id": "MDE5MzU2Njg4NDcyNjMxOTE4MzE6NDg3ODA5MzA2OjA", 151 | "title": "Writing", 152 | "updated": "2012-07-22T17:58:19.000Z", 153 | } 154 | ], 155 | "result": { 156 | "kind": "tasks#taskLists", 157 | "items": [ 158 | { 159 | "kind": "tasks#taskList", 160 | "id": "MDE5MzU2Njg4NDcyNjMxOTE4MzE6MDow", 161 | "title": "Default List", 162 | "updated": "2012-08-14T13:58:48.000Z", 163 | }, 164 | { 165 | "kind": "tasks#taskList", 166 | "id": "MDE5MzU2Njg4NDcyNjMxOTE4MzE6NDg3ODA5MzA2OjA", 167 | "title": "Writing", 168 | "updated": "2012-07-22T17:58:19.000Z", 169 | } 170 | ] 171 | } 172 | }); 173 | } 174 | }; 175 | } 176 | } 177 | } 178 | }; 179 | gapi.auth = { 180 | authorize: function(options, cb) { 181 | cb({}); 182 | } 183 | }; 184 | -------------------------------------------------------------------------------- /app/js/lib/underscore-min.js: -------------------------------------------------------------------------------- 1 | (function(){var n=this,t=n._,r={},e=Array.prototype,u=Object.prototype,i=Function.prototype,a=e.push,o=e.slice,c=e.concat,l=u.toString,f=u.hasOwnProperty,s=e.forEach,p=e.map,v=e.reduce,h=e.reduceRight,g=e.filter,d=e.every,m=e.some,y=e.indexOf,b=e.lastIndexOf,x=Array.isArray,_=Object.keys,j=i.bind,w=function(n){return n instanceof w?n:this instanceof w?(this._wrapped=n,void 0):new w(n)};"undefined"!=typeof exports?("undefined"!=typeof module&&module.exports&&(exports=module.exports=w),exports._=w):n._=w,w.VERSION="1.4.3";var A=w.each=w.forEach=function(n,t,e){if(null!=n)if(s&&n.forEach===s)n.forEach(t,e);else if(n.length===+n.length){for(var u=0,i=n.length;i>u;u++)if(t.call(e,n[u],u,n)===r)return}else for(var a in n)if(w.has(n,a)&&t.call(e,n[a],a,n)===r)return};w.map=w.collect=function(n,t,r){var e=[];return null==n?e:p&&n.map===p?n.map(t,r):(A(n,function(n,u,i){e[e.length]=t.call(r,n,u,i)}),e)};var O="Reduce of empty array with no initial value";w.reduce=w.foldl=w.inject=function(n,t,r,e){var u=arguments.length>2;if(null==n&&(n=[]),v&&n.reduce===v)return e&&(t=w.bind(t,e)),u?n.reduce(t,r):n.reduce(t);if(A(n,function(n,i,a){u?r=t.call(e,r,n,i,a):(r=n,u=!0)}),!u)throw new TypeError(O);return r},w.reduceRight=w.foldr=function(n,t,r,e){var u=arguments.length>2;if(null==n&&(n=[]),h&&n.reduceRight===h)return e&&(t=w.bind(t,e)),u?n.reduceRight(t,r):n.reduceRight(t);var i=n.length;if(i!==+i){var a=w.keys(n);i=a.length}if(A(n,function(o,c,l){c=a?a[--i]:--i,u?r=t.call(e,r,n[c],c,l):(r=n[c],u=!0)}),!u)throw new TypeError(O);return r},w.find=w.detect=function(n,t,r){var e;return E(n,function(n,u,i){return t.call(r,n,u,i)?(e=n,!0):void 0}),e},w.filter=w.select=function(n,t,r){var e=[];return null==n?e:g&&n.filter===g?n.filter(t,r):(A(n,function(n,u,i){t.call(r,n,u,i)&&(e[e.length]=n)}),e)},w.reject=function(n,t,r){return w.filter(n,function(n,e,u){return!t.call(r,n,e,u)},r)},w.every=w.all=function(n,t,e){t||(t=w.identity);var u=!0;return null==n?u:d&&n.every===d?n.every(t,e):(A(n,function(n,i,a){return(u=u&&t.call(e,n,i,a))?void 0:r}),!!u)};var E=w.some=w.any=function(n,t,e){t||(t=w.identity);var u=!1;return null==n?u:m&&n.some===m?n.some(t,e):(A(n,function(n,i,a){return u||(u=t.call(e,n,i,a))?r:void 0}),!!u)};w.contains=w.include=function(n,t){return null==n?!1:y&&n.indexOf===y?-1!=n.indexOf(t):E(n,function(n){return n===t})},w.invoke=function(n,t){var r=o.call(arguments,2);return w.map(n,function(n){return(w.isFunction(t)?t:n[t]).apply(n,r)})},w.pluck=function(n,t){return w.map(n,function(n){return n[t]})},w.where=function(n,t){return w.isEmpty(t)?[]:w.filter(n,function(n){for(var r in t)if(t[r]!==n[r])return!1;return!0})},w.max=function(n,t,r){if(!t&&w.isArray(n)&&n[0]===+n[0]&&65535>n.length)return Math.max.apply(Math,n);if(!t&&w.isEmpty(n))return-1/0;var e={computed:-1/0,value:-1/0};return A(n,function(n,u,i){var a=t?t.call(r,n,u,i):n;a>=e.computed&&(e={value:n,computed:a})}),e.value},w.min=function(n,t,r){if(!t&&w.isArray(n)&&n[0]===+n[0]&&65535>n.length)return Math.min.apply(Math,n);if(!t&&w.isEmpty(n))return 1/0;var e={computed:1/0,value:1/0};return A(n,function(n,u,i){var a=t?t.call(r,n,u,i):n;e.computed>a&&(e={value:n,computed:a})}),e.value},w.shuffle=function(n){var t,r=0,e=[];return A(n,function(n){t=w.random(r++),e[r-1]=e[t],e[t]=n}),e};var F=function(n){return w.isFunction(n)?n:function(t){return t[n]}};w.sortBy=function(n,t,r){var e=F(t);return w.pluck(w.map(n,function(n,t,u){return{value:n,index:t,criteria:e.call(r,n,t,u)}}).sort(function(n,t){var r=n.criteria,e=t.criteria;if(r!==e){if(r>e||void 0===r)return 1;if(e>r||void 0===e)return-1}return n.indexi;){var o=i+a>>>1;u>r.call(e,n[o])?i=o+1:a=o}return i},w.toArray=function(n){return n?w.isArray(n)?o.call(n):n.length===+n.length?w.map(n,w.identity):w.values(n):[]},w.size=function(n){return null==n?0:n.length===+n.length?n.length:w.keys(n).length},w.first=w.head=w.take=function(n,t,r){return null==n?void 0:null==t||r?n[0]:o.call(n,0,t)},w.initial=function(n,t,r){return o.call(n,0,n.length-(null==t||r?1:t))},w.last=function(n,t,r){return null==n?void 0:null==t||r?n[n.length-1]:o.call(n,Math.max(n.length-t,0))},w.rest=w.tail=w.drop=function(n,t,r){return o.call(n,null==t||r?1:t)},w.compact=function(n){return w.filter(n,w.identity)};var R=function(n,t,r){return A(n,function(n){w.isArray(n)?t?a.apply(r,n):R(n,t,r):r.push(n)}),r};w.flatten=function(n,t){return R(n,t,[])},w.without=function(n){return w.difference(n,o.call(arguments,1))},w.uniq=w.unique=function(n,t,r,e){w.isFunction(t)&&(e=r,r=t,t=!1);var u=r?w.map(n,r,e):n,i=[],a=[];return A(u,function(r,e){(t?e&&a[a.length-1]===r:w.contains(a,r))||(a.push(r),i.push(n[e]))}),i},w.union=function(){return w.uniq(c.apply(e,arguments))},w.intersection=function(n){var t=o.call(arguments,1);return w.filter(w.uniq(n),function(n){return w.every(t,function(t){return w.indexOf(t,n)>=0})})},w.difference=function(n){var t=c.apply(e,o.call(arguments,1));return w.filter(n,function(n){return!w.contains(t,n)})},w.zip=function(){for(var n=o.call(arguments),t=w.max(w.pluck(n,"length")),r=Array(t),e=0;t>e;e++)r[e]=w.pluck(n,""+e);return r},w.object=function(n,t){if(null==n)return{};for(var r={},e=0,u=n.length;u>e;e++)t?r[n[e]]=t[e]:r[n[e][0]]=n[e][1];return r},w.indexOf=function(n,t,r){if(null==n)return-1;var e=0,u=n.length;if(r){if("number"!=typeof r)return e=w.sortedIndex(n,t),n[e]===t?e:-1;e=0>r?Math.max(0,u+r):r}if(y&&n.indexOf===y)return n.indexOf(t,r);for(;u>e;e++)if(n[e]===t)return e;return-1},w.lastIndexOf=function(n,t,r){if(null==n)return-1;var e=null!=r;if(b&&n.lastIndexOf===b)return e?n.lastIndexOf(t,r):n.lastIndexOf(t);for(var u=e?r:n.length;u--;)if(n[u]===t)return u;return-1},w.range=function(n,t,r){1>=arguments.length&&(t=n||0,n=0),r=arguments[2]||1;for(var e=Math.max(Math.ceil((t-n)/r),0),u=0,i=Array(e);e>u;)i[u++]=n,n+=r;return i};var I=function(){};w.bind=function(n,t){var r,e;if(n.bind===j&&j)return j.apply(n,o.call(arguments,1));if(!w.isFunction(n))throw new TypeError;return r=o.call(arguments,2),e=function(){if(!(this instanceof e))return n.apply(t,r.concat(o.call(arguments)));I.prototype=n.prototype;var u=new I;I.prototype=null;var i=n.apply(u,r.concat(o.call(arguments)));return Object(i)===i?i:u}},w.bindAll=function(n){var t=o.call(arguments,1);return 0==t.length&&(t=w.functions(n)),A(t,function(t){n[t]=w.bind(n[t],n)}),n},w.memoize=function(n,t){var r={};return t||(t=w.identity),function(){var e=t.apply(this,arguments);return w.has(r,e)?r[e]:r[e]=n.apply(this,arguments)}},w.delay=function(n,t){var r=o.call(arguments,2);return setTimeout(function(){return n.apply(null,r)},t)},w.defer=function(n){return w.delay.apply(w,[n,1].concat(o.call(arguments,1)))},w.throttle=function(n,t){var r,e,u,i,a=0,o=function(){a=new Date,u=null,i=n.apply(r,e)};return function(){var c=new Date,l=t-(c-a);return r=this,e=arguments,0>=l?(clearTimeout(u),u=null,a=c,i=n.apply(r,e)):u||(u=setTimeout(o,l)),i}},w.debounce=function(n,t,r){var e,u;return function(){var i=this,a=arguments,o=function(){e=null,r||(u=n.apply(i,a))},c=r&&!e;return clearTimeout(e),e=setTimeout(o,t),c&&(u=n.apply(i,a)),u}},w.once=function(n){var t,r=!1;return function(){return r?t:(r=!0,t=n.apply(this,arguments),n=null,t)}},w.wrap=function(n,t){return function(){var r=[n];return a.apply(r,arguments),t.apply(this,r)}},w.compose=function(){var n=arguments;return function(){for(var t=arguments,r=n.length-1;r>=0;r--)t=[n[r].apply(this,t)];return t[0]}},w.after=function(n,t){return 0>=n?t():function(){return 1>--n?t.apply(this,arguments):void 0}},w.keys=_||function(n){if(n!==Object(n))throw new TypeError("Invalid object");var t=[];for(var r in n)w.has(n,r)&&(t[t.length]=r);return t},w.values=function(n){var t=[];for(var r in n)w.has(n,r)&&t.push(n[r]);return t},w.pairs=function(n){var t=[];for(var r in n)w.has(n,r)&&t.push([r,n[r]]);return t},w.invert=function(n){var t={};for(var r in n)w.has(n,r)&&(t[n[r]]=r);return t},w.functions=w.methods=function(n){var t=[];for(var r in n)w.isFunction(n[r])&&t.push(r);return t.sort()},w.extend=function(n){return A(o.call(arguments,1),function(t){if(t)for(var r in t)n[r]=t[r]}),n},w.pick=function(n){var t={},r=c.apply(e,o.call(arguments,1));return A(r,function(r){r in n&&(t[r]=n[r])}),t},w.omit=function(n){var t={},r=c.apply(e,o.call(arguments,1));for(var u in n)w.contains(r,u)||(t[u]=n[u]);return t},w.defaults=function(n){return A(o.call(arguments,1),function(t){if(t)for(var r in t)null==n[r]&&(n[r]=t[r])}),n},w.clone=function(n){return w.isObject(n)?w.isArray(n)?n.slice():w.extend({},n):n},w.tap=function(n,t){return t(n),n};var S=function(n,t,r,e){if(n===t)return 0!==n||1/n==1/t;if(null==n||null==t)return n===t;n instanceof w&&(n=n._wrapped),t instanceof w&&(t=t._wrapped);var u=l.call(n);if(u!=l.call(t))return!1;switch(u){case"[object String]":return n==t+"";case"[object Number]":return n!=+n?t!=+t:0==n?1/n==1/t:n==+t;case"[object Date]":case"[object Boolean]":return+n==+t;case"[object RegExp]":return n.source==t.source&&n.global==t.global&&n.multiline==t.multiline&&n.ignoreCase==t.ignoreCase}if("object"!=typeof n||"object"!=typeof t)return!1;for(var i=r.length;i--;)if(r[i]==n)return e[i]==t;r.push(n),e.push(t);var a=0,o=!0;if("[object Array]"==u){if(a=n.length,o=a==t.length)for(;a--&&(o=S(n[a],t[a],r,e)););}else{var c=n.constructor,f=t.constructor;if(c!==f&&!(w.isFunction(c)&&c instanceof c&&w.isFunction(f)&&f instanceof f))return!1;for(var s in n)if(w.has(n,s)&&(a++,!(o=w.has(t,s)&&S(n[s],t[s],r,e))))break;if(o){for(s in t)if(w.has(t,s)&&!a--)break;o=!a}}return r.pop(),e.pop(),o};w.isEqual=function(n,t){return S(n,t,[],[])},w.isEmpty=function(n){if(null==n)return!0;if(w.isArray(n)||w.isString(n))return 0===n.length;for(var t in n)if(w.has(n,t))return!1;return!0},w.isElement=function(n){return!(!n||1!==n.nodeType)},w.isArray=x||function(n){return"[object Array]"==l.call(n)},w.isObject=function(n){return n===Object(n)},A(["Arguments","Function","String","Number","Date","RegExp"],function(n){w["is"+n]=function(t){return l.call(t)=="[object "+n+"]"}}),w.isArguments(arguments)||(w.isArguments=function(n){return!(!n||!w.has(n,"callee"))}),w.isFunction=function(n){return"function"==typeof n},w.isFinite=function(n){return isFinite(n)&&!isNaN(parseFloat(n))},w.isNaN=function(n){return w.isNumber(n)&&n!=+n},w.isBoolean=function(n){return n===!0||n===!1||"[object Boolean]"==l.call(n)},w.isNull=function(n){return null===n},w.isUndefined=function(n){return void 0===n},w.has=function(n,t){return f.call(n,t)},w.noConflict=function(){return n._=t,this},w.identity=function(n){return n},w.times=function(n,t,r){for(var e=Array(n),u=0;n>u;u++)e[u]=t.call(r,u);return e},w.random=function(n,t){return null==t&&(t=n,n=0),n+(0|Math.random()*(t-n+1))};var T={escape:{"&":"&","<":"<",">":">",'"':""","'":"'","/":"/"}};T.unescape=w.invert(T.escape);var M={escape:RegExp("["+w.keys(T.escape).join("")+"]","g"),unescape:RegExp("("+w.keys(T.unescape).join("|")+")","g")};w.each(["escape","unescape"],function(n){w[n]=function(t){return null==t?"":(""+t).replace(M[n],function(t){return T[n][t]})}}),w.result=function(n,t){if(null==n)return null;var r=n[t];return w.isFunction(r)?r.call(n):r},w.mixin=function(n){A(w.functions(n),function(t){var r=w[t]=n[t];w.prototype[t]=function(){var n=[this._wrapped];return a.apply(n,arguments),z.call(this,r.apply(w,n))}})};var N=0;w.uniqueId=function(n){var t=""+ ++N;return n?n+t:t},w.templateSettings={evaluate:/<%([\s\S]+?)%>/g,interpolate:/<%=([\s\S]+?)%>/g,escape:/<%-([\s\S]+?)%>/g};var q=/(.)^/,B={"'":"'","\\":"\\","\r":"r","\n":"n"," ":"t","\u2028":"u2028","\u2029":"u2029"},D=/\\|'|\r|\n|\t|\u2028|\u2029/g;w.template=function(n,t,r){r=w.defaults({},r,w.templateSettings);var e=RegExp([(r.escape||q).source,(r.interpolate||q).source,(r.evaluate||q).source].join("|")+"|$","g"),u=0,i="__p+='";n.replace(e,function(t,r,e,a,o){return i+=n.slice(u,o).replace(D,function(n){return"\\"+B[n]}),r&&(i+="'+\n((__t=("+r+"))==null?'':_.escape(__t))+\n'"),e&&(i+="'+\n((__t=("+e+"))==null?'':__t)+\n'"),a&&(i+="';\n"+a+"\n__p+='"),u=o+t.length,t}),i+="';\n",r.variable||(i="with(obj||{}){\n"+i+"}\n"),i="var __t,__p='',__j=Array.prototype.join,print=function(){__p+=__j.call(arguments,'');};\n"+i+"return __p;\n";try{var a=Function(r.variable||"obj","_",i)}catch(o){throw o.source=i,o}if(t)return a(t,w);var c=function(n){return a.call(this,n,w)};return c.source="function("+(r.variable||"obj")+"){\n"+i+"}",c},w.chain=function(n){return w(n).chain()};var z=function(n){return this._chain?w(n).chain():n};w.mixin(w),A(["pop","push","reverse","shift","sort","splice","unshift"],function(n){var t=e[n];w.prototype[n]=function(){var r=this._wrapped;return t.apply(r,arguments),"shift"!=n&&"splice"!=n||0!==r.length||delete r[0],z.call(this,r)}}),A(["concat","join","slice"],function(n){var t=e[n];w.prototype[n]=function(){return z.call(this,t.apply(this._wrapped,arguments))}}),w.extend(w.prototype,{chain:function(){return this._chain=!0,this},value:function(){return this._wrapped}})}).call(this); -------------------------------------------------------------------------------- /app/js/lib/text.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license RequireJS text 2.0.3 Copyright (c) 2010-2012, The Dojo Foundation All Rights Reserved. 3 | * Available via the MIT or new BSD license. 4 | * see: http://github.com/requirejs/text for details 5 | */ 6 | /*jslint regexp: true */ 7 | /*global require: false, XMLHttpRequest: false, ActiveXObject: false, 8 | define: false, window: false, process: false, Packages: false, 9 | java: false, location: false */ 10 | 11 | define(['module'], function (module) { 12 | 'use strict'; 13 | 14 | var text, fs, 15 | progIds = ['Msxml2.XMLHTTP', 'Microsoft.XMLHTTP', 'Msxml2.XMLHTTP.4.0'], 16 | xmlRegExp = /^\s*<\?xml(\s)+version=[\'\"](\d)*.(\d)*[\'\"](\s)*\?>/im, 17 | bodyRegExp = /]*>\s*([\s\S]+)\s*<\/body>/im, 18 | hasLocation = typeof location !== 'undefined' && location.href, 19 | defaultProtocol = hasLocation && location.protocol && location.protocol.replace(/\:/, ''), 20 | defaultHostName = hasLocation && location.hostname, 21 | defaultPort = hasLocation && (location.port || undefined), 22 | buildMap = [], 23 | masterConfig = (module.config && module.config()) || {}; 24 | 25 | text = { 26 | version: '2.0.3', 27 | 28 | strip: function (content) { 29 | //Strips declarations so that external SVG and XML 30 | //documents can be added to a document without worry. Also, if the string 31 | //is an HTML document, only the part inside the body tag is returned. 32 | if (content) { 33 | content = content.replace(xmlRegExp, ""); 34 | var matches = content.match(bodyRegExp); 35 | if (matches) { 36 | content = matches[1]; 37 | } 38 | } else { 39 | content = ""; 40 | } 41 | return content; 42 | }, 43 | 44 | jsEscape: function (content) { 45 | return content.replace(/(['\\])/g, '\\$1') 46 | .replace(/[\f]/g, "\\f") 47 | .replace(/[\b]/g, "\\b") 48 | .replace(/[\n]/g, "\\n") 49 | .replace(/[\t]/g, "\\t") 50 | .replace(/[\r]/g, "\\r") 51 | .replace(/[\u2028]/g, "\\u2028") 52 | .replace(/[\u2029]/g, "\\u2029"); 53 | }, 54 | 55 | createXhr: masterConfig.createXhr || function () { 56 | //Would love to dump the ActiveX crap in here. Need IE 6 to die first. 57 | var xhr, i, progId; 58 | if (typeof XMLHttpRequest !== "undefined") { 59 | return new XMLHttpRequest(); 60 | } else if (typeof ActiveXObject !== "undefined") { 61 | for (i = 0; i < 3; i += 1) { 62 | progId = progIds[i]; 63 | try { 64 | xhr = new ActiveXObject(progId); 65 | } catch (e) {} 66 | 67 | if (xhr) { 68 | progIds = [progId]; // so faster next time 69 | break; 70 | } 71 | } 72 | } 73 | 74 | return xhr; 75 | }, 76 | 77 | /** 78 | * Parses a resource name into its component parts. Resource names 79 | * look like: module/name.ext!strip, where the !strip part is 80 | * optional. 81 | * @param {String} name the resource name 82 | * @returns {Object} with properties "moduleName", "ext" and "strip" 83 | * where strip is a boolean. 84 | */ 85 | parseName: function (name) { 86 | var strip = false, index = name.indexOf("."), 87 | modName = name.substring(0, index), 88 | ext = name.substring(index + 1, name.length); 89 | 90 | index = ext.indexOf("!"); 91 | if (index !== -1) { 92 | //Pull off the strip arg. 93 | strip = ext.substring(index + 1, ext.length); 94 | strip = strip === "strip"; 95 | ext = ext.substring(0, index); 96 | } 97 | 98 | return { 99 | moduleName: modName, 100 | ext: ext, 101 | strip: strip 102 | }; 103 | }, 104 | 105 | xdRegExp: /^((\w+)\:)?\/\/([^\/\\]+)/, 106 | 107 | /** 108 | * Is an URL on another domain. Only works for browser use, returns 109 | * false in non-browser environments. Only used to know if an 110 | * optimized .js version of a text resource should be loaded 111 | * instead. 112 | * @param {String} url 113 | * @returns Boolean 114 | */ 115 | useXhr: function (url, protocol, hostname, port) { 116 | var uProtocol, uHostName, uPort, 117 | match = text.xdRegExp.exec(url); 118 | if (!match) { 119 | return true; 120 | } 121 | uProtocol = match[2]; 122 | uHostName = match[3]; 123 | 124 | uHostName = uHostName.split(':'); 125 | uPort = uHostName[1]; 126 | uHostName = uHostName[0]; 127 | 128 | return (!uProtocol || uProtocol === protocol) && 129 | (!uHostName || uHostName.toLowerCase() === hostname.toLowerCase()) && 130 | ((!uPort && !uHostName) || uPort === port); 131 | }, 132 | 133 | finishLoad: function (name, strip, content, onLoad) { 134 | content = strip ? text.strip(content) : content; 135 | if (masterConfig.isBuild) { 136 | buildMap[name] = content; 137 | } 138 | onLoad(content); 139 | }, 140 | 141 | load: function (name, req, onLoad, config) { 142 | //Name has format: some.module.filext!strip 143 | //The strip part is optional. 144 | //if strip is present, then that means only get the string contents 145 | //inside a body tag in an HTML string. For XML/SVG content it means 146 | //removing the declarations so the content can be inserted 147 | //into the current doc without problems. 148 | 149 | // Do not bother with the work if a build and text will 150 | // not be inlined. 151 | if (config.isBuild && !config.inlineText) { 152 | onLoad(); 153 | return; 154 | } 155 | 156 | masterConfig.isBuild = config.isBuild; 157 | 158 | var parsed = text.parseName(name), 159 | nonStripName = parsed.moduleName + '.' + parsed.ext, 160 | url = req.toUrl(nonStripName), 161 | useXhr = (masterConfig.useXhr) || 162 | text.useXhr; 163 | 164 | //Load the text. Use XHR if possible and in a browser. 165 | if (!hasLocation || useXhr(url, defaultProtocol, defaultHostName, defaultPort)) { 166 | text.get(url, function (content) { 167 | text.finishLoad(name, parsed.strip, content, onLoad); 168 | }, function (err) { 169 | if (onLoad.error) { 170 | onLoad.error(err); 171 | } 172 | }); 173 | } else { 174 | //Need to fetch the resource across domains. Assume 175 | //the resource has been optimized into a JS module. Fetch 176 | //by the module name + extension, but do not include the 177 | //!strip part to avoid file system issues. 178 | req([nonStripName], function (content) { 179 | text.finishLoad(parsed.moduleName + '.' + parsed.ext, 180 | parsed.strip, content, onLoad); 181 | }); 182 | } 183 | }, 184 | 185 | write: function (pluginName, moduleName, write, config) { 186 | if (buildMap.hasOwnProperty(moduleName)) { 187 | var content = text.jsEscape(buildMap[moduleName]); 188 | write.asModule(pluginName + "!" + moduleName, 189 | "define(function () { return '" + 190 | content + 191 | "';});\n"); 192 | } 193 | }, 194 | 195 | writeFile: function (pluginName, moduleName, req, write, config) { 196 | var parsed = text.parseName(moduleName), 197 | nonStripName = parsed.moduleName + '.' + parsed.ext, 198 | //Use a '.js' file name so that it indicates it is a 199 | //script that can be loaded across domains. 200 | fileName = req.toUrl(parsed.moduleName + '.' + 201 | parsed.ext) + '.js'; 202 | 203 | //Leverage own load() method to load plugin value, but only 204 | //write out values that do not have the strip argument, 205 | //to avoid any potential issues with ! in file names. 206 | text.load(nonStripName, req, function (value) { 207 | //Use own write() method to construct full module value. 208 | //But need to create shell that translates writeFile's 209 | //write() to the right interface. 210 | var textWrite = function (contents) { 211 | return write(fileName, contents); 212 | }; 213 | textWrite.asModule = function (moduleName, contents) { 214 | return write.asModule(moduleName, fileName, contents); 215 | }; 216 | 217 | text.write(pluginName, nonStripName, textWrite, config); 218 | }, config); 219 | } 220 | }; 221 | 222 | if (masterConfig.env === 'node' || (!masterConfig.env && 223 | typeof process !== "undefined" && 224 | process.versions && 225 | !!process.versions.node)) { 226 | //Using special require.nodeRequire, something added by r.js. 227 | fs = require.nodeRequire('fs'); 228 | 229 | text.get = function (url, callback) { 230 | var file = fs.readFileSync(url, 'utf8'); 231 | //Remove BOM (Byte Mark Order) from utf8 files if it is there. 232 | if (file.indexOf('\uFEFF') === 0) { 233 | file = file.substring(1); 234 | } 235 | callback(file); 236 | }; 237 | } else if (masterConfig.env === 'xhr' || (!masterConfig.env && 238 | text.createXhr())) { 239 | text.get = function (url, callback, errback) { 240 | var xhr = text.createXhr(); 241 | xhr.open('GET', url, true); 242 | 243 | //Allow overrides specified in config 244 | if (masterConfig.onXhr) { 245 | masterConfig.onXhr(xhr, url); 246 | } 247 | 248 | xhr.onreadystatechange = function (evt) { 249 | var status, err; 250 | //Do not explicitly handle errors, those should be 251 | //visible via console output in the browser. 252 | if (xhr.readyState === 4) { 253 | status = xhr.status; 254 | if (status > 399 && status < 600) { 255 | //An http 4xx or 5xx error. Signal an error. 256 | err = new Error(url + ' HTTP status: ' + status); 257 | err.xhr = xhr; 258 | errback(err); 259 | } else { 260 | callback(xhr.responseText); 261 | } 262 | } 263 | }; 264 | xhr.send(null); 265 | }; 266 | } else if (masterConfig.env === 'rhino' || (!masterConfig.env && 267 | typeof Packages !== 'undefined' && typeof java !== 'undefined')) { 268 | //Why Java, why is this so awkward? 269 | text.get = function (url, callback) { 270 | var stringBuffer, line, 271 | encoding = "utf-8", 272 | file = new java.io.File(url), 273 | lineSeparator = java.lang.System.getProperty("line.separator"), 274 | input = new java.io.BufferedReader(new java.io.InputStreamReader(new java.io.FileInputStream(file), encoding)), 275 | content = ''; 276 | try { 277 | stringBuffer = new java.lang.StringBuffer(); 278 | line = input.readLine(); 279 | 280 | // Byte Order Mark (BOM) - The Unicode Standard, version 3.0, page 324 281 | // http://www.unicode.org/faq/utf_bom.html 282 | 283 | // Note that when we use utf-8, the BOM should appear as "EF BB BF", but it doesn't due to this bug in the JDK: 284 | // http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4508058 285 | if (line && line.length() && line.charAt(0) === 0xfeff) { 286 | // Eat the BOM, since we've already found the encoding on this file, 287 | // and we plan to concatenating this buffer with others; the BOM should 288 | // only appear at the top of a file. 289 | line = line.substring(1); 290 | } 291 | 292 | stringBuffer.append(line); 293 | 294 | while ((line = input.readLine()) !== null) { 295 | stringBuffer.append(lineSeparator); 296 | stringBuffer.append(line); 297 | } 298 | //Make sure we return a JavaScript string and not a Java string. 299 | content = String(stringBuffer.toString()); //String 300 | } finally { 301 | input.close(); 302 | } 303 | callback(content); 304 | }; 305 | } 306 | 307 | return text; 308 | }); 309 | -------------------------------------------------------------------------------- /app/js/lib/backbone-min.js: -------------------------------------------------------------------------------- 1 | // Backbone.js 0.9.2 2 | 3 | // (c) 2010-2012 Jeremy Ashkenas, DocumentCloud Inc. 4 | // Backbone may be freely distributed under the MIT license. 5 | // For all details and documentation: 6 | // http://backbonejs.org 7 | (function(){var l=this,y=l.Backbone,z=Array.prototype.slice,A=Array.prototype.splice,g;g="undefined"!==typeof exports?exports:l.Backbone={};g.VERSION="0.9.2";var f=l._;!f&&"undefined"!==typeof require&&(f=require("underscore"));var i=l.jQuery||l.Zepto||l.ender;g.setDomLibrary=function(a){i=a};g.noConflict=function(){l.Backbone=y;return this};g.emulateHTTP=!1;g.emulateJSON=!1;var p=/\s+/,k=g.Events={on:function(a,b,c){var d,e,f,g,j;if(!b)return this;a=a.split(p);for(d=this._callbacks||(this._callbacks= 8 | {});e=a.shift();)f=(j=d[e])?j.tail:{},f.next=g={},f.context=c,f.callback=b,d[e]={tail:g,next:j?j.next:f};return this},off:function(a,b,c){var d,e,h,g,j,q;if(e=this._callbacks){if(!a&&!b&&!c)return delete this._callbacks,this;for(a=a?a.split(p):f.keys(e);d=a.shift();)if(h=e[d],delete e[d],h&&(b||c))for(g=h.tail;(h=h.next)!==g;)if(j=h.callback,q=h.context,b&&j!==b||c&&q!==c)this.on(d,j,q);return this}},trigger:function(a){var b,c,d,e,f,g;if(!(d=this._callbacks))return this;f=d.all;a=a.split(p);for(g= 9 | z.call(arguments,1);b=a.shift();){if(c=d[b])for(e=c.tail;(c=c.next)!==e;)c.callback.apply(c.context||this,g);if(c=f){e=c.tail;for(b=[b].concat(g);(c=c.next)!==e;)c.callback.apply(c.context||this,b)}}return this}};k.bind=k.on;k.unbind=k.off;var o=g.Model=function(a,b){var c;a||(a={});b&&b.parse&&(a=this.parse(a));if(c=n(this,"defaults"))a=f.extend({},c,a);b&&b.collection&&(this.collection=b.collection);this.attributes={};this._escapedAttributes={};this.cid=f.uniqueId("c");this.changed={};this._silent= 10 | {};this._pending={};this.set(a,{silent:!0});this.changed={};this._silent={};this._pending={};this._previousAttributes=f.clone(this.attributes);this.initialize.apply(this,arguments)};f.extend(o.prototype,k,{changed:null,_silent:null,_pending:null,idAttribute:"id",initialize:function(){},toJSON:function(){return f.clone(this.attributes)},get:function(a){return this.attributes[a]},escape:function(a){var b;if(b=this._escapedAttributes[a])return b;b=this.get(a);return this._escapedAttributes[a]=f.escape(null== 11 | b?"":""+b)},has:function(a){return null!=this.get(a)},set:function(a,b,c){var d,e;f.isObject(a)||null==a?(d=a,c=b):(d={},d[a]=b);c||(c={});if(!d)return this;d instanceof o&&(d=d.attributes);if(c.unset)for(e in d)d[e]=void 0;if(!this._validate(d,c))return!1;this.idAttribute in d&&(this.id=d[this.idAttribute]);var b=c.changes={},h=this.attributes,g=this._escapedAttributes,j=this._previousAttributes||{};for(e in d){a=d[e];if(!f.isEqual(h[e],a)||c.unset&&f.has(h,e))delete g[e],(c.silent?this._silent: 12 | b)[e]=!0;c.unset?delete h[e]:h[e]=a;!f.isEqual(j[e],a)||f.has(h,e)!=f.has(j,e)?(this.changed[e]=a,c.silent||(this._pending[e]=!0)):(delete this.changed[e],delete this._pending[e])}c.silent||this.change(c);return this},unset:function(a,b){(b||(b={})).unset=!0;return this.set(a,null,b)},clear:function(a){(a||(a={})).unset=!0;return this.set(f.clone(this.attributes),a)},fetch:function(a){var a=a?f.clone(a):{},b=this,c=a.success;a.success=function(d,e,f){if(!b.set(b.parse(d,f),a))return!1;c&&c(b,d)}; 13 | a.error=g.wrapError(a.error,b,a);return(this.sync||g.sync).call(this,"read",this,a)},save:function(a,b,c){var d,e;f.isObject(a)||null==a?(d=a,c=b):(d={},d[a]=b);c=c?f.clone(c):{};if(c.wait){if(!this._validate(d,c))return!1;e=f.clone(this.attributes)}a=f.extend({},c,{silent:!0});if(d&&!this.set(d,c.wait?a:c))return!1;var h=this,i=c.success;c.success=function(a,b,e){b=h.parse(a,e);if(c.wait){delete c.wait;b=f.extend(d||{},b)}if(!h.set(b,c))return false;i?i(h,a):h.trigger("sync",h,a,c)};c.error=g.wrapError(c.error, 14 | h,c);b=this.isNew()?"create":"update";b=(this.sync||g.sync).call(this,b,this,c);c.wait&&this.set(e,a);return b},destroy:function(a){var a=a?f.clone(a):{},b=this,c=a.success,d=function(){b.trigger("destroy",b,b.collection,a)};if(this.isNew())return d(),!1;a.success=function(e){a.wait&&d();c?c(b,e):b.trigger("sync",b,e,a)};a.error=g.wrapError(a.error,b,a);var e=(this.sync||g.sync).call(this,"delete",this,a);a.wait||d();return e},url:function(){var a=n(this,"urlRoot")||n(this.collection,"url")||t(); 15 | return this.isNew()?a:a+("/"==a.charAt(a.length-1)?"":"/")+encodeURIComponent(this.id)},parse:function(a){return a},clone:function(){return new this.constructor(this.attributes)},isNew:function(){return null==this.id},change:function(a){a||(a={});var b=this._changing;this._changing=!0;for(var c in this._silent)this._pending[c]=!0;var d=f.extend({},a.changes,this._silent);this._silent={};for(c in d)this.trigger("change:"+c,this,this.get(c),a);if(b)return this;for(;!f.isEmpty(this._pending);){this._pending= 16 | {};this.trigger("change",this,a);for(c in this.changed)!this._pending[c]&&!this._silent[c]&&delete this.changed[c];this._previousAttributes=f.clone(this.attributes)}this._changing=!1;return this},hasChanged:function(a){return!arguments.length?!f.isEmpty(this.changed):f.has(this.changed,a)},changedAttributes:function(a){if(!a)return this.hasChanged()?f.clone(this.changed):!1;var b,c=!1,d=this._previousAttributes,e;for(e in a)if(!f.isEqual(d[e],b=a[e]))(c||(c={}))[e]=b;return c},previous:function(a){return!arguments.length|| 17 | !this._previousAttributes?null:this._previousAttributes[a]},previousAttributes:function(){return f.clone(this._previousAttributes)},isValid:function(){return!this.validate(this.attributes)},_validate:function(a,b){if(b.silent||!this.validate)return!0;var a=f.extend({},this.attributes,a),c=this.validate(a,b);if(!c)return!0;b&&b.error?b.error(this,c,b):this.trigger("error",this,c,b);return!1}});var r=g.Collection=function(a,b){b||(b={});b.model&&(this.model=b.model);b.comparator&&(this.comparator=b.comparator); 18 | this._reset();this.initialize.apply(this,arguments);a&&this.reset(a,{silent:!0,parse:b.parse})};f.extend(r.prototype,k,{model:o,initialize:function(){},toJSON:function(a){return this.map(function(b){return b.toJSON(a)})},add:function(a,b){var c,d,e,g,i,j={},k={},l=[];b||(b={});a=f.isArray(a)?a.slice():[a];c=0;for(d=a.length;c=b))this.iframe=i('