From a98ecc9024c511c00456f5447a076d50ccf83fea Mon Sep 17 00:00:00 2001 From: Andrei Bocan Date: Wed, 24 Nov 2010 02:22:29 +0200 Subject: [PATCH 01/11] Helpful error messages on rake tasks --- Rakefile | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/Rakefile b/Rakefile index 6d1b8d463..a77c97d48 100644 --- a/Rakefile +++ b/Rakefile @@ -1,10 +1,15 @@ require 'rubygems' -require 'closure-compiler' HEADER = /((^\s*\/\/.*\n)+)/ desc "rebuild the backbone-min.js files for distribution" task :build do + begin + require 'closure-compiler' + rescue LoadError + puts %{closure-compiler not found.\nInstall it by running 'gem install closure-compiler'} + exit + end source = File.read 'backbone.js' header = source.match(HEADER) File.open('backbone-min.js', 'w+') do |file| @@ -14,6 +19,8 @@ end desc "build the docco documentation" task :doc do + check('docco', 'docco', 'https://github.com/jashkenas/docco') + system [ 'docco backbone.js', 'docco examples/todos/todos.js examples/backbone-localstorage.js' @@ -27,5 +34,15 @@ end desc "test the CoffeeScript integration" task :test do + check('coffee', 'CoffeeScript', 'https://github.com/jashkenas/coffee-script.git') + system "coffee test/*.coffee" -end \ No newline at end of file +end + + +def check(exec, name, url) + return unless `which #{exec}`.empty? + + puts "#{name} not found.\nGet it from #{url}" + exit +end From 9994d2bab814cee33d4832d4f9f9324212edd71b Mon Sep 17 00:00:00 2001 From: Jeremy Ashkenas Date: Sat, 27 Nov 2010 10:24:39 -0800 Subject: [PATCH 02/11] merging in zmack's enhanced Rakefile --- Rakefile | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/Rakefile b/Rakefile index a77c97d48..f0b0cf6ad 100644 --- a/Rakefile +++ b/Rakefile @@ -7,7 +7,7 @@ task :build do begin require 'closure-compiler' rescue LoadError - puts %{closure-compiler not found.\nInstall it by running 'gem install closure-compiler'} + puts "closure-compiler not found.\nInstall it by running 'gem install closure-compiler" exit end source = File.read 'backbone.js' @@ -19,12 +19,8 @@ end desc "build the docco documentation" task :doc do - check('docco', 'docco', 'https://github.com/jashkenas/docco') - - system [ - 'docco backbone.js', - 'docco examples/todos/todos.js examples/backbone-localstorage.js' - ].join(' && ') + check 'docco', 'docco', 'https://github.com/jashkenas/docco' + system 'docco backbone.js && docco examples/todos/todos.js examples/backbone-localstorage.js' end desc "run JavaScriptLint on the source" @@ -34,15 +30,13 @@ end desc "test the CoffeeScript integration" task :test do - check('coffee', 'CoffeeScript', 'https://github.com/jashkenas/coffee-script.git') - + check 'coffee', 'CoffeeScript', 'https://github.com/jashkenas/coffee-script.git' system "coffee test/*.coffee" end - +# Check for the existence of an executable. def check(exec, name, url) return unless `which #{exec}`.empty? - - puts "#{name} not found.\nGet it from #{url}" + puts "#{name} not found.\nInstall it from #{url}" exit end From 7ae0384120c2552e1c426cda7fb02fdce6ef1076 Mon Sep 17 00:00:00 2001 From: Jeremy Ashkenas Date: Mon, 29 Nov 2010 12:58:47 -0500 Subject: [PATCH 03/11] first draft of Model#escape --- backbone.js | 22 +++++++++++++++++++++- test/model.js | 12 +++++++++++- 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/backbone.js b/backbone.js index 4e1a8b4e9..a8fbefeff 100644 --- a/backbone.js +++ b/backbone.js @@ -116,6 +116,7 @@ attributes || (attributes = {}); if (this.defaults) attributes = _.extend({}, this.defaults, attributes); this.attributes = {}; + this._escapedAttributes = {}; this.cid = _.uniqueId('c'); this.set(attributes, {silent : true}); this._previousAttributes = _.clone(this.attributes); @@ -147,6 +148,14 @@ return this.attributes[attr]; }, + // Get the HTML-escaped value of an attribute. + escape : function(attr) { + var html; + if (html = this._escapedAttributes[attr]) return html; + var val = this.attributes[attr]; + return this._escapedAttributes[attr] = escapeHTML(val == null ? '' : val); + }, + // Set a hash of model attributes on the object, firing `"change"` unless you // choose to silence it. set : function(attrs, options) { @@ -155,7 +164,7 @@ options || (options = {}); if (!attrs) return this; if (attrs.attributes) attrs = attrs.attributes; - var now = this.attributes; + var now = this.attributes, escaped = this._escapedAttributes; // Run validation. if (!options.silent && this.validate && !this._performValidation(attrs, options)) return false; @@ -168,6 +177,7 @@ var val = attrs[attr]; if (!_.isEqual(now[attr], val)) { now[attr] = val; + delete escaped[attr]; if (!options.silent) { this._changed = true; this.trigger('change:' + attr, this, val); @@ -193,6 +203,7 @@ // Remove the attribute. delete this.attributes[attr]; + delete this._escapedAttributes[attr]; if (!options.silent) { this._changed = true; this.trigger('change:' + attr, this); @@ -213,6 +224,7 @@ if (!options.silent && this.validate && !this._performValidation(validObj, options)) return false; this.attributes = {}; + this._escapedAttributes = {}; if (!options.silent) { this._changed = true; for (attr in old) { @@ -981,4 +993,12 @@ return _.isFunction(object.url) ? object.url() : object.url; }; + // Helper function to escape a string for HTML rendering. + var escapeHTML = function(string) { + return string.replace(/&(?!\w+;)/g, '&') + .replace(//g, '>') + .replace(/"/g, '"'); + }; + })(); diff --git a/test/model.js b/test/model.js index c103b98c2..b37b99a51 100644 --- a/test/model.js +++ b/test/model.js @@ -94,6 +94,16 @@ $(document).ready(function() { equals(doc.get('author'), 'Bill Shakespeare'); }); + test("Model: escape", function() { + equals(doc.escape('title'), 'The Tempest'); + doc.set({audience: 'Bill & Bob'}); + equals(doc.escape('audience'), 'Bill & Bob'); + doc.set({audience: 'Tim > Joan'}); + equals(doc.escape('audience'), 'Tim > Joan'); + doc.unset('audience'); + equals(doc.escape('audience'), ''); + }); + test("Model: set and unset", function() { attrs = { 'foo': 1, 'bar': 2, 'baz': 3}; a = new Backbone.Model(attrs); @@ -151,7 +161,7 @@ $(document).ready(function() { model.change(); equals(model.get('name'), 'Rob'); }); - + test("Model: save within change event", function () { var model = new Backbone.Model({firstName : "Taylor", lastName: "Swift"}); model.bind('change', function () { From 160f0ba9dabefc093b67e83ebb380f18dd55a367 Mon Sep 17 00:00:00 2001 From: Jeremy Ashkenas Date: Mon, 29 Nov 2010 13:33:07 -0500 Subject: [PATCH 04/11] first draft of REST-failure 'error' events. --- backbone.js | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/backbone.js b/backbone.js index a8fbefeff..4ca830a70 100644 --- a/backbone.js +++ b/backbone.js @@ -245,7 +245,7 @@ if (!model.set(model.parse(resp), options)) return false; if (options.success) options.success(model, resp); }; - var error = options.error && _.bind(options.error, null, model); + var error = wrapError(options.error, model); (this.sync || Backbone.sync)('read', this, success, error); return this; }, @@ -261,7 +261,7 @@ if (!model.set(model.parse(resp), options)) return false; if (options.success) options.success(model, resp); }; - var error = options.error && _.bind(options.error, null, model); + var error = wrapError(options.error, model); var method = this.isNew() ? 'create' : 'update'; (this.sync || Backbone.sync)(method, this, success, error); return this; @@ -276,7 +276,7 @@ if (model.collection) model.collection.remove(model); if (options.success) options.success(model, resp); }; - var error = options.error && _.bind(options.error, null, model); + var error = wrapError(options.error, model); (this.sync || Backbone.sync)('delete', this, success, error); return this; }, @@ -483,7 +483,7 @@ collection.refresh(collection.parse(resp)); if (options.success) options.success(collection, resp); }; - var error = options.error && _.bind(options.error, null, collection); + var error = wrapError(options.error, collection); (this.sync || Backbone.sync)('read', this, success, error); return this; }, @@ -993,6 +993,17 @@ return _.isFunction(object.url) ? object.url() : object.url; }; + // Wrap an optional error callback with a fallback error event. + var wrapError = function(onError, model) { + return function(resp) { + if (onError) { + onError(model, resp); + } else { + model.trigger('error', model, resp); + } + }; + }; + // Helper function to escape a string for HTML rendering. var escapeHTML = function(string) { return string.replace(/&(?!\w+;)/g, '&') From fa9a4c879d9d43014eeb603b610c304a276fefbc Mon Sep 17 00:00:00 2001 From: Jeremy Ashkenas Date: Tue, 30 Nov 2010 15:35:43 -0500 Subject: [PATCH 05/11] Passing through the options argument to 'change' events. --- backbone.js | 10 +++++----- test/model.js | 15 ++++++++++++++- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/backbone.js b/backbone.js index 4ca830a70..a77ad53cf 100644 --- a/backbone.js +++ b/backbone.js @@ -186,7 +186,7 @@ } // Fire the `"change"` event, if the model has been changed. - if (!options.silent && this._changed) this.change(); + if (!options.silent && this._changed) this.change(options); return this; }, @@ -207,7 +207,7 @@ if (!options.silent) { this._changed = true; this.trigger('change:' + attr, this); - this.change(); + this.change(options); } return this; }, @@ -230,7 +230,7 @@ for (attr in old) { this.trigger('change:' + attr, this); } - this.change(); + this.change(options); } return this; }, @@ -309,8 +309,8 @@ // Call this method to manually fire a `change` event for this model. // Calling this will cause all objects observing the model to update. - change : function() { - this.trigger('change', this); + change : function(options) { + this.trigger('change', this, options); this._previousAttributes = _.clone(this.attributes); this._changed = false; }, diff --git a/test/model.js b/test/model.js index b37b99a51..eb3efaed9 100644 --- a/test/model.js +++ b/test/model.js @@ -148,7 +148,7 @@ $(document).ready(function() { equals(model.get('two'), null); }); - test("Model: changed, hasChanged, changedAttributes, previous, previousAttributes", function() { + test("Model: change, hasChanged, changedAttributes, previous, previousAttributes", function() { var model = new Backbone.Model({name : "Tim", age : 10}); model.bind('change', function() { ok(model.hasChanged('name'), 'name changed'); @@ -162,6 +162,19 @@ $(document).ready(function() { equals(model.get('name'), 'Rob'); }); + test("Model: change with options", function() { + var value; + var model = new Backbone.Model({name: 'Rob'}); + model.bind('change', function(model, options) { + value = options.prefix + model.get('name'); + }); + model.set({name: 'Bob'}, {silent: true}); + model.change({prefix: 'Mr. '}); + equals(value, 'Mr. Bob'); + model.set({name: 'Sue'}, {prefix: 'Ms. '}); + equals(value, 'Ms. Sue'); + }); + test("Model: save within change event", function () { var model = new Backbone.Model({firstName : "Taylor", lastName: "Swift"}); model.bind('change', function () { From 6e4046df026d44e056af6c5bfa7d7f8b97a56f45 Mon Sep 17 00:00:00 2001 From: Jeremy Ashkenas Date: Tue, 30 Nov 2010 16:04:55 -0500 Subject: [PATCH 06/11] All Backbone events now pass through their options as the ffinal argument. --- backbone.js | 28 ++++++++++++++-------------- test/collection.js | 12 ++++++++---- 2 files changed, 22 insertions(+), 18 deletions(-) diff --git a/backbone.js b/backbone.js index a77ad53cf..313819106 100644 --- a/backbone.js +++ b/backbone.js @@ -180,7 +180,7 @@ delete escaped[attr]; if (!options.silent) { this._changed = true; - this.trigger('change:' + attr, this, val); + this.trigger('change:' + attr, this, val, options); } } } @@ -206,7 +206,7 @@ delete this._escapedAttributes[attr]; if (!options.silent) { this._changed = true; - this.trigger('change:' + attr, this); + this.trigger('change:' + attr, this, void 0, options); this.change(options); } return this; @@ -228,7 +228,7 @@ if (!options.silent) { this._changed = true; for (attr in old) { - this.trigger('change:' + attr, this); + this.trigger('change:' + attr, this, void 0, options); } this.change(options); } @@ -245,7 +245,7 @@ if (!model.set(model.parse(resp), options)) return false; if (options.success) options.success(model, resp); }; - var error = wrapError(options.error, model); + var error = wrapError(options.error, model, options); (this.sync || Backbone.sync)('read', this, success, error); return this; }, @@ -261,7 +261,7 @@ if (!model.set(model.parse(resp), options)) return false; if (options.success) options.success(model, resp); }; - var error = wrapError(options.error, model); + var error = wrapError(options.error, model, options); var method = this.isNew() ? 'create' : 'update'; (this.sync || Backbone.sync)(method, this, success, error); return this; @@ -276,7 +276,7 @@ if (model.collection) model.collection.remove(model); if (options.success) options.success(model, resp); }; - var error = wrapError(options.error, model); + var error = wrapError(options.error, model, options); (this.sync || Backbone.sync)('delete', this, success, error); return this; }, @@ -361,7 +361,7 @@ if (options.error) { options.error(this, error); } else { - this.trigger('error', this, error); + this.trigger('error', this, error, options); } return false; } @@ -453,7 +453,7 @@ options || (options = {}); if (!this.comparator) throw new Error('Cannot sort a set without a comparator'); this.models = this.sortBy(this.comparator); - if (!options.silent) this.trigger('refresh', this); + if (!options.silent) this.trigger('refresh', this, options); return this; }, @@ -470,7 +470,7 @@ options || (options = {}); this._reset(); this.add(models, {silent: true}); - if (!options.silent) this.trigger('refresh', this); + if (!options.silent) this.trigger('refresh', this, options); return this; }, @@ -483,7 +483,7 @@ collection.refresh(collection.parse(resp)); if (options.success) options.success(collection, resp); }; - var error = wrapError(options.error, collection); + var error = wrapError(options.error, collection, options); (this.sync || Backbone.sync)('read', this, success, error); return this; }, @@ -542,7 +542,7 @@ this.models.splice(index, 0, model); model.bind('all', this._boundOnModelEvent); this.length++; - if (!options.silent) model.trigger('add', model, this); + if (!options.silent) model.trigger('add', model, this, options); return model; }, @@ -557,7 +557,7 @@ delete model.collection; this.models.splice(this.indexOf(model), 1); this.length--; - if (!options.silent) model.trigger('remove', model, this); + if (!options.silent) model.trigger('remove', model, this, options); model.unbind('all', this._boundOnModelEvent); return model; }, @@ -994,12 +994,12 @@ }; // Wrap an optional error callback with a fallback error event. - var wrapError = function(onError, model) { + var wrapError = function(onError, model, options) { return function(resp) { if (onError) { onError(model, resp); } else { - model.trigger('error', model, resp); + model.trigger('error', model, resp, options); } }; }; diff --git a/test/collection.js b/test/collection.js index 372e71ad2..4fccb8d81 100644 --- a/test/collection.js +++ b/test/collection.js @@ -53,13 +53,17 @@ $(document).ready(function() { }); test("Collection: add", function() { - var added = null; - col.bind('add', function(model){ added = model.get('label'); }); + var added = opts = null; + col.bind('add', function(model, collection, options){ + added = model.get('label'); + opts = options; + }); e = new Backbone.Model({id: 10, label : 'e'}); - col.add(e); + col.add(e, {amazing: true}); equals(added, 'e'); equals(col.length, 5); equals(col.last(), e); + ok(opts.amazing); }); test("Collection: remove", function() { @@ -70,7 +74,7 @@ $(document).ready(function() { equals(col.length, 4); equals(col.first(), d); }); - + test("Collection: remove in multiple collections", function() { var modelData = { id : 5, From e0cb5ee3b6415028bf6bae716f90582a825f3c0d Mon Sep 17 00:00:00 2001 From: Sam Stephenson Date: Tue, 30 Nov 2010 16:53:21 -0600 Subject: [PATCH 07/11] Zepto support --- backbone.js | 4 +- test/test-zepto.html | 30 +++++ test/vendor/zepto.js | 276 +++++++++++++++++++++++++++++++++++++++++++ test/view.js | 6 +- 4 files changed, 311 insertions(+), 5 deletions(-) create mode 100644 test/test-zepto.html create mode 100644 test/vendor/zepto.js diff --git a/backbone.js b/backbone.js index 4ca830a70..ba4988442 100644 --- a/backbone.js +++ b/backbone.js @@ -26,7 +26,7 @@ if (!_ && (typeof require !== 'undefined')) _ = require("underscore")._; // For Backbone's purposes, jQuery owns the `$` variable. - var $ = this.jQuery; + var $ = this.jQuery || this.Zepto; // Turn on `emulateHTTP` to use support legacy HTTP servers. Setting this option will // fake `"PUT"` and `"DELETE"` requests via the `_method` parameter and set a @@ -861,7 +861,7 @@ if (this.el) return; var attrs = {}; if (this.id) attrs.id = this.id; - if (this.className) attrs.className = this.className; + if (this.className) attrs["class"] = this.className; this.el = this.make(this.tagName, attrs); } diff --git a/test/test-zepto.html b/test/test-zepto.html new file mode 100644 index 000000000..c9f54877c --- /dev/null +++ b/test/test-zepto.html @@ -0,0 +1,30 @@ + + + + Backbone Test Suite + + + + + + + + + + + + + + + + + +

Backbone Test Suite

+

+

+
    +

    +

    Backbone Speed Suite

    +
    + + diff --git a/test/vendor/zepto.js b/test/vendor/zepto.js new file mode 100644 index 000000000..59de84a95 --- /dev/null +++ b/test/vendor/zepto.js @@ -0,0 +1,276 @@ +var Zepto = (function() { + var slice=[].slice, d=document, + ADJ_OPS={append: 'beforeEnd', prepend: 'afterBegin', before: 'beforeBegin', after: 'afterEnd'}, + e, k, css, un, $$; + + // fix for iOS 3.2 + if(String.prototype.trim === un) + String.prototype.trim = function(){ return this.replace(/^\s+/, '').replace(/\s+$/, '') }; + + function classRE(name){ return new RegExp("(^|\\s)"+name+"(\\s|$)") } + function compact(array){ return array.filter(function(el){ return el !== un && el !== null }) } + + function Z(dom, _){ this.dom = dom || []; this.selector = _ || '' } + Z.prototype = $.fn; + + function $(_, context){ + return _ == d ? new Z : (context !== un) ? $(context).find(_) : new Z(compact(_ instanceof Z ? _.dom : (_ instanceof Array ? _ : (_ instanceof Element || _ === window ? [_] : $$(d, _)))), _); + } + + $.extend = function(target, src){ for(k in src) target[k] = src[k] } + $.qsa = $$ = function(el, selector){ return slice.call(el.querySelectorAll(selector)) } + camelize = function(str){ return str.replace(/-+(.)?/g, function(match, chr){ return chr ? chr.toUpperCase() : '' }) } + + $.fn = { + ready: function(callback){ + d.addEventListener('DOMContentLoaded', callback, false); return this; + }, + compact: function(){ this.dom=compact(this.dom); return this }, + get: function(idx){ return idx === un ? this.dom : this.dom[idx] }, + remove: function(){ + return this.each(function(el){ el.parentNode.removeChild(el) }); + }, + each: function(callback){ this.dom.forEach(callback); return this }, + filter: function(selector){ + return $(this.dom.filter(function(el){ return $$(el.parentNode, selector).indexOf(el)>=0; })); + }, + is: function(selector){ + return this.dom.length>0 && $(this.dom[0]).filter(selector).dom.length>0; + }, + first: function(callback){ this.dom=compact([this.dom[0]]); return this }, + find: function(selector){ + return $(this.dom.map(function(el){ return $$(el, selector) }).reduce(function(a,b){ return a.concat(b) }, [])); + }, + closest: function(selector){ + var el = this.dom[0].parentNode, nodes = $$(d, selector); + while(el && nodes.indexOf(el)<0) el = el.parentNode; + return $(el && !(el===d) ? el : []); + }, + pluck: function(property){ return this.dom.map(function(el){ return el[property] }) }, + show: function(){ return this.css('display', 'block') }, + hide: function(){ return this.css('display', 'none') }, + prev: function(){ return $(this.pluck('previousElementSibling')) }, + next: function(){ return $(this.pluck('nextElementSibling')) }, + html: function(html){ + return html === un ? + (this.dom.length>0 ? this.dom[0].innerHTML : null) : + this.each(function(el){ el.innerHTML = html }); + }, + text: function(text){ + return text === un ? + (this.dom.length>0 ? this.dom[0].innerText : null) : + this.each(function(el){ el.innerText = text }); + }, + attr: function(name,value){ + return (typeof name == 'string' && value === un) ? + (this.dom.length>0 ? this.dom[0].getAttribute(name) || undefined : null) : + this.each(function(el){ + if (typeof name == 'object') for(k in name) el.setAttribute(k, name[k]) + else el.setAttribute(name,value); + }); + }, + offset: function(){ + var obj = this.dom[0].getBoundingClientRect(); + return { left: obj.left+d.body.scrollLeft, top: obj.top+d.body.scrollTop, width: obj.width, height: obj.height }; + }, + css: function(prop, value){ + if(value === un && typeof prop == 'string') return this.dom[0].style[camelize(prop)]; + css=""; for(k in prop) css += k+':'+prop[k]+';'; + if(typeof prop == 'string') css = prop+":"+value; + return this.each(function(el) { el.style.cssText += ';' + css }); + }, + index: function(el){ + return this.dom.indexOf($(el).get(0)); + }, + hasClass: function(name){ + return classRE(name).test(this.dom[0].className); + }, + addClass: function(name){ + return this.each(function(el){ !$(el).hasClass(name) && (el.className += (el.className ? ' ' : '') + name) }); + }, + removeClass: function(name){ + return this.each(function(el){ el.className = el.className.replace(classRE(name), ' ').trim() }); + } + }; + + ['width','height'].forEach(function(m){ $.fn[m] = function(){ return this.offset()[m] }}); + + for(k in ADJ_OPS) + $.fn[k] = (function(op){ + return function(html){ return this.each(function(el){ + el['insertAdjacent' + (html instanceof Element ? 'Element' : 'HTML')](op,html) + })}; + })(ADJ_OPS[k]); + + Z.prototype = $.fn; + + return $; +})(); + +'$' in window||(window.$=Zepto); +(function($){ + var d=document, $$=$.qsa, handlers=[]; + function find(el, ev, fn) { + return handlers.filter(function(handler){ + return handler && handler.el===el && (!ev || handler.ev===ev) && (!fn || handler.fn===fn); + }); + } + $.event = { + add: function(el, events, fn){ + events.split(/\s/).forEach(function(ev){ + var handler = {ev: ev, el: el, fn: fn, i: handlers.length}; + handlers.push(handler); + el.addEventListener(ev, fn, false); + }); + }, + remove: function(el, events, fn){ + (events||'').split(/\s/).forEach(function(ev){ + find(el, ev, fn).forEach(function(handler){ + handlers[handler.i] = null; + el.removeEventListener(handler.ev, handler.fn, false); + }); + }); + } + }; + $.fn.bind = function(event, callback){ + return this.each(function(el){ $.event.add(el, event, callback) }); + }; + $.fn.unbind = function(event, callback){ + return this.each(function(el){ $.event.remove(el, event, callback) }); + }; + $.fn.delegate = function(selector, event, callback){ + return this.each(function(el){ + $.event.add(el, event, function(event){ + var target = event.target, nodes = $$(el, selector); + while(target && nodes.indexOf(target)<0) target = target.parentNode; + if(target && !(target===el) && !(target===d)) callback.call(target, event); + }, false); + }); + }; + $.fn.live = function(event, callback){ + $(d.body).delegate(this.selector, event, callback); return this; + }; + $.fn.trigger = function(event){ + return this.each(function(el){ var e; el.dispatchEvent(e = d.createEvent('Events'), e.initEvent(event, true, false)) }); + }; +})(Zepto); +(function($){ + function detect(ua){ + var ua = ua, os = {}, + android = ua.match(/(Android)\s+([\d.]+)/), + iphone = ua.match(/(iPhone\sOS)\s([\d_]+)/), + ipad = ua.match(/(iPad).*OS\s([\d_]+)/), + webos = ua.match(/(webOS)\/([\d.]+)/); + if(android) os.android = true, os.version = android[2]; + if(iphone) os.ios = true, os.version = iphone[2].replace(/_/g,'.'), os.iphone = true; + if(ipad) os.ios = true, os.version = ipad[2].replace(/_/g,'.'), os.ipad = true; + if(webos) os.webos = true, os.version = webos[2]; + return os; + } + $.os = detect(navigator.userAgent); + $.__detect = detect; + $.browser = { + webkit: true, + version: navigator.userAgent.match(/WebKit\/([\d.]+)/)[1] + } +})(Zepto); +(function($){ + $.fn.anim = function(props, dur, ease){ + var transforms = [], opacity, k; + for (k in props) k === 'opacity' ? opacity=props[k] : transforms.push(k+'('+props[k]+')'); + return this.css({ '-webkit-transition': 'all '+(dur||0.5)+'s '+(ease||''), + '-webkit-transform': transforms.join(' '), opacity: opacity }); + } +})(Zepto); +(function($){ + var touch={}, touchTimeout; + + function parentIfText(node){ + return 'tagName' in node ? node : node.parentNode; + } + + $(document).ready(function(){ + $(document.body).bind('touchstart', function(e){ + var now = Date.now(), delta = now-(touch.last || now); + touch.target = parentIfText(e.touches[0].target); + touchTimeout && clearTimeout(touchTimeout); + touch.x1 = e.touches[0].pageX; + if (delta > 0 && delta <= 250) touch.isDoubleTap = true; + touch.last = now; + }).bind('touchmove', function(e){ + touch.x2 = e.touches[0].pageX + }).bind('touchend', function(e){ + if (touch.isDoubleTap) { + $(touch.target).trigger('doubleTap'); + touch = {}; + } else if (touch.x2 > 0) { + Math.abs(touch.x1-touch.x2)>30 && $(touch.target).trigger('swipe'); + touch.x1 = touch.x2 = touch.last = 0; + } else if ('last' in touch) { + touchTimeout = setTimeout(function(){ + touchTimeout = null; + $(touch.target).trigger('tap') + touch = {}; + }, 250); + } + }).bind('touchcancel', function(){ touch={} }); + }); + + ['swipe', 'doubleTap', 'tap'].forEach(function(m){ + $.fn[m] = function(callback){ return this.bind(m, callback) } + }); +})(Zepto); +(function($){ + function ajax(method, url, success, data, type){ + data = data || null; + var r = new XMLHttpRequest(); + if (success instanceof Function) { + r.onreadystatechange = function(){ + if(r.readyState==4 && (r.status==200 || r.status==0)) + success(r.responseText); + }; + } + r.open(method,url,true); + if (type) r.setRequestHeader("Accept", type ); + if (data instanceof Object) data = JSON.stringify(data), r.setRequestHeader('Content-Type','application/json'); + r.setRequestHeader('X-Requested-With','XMLHttpRequest'); + r.send(data); + } + + $.get = function(url, success){ ajax('GET', url, success); }; + $.post = function(url, data, success, type){ + if (data instanceof Function) type = type || success, success = data, data = null; + ajax('POST', url, success, data, type); + }; + $.getJSON = function(url, success){ + $.get(url, function(json){ success(JSON.parse(json)) }); + }; + + $.fn.load = function(url, success){ + var self = this, parts = url.split(/\s/), selector; + if(!this.dom.length) return this; + if(parts.length>1) url = parts[0], selector = parts[1]; + $.get(url, function(response){ + self.html(selector ? + $(document.createElement('div')).html(response).find(selector).html() + : response); + success && success(); + }); + return this; + }; +})(Zepto); +(function($){ + var cache = [], timeout; + + $.fn.remove = function(){ + return this.each(function(el){ + if(el.tagName=='IMG'){ + cache.push(el); + el.src = 'data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs='; + if(timeout) clearTimeout(timeout); + timeout = setTimeout(function(){ cache = [] }, 60000); + } + el.parentNode.removeChild(el); + }); + } +})(Zepto); diff --git a/test/view.js b/test/view.js index 2f343e2af..29adc789f 100644 --- a/test/view.js +++ b/test/view.js @@ -16,8 +16,8 @@ $(document).ready(function() { test("View: jQuery", function() { view.el = document.body; - equals(view.$('#qunit-header')[0].innerHTML, 'Backbone Test Suite'); - equals(view.$('#qunit-header')[1].innerHTML, 'Backbone Speed Suite'); + equals(view.$('#qunit-header').get(0).innerHTML, 'Backbone Test Suite'); + equals(view.$('#qunit-header').get(1).innerHTML, 'Backbone Speed Suite'); }); test("View: make", function() { @@ -62,4 +62,4 @@ $(document).ready(function() { equals(view.el, document.body); }); -}); \ No newline at end of file +}); From de44d9ec349904e867104b5e39e939e0ef4740bc Mon Sep 17 00:00:00 2001 From: Sam Stephenson Date: Wed, 1 Dec 2010 11:02:08 -0600 Subject: [PATCH 08/11] Update to Zepto 0.2 --- test/test-zepto.html | 2 +- test/vendor/{zepto.js => zepto-0.2.js} | 28 +++++++++++++------------- 2 files changed, 15 insertions(+), 15 deletions(-) rename test/vendor/{zepto.js => zepto-0.2.js} (97%) diff --git a/test/test-zepto.html b/test/test-zepto.html index c9f54877c..44ce2a2ee 100644 --- a/test/test-zepto.html +++ b/test/test-zepto.html @@ -4,7 +4,7 @@ Backbone Test Suite - + diff --git a/test/vendor/zepto.js b/test/vendor/zepto-0.2.js similarity index 97% rename from test/vendor/zepto.js rename to test/vendor/zepto-0.2.js index 59de84a95..306342bb2 100644 --- a/test/vendor/zepto.js +++ b/test/vendor/zepto-0.2.js @@ -22,7 +22,7 @@ var Zepto = (function() { camelize = function(str){ return str.replace(/-+(.)?/g, function(match, chr){ return chr ? chr.toUpperCase() : '' }) } $.fn = { - ready: function(callback){ + ready: function(callback){ d.addEventListener('DOMContentLoaded', callback, false); return this; }, compact: function(){ this.dom=compact(this.dom); return this }, @@ -34,7 +34,7 @@ var Zepto = (function() { filter: function(selector){ return $(this.dom.filter(function(el){ return $$(el.parentNode, selector).indexOf(el)>=0; })); }, - is: function(selector){ + is: function(selector){ return this.dom.length>0 && $(this.dom[0]).filter(selector).dom.length>0; }, first: function(callback){ this.dom=compact([this.dom[0]]); return this }, @@ -52,8 +52,8 @@ var Zepto = (function() { prev: function(){ return $(this.pluck('previousElementSibling')) }, next: function(){ return $(this.pluck('nextElementSibling')) }, html: function(html){ - return html === un ? - (this.dom.length>0 ? this.dom[0].innerHTML : null) : + return html === un ? + (this.dom.length>0 ? this.dom[0].innerHTML : null) : this.each(function(el){ el.innerHTML = html }); }, text: function(text){ @@ -62,7 +62,7 @@ var Zepto = (function() { this.each(function(el){ el.innerText = text }); }, attr: function(name,value){ - return (typeof name == 'string' && value === un) ? + return (typeof name == 'string' && value === un) ? (this.dom.length>0 ? this.dom[0].getAttribute(name) || undefined : null) : this.each(function(el){ if (typeof name == 'object') for(k in name) el.setAttribute(k, name[k]) @@ -92,7 +92,7 @@ var Zepto = (function() { return this.each(function(el){ el.className = el.className.replace(classRE(name), ' ').trim() }); } }; - + ['width','height'].forEach(function(m){ $.fn[m] = function(){ return this.offset()[m] }}); for(k in ADJ_OPS) @@ -184,11 +184,11 @@ var Zepto = (function() { })(Zepto); (function($){ var touch={}, touchTimeout; - + function parentIfText(node){ return 'tagName' in node ? node : node.parentNode; } - + $(document).ready(function(){ $(document.body).bind('touchstart', function(e){ var now = Date.now(), delta = now-(touch.last || now); @@ -197,8 +197,8 @@ var Zepto = (function() { touch.x1 = e.touches[0].pageX; if (delta > 0 && delta <= 250) touch.isDoubleTap = true; touch.last = now; - }).bind('touchmove', function(e){ - touch.x2 = e.touches[0].pageX + }).bind('touchmove', function(e){ + touch.x2 = e.touches[0].pageX }).bind('touchend', function(e){ if (touch.isDoubleTap) { $(touch.target).trigger('doubleTap'); @@ -215,7 +215,7 @@ var Zepto = (function() { } }).bind('touchcancel', function(){ touch={} }); }); - + ['swipe', 'doubleTap', 'tap'].forEach(function(m){ $.fn[m] = function(callback){ return this.bind(m, callback) } }); @@ -245,7 +245,7 @@ var Zepto = (function() { $.getJSON = function(url, success){ $.get(url, function(json){ success(JSON.parse(json)) }); }; - + $.fn.load = function(url, success){ var self = this, parts = url.split(/\s/), selector; if(!this.dom.length) return this; @@ -261,7 +261,7 @@ var Zepto = (function() { })(Zepto); (function($){ var cache = [], timeout; - + $.fn.remove = function(){ return this.each(function(el){ if(el.tagName=='IMG'){ @@ -272,5 +272,5 @@ var Zepto = (function() { } el.parentNode.removeChild(el); }); - } + } })(Zepto); From 418b77ffffe9366a053d3ed73d8d7f6a204e188e Mon Sep 17 00:00:00 2001 From: Jeremy Ashkenas Date: Wed, 1 Dec 2010 12:55:05 -0500 Subject: [PATCH 09/11] Upgrading test suite to latest Underscore. --- test/test.html | 2 +- ...nderscore-1.1.0.js => underscore-1.1.3.js} | 405 ++++++++++-------- 2 files changed, 221 insertions(+), 186 deletions(-) rename test/vendor/{underscore-1.1.0.js => underscore-1.1.3.js} (64%) diff --git a/test/test.html b/test/test.html index cd43ee2e7..b8adb6cbb 100644 --- a/test/test.html +++ b/test/test.html @@ -7,7 +7,7 @@ - + diff --git a/test/vendor/underscore-1.1.0.js b/test/vendor/underscore-1.1.3.js similarity index 64% rename from test/vendor/underscore-1.1.0.js rename to test/vendor/underscore-1.1.3.js index 60bf47ccd..887d039c3 100644 --- a/test/vendor/underscore-1.1.0.js +++ b/test/vendor/underscore-1.1.3.js @@ -1,37 +1,36 @@ -// Underscore.js -// (c) 2010 Jeremy Ashkenas, DocumentCloud Inc. -// Underscore is freely distributable under the terms of the MIT license. -// Portions of Underscore are inspired by or borrowed from Prototype.js, -// Oliver Steele's Functional, and John Resig's Micro-Templating. -// For all details and documentation: -// http://documentcloud.github.com/underscore +// Underscore.js 1.1.3 +// (c) 2010 Jeremy Ashkenas, DocumentCloud Inc. +// Underscore is freely distributable under the MIT license. +// Portions of Underscore are inspired or borrowed from Prototype, +// Oliver Steele's Functional, and John Resig's Micro-Templating. +// For all details and documentation: +// http://documentcloud.github.com/underscore (function() { - // ------------------------- Baseline setup --------------------------------- - // Establish the root object, "window" in the browser, or "global" on the server. + // Baseline setup + // -------------- + + // Establish the root object, `window` in the browser, or `global` on the server. var root = this; - // Save the previous value of the "_" variable. + // Save the previous value of the `_` variable. var previousUnderscore = root._; - // Establish the object that gets thrown to break out of a loop iteration. - var breaker = typeof StopIteration !== 'undefined' ? StopIteration : '__break__'; - - // Quick regexp-escaping function, because JS doesn't have RegExp.escape(). - var escapeRegExp = function(s) { return s.replace(/([.*+?^${}()|[\]\/\\])/g, '\\$1'); }; + // Establish the object that gets returned to break out of a loop iteration. + var breaker = {}; // Save bytes in the minified (but not gzipped) version: var ArrayProto = Array.prototype, ObjProto = Object.prototype; // Create quick reference variables for speed access to core prototypes. - var slice = ArrayProto.slice, - unshift = ArrayProto.unshift, - toString = ObjProto.toString, - hasOwnProperty = ObjProto.hasOwnProperty, - propertyIsEnumerable = ObjProto.propertyIsEnumerable; + var slice = ArrayProto.slice, + unshift = ArrayProto.unshift, + toString = ObjProto.toString, + hasOwnProperty = ObjProto.hasOwnProperty; - // All ECMA5 native implementations we hope to use are declared here. + // All **ECMAScript 5** native function implementations that we hope to use + // are declared here. var nativeForEach = ArrayProto.forEach, nativeMap = ArrayProto.map, @@ -48,91 +47,102 @@ // Create a safe reference to the Underscore object for use below. var _ = function(obj) { return new wrapper(obj); }; - // Export the Underscore object for CommonJS. - if (typeof exports !== 'undefined') exports._ = _; - - // Export underscore to global scope. - root._ = _; + // Export the Underscore object for **CommonJS**, with backwards-compatibility + // for the old `require()` API. If we're not in CommonJS, add `_` to the + // global object. + if (typeof module !== 'undefined' && module.exports) { + module.exports = _; + _._ = _; + } else { + root._ = _; + } // Current version. - _.VERSION = '1.1.0'; - - // ------------------------ Collection Functions: --------------------------- - - // The cornerstone, an each implementation. - // Handles objects implementing forEach, arrays, and raw objects. - // Delegates to JavaScript 1.6's native forEach if available. - var each = _.forEach = function(obj, iterator, context) { - try { - if (nativeForEach && obj.forEach === nativeForEach) { - obj.forEach(iterator, context); - } else if (_.isNumber(obj.length)) { - for (var i = 0, l = obj.length; i < l; i++) iterator.call(context, obj[i], i, obj); - } else { - for (var key in obj) { - if (hasOwnProperty.call(obj, key)) iterator.call(context, obj[key], key, obj); + _.VERSION = '1.1.3'; + + // Collection Functions + // -------------------- + + // The cornerstone, an `each` implementation, aka `forEach`. + // Handles objects implementing `forEach`, arrays, and raw objects. + // Delegates to **ECMAScript 5**'s native `forEach` if available. + var each = _.each = _.forEach = function(obj, iterator, context) { + var value; + if (nativeForEach && obj.forEach === nativeForEach) { + obj.forEach(iterator, context); + } else if (_.isNumber(obj.length)) { + for (var i = 0, l = obj.length; i < l; i++) { + if (iterator.call(context, obj[i], i, obj) === breaker) return; + } + } else { + for (var key in obj) { + if (hasOwnProperty.call(obj, key)) { + if (iterator.call(context, obj[key], key, obj) === breaker) return; } } - } catch(e) { - if (e != breaker) throw e; } - return obj; }; // Return the results of applying the iterator to each element. - // Delegates to JavaScript 1.6's native map if available. + // Delegates to **ECMAScript 5**'s native `map` if available. _.map = function(obj, iterator, context) { if (nativeMap && obj.map === nativeMap) return obj.map(iterator, context); var results = []; each(obj, function(value, index, list) { - results.push(iterator.call(context, value, index, list)); + results[results.length] = iterator.call(context, value, index, list); }); return results; }; - // Reduce builds up a single result from a list of values, aka inject, or foldl. - // Delegates to JavaScript 1.8's native reduce if available. - _.reduce = function(obj, iterator, memo, context) { + // **Reduce** builds up a single result from a list of values, aka `inject`, + // or `foldl`. Delegates to **ECMAScript 5**'s native `reduce` if available. + _.reduce = _.foldl = _.inject = function(obj, iterator, memo, context) { + var initial = memo !== void 0; if (nativeReduce && obj.reduce === nativeReduce) { if (context) iterator = _.bind(iterator, context); - return obj.reduce(iterator, memo); + return initial ? obj.reduce(iterator, memo) : obj.reduce(iterator); } each(obj, function(value, index, list) { - memo = iterator.call(context, memo, value, index, list); + if (!initial && index === 0) { + memo = value; + } else { + memo = iterator.call(context, memo, value, index, list); + } }); return memo; }; - // The right-associative version of reduce, also known as foldr. Uses - // Delegates to JavaScript 1.8's native reduceRight if available. - _.reduceRight = function(obj, iterator, memo, context) { + // The right-associative version of reduce, also known as `foldr`. + // Delegates to **ECMAScript 5**'s native `reduceRight` if available. + _.reduceRight = _.foldr = function(obj, iterator, memo, context) { if (nativeReduceRight && obj.reduceRight === nativeReduceRight) { if (context) iterator = _.bind(iterator, context); - return obj.reduceRight(iterator, memo); + return memo !== void 0 ? obj.reduceRight(iterator, memo) : obj.reduceRight(iterator); } - var reversed = _.clone(_.toArray(obj)).reverse(); + var reversed = (_.isArray(obj) ? obj.slice() : _.toArray(obj)).reverse(); return _.reduce(reversed, iterator, memo, context); }; - // Return the first value which passes a truth test. - _.detect = function(obj, iterator, context) { + // Return the first value which passes a truth test. Aliased as `detect`. + _.find = _.detect = function(obj, iterator, context) { var result; - each(obj, function(value, index, list) { + any(obj, function(value, index, list) { if (iterator.call(context, value, index, list)) { result = value; - _.breakLoop(); + return true; } }); return result; }; // Return all the elements that pass a truth test. - // Delegates to JavaScript 1.6's native filter if available. - _.filter = function(obj, iterator, context) { + // Delegates to **ECMAScript 5**'s native `filter` if available. + // Aliased as `select`. + _.filter = _.select = function(obj, iterator, context) { if (nativeFilter && obj.filter === nativeFilter) return obj.filter(iterator, context); var results = []; each(obj, function(value, index, list) { - iterator.call(context, value, index, list) && results.push(value); + if (iterator.call(context, value, index, list)) results[results.length] = value; }); return results; }; @@ -141,59 +151,62 @@ _.reject = function(obj, iterator, context) { var results = []; each(obj, function(value, index, list) { - !iterator.call(context, value, index, list) && results.push(value); + if (!iterator.call(context, value, index, list)) results[results.length] = value; }); return results; }; // Determine whether all of the elements match a truth test. - // Delegates to JavaScript 1.6's native every if available. - _.every = function(obj, iterator, context) { + // Delegates to **ECMAScript 5**'s native `every` if available. + // Aliased as `all`. + _.every = _.all = function(obj, iterator, context) { iterator = iterator || _.identity; if (nativeEvery && obj.every === nativeEvery) return obj.every(iterator, context); var result = true; each(obj, function(value, index, list) { - if (!(result = result && iterator.call(context, value, index, list))) _.breakLoop(); + if (!(result = result && iterator.call(context, value, index, list))) return breaker; }); return result; }; // Determine if at least one element in the object matches a truth test. - // Delegates to JavaScript 1.6's native some if available. - _.some = function(obj, iterator, context) { + // Delegates to **ECMAScript 5**'s native `some` if available. + // Aliased as `any`. + var any = _.some = _.any = function(obj, iterator, context) { iterator = iterator || _.identity; if (nativeSome && obj.some === nativeSome) return obj.some(iterator, context); var result = false; each(obj, function(value, index, list) { - if (result = iterator.call(context, value, index, list)) _.breakLoop(); + if (result = iterator.call(context, value, index, list)) return breaker; }); return result; }; - // Determine if a given value is included in the array or object using '==='. - _.include = function(obj, target) { + // Determine if a given value is included in the array or object using `===`. + // Aliased as `contains`. + _.include = _.contains = function(obj, target) { if (nativeIndexOf && obj.indexOf === nativeIndexOf) return obj.indexOf(target) != -1; var found = false; - each(obj, function(value) { - if (found = value === target) _.breakLoop(); + any(obj, function(value) { + if (found = value === target) return true; }); return found; }; - // Invoke a method with arguments on every item in a collection. + // Invoke a method (with arguments) on every item in a collection. _.invoke = function(obj, method) { - var args = _.rest(arguments, 2); + var args = slice.call(arguments, 2); return _.map(obj, function(value) { return (method ? value[method] : value).apply(value, args); }); }; - // Convenience version of a common use case of map: fetching a property. + // Convenience version of a common use case of `map`: fetching a property. _.pluck = function(obj, key) { return _.map(obj, function(value){ return value[key]; }); }; - // Return the maximum item or (item-based computation). + // Return the maximum element or (element-based computation). _.max = function(obj, iterator, context) { if (!iterator && _.isArray(obj)) return Math.max.apply(Math, obj); var result = {computed : -Infinity}; @@ -240,7 +253,7 @@ return low; }; - // Convert anything iterable into a real, live array. + // Safely convert anything iterable into a real, live array. _.toArray = function(iterable) { if (!iterable) return []; if (iterable.toArray) return iterable.toArray(); @@ -254,20 +267,21 @@ return _.toArray(obj).length; }; - // -------------------------- Array Functions: ------------------------------ + // Array Functions + // --------------- - // Get the first element of an array. Passing "n" will return the first N - // values in the array. Aliased as "head". The "guard" check allows it to work - // with _.map. - _.first = function(array, n, guard) { + // Get the first element of an array. Passing **n** will return the first N + // values in the array. Aliased as `head`. The **guard** check allows it to work + // with `_.map`. + _.first = _.head = function(array, n, guard) { return n && !guard ? slice.call(array, 0, n) : array[0]; }; - // Returns everything but the first entry of the array. Aliased as "tail". - // Especially useful on the arguments object. Passing an "index" will return - // the rest of the values in the array from that index onward. The "guard" - //check allows it to work with _.map. - _.rest = function(array, index, guard) { + // Returns everything but the first entry of the array. Aliased as `tail`. + // Especially useful on the arguments object. Passing an **index** will return + // the rest of the values in the array from that index onward. The **guard** + // check allows it to work with `_.map`. + _.rest = _.tail = function(array, index, guard) { return slice.call(array, _.isUndefined(index) || guard ? 1 : index); }; @@ -285,22 +299,23 @@ _.flatten = function(array) { return _.reduce(array, function(memo, value) { if (_.isArray(value)) return memo.concat(_.flatten(value)); - memo.push(value); + memo[memo.length] = value; return memo; }, []); }; // Return a version of the array that does not contain the specified value(s). _.without = function(array) { - var values = _.rest(arguments); + var values = slice.call(arguments, 1); return _.filter(array, function(value){ return !_.include(values, value); }); }; // Produce a duplicate-free version of the array. If the array has already // been sorted, you have the option of using a faster algorithm. - _.uniq = function(array, isSorted) { + // Aliased as `unique`. + _.uniq = _.unique = function(array, isSorted) { return _.reduce(array, function(memo, el, i) { - if (0 == i || (isSorted === true ? _.last(memo) != el : !_.include(memo, el))) memo.push(el); + if (0 == i || (isSorted === true ? _.last(memo) != el : !_.include(memo, el))) memo[memo.length] = el; return memo; }, []); }; @@ -308,7 +323,7 @@ // Produce an array that contains every item shared between all the // passed-in arrays. _.intersect = function(array) { - var rest = _.rest(arguments); + var rest = slice.call(arguments, 1); return _.filter(_.uniq(array), function(item) { return _.every(rest, function(other) { return _.indexOf(other, item) >= 0; @@ -319,17 +334,17 @@ // Zip together multiple lists into a single array -- elements that share // an index go together. _.zip = function() { - var args = _.toArray(arguments); + var args = slice.call(arguments); var length = _.max(_.pluck(args, 'length')); var results = new Array(length); - for (var i = 0; i < length; i++) results[i] = _.pluck(args, String(i)); + for (var i = 0; i < length; i++) results[i] = _.pluck(args, "" + i); return results; }; - // If the browser doesn't supply us with indexOf (I'm looking at you, MSIE), - // we need this function. Return the position of the first occurence of an + // If the browser doesn't supply us with indexOf (I'm looking at you, **MSIE**), + // we need this function. Return the position of the first occurrence of an // item in an array, or -1 if the item is not included in the array. - // Delegates to JavaScript 1.8's native indexOf if available. + // Delegates to **ECMAScript 5**'s native `indexOf` if available. _.indexOf = function(array, item) { if (nativeIndexOf && array.indexOf === nativeIndexOf) return array.indexOf(item); for (var i = 0, l = array.length; i < l; i++) if (array[i] === item) return i; @@ -337,7 +352,7 @@ }; - // Delegates to JavaScript 1.6's native lastIndexOf if available. + // Delegates to **ECMAScript 5**'s native `lastIndexOf` if available. _.lastIndexOf = function(array, item) { if (nativeLastIndexOf && array.lastIndexOf === nativeLastIndexOf) return array.lastIndexOf(item); var i = array.length; @@ -346,36 +361,40 @@ }; // Generate an integer Array containing an arithmetic progression. A port of - // the native Python range() function. See: - // http://docs.python.org/library/functions.html#range + // the native Python `range()` function. See + // [the Python documentation](http://docs.python.org/library/functions.html#range). _.range = function(start, stop, step) { - var a = _.toArray(arguments); - var solo = a.length <= 1; - var start = solo ? 0 : a[0], stop = solo ? a[0] : a[1], step = a[2] || 1; - var len = Math.ceil((stop - start) / step); - if (len <= 0) return []; - var range = new Array(len); - for (var i = start, idx = 0; true; i += step) { - if ((step > 0 ? i - stop : stop - i) >= 0) return range; - range[idx++] = i; + var args = slice.call(arguments), + solo = args.length <= 1, + start = solo ? 0 : args[0], + stop = solo ? args[0] : args[1], + step = args[2] || 1, + len = Math.max(Math.ceil((stop - start) / step), 0), + idx = 0, + range = new Array(len); + while (idx < len) { + range[idx++] = start; + start += step; } + return range; }; - // ----------------------- Function Functions: ------------------------------ + // Function (ahem) Functions + // ------------------ - // Create a function bound to a given object (assigning 'this', and arguments, - // optionally). Binding with arguments is also known as 'curry'. + // Create a function bound to a given object (assigning `this`, and arguments, + // optionally). Binding with arguments is also known as `curry`. _.bind = function(func, obj) { - var args = _.rest(arguments, 2); + var args = slice.call(arguments, 2); return function() { - return func.apply(obj || {}, args.concat(_.toArray(arguments))); + return func.apply(obj || {}, args.concat(slice.call(arguments))); }; }; // Bind all of an object's methods to that object. Useful for ensuring that // all callbacks defined on an object belong to it. _.bindAll = function(obj) { - var funcs = _.rest(arguments); + var funcs = slice.call(arguments, 1); if (funcs.length == 0) funcs = _.functions(obj); each(funcs, function(f) { obj[f] = _.bind(obj[f], obj); }); return obj; @@ -394,14 +413,41 @@ // Delays a function for the given number of milliseconds, and then calls // it with the arguments supplied. _.delay = function(func, wait) { - var args = _.rest(arguments, 2); + var args = slice.call(arguments, 2); return setTimeout(function(){ return func.apply(func, args); }, wait); }; // Defers a function, scheduling it to run after the current call stack has // cleared. _.defer = function(func) { - return _.delay.apply(_, [func, 1].concat(_.rest(arguments))); + return _.delay.apply(_, [func, 1].concat(slice.call(arguments, 1))); + }; + + // Internal function used to implement `_.throttle` and `_.debounce`. + var limit = function(func, wait, debounce) { + var timeout; + return function() { + var context = this, args = arguments; + var throttler = function() { + timeout = null; + func.apply(context, args); + }; + if (debounce) clearTimeout(timeout); + if (debounce || !timeout) timeout = setTimeout(throttler, wait); + }; + }; + + // Returns a function, that, when invoked, will only be triggered at most once + // during a given window of time. + _.throttle = function(func, wait) { + return limit(func, wait, false); + }; + + // Returns a function, that, as long as it continues to be invoked, will not + // be triggered. The function will be called after it stops being called for + // N milliseconds. + _.debounce = function(func, wait) { + return limit(func, wait, true); }; // Returns the first function passed as an argument to the second, @@ -409,7 +455,7 @@ // conditionally execute the original function. _.wrap = function(func, wrapper) { return function() { - var args = [func].concat(_.toArray(arguments)); + var args = [func].concat(slice.call(arguments)); return wrapper.apply(wrapper, args); }; }; @@ -417,9 +463,9 @@ // Returns a function that is the composition of a list of functions, each // consuming the return value of the function that follows. _.compose = function() { - var funcs = _.toArray(arguments); + var funcs = slice.call(arguments); return function() { - var args = _.toArray(arguments); + var args = slice.call(arguments); for (var i=funcs.length-1; i >= 0; i--) { args = [funcs[i].apply(this, args)]; } @@ -427,14 +473,15 @@ }; }; - // ------------------------- Object Functions: ------------------------------ + // Object Functions + // ---------------- // Retrieve the names of an object's properties. - // Delegates to ECMA5's native Object.keys + // Delegates to **ECMAScript 5**'s native `Object.keys` _.keys = nativeKeys || function(obj) { if (_.isArray(obj)) return _.range(0, obj.length); var keys = []; - for (var key in obj) if (hasOwnProperty.call(obj, key)) keys.push(key); + for (var key in obj) if (hasOwnProperty.call(obj, key)) keys[keys.length] = key; return keys; }; @@ -444,13 +491,14 @@ }; // Return a sorted list of the function names available on the object. - _.functions = function(obj) { + // Aliased as `methods` + _.functions = _.methods = function(obj) { return _.filter(_.keys(obj), function(key){ return _.isFunction(obj[key]); }).sort(); }; // Extend a given object with all the properties in passed-in object(s). _.extend = function(obj) { - each(_.rest(arguments), function(source) { + each(slice.call(arguments, 1), function(source) { for (var prop in source) obj[prop] = source[prop]; }); return obj; @@ -458,12 +506,12 @@ // Create a (shallow-cloned) duplicate of an object. _.clone = function(obj) { - if (_.isArray(obj)) return obj.slice(0); - return _.extend({}, obj); + return _.isArray(obj) ? obj.slice() : _.extend({}, obj); }; // Invokes interceptor with the obj, and then returns obj. - // The primary purpose of this method is to "tap into" a method chain, in order to perform operations on intermediate results within the chain. + // The primary purpose of this method is to "tap into" a method chain, in + // order to perform operations on intermediate results within the chain. _.tap = function(obj, interceptor) { interceptor(obj); return obj; @@ -525,7 +573,7 @@ // Is a given variable an arguments object? _.isArguments = function(obj) { - return obj && obj.callee; + return !!(obj && obj.callee); }; // Is a given value a function? @@ -540,7 +588,13 @@ // Is a given value a number? _.isNumber = function(obj) { - return (obj === +obj) || (toString.call(obj) === '[object Number]'); + return !!(obj === 0 || (obj && obj.toExponential && obj.toFixed)); + }; + + // Is the given value NaN -- this one is interesting. NaN != NaN, and + // isNaN(undefined) == true, so we make sure it's a number first. + _.isNaN = function(obj) { + return toString.call(obj) === '[object Number]' && isNaN(obj); }; // Is a given value a boolean? @@ -558,12 +612,6 @@ return !!(obj && obj.test && obj.exec && (obj.ignoreCase || obj.ignoreCase === false)); }; - // Is the given value NaN -- this one is interesting. NaN != NaN, and - // isNaN(undefined) == true, so we make sure it's a number first. - _.isNaN = function(obj) { - return _.isNumber(obj) && isNaN(obj); - }; - // Is a given value equal to null? _.isNull = function(obj) { return obj === null; @@ -571,12 +619,13 @@ // Is a given variable undefined? _.isUndefined = function(obj) { - return typeof obj == 'undefined'; + return obj === void 0; }; - // -------------------------- Utility Functions: ---------------------------- + // Utility Functions + // ----------------- - // Run Underscore.js in noConflict mode, returning the '_' variable to its + // Run Underscore.js in *noConflict* mode, returning the `_` variable to its // previous owner. Returns a reference to the Underscore object. _.noConflict = function() { root._ = previousUnderscore; @@ -588,16 +637,11 @@ return value; }; - // Run a function n times. + // Run a function **n** times. _.times = function (n, iterator, context) { for (var i = 0; i < n; i++) iterator.call(context, i); }; - // Break out of the middle of an iteration. - _.breakLoop = function() { - throw breaker; - }; - // Add your own custom functions to the Underscore object, ensuring that // they're correctly added to the OOP wrapper as well. _.mixin = function(obj) { @@ -617,54 +661,45 @@ // By default, Underscore uses ERB-style template delimiters, change the // following template settings to use alternative delimiters. _.templateSettings = { - start : '<%', - end : '%>', - interpolate : /<%=(.+?)%>/g + evaluate : /<%([\s\S]+?)%>/g, + interpolate : /<%=([\s\S]+?)%>/g }; - // JavaScript templating a-la ERB, pilfered from John Resig's - // "Secrets of the JavaScript Ninja", page 83. - // Single-quote fix from Rick Strahl's version. - // With alterations for arbitrary delimiters, and to preserve whitespace. + // JavaScript micro-templating, similar to John Resig's implementation. + // Underscore templating handles arbitrary delimiters, preserves whitespace, + // and correctly escapes quotes within interpolated code. _.template = function(str, data) { var c = _.templateSettings; - var endMatch = new RegExp("'(?=[^"+c.end.substr(0, 1)+"]*"+escapeRegExp(c.end)+")","g"); - var fn = new Function('obj', - 'var p=[],print=function(){p.push.apply(p,arguments);};' + - 'with(obj||{}){p.push(\'' + - str.replace(/\r/g, '\\r') + var tmpl = 'var __p=[],print=function(){__p.push.apply(__p,arguments);};' + + 'with(obj||{}){__p.push(\'' + + str.replace(/\\/g, '\\\\') + .replace(/'/g, "\\'") + .replace(c.interpolate, function(match, code) { + return "'," + code.replace(/\\'/g, "'") + ",'"; + }) + .replace(c.evaluate || null, function(match, code) { + return "');" + code.replace(/\\'/g, "'") + .replace(/[\r\n\t]/g, ' ') + "__p.push('"; + }) + .replace(/\r/g, '\\r') .replace(/\n/g, '\\n') .replace(/\t/g, '\\t') - .replace(endMatch,"✄") - .split("'").join("\\'") - .split("✄").join("'") - .replace(c.interpolate, "',$1,'") - .split(c.start).join("');") - .split(c.end).join("p.push('") - + "');}return p.join('');"); - return data ? fn(data) : fn; - }; - - // ------------------------------- Aliases ---------------------------------- - - _.each = _.forEach; - _.foldl = _.inject = _.reduce; - _.foldr = _.reduceRight; - _.select = _.filter; - _.all = _.every; - _.any = _.some; - _.contains = _.include; - _.head = _.first; - _.tail = _.rest; - _.methods = _.functions; - - // ------------------------ Setup the OOP Wrapper: -------------------------- + + "');}return __p.join('');"; + var func = new Function('obj', tmpl); + return data ? func(data) : func; + }; + + // The OOP Wrapper + // --------------- // If Underscore is called as a function, it returns a wrapped object that // can be used OO-style. This wrapper holds altered versions of all the // underscore functions. Wrapped objects may be chained. var wrapper = function(obj) { this._wrapped = obj; }; + // Expose `wrapper.prototype` as `_.prototype` + _.prototype = wrapper.prototype; + // Helper function to continue chaining intermediate results. var result = function(obj, chain) { return chain ? _(obj).chain() : obj; @@ -673,7 +708,7 @@ // A method to easily add functions to the OOP wrapper. var addToWrapper = function(name, func) { wrapper.prototype[name] = function() { - var args = _.toArray(arguments); + var args = slice.call(arguments); unshift.call(args, this._wrapped); return result(func.apply(_, args), this._chain); }; From c7a7aa5b10cde0ba062830702ae713386f9b5231 Mon Sep 17 00:00:00 2001 From: Jeremy Ashkenas Date: Wed, 1 Dec 2010 12:58:59 -0500 Subject: [PATCH 10/11] Updating test-zepto.html to latest Underscore --- test/controller.js | 1 + test/test-zepto.html | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/test/controller.js b/test/controller.js index bdb5cb015..316806b32 100644 --- a/test/controller.js +++ b/test/controller.js @@ -74,6 +74,7 @@ $(document).ready(function() { equals(controller.part, 'part'); equals(controller.rest, 'four/five/six/seven'); start(); + window.location.hash = ''; }, 10); }); diff --git a/test/test-zepto.html b/test/test-zepto.html index 44ce2a2ee..d6320a477 100644 --- a/test/test-zepto.html +++ b/test/test-zepto.html @@ -7,7 +7,7 @@ - + From 3505bde58ec82f15e920298813d37476242793bb Mon Sep 17 00:00:00 2001 From: Jeremy Ashkenas Date: Wed, 1 Dec 2010 13:27:10 -0500 Subject: [PATCH 11/11] Backbone.js 0.3.3 --- backbone-min.js | 46 ++++---- backbone.js | 26 ++--- docs/backbone.html | 234 ++++++++++++++++++++------------------ examples/todos/index.html | 2 +- index.html | 218 ++++++++++++++++++++--------------- package.json | 2 +- 6 files changed, 286 insertions(+), 242 deletions(-) diff --git a/backbone-min.js b/backbone-min.js index f8d7f9e8d..161b401e4 100644 --- a/backbone-min.js +++ b/backbone-min.js @@ -1,27 +1,27 @@ -// Backbone.js 0.3.2 +// Backbone.js 0.3.3 // (c) 2010 Jeremy Ashkenas, DocumentCloud Inc. // Backbone may be freely distributed under the MIT license. // For all details and documentation: // http://documentcloud.github.com/backbone -(function(){var e;e=typeof exports!=="undefined"?exports:this.Backbone={};e.VERSION="0.3.2";var f=this._;if(!f&&typeof require!=="undefined")f=require("underscore")._;var h=this.jQuery;e.emulateHTTP=false;e.emulateJSON=false;e.Events={bind:function(a,b){this._callbacks||(this._callbacks={});(this._callbacks[a]||(this._callbacks[a]=[])).push(b);return this},unbind:function(a,b){var c;if(a){if(c=this._callbacks)if(b){c=c[a];if(!c)return this;for(var d=0,g=c.length;d').hide().appendTo("body")[0].contentWindow;"onhashchange"in window&&!a?h(window).bind("hashchange",this.checkUrl):setInterval(this.checkUrl,this.interval);return this.loadUrl()},route:function(a,b){this.handlers.push({route:a,callback:b})},checkUrl:function(){var a=this.getFragment();if(a==this.fragment&&this.iframe)a=this.getFragment(this.iframe.location);if(a==this.fragment||a==decodeURIComponent(this.fragment))return false; -if(this.iframe)window.location.hash=this.iframe.location.hash=a;this.loadUrl()},loadUrl:function(){var a=this.fragment=this.getFragment();return f.any(this.handlers,function(b){if(b.route.test(a)){b.callback(a);return true}})},saveLocation:function(a){a=(a||"").replace(k,"");if(this.fragment!=a){window.location.hash=this.fragment=a;if(this.iframe&&a!=this.getFragment(this.iframe.location)){this.iframe.document.open().close();this.iframe.location.hash=a}}}});e.View=function(a){this._configure(a||{}); -this._ensureElement();this.delegateEvents();this.initialize(a)};var l=function(a){return h(a,this.el)},q=/^(\w+)\s*(.*)$/;f.extend(e.View.prototype,e.Events,{tagName:"div",$:l,jQuery:l,initialize:function(){},render:function(){return this},remove:function(){h(this.el).remove();return this},make:function(a,b,c){a=document.createElement(a);b&&h(a).attr(b);c&&h(a).html(c);return a},delegateEvents:function(a){if(a||(a=this.events)){h(this.el).unbind();for(var b in a){var c=a[b],d=b.match(q),g=d[1];d= -d[2];c=f.bind(this[c],this);d===""?h(this.el).bind(g,c):h(this.el).delegate(d,g,c)}}},_configure:function(a){if(this.options)a=f.extend({},this.options,a);if(a.model)this.model=a.model;if(a.collection)this.collection=a.collection;if(a.el)this.el=a.el;if(a.id)this.id=a.id;if(a.className)this.className=a.className;if(a.tagName)this.tagName=a.tagName;this.options=a},_ensureElement:function(){if(!this.el){var a={};if(this.id)a.id=this.id;if(this.className)a.className=this.className;this.el=this.make(this.tagName, -a)}}});var m=function(a,b){var c=r(this,a,b);c.extend=m;return c};e.Model.extend=e.Collection.extend=e.Controller.extend=e.View.extend=m;var s={create:"POST",update:"PUT","delete":"DELETE",read:"GET"};e.sync=function(a,b,c,d){var g=s[a];a=a==="create"||a==="update"?JSON.stringify(b.toJSON()):null;b={url:j(b),type:g,contentType:"application/json",data:a,dataType:"json",processData:false,success:c,error:d};if(e.emulateJSON){b.contentType="application/x-www-form-urlencoded";b.processData=true;b.data= -a?{model:a}:{}}if(e.emulateHTTP)if(g==="PUT"||g==="DELETE"){if(e.emulateJSON)b.data._method=g;b.type="POST";b.beforeSend=function(i){i.setRequestHeader("X-HTTP-Method-Override",g)}}h.ajax(b)};var n=function(){},r=function(a,b,c){var d;d=b&&b.hasOwnProperty("constructor")?b.constructor:function(){return a.apply(this,arguments)};n.prototype=a.prototype;d.prototype=new n;b&&f.extend(d.prototype,b);c&&f.extend(d,c);d.prototype.constructor=d;d.__super__=a.prototype;return d},j=function(a){if(!(a&&a.url))throw Error("A 'url' property or function must be specified"); -return f.isFunction(a.url)?a.url():a.url}})(); +(function(){var e;e=typeof exports!=="undefined"?exports:this.Backbone={};e.VERSION="0.3.3";var f=this._;if(!f&&typeof require!=="undefined")f=require("underscore")._;var h=this.jQuery||this.Zepto;e.emulateHTTP=false;e.emulateJSON=false;e.Events={bind:function(a,b){this._callbacks||(this._callbacks={});(this._callbacks[a]||(this._callbacks[a]=[])).push(b);return this},unbind:function(a,b){var c;if(a){if(c=this._callbacks)if(b){c=c[a];if(!c)return this;for(var d=0,g=c.length;d/g,">").replace(/"/g, +""")},set:function(a,b){b||(b={});if(!a)return this;if(a.attributes)a=a.attributes;var c=this.attributes,d=this._escapedAttributes;if(!b.silent&&this.validate&&!this._performValidation(a,b))return false;if("id"in a)this.id=a.id;for(var g in a){var i=a[g];if(!f.isEqual(c[g],i)){c[g]=i;delete d[g];if(!b.silent){this._changed=true;this.trigger("change:"+g,this,i,b)}}}!b.silent&&this._changed&&this.change(b);return this},unset:function(a,b){b||(b={});var c={};c[a]=void 0;if(!b.silent&&this.validate&& +!this._performValidation(c,b))return false;delete this.attributes[a];delete this._escapedAttributes[a];if(!b.silent){this._changed=true;this.trigger("change:"+a,this,void 0,b);this.change(b)}return this},clear:function(a){a||(a={});var b=this.attributes,c={};for(attr in b)c[attr]=void 0;if(!a.silent&&this.validate&&!this._performValidation(c,a))return false;this.attributes={};this._escapedAttributes={};if(!a.silent){this._changed=true;for(attr in b)this.trigger("change:"+attr,this,void 0,a);this.change(a)}return this}, +fetch:function(a){a||(a={});var b=this,c=j(a.error,b,a);(this.sync||e.sync)("read",this,function(d){if(!b.set(b.parse(d),a))return false;a.success&&a.success(b,d)},c);return this},save:function(a,b){b||(b={});if(a&&!this.set(a,b))return false;var c=this,d=j(b.error,c,b),g=this.isNew()?"create":"update";(this.sync||e.sync)(g,this,function(i){if(!c.set(c.parse(i),b))return false;b.success&&b.success(c,i)},d);return this},destroy:function(a){a||(a={});var b=this,c=j(a.error,b,a);(this.sync||e.sync)("delete", +this,function(d){b.collection&&b.collection.remove(b);a.success&&a.success(b,d)},c);return this},url:function(){var a=k(this.collection);if(this.isNew())return a;return a+(a.charAt(a.length-1)=="/"?"":"/")+this.id},parse:function(a){return a},clone:function(){return new this.constructor(this)},isNew:function(){return!this.id},change:function(a){this.trigger("change",this,a);this._previousAttributes=f.clone(this.attributes);this._changed=false},hasChanged:function(a){if(a)return this._previousAttributes[a]!= +this.attributes[a];return this._changed},changedAttributes:function(a){a||(a=this.attributes);var b=this._previousAttributes,c=false,d;for(d in a)if(!f.isEqual(b[d],a[d])){c=c||{};c[d]=a[d]}return c},previous:function(a){if(!a||!this._previousAttributes)return null;return this._previousAttributes[a]},previousAttributes:function(){return f.clone(this._previousAttributes)},_performValidation:function(a,b){var c=this.validate(a);if(c){b.error?b.error(this,c):this.trigger("error",this,c,b);return false}return true}}); +e.Collection=function(a,b){b||(b={});if(b.comparator){this.comparator=b.comparator;delete b.comparator}this._boundOnModelEvent=f.bind(this._onModelEvent,this);this._reset();a&&this.refresh(a,{silent:true});this.initialize(a,b)};f.extend(e.Collection.prototype,e.Events,{model:e.Model,initialize:function(){},toJSON:function(){return this.map(function(a){return a.toJSON()})},add:function(a,b){if(f.isArray(a))for(var c=0,d=a.length;c').hide().appendTo("body")[0].contentWindow; +"onhashchange"in window&&!a?h(window).bind("hashchange",this.checkUrl):setInterval(this.checkUrl,this.interval);return this.loadUrl()},route:function(a,b){this.handlers.push({route:a,callback:b})},checkUrl:function(){var a=this.getFragment();if(a==this.fragment&&this.iframe)a=this.getFragment(this.iframe.location);if(a==this.fragment||a==decodeURIComponent(this.fragment))return false;if(this.iframe)window.location.hash=this.iframe.location.hash=a;this.loadUrl()},loadUrl:function(){var a=this.fragment= +this.getFragment();return f.any(this.handlers,function(b){if(b.route.test(a)){b.callback(a);return true}})},saveLocation:function(a){a=(a||"").replace(l,"");if(this.fragment!=a){window.location.hash=this.fragment=a;if(this.iframe&&a!=this.getFragment(this.iframe.location)){this.iframe.document.open().close();this.iframe.location.hash=a}}}});e.View=function(a){this._configure(a||{});this._ensureElement();this.delegateEvents();this.initialize(a)};var q=/^(\w+)\s*(.*)$/;f.extend(e.View.prototype,e.Events, +{tagName:"div",$:function(a){return h(a,this.el)},initialize:function(){},render:function(){return this},remove:function(){h(this.el).remove();return this},make:function(a,b,c){a=document.createElement(a);b&&h(a).attr(b);c&&h(a).html(c);return a},delegateEvents:function(a){if(a||(a=this.events)){h(this.el).unbind();for(var b in a){var c=a[b],d=b.match(q),g=d[1];d=d[2];c=f.bind(this[c],this);d===""?h(this.el).bind(g,c):h(this.el).delegate(d,g,c)}}},_configure:function(a){if(this.options)a=f.extend({}, +this.options,a);if(a.model)this.model=a.model;if(a.collection)this.collection=a.collection;if(a.el)this.el=a.el;if(a.id)this.id=a.id;if(a.className)this.className=a.className;if(a.tagName)this.tagName=a.tagName;this.options=a},_ensureElement:function(){if(!this.el){var a={};if(this.id)a.id=this.id;if(this.className)a["class"]=this.className;this.el=this.make(this.tagName,a)}}});var m=function(a,b){var c=r(this,a,b);c.extend=m;return c};e.Model.extend=e.Collection.extend=e.Controller.extend=e.View.extend= +m;var s={create:"POST",update:"PUT","delete":"DELETE",read:"GET"};e.sync=function(a,b,c,d){var g=s[a];a=a==="create"||a==="update"?JSON.stringify(b.toJSON()):null;b={url:k(b),type:g,contentType:"application/json",data:a,dataType:"json",processData:false,success:c,error:d};if(e.emulateJSON){b.contentType="application/x-www-form-urlencoded";b.processData=true;b.data=a?{model:a}:{}}if(e.emulateHTTP)if(g==="PUT"||g==="DELETE"){if(e.emulateJSON)b.data._method=g;b.type="POST";b.beforeSend=function(i){i.setRequestHeader("X-HTTP-Method-Override", +g)}}h.ajax(b)};var n=function(){},r=function(a,b,c){var d;d=b&&b.hasOwnProperty("constructor")?b.constructor:function(){return a.apply(this,arguments)};n.prototype=a.prototype;d.prototype=new n;b&&f.extend(d.prototype,b);c&&f.extend(d,c);d.prototype.constructor=d;d.__super__=a.prototype;return d},k=function(a){if(!(a&&a.url))throw Error("A 'url' property or function must be specified");return f.isFunction(a.url)?a.url():a.url},j=function(a,b,c){return function(d){a?a(b,d):b.trigger("error",b,d,c)}}})(); diff --git a/backbone.js b/backbone.js index 159d387c7..04fe46ea6 100644 --- a/backbone.js +++ b/backbone.js @@ -1,4 +1,4 @@ -// Backbone.js 0.3.2 +// Backbone.js 0.3.3 // (c) 2010 Jeremy Ashkenas, DocumentCloud Inc. // Backbone may be freely distributed under the MIT license. // For all details and documentation: @@ -19,13 +19,13 @@ } // Current version of the library. Keep in sync with `package.json`. - Backbone.VERSION = '0.3.2'; + Backbone.VERSION = '0.3.3'; // Require Underscore, if we're on the server, and it's not already present. var _ = this._; if (!_ && (typeof require !== 'undefined')) _ = require("underscore")._; - // For Backbone's purposes, jQuery owns the `$` variable. + // For Backbone's purposes, either jQuery or Zepto owns the `$` variable. var $ = this.jQuery || this.Zepto; // Turn on `emulateHTTP` to use support legacy HTTP servers. Setting this option will @@ -762,10 +762,10 @@ this.initialize(options); }; - // jQuery lookup, scoped to DOM elements within the current view. - // This should be prefered to global jQuery lookups, if you're dealing with + // Element lookup, scoped to DOM elements within the current view. + // This should be prefered to global lookups, if you're dealing with // a specific view. - var jQueryDelegate = function(selector) { + var selectorDelegate = function(selector) { return $(selector, this.el); }; @@ -778,9 +778,8 @@ // The default `tagName` of a View's element is `"div"`. tagName : 'div', - // Attach the jQuery function as the `$` and `jQuery` properties. - $ : jQueryDelegate, - jQuery : jQueryDelegate, + // Attach the `selectorDelegate` function as the `$` property. + $ : selectorDelegate, // Initialize is an empty function by default. Override it with your own // initialization logic. @@ -822,7 +821,7 @@ // } // // pairs. Callbacks will be bound to the view, with `this` set properly. - // Uses jQuery event delegation for efficiency. + // Uses event delegation for efficiency. // Omitting the selector binds the event to `this.el`. // This only works for delegate-able events: not `focus`, `blur`, and // not `change`, `submit`, and `reset` in Internet Explorer. @@ -891,7 +890,7 @@ // Override this function to change the manner in which Backbone persists // models to the server. You will be passed the type of request, and the - // model in question. By default, uses jQuery to make a RESTful Ajax request + // model in question. By default, uses makes a RESTful Ajax request // to the model's `url()`. Some possible customizations could be: // // * Use `setTimeout` to batch rapid-fire updates into a single request. @@ -1006,10 +1005,7 @@ // Helper function to escape a string for HTML rendering. var escapeHTML = function(string) { - return string.replace(/&(?!\w+;)/g, '&') - .replace(//g, '>') - .replace(/"/g, '"'); + return string.replace(/&(?!\w+;)/g, '&').replace(//g, '>').replace(/"/g, '"'); }; })(); diff --git a/docs/backbone.html b/docs/backbone.html index e8e3ce4c6..f6c79011a 100644 --- a/docs/backbone.html +++ b/docs/backbone.html @@ -1,4 +1,4 @@ - backbone.js

    backbone.js

    Backbone.js 0.3.2
    +      backbone.js           

    backbone.js

    Backbone.js 0.3.3
     (c) 2010 Jeremy Ashkenas, DocumentCloud Inc.
     Backbone may be freely distributed under the MIT license.
     For all details and documentation:
    @@ -9,8 +9,8 @@
         Backbone = exports;
       } else {
         Backbone = this.Backbone = {};
    -  }

    Current version of the library. Keep in sync with package.json.

      Backbone.VERSION = '0.3.2';

    Require Underscore, if we're on the server, and it's not already present.

      var _ = this._;
    -  if (!_ && (typeof require !== 'undefined')) _ = require("underscore")._;

    For Backbone's purposes, jQuery owns the $ variable.

      var $ = this.jQuery;

    Turn on emulateHTTP to use support legacy HTTP servers. Setting this option will + }

    Current version of the library. Keep in sync with package.json.

      Backbone.VERSION = '0.3.3';

    Require Underscore, if we're on the server, and it's not already present.

      var _ = this._;
    +  if (!_ && (typeof require !== 'undefined')) _ = require("underscore")._;

    For Backbone's purposes, either jQuery or Zepto owns the $ variable.

      var $ = this.jQuery || this.Zepto;

    Turn on emulateHTTP to use support legacy HTTP servers. Setting this option will fake "PUT" and "DELETE" requests via the _method parameter and set a X-Http-Method-Override header.

      Backbone.emulateHTTP = false;

    Turn on emulateJSON to support legacy servers that can't deal with direct application/json requests ... will encode the body as @@ -73,6 +73,7 @@ attributes || (attributes = {}); if (this.defaults) attributes = _.extend({}, this.defaults, attributes); this.attributes = {}; + this._escapedAttributes = {}; this.cid = _.uniqueId('c'); this.set(attributes, {silent : true}); this._previousAttributes = _.clone(this.attributes); @@ -84,50 +85,58 @@ return _.clone(this.attributes); },

    Get the value of an attribute.

        get : function(attr) {
           return this.attributes[attr];
    -    },

    Set a hash of model attributes on the object, firing "change" unless you -choose to silence it.

        set : function(attrs, options) {

    Extract attributes and options.

          options || (options = {});
    +    },

    Get the HTML-escaped value of an attribute.

        escape : function(attr) {
    +      var html;
    +      if (html = this._escapedAttributes[attr]) return html;
    +      var val = this.attributes[attr];
    +      return this._escapedAttributes[attr] = escapeHTML(val == null ? '' : val);
    +    },

    Set a hash of model attributes on the object, firing "change" unless you +choose to silence it.

        set : function(attrs, options) {

    Extract attributes and options.

          options || (options = {});
           if (!attrs) return this;
           if (attrs.attributes) attrs = attrs.attributes;
    -      var now = this.attributes;

    Run validation.

          if (!options.silent && this.validate && !this._performValidation(attrs, options)) return false;

    Check for changes of id.

          if ('id' in attrs) this.id = attrs.id;

    Update attributes.

          for (var attr in attrs) {
    +      var now = this.attributes, escaped = this._escapedAttributes;

    Run validation.

          if (!options.silent && this.validate && !this._performValidation(attrs, options)) return false;

    Check for changes of id.

          if ('id' in attrs) this.id = attrs.id;

    Update attributes.

          for (var attr in attrs) {
             var val = attrs[attr];
             if (!_.isEqual(now[attr], val)) {
               now[attr] = val;
    +          delete escaped[attr];
               if (!options.silent) {
                 this._changed = true;
    -            this.trigger('change:' + attr, this, val);
    +            this.trigger('change:' + attr, this, val, options);
               }
             }
    -      }

    Fire the "change" event, if the model has been changed.

          if (!options.silent && this._changed) this.change();
    +      }

    Fire the "change" event, if the model has been changed.

          if (!options.silent && this._changed) this.change(options);
           return this;
    -    },

    Remove an attribute from the model, firing "change" unless you choose + },

    Remove an attribute from the model, firing "change" unless you choose to silence it.

        unset : function(attr, options) {
           options || (options = {});
    -      var value = this.attributes[attr];

    Run validation.

          var validObj = {};
    +      var value = this.attributes[attr];

    Run validation.

          var validObj = {};
           validObj[attr] = void 0;
    -      if (!options.silent && this.validate && !this._performValidation(validObj, options)) return false;

    Remove the attribute.

          delete this.attributes[attr];
    +      if (!options.silent && this.validate && !this._performValidation(validObj, options)) return false;

    Remove the attribute.

          delete this.attributes[attr];
    +      delete this._escapedAttributes[attr];
           if (!options.silent) {
             this._changed = true;
    -        this.trigger('change:' + attr, this);
    -        this.change();
    +        this.trigger('change:' + attr, this, void 0, options);
    +        this.change(options);
           }
           return this;
    -    },

    Clear all attributes on the model, firing "change" unless you choose + },

    Clear all attributes on the model, firing "change" unless you choose to silence it.

        clear : function(options) {
           options || (options = {});
    -      var old = this.attributes;

    Run validation.

          var validObj = {};
    +      var old = this.attributes;

    Run validation.

          var validObj = {};
           for (attr in old) validObj[attr] = void 0;
           if (!options.silent && this.validate && !this._performValidation(validObj, options)) return false;
     
           this.attributes = {};
    +      this._escapedAttributes = {};
           if (!options.silent) {
             this._changed = true;
             for (attr in old) {
    -          this.trigger('change:' + attr, this);
    +          this.trigger('change:' + attr, this, void 0, options);
             }
    -        this.change();
    +        this.change(options);
           }
           return this;
    -    },

    Fetch the model from the server. If the server's representation of the + },

    Fetch the model from the server. If the server's representation of the model differs from its current attributes, they will be overriden, triggering a "change" event.

        fetch : function(options) {
           options || (options = {});
    @@ -136,10 +145,10 @@
             if (!model.set(model.parse(resp), options)) return false;
             if (options.success) options.success(model, resp);
           };
    -      var error = options.error && _.bind(options.error, null, model);
    +      var error = wrapError(options.error, model, options);
           (this.sync || Backbone.sync)('read', this, success, error);
           return this;
    -    },

    Set a hash of model attributes, and sync the model to the server. + },

    Set a hash of model attributes, and sync the model to the server. If the server returns an attributes hash that differs, the model's state will be set again.

        save : function(attrs, options) {
           options || (options = {});
    @@ -149,11 +158,11 @@
             if (!model.set(model.parse(resp), options)) return false;
             if (options.success) options.success(model, resp);
           };
    -      var error = options.error && _.bind(options.error, null, model);
    +      var error = wrapError(options.error, model, options);
           var method = this.isNew() ? 'create' : 'update';
           (this.sync || Backbone.sync)(method, this, success, error);
           return this;
    -    },

    Destroy this model on the server. Upon success, the model is removed + },

    Destroy this model on the server. Upon success, the model is removed from its collection, if it has one.

        destroy : function(options) {
           options || (options = {});
           var model = this;
    @@ -161,33 +170,33 @@
             if (model.collection) model.collection.remove(model);
             if (options.success) options.success(model, resp);
           };
    -      var error = options.error && _.bind(options.error, null, model);
    +      var error = wrapError(options.error, model, options);
           (this.sync || Backbone.sync)('delete', this, success, error);
           return this;
    -    },

    Default URL for the model's representation on the server -- if you're + },

    Default URL for the model's representation on the server -- if you're using Backbone's restful methods, override this to change the endpoint that will be called.

        url : function() {
           var base = getUrl(this.collection);
           if (this.isNew()) return base;
           return base + (base.charAt(base.length - 1) == '/' ? '' : '/') + this.id;
    -    },

    parse converts a response into the hash of attributes to be set on + },

    parse converts a response into the hash of attributes to be set on the model. The default implementation is just to pass the response along.

        parse : function(resp) {
           return resp;
    -    },

    Create a new model with identical attributes to this one.

        clone : function() {
    +    },

    Create a new model with identical attributes to this one.

        clone : function() {
           return new this.constructor(this);
    -    },

    A model is new if it has never been saved to the server, and has a negative + },

    A model is new if it has never been saved to the server, and has a negative ID.

        isNew : function() {
           return !this.id;
    -    },

    Call this method to manually fire a change event for this model. -Calling this will cause all objects observing the model to update.

        change : function() {
    -      this.trigger('change', this);
    +    },

    Call this method to manually fire a change event for this model. +Calling this will cause all objects observing the model to update.

        change : function(options) {
    +      this.trigger('change', this, options);
           this._previousAttributes = _.clone(this.attributes);
           this._changed = false;
    -    },

    Determine if the model has changed since the last "change" event. + },

    Determine if the model has changed since the last "change" event. If you specify an attribute name, determine if that attribute has changed.

        hasChanged : function(attr) {
           if (attr) return this._previousAttributes[attr] != this.attributes[attr];
           return this._changed;
    -    },

    Return an object containing all the attributes that have changed, or false + },

    Return an object containing all the attributes that have changed, or false if there are no changed attributes. Useful for determining what parts of a view need to be updated and/or what attributes need to be persisted to the server.

        changedAttributes : function(now) {
    @@ -201,14 +210,14 @@
             }
           }
           return changed;
    -    },

    Get the previous value of an attribute, recorded at the time the last + },

    Get the previous value of an attribute, recorded at the time the last "change" event was fired.

        previous : function(attr) {
           if (!attr || !this._previousAttributes) return null;
           return this._previousAttributes[attr];
    -    },

    Get all of the attributes of the model at the time of the previous + },

    Get all of the attributes of the model at the time of the previous "change" event.

        previousAttributes : function() {
           return _.clone(this._previousAttributes);
    -    },

    Run validation against a set of incoming attributes, returning true + },

    Run validation against a set of incoming attributes, returning true if all is well. If a specific error callback has been passed, call that instead of firing the general "error" event.

        _performValidation : function(attrs, options) {
           var error = this.validate(attrs);
    @@ -216,14 +225,14 @@
             if (options.error) {
               options.error(this, error);
             } else {
    -          this.trigger('error', this, error);
    +          this.trigger('error', this, error, options);
             }
             return false;
           }
           return true;
         }
     
    -  });

    Backbone.Collection

    Provides a standard collection class for our sets of models, ordered + });

    Backbone.Collection

    Provides a standard collection class for our sets of models, ordered or unordered. If a comparator is specified, the Collection will maintain its models in sort order, as they're added and removed.

      Backbone.Collection = function(models, options) {
         options || (options = {});
    @@ -235,12 +244,12 @@
         this._reset();
         if (models) this.refresh(models, {silent: true});
         this.initialize(models, options);
    -  };

    Define the Collection's inheritable methods.

      _.extend(Backbone.Collection.prototype, Backbone.Events, {

    The default model for a collection is just a Backbone.Model. -This should be overridden in most cases.

        model : Backbone.Model,

    Initialize is an empty function by default. Override it with your own -initialization logic.

        initialize : function(){},

    The JSON representation of a Collection is an array of the + };

    Define the Collection's inheritable methods.

      _.extend(Backbone.Collection.prototype, Backbone.Events, {

    The default model for a collection is just a Backbone.Model. +This should be overridden in most cases.

        model : Backbone.Model,

    Initialize is an empty function by default. Override it with your own +initialization logic.

        initialize : function(){},

    The JSON representation of a Collection is an array of the models' attributes.

        toJSON : function() {
           return this.map(function(model){ return model.toJSON(); });
    -    },

    Add a model, or list of models to the set. Pass silent to avoid + },

    Add a model, or list of models to the set. Pass silent to avoid firing the added event for every new model.

        add : function(models, options) {
           if (_.isArray(models)) {
             for (var i = 0, l = models.length; i < l; i++) {
    @@ -250,7 +259,7 @@
             this._add(models, options);
           }
           return this;
    -    },

    Remove a model, or a list of models from the set. Pass silent to avoid + },

    Remove a model, or a list of models from the set. Pass silent to avoid firing the removed event for every model removed.

        remove : function(models, options) {
           if (_.isArray(models)) {
             for (var i = 0, l = models.length; i < l; i++) {
    @@ -260,32 +269,32 @@
             this._remove(models, options);
           }
           return this;
    -    },

    Get a model from the set by id.

        get : function(id) {
    +    },

    Get a model from the set by id.

        get : function(id) {
           if (id == null) return null;
           return this._byId[id.id != null ? id.id : id];
    -    },

    Get a model from the set by client id.

        getByCid : function(cid) {
    +    },

    Get a model from the set by client id.

        getByCid : function(cid) {
           return cid && this._byCid[cid.cid || cid];
    -    },

    Get the model at the given index.

        at: function(index) {
    +    },

    Get the model at the given index.

        at: function(index) {
           return this.models[index];
    -    },

    Force the collection to re-sort itself. You don't need to call this under normal + },

    Force the collection to re-sort itself. You don't need to call this under normal circumstances, as the set will maintain sort order as each item is added.

        sort : function(options) {
           options || (options = {});
           if (!this.comparator) throw new Error('Cannot sort a set without a comparator');
           this.models = this.sortBy(this.comparator);
    -      if (!options.silent) this.trigger('refresh', this);
    +      if (!options.silent) this.trigger('refresh', this, options);
           return this;
    -    },

    Pluck an attribute from each model in the collection.

        pluck : function(attr) {
    +    },

    Pluck an attribute from each model in the collection.

        pluck : function(attr) {
           return _.map(this.models, function(model){ return model.get(attr); });
    -    },

    When you have more items than you want to add or remove individually, + },

    When you have more items than you want to add or remove individually, you can refresh the entire set with a new list of models, without firing any added or removed events. Fires refresh when finished.

        refresh : function(models, options) {
           models  || (models = []);
           options || (options = {});
           this._reset();
           this.add(models, {silent: true});
    -      if (!options.silent) this.trigger('refresh', this);
    +      if (!options.silent) this.trigger('refresh', this, options);
           return this;
    -    },

    Fetch the default set of models for this collection, refreshing the + },

    Fetch the default set of models for this collection, refreshing the collection when they arrive.

        fetch : function(options) {
           options || (options = {});
           var collection = this;
    @@ -293,10 +302,10 @@
             collection.refresh(collection.parse(resp));
             if (options.success) options.success(collection, resp);
           };
    -      var error = options.error && _.bind(options.error, null, collection);
    +      var error = wrapError(options.error, collection, options);
           (this.sync || Backbone.sync)('read', this, success, error);
           return this;
    -    },

    Create a new instance of a model in this collection. After the model + },

    Create a new instance of a model in this collection. After the model has been created on the server, it will be added to the collection.

        create : function(model, options) {
           var coll = this;
           options || (options = {});
    @@ -310,19 +319,19 @@
             if (options.success) options.success(nextModel, resp);
           };
           return model.save(null, {success : success, error : options.error});
    -    },

    parse converts a response into a list of models to be added to the + },

    parse converts a response into a list of models to be added to the collection. The default implementation is just to pass it through.

        parse : function(resp) {
           return resp;
    -    },

    Proxy to _'s chain. Can't be proxied the same way the rest of the + },

    Proxy to _'s chain. Can't be proxied the same way the rest of the underscore methods are proxied because it relies on the underscore constructor.

        chain: function () {
           return _(this.models).chain();
    -    },

    Reset all internal state. Called when the collection is refreshed.

        _reset : function(options) {
    +    },

    Reset all internal state. Called when the collection is refreshed.

        _reset : function(options) {
           this.length = 0;
           this.models = [];
           this._byId  = {};
           this._byCid = {};
    -    },

    Internal implementation of adding a single model to the set, updating + },

    Internal implementation of adding a single model to the set, updating hash indexes for id and cid lookups.

        _add : function(model, options) {
           options || (options = {});
           if (!(model instanceof Backbone.Model)) {
    @@ -337,9 +346,9 @@
           this.models.splice(index, 0, model);
           model.bind('all', this._boundOnModelEvent);
           this.length++;
    -      if (!options.silent) model.trigger('add', model, this);
    +      if (!options.silent) model.trigger('add', model, this, options);
           return model;
    -    },

    Internal implementation of removing a single model from the set, updating + },

    Internal implementation of removing a single model from the set, updating hash indexes for id and cid lookups.

        _remove : function(model, options) {
           options || (options = {});
           model = this.getByCid(model) || this.get(model);
    @@ -349,10 +358,10 @@
           delete model.collection;
           this.models.splice(this.indexOf(model), 1);
           this.length--;
    -      if (!options.silent) model.trigger('remove', model, this);
    +      if (!options.silent) model.trigger('remove', model, this, options);
           model.unbind('all', this._boundOnModelEvent);
           return model;
    -    },

    Internal method called every time a model in the set fires an event. + },

    Internal method called every time a model in the set fires an event. Sets need to update their indexes when models change ids. All other events simply proxy through.

        _onModelEvent : function(ev, model) {
           if (ev === 'change:id') {
    @@ -362,23 +371,23 @@
           this.trigger.apply(this, arguments);
         }
     
    -  });

    Underscore methods that we want to implement on the Collection.

      var methods = ['forEach', 'each', 'map', 'reduce', 'reduceRight', 'find', 'detect',
    +  });

    Underscore methods that we want to implement on the Collection.

      var methods = ['forEach', 'each', 'map', 'reduce', 'reduceRight', 'find', 'detect',
         'filter', 'select', 'reject', 'every', 'all', 'some', 'any', 'include',
         'invoke', 'max', 'min', 'sortBy', 'sortedIndex', 'toArray', 'size',
    -    'first', 'rest', 'last', 'without', 'indexOf', 'lastIndexOf', 'isEmpty'];

    Mix in each Underscore method as a proxy to Collection#models.

      _.each(methods, function(method) {
    +    'first', 'rest', 'last', 'without', 'indexOf', 'lastIndexOf', 'isEmpty'];

    Mix in each Underscore method as a proxy to Collection#models.

      _.each(methods, function(method) {
         Backbone.Collection.prototype[method] = function() {
           return _[method].apply(_, [this.models].concat(_.toArray(arguments)));
         };
    -  });

    Backbone.Controller

    Controllers map faux-URLs to actions, and fire events when routes are + });

    Backbone.Controller

    Controllers map faux-URLs to actions, and fire events when routes are matched. Creating a new one sets its routes hash, if not set statically.

      Backbone.Controller = function(options) {
         options || (options = {});
         if (options.routes) this.routes = options.routes;
         this._bindRoutes();
         this.initialize(options);
    -  };

    Cached regular expressions for matching named param parts and splatted + };

    Cached regular expressions for matching named param parts and splatted parts of route strings.

      var namedParam = /:([\w\d]+)/g;
    -  var splatParam = /\*([\w\d]+)/g;

    Set up all inheritable Backbone.Controller properties and methods.

      _.extend(Backbone.Controller.prototype, Backbone.Events, {

    Initialize is an empty function by default. Override it with your own -initialization logic.

        initialize : function(){},

    Manually bind a single named route to a callback. For example:

    + var splatParam = /\*([\w\d]+)/g;

    Set up all inheritable Backbone.Controller properties and methods.

      _.extend(Backbone.Controller.prototype, Backbone.Events, {

    Initialize is an empty function by default. Override it with your own +initialization logic.

        initialize : function(){},

    Manually bind a single named route to a callback. For example:

    this.route('search/:query/p:num', 'search', function(query, num) {
       ...
    @@ -391,33 +400,33 @@
             callback.apply(this, args);
             this.trigger.apply(this, ['route:' + name].concat(args));
           }, this));
    -    },

    Simple proxy to Backbone.history to save a fragment into the history, + },

    Simple proxy to Backbone.history to save a fragment into the history, without triggering routes.

        saveLocation : function(fragment) {
           Backbone.history.saveLocation(fragment);
    -    },

    Bind all defined routes to Backbone.history.

        _bindRoutes : function() {
    +    },

    Bind all defined routes to Backbone.history.

        _bindRoutes : function() {
           if (!this.routes) return;
           for (var route in this.routes) {
             var name = this.routes[route];
             this.route(route, name, this[name]);
           }
    -    },

    Convert a route string into a regular expression, suitable for matching + },

    Convert a route string into a regular expression, suitable for matching against the current location fragment.

        _routeToRegExp : function(route) {
           route = route.replace(namedParam, "([^\/]*)").replace(splatParam, "(.*?)");
           return new RegExp('^' + route + '$');
    -    },

    Given a route, and a URL fragment that it matches, return the array of + },

    Given a route, and a URL fragment that it matches, return the array of extracted parameters.

        _extractParameters : function(route, fragment) {
           return route.exec(fragment).slice(1);
         }
     
    -  });

    Backbone.History

    Handles cross-browser history management, based on URL hashes. If the + });

    Backbone.History

    Handles cross-browser history management, based on URL hashes. If the browser does not support onhashchange, falls back to polling.

      Backbone.History = function() {
         this.handlers = [];
         this.fragment = this.getFragment();
         _.bindAll(this, 'checkUrl');
    -  };

    Cached regex for cleaning hashes.

      var hashStrip = /^#*/;

    Set up all inheritable Backbone.History properties and methods.

      _.extend(Backbone.History.prototype, {

    The default interval to poll for hash changes, if necessary, is -twenty times a second.

        interval: 50,

    Get the cross-browser normalized URL fragment.

        getFragment : function(loc) {
    +  };

    Cached regex for cleaning hashes.

      var hashStrip = /^#*/;

    Set up all inheritable Backbone.History properties and methods.

      _.extend(Backbone.History.prototype, {

    The default interval to poll for hash changes, if necessary, is +twenty times a second.

        interval: 50,

    Get the cross-browser normalized URL fragment.

        getFragment : function(loc) {
           return (loc || window.location).hash.replace(hashStrip, '');
    -    },

    Start the hash change handling, returning true if the current URL matches + },

    Start the hash change handling, returning true if the current URL matches an existing route, and false otherwise.

        start : function() {
           var docMode = document.documentMode;
           var oldIE = ($.browser.msie && (!docMode || docMode <= 7));
    @@ -430,10 +439,10 @@
             setInterval(this.checkUrl, this.interval);
           }
           return this.loadUrl();
    -    },

    Add a route to be tested when the hash changes. Routes are matched in the + },

    Add a route to be tested when the hash changes. Routes are matched in the order they are added.

        route : function(route, callback) {
           this.handlers.push({route : route, callback : callback});
    -    },

    Checks the current URL to see if it has changed, and if it has, + },

    Checks the current URL to see if it has changed, and if it has, calls loadUrl, normalizing across the hidden iframe.

        checkUrl : function() {
           var current = this.getFragment();
           if (current == this.fragment && this.iframe) {
    @@ -445,7 +454,7 @@
             window.location.hash = this.iframe.location.hash = current;
           }
           this.loadUrl();
    -    },

    Attempt to load the current URL fragment. If a route succeeds with a + },

    Attempt to load the current URL fragment. If a route succeeds with a match, returns true. If no defined routes matches the fragment, returns false.

        loadUrl : function() {
           var fragment = this.fragment = this.getFragment();
    @@ -456,7 +465,7 @@
             }
           });
           return matched;
    -    },

    Save a fragment into the hash history. You are responsible for properly + },

    Save a fragment into the hash history. You are responsible for properly URL-encoding the fragment in advance. This does not trigger a hashchange event.

        saveLocation : function(fragment) {
           fragment = (fragment || '').replace(hashStrip, '');
    @@ -468,27 +477,26 @@
           }
         }
     
    -  });

    Backbone.View

    Creating a Backbone.View creates its initial element outside of the DOM, + });

    Backbone.View

    Creating a Backbone.View creates its initial element outside of the DOM, if an existing element is not provided...

      Backbone.View = function(options) {
         this._configure(options || {});
         this._ensureElement();
         this.delegateEvents();
         this.initialize(options);
    -  };

    jQuery lookup, scoped to DOM elements within the current view. -This should be prefered to global jQuery lookups, if you're dealing with -a specific view.

      var jQueryDelegate = function(selector) {
    +  };

    Element lookup, scoped to DOM elements within the current view. +This should be prefered to global lookups, if you're dealing with +a specific view.

      var selectorDelegate = function(selector) {
         return $(selector, this.el);
    -  };

    Cached regex to split keys for delegate.

      var eventSplitter = /^(\w+)\s*(.*)$/;

    Set up all inheritable Backbone.View properties and methods.

      _.extend(Backbone.View.prototype, Backbone.Events, {

    The default tagName of a View's element is "div".

        tagName : 'div',

    Attach the jQuery function as the $ and jQuery properties.

        $       : jQueryDelegate,
    -    jQuery  : jQueryDelegate,

    Initialize is an empty function by default. Override it with your own -initialization logic.

        initialize : function(){},

    render is the core function that your view should override, in order + };

    Cached regex to split keys for delegate.

      var eventSplitter = /^(\w+)\s*(.*)$/;

    Set up all inheritable Backbone.View properties and methods.

      _.extend(Backbone.View.prototype, Backbone.Events, {

    The default tagName of a View's element is "div".

        tagName : 'div',

    Attach the selectorDelegate function as the $ property.

        $       : selectorDelegate,

    Initialize is an empty function by default. Override it with your own +initialization logic.

        initialize : function(){},

    render is the core function that your view should override, in order to populate its element (this.el), with the appropriate HTML. The convention is for render to always return this.

        render : function() {
           return this;
    -    },

    Remove this view from the DOM. Note that the view isn't present in the + },

    Remove this view from the DOM. Note that the view isn't present in the DOM by default, so calling this method may be a no-op.

        remove : function() {
           $(this.el).remove();
           return this;
    -    },

    For small amounts of DOM Elements, where a full-blown template isn't + },

    For small amounts of DOM Elements, where a full-blown template isn't needed, use make to manufacture elements, one at a time.

    var el = this.make('li', {'class': 'row'}, this.model.get('title'));
    @@ -497,7 +505,7 @@
           if (attributes) $(el).attr(attributes);
           if (content) $(el).html(content);
           return el;
    -    },

    Set callbacks, where this.callbacks is a hash of

    + },

    Set callbacks, where this.callbacks is a hash of

    {"event selector": "callback"}

    @@ -508,7 +516,7 @@

    pairs. Callbacks will be bound to the view, with this set properly. -Uses jQuery event delegation for efficiency. +Uses event delegation for efficiency. Omitting the selector binds the event to this.el. This only works for delegate-able events: not focus, blur, and not change, submit, and reset in Internet Explorer.

        delegateEvents : function(events) {
    @@ -525,7 +533,7 @@
               $(this.el).delegate(selector, eventName, method);
             }
           }
    -    },

    Performs the initial configuration of a View with a set of options. + },

    Performs the initial configuration of a View with a set of options. Keys with special meaning (model, collection, id, className), are attached directly to the view.

        _configure : function(options) {
           if (this.options) options = _.extend({}, this.options, options);
    @@ -536,27 +544,27 @@
           if (options.className)  this.className  = options.className;
           if (options.tagName)    this.tagName    = options.tagName;
           this.options = options;
    -    },

    Ensure that the View has a DOM element to render into.

        _ensureElement : function() {
    +    },

    Ensure that the View has a DOM element to render into.

        _ensureElement : function() {
           if (this.el) return;
           var attrs = {};
           if (this.id) attrs.id = this.id;
    -      if (this.className) attrs.className = this.className;
    +      if (this.className) attrs["class"] = this.className;
           this.el = this.make(this.tagName, attrs);
         }
     
    -  });

    The self-propagating extend function that Backbone classes use.

      var extend = function (protoProps, classProps) {
    +  });

    The self-propagating extend function that Backbone classes use.

      var extend = function (protoProps, classProps) {
         var child = inherits(this, protoProps, classProps);
         child.extend = extend;
         return child;
    -  };

    Set up inheritance for the model, collection, and view.

      Backbone.Model.extend = Backbone.Collection.extend =
    -    Backbone.Controller.extend = Backbone.View.extend = extend;

    Map from CRUD to HTTP for our default Backbone.sync implementation.

      var methodMap = {
    +  };

    Set up inheritance for the model, collection, and view.

      Backbone.Model.extend = Backbone.Collection.extend =
    +    Backbone.Controller.extend = Backbone.View.extend = extend;

    Map from CRUD to HTTP for our default Backbone.sync implementation.

      var methodMap = {
         'create': 'POST',
         'update': 'PUT',
         'delete': 'DELETE',
         'read'  : 'GET'
    -  };

    Backbone.sync

    Override this function to change the manner in which Backbone persists + };

    Backbone.sync

    Override this function to change the manner in which Backbone persists models to the server. You will be passed the type of request, and the -model in question. By default, uses jQuery to make a RESTful Ajax request +model in question. By default, uses makes a RESTful Ajax request to the model's url(). Some possible customizations could be:

      @@ -573,7 +581,7 @@ it difficult to read the body of PUT requests.

      Backbone.sync = function(method, model, success, error) {
         var type = methodMap[method];
         var modelJSON = (method === 'create' || method === 'update') ?
    -                    JSON.stringify(model.toJSON()) : null;

    Default JSON-request options.

        var params = {
    +                    JSON.stringify(model.toJSON()) : null;

    Default JSON-request options.

        var params = {
           url:          getUrl(model),
           type:         type,
           contentType:  'application/json',
    @@ -582,11 +590,11 @@
           processData:  false,
           success:      success,
           error:        error
    -    };

    For older servers, emulate JSON by encoding the request into an HTML-form.

        if (Backbone.emulateJSON) {
    +    };

    For older servers, emulate JSON by encoding the request into an HTML-form.

        if (Backbone.emulateJSON) {
           params.contentType = 'application/x-www-form-urlencoded';
           params.processData = true;
           params.data        = modelJSON ? {model : modelJSON} : {};
    -    }

    For older servers, emulate HTTP by mimicking the HTTP method with _method + }

    For older servers, emulate HTTP by mimicking the HTTP method with _method And an X-HTTP-Method-Override header.

        if (Backbone.emulateHTTP) {
           if (type === 'PUT' || type === 'DELETE') {
             if (Backbone.emulateJSON) params.data._method = type;
    @@ -595,26 +603,36 @@
               xhr.setRequestHeader("X-HTTP-Method-Override", type);
             };
           }
    -    }

    Make the request.

        $.ajax(params);
    -  };

    Helpers

    Shared empty constructor function to aid in prototype-chain creation.

      var ctor = function(){};

    Helper function to correctly set up the prototype chain, for subclasses. + }

    Make the request.

        $.ajax(params);
    +  };

    Helpers

    Shared empty constructor function to aid in prototype-chain creation.

      var ctor = function(){};

    Helper function to correctly set up the prototype chain, for subclasses. Similar to goog.inherits, but uses a hash of prototype properties and class properties to be extended.

      var inherits = function(parent, protoProps, staticProps) {
    -    var child;

    The constructor function for the new subclass is either defined by you + var child;

    The constructor function for the new subclass is either defined by you (the "constructor" property in your extend definition), or defaulted by us to simply call super().

        if (protoProps && protoProps.hasOwnProperty('constructor')) {
           child = protoProps.constructor;
         } else {
           child = function(){ return parent.apply(this, arguments); };
    -    }

    Set the prototype chain to inherit from parent, without calling + }

    Set the prototype chain to inherit from parent, without calling parent's constructor function.

        ctor.prototype = parent.prototype;
    -    child.prototype = new ctor();

    Add prototype properties (instance properties) to the subclass, -if supplied.

        if (protoProps) _.extend(child.prototype, protoProps);

    Add static properties to the constructor function, if supplied.

        if (staticProps) _.extend(child, staticProps);

    Correctly set child's prototype.constructor, for instanceof.

        child.prototype.constructor = child;

    Set a convenience property in case the parent's prototype is needed later.

        child.__super__ = parent.prototype;
    +    child.prototype = new ctor();

    Add prototype properties (instance properties) to the subclass, +if supplied.

        if (protoProps) _.extend(child.prototype, protoProps);

    Add static properties to the constructor function, if supplied.

        if (staticProps) _.extend(child, staticProps);

    Correctly set child's prototype.constructor, for instanceof.

        child.prototype.constructor = child;

    Set a convenience property in case the parent's prototype is needed later.

        child.__super__ = parent.prototype;
     
         return child;
    -  };

    Helper function to get a URL from a Model or Collection as a property + };

    Helper function to get a URL from a Model or Collection as a property or as a function.

      var getUrl = function(object) {
         if (!(object && object.url)) throw new Error("A 'url' property or function must be specified");
         return _.isFunction(object.url) ? object.url() : object.url;
    +  };

    Wrap an optional error callback with a fallback error event.

      var wrapError = function(onError, model, options) {
    +    return function(resp) {
    +      if (onError) {
    +        onError(model, resp);
    +      } else {
    +        model.trigger('error', model, resp, options);
    +      }
    +    };
    +  };

    Helper function to escape a string for HTML rendering.

      var escapeHTML = function(string) {
    +    return string.replace(/&(?!\w+;)/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
       };
     
     })();
    diff --git a/examples/todos/index.html b/examples/todos/index.html
    index ff00038a4..1ff569d8d 100644
    --- a/examples/todos/index.html
    +++ b/examples/todos/index.html
    @@ -6,7 +6,7 @@
         
         
         
    -    
    +    
         
         
         
    diff --git a/index.html b/index.html
    index 1664d5b65..25a9462b3 100644
    --- a/index.html
    +++ b/index.html
    @@ -142,15 +142,15 @@
     
     
       
     
       
    @@ -285,7 +286,7 @@

    The project is hosted on GitHub, and the annotated source code is available, - as well as an online test suite, and + as well as an online test suite, and example application.

    @@ -310,12 +311,12 @@

    - - + + - - + +
    Development Version (0.3.2)33kb, Uncompressed with CommentsDevelopment Version (0.3.3)35kb, Uncompressed with Comments
    Production Version (0.3.2)3.7kb, Packed and GzippedProduction Version (0.3.3)3.9kb, Packed and Gzipped
    @@ -324,9 +325,9 @@

    Underscore.js. For RESTful persistence, and DOM manipulation with Backbone.View, - it's highly recommended to include jQuery, - and json2.js - (both of which you may already have on the page). + it's highly recommended to include + json2.js, and either + jQuery or Zepto.

    Introduction

    @@ -369,8 +370,9 @@

    Introduction

    Both frameworks measure in the hundreds of kilobytes when packed and gzipped, and megabytes of JavaScript, CSS, and images when loaded in the browser — there's a lot of room underneath for libraries of a more moderate scope. - Backbone is a 2 kilobyte include that provides - just the core concepts of models, events, collections, views, and persistence. + Backbone is a 4 kilobyte include that provides + just the core concepts of models, events, collections, views, controllers, + and persistence.

    @@ -548,6 +550,23 @@

    Backbone.Model

    Get the current value of an attribute from the model. For example: note.get("title")

    + +

    + escapemodel.escape(attribute) +
    + Similar to get, but returns the HTML-escaped version + of a model's attribute. If you're interpolating data from the model into + HTML, using escape to retrieve attributes will prevent + XSS attacks. +

    + +
    +var hacker = new Backbone.Model({
    +  name: "<script>alert('xss')</script>"
    +});
    +
    +alert(hacker.escape('name'));
    +

    setmodel.set(attributes, [options]) @@ -578,7 +597,7 @@

    Backbone.Model

    Remove an attribute by deleting it from the internal attributes hash. Fires a "change" event unless silent is passed as an option.

    - +

    clearmodel.clear([options])
    @@ -614,15 +633,15 @@

    Backbone.Model

    them directly. If you'd like to retrieve and munge a copy of the model's attributes, use toJSON instead.

    - +

    defaultsmodel.defaults
    The defaults hash can be used to specify the default attributes - for your model. When creating an instance of the model, any unspecified + for your model. When creating an instance of the model, any unspecified attributes will be set to their default value.

    - +
     var Meal = Backbone.Model.extend({
       defaults: {
    @@ -807,13 +826,13 @@ 

    Backbone.Model

    Override this if you need to work with a preexisting API, or better namespace your responses.

    - +

    If you're working with a Rails backend, you'll notice that Rails' default to_json implementation includes a model's attributes under a namespace. To disable this behavior for seamless Backbone integration, set:

    - +
     ActiveRecord::Base.include_root_in_json = false
     
    @@ -1288,24 +1307,24 @@

    Backbone.Controller

    Backbone.Controller provides methods for routing client-side URL fragments, and connecting them to actions and events.

    - +

    - Backbone controllers do not yet make use of HTML5 pushState and - replaceState. Currently, pushState and replaceState - need special handling on the server-side, cause you to mint duplicate URLs, - and have an incomplete API. We may start supporting them in the future + Backbone controllers do not yet make use of HTML5 pushState and + replaceState. Currently, pushState and replaceState + need special handling on the server-side, cause you to mint duplicate URLs, + and have an incomplete API. We may start supporting them in the future when these issues have been resolved.

    - +

    - During page load, after your application has finished creating all of its controllers, + During page load, after your application has finished creating all of its controllers, be sure to call Backbone.history.start() to route the initial URL.

    - +

    extendBackbone.Controller.extend(properties, [classProperties])
    - Get started by creating a custom controller class. You'll + Get started by creating a custom controller class. You'll want to define actions that are triggered when certain URL fragments are matched, and provide a routes hash that pairs routes to actions. @@ -1319,11 +1338,11 @@

    Backbone.Controller

    "search/:query": "search", // #search/kiwis "search/:query/p:page": "search" // #search/kiwis/p7 }, - + help: function() { ... }, - + search: function(query, page) { ... } @@ -1340,23 +1359,23 @@

    Backbone.Controller

    component between slashes; and splat parts *splat, which can match any number of URL components.

    - +

    - For example, a route of "search/:query/p:page" will match - a fragment of #search/obama/p2, passing "obama" - and "2" to the action. A route of "file/*path" will - match #file/nested/folder/file.txt, + For example, a route of "search/:query/p:page" will match + a fragment of #search/obama/p2, passing "obama" + and "2" to the action. A route of "file/*path" will + match #file/nested/folder/file.txt, passing "nested/folder/file.txt" to the action.

    - +

    When the visitor presses the back button, or enters a URL, and a particular - route is matched, the name of the action will be fired as an + route is matched, the name of the action will be fired as an event, so that other objects can listen to the controller, and be notified. In the following example, visiting #help/uploading will fire a route:help event from the controller.

    - +
     routes: {
       "help/:page":         "help",
    @@ -1375,31 +1394,31 @@ 

    Backbone.Controller

    constructor / initializenew Controller([options])
    - When creating a new controller, you may pass its - routes hash directly as an option, if you - choose. All options will also be passed to your initialize + When creating a new controller, you may pass its + routes hash directly as an option, if you + choose. All options will also be passed to your initialize function, if defined.

    - +

    routecontroller.route(route, name, callback)
    Manually create a route for the controller, The route argument may be a routing string or regular expression. - Each matching capture from the route or regular expression will be passed as + Each matching capture from the route or regular expression will be passed as an argument to the callback. The name argument will be triggered as a "route:name" event whenever the route is matched.

    - +
     initialize: function(options) {
    -  
    +
       // Matches #page/10, passing "10"
       this.route("page/:number", "page", function(number){ ... });
    -  
    +
       // Matches /117-a/b/c/open, passing "117-a/b/c"
       this.route(/^(.*?)\/open$/, "open", function(id){ ... });
    -  
    +
     }
     
    @@ -1411,32 +1430,32 @@

    Backbone.Controller

    without triggering a hashchange event. (If you would prefer to trigger the event and routing, you can just set the hash directly.)

    - +
     openPage: function(pageNumber) {
    -  this.document.pages.at(pageNumber).open();  
    +  this.document.pages.at(pageNumber).open();
       this.saveLocation("page/" + pageNumber);
     }
     
    - +

    Backbone.history

    History serves as a global router (per frame) to handle hashchange - events, match the appropriate route, and trigger callbacks. You shouldn't + events, match the appropriate route, and trigger callbacks. You shouldn't ever have to create one of these yourself — you should use the reference to Backbone.history that will be created for you automatically if you make use of Controllers with routes.

    - +

    startBackbone.history.start()
    - When all of your Controllers have been created, + When all of your Controllers have been created, and all of the routes are set up properly, call Backbone.history.start() to begin monitoring hashchange events, and dispatching routes.

    - +
     $(function(){
       new WorkspaceController();
    @@ -1450,7 +1469,7 @@ 

    Backbone.sync

    Backbone.sync is the function the Backbone calls every time it attempts to read or save a model to the server. By default, it uses - jQuery.ajax to make a RESTful JSON request. You can override + (jQuery/Zepto).ajax to make a RESTful JSON request. You can override it in order to use a different persistence strategy, such as WebSockets, XML transport, or Local Storage.

    @@ -1467,10 +1486,10 @@

    Backbone.sync

    - With the default implementation, when Backbone.sync sends up a request to save + With the default implementation, when Backbone.sync sends up a request to save a model, its attributes will be passed, serialized as JSON, and sent in the HTTP body - with content-type application/json. When returning a JSON response, - send down the attributes of the model that have been changed by the server, and need + with content-type application/json. When returning a JSON response, + send down the attributes of the model that have been changed by the server, and need to be updated on the client. When responding to a "read" request from a collection (Collection#fetch), send down an array of model attribute objects. @@ -1528,8 +1547,8 @@

    Backbone.sync


    If you're working with a legacy web server that can't handle requests encoded as application/json, setting Backbone.emulateJSON = true; - will cause the JSON to be serialized under a model parameter, and - the request to be made with a application/x-www-form-urlencoded + will cause the JSON to be serialized under a model parameter, and + the request to be made with a application/x-www-form-urlencoded mime type, as if from an HTML form.

    @@ -1620,11 +1639,11 @@

    Backbone.View

    and id properties, if specified. If not, el is an empty div.

    -

    - $ (jQuery)view.$(selector) +

    + $ (jQuery or Zepto)view.$(selector)
    - If jQuery is included on the page, each view has a $ or jQuery - function that runs queries scoped within the view's element. If you use this + If jQuery or Zepto is included on the page, each view has a + $ function that runs queries scoped within the view's element. If you use this scoped jQuery function, you don't have to use model ids as part of your query to pull out specific elements in a list, and can rely much more on HTML class attributes. It's equivalent to running: $(selector, this.el) @@ -1682,7 +1701,7 @@

    Backbone.View

    to package up JavaScript templates stored in /app/views as part of our main core.js asset package.

    - +

    removeview.remove()
    @@ -1788,26 +1807,26 @@

    Examples

    Todos
    - +

    - The DocumentCloud workspace - is built on Backbone.js, with Documents, Projects, + The DocumentCloud workspace + is built on Backbone.js, with Documents, Projects, Notes, and Accounts all as Backbone models and collections.

    - +
    DocumentCloud Workspace
    - +

    - Ben Nolan created + Ben Nolan created an example "Backbone Mobile" application, combining Backbone.js with jQuery Mobile. You can - try the app - in your browser, or view the + try the app + in your browser, or view the source code on Github.

    - +

    Change Log

    - + +

    + 0.3.3Dec 1, 2010
    + Backbone.js now supports
    Zepto, alongside + jQuery, as a framework for DOM manipulation and Ajax support. + Implemented Model#escape, to efficiently handle + attributes intended for HTML interpolation. When trying to persist a model, + failed requests will now trigger an "error" event. The + ubiquitous options argument is now passed as the final argument + to all "change" events. +

    +

    0.3.2Nov 23, 2010
    Bugfix for IE7 + iframe-based "hashchange" events. sync may now be overridden on a per-model, or per-collection basis. Fixed recursion error - when calling save with no changed attributes, within a + when calling save with no changed attributes, within a "change" event.

    - +

    0.3.1Nov 15, 2010
    All "add" and "remove" events are now sent through the model, so that views can listen for them without having to know about the collection. Added a remove method to Backbone.View. toJSON is no longer called at all for 'read' and 'delete' requests. - Backbone routes are now able to load empty URL fragments. + Backbone routes are now able to load empty URL fragments.

    - +

    0.3.0Nov 9, 2010
    - Backbone now has Controllers and - History, for doing client-side routing based on - URL fragments. + Backbone now has Controllers and + History, for doing client-side routing based on + URL fragments. Added emulateHTTP to provide support for legacy servers that don't do PUT and DELETE. Added emulateJSON for servers that can't accept application/json @@ -1893,7 +1923,7 @@

    Change Log

    - + diff --git a/package.json b/package.json index fe242f541..da66d8777 100644 --- a/package.json +++ b/package.json @@ -10,5 +10,5 @@ }, "lib" : ".", "main" : "backbone.js", - "version" : "0.3.2" + "version" : "0.3.3" }