Commit b4e7bc94 authored by Paul Miller's avatar Paul Miller

Update Brunch with Chaplin.

1. Add Bower integration. Since Brunch automatically concatenates all Bower stuff, we don’t need bower_components in repository (added to .gitignore, except of todomvc-common).
2. Update Brunch to 1.7.
    * Added automatic source maps generation, source maps are in repo too.
3. Update Chaplin to 0.10.
    * Remove `chaplin` module, switch to global variable.
    * Switch to new declarative events format.
    * Switch to controller compositions instead of static controllers.
    * Remove `application` since Chaplin now has this functionality by default.
parent 87c5b345
bower_components/
!bower_components/todomvc-common
# NPM packages folder.
node_modules/
......
Chaplin = require 'chaplin'
mediator = require 'mediator'
routes = require 'routes'
HeaderController = require 'controllers/header-controller'
FooterController = require 'controllers/footer-controller'
TodosController = require 'controllers/todos-controller'
Todos = require 'models/todos'
Layout = require 'views/layout'
......@@ -13,28 +8,6 @@ module.exports = class Application extends Chaplin.Application
# “Controller title – Site title” (see Layout#adjustTitle)
title: 'Chaplin • TodoMVC'
initialize: ->
super
# Initialize core components
@initDispatcher controllerSuffix: '-controller'
@initLayout()
@initMediator()
# Application-specific scaffold
@initControllers()
# Register all routes and start routing
@initRouter routes, pushState: no
# You might pass Router/History options as the second parameter.
# Chaplin enables pushState per default and Backbone uses / as
# the root per default. You might change that in the options
# if necessary:
# @initRouter routes, pushState: false, root: '/subdir/'
# Freeze the application instance to prevent further changes
Object.freeze? this
# Override standard layout initializer
# ------------------------------------
initLayout: ->
......@@ -42,19 +15,6 @@ module.exports = class Application extends Chaplin.Application
# no features to the standard Chaplin Layout, it’s an empty placeholder.
@layout = new Layout {@title}
# Instantiate common controllers
# ------------------------------
initControllers: ->
# These controllers are active during the whole application runtime.
# You don’t need to instantiate all controllers here, only special
# controllers which do not to respond to routes. They may govern models
# and views which are needed the whole time, for example header, footer
# or navigation views.
# e.g. new NavigationController()
new HeaderController()
new FooterController()
new TodosController()
# Create additional mediator properties
# -------------------------------------
initMediator: ->
......@@ -64,4 +24,4 @@ module.exports = class Application extends Chaplin.Application
mediator.todos = new Todos()
mediator.todos.fetch()
# Seal the mediator
mediator.seal()
super
......@@ -3,22 +3,14 @@
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<title>Chaplin & Brunch • TodoMVC</title>
<link rel="stylesheet" href="../../../../assets/base.css">
<title>Chaplin &amp; Brunch • TodoMVC</title>
<link rel="stylesheet" href="../bower_components/todomvc-common/base.css">
<link rel="stylesheet" href="../bower_components/todomvc-common/base.js">
<!--[if IE]>
<script src="../../../assets/ie.js"></script>
<![endif]-->
<link rel="stylesheet" href="stylesheets/app.css">
<!-- Usually all these files are concatenated automatically
by brunch and you need just to import `vendor.js` -->
<script src="../../../../assets/base.js"></script>
<script src="../../../../assets/jquery.min.js"></script>
<script src="../../../../assets/lodash.min.js"></script>
<script src="../../../../assets/handlebars.min.js"></script>
<script src="javascripts/vendor.js"></script>
<script src="javascripts/app.js"></script>
<link rel="stylesheet" href="app.css">
<script src="app.js"></script>
<script>require('initialize');</script>
</head>
<body>
......
Chaplin = require 'chaplin'
HeaderView = require 'views/header-view'
FooterView = require 'views/footer-view'
TodosView = require 'views/todos-view'
mediator = require 'mediator'
module.exports = class Controller extends Chaplin.Controller
beforeAction: ->
@compose 'footer', ->
params = collection: mediator.todos
@header = new HeaderView params
@footer = new FooterView params
@todos = new TodosView params
Controller = require 'controllers/base/controller'
FooterView = require 'views/footer-view'
mediator = require 'mediator'
module.exports = class FooterController extends Controller
initialize: ->
super
@view = new FooterView collection: mediator.todos
Controller = require 'controllers/base/controller'
HeaderView = require 'views/header-view'
mediator = require 'mediator'
module.exports = class HeaderController extends Controller
initialize: ->
super
@view = new HeaderView collection: mediator.todos
Controller = require 'controllers/base/controller'
Controller = require './base/controller'
module.exports = class IndexController extends Controller
title: 'Todo list'
list: (options) ->
@publishEvent 'todos:filter', options.filterer?.trim() ? 'all'
list: (params) ->
@publishEvent 'todos:filter', params.filterer?.trim() ? 'all'
Controller = require 'controllers/base/controller'
TodosView = require 'views/todos-view'
mediator = require 'mediator'
module.exports = class TodosController extends Controller
initialize: ->
super
@view = new TodosView collection: mediator.todos
Application = require 'application'
routes = require 'routes'
# Initialize the application on DOM ready event.
$ ->
app = new Application()
app.initialize()
new Application
controllerSuffix: '-controller', pushState: no, routes: routes
Chaplin = require 'chaplin'
utils = require 'lib/utils'
# Application-specific feature detection
# --------------------------------------
# Delegate to Chaplin’s support module
support = utils.beget Chaplin.support
# _(support).extend
# someMethod: ->
module.exports = support
Chaplin = require 'chaplin'
# Application-specific utilities
# ------------------------------
......
mediator = require 'mediator'
utils = require 'chaplin/lib/utils'
utils = require './utils'
# Application-specific view helpers
# ---------------------------------
......
module.exports = require('chaplin').mediator
module.exports = Chaplin.mediator
Chaplin = require 'chaplin'
Model = require 'models/base/model'
module.exports = class Collection extends Chaplin.Collection
......
Chaplin = require 'chaplin'
module.exports = class Model extends Chaplin.Model
# Mixin a synchronization state machine
# _(@prototype).extend Chaplin.SyncMachine
Chaplin = require 'chaplin'
View = require 'views/base/view'
module.exports = class CollectionView extends Chaplin.CollectionView
......
Chaplin = require 'chaplin'
require 'lib/view-helper' # Just load the view helpers, no return value
module.exports = class View extends Chaplin.View
......
......@@ -4,26 +4,26 @@ template = require 'views/templates/footer'
module.exports = class FooterView extends View
autoRender: yes
el: '#footer'
events:
'click #clear-completed': 'clearCompleted'
listen:
'todos:filter mediator': 'updateFilterer'
'all collection': 'renderCounter'
template: template
initialize: ->
super
@subscribeEvent 'todos:filter', @updateFilterer
@modelBind 'all', @renderCounter
@delegate 'click', '#clear-completed', @clearCompleted
render: =>
render: ->
super
@renderCounter()
updateFilterer: (filterer) =>
updateFilterer: (filterer) ->
console.log 'updateFilterer'
filterer = '' if filterer is 'all'
@$('#filters a')
.removeClass('selected')
.filter("[href='#/#{filterer}']")
.addClass('selected')
renderCounter: =>
renderCounter: ->
total = @collection.length
active = @collection.getActive().length
completed = @collection.getCompleted().length
......
......@@ -4,15 +4,13 @@ template = require 'views/templates/header'
module.exports = class HeaderView extends View
autoRender: yes
el: '#header'
events:
'keypress #new-todo': 'createOnEnter'
template: template
initialize: ->
super
@delegate 'keypress', '#new-todo', @createOnEnter
createOnEnter: (event) =>
ENTER_KEY = 13
title = $(event.currentTarget).val().trim()
return if event.keyCode isnt ENTER_KEY or not title
@collection.create {title}
@$('#new-todo').val ''
createOnEnter: (event) =>
ENTER_KEY = 13
title = $(event.currentTarget).val().trim()
return if event.keyCode isnt ENTER_KEY or not title
@collection.create {title}
@$('#new-todo').val ''
Chaplin = require 'chaplin'
# Layout is the top-level application ‘view’.
module.exports = class Layout extends Chaplin.Layout
initialize: ->
super
@subscribeEvent 'todos:filter', @changeFilterer
listen:
'todos:filter mediator': 'changeFilterer'
changeFilterer: (filterer = 'all') ->
$('#todoapp').attr 'class', "filter-#{filterer}"
......@@ -2,18 +2,19 @@ View = require 'views/base/view'
template = require 'views/templates/todo'
module.exports = class TodoView extends View
events:
'click .toggle': 'toggle'
'dblclick label': 'edit'
'keypress .edit': 'save'
'blur .edit': 'save'
'click .destroy': 'destroy'
listen:
'change model': 'render'
template: template
tagName: 'li'
initialize: ->
super
@modelBind 'change', @render
@delegate 'click', '.destroy', @destroy
@delegate 'dblclick', 'label', @edit
@delegate 'keypress', '.edit', @save
@delegate 'click', '.toggle', @toggle
@delegate 'blur', '.edit', @save
render: =>
super
# Reset classes, re-add the appropriate ones.
......
......@@ -4,16 +4,15 @@ TodoView = require 'views/todo-view'
module.exports = class TodosView extends CollectionView
el: '#main'
events:
'click #toggle-all': 'toggleCompleted'
itemView: TodoView
listSelector: '#todo-list'
listen:
'all collection': 'renderCheckbox'
'todos:clear mediator': 'clear'
template: template
initialize: ->
super
@subscribeEvent 'todos:clear', @clear
@modelBind 'all', @renderCheckbox
@delegate 'click', '#toggle-all', @toggleCompleted
render: =>
super
@renderCheckbox()
......
......@@ -2,6 +2,25 @@
"name": "todomvc-chaplin-brunch",
"version": "0.0.0",
"dependencies": {
"todomvc-common": "~0.1.6"
"todomvc-common": "~0.1.6",
"chaplin": "~0.10.0",
"underscore": "~1.4.4",
"backbone.localStorage": "~1.1.0",
"jquery": "~2.0.0"
},
"overrides": {
"todomvc-common": {
"main": "bg.png"
},
"backbone": {
"main": "backbone.js",
"dependencies": {
"underscore": "*",
"jquery": "*"
}
},
"underscore": {
"main": "underscore.js"
}
}
}
\ No newline at end of file
}
......@@ -136,10 +136,6 @@
}
function getFile(file, callback) {
if (!location.host) {
return console.info('Miss the info bar? Run TodoMVC from a server to avoid a cross-origin error.');
}
var xhr = new XMLHttpRequest();
xhr.open('GET', findRoot() + file, true);
......
......@@ -2,28 +2,10 @@ exports.config =
# See http://brunch.readthedocs.org/en/latest/config.html for documentation.
files:
javascripts:
joinTo:
'javascripts/app.js': /^app/
'javascripts/vendor.js': /^vendor/
'test/javascripts/test.js': /^test[\\/](?!vendor)/
'test/javascripts/test-vendor.js': /^test[\\/](?=vendor)/
order:
# Files in `vendor` directories are compiled before other files
# even if they aren't specified in order.before.
before: [
'vendor/scripts/console-helper.js',
'vendor/scripts/jquery-1.8.2.js',
'vendor/scripts/underscore-1.4.2.js',
'vendor/scripts/backbone-0.9.2.js'
]
joinTo: 'app.js'
stylesheets:
joinTo:
'stylesheets/app.css': /^(app|vendor)/
'test/stylesheets/test.css': /^test/
order:
before: ['vendor/styles/normalize-2.0.1.css']
after: ['vendor/styles/helpers.css']
joinTo: 'app.css'
templates:
joinTo: 'javascripts/app.js'
joinTo: 'app.js'
......@@ -11,16 +11,16 @@
"test": "brunch test"
},
"dependencies": {
"javascript-brunch": ">= 1.0 < 1.5",
"coffee-script-brunch": ">= 1.0 < 1.5",
"javascript-brunch": ">= 1.0 < 1.8",
"coffee-script-brunch": ">= 1.0 < 1.8",
"css-brunch": ">= 1.0 < 1.5",
"stylus-brunch": ">= 1.0 < 1.5",
"css-brunch": ">= 1.0 < 1.8",
"stylus-brunch": ">= 1.0 < 1.8",
"handlebars-brunch": ">= 1.0 < 1.5",
"handlebars-brunch": ">= 1.0 < 1.8",
"uglify-js-brunch": ">= 1.0 < 1.5",
"clean-css-brunch": ">= 1.0 < 1.5"
"uglify-js-brunch": ">= 1.0 < 1.8",
"clean-css-brunch": ">= 1.0 < 1.8"
},
"devDependencies": {
"chai": "~1.2.0",
......
#todoapp.filter-active #todo-list .completed {
display: none; }
#todoapp.filter-completed #todo-list .active {
display: none; }
/*@ sourceMappingURL=app.css.map*/
\ No newline at end of file
{"version":3,"file":"public/app.css","sources":["app/views/filters.css"],"names":[],"mappings":"AAAA;AACA;AACA;AACA;AACA;AACA","sourcesContent":["#todoapp.filter-active #todo-list .completed {\n display: none; }\n\n#todoapp.filter-completed #todo-list .active {\n display: none; }\n"]}
\ No newline at end of file
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
......@@ -3,22 +3,14 @@
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<title>Chaplin & Brunch • TodoMVC</title>
<title>Chaplin &amp; Brunch • TodoMVC</title>
<link rel="stylesheet" href="../bower_components/todomvc-common/base.css">
<link rel="stylesheet" href="../bower_components/todomvc-common/base.js">
<!--[if IE]>
<script src="../../../assets/ie.js"></script>
<![endif]-->
<link rel="stylesheet" href="stylesheets/app.css">
<!-- Usually all these files are concatenated automatically
by brunch and you need just to import `vendor.js` -->
<script src="../bower_components/todomvc-common/base.js"></script>
<script src="../../../../assets/jquery.min.js"></script>
<script src="../../../../assets/lodash.min.js"></script>
<script src="../../../../assets/handlebars.min.js"></script>
<script src="javascripts/vendor.js"></script>
<script src="javascripts/app.js"></script>
<link rel="stylesheet" href="app.css">
<script src="app.js"></script>
<script>require('initialize');</script>
</head>
<body>
......
(function(/*! Brunch !*/) {
'use strict';
var globals = typeof window !== 'undefined' ? window : global;
if (typeof globals.require === 'function') return;
var modules = {};
var cache = {};
var has = function(object, name) {
return ({}).hasOwnProperty.call(object, name);
};
var expand = function(root, name) {
var results = [], parts, part;
if (/^\.\.?(\/|$)/.test(name)) {
parts = [root, name].join('/').split('/');
} else {
parts = name.split('/');
}
for (var i = 0, length = parts.length; i < length; i++) {
part = parts[i];
if (part === '..') {
results.pop();
} else if (part !== '.' && part !== '') {
results.push(part);
}
}
return results.join('/');
};
var dirname = function(path) {
return path.split('/').slice(0, -1).join('/');
};
var localRequire = function(path) {
return function(name) {
var dir = dirname(path);
var absolute = expand(dir, name);
return globals.require(absolute);
};
};
var initModule = function(name, definition) {
var module = {id: name, exports: {}};
definition(module.exports, localRequire(name), module);
var exports = cache[name] = module.exports;
return exports;
};
var require = function(name) {
var path = expand(name, '.');
if (has(cache, path)) return cache[path];
if (has(modules, path)) return initModule(path, modules[path]);
var dirIndex = expand(path, './index');
if (has(cache, dirIndex)) return cache[dirIndex];
if (has(modules, dirIndex)) return initModule(dirIndex, modules[dirIndex]);
throw new Error('Cannot find module "' + name + '"');
};
var define = function(bundle, fn) {
if (typeof bundle === 'object') {
for (var key in bundle) {
if (has(bundle, key)) {
modules[key] = bundle[key];
}
}
} else {
modules[bundle] = fn;
}
}
globals.require = require;
globals.require.define = define;
globals.require.brunch = true;
})();
window.require.define({"application": function(exports, require, module) {
var Application, Chaplin, FooterController, HeaderController, Layout, Todos, TodosController, mediator, routes,
__hasProp = {}.hasOwnProperty,
__extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; };
Chaplin = require('chaplin');
mediator = require('mediator');
routes = require('routes');
HeaderController = require('controllers/header-controller');
FooterController = require('controllers/footer-controller');
TodosController = require('controllers/todos-controller');
Todos = require('models/todos');
Layout = require('views/layout');
module.exports = Application = (function(_super) {
__extends(Application, _super);
function Application() {
return Application.__super__.constructor.apply(this, arguments);
}
Application.prototype.title = 'Chaplin • TodoMVC';
Application.prototype.initialize = function() {
Application.__super__.initialize.apply(this, arguments);
this.initDispatcher({
controllerSuffix: '-controller'
});
this.initLayout();
this.initMediator();
this.initControllers();
this.initRouter(routes, {
pushState: false
});
return typeof Object.freeze === "function" ? Object.freeze(this) : void 0;
};
Application.prototype.initLayout = function() {
return this.layout = new Layout({
title: this.title
});
};
Application.prototype.initControllers = function() {
new HeaderController();
new FooterController();
return new TodosController();
};
Application.prototype.initMediator = function() {
mediator.user = null;
mediator.todos = new Todos();
mediator.todos.fetch();
return mediator.seal();
};
return Application;
})(Chaplin.Application);
}});
window.require.define({"controllers/base/controller": function(exports, require, module) {
var Chaplin, Controller,
__hasProp = {}.hasOwnProperty,
__extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; };
Chaplin = require('chaplin');
module.exports = Controller = (function(_super) {
__extends(Controller, _super);
function Controller() {
return Controller.__super__.constructor.apply(this, arguments);
}
return Controller;
})(Chaplin.Controller);
}});
window.require.define({"controllers/footer-controller": function(exports, require, module) {
var Controller, FooterController, FooterView, mediator,
__hasProp = {}.hasOwnProperty,
__extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; };
Controller = require('controllers/base/controller');
FooterView = require('views/footer-view');
mediator = require('mediator');
module.exports = FooterController = (function(_super) {
__extends(FooterController, _super);
function FooterController() {
return FooterController.__super__.constructor.apply(this, arguments);
}
FooterController.prototype.initialize = function() {
FooterController.__super__.initialize.apply(this, arguments);
return this.view = new FooterView({
collection: mediator.todos
});
};
return FooterController;
})(Controller);
}});
window.require.define({"controllers/header-controller": function(exports, require, module) {
var Controller, HeaderController, HeaderView, mediator,
__hasProp = {}.hasOwnProperty,
__extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; };
Controller = require('controllers/base/controller');
HeaderView = require('views/header-view');
mediator = require('mediator');
module.exports = HeaderController = (function(_super) {
__extends(HeaderController, _super);
function HeaderController() {
return HeaderController.__super__.constructor.apply(this, arguments);
}
HeaderController.prototype.initialize = function() {
HeaderController.__super__.initialize.apply(this, arguments);
return this.view = new HeaderView({
collection: mediator.todos
});
};
return HeaderController;
})(Controller);
}});
window.require.define({"controllers/index-controller": function(exports, require, module) {
var Controller, IndexController,
__hasProp = {}.hasOwnProperty,
__extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; };
Controller = require('controllers/base/controller');
module.exports = IndexController = (function(_super) {
__extends(IndexController, _super);
function IndexController() {
return IndexController.__super__.constructor.apply(this, arguments);
}
IndexController.prototype.title = 'Todo list';
IndexController.prototype.list = function(options) {
var _ref, _ref1;
return this.publishEvent('todos:filter', (_ref = (_ref1 = options.filterer) != null ? _ref1.trim() : void 0) != null ? _ref : 'all');
};
return IndexController;
})(Controller);
}});
window.require.define({"controllers/todos-controller": function(exports, require, module) {
var Controller, TodosController, TodosView, mediator,
__hasProp = {}.hasOwnProperty,
__extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; };
Controller = require('controllers/base/controller');
TodosView = require('views/todos-view');
mediator = require('mediator');
module.exports = TodosController = (function(_super) {
__extends(TodosController, _super);
function TodosController() {
return TodosController.__super__.constructor.apply(this, arguments);
}
TodosController.prototype.initialize = function() {
TodosController.__super__.initialize.apply(this, arguments);
return this.view = new TodosView({
collection: mediator.todos
});
};
return TodosController;
})(Controller);
}});
window.require.define({"initialize": function(exports, require, module) {
var Application;
Application = require('application');
$(function() {
var app;
app = new Application();
return app.initialize();
});
}});
window.require.define({"lib/support": function(exports, require, module) {
var Chaplin, support, utils;
Chaplin = require('chaplin');
utils = require('lib/utils');
support = utils.beget(Chaplin.support);
module.exports = support;
}});
window.require.define({"lib/utils": function(exports, require, module) {
var Chaplin, utils;
Chaplin = require('chaplin');
utils = Chaplin.utils.beget(Chaplin.utils);
module.exports = utils;
}});
window.require.define({"lib/view-helper": function(exports, require, module) {
var mediator, utils;
mediator = require('mediator');
utils = require('chaplin/lib/utils');
Handlebars.registerHelper('if_logged_in', function(options) {
if (mediator.user) {
return options.fn(this);
} else {
return options.inverse(this);
}
});
Handlebars.registerHelper('with', function(context, options) {
if (!context || Handlebars.Utils.isEmpty(context)) {
return options.inverse(this);
} else {
return options.fn(context);
}
});
Handlebars.registerHelper('without', function(context, options) {
var inverse;
inverse = options.inverse;
options.inverse = options.fn;
options.fn = inverse;
return Handlebars.helpers["with"].call(this, context, options);
});
Handlebars.registerHelper('with_user', function(options) {
var context, _ref;
context = ((_ref = mediator.user) != null ? _ref.serialize() : void 0) || {};
return Handlebars.helpers["with"].call(this, context, options);
});
}});
window.require.define({"mediator": function(exports, require, module) {
module.exports = require('chaplin').mediator;
}});
window.require.define({"models/base/collection": function(exports, require, module) {
var Chaplin, Collection, Model,
__hasProp = {}.hasOwnProperty,
__extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; };
Chaplin = require('chaplin');
Model = require('models/base/model');
module.exports = Collection = (function(_super) {
__extends(Collection, _super);
function Collection() {
return Collection.__super__.constructor.apply(this, arguments);
}
Collection.prototype.model = Model;
return Collection;
})(Chaplin.Collection);
}});
window.require.define({"models/base/model": function(exports, require, module) {
var Chaplin, Model,
__hasProp = {}.hasOwnProperty,
__extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; };
Chaplin = require('chaplin');
module.exports = Model = (function(_super) {
__extends(Model, _super);
function Model() {
return Model.__super__.constructor.apply(this, arguments);
}
return Model;
})(Chaplin.Model);
}});
window.require.define({"models/todo": function(exports, require, module) {
var Model, Todo,
__hasProp = {}.hasOwnProperty,
__extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; };
Model = require('models/base/model');
module.exports = Todo = (function(_super) {
__extends(Todo, _super);
function Todo() {
return Todo.__super__.constructor.apply(this, arguments);
}
Todo.prototype.defaults = {
title: '',
completed: false
};
Todo.prototype.initialize = function() {
Todo.__super__.initialize.apply(this, arguments);
if (this.isNew()) {
return this.set('created', Date.now());
}
};
Todo.prototype.toggle = function() {
return this.set({
completed: !this.get('completed')
});
};
Todo.prototype.isVisible = function() {
var isCompleted;
return isCompleted = this.get('completed');
};
return Todo;
})(Model);
}});
window.require.define({"models/todos": function(exports, require, module) {
var Collection, Todo, Todos,
__hasProp = {}.hasOwnProperty,
__extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; };
Collection = require('models/base/collection');
Todo = require('models/todo');
module.exports = Todos = (function(_super) {
__extends(Todos, _super);
function Todos() {
return Todos.__super__.constructor.apply(this, arguments);
}
Todos.prototype.model = Todo;
Todos.prototype.localStorage = new Store('todos-chaplin');
Todos.prototype.allAreCompleted = function() {
return this.getCompleted().length === this.length;
};
Todos.prototype.getCompleted = function() {
return this.where({
completed: true
});
};
Todos.prototype.getActive = function() {
return this.where({
completed: false
});
};
Todos.prototype.comparator = function(todo) {
return todo.get('created');
};
return Todos;
})(Collection);
}});
window.require.define({"routes": function(exports, require, module) {
module.exports = function(match) {
match(':filterer', 'index#list');
return match('', 'index#list');
};
}});
window.require.define({"views/base/collection-view": function(exports, require, module) {
var Chaplin, CollectionView, View,
__hasProp = {}.hasOwnProperty,
__extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; };
Chaplin = require('chaplin');
View = require('views/base/view');
module.exports = CollectionView = (function(_super) {
__extends(CollectionView, _super);
function CollectionView() {
return CollectionView.__super__.constructor.apply(this, arguments);
}
CollectionView.prototype.getTemplateFunction = View.prototype.getTemplateFunction;
return CollectionView;
})(Chaplin.CollectionView);
}});
window.require.define({"views/base/view": function(exports, require, module) {
var Chaplin, View,
__hasProp = {}.hasOwnProperty,
__extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; };
Chaplin = require('chaplin');
require('lib/view-helper');
module.exports = View = (function(_super) {
__extends(View, _super);
function View() {
return View.__super__.constructor.apply(this, arguments);
}
View.prototype.getTemplateFunction = function() {
return this.template;
};
return View;
})(Chaplin.View);
}});
window.require.define({"views/footer-view": function(exports, require, module) {
var FooterView, View, template,
__bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; },
__hasProp = {}.hasOwnProperty,
__extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; };
View = require('views/base/view');
template = require('views/templates/footer');
module.exports = FooterView = (function(_super) {
__extends(FooterView, _super);
function FooterView() {
this.renderCounter = __bind(this.renderCounter, this);
this.updateFilterer = __bind(this.updateFilterer, this);
this.render = __bind(this.render, this);
return FooterView.__super__.constructor.apply(this, arguments);
}
FooterView.prototype.autoRender = true;
FooterView.prototype.el = '#footer';
FooterView.prototype.template = template;
FooterView.prototype.initialize = function() {
FooterView.__super__.initialize.apply(this, arguments);
this.subscribeEvent('todos:filter', this.updateFilterer);
this.modelBind('all', this.renderCounter);
return this.delegate('click', '#clear-completed', this.clearCompleted);
};
FooterView.prototype.render = function() {
FooterView.__super__.render.apply(this, arguments);
return this.renderCounter();
};
FooterView.prototype.updateFilterer = function(filterer) {
if (filterer === 'all') {
filterer = '';
}
return this.$('#filters a').removeClass('selected').filter("[href='#/" + filterer + "']").addClass('selected');
};
FooterView.prototype.renderCounter = function() {
var active, completed, countDescription, total;
total = this.collection.length;
active = this.collection.getActive().length;
completed = this.collection.getCompleted().length;
this.$('#todo-count > strong').html(active);
countDescription = (active === 1 ? 'item' : 'items');
this.$('.todo-count-title').text(countDescription);
this.$('#completed-count').html("(" + completed + ")");
this.$('#clear-completed').toggle(completed > 0);
return this.$el.toggle(total > 0);
};
FooterView.prototype.clearCompleted = function() {
return this.publishEvent('todos:clear');
};
return FooterView;
})(View);
}});
window.require.define({"views/header-view": function(exports, require, module) {
var HeaderView, View, template,
__bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; },
__hasProp = {}.hasOwnProperty,
__extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; };
View = require('views/base/view');
template = require('views/templates/header');
module.exports = HeaderView = (function(_super) {
__extends(HeaderView, _super);
function HeaderView() {
this.createOnEnter = __bind(this.createOnEnter, this);
return HeaderView.__super__.constructor.apply(this, arguments);
}
HeaderView.prototype.autoRender = true;
HeaderView.prototype.el = '#header';
HeaderView.prototype.template = template;
HeaderView.prototype.initialize = function() {
HeaderView.__super__.initialize.apply(this, arguments);
return this.delegate('keypress', '#new-todo', this.createOnEnter);
};
HeaderView.prototype.createOnEnter = function(event) {
var ENTER_KEY, title;
ENTER_KEY = 13;
title = $(event.currentTarget).val().trim();
if (event.keyCode !== ENTER_KEY || !title) {
return;
}
this.collection.create({
title: title
});
return this.$('#new-todo').val('');
};
return HeaderView;
})(View);
}});
window.require.define({"views/layout": function(exports, require, module) {
var Chaplin, Layout,
__hasProp = {}.hasOwnProperty,
__extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; };
Chaplin = require('chaplin');
module.exports = Layout = (function(_super) {
__extends(Layout, _super);
function Layout() {
return Layout.__super__.constructor.apply(this, arguments);
}
Layout.prototype.initialize = function() {
Layout.__super__.initialize.apply(this, arguments);
return this.subscribeEvent('todos:filter', this.changeFilterer);
};
Layout.prototype.changeFilterer = function(filterer) {
if (filterer == null) {
filterer = 'all';
}
return $('#todoapp').attr('class', "filter-" + filterer);
};
return Layout;
})(Chaplin.Layout);
}});
window.require.define({"views/templates/footer": function(exports, require, module) {
module.exports = Handlebars.template(function (Handlebars,depth0,helpers,partials,data) {
helpers = helpers || Handlebars.helpers;
return "<span id=\"todo-count\">\n <strong></strong>\n <span class=\"todo-count-title\">items</span>\n left\n</span>\n<ul id=\"filters\">\n <li>\n <a href=\"#/\">All</a>\n </li>\n <li>\n <a href=\"#/active\">Active</a>\n </li>\n <li>\n <a href=\"#/completed\">Completed</a>\n </li>\n</ul>\n<button id=\"clear-completed\">\n Clear completed\n <span id=\"completed-count\"></span>\n</button>\n";});
}});
window.require.define({"views/templates/header": function(exports, require, module) {
module.exports = Handlebars.template(function (Handlebars,depth0,helpers,partials,data) {
helpers = helpers || Handlebars.helpers;
return "<h1>todos</h1>\n<input id=\"new-todo\" placeholder=\"What needs to be done?\" autofocus>\n";});
}});
window.require.define({"views/templates/todo": function(exports, require, module) {
module.exports = Handlebars.template(function (Handlebars,depth0,helpers,partials,data) {
helpers = helpers || Handlebars.helpers;
var buffer = "", stack1, foundHelper, self=this, functionType="function", escapeExpression=this.escapeExpression;
function program1(depth0,data) {
return " checked";}
buffer += "<div class=\"view\">\n <input class=\"toggle\" type=\"checkbox\"";
stack1 = depth0.completed;
stack1 = helpers['if'].call(depth0, stack1, {hash:{},inverse:self.noop,fn:self.program(1, program1, data)});
if(stack1 || stack1 === 0) { buffer += stack1; }
buffer += ">\n <label>";
foundHelper = helpers.title;
if (foundHelper) { stack1 = foundHelper.call(depth0, {hash:{}}); }
else { stack1 = depth0.title; stack1 = typeof stack1 === functionType ? stack1() : stack1; }
buffer += escapeExpression(stack1) + "</label>\n <button class=\"destroy\"></button>\n</div>\n<input class=\"edit\" value=\"";
foundHelper = helpers.title;
if (foundHelper) { stack1 = foundHelper.call(depth0, {hash:{}}); }
else { stack1 = depth0.title; stack1 = typeof stack1 === functionType ? stack1() : stack1; }
buffer += escapeExpression(stack1) + "\">\n";
return buffer;});
}});
window.require.define({"views/templates/todos": function(exports, require, module) {
module.exports = Handlebars.template(function (Handlebars,depth0,helpers,partials,data) {
helpers = helpers || Handlebars.helpers;
return "<input id=\"toggle-all\" type=\"checkbox\">\n<label for=\"toggle-all\">Mark all as complete</label>\n<ul id=\"todo-list\"></ul>\n";});
}});
window.require.define({"views/todo-view": function(exports, require, module) {
var TodoView, View, template,
__bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; },
__hasProp = {}.hasOwnProperty,
__extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; };
View = require('views/base/view');
template = require('views/templates/todo');
module.exports = TodoView = (function(_super) {
__extends(TodoView, _super);
function TodoView() {
this.save = __bind(this.save, this);
this.edit = __bind(this.edit, this);
this.toggle = __bind(this.toggle, this);
this.destroy = __bind(this.destroy, this);
this.render = __bind(this.render, this);
return TodoView.__super__.constructor.apply(this, arguments);
}
TodoView.prototype.template = template;
TodoView.prototype.tagName = 'li';
TodoView.prototype.initialize = function() {
TodoView.__super__.initialize.apply(this, arguments);
this.modelBind('change', this.render);
this.delegate('click', '.destroy', this.destroy);
this.delegate('dblclick', 'label', this.edit);
this.delegate('keypress', '.edit', this.save);
this.delegate('click', '.toggle', this.toggle);
return this.delegate('blur', '.edit', this.save);
};
TodoView.prototype.render = function() {
var className;
TodoView.__super__.render.apply(this, arguments);
this.$el.removeClass('active completed');
className = this.model.get('completed') ? 'completed' : 'active';
return this.$el.addClass(className);
};
TodoView.prototype.destroy = function() {
return this.model.destroy();
};
TodoView.prototype.toggle = function() {
return this.model.toggle().save();
};
TodoView.prototype.edit = function() {
this.$el.addClass('editing');
return this.$('.edit').focus();
};
TodoView.prototype.save = function(event) {
var ENTER_KEY, title;
ENTER_KEY = 13;
title = $(event.currentTarget).val().trim();
if (!title) {
return this.model.destroy();
}
if (event.type === 'keypress' && event.keyCode !== ENTER_KEY) {
return;
}
this.model.save({
title: title
});
return this.$el.removeClass('editing');
};
return TodoView;
})(View);
}});
window.require.define({"views/todos-view": function(exports, require, module) {
var CollectionView, TodoView, TodosView, template,
__bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; },
__hasProp = {}.hasOwnProperty,
__extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; };
CollectionView = require('views/base/collection-view');
template = require('views/templates/todos');
TodoView = require('views/todo-view');
module.exports = TodosView = (function(_super) {
__extends(TodosView, _super);
function TodosView() {
this.toggleCompleted = __bind(this.toggleCompleted, this);
this.renderCheckbox = __bind(this.renderCheckbox, this);
this.render = __bind(this.render, this);
return TodosView.__super__.constructor.apply(this, arguments);
}
TodosView.prototype.el = '#main';
TodosView.prototype.itemView = TodoView;
TodosView.prototype.listSelector = '#todo-list';
TodosView.prototype.template = template;
TodosView.prototype.initialize = function() {
TodosView.__super__.initialize.apply(this, arguments);
this.subscribeEvent('todos:clear', this.clear);
this.modelBind('all', this.renderCheckbox);
return this.delegate('click', '#toggle-all', this.toggleCompleted);
};
TodosView.prototype.render = function() {
TodosView.__super__.render.apply(this, arguments);
return this.renderCheckbox();
};
TodosView.prototype.renderCheckbox = function() {
this.$('#toggle-all').prop('checked', this.collection.allAreCompleted());
return this.$el.toggle(this.collection.length !== 0);
};
TodosView.prototype.toggleCompleted = function(event) {
var isChecked;
isChecked = event.currentTarget.checked;
return this.collection.each(function(todo) {
return todo.save({
completed: isChecked
});
});
};
TodosView.prototype.clear = function() {
return this.collection.getCompleted().forEach(function(model) {
return model.destroy();
});
};
return TodosView;
})(CollectionView);
}});
This source diff could not be displayed because it is too large. You can view the blob instead.
......@@ -40,7 +40,7 @@ If you haven't already installed [Brunch](http://brunch.io), run:
Once you have Brunch, install this application's dependencies:
# from labs/dependency-examples/chaplin-brunch
npm install
npm install & bower install
To build the app, run:
......
// Backbone.js 0.9.2
// (c) 2010-2012 Jeremy Ashkenas, DocumentCloud Inc.
// Backbone may be freely distributed under the MIT license.
// For all details and documentation:
// http://backbonejs.org
(function(){
// Initial Setup
// -------------
// Save a reference to the global object (`window` in the browser, `global`
// on the server).
var root = this;
// Save the previous value of the `Backbone` variable, so that it can be
// restored later on, if `noConflict` is used.
var previousBackbone = root.Backbone;
// Create a local reference to slice/splice.
var slice = Array.prototype.slice;
var splice = Array.prototype.splice;
// The top-level namespace. All public Backbone classes and modules will
// be attached to this. Exported for both CommonJS and the browser.
var Backbone;
if (typeof exports !== 'undefined') {
Backbone = exports;
} else {
Backbone = root.Backbone = {};
}
// Current version of the library. Keep in sync with `package.json`.
Backbone.VERSION = '0.9.2';
// Require Underscore, if we're on the server, and it's not already present.
var _ = root._;
if (!_ && (typeof require !== 'undefined')) _ = require('underscore');
// For Backbone's purposes, jQuery, Zepto, or Ender owns the `$` variable.
var $ = root.jQuery || root.Zepto || root.ender;
// Set the JavaScript library that will be used for DOM manipulation and
// Ajax calls (a.k.a. the `$` variable). By default Backbone will use: jQuery,
// Zepto, or Ender; but the `setDomLibrary()` method lets you inject an
// alternate JavaScript library (or a mock library for testing your views
// outside of a browser).
Backbone.setDomLibrary = function(lib) {
$ = lib;
};
// Runs Backbone.js in *noConflict* mode, returning the `Backbone` variable
// to its previous owner. Returns a reference to this Backbone object.
Backbone.noConflict = function() {
root.Backbone = previousBackbone;
return this;
};
// Turn on `emulateHTTP` to support legacy HTTP servers. Setting this option
// will fake `"PUT"` and `"DELETE"` requests via the `_method` parameter and
// set a `X-Http-Method-Override` header.
Backbone.emulateHTTP = false;
// Turn on `emulateJSON` to support legacy servers that can't deal with direct
// `application/json` requests ... will encode the body as
// `application/x-www-form-urlencoded` instead and will send the model in a
// form param named `model`.
Backbone.emulateJSON = false;
// Backbone.Events
// -----------------
// Regular expression used to split event strings
var eventSplitter = /\s+/;
// A module that can be mixed in to *any object* in order to provide it with
// custom events. You may bind with `on` or remove with `off` callback functions
// to an event; trigger`-ing an event fires all callbacks in succession.
//
// var object = {};
// _.extend(object, Backbone.Events);
// object.on('expand', function(){ alert('expanded'); });
// object.trigger('expand');
//
var Events = Backbone.Events = {
// Bind one or more space separated events, `events`, to a `callback`
// function. Passing `"all"` will bind the callback to all events fired.
on: function(events, callback, context) {
var calls, event, node, tail, list;
if (!callback) return this;
events = events.split(eventSplitter);
calls = this._callbacks || (this._callbacks = {});
// Create an immutable callback list, allowing traversal during
// modification. The tail is an empty object that will always be used
// as the next node.
while (event = events.shift()) {
list = calls[event];
node = list ? list.tail : {};
node.next = tail = {};
node.context = context;
node.callback = callback;
calls[event] = {tail: tail, next: list ? list.next : node};
}
return this;
},
// Remove one or many callbacks. If `context` is null, removes all callbacks
// with that function. If `callback` is null, removes all callbacks for the
// event. If `events` is null, removes all bound callbacks for all events.
off: function(events, callback, context) {
var event, calls, node, tail, cb, ctx;
// No events, or removing *all* events.
if (!(calls = this._callbacks)) return;
if (!(events || callback || context)) {
delete this._callbacks;
return this;
}
// Loop through the listed events and contexts, splicing them out of the
// linked list of callbacks if appropriate.
events = events ? events.split(eventSplitter) : _.keys(calls);
while (event = events.shift()) {
node = calls[event];
delete calls[event];
if (!node || !(callback || context)) continue;
// Create a new list, omitting the indicated callbacks.
tail = node.tail;
while ((node = node.next) !== tail) {
cb = node.callback;
ctx = node.context;
if ((callback && cb !== callback) || (context && ctx !== context)) {
this.on(event, cb, ctx);
}
}
}
return this;
},
// Trigger one or many events, firing all bound callbacks. Callbacks are
// passed the same arguments as `trigger` is, apart from the event name
// (unless you're listening on `"all"`, which will cause your callback to
// receive the true name of the event as the first argument).
trigger: function(events) {
var event, node, calls, tail, args, all, rest;
if (!(calls = this._callbacks)) return this;
all = calls.all;
events = events.split(eventSplitter);
rest = slice.call(arguments, 1);
// For each event, walk through the linked list of callbacks twice,
// first to trigger the event, then to trigger any `"all"` callbacks.
while (event = events.shift()) {
if (node = calls[event]) {
tail = node.tail;
while ((node = node.next) !== tail) {
node.callback.apply(node.context || this, rest);
}
}
if (node = all) {
tail = node.tail;
args = [event].concat(rest);
while ((node = node.next) !== tail) {
node.callback.apply(node.context || this, args);
}
}
}
return this;
}
};
// Aliases for backwards compatibility.
Events.bind = Events.on;
Events.unbind = Events.off;
// Backbone.Model
// --------------
// Create a new model, with defined attributes. A client id (`cid`)
// is automatically generated and assigned for you.
var Model = Backbone.Model = function(attributes, options) {
var defaults;
attributes || (attributes = {});
if (options && options.parse) attributes = this.parse(attributes);
if (defaults = getValue(this, 'defaults')) {
attributes = _.extend({}, defaults, attributes);
}
if (options && options.collection) this.collection = options.collection;
this.attributes = {};
this._escapedAttributes = {};
this.cid = _.uniqueId('c');
this.changed = {};
this._silent = {};
this._pending = {};
this.set(attributes, {silent: true});
// Reset change tracking.
this.changed = {};
this._silent = {};
this._pending = {};
this._previousAttributes = _.clone(this.attributes);
this.initialize.apply(this, arguments);
};
// Attach all inheritable methods to the Model prototype.
_.extend(Model.prototype, Events, {
// A hash of attributes whose current and previous value differ.
changed: null,
// A hash of attributes that have silently changed since the last time
// `change` was called. Will become pending attributes on the next call.
_silent: null,
// A hash of attributes that have changed since the last `'change'` event
// began.
_pending: null,
// The default name for the JSON `id` attribute is `"id"`. MongoDB and
// CouchDB users may want to set this to `"_id"`.
idAttribute: 'id',
// Initialize is an empty function by default. Override it with your own
// initialization logic.
initialize: function(){},
// Return a copy of the model's `attributes` object.
toJSON: function(options) {
return _.clone(this.attributes);
},
// Get the value of an attribute.
get: function(attr) {
return this.attributes[attr];
},
// Get the HTML-escaped value of an attribute.
escape: function(attr) {
var html;
if (html = this._escapedAttributes[attr]) return html;
var val = this.get(attr);
return this._escapedAttributes[attr] = _.escape(val == null ? '' : '' + val);
},
// Returns `true` if the attribute contains a value that is not null
// or undefined.
has: function(attr) {
return this.get(attr) != null;
},
// Set a hash of model attributes on the object, firing `"change"` unless
// you choose to silence it.
set: function(key, value, options) {
var attrs, attr, val;
// Handle both
if (_.isObject(key) || key == null) {
attrs = key;
options = value;
} else {
attrs = {};
attrs[key] = value;
}
// Extract attributes and options.
options || (options = {});
if (!attrs) return this;
if (attrs instanceof Model) attrs = attrs.attributes;
if (options.unset) for (attr in attrs) attrs[attr] = void 0;
// Run validation.
if (!this._validate(attrs, options)) return false;
// Check for changes of `id`.
if (this.idAttribute in attrs) this.id = attrs[this.idAttribute];
var changes = options.changes = {};
var now = this.attributes;
var escaped = this._escapedAttributes;
var prev = this._previousAttributes || {};
// For each `set` attribute...
for (attr in attrs) {
val = attrs[attr];
// If the new and current value differ, record the change.
if (!_.isEqual(now[attr], val) || (options.unset && _.has(now, attr))) {
delete escaped[attr];
(options.silent ? this._silent : changes)[attr] = true;
}
// Update or delete the current value.
options.unset ? delete now[attr] : now[attr] = val;
// If the new and previous value differ, record the change. If not,
// then remove changes for this attribute.
if (!_.isEqual(prev[attr], val) || (_.has(now, attr) != _.has(prev, attr))) {
this.changed[attr] = val;
if (!options.silent) this._pending[attr] = true;
} else {
delete this.changed[attr];
delete this._pending[attr];
}
}
// Fire the `"change"` events.
if (!options.silent) this.change(options);
return this;
},
// Remove an attribute from the model, firing `"change"` unless you choose
// to silence it. `unset` is a noop if the attribute doesn't exist.
unset: function(attr, options) {
(options || (options = {})).unset = true;
return this.set(attr, null, options);
},
// Clear all attributes on the model, firing `"change"` unless you choose
// to silence it.
clear: function(options) {
(options || (options = {})).unset = true;
return this.set(_.clone(this.attributes), options);
},
// Fetch the model from the server. If the server's representation of the
// model differs from its current attributes, they will be overriden,
// triggering a `"change"` event.
fetch: function(options) {
options = options ? _.clone(options) : {};
var model = this;
var success = options.success;
options.success = function(resp, status, xhr) {
if (!model.set(model.parse(resp, xhr), options)) return false;
if (success) success(model, resp);
};
options.error = Backbone.wrapError(options.error, model, options);
return (this.sync || Backbone.sync).call(this, 'read', this, options);
},
// Set a hash of model attributes, and sync the model to the server.
// If the server returns an attributes hash that differs, the model's
// state will be `set` again.
save: function(key, value, options) {
var attrs, current;
// Handle both `("key", value)` and `({key: value})` -style calls.
if (_.isObject(key) || key == null) {
attrs = key;
options = value;
} else {
attrs = {};
attrs[key] = value;
}
options = options ? _.clone(options) : {};
// If we're "wait"-ing to set changed attributes, validate early.
if (options.wait) {
if (!this._validate(attrs, options)) return false;
current = _.clone(this.attributes);
}
// Regular saves `set` attributes before persisting to the server.
var silentOptions = _.extend({}, options, {silent: true});
if (attrs && !this.set(attrs, options.wait ? silentOptions : options)) {
return false;
}
// After a successful server-side save, the client is (optionally)
// updated with the server-side state.
var model = this;
var success = options.success;
options.success = function(resp, status, xhr) {
var serverAttrs = model.parse(resp, xhr);
if (options.wait) {
delete options.wait;
serverAttrs = _.extend(attrs || {}, serverAttrs);
}
if (!model.set(serverAttrs, options)) return false;
if (success) {
success(model, resp);
} else {
model.trigger('sync', model, resp, options);
}
};
// Finish configuring and sending the Ajax request.
options.error = Backbone.wrapError(options.error, model, options);
var method = this.isNew() ? 'create' : 'update';
var xhr = (this.sync || Backbone.sync).call(this, method, this, options);
if (options.wait) this.set(current, silentOptions);
return xhr;
},
// Destroy this model on the server if it was already persisted.
// Optimistically removes the model from its collection, if it has one.
// If `wait: true` is passed, waits for the server to respond before removal.
destroy: function(options) {
options = options ? _.clone(options) : {};
var model = this;
var success = options.success;
var triggerDestroy = function() {
model.trigger('destroy', model, model.collection, options);
};
if (this.isNew()) {
triggerDestroy();
return false;
}
options.success = function(resp) {
if (options.wait) triggerDestroy();
if (success) {
success(model, resp);
} else {
model.trigger('sync', model, resp, options);
}
};
options.error = Backbone.wrapError(options.error, model, options);
var xhr = (this.sync || Backbone.sync).call(this, 'delete', this, options);
if (!options.wait) triggerDestroy();
return xhr;
},
// Default URL for the model's representation on the server -- if you're
// using Backbone's restful methods, override this to change the endpoint
// that will be called.
url: function() {
var base = getValue(this, 'urlRoot') || getValue(this.collection, 'url') || urlError();
if (this.isNew()) return base;
return base + (base.charAt(base.length - 1) == '/' ? '' : '/') + encodeURIComponent(this.id);
},
// **parse** converts a response into the hash of attributes to be `set` on
// the model. The default implementation is just to pass the response along.
parse: function(resp, xhr) {
return resp;
},
// Create a new model with identical attributes to this one.
clone: function() {
return new this.constructor(this.attributes);
},
// A model is new if it has never been saved to the server, and lacks an id.
isNew: function() {
return this.id == null;
},
// Call this method to manually fire a `"change"` event for this model and
// a `"change:attribute"` event for each changed attribute.
// Calling this will cause all objects observing the model to update.
change: function(options) {
options || (options = {});
var changing = this._changing;
this._changing = true;
// Silent changes become pending changes.
for (var attr in this._silent) this._pending[attr] = true;
// Silent changes are triggered.
var changes = _.extend({}, options.changes, this._silent);
this._silent = {};
for (var attr in changes) {
this.trigger('change:' + attr, this, this.get(attr), options);
}
if (changing) return this;
// Continue firing `"change"` events while there are pending changes.
while (!_.isEmpty(this._pending)) {
this._pending = {};
this.trigger('change', this, options);
// Pending and silent changes still remain.
for (var attr in this.changed) {
if (this._pending[attr] || this._silent[attr]) continue;
delete this.changed[attr];
}
this._previousAttributes = _.clone(this.attributes);
}
this._changing = false;
return this;
},
// Determine if the model has changed since the last `"change"` event.
// If you specify an attribute name, determine if that attribute has changed.
hasChanged: function(attr) {
if (!arguments.length) return !_.isEmpty(this.changed);
return _.has(this.changed, attr);
},
// Return an object containing all the attributes that have changed, or
// false if there are no changed attributes. Useful for determining what
// parts of a view need to be updated and/or what attributes need to be
// persisted to the server. Unset attributes will be set to undefined.
// You can also pass an attributes object to diff against the model,
// determining if there *would be* a change.
changedAttributes: function(diff) {
if (!diff) return this.hasChanged() ? _.clone(this.changed) : false;
var val, changed = false, old = this._previousAttributes;
for (var attr in diff) {
if (_.isEqual(old[attr], (val = diff[attr]))) continue;
(changed || (changed = {}))[attr] = val;
}
return changed;
},
// Get the previous value of an attribute, recorded at the time the last
// `"change"` event was fired.
previous: function(attr) {
if (!arguments.length || !this._previousAttributes) return null;
return this._previousAttributes[attr];
},
// Get all of the attributes of the model at the time of the previous
// `"change"` event.
previousAttributes: function() {
return _.clone(this._previousAttributes);
},
// Check if the model is currently in a valid state. It's only possible to
// get into an *invalid* state if you're using silent changes.
isValid: function() {
return !this.validate(this.attributes);
},
// Run validation against the next complete set of model attributes,
// returning `true` if all is well. If a specific `error` callback has
// been passed, call that instead of firing the general `"error"` event.
_validate: function(attrs, options) {
if (options.silent || !this.validate) return true;
attrs = _.extend({}, this.attributes, attrs);
var error = this.validate(attrs, options);
if (!error) return true;
if (options && options.error) {
options.error(this, error, options);
} else {
this.trigger('error', this, error, options);
}
return false;
}
});
// Backbone.Collection
// -------------------
// Provides a standard collection class for our sets of models, ordered
// or unordered. If a `comparator` is specified, the Collection will maintain
// its models in sort order, as they're added and removed.
var Collection = Backbone.Collection = function(models, options) {
options || (options = {});
if (options.model) this.model = options.model;
if (options.comparator) this.comparator = options.comparator;
this._reset();
this.initialize.apply(this, arguments);
if (models) this.reset(models, {silent: true, parse: options.parse});
};
// Define the Collection's inheritable methods.
_.extend(Collection.prototype, Events, {
// The default model for a collection is just a **Backbone.Model**.
// This should be overridden in most cases.
model: Model,
// Initialize is an empty function by default. Override it with your own
// initialization logic.
initialize: function(){},
// The JSON representation of a Collection is an array of the
// models' attributes.
toJSON: function(options) {
return this.map(function(model){ return model.toJSON(options); });
},
// Add a model, or list of models to the set. Pass **silent** to avoid
// firing the `add` event for every new model.
add: function(models, options) {
var i, index, length, model, cid, id, cids = {}, ids = {}, dups = [];
options || (options = {});
models = _.isArray(models) ? models.slice() : [models];
// Begin by turning bare objects into model references, and preventing
// invalid models or duplicate models from being added.
for (i = 0, length = models.length; i < length; i++) {
if (!(model = models[i] = this._prepareModel(models[i], options))) {
throw new Error("Can't add an invalid model to a collection");
}
cid = model.cid;
id = model.id;
if (cids[cid] || this._byCid[cid] || ((id != null) && (ids[id] || this._byId[id]))) {
dups.push(i);
continue;
}
cids[cid] = ids[id] = model;
}
// Remove duplicates.
i = dups.length;
while (i--) {
models.splice(dups[i], 1);
}
// Listen to added models' events, and index models for lookup by
// `id` and by `cid`.
for (i = 0, length = models.length; i < length; i++) {
(model = models[i]).on('all', this._onModelEvent, this);
this._byCid[model.cid] = model;
if (model.id != null) this._byId[model.id] = model;
}
// Insert models into the collection, re-sorting if needed, and triggering
// `add` events unless silenced.
this.length += length;
index = options.at != null ? options.at : this.models.length;
splice.apply(this.models, [index, 0].concat(models));
if (this.comparator) this.sort({silent: true});
if (options.silent) return this;
for (i = 0, length = this.models.length; i < length; i++) {
if (!cids[(model = this.models[i]).cid]) continue;
options.index = i;
model.trigger('add', model, this, options);
}
return this;
},
// Remove a model, or a list of models from the set. Pass silent to avoid
// firing the `remove` event for every model removed.
remove: function(models, options) {
var i, l, index, model;
options || (options = {});
models = _.isArray(models) ? models.slice() : [models];
for (i = 0, l = models.length; i < l; i++) {
model = this.getByCid(models[i]) || this.get(models[i]);
if (!model) continue;
delete this._byId[model.id];
delete this._byCid[model.cid];
index = this.indexOf(model);
this.models.splice(index, 1);
this.length--;
if (!options.silent) {
options.index = index;
model.trigger('remove', model, this, options);
}
this._removeReference(model);
}
return this;
},
// Add a model to the end of the collection.
push: function(model, options) {
model = this._prepareModel(model, options);
this.add(model, options);
return model;
},
// Remove a model from the end of the collection.
pop: function(options) {
var model = this.at(this.length - 1);
this.remove(model, options);
return model;
},
// Add a model to the beginning of the collection.
unshift: function(model, options) {
model = this._prepareModel(model, options);
this.add(model, _.extend({at: 0}, options));
return model;
},
// Remove a model from the beginning of the collection.
shift: function(options) {
var model = this.at(0);
this.remove(model, options);
return model;
},
// Get a model from the set by id.
get: function(id) {
if (id == null) return void 0;
return this._byId[id.id != null ? id.id : id];
},
// Get a model from the set by client id.
getByCid: function(cid) {
return cid && this._byCid[cid.cid || cid];
},
// Get the model at the given index.
at: function(index) {
return this.models[index];
},
// Return models with matching attributes. Useful for simple cases of `filter`.
where: function(attrs) {
if (_.isEmpty(attrs)) return [];
return this.filter(function(model) {
for (var key in attrs) {
if (attrs[key] !== model.get(key)) return false;
}
return true;
});
},
// Force the collection to re-sort itself. You don't need to call this under
// normal circumstances, as the set will maintain sort order as each item
// is added.
sort: function(options) {
options || (options = {});
if (!this.comparator) throw new Error('Cannot sort a set without a comparator');
var boundComparator = _.bind(this.comparator, this);
if (this.comparator.length == 1) {
this.models = this.sortBy(boundComparator);
} else {
this.models.sort(boundComparator);
}
if (!options.silent) this.trigger('reset', this, options);
return this;
},
// Pluck an attribute from each model in the collection.
pluck: function(attr) {
return _.map(this.models, function(model){ return model.get(attr); });
},
// When you have more items than you want to add or remove individually,
// you can reset the entire set with a new list of models, without firing
// any `add` or `remove` events. Fires `reset` when finished.
reset: function(models, options) {
models || (models = []);
options || (options = {});
for (var i = 0, l = this.models.length; i < l; i++) {
this._removeReference(this.models[i]);
}
this._reset();
this.add(models, _.extend({silent: true}, options));
if (!options.silent) this.trigger('reset', this, options);
return this;
},
// Fetch the default set of models for this collection, resetting the
// collection when they arrive. If `add: true` is passed, appends the
// models to the collection instead of resetting.
fetch: function(options) {
options = options ? _.clone(options) : {};
if (options.parse === undefined) options.parse = true;
var collection = this;
var success = options.success;
options.success = function(resp, status, xhr) {
collection[options.add ? 'add' : 'reset'](collection.parse(resp, xhr), options);
if (success) success(collection, resp);
};
options.error = Backbone.wrapError(options.error, collection, options);
return (this.sync || Backbone.sync).call(this, 'read', this, options);
},
// Create a new instance of a model in this collection. Add the model to the
// collection immediately, unless `wait: true` is passed, in which case we
// wait for the server to agree.
create: function(model, options) {
var coll = this;
options = options ? _.clone(options) : {};
model = this._prepareModel(model, options);
if (!model) return false;
if (!options.wait) coll.add(model, options);
var success = options.success;
options.success = function(nextModel, resp, xhr) {
if (options.wait) coll.add(nextModel, options);
if (success) {
success(nextModel, resp);
} else {
nextModel.trigger('sync', model, resp, options);
}
};
model.save(null, options);
return model;
},
// **parse** converts a response into a list of models to be added to the
// collection. The default implementation is just to pass it through.
parse: function(resp, xhr) {
return resp;
},
// Proxy to _'s chain. Can't be proxied the same way the rest of the
// underscore methods are proxied because it relies on the underscore
// constructor.
chain: function () {
return _(this.models).chain();
},
// Reset all internal state. Called when the collection is reset.
_reset: function(options) {
this.length = 0;
this.models = [];
this._byId = {};
this._byCid = {};
},
// Prepare a model or hash of attributes to be added to this collection.
_prepareModel: function(model, options) {
options || (options = {});
if (!(model instanceof Model)) {
var attrs = model;
options.collection = this;
model = new this.model(attrs, options);
if (!model._validate(model.attributes, options)) model = false;
} else if (!model.collection) {
model.collection = this;
}
return model;
},
// Internal method to remove a model's ties to a collection.
_removeReference: function(model) {
if (this == model.collection) {
delete model.collection;
}
model.off('all', this._onModelEvent, this);
},
// Internal method called every time a model in the set fires an event.
// Sets need to update their indexes when models change ids. All other
// events simply proxy through. "add" and "remove" events that originate
// in other collections are ignored.
_onModelEvent: function(event, model, collection, options) {
if ((event == 'add' || event == 'remove') && collection != this) return;
if (event == 'destroy') {
this.remove(model, options);
}
if (model && event === 'change:' + model.idAttribute) {
delete this._byId[model.previous(model.idAttribute)];
this._byId[model.id] = model;
}
this.trigger.apply(this, arguments);
}
});
// Underscore methods that we want to implement on the Collection.
var methods = ['forEach', 'each', 'map', 'reduce', 'reduceRight', 'find',
'detect', 'filter', 'select', 'reject', 'every', 'all', 'some', 'any',
'include', 'contains', 'invoke', 'max', 'min', 'sortBy', 'sortedIndex',
'toArray', 'size', 'first', 'initial', 'rest', 'last', 'without', 'indexOf',
'shuffle', 'lastIndexOf', 'isEmpty', 'groupBy'];
// Mix in each Underscore method as a proxy to `Collection#models`.
_.each(methods, function(method) {
Collection.prototype[method] = function() {
return _[method].apply(_, [this.models].concat(_.toArray(arguments)));
};
});
// Backbone.Router
// -------------------
// Routers map faux-URLs to actions, and fire events when routes are
// matched. Creating a new one sets its `routes` hash, if not set statically.
var Router = Backbone.Router = function(options) {
options || (options = {});
if (options.routes) this.routes = options.routes;
this._bindRoutes();
this.initialize.apply(this, arguments);
};
// Cached regular expressions for matching named param parts and splatted
// parts of route strings.
var namedParam = /:\w+/g;
var splatParam = /\*\w+/g;
var escapeRegExp = /[-[\]{}()+?.,\\^$|#\s]/g;
// Set up all inheritable **Backbone.Router** properties and methods.
_.extend(Router.prototype, Events, {
// Initialize is an empty function by default. Override it with your own
// initialization logic.
initialize: function(){},
// Manually bind a single named route to a callback. For example:
//
// this.route('search/:query/p:num', 'search', function(query, num) {
// ...
// });
//
route: function(route, name, callback) {
Backbone.history || (Backbone.history = new History);
if (!_.isRegExp(route)) route = this._routeToRegExp(route);
if (!callback) callback = this[name];
Backbone.history.route(route, _.bind(function(fragment) {
var args = this._extractParameters(route, fragment);
callback && callback.apply(this, args);
this.trigger.apply(this, ['route:' + name].concat(args));
Backbone.history.trigger('route', this, name, args);
}, this));
return this;
},
// Simple proxy to `Backbone.history` to save a fragment into the history.
navigate: function(fragment, options) {
Backbone.history.navigate(fragment, options);
},
// Bind all defined routes to `Backbone.history`. We have to reverse the
// order of the routes here to support behavior where the most general
// routes can be defined at the bottom of the route map.
_bindRoutes: function() {
if (!this.routes) return;
var routes = [];
for (var route in this.routes) {
routes.unshift([route, this.routes[route]]);
}
for (var i = 0, l = routes.length; i < l; i++) {
this.route(routes[i][0], routes[i][1], this[routes[i][1]]);
}
},
// Convert a route string into a regular expression, suitable for matching
// against the current location hash.
_routeToRegExp: function(route) {
route = route.replace(escapeRegExp, '\\$&')
.replace(namedParam, '([^\/]+)')
.replace(splatParam, '(.*?)');
return new RegExp('^' + route + '$');
},
// Given a route, and a URL fragment that it matches, return the array of
// extracted parameters.
_extractParameters: function(route, fragment) {
return route.exec(fragment).slice(1);
}
});
// Backbone.History
// ----------------
// Handles cross-browser history management, based on URL fragments. If the
// browser does not support `onhashchange`, falls back to polling.
var History = Backbone.History = function() {
this.handlers = [];
_.bindAll(this, 'checkUrl');
};
// Cached regex for cleaning leading hashes and slashes .
var routeStripper = /^[#\/]/;
// Cached regex for detecting MSIE.
var isExplorer = /msie [\w.]+/;
// Has the history handling already been started?
History.started = false;
// Set up all inheritable **Backbone.History** properties and methods.
_.extend(History.prototype, Events, {
// The default interval to poll for hash changes, if necessary, is
// twenty times a second.
interval: 50,
// Gets the true hash value. Cannot use location.hash directly due to bug
// in Firefox where location.hash will always be decoded.
getHash: function(windowOverride) {
var loc = windowOverride ? windowOverride.location : window.location;
var match = loc.href.match(/#(.*)$/);
return match ? match[1] : '';
},
// Get the cross-browser normalized URL fragment, either from the URL,
// the hash, or the override.
getFragment: function(fragment, forcePushState) {
if (fragment == null) {
if (this._hasPushState || forcePushState) {
fragment = window.location.pathname;
var search = window.location.search;
if (search) fragment += search;
} else {
fragment = this.getHash();
}
}
if (!fragment.indexOf(this.options.root)) fragment = fragment.substr(this.options.root.length);
return fragment.replace(routeStripper, '');
},
// Start the hash change handling, returning `true` if the current URL matches
// an existing route, and `false` otherwise.
start: function(options) {
if (History.started) throw new Error("Backbone.history has already been started");
History.started = true;
// Figure out the initial configuration. Do we need an iframe?
// Is pushState desired ... is it available?
this.options = _.extend({}, {root: '/'}, this.options, options);
this._wantsHashChange = this.options.hashChange !== false;
this._wantsPushState = !!this.options.pushState;
this._hasPushState = !!(this.options.pushState && window.history && window.history.pushState);
var fragment = this.getFragment();
var docMode = document.documentMode;
var oldIE = (isExplorer.exec(navigator.userAgent.toLowerCase()) && (!docMode || docMode <= 7));
if (oldIE) {
this.iframe = $('<iframe src="javascript:0" tabindex="-1" />').hide().appendTo('body')[0].contentWindow;
this.navigate(fragment);
}
// Depending on whether we're using pushState or hashes, and whether
// 'onhashchange' is supported, determine how we check the URL state.
if (this._hasPushState) {
$(window).bind('popstate', this.checkUrl);
} else if (this._wantsHashChange && ('onhashchange' in window) && !oldIE) {
$(window).bind('hashchange', this.checkUrl);
} else if (this._wantsHashChange) {
this._checkUrlInterval = setInterval(this.checkUrl, this.interval);
}
// Determine if we need to change the base url, for a pushState link
// opened by a non-pushState browser.
this.fragment = fragment;
var loc = window.location;
var atRoot = loc.pathname == this.options.root;
// If we've started off with a route from a `pushState`-enabled browser,
// but we're currently in a browser that doesn't support it...
if (this._wantsHashChange && this._wantsPushState && !this._hasPushState && !atRoot) {
this.fragment = this.getFragment(null, true);
window.location.replace(this.options.root + '#' + this.fragment);
// Return immediately as browser will do redirect to new url
return true;
// Or if we've started out with a hash-based route, but we're currently
// in a browser where it could be `pushState`-based instead...
} else if (this._wantsPushState && this._hasPushState && atRoot && loc.hash) {
this.fragment = this.getHash().replace(routeStripper, '');
window.history.replaceState({}, document.title, loc.protocol + '//' + loc.host + this.options.root + this.fragment);
}
if (!this.options.silent) {
return this.loadUrl();
}
},
// Disable Backbone.history, perhaps temporarily. Not useful in a real app,
// but possibly useful for unit testing Routers.
stop: function() {
$(window).unbind('popstate', this.checkUrl).unbind('hashchange', this.checkUrl);
clearInterval(this._checkUrlInterval);
History.started = false;
},
// Add a route to be tested when the fragment changes. Routes added later
// may override previous routes.
route: function(route, callback) {
this.handlers.unshift({route: route, callback: callback});
},
// Checks the current URL to see if it has changed, and if it has,
// calls `loadUrl`, normalizing across the hidden iframe.
checkUrl: function(e) {
var current = this.getFragment();
if (current == this.fragment && this.iframe) current = this.getFragment(this.getHash(this.iframe));
if (current == this.fragment) return false;
if (this.iframe) this.navigate(current);
this.loadUrl() || this.loadUrl(this.getHash());
},
// Attempt to load the current URL fragment. If a route succeeds with a
// match, returns `true`. If no defined routes matches the fragment,
// returns `false`.
loadUrl: function(fragmentOverride) {
var fragment = this.fragment = this.getFragment(fragmentOverride);
var matched = _.any(this.handlers, function(handler) {
if (handler.route.test(fragment)) {
handler.callback(fragment);
return true;
}
});
return matched;
},
// Save a fragment into the hash history, or replace the URL state if the
// 'replace' option is passed. You are responsible for properly URL-encoding
// the fragment in advance.
//
// The options object can contain `trigger: true` if you wish to have the
// route callback be fired (not usually desirable), or `replace: true`, if
// you wish to modify the current URL without adding an entry to the history.
navigate: function(fragment, options) {
if (!History.started) return false;
if (!options || options === true) options = {trigger: options};
var frag = (fragment || '').replace(routeStripper, '');
if (this.fragment == frag) return;
// If pushState is available, we use it to set the fragment as a real URL.
if (this._hasPushState) {
if (frag.indexOf(this.options.root) != 0) frag = this.options.root + frag;
this.fragment = frag;
window.history[options.replace ? 'replaceState' : 'pushState']({}, document.title, frag);
// If hash changes haven't been explicitly disabled, update the hash
// fragment to store history.
} else if (this._wantsHashChange) {
this.fragment = frag;
this._updateHash(window.location, frag, options.replace);
if (this.iframe && (frag != this.getFragment(this.getHash(this.iframe)))) {
// Opening and closing the iframe tricks IE7 and earlier to push a history entry on hash-tag change.
// When replace is true, we don't want this.
if(!options.replace) this.iframe.document.open().close();
this._updateHash(this.iframe.location, frag, options.replace);
}
// If you've told us that you explicitly don't want fallback hashchange-
// based history, then `navigate` becomes a page refresh.
} else {
window.location.assign(this.options.root + fragment);
}
if (options.trigger) this.loadUrl(fragment);
},
// Update the hash location, either replacing the current entry, or adding
// a new one to the browser history.
_updateHash: function(location, fragment, replace) {
if (replace) {
location.replace(location.toString().replace(/(javascript:|#).*$/, '') + '#' + fragment);
} else {
location.hash = fragment;
}
}
});
// Backbone.View
// -------------
// Creating a Backbone.View creates its initial element outside of the DOM,
// if an existing element is not provided...
var View = Backbone.View = function(options) {
this.cid = _.uniqueId('view');
this._configure(options || {});
this._ensureElement();
this.initialize.apply(this, arguments);
this.delegateEvents();
};
// Cached regex to split keys for `delegate`.
var delegateEventSplitter = /^(\S+)\s*(.*)$/;
// List of view options to be merged as properties.
var viewOptions = ['model', 'collection', 'el', 'id', 'attributes', 'className', 'tagName'];
// Set up all inheritable **Backbone.View** properties and methods.
_.extend(View.prototype, Events, {
// The default `tagName` of a View's element is `"div"`.
tagName: 'div',
// jQuery delegate for element lookup, scoped to DOM elements within the
// current view. This should be prefered to global lookups where possible.
$: function(selector) {
return this.$el.find(selector);
},
// Initialize is an empty function by default. Override it with your own
// initialization logic.
initialize: function(){},
// **render** is the core function that your view should override, in order
// to populate its element (`this.el`), with the appropriate HTML. The
// convention is for **render** to always return `this`.
render: function() {
return this;
},
// Remove this view from the DOM. Note that the view isn't present in the
// DOM by default, so calling this method may be a no-op.
remove: function() {
this.$el.remove();
return this;
},
// For small amounts of DOM Elements, where a full-blown template isn't
// needed, use **make** to manufacture elements, one at a time.
//
// var el = this.make('li', {'class': 'row'}, this.model.escape('title'));
//
make: function(tagName, attributes, content) {
var el = document.createElement(tagName);
if (attributes) $(el).attr(attributes);
if (content) $(el).html(content);
return el;
},
// Change the view's element (`this.el` property), including event
// re-delegation.
setElement: function(element, delegate) {
if (this.$el) this.undelegateEvents();
this.$el = (element instanceof $) ? element : $(element);
this.el = this.$el[0];
if (delegate !== false) this.delegateEvents();
return this;
},
// Set callbacks, where `this.events` is a hash of
//
// *{"event selector": "callback"}*
//
// {
// 'mousedown .title': 'edit',
// 'click .button': 'save'
// 'click .open': function(e) { ... }
// }
//
// pairs. Callbacks will be bound to the view, with `this` set properly.
// Uses event delegation for efficiency.
// Omitting the selector binds the event to `this.el`.
// This only works for delegate-able events: not `focus`, `blur`, and
// not `change`, `submit`, and `reset` in Internet Explorer.
delegateEvents: function(events) {
if (!(events || (events = getValue(this, 'events')))) return;
this.undelegateEvents();
for (var key in events) {
var method = events[key];
if (!_.isFunction(method)) method = this[events[key]];
if (!method) throw new Error('Method "' + events[key] + '" does not exist');
var match = key.match(delegateEventSplitter);
var eventName = match[1], selector = match[2];
method = _.bind(method, this);
eventName += '.delegateEvents' + this.cid;
if (selector === '') {
this.$el.bind(eventName, method);
} else {
this.$el.delegate(selector, eventName, method);
}
}
},
// Clears all callbacks previously bound to the view with `delegateEvents`.
// You usually don't need to use this, but may wish to if you have multiple
// Backbone views attached to the same DOM element.
undelegateEvents: function() {
this.$el.unbind('.delegateEvents' + this.cid);
},
// Performs the initial configuration of a View with a set of options.
// Keys with special meaning *(model, collection, id, className)*, are
// attached directly to the view.
_configure: function(options) {
if (this.options) options = _.extend({}, this.options, options);
for (var i = 0, l = viewOptions.length; i < l; i++) {
var attr = viewOptions[i];
if (options[attr]) this[attr] = options[attr];
}
this.options = options;
},
// Ensure that the View has a DOM element to render into.
// If `this.el` is a string, pass it through `$()`, take the first
// matching element, and re-assign it to `el`. Otherwise, create
// an element from the `id`, `className` and `tagName` properties.
_ensureElement: function() {
if (!this.el) {
var attrs = getValue(this, 'attributes') || {};
if (this.id) attrs.id = this.id;
if (this.className) attrs['class'] = this.className;
this.setElement(this.make(this.tagName, attrs), false);
} else {
this.setElement(this.el, false);
}
}
});
// The self-propagating extend function that Backbone classes use.
var extend = function (protoProps, classProps) {
var child = inherits(this, protoProps, classProps);
child.extend = this.extend;
return child;
};
// Set up inheritance for the model, collection, and view.
Model.extend = Collection.extend = Router.extend = View.extend = extend;
// Backbone.sync
// -------------
// Map from CRUD to HTTP for our default `Backbone.sync` implementation.
var methodMap = {
'create': 'POST',
'update': 'PUT',
'delete': 'DELETE',
'read': 'GET'
};
// Override this function to change the manner in which Backbone persists
// models to the server. You will be passed the type of request, and the
// model in question. By default, makes a RESTful Ajax request
// to the model's `url()`. Some possible customizations could be:
//
// * Use `setTimeout` to batch rapid-fire updates into a single request.
// * Send up the models as XML instead of JSON.
// * Persist models via WebSockets instead of Ajax.
//
// Turn on `Backbone.emulateHTTP` in order to send `PUT` and `DELETE` requests
// as `POST`, with a `_method` parameter containing the true HTTP method,
// as well as all requests with the body as `application/x-www-form-urlencoded`
// instead of `application/json` with the model in a param named `model`.
// Useful when interfacing with server-side languages like **PHP** that make
// it difficult to read the body of `PUT` requests.
Backbone.sync = function(method, model, options) {
var type = methodMap[method];
// Default options, unless specified.
options || (options = {});
// Default JSON-request options.
var params = {type: type, dataType: 'json'};
// Ensure that we have a URL.
if (!options.url) {
params.url = getValue(model, 'url') || urlError();
}
// Ensure that we have the appropriate request data.
if (!options.data && model && (method == 'create' || method == 'update')) {
params.contentType = 'application/json';
params.data = JSON.stringify(model.toJSON());
}
// For older servers, emulate JSON by encoding the request into an HTML-form.
if (Backbone.emulateJSON) {
params.contentType = 'application/x-www-form-urlencoded';
params.data = params.data ? {model: params.data} : {};
}
// For older servers, emulate HTTP by mimicking the HTTP method with `_method`
// And an `X-HTTP-Method-Override` header.
if (Backbone.emulateHTTP) {
if (type === 'PUT' || type === 'DELETE') {
if (Backbone.emulateJSON) params.data._method = type;
params.type = 'POST';
params.beforeSend = function(xhr) {
xhr.setRequestHeader('X-HTTP-Method-Override', type);
};
}
}
// Don't process data on a non-GET request.
if (params.type !== 'GET' && !Backbone.emulateJSON) {
params.processData = false;
}
// Make the request, allowing the user to override any Ajax options.
return $.ajax(_.extend(params, options));
};
// Wrap an optional error callback with a fallback error event.
Backbone.wrapError = function(onError, originalModel, options) {
return function(model, resp) {
resp = model === originalModel ? resp : model;
if (onError) {
onError(originalModel, resp, options);
} else {
originalModel.trigger('error', originalModel, resp, options);
}
};
};
// Helpers
// -------
// Shared empty constructor function to aid in prototype-chain creation.
var ctor = function(){};
// Helper function to correctly set up the prototype chain, for subclasses.
// Similar to `goog.inherits`, but uses a hash of prototype properties and
// class properties to be extended.
var inherits = function(parent, protoProps, staticProps) {
var child;
// The constructor function for the new subclass is either defined by you
// (the "constructor" property in your `extend` definition), or defaulted
// by us to simply call the parent's constructor.
if (protoProps && protoProps.hasOwnProperty('constructor')) {
child = protoProps.constructor;
} else {
child = function(){ parent.apply(this, arguments); };
}
// Inherit class (static) properties from parent.
_.extend(child, parent);
// Set the prototype chain to inherit from `parent`, without calling
// `parent`'s constructor function.
ctor.prototype = parent.prototype;
child.prototype = new ctor();
// Add prototype properties (instance properties) to the subclass,
// if supplied.
if (protoProps) _.extend(child.prototype, protoProps);
// Add static properties to the constructor function, if supplied.
if (staticProps) _.extend(child, staticProps);
// Correctly set child's `prototype.constructor`.
child.prototype.constructor = child;
// Set a convenience property in case the parent's prototype is needed later.
child.__super__ = parent.prototype;
return child;
};
// Helper function to get a value from a Backbone object as a property
// or as a function.
var getValue = function(object, prop) {
if (!(object && object[prop])) return null;
return _.isFunction(object[prop]) ? object[prop]() : object[prop];
};
// Throw an error when a URL is needed, and none is supplied.
var urlError = function() {
throw new Error('A "url" property or function must be specified');
};
}).call(this);
\ No newline at end of file
/**
* Backbone localStorage Adapter
* https://github.com/jeromegn/Backbone.localStorage
*/
(function() {
// A simple module to replace `Backbone.sync` with *localStorage*-based
// persistence. Models are given GUIDS, and saved into a JSON object. Simple
// as that.
// Hold reference to Underscore.js and Backbone.js in the closure in order
// to make things work even if they are removed from the global namespace
var _ = this._;
var Backbone = this.Backbone;
// Generate four random hex digits.
function S4() {
return (((1+Math.random())*0x10000)|0).toString(16).substring(1);
};
// Generate a pseudo-GUID by concatenating random hexadecimal.
function guid() {
return (S4()+S4()+"-"+S4()+"-"+S4()+"-"+S4()+"-"+S4()+S4()+S4());
};
// Our Store is represented by a single JS object in *localStorage*. Create it
// with a meaningful name, like the name you'd give a table.
// window.Store is deprectated, use Backbone.LocalStorage instead
Backbone.LocalStorage = window.Store = function(name) {
this.name = name;
var store = this.localStorage().getItem(this.name);
this.records = (store && store.split(",")) || [];
};
_.extend(Backbone.LocalStorage.prototype, {
// Save the current state of the **Store** to *localStorage*.
save: function() {
this.localStorage().setItem(this.name, this.records.join(","));
},
// Add a model, giving it a (hopefully)-unique GUID, if it doesn't already
// have an id of it's own.
create: function(model) {
if (!model.id) {
model.id = guid();
model.set(model.idAttribute, model.id);
}
this.localStorage().setItem(this.name+"-"+model.id, JSON.stringify(model));
this.records.push(model.id.toString());
this.save();
return model.toJSON();
},
// Update a model by replacing its copy in `this.data`.
update: function(model) {
this.localStorage().setItem(this.name+"-"+model.id, JSON.stringify(model));
if (!_.include(this.records, model.id.toString())) this.records.push(model.id.toString()); this.save();
return model.toJSON();
},
// Retrieve a model from `this.data` by id.
find: function(model) {
return JSON.parse(this.localStorage().getItem(this.name+"-"+model.id));
},
// Return the array of all models currently in storage.
findAll: function() {
return _(this.records).chain()
.map(function(id){return JSON.parse(this.localStorage().getItem(this.name+"-"+id));}, this)
.compact()
.value();
},
// Delete a model from `this.data`, returning it.
destroy: function(model) {
this.localStorage().removeItem(this.name+"-"+model.id);
this.records = _.reject(this.records, function(record_id){return record_id == model.id.toString();});
this.save();
return model;
},
localStorage: function() {
return localStorage;
}
});
// localSync delegate to the model or collection's
// *localStorage* property, which should be an instance of `Store`.
// window.Store.sync and Backbone.localSync is deprectated, use Backbone.LocalStorage.sync instead
Backbone.LocalStorage.sync = window.Store.sync = Backbone.localSync = function(method, model, options, error) {
var store = model.localStorage || model.collection.localStorage;
// Backwards compatibility with Backbone <= 0.3.3
if (typeof options == 'function') {
options = {
success: options,
error: error
};
}
var resp;
switch (method) {
case "read": resp = model.id != undefined ? store.find(model) : store.findAll(); break;
case "create": resp = store.create(model); break;
case "update": resp = store.update(model); break;
case "delete": resp = store.destroy(model); break;
}
if (resp) {
options.success(resp);
} else {
options.error("Record not found");
}
};
Backbone.ajaxSync = Backbone.sync;
Backbone.getSyncMethod = function(model) {
if(model.localStorage || (model.collection && model.collection.localStorage))
{
return Backbone.localSync;
}
return Backbone.ajaxSync;
};
// Override 'Backbone.sync' to default to localSync,
// the original 'Backbone.sync' is still available in 'Backbone.ajaxSync'
Backbone.sync = function(method, model, options, error) {
return Backbone.getSyncMethod(model).apply(this, [method, model, options, error]);
};
})();
###
Chaplin 0.5.0.
Chaplin may be freely distributed under the MIT license.
For all details and documentation:
http://github.com/chaplinjs/chaplin
###
'use strict'
require.define
'jquery': (require, exports, module) -> module.exports = $
'underscore': (require, exports, module) -> module.exports = _
'backbone': (require, exports, module) -> module.exports = Backbone
require.define 'chaplin/application': (exports, require, module) ->
Backbone = require 'backbone'
mediator = require 'chaplin/mediator'
Dispatcher = require 'chaplin/dispatcher'
Layout = require 'chaplin/views/layout'
Router = require 'chaplin/lib/router'
EventBroker = require 'chaplin/lib/event_broker'
# The application bootstrapper
# ----------------------------
module.exports = class Application
# Borrow the static extend method from Backbone
@extend = Backbone.Model.extend
# Mixin an EventBroker
_(@prototype).extend EventBroker
# The site title used in the document title
title: ''
# The application instantiates these three core modules
dispatcher: null
layout: null
router: null
initialize: ->
initDispatcher: (options) ->
@dispatcher = new Dispatcher options
initLayout: (options = {}) ->
options.title ?= @title
@layout = new Layout options
# Instantiate the dispatcher
# --------------------------
# Pass the function typically returned by routes.coffee
initRouter: (routes, options) ->
# Save the reference for testing introspection only.
# Modules should communicate with each other via Pub/Sub.
@router = new Router options
# Register all routes declared in routes.coffee
routes? @router.match
# After registering the routes, start Backbone.history
@router.startHistory()
# Disposal
# --------
disposed: false
dispose: ->
return if @disposed
properties = ['dispatcher', 'layout', 'router']
for prop in properties when this[prop]?
this[prop].dispose()
delete this[prop]
@disposed = true
# You’re frozen when your heart’s not open
Object.freeze? this
require.define 'chaplin/mediator': (exports, require, module) ->
_ = require 'underscore'
Backbone = require 'backbone'
support = require 'chaplin/lib/support'
utils = require 'chaplin/lib/utils'
# Mediator
# --------
# The mediator is a simple object all others modules use to communicate
# with each other. It implements the Publish/Subscribe pattern.
#
# Additionally, it holds objects which need to be shared between modules.
# In this case, a `user` property is created for getting the user object
# and a `setUser` method for setting the user.
#
# This module returns the singleton object. This is the
# application-wide mediator you might load into modules
# which need to talk to other modules using Publish/Subscribe.
# Start with a simple object
mediator = {}
# Publish / Subscribe
# -------------------
# Mixin event methods from Backbone.Events,
# create Publish/Subscribe aliases
mediator.subscribe = Backbone.Events.on
mediator.unsubscribe = Backbone.Events.off
mediator.publish = Backbone.Events.trigger
# The `on` method should not be used,
# it is kept only for purpose of compatibility with Backbone.
mediator.on = mediator.subscribe
# Initialize an empty callback list so we might seal the mediator later
mediator._callbacks = null
# Make properties readonly
utils.readonly mediator, 'subscribe', 'unsubscribe', 'publish', 'on'
# Sealing the mediator
# --------------------
# After adding all needed properties, you should seal the mediator
# using this method
mediator.seal = ->
# Prevent extensions and make all properties non-configurable
if support.propertyDescriptors and Object.seal
Object.seal mediator
# Make the method readonly
utils.readonly mediator, 'seal'
# Return our creation
module.exports = mediator
require.define 'chaplin/dispatcher': (exports, require, module) ->
_ = require 'underscore'
Backbone = require 'backbone'
utils = require 'chaplin/lib/utils'
EventBroker = require 'chaplin/lib/event_broker'
module.exports = class Dispatcher
# Borrow the static extend method from Backbone
@extend = Backbone.Model.extend
# Mixin an EventBroker
_(@prototype).extend EventBroker
# The previous controller name
previousControllerName: null
# The current controller, its name, main view and parameters
currentControllerName: null
currentController: null
currentAction: null
currentParams: null
# The current URL
url: null
constructor: ->
@initialize arguments...
initialize: (options = {}) ->
# Merge the options
@settings = _(options).defaults
controllerPath: 'controllers/'
controllerSuffix: '_controller'
# Listen to global events
@subscribeEvent 'matchRoute', @matchRoute
@subscribeEvent '!startupController', @startupController
# Controller management
# Starting and disposing controllers
# ----------------------------------
# Handler for the global matchRoute event
matchRoute: (route, params) ->
@startupController route.controller, route.action, params
# Handler for the global !startupController event
#
# The standard flow is:
#
# 1. Test if it’s a new controller/action with new params
# 1. Hide the old view
# 2. Dispose the old controller
# 3. Instantiate the new controller, call the controller action
# 4. Show the new view
#
startupController: (controllerName, action = 'index', params = {}) ->
# Set default flags
# Whether to update the URL after controller startup
# Default to true unless explicitly set to false
if params.changeURL isnt false
params.changeURL = true
# Whether to force the controller startup even
# when current and new controllers and params match
# Default to false unless explicitly set to true
if params.forceStartup isnt true
params.forceStartup = false
# Check if the desired controller is already active
isSameController =
not params.forceStartup and
@currentControllerName is controllerName and
@currentAction is action and
# Deep parameters check is not nice but the simplest way for now
(not @currentParams or _(params).isEqual(@currentParams))
# Stop if it’s the same controller/action with the same params
return if isSameController
# Fetch the new controller, then go on
handler = _(@controllerLoaded).bind(this, controllerName, action, params)
@loadController controllerName, handler
# Load the constructor for a given controller name.
# The default implementation uses require() from a AMD module loader
# like RequireJS to fetch the constructor.
loadController: (controllerName, handler) ->
controllerFileName = utils.underscorize(controllerName) + @settings.controllerSuffix
path = @settings.controllerPath + controllerFileName
if define?.amd
require [path], handler
else
handler require path
# Handler for the controller lazy-loading
controllerLoaded: (controllerName, action, params, ControllerConstructor) ->
# Shortcuts for the old controller
currentControllerName = @currentControllerName or null
currentController = @currentController or null
# Dispose the current controller
if currentController
# Notify the rest of the world beforehand
@publishEvent 'beforeControllerDispose', currentController
# Passing the params and the new controller name
currentController.dispose params, controllerName
# Initialize the new controller
# Passing the params and the old controller name
controller = new ControllerConstructor params, currentControllerName
# Call the specific controller action
# Passing the params and the old controller name
controller[action] params, currentControllerName
# Stop if the action triggered a redirect
return if controller.redirected
# Save the new controller
@previousControllerName = currentControllerName
@currentControllerName = controllerName
@currentController = controller
@currentAction = action
@currentParams = params
@adjustURL controller, params
# We're done! Spread the word!
@publishEvent 'startupController',
previousControllerName: @previousControllerName
controller: @currentController
controllerName: @currentControllerName
params: @currentParams
# Change the URL to the new controller using the router
adjustURL: (controller, params) ->
if params.path or params.path is ''
# Just use the matched path
url = params.path
else if typeof controller.historyURL is 'function'
# Use controller.historyURL to get the URL
# If the property is a function, call it
url = controller.historyURL params
else if typeof controller.historyURL is 'string'
# If the property is a string, read it
url = controller.historyURL
else
throw new Error 'Dispatcher#adjustURL: controller for ' +
"#{@currentControllerName} does not provide a historyURL"
# Tell the router to actually change the current URL
@publishEvent '!router:changeURL', url if params.changeURL
# Save the URL
@url = url
# Disposal
# --------
disposed: false
dispose: ->
return if @disposed
@unsubscribeAllEvents()
@disposed = true
# You’re frozen when your heart’s not open
Object.freeze? this
require.define 'chaplin/controllers/controller': (exports, require, module) ->
_ = require 'underscore'
Backbone = require 'backbone'
EventBroker = require 'chaplin/lib/event_broker'
module.exports = class Controller
# Borrow the static extend method from Backbone
@extend = Backbone.Model.extend
# Mixin an EventBroker
_(@prototype).extend EventBroker
view: null
currentId: null
# Internal flag which stores whether `redirectTo`
# was called in the current action
redirected: false
# You should set a `title` property and a `historyURL` property or method
# on the derived controller. Like this:
# title: 'foo'
# historyURL: 'foo'
# historyURL: ->
constructor: ->
@initialize arguments...
initialize: ->
# Empty per default
# Redirection
# -----------
redirectTo: (arg1, action, params) ->
@redirected = true
if arguments.length is 1
# URL was passed, try to route it
@publishEvent '!router:route', arg1, (routed) ->
unless routed
throw new Error 'Controller#redirectTo: no route matched'
else
# Assume controller and action names were passed
@publishEvent '!startupController', arg1, action, params
# Disposal
# --------
disposed: false
dispose: ->
return if @disposed
# Dispose and delete all members which are disposable
for own prop of this
obj = this[prop]
if obj and typeof obj.dispose is 'function'
obj.dispose()
delete this[prop]
# Unbind handlers of global events
@unsubscribeAllEvents()
# Remove properties which are not disposable
properties = ['currentId', 'redirected']
delete this[prop] for prop in properties
# Finished
@disposed = true
# You're frozen when your heart’s not open
Object.freeze? this
require.define 'chaplin/models/collection': (exports, require, module) ->
_ = require 'underscore'
Backbone = require 'backbone'
EventBroker = require 'chaplin/lib/event_broker'
Model = require 'chaplin/models/model'
# Abstract class which extends the standard Backbone collection
# in order to add some functionality
module.exports = class Collection extends Backbone.Collection
# Mixin an EventBroker
_(@prototype).extend EventBroker
# Use the Chaplin model per default, not Backbone.Model
model: Model
# Mixin a Deferred
initDeferred: ->
_(this).extend $.Deferred()
# Serializes collection
serialize: ->
for model in @models
if model instanceof Model
# Use optimized Chaplin serialization
model.serialize()
else
# Fall back to unoptimized Backbone stuff
model.toJSON()
# Adds a collection atomically, i.e. throws no event until
# all members have been added
addAtomic: (models, options = {}) ->
return unless models.length
options.silent = true
direction = if typeof options.at is 'number' then 'pop' else 'shift'
while model = models[direction]()
@add model, options
@trigger 'reset'
# Updates a collection with a list of models
# Just like the reset method, but only adds new items and
# removes items which are not in the new list.
# Fires individual `add` and `remove` event instead of one `reset`.
#
# options:
# deep: Boolean flag to specify whether existing models
# should be updated with new values
update: (models, options = {}) ->
fingerPrint = @pluck('id').join()
ids = _(models).pluck('id')
newFingerPrint = ids.join()
# Only remove if ID fingerprints differ
if newFingerPrint isnt fingerPrint
# Remove items which are not in the new list
_ids = _(ids) # Underscore wrapper
i = @models.length
while i--
model = @models[i]
unless _ids.include model.id
@remove model
# Only add/update list if ID fingerprints differ
# or update is deep (member attributes)
if newFingerPrint isnt fingerPrint or options.deep
# Add items which are not yet in the list
for model, i in models
preexistent = @get model.id
if preexistent
# Update existing model
preexistent.set model if options.deep
else
# Insert new model
@add model, at: i
return
# Disposal
# --------
disposed: false
dispose: ->
return if @disposed
# Fire an event to notify associated views
@trigger 'dispose', this
# Empty the list silently, but do not dispose all models since
# they might be referenced elsewhere
@reset [], silent: true
# Unbind all global event handlers
@unsubscribeAllEvents()
# Remove all event handlers on this module
@off()
# If the model is a Deferred, reject it
# This does nothing if it was resolved before
@reject?()
# Remove model constructor reference, internal model lists
# and event handlers
properties = [
'model',
'models', '_byId', '_byCid',
'_callbacks'
]
delete this[prop] for prop in properties
# Finished
@disposed = true
# You’re frozen when your heart’s not open
Object.freeze? this
require.define 'chaplin/models/model': (exports, require, module) ->
_ = require 'underscore'
Backbone = require 'backbone'
utils = require 'chaplin/lib/utils'
EventBroker = require 'chaplin/lib/event_broker'
module.exports = class Model extends Backbone.Model
# Mixin an EventBroker
_(@prototype).extend EventBroker
# Mixin a Deferred
initDeferred: ->
_(this).extend $.Deferred()
# This method is used to get the attributes for the view template
# and might be overwritten by decorators which cannot create a
# proper `attributes` getter due to ECMAScript 3 limits.
getAttributes: ->
@attributes
# Private helper function for serializing attributes recursively,
# creating objects which delegate to the original attributes
# when a property needs to be overwritten.
serializeAttributes = (model, attributes, modelStack) ->
# Create a delegator on initial call
unless modelStack
delegator = utils.beget attributes
modelStack = [model]
else
# Add model to stack
modelStack.push model
# Map model/collection to their attributes
for key, value of attributes
if value instanceof Backbone.Model
# Don’t change the original attribute, create a property
# on the delegator which shadows the original attribute
delegator ?= utils.beget attributes
delegator[key] = if value is model or value in modelStack
# Nullify circular references
null
else
# Serialize recursively
serializeAttributes(
value, value.getAttributes(), modelStack
)
else if value instanceof Backbone.Collection
delegator ?= utils.beget attributes
delegator[key] = for item in value.models
serializeAttributes(
item, item.getAttributes(), modelStack
)
# Remove model from stack
modelStack.pop()
# Return the delegator if it was created, otherwise the plain attributes
delegator or attributes
# Return an object which delegates to the attributes
# (i.e. an object which has the attributes as prototype)
# so primitive values might be added and altered safely.
# Map models to their attributes, recursively.
serialize: ->
serializeAttributes this, @getAttributes()
# Disposal
# --------
disposed: false
dispose: ->
return if @disposed
# Fire an event to notify associated collections and views
@trigger 'dispose', this
# Unbind all global event handlers
@unsubscribeAllEvents()
# Remove all event handlers on this module
@off()
# If the model is a Deferred, reject it
# This does nothing if it was resolved before
@reject?()
# Remove the collection reference, internal attribute hashes
# and event handlers
properties = [
'collection',
'attributes', 'changed'
'_escapedAttributes', '_previousAttributes',
'_silent', '_pending',
'_callbacks'
]
delete this[prop] for prop in properties
# Finished
@disposed = true
# You’re frozen when your heart’s not open
Object.freeze? this
require.define 'chaplin/views/layout': (exports, require, module) ->
$ = require 'jquery'
_ = require 'underscore'
Backbone = require 'backbone'
utils = require 'chaplin/lib/utils'
EventBroker = require 'chaplin/lib/event_broker'
module.exports = class Layout # This class does not extend View
# Borrow the static extend method from Backbone
@extend = Backbone.Model.extend
# Mixin an EventBroker
_(@prototype).extend EventBroker
# The site title used in the document title.
# This should be set in your app-specific Application class
# and passed as an option
title: ''
# An hash to register events, like in Backbone.View
# It is only meant for events that are app-wide
# independent from any view
events: {}
# Register @el, @$el and @cid for delegating events
el: document
$el: $(document)
cid: 'chaplin-layout'
constructor: ->
@initialize arguments...
initialize: (options = {}) ->
@title = options.title
@settings = _(options).defaults
titleTemplate: _.template("<%= subtitle %> \u2013 <%= title %>")
openExternalToBlank: false
routeLinks: 'a, .go-to'
skipRouting: '.noscript'
# Per default, jump to the top of the page
scrollTo: [0, 0]
@subscribeEvent 'beforeControllerDispose', @hideOldView
@subscribeEvent 'startupController', @showNewView
@subscribeEvent 'startupController', @adjustTitle
# Set the app link routing
if @settings.routeLinks
@startLinkRouting()
# Set app wide event handlers
@delegateEvents()
# Take (un)delegateEvents from Backbone
# -------------------------------------
delegateEvents: Backbone.View::delegateEvents
undelegateEvents: Backbone.View::undelegateEvents
# Controller startup and disposal
# -------------------------------
# Handler for the global beforeControllerDispose event
hideOldView: (controller) ->
# Reset the scroll position
scrollTo = @settings.scrollTo
if scrollTo
window.scrollTo scrollTo[0], scrollTo[1]
# Hide the current view
view = controller.view
if view
view.$el.css 'display', 'none'
# Handler for the global startupController event
# Show the new view
showNewView: (context) ->
view = context.controller.view
if view
view.$el.css display: 'block', opacity: 1, visibility: 'visible'
# Handler for the global startupController event
# Change the document title to match the new controller
# Get the title from the title property of the current controller
adjustTitle: (context) ->
title = @title or ''
subtitle = context.controller.title or ''
title = @settings.titleTemplate {title, subtitle}
# Internet Explorer < 9 workaround
setTimeout (-> document.title = title), 50
# Automatic routing of internal links
# -----------------------------------
startLinkRouting: ->
if @settings.routeLinks
$(document).on 'click', @settings.routeLinks, @openLink
stopLinkRouting: ->
if @settings.routeLinks
$(document).off 'click', @settings.routeLinks
# Handle all clicks on A elements and try to route them internally
openLink: (event) =>
return if utils.modifierKeyPressed(event)
el = event.currentTarget
$el = $(el)
isAnchor = el.nodeName is 'A'
# Get the href and perform checks on it
href = $el.attr('href') or $el.data('href') or null
# Basic href checks
return if href is null or href is undefined or
# Technically an empty string is a valid relative URL
# but it doesn’t make sense to route it.
href is '' or
# Exclude fragment links
href.charAt(0) is '#'
# Checks for A elements
return if isAnchor and (
# Exclude links marked as external
$el.attr('target') is '_blank' or
$el.attr('rel') is 'external' or
# Exclude links to non-HTTP ressources
el.protocol not in ['http:', 'https:', 'file:']
)
# Apply skipRouting option
skipRouting = @settings.skipRouting
type = typeof skipRouting
return if type is 'function' and not skipRouting(href, el) or
type is 'string' and $el.is skipRouting
# Handle external links
internal = not isAnchor or el.hostname in [location.hostname, '']
unless internal
if @settings.openExternalToBlank
# Open external links normally in a new tab
event.preventDefault()
window.open el.href
return
if isAnchor
# Get the path with query string
path = el.pathname + el.search
# Leading slash for IE8
path = "/#{path}" if path.charAt(0) isnt '/'
else
path = href
# Pass to the router, try to route the path internally
@publishEvent '!router:route', path, (routed) ->
# Prevent default handling if the URL could be routed
if routed
event.preventDefault()
else unless isAnchor
location.href = path
return
return
# Disposal
# --------
disposed: false
dispose: ->
return if @disposed
@stopLinkRouting()
@unsubscribeAllEvents()
@undelegateEvents()
delete @title
@disposed = true
# You’re frozen when your heart’s not open
Object.freeze? this
require.define 'chaplin/views/view': (exports, require, module) ->
$ = require 'jquery'
_ = require 'underscore'
Backbone = require 'backbone'
utils = require 'chaplin/lib/utils'
EventBroker = require 'chaplin/lib/event_broker'
Model = require 'chaplin/models/model'
Collection = require 'chaplin/models/collection'
module.exports = class View extends Backbone.View
# Mixin an EventBroker
_(@prototype).extend EventBroker
# Automatic rendering
# -------------------
# Flag whether to render the view automatically on initialization.
# As an alternative you might pass a `render` option to the constructor.
autoRender: false
# Automatic inserting into DOM
# ----------------------------
# View container element
# Set this property in a derived class to specify the container element.
# Normally this is a selector string but it might also be an element or
# jQuery object.
# The view is automatically inserted into the container when it’s rendered.
# As an alternative you might pass a `container` option to the constructor.
container: null
# Method which is used for adding the view to the DOM
# Like jQuery’s `html`, `prepend`, `append`, `after`, `before` etc.
containerMethod: 'append'
# Subviews
# --------
# List of subviews
subviews: null
subviewsByName: null
# Method wrapping to enable `afterRender` and `afterInitialize`
# -------------------------------------------------------------
# Wrap a method in order to call the corresponding
# `after-` method automatically
wrapMethod: (name) ->
instance = this
# Enclose the original function
func = instance[name]
# Set a flag
instance["#{name}IsWrapped"] = true
# Create the wrapper method
instance[name] = ->
# Stop if the view was already disposed
return false if @disposed
# Call the original method
func.apply instance, arguments
# Call the corresponding `after-` method
instance["after#{utils.upcase(name)}"] arguments...
# Return the view
instance
constructor: ->
# Wrap `initialize` so `afterInitialize` is called afterwards
# Only wrap if there is an overring method, otherwise we
# can call the `after-` method directly
unless @initialize is View::initialize
@wrapMethod 'initialize'
# Wrap `render` so `afterRender` is called afterwards
unless @render is View::render
@wrapMethod 'render'
else
# Otherwise just bind the `render` method
@render = _(@render).bind this
# Call Backbone’s constructor
super
initialize: (options) ->
# No super call here, Backbone’s `initialize` is a no-op
# Copy some options to instance properties
if options
for prop in ['autoRender', 'container', 'containerMethod']
if options[prop]?
@[prop] = options[prop]
# Initialize subviews
@subviews = []
@subviewsByName = {}
# Listen for disposal of the model
# If the model is disposed, automatically dispose the associated view
if @model or @collection
@modelBind 'dispose', @dispose
# Call `afterInitialize` if `initialize` was not wrapped
unless @initializeIsWrapped
@afterInitialize()
# This method is called after a specific `initialize` of a derived class
afterInitialize: ->
# Render automatically if set by options or instance property
@render() if @autoRender
# User input event handling
# -------------------------
# Event handling using event delegation
# Register a handler for a specific event type
# For the whole view:
# delegate(eventType, handler)
# e.g.
# @delegate('click', @clicked)
# For an element in the passing a selector:
# delegate(eventType, selector, handler)
# e.g.
# @delegate('click', 'button.confirm', @confirm)
delegate: (eventType, second, third) ->
if typeof eventType isnt 'string'
throw new TypeError 'View#delegate: first argument must be a string'
if arguments.length is 2
handler = second
else if arguments.length is 3
selector = second
if typeof selector isnt 'string'
throw new TypeError 'View#delegate: ' +
'second argument must be a string'
handler = third
else
throw new TypeError 'View#delegate: ' +
'only two or three arguments are allowed'
if typeof handler isnt 'function'
throw new TypeError 'View#delegate: ' +
'handler argument must be function'
# Add an event namespace
list = ("#{event}.delegate#{@cid}" for event in eventType.split(' '))
events = list.join(' ')
# Bind the handler to the view
handler = _(handler).bind(this)
if selector
# Register handler
@$el.on events, selector, handler
else
# Register handler
@$el.on events, handler
# Return the bound handler
handler
# Remove all handlers registered with @delegate
undelegate: ->
@$el.unbind ".delegate#{@cid}"
# Model binding
# The following implementation resembles EventBroker
# --------------------------------------------------
# Bind to a model event
modelBind: (type, handler) ->
if typeof type isnt 'string'
throw new TypeError 'View#modelBind: ' +
'type must be a string'
if typeof handler isnt 'function'
throw new TypeError 'View#modelBind: ' +
'handler argument must be function'
# Get model/collection reference
modelOrCollection = @model or @collection
unless modelOrCollection
throw new TypeError 'View#modelBind: no model or collection set'
# Ensure that a handler isn’t registered twice
modelOrCollection.off type, handler, this
# Register model handler, force context to the view
modelOrCollection.on type, handler, this
# Unbind from a model event
modelUnbind: (type, handler) ->
if typeof type isnt 'string'
throw new TypeError 'View#modelUnbind: ' +
'type argument must be a string'
if typeof handler isnt 'function'
throw new TypeError 'View#modelUnbind: ' +
'handler argument must be a function'
# Get model/collection reference
modelOrCollection = @model or @collection
return unless modelOrCollection
# Remove model handler
modelOrCollection.off type, handler
# Unbind all recorded model event handlers
modelUnbindAll: ->
# Get model/collection reference
modelOrCollection = @model or @collection
return unless modelOrCollection
# Remove all handlers with a context of this view
modelOrCollection.off null, null, this
# Setup a simple one-way model-view binding
# Pass changed attribute values to specific elements in the view
# For form controls, the value is changed, otherwise the element
# text content is set to the model attribute value.
# Example: @pass 'attribute', '.selector'
pass: (attribute, selector) ->
@modelBind "change:#{attribute}", (model, value) =>
$el = @$(selector)
if $el.is('input, textarea, select, button')
$el.val value
else
$el.text value
# Subviews
# --------
# Getting or adding a subview
subview: (name, view) ->
if name and view
# Add the subview, ensure it’s unique
@removeSubview name
@subviews.push view
@subviewsByName[name] = view
view
else if name
# Get and return the subview by the given name
@subviewsByName[name]
# Removing a subview
removeSubview: (nameOrView) ->
return unless nameOrView
if typeof nameOrView is 'string'
# Name given, search for a subview by name
name = nameOrView
view = @subviewsByName[name]
else
# View instance given, search for the corresponding name
view = nameOrView
for otherName, otherView of @subviewsByName
if view is otherView
name = otherName
break
# Break if no view and name were found
return unless name and view and view.dispose
# Dispose the view
view.dispose()
# Remove the subview from the lists
index = _(@subviews).indexOf(view)
if index > -1
@subviews.splice index, 1
delete @subviewsByName[name]
# Rendering
# ---------
# Get the model/collection data for the templating function
# Uses optimized Chaplin serialization if available.
getTemplateData: ->
if @model
templateData = if @model instanceof Model
@model.serialize()
else
utils.beget @model.attributes
else if @collection
# Collection: Serialize all models
if @collection instanceof Collection
items = @collection.serialize()
else
items = []
for model in @collection.models
items.push utils.beget(model.attributes)
templateData = {items}
else
# Empty object
templateData = {}
modelOrCollection = @model or @collection
if modelOrCollection
# If the model/collection is a Deferred, add a `resolved` flag,
# but only if it’s not present yet
if typeof modelOrCollection.state is 'function' and
not ('resolved' of templateData)
templateData.resolved = modelOrCollection.state() is 'resolved'
# If the model/collection is a SyncMachine, add a `synced` flag,
# but only if it’s not present yet
if typeof modelOrCollection.isSynced is 'function' and
not ('synced' of templateData)
templateData.synced = modelOrCollection.isSynced()
templateData
# Returns the compiled template function
getTemplateFunction: ->
# Chaplin doesn’t define how you load and compile templates in order to
# render views. The example application uses Handlebars and RequireJS
# to load and compile templates on the client side. See the derived
# View class in the example application:
# https://github.com/chaplinjs/facebook-example/blob/master/coffee/views/base/view.coffee
#
# If you precompile templates to JavaScript functions on the server,
# you might just return a reference to that function.
# Several precompilers create a global `JST` hash which stores the
# template functions. You can get the function by the template name:
# JST[@templateName]
throw new Error 'View#getTemplateFunction must be overridden'
# Main render function
# This method is bound to the instance in the constructor (see above)
render: ->
# Do not render if the object was disposed
# (render might be called as an event handler which wasn’t
# removed correctly)
return false if @disposed
templateFunc = @getTemplateFunction()
if typeof templateFunc is 'function'
# Call the template function passing the template data
html = templateFunc @getTemplateData()
# Replace HTML
# ------------
# This is a workaround for an apparent issue with jQuery 1.7’s
# innerShiv feature. Using @$el.html(html) caused issues with
# HTML5-only tags in IE7 and IE8.
@$el.empty().append html
# Call `afterRender` if `render` was not wrapped
@afterRender() unless @renderIsWrapped
# Return the view
this
# This method is called after a specific `render` of a derived class
afterRender: ->
# Automatically append to DOM if the container element is set
if @container
# Append the view to the DOM
$(@container)[@containerMethod] @el
# Trigger an event
@trigger 'addedToDOM'
# Disposal
# --------
disposed: false
dispose: ->
return if @disposed
# Dispose subviews
subview.dispose() for subview in @subviews
# Unbind handlers of global events
@unsubscribeAllEvents()
# Unbind all model handlers
@modelUnbindAll()
# Remove all event handlers on this module
@off()
# Remove the topmost element from DOM. This also removes all event
# handlers from the element and all its children.
@$el.remove()
# Remove element references, options,
# model/collection references and subview lists
properties = [
'el', '$el',
'options', 'model', 'collection',
'subviews', 'subviewsByName',
'_callbacks'
]
delete this[prop] for prop in properties
# Finished
@disposed = true
# You’re frozen when your heart’s not open
Object.freeze? this
require.define 'chaplin/views/collection_view': (exports, require, module) ->
$ = require 'jquery'
_ = require 'underscore'
View = require 'chaplin/views/view'
# General class for rendering Collections.
# Derive this class and declare at least `itemView` or override
# `getView`. `getView` gets an item model and should instantiate
# and return a corresponding item view.
module.exports = class CollectionView extends View
# Configuration options
# ---------------------
# These options may be overwritten in derived classes.
# A class of item in collection.
# This property has to be overridden by a derived class.
itemView: null
# Automatic rendering
# Per default, render the view itself and all items on creation
autoRender: true
renderItems: true
# Animation
# When new items are added, their views are faded in.
# Animation duration in milliseconds (set to 0 to disable fade in)
animationDuration: 500
# By default, fading in is done by javascript function which can be
# slow on mobile devices. CSS animations are faster,
# but require user’s manual definitions.
# CSS classes used are: animated-item-view, animated-item-view-end.
useCssAnimation: false
# Selectors and Elements
# A collection view may have a template and use one of its child elements
# as the container of the item views. If you specify `listSelector`, the
# item views will be appended to this element. If empty, $el is used.
listSelector: null
# The actual element which is fetched using `listSelector`
$list: null
# Selector for a fallback element which is shown if the collection is empty.
fallbackSelector: null
# The actual element which is fetched using `fallbackSelector`
$fallback: null
# Selector for a loading indicator element which is shown
# while the collection is syncing.
loadingSelector: null
# The actual element which is fetched using `loadingSelector`
$loading: null
# Selector which identifies child elements belonging to collection
# If empty, all children of $list are considered
itemSelector: null
# Filtering
# The filter function, if any
filterer: null
# A function that will be executed after each filter.
# Hides excluded items by default.
filterCallback: (view, included) ->
display = if included then '' else 'none'
view.$el.stop(true, true).css('display', display)
# View lists
# Track a list of the visible views
visibleItems: null
# Initialization
# --------------
initialize: (options = {}) ->
super
# Initialize list for visible items
@visibleItems = []
# Start observing the collection
@addCollectionListeners()
# Apply options
@renderItems = options.renderItems if options.renderItems?
@itemView = options.itemView if options.itemView?
@filter options.filterer if options.filterer?
# Binding of collection listeners
addCollectionListeners: ->
@modelBind 'add', @itemAdded
@modelBind 'remove', @itemRemoved
@modelBind 'reset', @itemsResetted
# Rendering
# ---------
# In contrast to normal views, a template is not mandatory
# for CollectionViews. Provide an empty `getTemplateFunction`.
getTemplateFunction: ->
# Main render method (should be called only once)
render: ->
super
# Set the $list property with the actual list container
@$list = if @listSelector then @$(@listSelector) else @$el
@initFallback()
@initLoadingIndicator()
# Render all items
@renderAllItems() if @renderItems
# Adding / Removing
# -----------------
# When an item is added, create a new view and insert it
itemAdded: (item, collection, options = {}) =>
@renderAndInsertItem item, options.index
# When an item is removed, remove the corresponding view from DOM and caches
itemRemoved: (item) =>
@removeViewForItem item
# When all items are resetted, render all anew
itemsResetted: =>
@renderAllItems()
# Fallback message when the collection is empty
# ---------------------------------------------
initFallback: ->
return unless @fallbackSelector
# Set the $fallback property
@$fallback = @$(@fallbackSelector)
# Listen for visible items changes
@on 'visibilityChange', @showHideFallback
# Listen for sync events on the collection
@modelBind 'syncStateChange', @showHideFallback
# Set visibility initially
@showHideFallback()
# Show fallback if no item is visible and the collection is synced
showHideFallback: =>
visible = @visibleItems.length is 0 and (
if typeof @collection.isSynced is 'function'
# Collection is a SyncMachine
@collection.isSynced()
else
# Assume it is synced
true
)
@$fallback.css 'display', if visible then 'block' else 'none'
# Loading indicator
# -----------------
initLoadingIndicator: ->
# The loading indicator only works for Collections
# which are SyncMachines.
return unless @loadingSelector and
typeof @collection.isSyncing is 'function'
# Set the $loading property
@$loading = @$(@loadingSelector)
# Listen for sync events on the collection
@modelBind 'syncStateChange', @showHideLoadingIndicator
# Set visibility initially
@showHideLoadingIndicator()
showHideLoadingIndicator: ->
# Only show the loading indicator if the collection is empty.
# Otherwise loading more items in order to append them would
# show the loading indicator. If you want the indicator to
# show up in this case, you need to overwrite this method to
# disable the check.
visible = @collection.length is 0 and @collection.isSyncing()
@$loading.css 'display', if visible then 'block' else 'none'
# Filtering
# ---------
# Filters only child item views from all current subviews.
getItemViews: ->
itemViews = {}
for name, view of @subviewsByName when name.slice(0, 9) is 'itemView:'
itemViews[name.slice(9)] = view
itemViews
# Applies a filter to the collection view.
# Expects an iterator function as first parameter
# which need to return true or false.
# Optional filter callback which is called to
# show/hide the view or mark it otherwise as filtered.
filter: (filterer, filterCallback) ->
# Save the filterer and filterCallback functions
@filterer = filterer
@filterCallback = filterCallback if filterCallback
filterCallback ?= @filterCallback
# Show/hide existing views
unless _(@getItemViews()).isEmpty()
for item, index in @collection.models
# Apply filter to the item
included = if typeof filterer is 'function'
filterer item, index
else
true
# Show/hide the view accordingly
view = @subview "itemView:#{item.cid}"
# A view has not been created for this item yet
unless view
throw new Error 'CollectionView#filter: ' +
"no view found for #{item.cid}"
# Show/hide or mark the view accordingly
@filterCallback view, included
# Update visibleItems list, but do not trigger an event immediately
@updateVisibleItems view.model, included, false
# Trigger a combined `visibilityChange` event
@trigger 'visibilityChange', @visibleItems
# Item view rendering
# -------------------
# Render and insert all items
renderAllItems: =>
items = @collection.models
# Reset visible items
@visibleItems = []
# Collect remaining views
remainingViewsByCid = {}
for item in items
view = @subview "itemView:#{item.cid}"
if view
# View remains
remainingViewsByCid[item.cid] = view
# Remove old views of items not longer in the list
for own cid, view of @getItemViews() when cid not of remainingViewsByCid
# Remove the view
@removeSubview "itemView:#{cid}"
# Re-insert remaining items; render and insert new items
for item, index in items
# Check if view was already created
view = @subview "itemView:#{item.cid}"
if view
# Re-insert the view
@insertView item, view, index, false
else
# Create a new view, render and insert it
@renderAndInsertItem item, index
# If no view was created, trigger `visibilityChange` event manually
unless items.length
@trigger 'visibilityChange', @visibleItems
# Render the view for an item
renderAndInsertItem: (item, index) ->
view = @renderItem item
@insertView item, view, index
# Instantiate and render an item using the `viewsByCid` hash as a cache
renderItem: (item) ->
# Get the existing view
view = @subview "itemView:#{item.cid}"
# Instantiate a new view if necessary
unless view
view = @getView item
# Save the view in the subviews
@subview "itemView:#{item.cid}", view
# Render in any case
view.render()
view
# Returns an instance of the view class. Override this
# method to use several item view constructors depending
# on the model type or data.
getView: (model) ->
if @itemView
new @itemView {model}
else
throw new Error 'The CollectionView#itemView property ' +
'must be defined or the getView() must be overridden.'
# Inserts a view into the list at the proper position
insertView: (item, view, index = null, enableAnimation = true) ->
# Get the insertion offset
position = if typeof index is 'number'
index
else
@collection.indexOf item
# Is the item included in the filter?
included = if typeof @filterer is 'function'
@filterer item, position
else
true
# Get the view’s top element
viewEl = view.el
$viewEl = view.$el
if included
# Make view transparent if animation is enabled
if enableAnimation
if @useCssAnimation
$viewEl.addClass 'animated-item-view'
else
$viewEl.css 'opacity', 0
else
# Hide the view if it’s filtered
@filterCallback view, included
# Insert the view into the list
$list = @$list
# Get the children which originate from item views
children = if @itemSelector
$list.children @itemSelector
else
$list.children()
# Check if it needs to be inserted
unless children.get(position) is viewEl
length = children.length
if length is 0 or position is length
# Insert at the end
$list.append viewEl
else
# Insert at the right position
if position is 0
$next = children.eq position
$next.before viewEl
else
$previous = children.eq position - 1
$previous.after viewEl
# Tell the view that it was added to the DOM
view.trigger 'addedToDOM'
# Update the list of visible items, trigger a `visibilityChange` event
@updateVisibleItems item, included
# Fade the view in if it was made transparent before
if enableAnimation and included
if @useCssAnimation
# Wait for DOM state change.
setTimeout =>
$viewEl.addClass 'animated-item-view-end'
, 0
else
$viewEl.animate {opacity: 1}, @animationDuration
return
# Remove the view for an item
removeViewForItem: (item) ->
# Remove item from visibleItems list, trigger a `visibilityChange` event
@updateVisibleItems item, false
@removeSubview "itemView:#{item.cid}"
# List of visible items
# ---------------------
# Update visibleItems list and trigger a `visibilityChanged` event
# if an item changed its visibility
updateVisibleItems: (item, includedInFilter, triggerEvent = true) ->
visibilityChanged = false
visibleItemsIndex = _(@visibleItems).indexOf item
includedInVisibleItems = visibleItemsIndex > -1
if includedInFilter and not includedInVisibleItems
# Add item to the visible items list
@visibleItems.push item
visibilityChanged = true
else if not includedInFilter and includedInVisibleItems
# Remove item from the visible items list
@visibleItems.splice visibleItemsIndex, 1
visibilityChanged = true
# Trigger a `visibilityChange` event if the visible items changed
if visibilityChanged and triggerEvent
@trigger 'visibilityChange', @visibleItems
visibilityChanged
# Disposal
# --------
dispose: ->
return if @disposed
# Remove jQuery objects, item view cache and visible items list
properties = [
'$list', '$fallback', '$loading',
'visibleItems'
]
delete this[prop] for prop in properties
# Self-disposal
super
require.define 'chaplin/lib/route': (exports, require, module) ->
_ = require 'underscore'
Backbone = require 'backbone'
EventBroker = require 'chaplin/lib/event_broker'
Controller = require 'chaplin/controllers/controller'
module.exports = class Route
# Borrow the static extend method from Backbone
@extend = Backbone.Model.extend
# Mixin an EventBroker
_(@prototype).extend EventBroker
reservedParams = ['path', 'changeURL']
# Taken from Backbone.Router
escapeRegExp = /[-[\]{}()+?.,\\^$|#\s]/g
queryStringFieldSeparator = '&'
queryStringValueSeparator = '='
# Create a route for a URL pattern and a controller action
# e.g. new Route '/users/:id', 'users#show'
constructor: (pattern, target, @options = {}) ->
# Save the raw pattern
@pattern = pattern
# Separate target into controller and controller action
[@controller, @action] = target.split('#')
# Check if the action is a reserved name
if _(Controller.prototype).has @action
throw new Error 'Route: You should not use existing controller properties as action names'
@createRegExp()
createRegExp: ->
if _.isRegExp(@pattern)
@regExp = @pattern
return
pattern = @pattern
# Escape magic characters
.replace(escapeRegExp, '\\$&')
# Replace named parameters, collecting their names
.replace(/(?::|\*)(\w+)/g, @addParamName)
# Create the actual regular expression
# Match until the end of the URL or the begin of query string
@regExp = ///^#{pattern}(?=\?|$)///
addParamName: (match, paramName) =>
@paramNames ?= []
# Test if parameter name is reserved
if _(reservedParams).include(paramName)
throw new Error "Route#addParamName: parameter name #{paramName} is reserved"
# Save parameter name
@paramNames.push paramName
# Replace with a character class
if match.charAt(0) is ':'
# Regexp for :foo
'([^\/\?]+)'
else
# Regexp for *foo
'(.*?)'
# Test if the route matches to a path (called by Backbone.History#loadUrl)
test: (path) ->
# Test the main RegExp
matched = @regExp.test path
return false unless matched
# Apply the parameter constraints
constraints = @options.constraints
if constraints
params = @extractParams path
for own name, constraint of constraints
unless constraint.test(params[name])
return false
return true
# The handler which is called by Backbone.History when the route matched.
# It is also called by Router#follow which might pass options
handler: (path, options) =>
# Build params hash
params = @buildParams path, options
# Publish a global matchRoute event passing the route and the params
@publishEvent 'matchRoute', this, params
# Create a proper Rails-like params hash, not an array like Backbone
# `matches` and `additionalParams` arguments are optional
buildParams: (path, options) ->
params = {}
# Add params from query string
queryParams = @extractQueryParams path
_(params).extend queryParams
# Add named params from pattern matches
patternParams = @extractParams path
_(params).extend patternParams
# Add additional params from options
# (they might overwrite params extracted from URL)
_(params).extend @options.params
# Add a `changeURL` param whether to change the URL after routing
# Defaults to false unless explicitly set in options
params.changeURL = Boolean(options and options.changeURL)
# Add a `path param with the whole path match
params.path = path
params
# Extract named parameters from the URL path
extractParams: (path) ->
params = {}
# Apply the regular expression
matches = @regExp.exec path
# Fill the hash using the paramNames and the matches
for match, index in matches.slice(1)
paramName = if @paramNames then @paramNames[index] else index
params[paramName] = match
params
# Extract parameters from the query string
extractQueryParams: (path) ->
params = {}
regExp = /\?(.+?)(?=#|$)/
matches = regExp.exec path
return params unless matches
queryString = matches[1]
pairs = queryString.split queryStringFieldSeparator
for pair in pairs
continue unless pair.length
[field, value] = pair.split queryStringValueSeparator
continue unless field.length
field = decodeURIComponent field
value = decodeURIComponent value
current = params[field]
if current
# Handle multiple params with same name:
# Aggregate them in an array
if current.push
# Add the existing array
current.push value
else
# Create a new array
params[field] = [current, value]
else
params[field] = value
params
require.define 'chaplin/lib/router': (exports, require, module) ->
_ = require 'underscore'
Backbone = require 'backbone'
mediator = require 'chaplin/mediator'
EventBroker = require 'chaplin/lib/event_broker'
Route = require 'chaplin/lib/route'
# The router which is a replacement for Backbone.Router.
# Like the standard router, it creates a Backbone.History
# instance and registers routes on it.
module.exports = class Router # This class does not extend Backbone.Router
# Borrow the static extend method from Backbone
@extend = Backbone.Model.extend
# Mixin an EventBroker
_(@prototype).extend EventBroker
constructor: (@options = {}) ->
_(@options).defaults
pushState: true
@subscribeEvent '!router:route', @routeHandler
@subscribeEvent '!router:changeURL', @changeURLHandler
@createHistory()
# Create a Backbone.History instance
createHistory: ->
Backbone.history or= new Backbone.History()
startHistory: ->
# Start the Backbone.History instance to start routing
# This should be called after all routes have been registered
Backbone.history.start @options
# Stop the current Backbone.History instance from observing URL changes
stopHistory: ->
Backbone.history.stop() if Backbone.History.started
# Connect an address with a controller action
# Directly create a route on the Backbone.History instance
match: (pattern, target, options = {}) =>
# Create the route
route = new Route pattern, target, options
# Register the route at the Backbone.History instance.
# Don’t use Backbone.history.route here because it calls
# handlers.unshift, inserting the handler at the top of the list.
# Since we want routes to match in the order they were specified,
# we’re appending the route at the end.
Backbone.history.handlers.push {route, callback: route.handler}
route
# Route a given URL path manually, returns whether a route matched
# This looks quite like Backbone.History::loadUrl but it
# accepts an absolute URL with a leading slash (e.g. /foo)
# and passes a changeURL param to the callback function.
route: (path) =>
# Remove leading hash or slash
path = path.replace /^(\/#|\/)/, ''
for handler in Backbone.history.handlers
if handler.route.test(path)
handler.callback path, changeURL: true
return true
false
# Handler for the global !router:route event
routeHandler: (path, callback) ->
routed = @route path
callback? routed
# Change the current URL, add a history entry.
# Do not trigger any routes (which is Backbone’s
# default behavior, but added for clarity)
changeURL: (url) ->
Backbone.history.navigate url, trigger: false
# Handler for the global !router:changeURL event
changeURLHandler: (url) ->
@changeURL url
# Disposal
# --------
disposed: false
dispose: ->
return if @disposed
# Stop Backbone.History instance and remove it
@stopHistory()
delete Backbone.history
@unsubscribeAllEvents()
# Finished
@disposed = true
# You’re frozen when your heart’s not open
Object.freeze? this
require.define 'chaplin/lib/delayer': (exports, require, module) ->
# Delayer
# -------
#
# Add functionality to set unique, named timeouts and intervals
# so they can be cleared afterwards when disposing the object.
# This is especially useful in your custom View class which inherits
# from the standard Chaplin.View.
#
# Mixin this object to add the delayer capability to any object:
# _(object).extend Delayer
#
# Or to a prototype of a class:
# _(@prototype).extend Delayer
#
# In the dispose method, call `clearDelayed` to remove all pending
# timeouts and running intervals:
#
# dispose: ->
# return if @disposed
# @clearDelayed()
# super
Delayer =
setTimeout: (name, time, handler) ->
@timeouts ?= {}
@clearTimeout name
wrappedHandler = =>
delete @timeouts[name]
handler()
handle = setTimeout wrappedHandler, time
@timeouts[name] = handle
handle
clearTimeout: (name) ->
return unless @timeouts and @timeouts[name]?
clearTimeout @timeouts[name]
delete @timeouts[name]
return
clearAllTimeouts: ->
return unless @timeouts
for name, handle of @timeouts
@clearTimeout name
return
setInterval: (name, time, handler) ->
@clearInterval name
@intervals ?= {}
handle = setInterval handler, time
@intervals[name] = handle
handle
clearInterval: (name) ->
return unless @intervals and @intervals[name]
clearInterval @intervals[name]
delete @intervals[name]
return
clearAllIntervals: ->
return unless @intervals
for name, handle of @intervals
@clearInterval name
return
clearDelayed: ->
@clearAllTimeouts()
@clearAllIntervals()
return
# You’re frozen when your heart’s not open
Object.freeze? Delayer
module.exports = Delayer
require.define 'chaplin/lib/event_broker': (exports, require, module) ->
mediator = require 'chaplin/mediator'
# Add functionality to subscribe and publish to global
# Publish/Subscribe events so they can be removed afterwards
# when disposing the object.
#
# Mixin this object to add the subscriber capability to any object:
# _(object).extend EventBroker
# Or to a prototype of a class:
# _(@prototype).extend EventBroker
#
# Since Backbone 0.9.2 this abstraction just serves the purpose
# that a handler cannot be registered twice for the same event.
EventBroker =
subscribeEvent: (type, handler) ->
if typeof type isnt 'string'
throw new TypeError 'EventBroker#subscribeEvent: ' +
'type argument must be a string'
if typeof handler isnt 'function'
throw new TypeError 'EventBroker#subscribeEvent: ' +
'handler argument must be a function'
# Ensure that a handler isn’t registered twice
mediator.unsubscribe type, handler, this
# Register global handler, force context to the subscriber
mediator.subscribe type, handler, this
unsubscribeEvent: (type, handler) ->
if typeof type isnt 'string'
throw new TypeError 'EventBroker#unsubscribeEvent: ' +
'type argument must be a string'
if typeof handler isnt 'function'
throw new TypeError 'EventBroker#unsubscribeEvent: ' +
'handler argument must be a function'
# Remove global handler
mediator.unsubscribe type, handler
# Unbind all global handlers
unsubscribeAllEvents: ->
# Remove all handlers with a context of this subscriber
mediator.unsubscribe null, null, this
publishEvent: (type, args...) ->
if typeof type isnt 'string'
throw new TypeError 'EventBroker#publishEvent: ' +
'type argument must be a string'
# Publish global handler
mediator.publish type, args...
# You’re frozen when your heart’s not open
Object.freeze? EventBroker
module.exports = EventBroker
require.define 'chaplin/lib/support': (exports, require, module) ->
# Feature detection
# -----------------
support =
# Test for defineProperty support
# (IE 8 knows the method but will throw an exception)
propertyDescriptors: do ->
unless typeof Object.defineProperty is 'function' and
typeof Object.defineProperties is 'function'
return false
try
o = {}
Object.defineProperty o, 'foo', value: 'bar'
return o.foo is 'bar'
catch error
return false
module.exports = support
require.define 'chaplin/lib/sync_machine': (exports, require, module) ->
# Simple finite state machine for synchronization of models/collections
# Three states: unsynced, syncing and synced
# Several transitions between them
# Fires Backbone events on every transition
# (unsynced, syncing, synced; syncStateChange)
# Provides shortcut methods to call handlers when a given state is reached
# (named after the events above)
UNSYNCED = 'unsynced'
SYNCING = 'syncing'
SYNCED = 'synced'
STATE_CHANGE = 'syncStateChange'
SyncMachine =
_syncState: UNSYNCED
_previousSyncState: null
# Get the current state
# ---------------------
syncState: ->
@_syncState
isUnsynced: ->
@_syncState is UNSYNCED
isSynced: ->
@_syncState is SYNCED
isSyncing: ->
@_syncState is SYNCING
# Transitions
# -----------
unsync: ->
if @_syncState in [SYNCING, SYNCED]
@_previousSync = @_syncState
@_syncState = UNSYNCED
@trigger @_syncState, this, @_syncState
@trigger STATE_CHANGE, this, @_syncState
# when UNSYNCED do nothing
return
beginSync: ->
if @_syncState in [UNSYNCED, SYNCED]
@_previousSync = @_syncState
@_syncState = SYNCING
@trigger @_syncState, this, @_syncState
@trigger STATE_CHANGE, this, @_syncState
# when SYNCING do nothing
return
finishSync: ->
if @_syncState is SYNCING
@_previousSync = @_syncState
@_syncState = SYNCED
@trigger @_syncState, this, @_syncState
@trigger STATE_CHANGE, this, @_syncState
# when SYNCED, UNSYNCED do nothing
return
abortSync: ->
if @_syncState is SYNCING
@_syncState = @_previousSync
@_previousSync = @_syncState
@trigger @_syncState, this, @_syncState
@trigger STATE_CHANGE, this, @_syncState
# when UNSYNCED, SYNCED do nothing
return
# Create shortcut methods to bind a handler to a state change
# -----------------------------------------------------------
for event in [UNSYNCED, SYNCING, SYNCED, STATE_CHANGE]
do (event) ->
SyncMachine[event] = (callback, context = @) ->
@on event, callback, context
callback.call(context) if @_syncState is event
# You’re frozen when your heart’s not open
Object.freeze? SyncMachine
module.exports = SyncMachine
require.define 'chaplin/lib/utils': (exports, require, module) ->
support = require 'chaplin/lib/support'
# Utilities
# ---------
utils =
# Object Helpers
# --------------
# Prototypal delegation. Create an object which delegates
# to another object.
beget: do ->
if typeof Object.create is 'function'
Object.create
else
ctor = ->
(obj) ->
ctor:: = obj
new ctor
# Make properties readonly and not configurable
# using ECMAScript 5 property descriptors
readonly: do ->
if support.propertyDescriptors
readonlyDescriptor =
writable: false
enumerable: true
configurable: false
(obj, properties...) ->
for prop in properties
readonlyDescriptor.value = obj[prop]
Object.defineProperty obj, prop, readonlyDescriptor
true
else
->
false
# String Helpers
# --------------
# Upcase the first character
upcase: (str) ->
str.charAt(0).toUpperCase() + str.substring(1)
# underScoreHelper -> under_score_helper
underscorize: (string) ->
string.replace /[A-Z]/g, (char, index) ->
(if index isnt 0 then '_' else '') + char.toLowerCase()
# Event handling helpers
# ----------------------
# Returns whether a modifier key is pressed during a keypress or mouse click
modifierKeyPressed: (event) ->
event.shiftKey or event.altKey or event.ctrlKey or event.metaKey
# Finish
# ------
# Seal the utils object
Object.seal? utils
module.exports = utils
require.define 'chaplin': (exports, require, module) ->
Application = require 'chaplin/application'
mediator = require 'chaplin/mediator'
Dispatcher = require 'chaplin/dispatcher'
Controller = require 'chaplin/controllers/controller'
Collection = require 'chaplin/models/collection'
Model = require 'chaplin/models/model'
Layout = require 'chaplin/views/layout'
View = require 'chaplin/views/view'
CollectionView = require 'chaplin/views/collection_view'
Route = require 'chaplin/lib/route'
Router = require 'chaplin/lib/router'
Delayer = require 'chaplin/lib/delayer'
EventBroker = require 'chaplin/lib/event_broker'
support = require 'chaplin/lib/support'
SyncMachine = require 'chaplin/lib/sync_machine'
utils = require 'chaplin/lib/utils'
module.exports = {
Application,
mediator,
Dispatcher,
Controller,
Collection,
Model,
Layout,
View,
CollectionView,
Route,
Router,
Delayer,
EventBroker,
support,
SyncMachine,
utils
}
\ No newline at end of file
// Make it safe to do console.log() always.
(function (con) {
var method;
var dummy = function() {};
var methods = ('assert,count,debug,dir,dirxml,error,exception,group,' +
'groupCollapsed,groupEnd,info,log,markTimeline,profile,profileEnd,' +
'time,timeEnd,trace,warn').split(',');
while (method = methods.pop()) {
con[method] = con[method] || dummy;
}
})(window.console = window.console || {});
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment