Commit 07898f7f authored by Sindre Sorhus's avatar Sindre Sorhus

Merge pull request #319 from paulmillr/topics/chaplin

Add Chaplin application.
parents faa859eb 00ca0421
# editorconfig.org
root = true
[*.coffee]
indent_style = space
indent_size = 2
# NPM packages folder.
node_modules/
# Brunch folder for temporary files.
tmp/
# Brunch with Chaplin TODOMVC
Brunch with Chaplin is a skeleton (boilerplate) for [Brunch](http://brunch.io)
based on [Chaplin](https://github.com/chaplinjs/chaplin) framework.
The application is based on the skeleton.
## Getting started
* Install [Brunch](http://brunch.io) if you hadn’t already (`npm install -g brunch`).
* Execute `npm install` in the root directory once.
* Execute `brunch build` in the root directory to build app every time. That’s all.
* Execute `brunch watch` if you want to continiously rebuild the app
on every change. To run the app then, you will need to open `public/index.html` in your browser (assuming the root is `/todomvc/` root or so).
## Author
The stuff was made by [@paulmillr](http://paulmillr.com).
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'
# The application object
module.exports = class Application extends Chaplin.Application
# Set your application name here so the document title is set to
# “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: ->
# Use an application-specific Layout class. Currently this adds
# 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: ->
# Create a user property
mediator.user = null
# Add additional application-specific properties and methods
mediator.todos = new Todos()
mediator.todos.fetch()
# Seal the mediator
mediator.seal()
<!doctype html>
<html class="no-js" lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<title>Chaplin • TodoMVC</title>
<link rel="stylesheet" href="../../../../assets/base.css">
<!--[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>
<script>require('initialize');</script>
</head>
<body>
<section id="todoapp">
<header id="header"></header>
<section id="main"></section>
<footer id="footer"></footer>
</section>
<footer id="info">
<p>Double-click to edit a todo</p>
<p>Created by <a href="http://paulmillr.com">Paul Miller</a></p>
<p>Part of <a href="http://todomvc.com">TodoMVC</a></p>
</footer>
</body>
</html>
Chaplin = require 'chaplin'
module.exports = class Controller extends Chaplin.Controller
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'
module.exports = class IndexController extends Controller
title: 'Todo list'
list: (options) ->
@publishEvent 'todos:filter', options.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'
# Initialize the application on DOM ready event.
$ ->
app = new Application()
app.initialize()
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
# ------------------------------
# Delegate to Chaplin’s utils module
utils = Chaplin.utils.beget Chaplin.utils
# _(utils).extend
# someMethod: ->
module.exports = utils
mediator = require 'mediator'
utils = require 'chaplin/lib/utils'
# Application-specific view helpers
# ---------------------------------
# http://handlebarsjs.com/#helpers
# Conditional evaluation
# ----------------------
# Choose block by user login status
Handlebars.registerHelper 'if_logged_in', (options) ->
if mediator.user
options.fn(this)
else
options.inverse(this)
# Map helpers
# -----------
# Make 'with' behave a little more mustachey
Handlebars.registerHelper 'with', (context, options) ->
if not context or Handlebars.Utils.isEmpty context
options.inverse(this)
else
options.fn(context)
# Inverse for 'with'
Handlebars.registerHelper 'without', (context, options) ->
inverse = options.inverse
options.inverse = options.fn
options.fn = inverse
Handlebars.helpers.with.call(this, context, options)
# Evaluate block with context being current user
Handlebars.registerHelper 'with_user', (options) ->
context = mediator.user?.serialize() or {}
Handlebars.helpers.with.call(this, context, options)
module.exports = require('chaplin').mediator
Chaplin = require 'chaplin'
Model = require 'models/base/model'
module.exports = class Collection extends Chaplin.Collection
# Use the project base model per default, not Chaplin.Model
model: Model
# Mixin a synchronization state machine
# _(@prototype).extend Chaplin.SyncMachine
Chaplin = require 'chaplin'
module.exports = class Model extends Chaplin.Model
# Mixin a synchronization state machine
# _(@prototype).extend Chaplin.SyncMachine
Model = require 'models/base/model'
module.exports = class Todo extends Model
defaults:
title: ''
completed: no
initialize: ->
super
@set 'created', Date.now() if @isNew()
toggle: ->
@set completed: not @get('completed')
isVisible: ->
isCompleted = @get('completed')
Collection = require 'models/base/collection'
Todo = require 'models/todo'
module.exports = class Todos extends Collection
model: Todo
localStorage: new Store 'todos-chaplin'
allAreCompleted: ->
@getCompleted().length is @length
getCompleted: ->
@where completed: yes
getActive: ->
@where completed: no
comparator: (todo) ->
todo.get('created')
module.exports = (match) ->
match ':filterer', 'index#list'
match '', 'index#list'
Chaplin = require 'chaplin'
View = require 'views/base/view'
module.exports = class CollectionView extends Chaplin.CollectionView
# This class doesn’t inherit from the application-specific View class,
# so we need to borrow the method from the View prototype:
getTemplateFunction: View::getTemplateFunction
Chaplin = require 'chaplin'
require 'lib/view-helper' # Just load the view helpers, no return value
module.exports = class View extends Chaplin.View
# Precompiled templates function initializer.
getTemplateFunction: ->
@template
View = require 'views/base/view'
template = require 'views/templates/footer'
module.exports = class FooterView extends View
autoRender: yes
el: '#footer'
template: template
initialize: ->
super
@subscribeEvent 'todos:filter', @updateFilterer
@modelBind 'all', @renderCounter
@delegate 'click', '#clear-completed', @clearCompleted
render: =>
super
@renderCounter()
updateFilterer: (filterer) =>
filterer = '' if filterer is 'all'
@$('#filters a')
.removeClass('selected')
.filter("[href='#/#{filterer}']")
.addClass('selected')
renderCounter: =>
total = @collection.length
active = @collection.getActive().length
completed = @collection.getCompleted().length
@$('#todo-count > strong').html active
countDescription = (if active is 1 then 'item' else 'items')
@$('.todo-count-title').text countDescription
@$('#completed-count').html "(#{completed})"
@$('#clear-completed').toggle(completed > 0)
@$el.toggle(total > 0)
clearCompleted: ->
@publishEvent 'todos:clear'
View = require 'views/base/view'
template = require 'views/templates/header'
module.exports = class HeaderView extends View
autoRender: yes
el: '#header'
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 ''
Chaplin = require 'chaplin'
# Layout is the top-level application ‘view’.
module.exports = class Layout extends Chaplin.Layout
initialize: ->
super
@subscribeEvent 'todos:filter', @changeFilterer
changeFilterer: (filterer = 'all') ->
$('#todoapp').attr 'class', "filter-#{filterer}"
<span id="todo-count">
<strong></strong>
<span class="todo-count-title">items</span>
left
</span>
<ul id="filters">
<li>
<a href="#/">All</a>
</li>
<li>
<a href="#/active">Active</a>
</li>
<li>
<a href="#/completed">Completed</a>
</li>
</ul>
<button id="clear-completed">
Clear completed
<span id="completed-count"></span>
</button>
<h1>todos</h1>
<input id="new-todo" placeholder="What needs to be done?" autofocus>
<div class="view">
<input class="toggle" type="checkbox"{{#if completed}} checked{{/if}}>
<label>{{title}}</label>
<button class="destroy"></button>
</div>
<input class="edit" value="{{title}}">
<input id="toggle-all" type="checkbox">
<label for="toggle-all">Mark all as complete</label>
<ul id="todo-list"></ul>
View = require 'views/base/view'
template = require 'views/templates/todo'
module.exports = class TodoView extends View
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.
@$el.removeClass 'active completed'
className = if @model.get('completed') then 'completed' else 'active'
@$el.addClass className
destroy: =>
@model.destroy()
toggle: =>
@model.toggle().save()
edit: =>
@$el.addClass 'editing'
@$('.edit').focus()
save: (event) =>
ENTER_KEY = 13
title = $(event.currentTarget).val().trim()
return @model.destroy() unless title
return if event.type is 'keypress' and event.keyCode isnt ENTER_KEY
@model.save {title}
@$el.removeClass 'editing'
CollectionView = require 'views/base/collection-view'
template = require 'views/templates/todos'
TodoView = require 'views/todo-view'
module.exports = class TodosView extends CollectionView
el: '#main'
itemView: TodoView
listSelector: '#todo-list'
template: template
initialize: ->
super
@subscribeEvent 'todos:clear', @clear
@modelBind 'all', @renderCheckbox
@delegate 'click', '#toggle-all', @toggleCompleted
render: =>
super
@renderCheckbox()
renderCheckbox: =>
@$('#toggle-all').prop 'checked', @collection.allAreCompleted()
@$el.toggle(@collection.length isnt 0)
toggleCompleted: (event) =>
isChecked = event.currentTarget.checked
@collection.each (todo) -> todo.save completed: isChecked
clear: ->
@collection.getCompleted().forEach (model) ->
model.destroy()
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'
]
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']
templates:
joinTo: 'javascripts/app.js'
{
"author": "Paul Miller (http://paulmillr.com/)",
"name": "brunch-with-chaplin-todomvc",
"description": "Brunch with Chaplin TODOMVC app",
"version": "0.0.1",
"engines": {
"node": "0.8 || 0.9"
},
"scripts": {
"start": "brunch watch --server",
"test": "brunch test"
},
"dependencies": {
"javascript-brunch": ">= 1.0 < 1.5",
"coffee-script-brunch": ">= 1.0 < 1.5",
"css-brunch": ">= 1.0 < 1.5",
"stylus-brunch": ">= 1.0 < 1.5",
"handlebars-brunch": ">= 1.0 < 1.5",
"uglify-js-brunch": ">= 1.0 < 1.5",
"clean-css-brunch": ">= 1.0 < 1.5"
},
"devDependencies": {
"chai": "~1.2.0",
"sinon": "~1.4.2",
"sinon-chai": "~2.1.2"
}
}
<!doctype html>
<html class="no-js" lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<title>Chaplin • TodoMVC</title>
<link rel="stylesheet" href="../../../../assets/base.css">
<!--[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>
<script>require('initialize');</script>
</head>
<body>
<section id="todoapp">
<header id="header"></header>
<section id="main"></section>
<footer id="footer"></footer>
</section>
<footer id="info">
<p>Double-click to edit a todo</p>
<p>Created by <a href="http://paulmillr.com">Paul Miller</a></p>
<p>Part of <a href="http://todomvc.com">TodoMVC</a></p>
</footer>
</body>
</html>
(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.
#todoapp.filter-active #todo-list .completed {
display: none; }
#todoapp.filter-completed #todo-list .active {
display: none; }
// 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