├── .gitignore ├── Cakefile ├── README.md ├── app ├── application.coffee ├── assets │ ├── images │ │ └── destroy.png │ └── index.html ├── initialize.coffee ├── lib │ ├── router.coffee │ └── view_helper.coffee ├── models │ ├── collection.coffee │ ├── model.coffee │ ├── todo.coffee │ └── todos.coffee └── views │ ├── home_view.coffee │ ├── new_todo_view.coffee │ ├── stats_view.coffee │ ├── styles │ └── application.styl │ ├── templates │ ├── home.hbs │ ├── new_todo.hbs │ ├── stats.hbs │ ├── todo.hbs │ └── todos.hbs │ ├── todo_view.coffee │ ├── todos_view.coffee │ ├── user.coffee │ └── view.coffee ├── config.coffee ├── package.json ├── public ├── images │ └── destroy.png ├── index.html ├── javascripts │ ├── app.js │ └── vendor.js └── stylesheets │ └── app.css ├── test ├── functional │ └── app.coffee8 ├── helpers.coffee ├── index.html ├── mocha.css ├── mocha.js └── unit │ ├── collections │ └── todo_list.coffee │ ├── models │ └── todo.coffee │ ├── routers │ └── main.coffee │ └── views │ ├── home.coffee │ ├── new_todo.coffee │ ├── stats.coffee │ ├── todo.coffee │ ├── todos.coffee │ └── user.coffee └── vendor └── scripts ├── backbone-0.9.2.js ├── backbone.localStorage.js ├── console-helper.js ├── jquery-1.7.2.js └── underscore-1.3.1.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Numerous always-ignore extensions 2 | *.diff 3 | *.err 4 | *.orig 5 | *.log 6 | *.rej 7 | *.swo 8 | *.swp 9 | *.vi 10 | *~ 11 | *.sass-cache 12 | 13 | # OS or Editor folders 14 | .DS_Store 15 | .cache 16 | .project 17 | .settings 18 | .tmproj 19 | nbproject 20 | Thumbs.db 21 | 22 | # NPM packages folder. 23 | node_modules/ 24 | 25 | # Brunch folder for temporary files. 26 | tmp/ 27 | -------------------------------------------------------------------------------- /Cakefile: -------------------------------------------------------------------------------- 1 | sys = require 'sys' 2 | {spawn} = require 'child_process' 3 | 4 | task 'test', 'Run all unit, integration and functional tests', -> 5 | task 'test:functionals', 'Run functional tests', -> 6 | task 'test:integration', 'Run integration tests', -> 7 | task 'test:units', 'Run unit tests', -> 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Todos 2 | 3 | **Deprecated!** Use [todomvc chaplin-brunch](https://github.com/addyosmani/todomvc/tree/gh-pages/labs/dependency-examples/chaplin-brunch) instead. 4 | 5 | This is a rewrite of Todos (Backbone's example application). We rewrote it to provide a simple and complete brunch example. Special thanks to [Jérôme Gravel-Niquet](http://jgn.me/) for his groundwork. 6 | 7 | ## Development 8 | 9 | Use `brunch build` or `brunch watch` to compile changes in src folder. 10 | Usually we don't track the js/css files in our repositories, but decided to keep them here because we hope it would be easier to start using it. 11 | -------------------------------------------------------------------------------- /app/application.coffee: -------------------------------------------------------------------------------- 1 | # The application bootstrapper. 2 | Application = 3 | initialize: -> 4 | Todos = require 'models/todos' 5 | Router = require 'lib/router' 6 | HomeView = require 'views/home_view' 7 | NewTodoView = require 'views/new_todo_view' 8 | StatsView = require 'views/stats_view' 9 | TodoView = require 'views/todo_view' 10 | TodosView = require 'views/todos_view' 11 | 12 | # Ideally, initialized classes should be kept in controllers & mediator. 13 | # If you're making big webapp, here's more sophisticated skeleton 14 | # https://github.com/paulmillr/brunch-with-chaplin 15 | @todos = new Todos() 16 | 17 | @homeView = new HomeView() 18 | @statsView = new StatsView() 19 | @newTodoView = new NewTodoView() 20 | @todosView = new TodosView() 21 | 22 | # Instantiate the router 23 | @router = new Router() 24 | # Freeze the object 25 | Object.freeze? Application 26 | 27 | module.exports = Application 28 | -------------------------------------------------------------------------------- /app/assets/images/destroy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brunch/todos/ae93e8a61786de4f6f8ad2f57967eb4898d2fe6d/app/assets/images/destroy.png -------------------------------------------------------------------------------- /app/assets/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Example brunch application 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | -------------------------------------------------------------------------------- /app/initialize.coffee: -------------------------------------------------------------------------------- 1 | application = require 'application' 2 | 3 | $ -> 4 | application.initialize() 5 | Backbone.history.start() 6 | -------------------------------------------------------------------------------- /app/lib/router.coffee: -------------------------------------------------------------------------------- 1 | application = require 'application' 2 | 3 | module.exports = class Router extends Backbone.Router 4 | routes: 5 | '': 'home' 6 | 7 | home: -> 8 | application.homeView.render() 9 | application.todos.fetch() 10 | -------------------------------------------------------------------------------- /app/lib/view_helper.coffee: -------------------------------------------------------------------------------- 1 | Handlebars.registerHelper 'pluralize', (count, fn) -> 2 | string = fn() 3 | pluralized = if count is 1 4 | string 5 | else 6 | "#{string}s" 7 | new Handlebars.SafeString pluralized 8 | -------------------------------------------------------------------------------- /app/models/collection.coffee: -------------------------------------------------------------------------------- 1 | # Base class for all collections. 2 | module.exports = class Collection extends Backbone.Collection 3 | -------------------------------------------------------------------------------- /app/models/model.coffee: -------------------------------------------------------------------------------- 1 | # Base class for all models. 2 | module.exports = class Model extends Backbone.Model 3 | -------------------------------------------------------------------------------- /app/models/todo.coffee: -------------------------------------------------------------------------------- 1 | Model = require './model' 2 | 3 | module.exports = class Todo extends Model 4 | defaults: 5 | content: 'Empty todo...' 6 | done: no 7 | 8 | toggle: -> 9 | @save done: not @get 'done' 10 | 11 | clear: -> 12 | @destroy() 13 | @view.remove() 14 | -------------------------------------------------------------------------------- /app/models/todos.coffee: -------------------------------------------------------------------------------- 1 | Collection = require './collection' 2 | Todo = require 'models/todo' 3 | 4 | module.exports = class Todos extends Collection 5 | model: Todo 6 | 7 | initialize: -> 8 | @localStorage = new Store 'todos' 9 | 10 | done: -> 11 | @filter (todo) -> 12 | todo.get 'done' 13 | 14 | remaining: -> 15 | @without.apply this, @done() 16 | 17 | nextOrder: -> 18 | return 1 unless @length 19 | @last().get('order') + 1 20 | 21 | comparator: (todo) -> 22 | todo.get 'order' 23 | 24 | clearCompleted: -> 25 | _.each @done(), (todo) -> todo.clear() 26 | -------------------------------------------------------------------------------- /app/views/home_view.coffee: -------------------------------------------------------------------------------- 1 | View = require './view' 2 | application = require 'application' 3 | template = require './templates/home' 4 | 5 | module.exports = class HomeView extends View 6 | template: template 7 | el: '#home-view' 8 | 9 | afterRender: -> 10 | $todo = @$el.find('#todo-app') 11 | for viewName in ['newTodo', 'todos', 'stats'] 12 | $todo.append application["#{viewName}View"].render().el 13 | -------------------------------------------------------------------------------- /app/views/new_todo_view.coffee: -------------------------------------------------------------------------------- 1 | View = require './view' 2 | application = require 'application' 3 | template = require './templates/new_todo' 4 | 5 | module.exports = class NewTodoView extends View 6 | template: template 7 | id: 'new-todo-view' 8 | events: 9 | 'keypress #new-todo': 'createOnEnter' 10 | 'keyup #new-todo': 'showHint' 11 | 12 | newAttributes: -> 13 | attributes = 14 | order: application.todos.nextOrder() 15 | attributes.content = @$('#new-todo').val() if @$('#new-todo').val() 16 | attributes 17 | 18 | createOnEnter: (event) -> 19 | return unless event.keyCode is 13 20 | application.todos.create @newAttributes() 21 | @$('#new-todo').val '' 22 | 23 | showHint: (event) -> 24 | tooltip = @$('.ui-tooltip-top') 25 | input = @$('#new-todo') 26 | tooltip.fadeOut() 27 | clearTimeout @tooltipTimeout if @tooltipTimeout 28 | return if input.val() is '' or input.val() is input.attr 'placeholder' 29 | @tooltipTimeout = setTimeout (-> tooltip.fadeIn()), 1000 30 | -------------------------------------------------------------------------------- /app/views/stats_view.coffee: -------------------------------------------------------------------------------- 1 | View = require './view' 2 | application = require 'application' 3 | template = require './templates/stats' 4 | 5 | module.exports = class StatsView extends View 6 | template: template 7 | id: 'stats-view' 8 | events: 9 | 'click .todo-clear' : 'clearCompleted' 10 | 11 | getRenderData: -> 12 | { 13 | stats: 14 | total: application.todos.length 15 | done: application.todos.done().length 16 | remaining: application.todos.remaining().length 17 | } 18 | 19 | clearCompleted: -> 20 | application.todos.clearCompleted() 21 | -------------------------------------------------------------------------------- /app/views/styles/application.styl: -------------------------------------------------------------------------------- 1 | @import 'nib' 2 | 3 | html 4 | background: #A1AFC9 5 | 6 | body 7 | font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif 8 | font-size: 14px 9 | line-height: 1.4em 10 | background: #A1AFC9 11 | color: #000 12 | 13 | ul 14 | list-style-type: none 15 | 16 | #todo-app 17 | width: 480px 18 | margin: 0 auto 40px 19 | background: white 20 | padding: 20px 21 | box-shadow: rgba(0, 0, 0, 0.2) 0 5px 6px 0 22 | 23 | h1 24 | font-size: 36px 25 | font-weight: bold 26 | text-align: center 27 | padding: 20px 0 30px 0 28 | line-height: 1 29 | 30 | #new-todo-view 31 | position: relative 32 | 33 | input 34 | width: 466px 35 | font-size: 24px 36 | font-family: inherit 37 | line-height: 1.4em 38 | border: 1px solid #999 39 | outline: none 40 | padding: 6px 41 | box-shadow: rgba(0, 0, 0, 0.2) 0 1px 2px 0 inset 42 | 43 | input::-webkit-input-placeholder 44 | font-style: italic 45 | 46 | .ui-tooltip-top 47 | display: none 48 | position: absolute 49 | z-index: 999 50 | width: 170px 51 | left: 50% 52 | margin-left: -85px 53 | 54 | #todos 55 | margin-top: 10px 56 | padding-left: 0 57 | 58 | li 59 | padding: 12px 20px 11px 0 60 | position: relative 61 | font-size: 24px 62 | line-height: 1.1em 63 | border-bottom: 1px solid #ccc 64 | 65 | li:after 66 | content: "\0020" 67 | display: block 68 | height: 0 69 | clear: both 70 | overflow: hidden 71 | visibility: hidden 72 | 73 | li.editing 74 | padding: 0 75 | border-bottom: 0 76 | width: 160 + 120px 77 | 78 | .editing .display, .edit 79 | display: none 80 | 81 | .editing 82 | .edit 83 | display: block 84 | 85 | input 86 | width: 444px 87 | font-size: 24px 88 | font-family: inherit 89 | margin: 0 90 | line-height: 1.6em 91 | border: 1px solid #999 92 | outline: none 93 | padding: 10px 7px 0px 27px 94 | box-shadow: rgba(0, 0, 0, 0.2) 0 1px 2px 0 inset 95 | 96 | .check 97 | position: relative 98 | top: 9px 99 | margin: 0 10px 0 7px 100 | float: left 101 | 102 | .done .todo-content 103 | text-decoration: line-through 104 | color: #777 105 | 106 | .todo-destroy 107 | position: absolute 108 | right: 5px 109 | top: 14px 110 | display: none 111 | cursor: pointer 112 | width: 20px 113 | height: 20px 114 | background: url("../images/destroy.png") no-repeat 0 0 115 | 116 | li:hover .todo-destroy 117 | display: block 118 | 119 | .todo-destroy:hover 120 | background-position: 0 -20px 121 | 122 | #stats-view:after 123 | content: "\0020" 124 | display: block 125 | height: 0 126 | clear: both 127 | overflow: hidden 128 | visibility: hidden 129 | 130 | #stats-view 131 | margin-top: 10px 132 | color: #777 133 | 134 | .todo-count 135 | float: left 136 | 137 | .todo-count .number 138 | font-weight: bold 139 | color: #333 140 | 141 | .todo-clear 142 | float: right 143 | text-decoration: underline 144 | cursor: pointer 145 | 146 | .todo-clear 147 | a 148 | color: #777 149 | font-size: 12px 150 | 151 | a:visited 152 | color: #777 153 | 154 | a:hover 155 | color: #336699 156 | 157 | #instructions 158 | width: 520px 159 | margin: 10px auto 160 | padding-left: 0 161 | color: #777 162 | text-shadow: rgba(200, 210, 230, 0.8) 0 1px 0 163 | text-align: center 164 | 165 | a 166 | color: #369 167 | 168 | #credits 169 | width: 520px 170 | margin: 30px auto 171 | color: #333 172 | text-shadow: rgba(200, 210, 230, 0.8) 0 1px 0 173 | text-align: center 174 | 175 | span, a 176 | display: block 177 | 178 | a 179 | color: #555 180 | 181 | .ui-tooltip:after, .ui-tooltip-top:after, .ui-tooltip-right:after, .ui-tooltip-bottom:after, .ui-tooltip-left:after 182 | content: "\25B8" 183 | display: block 184 | font-size: 2em 185 | height: 0 186 | line-height: 0 187 | position: absolute 188 | 189 | .ui-tooltip:after, .ui-tooltip-bottom:after 190 | color: #2a2a2a 191 | bottom 0 192 | left: 1px 193 | text-align: center 194 | text-shadow: 1px 0 2px #000 195 | transform: rotate(90deg) 196 | width: 100% 197 | 198 | .ui-tooltip-top:after 199 | bottom auto 200 | color: #4f4f4f 201 | left: -2px 202 | top: 0 203 | text-align: center 204 | text-shadow: none 205 | transform: rotate(-90deg) 206 | width: 100% 207 | 208 | .ui-tooltip-right:after 209 | color: #222 210 | right -0.375em 211 | top: 50% 212 | margin-top: -.05em 213 | text-shadow: 0 1px 2px #000 214 | transform: rotate(0) 215 | 216 | .ui-tooltip-left:after 217 | color: #222 218 | left: -0.375em 219 | top: 50% 220 | margin-top: .1em 221 | text-shadow: 0 -1px 2px #000 222 | transform: rotate(180deg) 223 | 224 | .ui-tooltip, .ui-tooltip-top, .ui-tooltip-right, 225 | .ui-tooltip-bottom, .ui-tooltip-left 226 | color: #FFF 227 | cursor: normal 228 | display: inline-block 229 | font-size: 12px 230 | font-family: arial 231 | padding: 0.5em 1em 232 | position: relative 233 | text-align: center 234 | text-shadow: 0 -1px 1px #111 235 | border-radius: 4px 236 | box-shadow: 0 1px 2px #000\, inset 0 0 0 1px #222\, inset 0 2px #666666\, inset 0 -2px 2px #444 237 | background-color: #3b3b3b 238 | background-image: linear-gradient(top, #555, #222) 239 | 240 | .ui-tooltip:after, .ui-tooltip-top:after, .ui-tooltip-right:after, .ui-tooltip-bottom:after, .ui-tooltip-left:after 241 | content: "\25B8" 242 | display: block 243 | font-size: 2em 244 | height: 0 245 | line-height: 0 246 | position: absolute 247 | 248 | .ui-tooltip:after, .ui-tooltip-bottom:after 249 | color: #2a2a2a 250 | bottom: 0 251 | left: 1px 252 | text-align: center 253 | text-shadow: 1px 0 2px #000 254 | transform: rotate(90deg) 255 | width: 100% 256 | 257 | .ui-tooltip-top:after 258 | bottom: auto 259 | color: #4f4f4f 260 | left: -2px 261 | top: 0 262 | text-align: center 263 | text-shadow: none 264 | transform: rotate(-90deg) 265 | width: 100% 266 | 267 | .ui-tooltip-right:after 268 | color: #222 269 | right: -0.375em 270 | top: 50% 271 | margin-top: -0.05em 272 | text-shadow: 0 1px 2px #000 273 | transform: rotate(0) 274 | 275 | .ui-tooltip-left:after 276 | color: #222 277 | left: -0.375em 278 | top: 50% 279 | margin-top: 0.1em 280 | text-shadow: 0 -1px 2px #000 281 | transform: rotate(180deg) 282 | -------------------------------------------------------------------------------- /app/views/templates/home.hbs: -------------------------------------------------------------------------------- 1 |
2 |

Todos

3 |
4 | 8 |
9 | Originally created by 10 | Jérôme Gravel-Niquet 11 | Rewritten by 12 | Brunch Team 13 |
14 | -------------------------------------------------------------------------------- /app/views/templates/new_todo.hbs: -------------------------------------------------------------------------------- 1 | 2 |
Press Enter to save this task
3 | -------------------------------------------------------------------------------- /app/views/templates/stats.hbs: -------------------------------------------------------------------------------- 1 | {{#if stats.total}} 2 | 3 | {{stats.remaining}} 4 | 5 | {{#pluralize stats.remaining}}item{{/pluralize}} 6 | 7 | left. 8 | 9 | {{/if}} 10 | 11 | {{#if stats.done}} 12 | 13 | Clear {{stats.done}} completed 14 | 15 | {{#pluralize stats.done}}item{{/pluralize}} 16 | 17 | 18 | {{/if}} 19 | -------------------------------------------------------------------------------- /app/views/templates/todo.hbs: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
{{todo.content}}
5 | 6 |
7 |
8 | 9 |
10 |
11 | -------------------------------------------------------------------------------- /app/views/templates/todos.hbs: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /app/views/todo_view.coffee: -------------------------------------------------------------------------------- 1 | View = require './view' 2 | template = require './templates/todo' 3 | 4 | module.exports = class TodoView extends View 5 | template: template 6 | tagName: 'li' 7 | 8 | events: 9 | 'click .check': 'toggleDone' 10 | 'dblclick .todo-content': 'edit' 11 | 'click .todo-destroy': 'clear' 12 | 'keypress .todo-input': 'updateOnEnter' 13 | 14 | initialize: -> 15 | @model.bind 'change', @render 16 | @model.view = this 17 | 18 | getRenderData: -> 19 | { 20 | todo: @model.toJSON() 21 | } 22 | 23 | afterRender: -> 24 | @$('.todo-input').bind 'blur', @update 25 | 26 | toggleDone: -> 27 | @model.toggle() 28 | 29 | edit: -> 30 | @$el.addClass 'editing' 31 | $('.todo-input').focus() 32 | 33 | update: => 34 | @model.save content: @$('.todo-input').val() 35 | @$el.removeClass 'editing' 36 | 37 | updateOnEnter: (event) -> 38 | @update() if event.keyCode is 13 39 | 40 | remove: -> 41 | @$el.remove() 42 | 43 | clear: -> 44 | @model.clear() 45 | -------------------------------------------------------------------------------- /app/views/todos_view.coffee: -------------------------------------------------------------------------------- 1 | View = require './view' 2 | TodoView = require './todo_view' 3 | application = require 'application' 4 | template = require './templates/todos' 5 | 6 | module.exports = class TodosView extends View 7 | template: template 8 | id: 'todos-view' 9 | 10 | addOne: (todo) => 11 | view = new TodoView model: todo 12 | @$el.find('#todos').append view.render().el 13 | 14 | addAll: => 15 | # TODO explain why this is working - see underscore source 16 | application.todos.each @addOne 17 | 18 | initialize: -> 19 | application.todos.bind 'add', @addOne 20 | application.todos.bind 'reset', @addAll 21 | application.todos.bind 'all', @renderStats 22 | 23 | renderStats: => 24 | application.statsView.render() 25 | -------------------------------------------------------------------------------- /app/views/user.coffee: -------------------------------------------------------------------------------- 1 | View = require './view' 2 | 3 | module.exports = class User extends View 4 | -------------------------------------------------------------------------------- /app/views/view.coffee: -------------------------------------------------------------------------------- 1 | require 'lib/view_helper' 2 | 3 | # Base class for all views. 4 | module.exports = class View extends Backbone.View 5 | template: -> 6 | return 7 | 8 | getRenderData: -> 9 | return 10 | 11 | render: => 12 | console.debug "Rendering #{@constructor.name}" 13 | @$el.html @template @getRenderData() 14 | @afterRender() 15 | this 16 | 17 | afterRender: -> 18 | return 19 | -------------------------------------------------------------------------------- /config.coffee: -------------------------------------------------------------------------------- 1 | exports.config = 2 | # Edit the next line to change default build path. 3 | paths: 4 | public: 'public' 5 | 6 | files: 7 | javascripts: 8 | # Defines what file will be generated with `brunch generate`. 9 | defaultExtension: 'coffee' 10 | # Describes how files will be compiled & joined together. 11 | # Available formats: 12 | # * 'outputFilePath' 13 | # * map of ('outputFilePath': /regExp that matches input path/) 14 | # * map of ('outputFilePath': function that takes input path) 15 | joinTo: 16 | 'javascripts/app.js': /^app/ 17 | 'javascripts/vendor.js': /^vendor/ 18 | # Defines compilation order. 19 | # `vendor` files will be compiled before other ones 20 | # even if they are not present here. 21 | order: 22 | before: [ 23 | 'vendor/scripts/console-helper.js', 24 | 'vendor/scripts/jquery-1.7.2.js', 25 | 'vendor/scripts/underscore-1.3.1.js', 26 | 'vendor/scripts/backbone-0.9.2.js' 27 | ] 28 | 29 | stylesheets: 30 | joinTo: 'stylesheets/app.css' 31 | 32 | templates: 33 | joinTo: 'javascripts/app.js' 34 | 35 | # Change this if you're using something other than backbone (e.g. 'ember'). 36 | # Content of files, generated with `brunch generate` depends on the setting. 37 | # framework: 'backbone' 38 | 39 | # Settings of web server that will run with `brunch watch [--server]`. 40 | # server: 41 | # # Path to your server node.js module. 42 | # # If it's commented-out, brunch will use built-in express.js server. 43 | # path: 'server.coffee' 44 | # port: 3333 45 | # # Run even without `--server` option? 46 | # run: yes 47 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "Your Name", 3 | "name": "package-name", 4 | "description": "Package description", 5 | "version": "0.0.1", 6 | "homepage": "", 7 | "repository": { 8 | "type": "git", 9 | "url": "" 10 | }, 11 | "engines": { 12 | "node": "~0.6.10" 13 | }, 14 | "scripts": { 15 | "start": "brunch watch --server" 16 | }, 17 | "dependencies": { 18 | "javascript-brunch": ">= 1.0 < 1.5", 19 | "coffee-script-brunch": ">= 1.0 < 1.5", 20 | "css-brunch": ">= 1.0 < 1.5", 21 | "stylus-brunch": ">= 1.0 < 1.5", 22 | "handlebars-brunch": ">= 1.0 < 1.5", 23 | "uglify-js-brunch": ">= 1.0 < 1.5", 24 | "clean-css-brunch": ">= 1.0 < 1.5" 25 | }, 26 | "devDependencies": { 27 | "mocha": "0.14.0", 28 | "expect.js": "0.1.2" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /public/images/destroy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brunch/todos/ae93e8a61786de4f6f8ad2f57967eb4898d2fe6d/public/images/destroy.png -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Example brunch application 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | -------------------------------------------------------------------------------- /public/javascripts/app.js: -------------------------------------------------------------------------------- 1 | (function(/*! Brunch !*/) { 2 | 'use strict'; 3 | 4 | var globals = typeof window !== 'undefined' ? window : global; 5 | if (typeof globals.require === 'function') return; 6 | 7 | var modules = {}; 8 | var cache = {}; 9 | 10 | var has = function(object, name) { 11 | return ({}).hasOwnProperty.call(object, name); 12 | }; 13 | 14 | var expand = function(root, name) { 15 | var results = [], parts, part; 16 | if (/^\.\.?(\/|$)/.test(name)) { 17 | parts = [root, name].join('/').split('/'); 18 | } else { 19 | parts = name.split('/'); 20 | } 21 | for (var i = 0, length = parts.length; i < length; i++) { 22 | part = parts[i]; 23 | if (part === '..') { 24 | results.pop(); 25 | } else if (part !== '.' && part !== '') { 26 | results.push(part); 27 | } 28 | } 29 | return results.join('/'); 30 | }; 31 | 32 | var dirname = function(path) { 33 | return path.split('/').slice(0, -1).join('/'); 34 | }; 35 | 36 | var localRequire = function(path) { 37 | return function(name) { 38 | var dir = dirname(path); 39 | var absolute = expand(dir, name); 40 | return globals.require(absolute); 41 | }; 42 | }; 43 | 44 | var initModule = function(name, definition) { 45 | var module = {id: name, exports: {}}; 46 | definition(module.exports, localRequire(name), module); 47 | var exports = cache[name] = module.exports; 48 | return exports; 49 | }; 50 | 51 | var require = function(name) { 52 | var path = expand(name, '.'); 53 | 54 | if (has(cache, path)) return cache[path]; 55 | if (has(modules, path)) return initModule(path, modules[path]); 56 | 57 | var dirIndex = expand(path, './index'); 58 | if (has(cache, dirIndex)) return cache[dirIndex]; 59 | if (has(modules, dirIndex)) return initModule(dirIndex, modules[dirIndex]); 60 | 61 | throw new Error('Cannot find module "' + name + '"'); 62 | }; 63 | 64 | var define = function(bundle) { 65 | for (var key in bundle) { 66 | if (has(bundle, key)) { 67 | modules[key] = bundle[key]; 68 | } 69 | } 70 | } 71 | 72 | globals.require = require; 73 | globals.require.define = define; 74 | globals.require.brunch = true; 75 | })(); 76 | 77 | window.require.define({"application": function(exports, require, module) { 78 | (function() { 79 | var Application; 80 | 81 | Application = { 82 | initialize: function() { 83 | var HomeView, NewTodoView, Router, StatsView, TodoView, Todos, TodosView; 84 | Todos = require('models/todos'); 85 | Router = require('lib/router'); 86 | HomeView = require('views/home_view'); 87 | NewTodoView = require('views/new_todo_view'); 88 | StatsView = require('views/stats_view'); 89 | TodoView = require('views/todo_view'); 90 | TodosView = require('views/todos_view'); 91 | this.todos = new Todos(); 92 | this.homeView = new HomeView(); 93 | this.statsView = new StatsView(); 94 | this.newTodoView = new NewTodoView(); 95 | this.todosView = new TodosView(); 96 | this.router = new Router(); 97 | return typeof Object.freeze === "function" ? Object.freeze(Application) : void 0; 98 | } 99 | }; 100 | 101 | module.exports = Application; 102 | 103 | }).call(this); 104 | 105 | }}); 106 | 107 | window.require.define({"initialize": function(exports, require, module) { 108 | (function() { 109 | var application; 110 | 111 | application = require('application'); 112 | 113 | $(function() { 114 | application.initialize(); 115 | return Backbone.history.start(); 116 | }); 117 | 118 | }).call(this); 119 | 120 | }}); 121 | 122 | window.require.define({"lib/router": function(exports, require, module) { 123 | (function() { 124 | var Router, application, 125 | __hasProp = Object.prototype.hasOwnProperty, 126 | __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; }; 127 | 128 | application = require('application'); 129 | 130 | module.exports = Router = (function(_super) { 131 | 132 | __extends(Router, _super); 133 | 134 | function Router() { 135 | Router.__super__.constructor.apply(this, arguments); 136 | } 137 | 138 | Router.prototype.routes = { 139 | '': 'home' 140 | }; 141 | 142 | Router.prototype.home = function() { 143 | application.homeView.render(); 144 | return application.todos.fetch(); 145 | }; 146 | 147 | return Router; 148 | 149 | })(Backbone.Router); 150 | 151 | }).call(this); 152 | 153 | }}); 154 | 155 | window.require.define({"lib/view_helper": function(exports, require, module) { 156 | (function() { 157 | 158 | Handlebars.registerHelper('pluralize', function(count, fn) { 159 | var pluralized, string; 160 | string = fn(); 161 | pluralized = count === 1 ? string : "" + string + "s"; 162 | return new Handlebars.SafeString(pluralized); 163 | }); 164 | 165 | }).call(this); 166 | 167 | }}); 168 | 169 | window.require.define({"models/collection": function(exports, require, module) { 170 | (function() { 171 | var Collection, 172 | __hasProp = Object.prototype.hasOwnProperty, 173 | __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; }; 174 | 175 | module.exports = Collection = (function(_super) { 176 | 177 | __extends(Collection, _super); 178 | 179 | function Collection() { 180 | Collection.__super__.constructor.apply(this, arguments); 181 | } 182 | 183 | return Collection; 184 | 185 | })(Backbone.Collection); 186 | 187 | }).call(this); 188 | 189 | }}); 190 | 191 | window.require.define({"models/model": function(exports, require, module) { 192 | (function() { 193 | var Model, 194 | __hasProp = Object.prototype.hasOwnProperty, 195 | __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; }; 196 | 197 | module.exports = Model = (function(_super) { 198 | 199 | __extends(Model, _super); 200 | 201 | function Model() { 202 | Model.__super__.constructor.apply(this, arguments); 203 | } 204 | 205 | return Model; 206 | 207 | })(Backbone.Model); 208 | 209 | }).call(this); 210 | 211 | }}); 212 | 213 | window.require.define({"models/todo": function(exports, require, module) { 214 | (function() { 215 | var Model, Todo, 216 | __hasProp = Object.prototype.hasOwnProperty, 217 | __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; }; 218 | 219 | Model = require('./model'); 220 | 221 | module.exports = Todo = (function(_super) { 222 | 223 | __extends(Todo, _super); 224 | 225 | function Todo() { 226 | Todo.__super__.constructor.apply(this, arguments); 227 | } 228 | 229 | Todo.prototype.defaults = { 230 | content: 'Empty todo...', 231 | done: false 232 | }; 233 | 234 | Todo.prototype.toggle = function() { 235 | return this.save({ 236 | done: !this.get('done') 237 | }); 238 | }; 239 | 240 | Todo.prototype.clear = function() { 241 | this.destroy(); 242 | return this.view.remove(); 243 | }; 244 | 245 | return Todo; 246 | 247 | })(Model); 248 | 249 | }).call(this); 250 | 251 | }}); 252 | 253 | window.require.define({"models/todos": function(exports, require, module) { 254 | (function() { 255 | var Collection, Todo, Todos, 256 | __hasProp = Object.prototype.hasOwnProperty, 257 | __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; }; 258 | 259 | Collection = require('./collection'); 260 | 261 | Todo = require('models/todo'); 262 | 263 | module.exports = Todos = (function(_super) { 264 | 265 | __extends(Todos, _super); 266 | 267 | function Todos() { 268 | Todos.__super__.constructor.apply(this, arguments); 269 | } 270 | 271 | Todos.prototype.model = Todo; 272 | 273 | Todos.prototype.initialize = function() { 274 | return this.localStorage = new Store('todos'); 275 | }; 276 | 277 | Todos.prototype.done = function() { 278 | return this.filter(function(todo) { 279 | return todo.get('done'); 280 | }); 281 | }; 282 | 283 | Todos.prototype.remaining = function() { 284 | return this.without.apply(this, this.done()); 285 | }; 286 | 287 | Todos.prototype.nextOrder = function() { 288 | if (!this.length) return 1; 289 | return this.last().get('order') + 1; 290 | }; 291 | 292 | Todos.prototype.comparator = function(todo) { 293 | return todo.get('order'); 294 | }; 295 | 296 | Todos.prototype.clearCompleted = function() { 297 | return _.each(this.done(), function(todo) { 298 | return todo.clear(); 299 | }); 300 | }; 301 | 302 | return Todos; 303 | 304 | })(Collection); 305 | 306 | }).call(this); 307 | 308 | }}); 309 | 310 | window.require.define({"views/home_view": function(exports, require, module) { 311 | (function() { 312 | var HomeView, View, application, template, 313 | __hasProp = Object.prototype.hasOwnProperty, 314 | __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; }; 315 | 316 | View = require('./view'); 317 | 318 | application = require('application'); 319 | 320 | template = require('./templates/home'); 321 | 322 | module.exports = HomeView = (function(_super) { 323 | 324 | __extends(HomeView, _super); 325 | 326 | function HomeView() { 327 | HomeView.__super__.constructor.apply(this, arguments); 328 | } 329 | 330 | HomeView.prototype.template = template; 331 | 332 | HomeView.prototype.el = '#home-view'; 333 | 334 | HomeView.prototype.afterRender = function() { 335 | var $todo, viewName, _i, _len, _ref, _results; 336 | $todo = this.$el.find('#todo-app'); 337 | _ref = ['newTodo', 'todos', 'stats']; 338 | _results = []; 339 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 340 | viewName = _ref[_i]; 341 | _results.push($todo.append(application["" + viewName + "View"].render().el)); 342 | } 343 | return _results; 344 | }; 345 | 346 | return HomeView; 347 | 348 | })(View); 349 | 350 | }).call(this); 351 | 352 | }}); 353 | 354 | window.require.define({"views/new_todo_view": function(exports, require, module) { 355 | (function() { 356 | var NewTodoView, View, application, template, 357 | __hasProp = Object.prototype.hasOwnProperty, 358 | __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; }; 359 | 360 | View = require('./view'); 361 | 362 | application = require('application'); 363 | 364 | template = require('./templates/new_todo'); 365 | 366 | module.exports = NewTodoView = (function(_super) { 367 | 368 | __extends(NewTodoView, _super); 369 | 370 | function NewTodoView() { 371 | NewTodoView.__super__.constructor.apply(this, arguments); 372 | } 373 | 374 | NewTodoView.prototype.template = template; 375 | 376 | NewTodoView.prototype.id = 'new-todo-view'; 377 | 378 | NewTodoView.prototype.events = { 379 | 'keypress #new-todo': 'createOnEnter', 380 | 'keyup #new-todo': 'showHint' 381 | }; 382 | 383 | NewTodoView.prototype.newAttributes = function() { 384 | var attributes; 385 | attributes = { 386 | order: application.todos.nextOrder() 387 | }; 388 | if (this.$('#new-todo').val()) { 389 | attributes.content = this.$('#new-todo').val(); 390 | } 391 | return attributes; 392 | }; 393 | 394 | NewTodoView.prototype.createOnEnter = function(event) { 395 | if (event.keyCode !== 13) return; 396 | application.todos.create(this.newAttributes()); 397 | return this.$('#new-todo').val(''); 398 | }; 399 | 400 | NewTodoView.prototype.showHint = function(event) { 401 | var input, tooltip; 402 | tooltip = this.$('.ui-tooltip-top'); 403 | input = this.$('#new-todo'); 404 | tooltip.fadeOut(); 405 | if (this.tooltipTimeout) clearTimeout(this.tooltipTimeout); 406 | if (input.val() === '' || input.val() === input.attr('placeholder')) return; 407 | return this.tooltipTimeout = setTimeout((function() { 408 | return tooltip.fadeIn(); 409 | }), 1000); 410 | }; 411 | 412 | return NewTodoView; 413 | 414 | })(View); 415 | 416 | }).call(this); 417 | 418 | }}); 419 | 420 | window.require.define({"views/stats_view": function(exports, require, module) { 421 | (function() { 422 | var StatsView, View, application, template, 423 | __hasProp = Object.prototype.hasOwnProperty, 424 | __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; }; 425 | 426 | View = require('./view'); 427 | 428 | application = require('application'); 429 | 430 | template = require('./templates/stats'); 431 | 432 | module.exports = StatsView = (function(_super) { 433 | 434 | __extends(StatsView, _super); 435 | 436 | function StatsView() { 437 | StatsView.__super__.constructor.apply(this, arguments); 438 | } 439 | 440 | StatsView.prototype.template = template; 441 | 442 | StatsView.prototype.id = 'stats-view'; 443 | 444 | StatsView.prototype.events = { 445 | 'click .todo-clear': 'clearCompleted' 446 | }; 447 | 448 | StatsView.prototype.getRenderData = function() { 449 | return { 450 | stats: { 451 | total: application.todos.length, 452 | done: application.todos.done().length, 453 | remaining: application.todos.remaining().length 454 | } 455 | }; 456 | }; 457 | 458 | StatsView.prototype.clearCompleted = function() { 459 | return application.todos.clearCompleted(); 460 | }; 461 | 462 | return StatsView; 463 | 464 | })(View); 465 | 466 | }).call(this); 467 | 468 | }}); 469 | 470 | window.require.define({"views/templates/home": function(exports, require, module) { 471 | module.exports = Handlebars.template(function (Handlebars,depth0,helpers,partials,data) { 472 | helpers = helpers || Handlebars.helpers; 473 | var foundHelper, self=this; 474 | 475 | 476 | return "
\n

Todos

\n
\n\n
\n Originally created by\n Jérôme Gravel-Niquet\n Rewritten by\n Brunch Team\n
\n";}); 477 | }}); 478 | 479 | window.require.define({"views/templates/new_todo": function(exports, require, module) { 480 | module.exports = Handlebars.template(function (Handlebars,depth0,helpers,partials,data) { 481 | helpers = helpers || Handlebars.helpers; 482 | var foundHelper, self=this; 483 | 484 | 485 | return "\n
Press Enter to save this task
\n";}); 486 | }}); 487 | 488 | window.require.define({"views/templates/stats": function(exports, require, module) { 489 | module.exports = Handlebars.template(function (Handlebars,depth0,helpers,partials,data) { 490 | helpers = helpers || Handlebars.helpers; 491 | var buffer = "", stack1, stack2, foundHelper, tmp1, self=this, functionType="function", helperMissing=helpers.helperMissing, undef=void 0, escapeExpression=this.escapeExpression, blockHelperMissing=helpers.blockHelperMissing; 492 | 493 | function program1(depth0,data) { 494 | 495 | var buffer = "", stack1, stack2; 496 | buffer += "\n \n "; 497 | foundHelper = helpers.stats; 498 | stack1 = foundHelper || depth0.stats; 499 | stack1 = (stack1 === null || stack1 === undefined || stack1 === false ? stack1 : stack1.remaining); 500 | if(typeof stack1 === functionType) { stack1 = stack1.call(depth0, { hash: {} }); } 501 | else if(stack1=== undef) { stack1 = helperMissing.call(depth0, "stats.remaining", { hash: {} }); } 502 | buffer += escapeExpression(stack1) + "\n \n "; 503 | foundHelper = helpers.stats; 504 | stack1 = foundHelper || depth0.stats; 505 | stack1 = (stack1 === null || stack1 === undefined || stack1 === false ? stack1 : stack1.remaining); 506 | foundHelper = helpers.pluralize; 507 | stack2 = foundHelper || depth0.pluralize; 508 | tmp1 = self.program(2, program2, data); 509 | tmp1.hash = {}; 510 | tmp1.fn = tmp1; 511 | tmp1.inverse = self.noop; 512 | if(foundHelper && typeof stack2 === functionType) { stack1 = stack2.call(depth0, stack1, tmp1); } 513 | else { stack1 = blockHelperMissing.call(depth0, stack2, stack1, tmp1); } 514 | if(stack1 || stack1 === 0) { buffer += stack1; } 515 | buffer += "\n \n left.\n \n"; 516 | return buffer;} 517 | function program2(depth0,data) { 518 | 519 | 520 | return "item";} 521 | 522 | function program4(depth0,data) { 523 | 524 | var buffer = "", stack1, stack2; 525 | buffer += "\n \n Clear "; 526 | foundHelper = helpers.stats; 527 | stack1 = foundHelper || depth0.stats; 528 | stack1 = (stack1 === null || stack1 === undefined || stack1 === false ? stack1 : stack1.done); 529 | if(typeof stack1 === functionType) { stack1 = stack1.call(depth0, { hash: {} }); } 530 | else if(stack1=== undef) { stack1 = helperMissing.call(depth0, "stats.done", { hash: {} }); } 531 | buffer += escapeExpression(stack1) + " completed\n \n "; 532 | foundHelper = helpers.stats; 533 | stack1 = foundHelper || depth0.stats; 534 | stack1 = (stack1 === null || stack1 === undefined || stack1 === false ? stack1 : stack1.done); 535 | foundHelper = helpers.pluralize; 536 | stack2 = foundHelper || depth0.pluralize; 537 | tmp1 = self.program(5, program5, data); 538 | tmp1.hash = {}; 539 | tmp1.fn = tmp1; 540 | tmp1.inverse = self.noop; 541 | if(foundHelper && typeof stack2 === functionType) { stack1 = stack2.call(depth0, stack1, tmp1); } 542 | else { stack1 = blockHelperMissing.call(depth0, stack2, stack1, tmp1); } 543 | if(stack1 || stack1 === 0) { buffer += stack1; } 544 | buffer += "\n \n \n"; 545 | return buffer;} 546 | function program5(depth0,data) { 547 | 548 | 549 | return "item";} 550 | 551 | foundHelper = helpers.stats; 552 | stack1 = foundHelper || depth0.stats; 553 | stack1 = (stack1 === null || stack1 === undefined || stack1 === false ? stack1 : stack1.total); 554 | stack2 = helpers['if']; 555 | tmp1 = self.program(1, program1, data); 556 | tmp1.hash = {}; 557 | tmp1.fn = tmp1; 558 | tmp1.inverse = self.noop; 559 | stack1 = stack2.call(depth0, stack1, tmp1); 560 | if(stack1 || stack1 === 0) { buffer += stack1; } 561 | buffer += "\n\n"; 562 | foundHelper = helpers.stats; 563 | stack1 = foundHelper || depth0.stats; 564 | stack1 = (stack1 === null || stack1 === undefined || stack1 === false ? stack1 : stack1.done); 565 | stack2 = helpers['if']; 566 | tmp1 = self.program(4, program4, data); 567 | tmp1.hash = {}; 568 | tmp1.fn = tmp1; 569 | tmp1.inverse = self.noop; 570 | stack1 = stack2.call(depth0, stack1, tmp1); 571 | if(stack1 || stack1 === 0) { buffer += stack1; } 572 | buffer += "\n"; 573 | return buffer;}); 574 | }}); 575 | 576 | window.require.define({"views/templates/todo": function(exports, require, module) { 577 | module.exports = Handlebars.template(function (Handlebars,depth0,helpers,partials,data) { 578 | helpers = helpers || Handlebars.helpers; 579 | var buffer = "", stack1, stack2, foundHelper, tmp1, self=this, functionType="function", helperMissing=helpers.helperMissing, undef=void 0, escapeExpression=this.escapeExpression; 580 | 581 | function program1(depth0,data) { 582 | 583 | 584 | return "done";} 585 | 586 | function program3(depth0,data) { 587 | 588 | 589 | return "checked=\"checked\"";} 590 | 591 | buffer += "
\n
\n "; 614 | foundHelper = helpers.todo; 615 | stack1 = foundHelper || depth0.todo; 616 | stack1 = (stack1 === null || stack1 === undefined || stack1 === false ? stack1 : stack1.content); 617 | if(typeof stack1 === functionType) { stack1 = stack1.call(depth0, { hash: {} }); } 618 | else if(stack1=== undef) { stack1 = helperMissing.call(depth0, "todo.content", { hash: {} }); } 619 | buffer += escapeExpression(stack1) + "
\n \n
\n
\n \n
\n\n"; 626 | return buffer;}); 627 | }}); 628 | 629 | window.require.define({"views/templates/todos": function(exports, require, module) { 630 | module.exports = Handlebars.template(function (Handlebars,depth0,helpers,partials,data) { 631 | helpers = helpers || Handlebars.helpers; 632 | var foundHelper, self=this; 633 | 634 | 635 | return "\n";}); 636 | }}); 637 | 638 | window.require.define({"views/todo_view": function(exports, require, module) { 639 | (function() { 640 | var TodoView, View, template, 641 | __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }, 642 | __hasProp = Object.prototype.hasOwnProperty, 643 | __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; }; 644 | 645 | View = require('./view'); 646 | 647 | template = require('./templates/todo'); 648 | 649 | module.exports = TodoView = (function(_super) { 650 | 651 | __extends(TodoView, _super); 652 | 653 | function TodoView() { 654 | this.update = __bind(this.update, this); 655 | TodoView.__super__.constructor.apply(this, arguments); 656 | } 657 | 658 | TodoView.prototype.template = template; 659 | 660 | TodoView.prototype.tagName = 'li'; 661 | 662 | TodoView.prototype.events = { 663 | 'click .check': 'toggleDone', 664 | 'dblclick .todo-content': 'edit', 665 | 'click .todo-destroy': 'clear', 666 | 'keypress .todo-input': 'updateOnEnter' 667 | }; 668 | 669 | TodoView.prototype.initialize = function() { 670 | this.model.bind('change', this.render); 671 | return this.model.view = this; 672 | }; 673 | 674 | TodoView.prototype.getRenderData = function() { 675 | return { 676 | todo: this.model.toJSON() 677 | }; 678 | }; 679 | 680 | TodoView.prototype.afterRender = function() { 681 | return this.$('.todo-input').bind('blur', this.update); 682 | }; 683 | 684 | TodoView.prototype.toggleDone = function() { 685 | return this.model.toggle(); 686 | }; 687 | 688 | TodoView.prototype.edit = function() { 689 | this.$el.addClass('editing'); 690 | return $('.todo-input').focus(); 691 | }; 692 | 693 | TodoView.prototype.update = function() { 694 | this.model.save({ 695 | content: this.$('.todo-input').val() 696 | }); 697 | return this.$el.removeClass('editing'); 698 | }; 699 | 700 | TodoView.prototype.updateOnEnter = function(event) { 701 | if (event.keyCode === 13) return this.update(); 702 | }; 703 | 704 | TodoView.prototype.remove = function() { 705 | return this.$el.remove(); 706 | }; 707 | 708 | TodoView.prototype.clear = function() { 709 | return this.model.clear(); 710 | }; 711 | 712 | return TodoView; 713 | 714 | })(View); 715 | 716 | }).call(this); 717 | 718 | }}); 719 | 720 | window.require.define({"views/todos_view": function(exports, require, module) { 721 | (function() { 722 | var TodoView, TodosView, View, application, template, 723 | __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }, 724 | __hasProp = Object.prototype.hasOwnProperty, 725 | __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; }; 726 | 727 | View = require('./view'); 728 | 729 | TodoView = require('./todo_view'); 730 | 731 | application = require('application'); 732 | 733 | template = require('./templates/todos'); 734 | 735 | module.exports = TodosView = (function(_super) { 736 | 737 | __extends(TodosView, _super); 738 | 739 | function TodosView() { 740 | this.renderStats = __bind(this.renderStats, this); 741 | this.addAll = __bind(this.addAll, this); 742 | this.addOne = __bind(this.addOne, this); 743 | TodosView.__super__.constructor.apply(this, arguments); 744 | } 745 | 746 | TodosView.prototype.template = template; 747 | 748 | TodosView.prototype.id = 'todos-view'; 749 | 750 | TodosView.prototype.addOne = function(todo) { 751 | var view; 752 | view = new TodoView({ 753 | model: todo 754 | }); 755 | return this.$el.find('#todos').append(view.render().el); 756 | }; 757 | 758 | TodosView.prototype.addAll = function() { 759 | return application.todos.each(this.addOne); 760 | }; 761 | 762 | TodosView.prototype.initialize = function() { 763 | application.todos.bind('add', this.addOne); 764 | application.todos.bind('reset', this.addAll); 765 | return application.todos.bind('all', this.renderStats); 766 | }; 767 | 768 | TodosView.prototype.renderStats = function() { 769 | return application.statsView.render(); 770 | }; 771 | 772 | return TodosView; 773 | 774 | })(View); 775 | 776 | }).call(this); 777 | 778 | }}); 779 | 780 | window.require.define({"views/user": function(exports, require, module) { 781 | (function() { 782 | var User, View, 783 | __hasProp = Object.prototype.hasOwnProperty, 784 | __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; }; 785 | 786 | View = require('./view'); 787 | 788 | module.exports = User = (function(_super) { 789 | 790 | __extends(User, _super); 791 | 792 | function User() { 793 | User.__super__.constructor.apply(this, arguments); 794 | } 795 | 796 | return User; 797 | 798 | })(View); 799 | 800 | }).call(this); 801 | 802 | }}); 803 | 804 | window.require.define({"views/view": function(exports, require, module) { 805 | (function() { 806 | var View, 807 | __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }, 808 | __hasProp = Object.prototype.hasOwnProperty, 809 | __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; }; 810 | 811 | require('lib/view_helper'); 812 | 813 | module.exports = View = (function(_super) { 814 | 815 | __extends(View, _super); 816 | 817 | function View() { 818 | this.render = __bind(this.render, this); 819 | View.__super__.constructor.apply(this, arguments); 820 | } 821 | 822 | View.prototype.template = function() {}; 823 | 824 | View.prototype.getRenderData = function() {}; 825 | 826 | View.prototype.render = function() { 827 | console.debug("Rendering " + this.constructor.name); 828 | this.$el.html(this.template(this.getRenderData())); 829 | this.afterRender(); 830 | return this; 831 | }; 832 | 833 | View.prototype.afterRender = function() {}; 834 | 835 | return View; 836 | 837 | })(Backbone.View); 838 | 839 | }).call(this); 840 | 841 | }}); 842 | 843 | -------------------------------------------------------------------------------- /public/stylesheets/app.css: -------------------------------------------------------------------------------- 1 | html { 2 | background: #a1afc9; 3 | } 4 | body { 5 | font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; 6 | font-size: 14px; 7 | line-height: 1.4em; 8 | background: #a1afc9; 9 | color: #000; 10 | } 11 | ul { 12 | list-style-type: none; 13 | } 14 | #todo-app { 15 | width: 480px; 16 | margin: 0 auto 40px; 17 | background: #fff; 18 | padding: 20px; 19 | -webkit-box-shadow: rgba(0,0,0,0.20) 0 5px 6px 0; 20 | -moz-box-shadow: rgba(0,0,0,0.20) 0 5px 6px 0; 21 | box-shadow: rgba(0,0,0,0.20) 0 5px 6px 0; 22 | } 23 | #todo-app h1 { 24 | font-size: 36px; 25 | font-weight: bold; 26 | text-align: center; 27 | padding: 20px 0 30px 0; 28 | line-height: 1; 29 | } 30 | #new-todo-view { 31 | position: relative; 32 | } 33 | #new-todo-view input { 34 | width: 466px; 35 | font-size: 24px; 36 | font-family: inherit; 37 | line-height: 1.4em; 38 | border: 1px solid #999; 39 | outline: none; 40 | padding: 6px; 41 | -webkit-box-shadow: rgba(0,0,0,0.20) 0 1px 2px 0 inset; 42 | -moz-box-shadow: rgba(0,0,0,0.20) 0 1px 2px 0 inset; 43 | box-shadow: rgba(0,0,0,0.20) 0 1px 2px 0 inset; 44 | } 45 | #new-todo-view input::-webkit-input-placeholder { 46 | font-style: italic; 47 | } 48 | #new-todo-view .ui-tooltip-top { 49 | display: none; 50 | position: absolute; 51 | z-index: 999; 52 | width: 170px; 53 | left: 50%; 54 | margin-left: -85px; 55 | } 56 | #todos { 57 | margin-top: 10px; 58 | padding-left: 0; 59 | } 60 | #todos li { 61 | padding: 12px 20px 11px 0; 62 | position: relative; 63 | font-size: 24px; 64 | line-height: 1.1em; 65 | border-bottom: 1px solid #ccc; 66 | } 67 | #todos li:after { 68 | content: "\0020"; 69 | display: block; 70 | height: 0; 71 | clear: both; 72 | overflow: hidden; 73 | visibility: hidden; 74 | } 75 | #todos li.editing { 76 | padding: 0; 77 | border-bottom: 0; 78 | width: 280px; 79 | } 80 | #todos .editing .display, 81 | #todos .edit { 82 | display: none; 83 | } 84 | #todos .editing .edit { 85 | display: block; 86 | } 87 | #todos .editing input { 88 | width: 444px; 89 | font-size: 24px; 90 | font-family: inherit; 91 | margin: 0; 92 | line-height: 1.6em; 93 | border: 1px solid #999; 94 | outline: none; 95 | padding: 10px 7px 0px 27px; 96 | -webkit-box-shadow: rgba(0,0,0,0.20) 0 1px 2px 0 inset; 97 | -moz-box-shadow: rgba(0,0,0,0.20) 0 1px 2px 0 inset; 98 | box-shadow: rgba(0,0,0,0.20) 0 1px 2px 0 inset; 99 | } 100 | #todos .check { 101 | position: relative; 102 | top: 9px; 103 | margin: 0 10px 0 7px; 104 | float: left; 105 | } 106 | #todos .done .todo-content { 107 | text-decoration: line-through; 108 | color: #777; 109 | } 110 | #todos .todo-destroy { 111 | position: absolute; 112 | right: 5px; 113 | top: 14px; 114 | display: none; 115 | cursor: pointer; 116 | width: 20px; 117 | height: 20px; 118 | background: url("../images/destroy.png") no-repeat 0 0; 119 | } 120 | #todos li:hover .todo-destroy { 121 | display: block; 122 | } 123 | #todos .todo-destroy:hover { 124 | background-position: 0 -20px; 125 | } 126 | #stats-view:after { 127 | content: "\0020"; 128 | display: block; 129 | height: 0; 130 | clear: both; 131 | overflow: hidden; 132 | visibility: hidden; 133 | } 134 | #stats-view { 135 | margin-top: 10px; 136 | color: #777; 137 | } 138 | #stats-view .todo-count { 139 | float: left; 140 | } 141 | #stats-view .todo-count .number { 142 | font-weight: bold; 143 | color: #333; 144 | } 145 | #stats-view .todo-clear { 146 | float: right; 147 | text-decoration: underline; 148 | cursor: pointer; 149 | } 150 | #stats-view .todo-clear a { 151 | color: #777; 152 | font-size: 12px; 153 | } 154 | #stats-view .todo-clear a:visited { 155 | color: #777; 156 | } 157 | #stats-view .todo-clear a:hover { 158 | color: #369; 159 | } 160 | #instructions { 161 | width: 520px; 162 | margin: 10px auto; 163 | padding-left: 0; 164 | color: #777; 165 | text-shadow: rgba(200,210,230,0.80) 0 1px 0; 166 | text-align: center; 167 | } 168 | #instructions a { 169 | color: #369; 170 | } 171 | #credits { 172 | width: 520px; 173 | margin: 30px auto; 174 | color: #333; 175 | text-shadow: rgba(200,210,230,0.80) 0 1px 0; 176 | text-align: center; 177 | } 178 | #credits span, 179 | #credits a { 180 | display: block; 181 | } 182 | #credits a { 183 | color: #555; 184 | } 185 | .ui-tooltip:after, 186 | .ui-tooltip-top:after, 187 | .ui-tooltip-right:after, 188 | .ui-tooltip-bottom:after, 189 | .ui-tooltip-left:after { 190 | content: "\25B8"; 191 | display: block; 192 | font-size: 2em; 193 | height: 0; 194 | line-height: 0; 195 | position: absolute; 196 | } 197 | .ui-tooltip:after, 198 | .ui-tooltip-bottom:after { 199 | color: #2a2a2a; 200 | bottom: 0; 201 | left: 1px; 202 | text-align: center; 203 | text-shadow: 1px 0 2px #000; 204 | -webkit-transform: rotate(90deg); 205 | -moz-transform: rotate(90deg); 206 | -o-transform: rotate(90deg); 207 | -ms-transform: rotate(90deg); 208 | transform: rotate(90deg); 209 | width: 100%; 210 | } 211 | .ui-tooltip-top:after { 212 | bottom: auto; 213 | color: #4f4f4f; 214 | left: -2px; 215 | top: 0; 216 | text-align: center; 217 | text-shadow: none; 218 | -webkit-transform: rotate(-90deg); 219 | -moz-transform: rotate(-90deg); 220 | -o-transform: rotate(-90deg); 221 | -ms-transform: rotate(-90deg); 222 | transform: rotate(-90deg); 223 | width: 100%; 224 | } 225 | .ui-tooltip-right:after { 226 | color: #222; 227 | right: -0.375em; 228 | top: 50%; 229 | margin-top: -0.05em; 230 | text-shadow: 0 1px 2px #000; 231 | -webkit-transform: rotate(0); 232 | -moz-transform: rotate(0); 233 | -o-transform: rotate(0); 234 | -ms-transform: rotate(0); 235 | transform: rotate(0); 236 | } 237 | .ui-tooltip-left:after { 238 | color: #222; 239 | left: -0.375em; 240 | top: 50%; 241 | margin-top: 0.1em; 242 | text-shadow: 0 -1px 2px #000; 243 | -webkit-transform: rotate(180deg); 244 | -moz-transform: rotate(180deg); 245 | -o-transform: rotate(180deg); 246 | -ms-transform: rotate(180deg); 247 | transform: rotate(180deg); 248 | } 249 | .ui-tooltip, 250 | .ui-tooltip-top, 251 | .ui-tooltip-right, 252 | .ui-tooltip-bottom, 253 | .ui-tooltip-left { 254 | color: #fff; 255 | cursor: normal; 256 | display: inline-block; 257 | font-size: 12px; 258 | font-family: arial; 259 | padding: 0.5em 1em; 260 | position: relative; 261 | text-align: center; 262 | text-shadow: 0 -1px 1px #111; 263 | -webkit-border-radius: 4px; 264 | -moz-border-radius: 4px; 265 | border-radius: 4px; 266 | -webkit-box-shadow: 0 1px 2px #000 , inset 0 0 0 1px #222 , inset 0 2px #666 , inset 0 -2px 2px #444; 267 | -moz-box-shadow: 0 1px 2px #000 , inset 0 0 0 1px #222 , inset 0 2px #666 , inset 0 -2px 2px #444; 268 | box-shadow: 0 1px 2px #000 , inset 0 0 0 1px #222 , inset 0 2px #666 , inset 0 -2px 2px #444; 269 | background-color: #3b3b3b; 270 | background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0, #555), color-stop(1, #222)); 271 | background-image: -webkit-linear-gradient(top, #555 0%, #222 100%); 272 | background-image: -moz-linear-gradient(top, #555 0%, #222 100%); 273 | background-image: -o-linear-gradient(top, #555 0%, #222 100%); 274 | background-image: -ms-linear-gradient(top, #555 0%, #222 100%); 275 | background-image: linear-gradient(top, #555 0%, #222 100%); 276 | } 277 | .ui-tooltip:after, 278 | .ui-tooltip-top:after, 279 | .ui-tooltip-right:after, 280 | .ui-tooltip-bottom:after, 281 | .ui-tooltip-left:after { 282 | content: "\25B8"; 283 | display: block; 284 | font-size: 2em; 285 | height: 0; 286 | line-height: 0; 287 | position: absolute; 288 | } 289 | .ui-tooltip:after, 290 | .ui-tooltip-bottom:after { 291 | color: #2a2a2a; 292 | bottom: 0; 293 | left: 1px; 294 | text-align: center; 295 | text-shadow: 1px 0 2px #000; 296 | -webkit-transform: rotate(90deg); 297 | -moz-transform: rotate(90deg); 298 | -o-transform: rotate(90deg); 299 | -ms-transform: rotate(90deg); 300 | transform: rotate(90deg); 301 | width: 100%; 302 | } 303 | .ui-tooltip-top:after { 304 | bottom: auto; 305 | color: #4f4f4f; 306 | left: -2px; 307 | top: 0; 308 | text-align: center; 309 | text-shadow: none; 310 | -webkit-transform: rotate(-90deg); 311 | -moz-transform: rotate(-90deg); 312 | -o-transform: rotate(-90deg); 313 | -ms-transform: rotate(-90deg); 314 | transform: rotate(-90deg); 315 | width: 100%; 316 | } 317 | .ui-tooltip-right:after { 318 | color: #222; 319 | right: -0.375em; 320 | top: 50%; 321 | margin-top: -0.05em; 322 | text-shadow: 0 1px 2px #000; 323 | -webkit-transform: rotate(0); 324 | -moz-transform: rotate(0); 325 | -o-transform: rotate(0); 326 | -ms-transform: rotate(0); 327 | transform: rotate(0); 328 | } 329 | .ui-tooltip-left:after { 330 | color: #222; 331 | left: -0.375em; 332 | top: 50%; 333 | margin-top: 0.1em; 334 | text-shadow: 0 -1px 2px #000; 335 | -webkit-transform: rotate(180deg); 336 | -moz-transform: rotate(180deg); 337 | -o-transform: rotate(180deg); 338 | -ms-transform: rotate(180deg); 339 | transform: rotate(180deg); 340 | } 341 | 342 | body { 343 | font: 20px/1.5 "Helvetica Neue", Helvetica, Aria;, sans-serif; 344 | padding: 60px 50px; 345 | } 346 | 347 | #mocha h1, h2 { 348 | margin: 0; 349 | } 350 | 351 | #mocha h1 { 352 | font-size: 1em; 353 | font-weight: 200; 354 | } 355 | 356 | #mocha .suite .suite h1 { 357 | font-size: .8em; 358 | } 359 | 360 | #mocha h2 { 361 | font-size: 12px; 362 | font-weight: normal; 363 | cursor: pointer; 364 | } 365 | 366 | #mocha .suite { 367 | margin-left: 15px; 368 | } 369 | 370 | #mocha .test { 371 | margin-left: 15px; 372 | } 373 | 374 | #mocha .test:hover h2::after { 375 | position: relative; 376 | top: 0; 377 | right: -10px; 378 | content: '(view source)'; 379 | font-size: 12px; 380 | color: #888; 381 | } 382 | 383 | #mocha .test.pass::before { 384 | content: '✓'; 385 | font-size: 12px; 386 | display: block; 387 | float: left; 388 | margin-right: 5px; 389 | color: #00c41c; 390 | } 391 | 392 | #mocha .test.pending { 393 | color: #0b97c4; 394 | } 395 | 396 | #mocha .test.pending::before { 397 | content: '◦'; 398 | color: #0b97c4; 399 | } 400 | 401 | #mocha .test.fail { 402 | color: #c00; 403 | } 404 | 405 | #mocha .test.fail pre { 406 | color: black; 407 | } 408 | 409 | #mocha .test.fail::before { 410 | content: '✖'; 411 | font-size: 12px; 412 | display: block; 413 | float: left; 414 | margin-right: 5px; 415 | color: #c00; 416 | } 417 | 418 | #mocha .test pre.error { 419 | color: #c00; 420 | } 421 | 422 | #mocha .test pre { 423 | display: inline-block; 424 | font: 12px/1.5 monaco, monospace; 425 | margin: 5px; 426 | padding: 15px; 427 | border: 1px solid #eee; 428 | border-bottom-color: #ddd; 429 | -webkit-border-radius: 3px; 430 | -webkit-box-shadow: 0 1px 3px #eee; 431 | } 432 | 433 | #error { 434 | color: #c00; 435 | font-size: 1.5 em; 436 | font-weight: 100; 437 | letter-spacing: 1px; 438 | } 439 | 440 | #stats { 441 | position: fixed; 442 | top: 30px; 443 | right: 30px; 444 | font-size: 12px; 445 | margin: 0; 446 | color: #888; 447 | } 448 | 449 | #stats .progress { 450 | margin-bottom: 10px; 451 | } 452 | 453 | #stats em { 454 | color: black; 455 | } 456 | 457 | #stats li { 458 | list-style: none; 459 | } -------------------------------------------------------------------------------- /test/functional/app.coffee8: -------------------------------------------------------------------------------- 1 | describe 'Functional app testing', -> 2 | beforeEach -> 3 | window.location.hash = '' 4 | app.initialize() 5 | Backbone.history.loadUrl() 6 | 7 | afterEach -> 8 | localStorage.clear() 9 | 10 | it 'should add new todo', -> 11 | expect 4 12 | # insert data and press enter 13 | $('#new-todo').val 'bring out the garbage' 14 | testHelpers.keydown $.ui.keyCode.ENTER, '#new-todo' 15 | todo = app.collections.todos.at(0) 16 | 17 | # check for model attributes 18 | equals todo.get('content'), 'bring out the garbage' 19 | equals todo.get('done'), false 20 | 21 | # check for todo entry in dom 22 | todoDOMEntry = $('#todos > li') 23 | equals todoDOMEntry.find('.todo-content').html(), 'bring out the garbage' 24 | equals todoDOMEntry.find('.check').is(':checked'), false 25 | 26 | it 'should add empty todo', -> 27 | expect 4 28 | # press enter on empty input field 29 | $('#new-todo').val '' 30 | testHelpers.keydown $.ui.keyCode.ENTER, '#new-todo' 31 | todo = app.collections.todos.at(0) 32 | 33 | # check for model attributes 34 | equals todo.get('content'), 'empty todo...' 35 | equals todo.get('done'), false 36 | 37 | # check for todo entry in dom 38 | todoDOMEntry = $('#todos > li') 39 | equals todoDOMEntry.find('.todo-content').html(), 'empty todo...' 40 | equals todoDOMEntry.find('.check').is(':checked'), false 41 | 42 | it 'should update todo\'s content', -> 43 | expect 3 44 | testHelpers.createTodo() 45 | todoDOMEntry = $('#todos > li') 46 | # double click and check if edit mode is active 47 | # TODO check for editing should be moved to unit tests of the corresponding view 48 | todoDOMEntry.find('.todo-content').trigger 'dblclick' 49 | equals todoDOMEntry.hasClass('editing'), true 50 | 51 | # update content in todo and save it 52 | todoDOMEntry.find('.todo-input').val 'cleanup dirt from torn garbage bag' 53 | testHelpers.keydown $.ui.keyCode.ENTER, '.todo-input' 54 | 55 | # check for model content 56 | equals app.collections.todos.at(0).get('content'), 'cleanup dirt from torn garbage bag' 57 | 58 | # check for todo entry in dom 59 | equals todoDOMEntry.find('.todo-content').html(), 'cleanup dirt from torn garbage bag' 60 | 61 | it 'should Update todo\'s status', -> 62 | expect 2 63 | testHelpers.createTodo() 64 | todoDOMEntry = $('#todos > li') 65 | 66 | # click on todo's checkbox and check if model changed 67 | todoDOMEntry.find('.check').trigger('click') 68 | equals app.collections.todos.at(0).get('done'), true 69 | todoDOMEntry.find('.check').trigger('click') 70 | equals app.collections.todos.at(0).get('done'), false 71 | 72 | it 'should Delete todo', -> 73 | expect 2 74 | testHelpers.createTodo() 75 | $('#todos > li').find('.todo-destroy').trigger('click') 76 | equals app.collections.todos.length, 0 77 | equals $('#todos').html(), '' 78 | 79 | it 'should Check all stats (total, done and remaining)', -> 80 | expect 5 81 | # create 2 todos and mark one as done 82 | testHelpers.createTodo() 83 | testHelpers.createTodo('answer support request') 84 | $('#todos > li:first').find('.check').trigger('click') 85 | 86 | # check for collection stats 87 | equals app.collections.todos.length, 2 88 | equals app.collections.todos.done().length, 1 89 | equals app.collections.todos.remaining().length, 1 90 | equals $('.todo-count > .number').html(), '1' 91 | equals $('.todo-clear').find('.number-done').html(), '1' 92 | 93 | it 'should Clear todos', -> 94 | expect 2 95 | testHelpers.createTodo() 96 | 97 | # mark todo as done and clear all todos 98 | $('#todos > li').find('.check').trigger('click') 99 | $('.todo-clear > a').trigger('click') 100 | equals app.collections.todos.length, 0 101 | equals $('#todos').html(), '' 102 | -------------------------------------------------------------------------------- /test/helpers.coffee: -------------------------------------------------------------------------------- 1 | this.testHelpers = 2 | keydown: (keyCode, selector) -> 3 | e = $.Event "keypress" 4 | e.keyCode = keyCode 5 | $(selector).trigger('focus').trigger e 6 | 7 | createTodo: (content = 'bring out the garbage') -> 8 | $('#new-todo').val content 9 | testHelpers.keydown $.ui.keyCode.ENTER, '#new-todo' 10 | -------------------------------------------------------------------------------- /test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | es6-shim tests 6 | 7 | 8 | 9 | 10 | 11 | 12 | 18 | 23 | 24 | 25 |
26 | 27 | -------------------------------------------------------------------------------- /test/mocha.css: -------------------------------------------------------------------------------- 1 | 2 | body { 3 | font: 20px/1.5 "Helvetica Neue", Helvetica, Aria;, sans-serif; 4 | padding: 60px 50px; 5 | } 6 | 7 | #mocha h1, h2 { 8 | margin: 0; 9 | } 10 | 11 | #mocha h1 { 12 | font-size: 1em; 13 | font-weight: 200; 14 | } 15 | 16 | #mocha .suite .suite h1 { 17 | font-size: .8em; 18 | } 19 | 20 | #mocha h2 { 21 | font-size: 12px; 22 | font-weight: normal; 23 | cursor: pointer; 24 | } 25 | 26 | #mocha .suite { 27 | margin-left: 15px; 28 | } 29 | 30 | #mocha .test { 31 | margin-left: 15px; 32 | } 33 | 34 | #mocha .test:hover h2::after { 35 | position: relative; 36 | top: 0; 37 | right: -10px; 38 | content: '(view source)'; 39 | font-size: 12px; 40 | color: #888; 41 | } 42 | 43 | #mocha .test.pass::before { 44 | content: '✓'; 45 | font-size: 12px; 46 | display: block; 47 | float: left; 48 | margin-right: 5px; 49 | color: #00c41c; 50 | } 51 | 52 | #mocha .test.pending { 53 | color: #0b97c4; 54 | } 55 | 56 | #mocha .test.pending::before { 57 | content: '◦'; 58 | color: #0b97c4; 59 | } 60 | 61 | #mocha .test.fail { 62 | color: #c00; 63 | } 64 | 65 | #mocha .test.fail pre { 66 | color: black; 67 | } 68 | 69 | #mocha .test.fail::before { 70 | content: '✖'; 71 | font-size: 12px; 72 | display: block; 73 | float: left; 74 | margin-right: 5px; 75 | color: #c00; 76 | } 77 | 78 | #mocha .test pre.error { 79 | color: #c00; 80 | } 81 | 82 | #mocha .test pre { 83 | display: inline-block; 84 | font: 12px/1.5 monaco, monospace; 85 | margin: 5px; 86 | padding: 15px; 87 | border: 1px solid #eee; 88 | border-bottom-color: #ddd; 89 | -webkit-border-radius: 3px; 90 | -webkit-box-shadow: 0 1px 3px #eee; 91 | } 92 | 93 | #error { 94 | color: #c00; 95 | font-size: 1.5 em; 96 | font-weight: 100; 97 | letter-spacing: 1px; 98 | } 99 | 100 | #stats { 101 | position: fixed; 102 | top: 30px; 103 | right: 30px; 104 | font-size: 12px; 105 | margin: 0; 106 | color: #888; 107 | } 108 | 109 | #stats .progress { 110 | margin-bottom: 10px; 111 | } 112 | 113 | #stats em { 114 | color: black; 115 | } 116 | 117 | #stats li { 118 | list-style: none; 119 | } -------------------------------------------------------------------------------- /test/unit/collections/todo_list.coffee: -------------------------------------------------------------------------------- 1 | describe 'TodoList collection', -> 2 | beforeEach -> 3 | app.initialize() 4 | todoList = app.todoList 5 | 6 | afterEach -> 7 | localStorage.clear() 8 | 9 | it 'should check for initialized localstorage', -> 10 | expect(typeof todoList.localStorage).to.equal 'object' 11 | 12 | it 'should get done todos', -> 13 | todoList.create done: yes, content: 'first' 14 | todoList.create done: no, content: 'second' 15 | (expect todoList.done().length).to.equal 1 16 | (expect todoList.done()[0].get 'content').to.equal 'first' 17 | 18 | it 'should get remaining todos', -> 19 | todoList.create done: yes, content: 'first' 20 | todoList.create done: no, content: 'second' 21 | (expect todoList.remaining().length).to.equal 1 22 | (expect todoList.remaining()[0].get 'content').to.equal 'second' 23 | 24 | it '#nextOrder() should return next list entry position', -> 25 | expect(todoList.nextOrder()).to.equal 1 26 | todoList.create order: 1 27 | (expect todoList.nextOrder()).to.equal 2 28 | 29 | it 'should check order', -> 30 | todoList.create content: 'first', order: 2 31 | todoList.create content: 'second', order: 1 32 | (expect todoList.models[0].get('content')).to.equal 'second' 33 | (expect todoList.models[1].get('content')).to.equal 'first' 34 | 35 | it 'should clear all todos', -> 36 | todoList.create done: yes, content: 'first' 37 | todoList.create done: no, content: 'second' 38 | todoList.clearCompleted() 39 | (expect todoList.length).to.equal 1 40 | (expect todoList.models[0].get 'content').to.equal 'second' 41 | -------------------------------------------------------------------------------- /test/unit/models/todo.coffee: -------------------------------------------------------------------------------- 1 | describe 'app.models.Todo', -> 2 | todo = {} 3 | 4 | beforeEach: -> 5 | app.initialize() 6 | @todo = app.todoList.create() 7 | 8 | afterEach: -> 9 | localStorage.clear() 10 | @todo = {} 11 | 12 | it 'todo defaults', -> 13 | (expect @todo.get('done')).to.not.be.ok() 14 | (expect @todo.get('content')).to.equal 'empty todo...' 15 | 16 | it 'todo toggle', -> 17 | @todo.toggle() 18 | (expect @todo.get 'done').to.be.ok() 19 | @todo.toggle() 20 | (expect @todo.get 'done').to.not.be.ok() 21 | 22 | it 'todo clear', -> 23 | view = 24 | remove: -> 25 | ok true 26 | @todo.view = view 27 | @todo.clear() 28 | (expect app.todoList.length).to.equal 0 29 | -------------------------------------------------------------------------------- /test/unit/routers/main.coffee: -------------------------------------------------------------------------------- 1 | describe 'main router', -> 2 | beforeEach -> 3 | window.location.hash = "home" 4 | app.initialize() 5 | 6 | afterEach -> 7 | localStorage.clear() 8 | 9 | describe 'home route', -> 10 | it 'should work', (done) -> 11 | # stub methods of home view and todos 12 | app.views.home = 13 | render: done 14 | app.collections.todos = 15 | fetch: done 16 | Backbone.history.loadUrl() 17 | -------------------------------------------------------------------------------- /test/unit/views/home.coffee: -------------------------------------------------------------------------------- 1 | describe 'app.views.home_view', 2 | beforeEach -> 3 | window.location.hash = "home" 4 | app.initialize() 5 | Backbone.history.loadUrl() 6 | afterEach -> 7 | localStorage.clear() 8 | 9 | test 'render subviews', -> 10 | expect 3 11 | el = app.views.home.render().el 12 | equals $(el).find('#new-todo-view').length, 1 13 | equals $(el).find('#todos-view').length, 1 14 | equals $(el).find('#stats-view').length, 1 15 | -------------------------------------------------------------------------------- /test/unit/views/new_todo.coffee: -------------------------------------------------------------------------------- 1 | describe 'app.views.new_todo_view', 2 | beforeEach -> 3 | window.location.hash = "home" 4 | app.initialize() 5 | Backbone.history.loadUrl() 6 | afterEach -> 7 | localStorage.clear() 8 | 9 | it 'should render view', -> 10 | expect 1 11 | el = app.views.newTodo.render().el 12 | equals $(el).find('#new-todo').length, 1 13 | 14 | it 'should get attributes for new todo', -> 15 | expect 2 16 | $('#new-todo').val('bring out the garbage') 17 | attributes = app.views.newTodo.newAttributes() 18 | equals attributes.order, 1 19 | equals attributes.content, 'bring out the garbage' 20 | 21 | it 'should create new todo', -> 22 | expect 2 23 | $('#new-todo').val('bring out the garbage') 24 | event = 25 | keyCode: $.ui.keyCode.ENTER 26 | app.views.newTodo.createOnEnter(event) 27 | equals $("#new-todo").val(), '' 28 | equals app.todoList.length, 1 29 | 30 | asyncTest "show hint after 1 second", -> 31 | expect 1 32 | $('#new-todo').val('bring out the garbage') 33 | app.views.newTodo.showHint() 34 | setTimeout( -> 35 | equals $(".ui-tooltip-top").css('display'), 'block' 36 | start() 37 | , 1500) 38 | 39 | -------------------------------------------------------------------------------- /test/unit/views/stats.coffee: -------------------------------------------------------------------------------- 1 | describe 'app.views.stats_view', 2 | beforeEach -> 3 | window.location.hash = "home" 4 | app.initialize() 5 | Backbone.history.loadUrl() 6 | afterEach -> 7 | localStorage.clear() 8 | 9 | test 'render view', -> 10 | expect 1 11 | app.todoList.create() 12 | el = app.views.stats.render().el 13 | equals $(el).find('.todo-count').length, 1 14 | 15 | test 'clear completed ', -> 16 | expect 1 17 | app.todoList.clearCompleted = -> 18 | ok true 19 | app.views.stats.clearCompleted() 20 | -------------------------------------------------------------------------------- /test/unit/views/todo.coffee: -------------------------------------------------------------------------------- 1 | describe 'app.views.todo_view', 2 | beforeEach -> 3 | {TodoView} = require 'views/todo_view' 4 | window.location.hash = '' 5 | app.initialize() 6 | Backbone.history.loadUrl() 7 | @todo = app.todoList.create() 8 | @view = new TodoView model: @todo 9 | 10 | afterEach -> 11 | localStorage.clear() 12 | 13 | it 'should initialize view', -> 14 | expect 2 15 | ok @view.model._callbacks.change 16 | ok @view.model.view 17 | 18 | it 'should render view', -> 19 | expect 2 20 | el = @view.render().el 21 | equals $(el).find('.todo-input').length, 1 22 | ok $(el).find('.todo-input').data("events").blur 23 | -------------------------------------------------------------------------------- /test/unit/views/todos.coffee: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brunch/todos/ae93e8a61786de4f6f8ad2f57967eb4898d2fe6d/test/unit/views/todos.coffee -------------------------------------------------------------------------------- /test/unit/views/user.coffee: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brunch/todos/ae93e8a61786de4f6f8ad2f57967eb4898d2fe6d/test/unit/views/user.coffee -------------------------------------------------------------------------------- /vendor/scripts/backbone-0.9.2.js: -------------------------------------------------------------------------------- 1 | // Backbone.js 0.9.2 2 | 3 | // (c) 2010-2012 Jeremy Ashkenas, DocumentCloud Inc. 4 | // Backbone may be freely distributed under the MIT license. 5 | // For all details and documentation: 6 | // http://backbonejs.org 7 | 8 | (function(){ 9 | 10 | // Initial Setup 11 | // ------------- 12 | 13 | // Save a reference to the global object (`window` in the browser, `global` 14 | // on the server). 15 | var root = this; 16 | 17 | // Save the previous value of the `Backbone` variable, so that it can be 18 | // restored later on, if `noConflict` is used. 19 | var previousBackbone = root.Backbone; 20 | 21 | // Create a local reference to slice/splice. 22 | var slice = Array.prototype.slice; 23 | var splice = Array.prototype.splice; 24 | 25 | // The top-level namespace. All public Backbone classes and modules will 26 | // be attached to this. Exported for both CommonJS and the browser. 27 | var Backbone; 28 | if (typeof exports !== 'undefined') { 29 | Backbone = exports; 30 | } else { 31 | Backbone = root.Backbone = {}; 32 | } 33 | 34 | // Current version of the library. Keep in sync with `package.json`. 35 | Backbone.VERSION = '0.9.2'; 36 | 37 | // Require Underscore, if we're on the server, and it's not already present. 38 | var _ = root._; 39 | if (!_ && (typeof require !== 'undefined')) _ = require('underscore'); 40 | 41 | // For Backbone's purposes, jQuery, Zepto, or Ender owns the `$` variable. 42 | var $ = root.jQuery || root.Zepto || root.ender; 43 | 44 | // Set the JavaScript library that will be used for DOM manipulation and 45 | // Ajax calls (a.k.a. the `$` variable). By default Backbone will use: jQuery, 46 | // Zepto, or Ender; but the `setDomLibrary()` method lets you inject an 47 | // alternate JavaScript library (or a mock library for testing your views 48 | // outside of a browser). 49 | Backbone.setDomLibrary = function(lib) { 50 | $ = lib; 51 | }; 52 | 53 | // Runs Backbone.js in *noConflict* mode, returning the `Backbone` variable 54 | // to its previous owner. Returns a reference to this Backbone object. 55 | Backbone.noConflict = function() { 56 | root.Backbone = previousBackbone; 57 | return this; 58 | }; 59 | 60 | // Turn on `emulateHTTP` to support legacy HTTP servers. Setting this option 61 | // will fake `"PUT"` and `"DELETE"` requests via the `_method` parameter and 62 | // set a `X-Http-Method-Override` header. 63 | Backbone.emulateHTTP = false; 64 | 65 | // Turn on `emulateJSON` to support legacy servers that can't deal with direct 66 | // `application/json` requests ... will encode the body as 67 | // `application/x-www-form-urlencoded` instead and will send the model in a 68 | // form param named `model`. 69 | Backbone.emulateJSON = false; 70 | 71 | // Backbone.Events 72 | // ----------------- 73 | 74 | // Regular expression used to split event strings 75 | var eventSplitter = /\s+/; 76 | 77 | // A module that can be mixed in to *any object* in order to provide it with 78 | // custom events. You may bind with `on` or remove with `off` callback functions 79 | // to an event; trigger`-ing an event fires all callbacks in succession. 80 | // 81 | // var object = {}; 82 | // _.extend(object, Backbone.Events); 83 | // object.on('expand', function(){ alert('expanded'); }); 84 | // object.trigger('expand'); 85 | // 86 | var Events = Backbone.Events = { 87 | 88 | // Bind one or more space separated events, `events`, to a `callback` 89 | // function. Passing `"all"` will bind the callback to all events fired. 90 | on: function(events, callback, context) { 91 | 92 | var calls, event, node, tail, list; 93 | if (!callback) return this; 94 | events = events.split(eventSplitter); 95 | calls = this._callbacks || (this._callbacks = {}); 96 | 97 | // Create an immutable callback list, allowing traversal during 98 | // modification. The tail is an empty object that will always be used 99 | // as the next node. 100 | while (event = events.shift()) { 101 | list = calls[event]; 102 | node = list ? list.tail : {}; 103 | node.next = tail = {}; 104 | node.context = context; 105 | node.callback = callback; 106 | calls[event] = {tail: tail, next: list ? list.next : node}; 107 | } 108 | 109 | return this; 110 | }, 111 | 112 | // Remove one or many callbacks. If `context` is null, removes all callbacks 113 | // with that function. If `callback` is null, removes all callbacks for the 114 | // event. If `events` is null, removes all bound callbacks for all events. 115 | off: function(events, callback, context) { 116 | var event, calls, node, tail, cb, ctx; 117 | 118 | // No events, or removing *all* events. 119 | if (!(calls = this._callbacks)) return; 120 | if (!(events || callback || context)) { 121 | delete this._callbacks; 122 | return this; 123 | } 124 | 125 | // Loop through the listed events and contexts, splicing them out of the 126 | // linked list of callbacks if appropriate. 127 | events = events ? events.split(eventSplitter) : _.keys(calls); 128 | while (event = events.shift()) { 129 | node = calls[event]; 130 | delete calls[event]; 131 | if (!node || !(callback || context)) continue; 132 | // Create a new list, omitting the indicated callbacks. 133 | tail = node.tail; 134 | while ((node = node.next) !== tail) { 135 | cb = node.callback; 136 | ctx = node.context; 137 | if ((callback && cb !== callback) || (context && ctx !== context)) { 138 | this.on(event, cb, ctx); 139 | } 140 | } 141 | } 142 | 143 | return this; 144 | }, 145 | 146 | // Trigger one or many events, firing all bound callbacks. Callbacks are 147 | // passed the same arguments as `trigger` is, apart from the event name 148 | // (unless you're listening on `"all"`, which will cause your callback to 149 | // receive the true name of the event as the first argument). 150 | trigger: function(events) { 151 | var event, node, calls, tail, args, all, rest; 152 | if (!(calls = this._callbacks)) return this; 153 | all = calls.all; 154 | events = events.split(eventSplitter); 155 | rest = slice.call(arguments, 1); 156 | 157 | // For each event, walk through the linked list of callbacks twice, 158 | // first to trigger the event, then to trigger any `"all"` callbacks. 159 | while (event = events.shift()) { 160 | if (node = calls[event]) { 161 | tail = node.tail; 162 | while ((node = node.next) !== tail) { 163 | node.callback.apply(node.context || this, rest); 164 | } 165 | } 166 | if (node = all) { 167 | tail = node.tail; 168 | args = [event].concat(rest); 169 | while ((node = node.next) !== tail) { 170 | node.callback.apply(node.context || this, args); 171 | } 172 | } 173 | } 174 | 175 | return this; 176 | } 177 | 178 | }; 179 | 180 | // Aliases for backwards compatibility. 181 | Events.bind = Events.on; 182 | Events.unbind = Events.off; 183 | 184 | // Backbone.Model 185 | // -------------- 186 | 187 | // Create a new model, with defined attributes. A client id (`cid`) 188 | // is automatically generated and assigned for you. 189 | var Model = Backbone.Model = function(attributes, options) { 190 | var defaults; 191 | attributes || (attributes = {}); 192 | if (options && options.parse) attributes = this.parse(attributes); 193 | if (defaults = getValue(this, 'defaults')) { 194 | attributes = _.extend({}, defaults, attributes); 195 | } 196 | if (options && options.collection) this.collection = options.collection; 197 | this.attributes = {}; 198 | this._escapedAttributes = {}; 199 | this.cid = _.uniqueId('c'); 200 | this.changed = {}; 201 | this._silent = {}; 202 | this._pending = {}; 203 | this.set(attributes, {silent: true}); 204 | // Reset change tracking. 205 | this.changed = {}; 206 | this._silent = {}; 207 | this._pending = {}; 208 | this._previousAttributes = _.clone(this.attributes); 209 | this.initialize.apply(this, arguments); 210 | }; 211 | 212 | // Attach all inheritable methods to the Model prototype. 213 | _.extend(Model.prototype, Events, { 214 | 215 | // A hash of attributes whose current and previous value differ. 216 | changed: null, 217 | 218 | // A hash of attributes that have silently changed since the last time 219 | // `change` was called. Will become pending attributes on the next call. 220 | _silent: null, 221 | 222 | // A hash of attributes that have changed since the last `'change'` event 223 | // began. 224 | _pending: null, 225 | 226 | // The default name for the JSON `id` attribute is `"id"`. MongoDB and 227 | // CouchDB users may want to set this to `"_id"`. 228 | idAttribute: 'id', 229 | 230 | // Initialize is an empty function by default. Override it with your own 231 | // initialization logic. 232 | initialize: function(){}, 233 | 234 | // Return a copy of the model's `attributes` object. 235 | toJSON: function(options) { 236 | return _.clone(this.attributes); 237 | }, 238 | 239 | // Get the value of an attribute. 240 | get: function(attr) { 241 | return this.attributes[attr]; 242 | }, 243 | 244 | // Get the HTML-escaped value of an attribute. 245 | escape: function(attr) { 246 | var html; 247 | if (html = this._escapedAttributes[attr]) return html; 248 | var val = this.get(attr); 249 | return this._escapedAttributes[attr] = _.escape(val == null ? '' : '' + val); 250 | }, 251 | 252 | // Returns `true` if the attribute contains a value that is not null 253 | // or undefined. 254 | has: function(attr) { 255 | return this.get(attr) != null; 256 | }, 257 | 258 | // Set a hash of model attributes on the object, firing `"change"` unless 259 | // you choose to silence it. 260 | set: function(key, value, options) { 261 | var attrs, attr, val; 262 | 263 | // Handle both 264 | if (_.isObject(key) || key == null) { 265 | attrs = key; 266 | options = value; 267 | } else { 268 | attrs = {}; 269 | attrs[key] = value; 270 | } 271 | 272 | // Extract attributes and options. 273 | options || (options = {}); 274 | if (!attrs) return this; 275 | if (attrs instanceof Model) attrs = attrs.attributes; 276 | if (options.unset) for (attr in attrs) attrs[attr] = void 0; 277 | 278 | // Run validation. 279 | if (!this._validate(attrs, options)) return false; 280 | 281 | // Check for changes of `id`. 282 | if (this.idAttribute in attrs) this.id = attrs[this.idAttribute]; 283 | 284 | var changes = options.changes = {}; 285 | var now = this.attributes; 286 | var escaped = this._escapedAttributes; 287 | var prev = this._previousAttributes || {}; 288 | 289 | // For each `set` attribute... 290 | for (attr in attrs) { 291 | val = attrs[attr]; 292 | 293 | // If the new and current value differ, record the change. 294 | if (!_.isEqual(now[attr], val) || (options.unset && _.has(now, attr))) { 295 | delete escaped[attr]; 296 | (options.silent ? this._silent : changes)[attr] = true; 297 | } 298 | 299 | // Update or delete the current value. 300 | options.unset ? delete now[attr] : now[attr] = val; 301 | 302 | // If the new and previous value differ, record the change. If not, 303 | // then remove changes for this attribute. 304 | if (!_.isEqual(prev[attr], val) || (_.has(now, attr) != _.has(prev, attr))) { 305 | this.changed[attr] = val; 306 | if (!options.silent) this._pending[attr] = true; 307 | } else { 308 | delete this.changed[attr]; 309 | delete this._pending[attr]; 310 | } 311 | } 312 | 313 | // Fire the `"change"` events. 314 | if (!options.silent) this.change(options); 315 | return this; 316 | }, 317 | 318 | // Remove an attribute from the model, firing `"change"` unless you choose 319 | // to silence it. `unset` is a noop if the attribute doesn't exist. 320 | unset: function(attr, options) { 321 | (options || (options = {})).unset = true; 322 | return this.set(attr, null, options); 323 | }, 324 | 325 | // Clear all attributes on the model, firing `"change"` unless you choose 326 | // to silence it. 327 | clear: function(options) { 328 | (options || (options = {})).unset = true; 329 | return this.set(_.clone(this.attributes), options); 330 | }, 331 | 332 | // Fetch the model from the server. If the server's representation of the 333 | // model differs from its current attributes, they will be overriden, 334 | // triggering a `"change"` event. 335 | fetch: function(options) { 336 | options = options ? _.clone(options) : {}; 337 | var model = this; 338 | var success = options.success; 339 | options.success = function(resp, status, xhr) { 340 | if (!model.set(model.parse(resp, xhr), options)) return false; 341 | if (success) success(model, resp); 342 | }; 343 | options.error = Backbone.wrapError(options.error, model, options); 344 | return (this.sync || Backbone.sync).call(this, 'read', this, options); 345 | }, 346 | 347 | // Set a hash of model attributes, and sync the model to the server. 348 | // If the server returns an attributes hash that differs, the model's 349 | // state will be `set` again. 350 | save: function(key, value, options) { 351 | var attrs, current; 352 | 353 | // Handle both `("key", value)` and `({key: value})` -style calls. 354 | if (_.isObject(key) || key == null) { 355 | attrs = key; 356 | options = value; 357 | } else { 358 | attrs = {}; 359 | attrs[key] = value; 360 | } 361 | options = options ? _.clone(options) : {}; 362 | 363 | // If we're "wait"-ing to set changed attributes, validate early. 364 | if (options.wait) { 365 | if (!this._validate(attrs, options)) return false; 366 | current = _.clone(this.attributes); 367 | } 368 | 369 | // Regular saves `set` attributes before persisting to the server. 370 | var silentOptions = _.extend({}, options, {silent: true}); 371 | if (attrs && !this.set(attrs, options.wait ? silentOptions : options)) { 372 | return false; 373 | } 374 | 375 | // After a successful server-side save, the client is (optionally) 376 | // updated with the server-side state. 377 | var model = this; 378 | var success = options.success; 379 | options.success = function(resp, status, xhr) { 380 | var serverAttrs = model.parse(resp, xhr); 381 | if (options.wait) { 382 | delete options.wait; 383 | serverAttrs = _.extend(attrs || {}, serverAttrs); 384 | } 385 | if (!model.set(serverAttrs, options)) return false; 386 | if (success) { 387 | success(model, resp); 388 | } else { 389 | model.trigger('sync', model, resp, options); 390 | } 391 | }; 392 | 393 | // Finish configuring and sending the Ajax request. 394 | options.error = Backbone.wrapError(options.error, model, options); 395 | var method = this.isNew() ? 'create' : 'update'; 396 | var xhr = (this.sync || Backbone.sync).call(this, method, this, options); 397 | if (options.wait) this.set(current, silentOptions); 398 | return xhr; 399 | }, 400 | 401 | // Destroy this model on the server if it was already persisted. 402 | // Optimistically removes the model from its collection, if it has one. 403 | // If `wait: true` is passed, waits for the server to respond before removal. 404 | destroy: function(options) { 405 | options = options ? _.clone(options) : {}; 406 | var model = this; 407 | var success = options.success; 408 | 409 | var triggerDestroy = function() { 410 | model.trigger('destroy', model, model.collection, options); 411 | }; 412 | 413 | if (this.isNew()) { 414 | triggerDestroy(); 415 | return false; 416 | } 417 | 418 | options.success = function(resp) { 419 | if (options.wait) triggerDestroy(); 420 | if (success) { 421 | success(model, resp); 422 | } else { 423 | model.trigger('sync', model, resp, options); 424 | } 425 | }; 426 | 427 | options.error = Backbone.wrapError(options.error, model, options); 428 | var xhr = (this.sync || Backbone.sync).call(this, 'delete', this, options); 429 | if (!options.wait) triggerDestroy(); 430 | return xhr; 431 | }, 432 | 433 | // Default URL for the model's representation on the server -- if you're 434 | // using Backbone's restful methods, override this to change the endpoint 435 | // that will be called. 436 | url: function() { 437 | var base = getValue(this, 'urlRoot') || getValue(this.collection, 'url') || urlError(); 438 | if (this.isNew()) return base; 439 | return base + (base.charAt(base.length - 1) == '/' ? '' : '/') + encodeURIComponent(this.id); 440 | }, 441 | 442 | // **parse** converts a response into the hash of attributes to be `set` on 443 | // the model. The default implementation is just to pass the response along. 444 | parse: function(resp, xhr) { 445 | return resp; 446 | }, 447 | 448 | // Create a new model with identical attributes to this one. 449 | clone: function() { 450 | return new this.constructor(this.attributes); 451 | }, 452 | 453 | // A model is new if it has never been saved to the server, and lacks an id. 454 | isNew: function() { 455 | return this.id == null; 456 | }, 457 | 458 | // Call this method to manually fire a `"change"` event for this model and 459 | // a `"change:attribute"` event for each changed attribute. 460 | // Calling this will cause all objects observing the model to update. 461 | change: function(options) { 462 | options || (options = {}); 463 | var changing = this._changing; 464 | this._changing = true; 465 | 466 | // Silent changes become pending changes. 467 | for (var attr in this._silent) this._pending[attr] = true; 468 | 469 | // Silent changes are triggered. 470 | var changes = _.extend({}, options.changes, this._silent); 471 | this._silent = {}; 472 | for (var attr in changes) { 473 | this.trigger('change:' + attr, this, this.get(attr), options); 474 | } 475 | if (changing) return this; 476 | 477 | // Continue firing `"change"` events while there are pending changes. 478 | while (!_.isEmpty(this._pending)) { 479 | this._pending = {}; 480 | this.trigger('change', this, options); 481 | // Pending and silent changes still remain. 482 | for (var attr in this.changed) { 483 | if (this._pending[attr] || this._silent[attr]) continue; 484 | delete this.changed[attr]; 485 | } 486 | this._previousAttributes = _.clone(this.attributes); 487 | } 488 | 489 | this._changing = false; 490 | return this; 491 | }, 492 | 493 | // Determine if the model has changed since the last `"change"` event. 494 | // If you specify an attribute name, determine if that attribute has changed. 495 | hasChanged: function(attr) { 496 | if (!arguments.length) return !_.isEmpty(this.changed); 497 | return _.has(this.changed, attr); 498 | }, 499 | 500 | // Return an object containing all the attributes that have changed, or 501 | // false if there are no changed attributes. Useful for determining what 502 | // parts of a view need to be updated and/or what attributes need to be 503 | // persisted to the server. Unset attributes will be set to undefined. 504 | // You can also pass an attributes object to diff against the model, 505 | // determining if there *would be* a change. 506 | changedAttributes: function(diff) { 507 | if (!diff) return this.hasChanged() ? _.clone(this.changed) : false; 508 | var val, changed = false, old = this._previousAttributes; 509 | for (var attr in diff) { 510 | if (_.isEqual(old[attr], (val = diff[attr]))) continue; 511 | (changed || (changed = {}))[attr] = val; 512 | } 513 | return changed; 514 | }, 515 | 516 | // Get the previous value of an attribute, recorded at the time the last 517 | // `"change"` event was fired. 518 | previous: function(attr) { 519 | if (!arguments.length || !this._previousAttributes) return null; 520 | return this._previousAttributes[attr]; 521 | }, 522 | 523 | // Get all of the attributes of the model at the time of the previous 524 | // `"change"` event. 525 | previousAttributes: function() { 526 | return _.clone(this._previousAttributes); 527 | }, 528 | 529 | // Check if the model is currently in a valid state. It's only possible to 530 | // get into an *invalid* state if you're using silent changes. 531 | isValid: function() { 532 | return !this.validate(this.attributes); 533 | }, 534 | 535 | // Run validation against the next complete set of model attributes, 536 | // returning `true` if all is well. If a specific `error` callback has 537 | // been passed, call that instead of firing the general `"error"` event. 538 | _validate: function(attrs, options) { 539 | if (options.silent || !this.validate) return true; 540 | attrs = _.extend({}, this.attributes, attrs); 541 | var error = this.validate(attrs, options); 542 | if (!error) return true; 543 | if (options && options.error) { 544 | options.error(this, error, options); 545 | } else { 546 | this.trigger('error', this, error, options); 547 | } 548 | return false; 549 | } 550 | 551 | }); 552 | 553 | // Backbone.Collection 554 | // ------------------- 555 | 556 | // Provides a standard collection class for our sets of models, ordered 557 | // or unordered. If a `comparator` is specified, the Collection will maintain 558 | // its models in sort order, as they're added and removed. 559 | var Collection = Backbone.Collection = function(models, options) { 560 | options || (options = {}); 561 | if (options.model) this.model = options.model; 562 | if (options.comparator) this.comparator = options.comparator; 563 | this._reset(); 564 | this.initialize.apply(this, arguments); 565 | if (models) this.reset(models, {silent: true, parse: options.parse}); 566 | }; 567 | 568 | // Define the Collection's inheritable methods. 569 | _.extend(Collection.prototype, Events, { 570 | 571 | // The default model for a collection is just a **Backbone.Model**. 572 | // This should be overridden in most cases. 573 | model: Model, 574 | 575 | // Initialize is an empty function by default. Override it with your own 576 | // initialization logic. 577 | initialize: function(){}, 578 | 579 | // The JSON representation of a Collection is an array of the 580 | // models' attributes. 581 | toJSON: function(options) { 582 | return this.map(function(model){ return model.toJSON(options); }); 583 | }, 584 | 585 | // Add a model, or list of models to the set. Pass **silent** to avoid 586 | // firing the `add` event for every new model. 587 | add: function(models, options) { 588 | var i, index, length, model, cid, id, cids = {}, ids = {}, dups = []; 589 | options || (options = {}); 590 | models = _.isArray(models) ? models.slice() : [models]; 591 | 592 | // Begin by turning bare objects into model references, and preventing 593 | // invalid models or duplicate models from being added. 594 | for (i = 0, length = models.length; i < length; i++) { 595 | if (!(model = models[i] = this._prepareModel(models[i], options))) { 596 | throw new Error("Can't add an invalid model to a collection"); 597 | } 598 | cid = model.cid; 599 | id = model.id; 600 | if (cids[cid] || this._byCid[cid] || ((id != null) && (ids[id] || this._byId[id]))) { 601 | dups.push(i); 602 | continue; 603 | } 604 | cids[cid] = ids[id] = model; 605 | } 606 | 607 | // Remove duplicates. 608 | i = dups.length; 609 | while (i--) { 610 | models.splice(dups[i], 1); 611 | } 612 | 613 | // Listen to added models' events, and index models for lookup by 614 | // `id` and by `cid`. 615 | for (i = 0, length = models.length; i < length; i++) { 616 | (model = models[i]).on('all', this._onModelEvent, this); 617 | this._byCid[model.cid] = model; 618 | if (model.id != null) this._byId[model.id] = model; 619 | } 620 | 621 | // Insert models into the collection, re-sorting if needed, and triggering 622 | // `add` events unless silenced. 623 | this.length += length; 624 | index = options.at != null ? options.at : this.models.length; 625 | splice.apply(this.models, [index, 0].concat(models)); 626 | if (this.comparator) this.sort({silent: true}); 627 | if (options.silent) return this; 628 | for (i = 0, length = this.models.length; i < length; i++) { 629 | if (!cids[(model = this.models[i]).cid]) continue; 630 | options.index = i; 631 | model.trigger('add', model, this, options); 632 | } 633 | return this; 634 | }, 635 | 636 | // Remove a model, or a list of models from the set. Pass silent to avoid 637 | // firing the `remove` event for every model removed. 638 | remove: function(models, options) { 639 | var i, l, index, model; 640 | options || (options = {}); 641 | models = _.isArray(models) ? models.slice() : [models]; 642 | for (i = 0, l = models.length; i < l; i++) { 643 | model = this.getByCid(models[i]) || this.get(models[i]); 644 | if (!model) continue; 645 | delete this._byId[model.id]; 646 | delete this._byCid[model.cid]; 647 | index = this.indexOf(model); 648 | this.models.splice(index, 1); 649 | this.length--; 650 | if (!options.silent) { 651 | options.index = index; 652 | model.trigger('remove', model, this, options); 653 | } 654 | this._removeReference(model); 655 | } 656 | return this; 657 | }, 658 | 659 | // Add a model to the end of the collection. 660 | push: function(model, options) { 661 | model = this._prepareModel(model, options); 662 | this.add(model, options); 663 | return model; 664 | }, 665 | 666 | // Remove a model from the end of the collection. 667 | pop: function(options) { 668 | var model = this.at(this.length - 1); 669 | this.remove(model, options); 670 | return model; 671 | }, 672 | 673 | // Add a model to the beginning of the collection. 674 | unshift: function(model, options) { 675 | model = this._prepareModel(model, options); 676 | this.add(model, _.extend({at: 0}, options)); 677 | return model; 678 | }, 679 | 680 | // Remove a model from the beginning of the collection. 681 | shift: function(options) { 682 | var model = this.at(0); 683 | this.remove(model, options); 684 | return model; 685 | }, 686 | 687 | // Get a model from the set by id. 688 | get: function(id) { 689 | if (id == null) return void 0; 690 | return this._byId[id.id != null ? id.id : id]; 691 | }, 692 | 693 | // Get a model from the set by client id. 694 | getByCid: function(cid) { 695 | return cid && this._byCid[cid.cid || cid]; 696 | }, 697 | 698 | // Get the model at the given index. 699 | at: function(index) { 700 | return this.models[index]; 701 | }, 702 | 703 | // Return models with matching attributes. Useful for simple cases of `filter`. 704 | where: function(attrs) { 705 | if (_.isEmpty(attrs)) return []; 706 | return this.filter(function(model) { 707 | for (var key in attrs) { 708 | if (attrs[key] !== model.get(key)) return false; 709 | } 710 | return true; 711 | }); 712 | }, 713 | 714 | // Force the collection to re-sort itself. You don't need to call this under 715 | // normal circumstances, as the set will maintain sort order as each item 716 | // is added. 717 | sort: function(options) { 718 | options || (options = {}); 719 | if (!this.comparator) throw new Error('Cannot sort a set without a comparator'); 720 | var boundComparator = _.bind(this.comparator, this); 721 | if (this.comparator.length == 1) { 722 | this.models = this.sortBy(boundComparator); 723 | } else { 724 | this.models.sort(boundComparator); 725 | } 726 | if (!options.silent) this.trigger('reset', this, options); 727 | return this; 728 | }, 729 | 730 | // Pluck an attribute from each model in the collection. 731 | pluck: function(attr) { 732 | return _.map(this.models, function(model){ return model.get(attr); }); 733 | }, 734 | 735 | // When you have more items than you want to add or remove individually, 736 | // you can reset the entire set with a new list of models, without firing 737 | // any `add` or `remove` events. Fires `reset` when finished. 738 | reset: function(models, options) { 739 | models || (models = []); 740 | options || (options = {}); 741 | for (var i = 0, l = this.models.length; i < l; i++) { 742 | this._removeReference(this.models[i]); 743 | } 744 | this._reset(); 745 | this.add(models, _.extend({silent: true}, options)); 746 | if (!options.silent) this.trigger('reset', this, options); 747 | return this; 748 | }, 749 | 750 | // Fetch the default set of models for this collection, resetting the 751 | // collection when they arrive. If `add: true` is passed, appends the 752 | // models to the collection instead of resetting. 753 | fetch: function(options) { 754 | options = options ? _.clone(options) : {}; 755 | if (options.parse === undefined) options.parse = true; 756 | var collection = this; 757 | var success = options.success; 758 | options.success = function(resp, status, xhr) { 759 | collection[options.add ? 'add' : 'reset'](collection.parse(resp, xhr), options); 760 | if (success) success(collection, resp); 761 | }; 762 | options.error = Backbone.wrapError(options.error, collection, options); 763 | return (this.sync || Backbone.sync).call(this, 'read', this, options); 764 | }, 765 | 766 | // Create a new instance of a model in this collection. Add the model to the 767 | // collection immediately, unless `wait: true` is passed, in which case we 768 | // wait for the server to agree. 769 | create: function(model, options) { 770 | var coll = this; 771 | options = options ? _.clone(options) : {}; 772 | model = this._prepareModel(model, options); 773 | if (!model) return false; 774 | if (!options.wait) coll.add(model, options); 775 | var success = options.success; 776 | options.success = function(nextModel, resp, xhr) { 777 | if (options.wait) coll.add(nextModel, options); 778 | if (success) { 779 | success(nextModel, resp); 780 | } else { 781 | nextModel.trigger('sync', model, resp, options); 782 | } 783 | }; 784 | model.save(null, options); 785 | return model; 786 | }, 787 | 788 | // **parse** converts a response into a list of models to be added to the 789 | // collection. The default implementation is just to pass it through. 790 | parse: function(resp, xhr) { 791 | return resp; 792 | }, 793 | 794 | // Proxy to _'s chain. Can't be proxied the same way the rest of the 795 | // underscore methods are proxied because it relies on the underscore 796 | // constructor. 797 | chain: function () { 798 | return _(this.models).chain(); 799 | }, 800 | 801 | // Reset all internal state. Called when the collection is reset. 802 | _reset: function(options) { 803 | this.length = 0; 804 | this.models = []; 805 | this._byId = {}; 806 | this._byCid = {}; 807 | }, 808 | 809 | // Prepare a model or hash of attributes to be added to this collection. 810 | _prepareModel: function(model, options) { 811 | options || (options = {}); 812 | if (!(model instanceof Model)) { 813 | var attrs = model; 814 | options.collection = this; 815 | model = new this.model(attrs, options); 816 | if (!model._validate(model.attributes, options)) model = false; 817 | } else if (!model.collection) { 818 | model.collection = this; 819 | } 820 | return model; 821 | }, 822 | 823 | // Internal method to remove a model's ties to a collection. 824 | _removeReference: function(model) { 825 | if (this == model.collection) { 826 | delete model.collection; 827 | } 828 | model.off('all', this._onModelEvent, this); 829 | }, 830 | 831 | // Internal method called every time a model in the set fires an event. 832 | // Sets need to update their indexes when models change ids. All other 833 | // events simply proxy through. "add" and "remove" events that originate 834 | // in other collections are ignored. 835 | _onModelEvent: function(event, model, collection, options) { 836 | if ((event == 'add' || event == 'remove') && collection != this) return; 837 | if (event == 'destroy') { 838 | this.remove(model, options); 839 | } 840 | if (model && event === 'change:' + model.idAttribute) { 841 | delete this._byId[model.previous(model.idAttribute)]; 842 | this._byId[model.id] = model; 843 | } 844 | this.trigger.apply(this, arguments); 845 | } 846 | 847 | }); 848 | 849 | // Underscore methods that we want to implement on the Collection. 850 | var methods = ['forEach', 'each', 'map', 'reduce', 'reduceRight', 'find', 851 | 'detect', 'filter', 'select', 'reject', 'every', 'all', 'some', 'any', 852 | 'include', 'contains', 'invoke', 'max', 'min', 'sortBy', 'sortedIndex', 853 | 'toArray', 'size', 'first', 'initial', 'rest', 'last', 'without', 'indexOf', 854 | 'shuffle', 'lastIndexOf', 'isEmpty', 'groupBy']; 855 | 856 | // Mix in each Underscore method as a proxy to `Collection#models`. 857 | _.each(methods, function(method) { 858 | Collection.prototype[method] = function() { 859 | return _[method].apply(_, [this.models].concat(_.toArray(arguments))); 860 | }; 861 | }); 862 | 863 | // Backbone.Router 864 | // ------------------- 865 | 866 | // Routers map faux-URLs to actions, and fire events when routes are 867 | // matched. Creating a new one sets its `routes` hash, if not set statically. 868 | var Router = Backbone.Router = function(options) { 869 | options || (options = {}); 870 | if (options.routes) this.routes = options.routes; 871 | this._bindRoutes(); 872 | this.initialize.apply(this, arguments); 873 | }; 874 | 875 | // Cached regular expressions for matching named param parts and splatted 876 | // parts of route strings. 877 | var namedParam = /:\w+/g; 878 | var splatParam = /\*\w+/g; 879 | var escapeRegExp = /[-[\]{}()+?.,\\^$|#\s]/g; 880 | 881 | // Set up all inheritable **Backbone.Router** properties and methods. 882 | _.extend(Router.prototype, Events, { 883 | 884 | // Initialize is an empty function by default. Override it with your own 885 | // initialization logic. 886 | initialize: function(){}, 887 | 888 | // Manually bind a single named route to a callback. For example: 889 | // 890 | // this.route('search/:query/p:num', 'search', function(query, num) { 891 | // ... 892 | // }); 893 | // 894 | route: function(route, name, callback) { 895 | Backbone.history || (Backbone.history = new History); 896 | if (!_.isRegExp(route)) route = this._routeToRegExp(route); 897 | if (!callback) callback = this[name]; 898 | Backbone.history.route(route, _.bind(function(fragment) { 899 | var args = this._extractParameters(route, fragment); 900 | callback && callback.apply(this, args); 901 | this.trigger.apply(this, ['route:' + name].concat(args)); 902 | Backbone.history.trigger('route', this, name, args); 903 | }, this)); 904 | return this; 905 | }, 906 | 907 | // Simple proxy to `Backbone.history` to save a fragment into the history. 908 | navigate: function(fragment, options) { 909 | Backbone.history.navigate(fragment, options); 910 | }, 911 | 912 | // Bind all defined routes to `Backbone.history`. We have to reverse the 913 | // order of the routes here to support behavior where the most general 914 | // routes can be defined at the bottom of the route map. 915 | _bindRoutes: function() { 916 | if (!this.routes) return; 917 | var routes = []; 918 | for (var route in this.routes) { 919 | routes.unshift([route, this.routes[route]]); 920 | } 921 | for (var i = 0, l = routes.length; i < l; i++) { 922 | this.route(routes[i][0], routes[i][1], this[routes[i][1]]); 923 | } 924 | }, 925 | 926 | // Convert a route string into a regular expression, suitable for matching 927 | // against the current location hash. 928 | _routeToRegExp: function(route) { 929 | route = route.replace(escapeRegExp, '\\$&') 930 | .replace(namedParam, '([^\/]+)') 931 | .replace(splatParam, '(.*?)'); 932 | return new RegExp('^' + route + '$'); 933 | }, 934 | 935 | // Given a route, and a URL fragment that it matches, return the array of 936 | // extracted parameters. 937 | _extractParameters: function(route, fragment) { 938 | return route.exec(fragment).slice(1); 939 | } 940 | 941 | }); 942 | 943 | // Backbone.History 944 | // ---------------- 945 | 946 | // Handles cross-browser history management, based on URL fragments. If the 947 | // browser does not support `onhashchange`, falls back to polling. 948 | var History = Backbone.History = function() { 949 | this.handlers = []; 950 | _.bindAll(this, 'checkUrl'); 951 | }; 952 | 953 | // Cached regex for cleaning leading hashes and slashes . 954 | var routeStripper = /^[#\/]/; 955 | 956 | // Cached regex for detecting MSIE. 957 | var isExplorer = /msie [\w.]+/; 958 | 959 | // Has the history handling already been started? 960 | History.started = false; 961 | 962 | // Set up all inheritable **Backbone.History** properties and methods. 963 | _.extend(History.prototype, Events, { 964 | 965 | // The default interval to poll for hash changes, if necessary, is 966 | // twenty times a second. 967 | interval: 50, 968 | 969 | // Gets the true hash value. Cannot use location.hash directly due to bug 970 | // in Firefox where location.hash will always be decoded. 971 | getHash: function(windowOverride) { 972 | var loc = windowOverride ? windowOverride.location : window.location; 973 | var match = loc.href.match(/#(.*)$/); 974 | return match ? match[1] : ''; 975 | }, 976 | 977 | // Get the cross-browser normalized URL fragment, either from the URL, 978 | // the hash, or the override. 979 | getFragment: function(fragment, forcePushState) { 980 | if (fragment == null) { 981 | if (this._hasPushState || forcePushState) { 982 | fragment = window.location.pathname; 983 | var search = window.location.search; 984 | if (search) fragment += search; 985 | } else { 986 | fragment = this.getHash(); 987 | } 988 | } 989 | if (!fragment.indexOf(this.options.root)) fragment = fragment.substr(this.options.root.length); 990 | return fragment.replace(routeStripper, ''); 991 | }, 992 | 993 | // Start the hash change handling, returning `true` if the current URL matches 994 | // an existing route, and `false` otherwise. 995 | start: function(options) { 996 | if (History.started) throw new Error("Backbone.history has already been started"); 997 | History.started = true; 998 | 999 | // Figure out the initial configuration. Do we need an iframe? 1000 | // Is pushState desired ... is it available? 1001 | this.options = _.extend({}, {root: '/'}, this.options, options); 1002 | this._wantsHashChange = this.options.hashChange !== false; 1003 | this._wantsPushState = !!this.options.pushState; 1004 | this._hasPushState = !!(this.options.pushState && window.history && window.history.pushState); 1005 | var fragment = this.getFragment(); 1006 | var docMode = document.documentMode; 1007 | var oldIE = (isExplorer.exec(navigator.userAgent.toLowerCase()) && (!docMode || docMode <= 7)); 1008 | 1009 | if (oldIE) { 1010 | this.iframe = $('