Commit 79270d78 authored by Sindre Sorhus's avatar Sindre Sorhus

Backbone app - use Bower components

#475
parent 82c03e2a
{
"name": "todomvc-backbone",
"version": "0.0.0",
"dependencies": {
"backbone": "~0.9.10",
"lodash": "~1.0.1",
"jquery": "~1.9.1",
"todomvc-common": "~0.1.0",
"backbone.localStorage": "~1.1.0"
}
}
/** /**
* Backbone localStorage Adapter * Backbone localStorage Adapter
* Version 1.1.0
*
* https://github.com/jeromegn/Backbone.localStorage * https://github.com/jeromegn/Backbone.localStorage
*/ */
(function (root, factory) {
(function(_, Backbone) { if (typeof define === "function" && define.amd) {
// AMD. Register as an anonymous module.
define(["underscore","backbone"], function(_, Backbone) {
// Use global variables if the locals is undefined.
return factory(_ || root._, Backbone || root.Backbone);
});
} else {
// RequireJS isn't being used. Assume underscore and backbone is loaded in <script> tags
factory(_, Backbone);
}
}(this, function(_, Backbone) {
// A simple module to replace `Backbone.sync` with *localStorage*-based // A simple module to replace `Backbone.sync` with *localStorage*-based
// persistence. Models are given GUIDS, and saved into a JSON object. Simple // persistence. Models are given GUIDS, and saved into a JSON object. Simple
// as that. // as that.
...@@ -47,39 +59,51 @@ _.extend(Backbone.LocalStorage.prototype, { ...@@ -47,39 +59,51 @@ _.extend(Backbone.LocalStorage.prototype, {
this.localStorage().setItem(this.name+"-"+model.id, JSON.stringify(model)); this.localStorage().setItem(this.name+"-"+model.id, JSON.stringify(model));
this.records.push(model.id.toString()); this.records.push(model.id.toString());
this.save(); this.save();
return model.toJSON(); return this.find(model);
}, },
// Update a model by replacing its copy in `this.data`. // Update a model by replacing its copy in `this.data`.
update: function(model) { update: function(model) {
this.localStorage().setItem(this.name+"-"+model.id, JSON.stringify(model)); this.localStorage().setItem(this.name+"-"+model.id, JSON.stringify(model));
if (!_.include(this.records, model.id.toString())) this.records.push(model.id.toString()); this.save(); if (!_.include(this.records, model.id.toString()))
return model.toJSON(); this.records.push(model.id.toString()); this.save();
return this.find(model);
}, },
// Retrieve a model from `this.data` by id. // Retrieve a model from `this.data` by id.
find: function(model) { find: function(model) {
return JSON.parse(this.localStorage().getItem(this.name+"-"+model.id)); return this.jsonData(this.localStorage().getItem(this.name+"-"+model.id));
}, },
// Return the array of all models currently in storage. // Return the array of all models currently in storage.
findAll: function() { findAll: function() {
return _(this.records).chain() return _(this.records).chain()
.map(function(id){return JSON.parse(this.localStorage().getItem(this.name+"-"+id));}, this) .map(function(id){
return this.jsonData(this.localStorage().getItem(this.name+"-"+id));
}, this)
.compact() .compact()
.value(); .value();
}, },
// Delete a model from `this.data`, returning it. // Delete a model from `this.data`, returning it.
destroy: function(model) { destroy: function(model) {
if (model.isNew())
return false
this.localStorage().removeItem(this.name+"-"+model.id); this.localStorage().removeItem(this.name+"-"+model.id);
this.records = _.reject(this.records, function(record_id){return record_id == model.id.toString();}); this.records = _.reject(this.records, function(id){
return id === model.id.toString();
});
this.save(); this.save();
return model; return model;
}, },
localStorage: function() { localStorage: function() {
return localStorage; return localStorage;
},
// fix for "illegal access" error on Android when JSON.parse is passed null
jsonData: function (data) {
return data && JSON.parse(data);
} }
}); });
...@@ -90,22 +114,60 @@ _.extend(Backbone.LocalStorage.prototype, { ...@@ -90,22 +114,60 @@ _.extend(Backbone.LocalStorage.prototype, {
Backbone.LocalStorage.sync = window.Store.sync = Backbone.localSync = function(method, model, options) { Backbone.LocalStorage.sync = window.Store.sync = Backbone.localSync = function(method, model, options) {
var store = model.localStorage || model.collection.localStorage; var store = model.localStorage || model.collection.localStorage;
var resp, syncDfd = $.Deferred && $.Deferred(); //If $ is having Deferred - use it. var resp, errorMessage, syncDfd = $.Deferred && $.Deferred(); //If $ is having Deferred - use it.
try {
switch (method) { switch (method) {
case "read": resp = model.id != undefined ? store.find(model) : store.findAll(); break; case "read":
case "create": resp = store.create(model); break; resp = model.id != undefined ? store.find(model) : store.findAll();
case "update": resp = store.update(model); break; break;
case "delete": resp = store.destroy(model); break; case "create":
resp = store.create(model);
break;
case "update":
resp = store.update(model);
break;
case "delete":
resp = store.destroy(model);
break;
}
} catch(error) {
if (error.code === DOMException.QUOTA_EXCEEDED_ERR && window.localStorage.length === 0)
errorMessage = "Private browsing is unsupported";
else
errorMessage = error.message;
} }
if (resp) { if (resp) {
if (options && options.success) options.success(resp); if (options && options.success)
if (syncDfd) syncDfd.resolve(); if (Backbone.VERSION === "0.9.10") {
options.success(model, resp, options);
} else { } else {
if (options && options.error) options.error("Record not found"); options.success(resp);
if (syncDfd) syncDfd.reject();
} }
if (syncDfd)
syncDfd.resolve(resp);
} else {
errorMessage = errorMessage ? errorMessage
: "Record Not Found";
if (options && options.error)
if (Backbone.VERSION === "0.9.10") {
options.error(model, errorMessage, options);
} else {
options.error(errorMessage);
}
if (syncDfd)
syncDfd.reject(errorMessage);
}
// add compatibility with $.ajax
// always execute callback for success and error
if (options && options.complete) options.complete(resp);
return syncDfd && syncDfd.promise(); return syncDfd && syncDfd.promise();
}; };
...@@ -113,8 +175,7 @@ Backbone.LocalStorage.sync = window.Store.sync = Backbone.localSync = function(m ...@@ -113,8 +175,7 @@ Backbone.LocalStorage.sync = window.Store.sync = Backbone.localSync = function(m
Backbone.ajaxSync = Backbone.sync; Backbone.ajaxSync = Backbone.sync;
Backbone.getSyncMethod = function(model) { Backbone.getSyncMethod = function(model) {
if(model.localStorage || (model.collection && model.collection.localStorage)) if(model.localStorage || (model.collection && model.collection.localStorage)) {
{
return Backbone.localSync; return Backbone.localSync;
} }
...@@ -127,4 +188,5 @@ Backbone.sync = function(method, model, options) { ...@@ -127,4 +188,5 @@ Backbone.sync = function(method, model, options) {
return Backbone.getSyncMethod(model).apply(this, [method, model, options]); return Backbone.getSyncMethod(model).apply(this, [method, model, options]);
}; };
})(_, Backbone); return Backbone.LocalStorage;
}));
\ No newline at end of file
// Backbone.js 0.9.9 // Backbone.js 0.9.10
// (c) 2010-2012 Jeremy Ashkenas, DocumentCloud Inc. // (c) 2010-2012 Jeremy Ashkenas, DocumentCloud Inc.
// Backbone may be freely distributed under the MIT license. // Backbone may be freely distributed under the MIT license.
...@@ -34,7 +34,7 @@ ...@@ -34,7 +34,7 @@
} }
// Current version of the library. Keep in sync with `package.json`. // Current version of the library. Keep in sync with `package.json`.
Backbone.VERSION = '0.9.9'; Backbone.VERSION = '0.9.10';
// Require Underscore, if we're on the server, and it's not already present. // Require Underscore, if we're on the server, and it's not already present.
var _ = root._; var _ = root._;
...@@ -88,7 +88,7 @@ ...@@ -88,7 +88,7 @@
// Optimized internal dispatch function for triggering events. Tries to // Optimized internal dispatch function for triggering events. Tries to
// keep the usual cases speedy (most Backbone events have 3 arguments). // keep the usual cases speedy (most Backbone events have 3 arguments).
var triggerEvents = function(obj, events, args) { var triggerEvents = function(events, args) {
var ev, i = -1, l = events.length; var ev, i = -1, l = events.length;
switch (args.length) { switch (args.length) {
case 0: while (++i < l) (ev = events[i]).callback.call(ev.ctx); case 0: while (++i < l) (ev = events[i]).callback.call(ev.ctx);
...@@ -142,7 +142,7 @@ ...@@ -142,7 +142,7 @@
// Remove one or many callbacks. If `context` is null, removes all // Remove one or many callbacks. If `context` is null, removes all
// callbacks with that function. If `callback` is null, removes all // callbacks with that function. If `callback` is null, removes all
// callbacks for the event. If `events` is null, removes all bound // callbacks for the event. If `name` is null, removes all bound
// callbacks for all events. // callbacks for all events.
off: function(name, callback, context) { off: function(name, callback, context) {
var list, ev, events, names, i, l, j, k; var list, ev, events, names, i, l, j, k;
...@@ -160,7 +160,8 @@ ...@@ -160,7 +160,8 @@
if (callback || context) { if (callback || context) {
for (j = 0, k = list.length; j < k; j++) { for (j = 0, k = list.length; j < k; j++) {
ev = list[j]; ev = list[j];
if ((callback && callback !== (ev.callback._callback || ev.callback)) || if ((callback && callback !== ev.callback &&
callback !== ev.callback._callback) ||
(context && context !== ev.context)) { (context && context !== ev.context)) {
events.push(ev); events.push(ev);
} }
...@@ -183,32 +184,33 @@ ...@@ -183,32 +184,33 @@
if (!eventsApi(this, 'trigger', name, args)) return this; if (!eventsApi(this, 'trigger', name, args)) return this;
var events = this._events[name]; var events = this._events[name];
var allEvents = this._events.all; var allEvents = this._events.all;
if (events) triggerEvents(this, events, args); if (events) triggerEvents(events, args);
if (allEvents) triggerEvents(this, allEvents, arguments); if (allEvents) triggerEvents(allEvents, arguments);
return this; return this;
}, },
// An inversion-of-control version of `on`. Tell *this* object to listen to // An inversion-of-control version of `on`. Tell *this* object to listen to
// an event in another object ... keeping track of what it's listening to. // an event in another object ... keeping track of what it's listening to.
listenTo: function(object, events, callback) { listenTo: function(obj, name, callback) {
var listeners = this._listeners || (this._listeners = {}); var listeners = this._listeners || (this._listeners = {});
var id = object._listenerId || (object._listenerId = _.uniqueId('l')); var id = obj._listenerId || (obj._listenerId = _.uniqueId('l'));
listeners[id] = object; listeners[id] = obj;
object.on(events, callback || this, this); obj.on(name, typeof name === 'object' ? this : callback, this);
return this; return this;
}, },
// Tell this object to stop listening to either specific events ... or // Tell this object to stop listening to either specific events ... or
// to every object it's currently listening to. // to every object it's currently listening to.
stopListening: function(object, events, callback) { stopListening: function(obj, name, callback) {
var listeners = this._listeners; var listeners = this._listeners;
if (!listeners) return; if (!listeners) return;
if (object) { if (obj) {
object.off(events, callback, this); obj.off(name, typeof name === 'object' ? this : callback, this);
if (!events && !callback) delete listeners[object._listenerId]; if (!name && !callback) delete listeners[obj._listenerId];
} else { } else {
if (typeof name === 'object') callback = this;
for (var id in listeners) { for (var id in listeners) {
listeners[id].off(null, null, this); listeners[id].off(name, callback, this);
} }
this._listeners = {}; this._listeners = {};
} }
...@@ -233,15 +235,14 @@ ...@@ -233,15 +235,14 @@
var defaults; var defaults;
var attrs = attributes || {}; var attrs = attributes || {};
this.cid = _.uniqueId('c'); this.cid = _.uniqueId('c');
this.changed = {};
this.attributes = {}; this.attributes = {};
this._changes = [];
if (options && options.collection) this.collection = options.collection; if (options && options.collection) this.collection = options.collection;
if (options && options.parse) attrs = this.parse(attrs); if (options && options.parse) attrs = this.parse(attrs, options) || {};
if (defaults = _.result(this, 'defaults')) _.defaults(attrs, defaults); if (defaults = _.result(this, 'defaults')) {
this.set(attrs, {silent: true}); attrs = _.defaults({}, attrs, defaults);
this._currentAttributes = _.clone(this.attributes); }
this._previousAttributes = _.clone(this.attributes); this.set(attrs, options);
this.changed = {};
this.initialize.apply(this, arguments); this.initialize.apply(this, arguments);
}; };
...@@ -285,47 +286,72 @@ ...@@ -285,47 +286,72 @@
return this.get(attr) != null; return this.get(attr) != null;
}, },
// ----------------------------------------------------------------------
// Set a hash of model attributes on the object, firing `"change"` unless // Set a hash of model attributes on the object, firing `"change"` unless
// you choose to silence it. // you choose to silence it.
set: function(key, val, options) { set: function(key, val, options) {
var attr, attrs; var attr, attrs, unset, changes, silent, changing, prev, current;
if (key == null) return this; if (key == null) return this;
// Handle both `"key", value` and `{key: value}` -style arguments. // Handle both `"key", value` and `{key: value}` -style arguments.
if (_.isObject(key)) { if (typeof key === 'object') {
attrs = key; attrs = key;
options = val; options = val;
} else { } else {
(attrs = {})[key] = val; (attrs = {})[key] = val;
} }
// Extract attributes and options. options || (options = {});
var silent = options && options.silent;
var unset = options && options.unset;
// Run validation. // Run validation.
if (!this._validate(attrs, options)) return false; if (!this._validate(attrs, options)) return false;
// Extract attributes and options.
unset = options.unset;
silent = options.silent;
changes = [];
changing = this._changing;
this._changing = true;
if (!changing) {
this._previousAttributes = _.clone(this.attributes);
this.changed = {};
}
current = this.attributes, prev = this._previousAttributes;
// Check for changes of `id`. // Check for changes of `id`.
if (this.idAttribute in attrs) this.id = attrs[this.idAttribute]; if (this.idAttribute in attrs) this.id = attrs[this.idAttribute];
var now = this.attributes; // For each `set` attribute, update or delete the current value.
// For each `set` attribute...
for (attr in attrs) { for (attr in attrs) {
val = attrs[attr]; val = attrs[attr];
if (!_.isEqual(current[attr], val)) changes.push(attr);
// Update or delete the current value, and track the change. if (!_.isEqual(prev[attr], val)) {
unset ? delete now[attr] : now[attr] = val; this.changed[attr] = val;
this._changes.push(attr, val); } else {
delete this.changed[attr];
}
unset ? delete current[attr] : current[attr] = val;
} }
// Signal that the model's state has potentially changed, and we need // Trigger all relevant attribute changes.
// to recompute the actual changes. if (!silent) {
this._hasComputed = false; if (changes.length) this._pending = true;
for (var i = 0, l = changes.length; i < l; i++) {
this.trigger('change:' + changes[i], this, current[changes[i]], options);
}
}
// Fire the `"change"` events. if (changing) return this;
if (!silent) this.change(options); if (!silent) {
while (this._pending) {
this._pending = false;
this.trigger('change', this, options);
}
}
this._pending = false;
this._changing = false;
return this; return this;
}, },
...@@ -343,16 +369,54 @@ ...@@ -343,16 +369,54 @@
return this.set(attrs, _.extend({}, options, {unset: true})); return this.set(attrs, _.extend({}, options, {unset: true}));
}, },
// 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 == null) return !_.isEmpty(this.changed);
return _.has(this.changed, attr);
},
// Return an object containing all the attributes that have changed, or
// false if there are no changed attributes. Useful for determining what
// parts of a view need to be updated and/or what attributes need to be
// persisted to the server. Unset attributes will be set to undefined.
// You can also pass an attributes object to diff against the model,
// determining if there *would be* a change.
changedAttributes: function(diff) {
if (!diff) return this.hasChanged() ? _.clone(this.changed) : false;
var val, changed = false;
var old = this._changing ? this._previousAttributes : this.attributes;
for (var attr in diff) {
if (_.isEqual(old[attr], (val = diff[attr]))) continue;
(changed || (changed = {}))[attr] = val;
}
return changed;
},
// Get the previous value of an attribute, recorded at the time the last
// `"change"` event was fired.
previous: function(attr) {
if (attr == null || !this._previousAttributes) return null;
return this._previousAttributes[attr];
},
// Get all of the attributes of the model at the time of the previous
// `"change"` event.
previousAttributes: function() {
return _.clone(this._previousAttributes);
},
// ---------------------------------------------------------------------
// 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, // model differs from its current attributes, they will be overriden,
// triggering a `"change"` event. // triggering a `"change"` event.
fetch: function(options) { fetch: function(options) {
options = options ? _.clone(options) : {}; options = options ? _.clone(options) : {};
if (options.parse === void 0) options.parse = true; if (options.parse === void 0) options.parse = true;
var model = this;
var success = options.success; var success = options.success;
options.success = function(resp, status, xhr) { options.success = function(model, resp, options) {
if (!model.set(model.parse(resp), options)) return false; if (!model.set(model.parse(resp, options), options)) return false;
if (success) success(model, resp, options); if (success) success(model, resp, options);
}; };
return this.sync('read', this, options); return this.sync('read', this, options);
...@@ -362,55 +426,51 @@ ...@@ -362,55 +426,51 @@
// If the server returns an attributes hash that differs, the model's // If the server returns an attributes hash that differs, the model's
// state will be `set` again. // state will be `set` again.
save: function(key, val, options) { save: function(key, val, options) {
var attrs, current, done; var attrs, success, method, xhr, attributes = this.attributes;
// Handle both `"key", value` and `{key: value}` -style arguments. // Handle both `"key", value` and `{key: value}` -style arguments.
if (key == null || _.isObject(key)) { if (key == null || typeof key === 'object') {
attrs = key; attrs = key;
options = val; options = val;
} else if (key != null) { } else {
(attrs = {})[key] = val; (attrs = {})[key] = val;
} }
options = options ? _.clone(options) : {};
// If we're "wait"-ing to set changed attributes, validate early. // If we're not waiting and attributes exist, save acts as `set(attr).save(null, opts)`.
if (options.wait) { if (attrs && (!options || !options.wait) && !this.set(attrs, options)) return false;
if (attrs && !this._validate(attrs, options)) return false;
current = _.clone(this.attributes);
}
// Regular saves `set` attributes before persisting to the server. options = _.extend({validate: true}, options);
var silentOptions = _.extend({}, options, {silent: true});
if (attrs && !this.set(attrs, options.wait ? silentOptions : options)) {
return false;
}
// Do not persist invalid models. // Do not persist invalid models.
if (!attrs && !this._validate(null, options)) return false; if (!this._validate(attrs, options)) return false;
// Set temporary attributes if `{wait: true}`.
if (attrs && options.wait) {
this.attributes = _.extend({}, attributes, attrs);
}
// After a successful server-side save, the client is (optionally) // After a successful server-side save, the client is (optionally)
// updated with the server-side state. // updated with the server-side state.
var model = this; if (options.parse === void 0) options.parse = true;
var success = options.success; success = options.success;
options.success = function(resp, status, xhr) { options.success = function(model, resp, options) {
done = true; // Ensure attributes are restored during synchronous saves.
var serverAttrs = model.parse(resp); model.attributes = attributes;
var serverAttrs = model.parse(resp, options);
if (options.wait) serverAttrs = _.extend(attrs || {}, serverAttrs); if (options.wait) serverAttrs = _.extend(attrs || {}, serverAttrs);
if (!model.set(serverAttrs, options)) return false; if (_.isObject(serverAttrs) && !model.set(serverAttrs, options)) {
return false;
}
if (success) success(model, resp, options); if (success) success(model, resp, options);
}; };
// Finish configuring and sending the Ajax request. // Finish configuring and sending the Ajax request.
var method = this.isNew() ? 'create' : (options.patch ? 'patch' : 'update'); method = this.isNew() ? 'create' : (options.patch ? 'patch' : 'update');
if (method == 'patch') options.attrs = attrs; if (method === 'patch') options.attrs = attrs;
var xhr = this.sync(method, this, options); xhr = this.sync(method, this, options);
// When using `wait`, reset attributes to original values unless // Restore attributes.
// `success` has been called already. if (attrs && options.wait) this.attributes = attributes;
if (!done && options.wait) {
this.clear(silentOptions);
this.set(current, silentOptions);
}
return xhr; return xhr;
}, },
...@@ -427,13 +487,13 @@ ...@@ -427,13 +487,13 @@
model.trigger('destroy', model, model.collection, options); model.trigger('destroy', model, model.collection, options);
}; };
options.success = function(resp) { options.success = function(model, resp, options) {
if (options.wait || model.isNew()) destroy(); if (options.wait || model.isNew()) destroy();
if (success) success(model, resp, options); if (success) success(model, resp, options);
}; };
if (this.isNew()) { if (this.isNew()) {
options.success(); options.success(this, null, options);
return false; return false;
} }
...@@ -453,7 +513,7 @@ ...@@ -453,7 +513,7 @@
// **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. // the model. The default implementation is just to pass the response along.
parse: function(resp) { parse: function(resp, options) {
return resp; return resp;
}, },
...@@ -467,115 +527,20 @@ ...@@ -467,115 +527,20 @@
return this.id == null; return this.id == null;
}, },
// Call this method to manually fire a `"change"` event for this model and // Check if the model is currently in a valid state.
// a `"change:attribute"` event for each changed attribute. isValid: function(options) {
// Calling this will cause all objects observing the model to update. return !this.validate || !this.validate(this.attributes, options);
change: function(options) {
var changing = this._changing;
this._changing = true;
// Generate the changes to be triggered on the model.
var triggers = this._computeChanges(true);
this._pending = !!triggers.length;
for (var i = triggers.length - 2; i >= 0; i -= 2) {
this.trigger('change:' + triggers[i], this, triggers[i + 1], options);
}
if (changing) return this;
// Trigger a `change` while there have been changes.
while (this._pending) {
this._pending = false;
this.trigger('change', this, options);
this._previousAttributes = _.clone(this.attributes);
}
this._changing = false;
return this;
},
// Determine if the model has changed since the last `"change"` event.
// If you specify an attribute name, determine if that attribute has changed.
hasChanged: function(attr) {
if (!this._hasComputed) this._computeChanges();
if (attr == null) return !_.isEmpty(this.changed);
return _.has(this.changed, attr);
},
// Return an object containing all the attributes that have changed, or
// false if there are no changed attributes. Useful for determining what
// parts of a view need to be updated and/or what attributes need to be
// persisted to the server. Unset attributes will be set to undefined.
// You can also pass an attributes object to diff against the model,
// determining if there *would be* a change.
changedAttributes: function(diff) {
if (!diff) return this.hasChanged() ? _.clone(this.changed) : false;
var val, changed = false, old = this._previousAttributes;
for (var attr in diff) {
if (_.isEqual(old[attr], (val = diff[attr]))) continue;
(changed || (changed = {}))[attr] = val;
}
return changed;
},
// Looking at the built up list of `set` attribute changes, compute how
// many of the attributes have actually changed. If `loud`, return a
// boiled-down list of only the real changes.
_computeChanges: function(loud) {
this.changed = {};
var already = {};
var triggers = [];
var current = this._currentAttributes;
var changes = this._changes;
// Loop through the current queue of potential model changes.
for (var i = changes.length - 2; i >= 0; i -= 2) {
var key = changes[i], val = changes[i + 1];
if (already[key]) continue;
already[key] = true;
// Check if the attribute has been modified since the last change,
// and update `this.changed` accordingly. If we're inside of a `change`
// call, also add a trigger to the list.
if (current[key] !== val) {
this.changed[key] = val;
if (!loud) continue;
triggers.push(key, val);
current[key] = val;
}
}
if (loud) this._changes = [];
// Signals `this.changed` is current to prevent duplicate calls from `this.hasChanged`.
this._hasComputed = true;
return triggers;
},
// Get the previous value of an attribute, recorded at the time the last
// `"change"` event was fired.
previous: function(attr) {
if (attr == null || !this._previousAttributes) return null;
return this._previousAttributes[attr];
},
// Get all of the attributes of the model at the time of the previous
// `"change"` event.
previousAttributes: function() {
return _.clone(this._previousAttributes);
}, },
// Run validation against the next complete set of model attributes, // Run validation against the next complete set of model attributes,
// returning `true` if all is well. If a specific `error` callback has // returning `true` if all is well. Otherwise, fire a general
// been passed, call that instead of firing the general `"error"` event. // `"error"` event and call the error callback, if specified.
_validate: function(attrs, options) { _validate: function(attrs, options) {
if (!this.validate) return true; if (!options.validate || !this.validate) return true;
attrs = _.extend({}, this.attributes, attrs); attrs = _.extend({}, this.attributes, attrs);
var error = this.validate(attrs, options); var error = this.validationError = this.validate(attrs, options) || null;
if (!error) return true; if (!error) return true;
if (options && options.error) options.error(this, error, options); this.trigger('invalid', this, error, options || {});
this.trigger('error', this, error, options);
return false; return false;
} }
...@@ -591,6 +556,7 @@ ...@@ -591,6 +556,7 @@
options || (options = {}); options || (options = {});
if (options.model) this.model = options.model; if (options.model) this.model = options.model;
if (options.comparator !== void 0) this.comparator = options.comparator; if (options.comparator !== void 0) this.comparator = options.comparator;
this.models = [];
this._reset(); this._reset();
this.initialize.apply(this, arguments); this.initialize.apply(this, arguments);
if (models) this.reset(models, _.extend({silent: true}, options)); if (models) this.reset(models, _.extend({silent: true}, options));
...@@ -618,74 +584,81 @@ ...@@ -618,74 +584,81 @@
return Backbone.sync.apply(this, arguments); return Backbone.sync.apply(this, arguments);
}, },
// Add a model, or list of models to the set. Pass **silent** to avoid // Add a model, or list of models to the set.
// firing the `add` event for every new model.
add: function(models, options) { add: function(models, options) {
var i, args, length, model, existing, needsSort;
var at = options && options.at;
var sort = ((options && options.sort) == null ? true : options.sort);
models = _.isArray(models) ? models.slice() : [models]; models = _.isArray(models) ? models.slice() : [models];
options || (options = {});
var i, l, model, attrs, existing, doSort, add, at, sort, sortAttr;
add = [];
at = options.at;
sort = this.comparator && (at == null) && options.sort != false;
sortAttr = _.isString(this.comparator) ? this.comparator : null;
// Turn bare objects into model references, and prevent invalid models // Turn bare objects into model references, and prevent invalid models
// from being added. // from being added.
for (i = models.length - 1; i >= 0; i--) { for (i = 0, l = models.length; i < l; i++) {
if(!(model = this._prepareModel(models[i], options))) { if (!(model = this._prepareModel(attrs = models[i], options))) {
this.trigger("error", this, models[i], options); this.trigger('invalid', this, attrs, options);
models.splice(i, 1);
continue; continue;
} }
models[i] = model;
existing = model.id != null && this._byId[model.id];
// If a duplicate is found, prevent it from being added and // If a duplicate is found, prevent it from being added and
// optionally merge it into the existing model. // optionally merge it into the existing model.
if (existing || this._byCid[model.cid]) { if (existing = this.get(model)) {
if (options && options.merge && existing) { if (options.merge) {
existing.set(model.attributes, options); existing.set(attrs === model ? model.attributes : attrs, options);
needsSort = sort; if (sort && !doSort && existing.hasChanged(sortAttr)) doSort = true;
} }
models.splice(i, 1);
continue; continue;
} }
// This is a new model, push it to the `add` list.
add.push(model);
// Listen to added models' events, and index models for lookup by // Listen to added models' events, and index models for lookup by
// `id` and by `cid`. // `id` and by `cid`.
model.on('all', this._onModelEvent, this); model.on('all', this._onModelEvent, this);
this._byCid[model.cid] = model; this._byId[model.cid] = model;
if (model.id != null) this._byId[model.id] = model; if (model.id != null) this._byId[model.id] = model;
} }
// See if sorting is needed, update `length` and splice in new models. // See if sorting is needed, update `length` and splice in new models.
if (models.length) needsSort = sort; if (add.length) {
this.length += models.length; if (sort) doSort = true;
args = [at != null ? at : this.models.length, 0]; this.length += add.length;
push.apply(args, models); if (at != null) {
splice.apply(this.models, args); splice.apply(this.models, [at, 0].concat(add));
} else {
push.apply(this.models, add);
}
}
// Sort the collection if appropriate. // Silently sort the collection if appropriate.
if (needsSort && this.comparator && at == null) this.sort({silent: true}); if (doSort) this.sort({silent: true});
if (options && options.silent) return this; if (options.silent) return this;
// Trigger `add` events. // Trigger `add` events.
while (model = models.shift()) { for (i = 0, l = add.length; i < l; i++) {
model.trigger('add', model, this, options); (model = add[i]).trigger('add', model, this, options);
} }
// Trigger `sort` if the collection was sorted.
if (doSort) this.trigger('sort', this, options);
return this; 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.
// firing the `remove` event for every model removed.
remove: function(models, options) { remove: function(models, options) {
var i, l, index, model;
options || (options = {});
models = _.isArray(models) ? models.slice() : [models]; models = _.isArray(models) ? models.slice() : [models];
options || (options = {});
var i, l, index, model;
for (i = 0, l = models.length; i < l; i++) { for (i = 0, l = models.length; i < l; i++) {
model = this.get(models[i]); model = this.get(models[i]);
if (!model) continue; if (!model) continue;
delete this._byId[model.id]; delete this._byId[model.id];
delete this._byCid[model.cid]; delete this._byId[model.cid];
index = this.indexOf(model); index = this.indexOf(model);
this.models.splice(index, 1); this.models.splice(index, 1);
this.length--; this.length--;
...@@ -734,7 +707,8 @@ ...@@ -734,7 +707,8 @@
// Get a model from the set by id. // Get a model from the set by id.
get: function(obj) { get: function(obj) {
if (obj == null) return void 0; if (obj == null) return void 0;
return this._byId[obj.id != null ? obj.id : obj] || this._byCid[obj.cid || obj]; this._idAttr || (this._idAttr = this.model.prototype.idAttribute);
return this._byId[obj.id || obj.cid || obj[this._idAttr] || obj];
}, },
// Get the model at the given index. // Get the model at the given index.
...@@ -760,14 +734,16 @@ ...@@ -760,14 +734,16 @@
if (!this.comparator) { if (!this.comparator) {
throw new Error('Cannot sort a set without a comparator'); throw new Error('Cannot sort a set without a comparator');
} }
options || (options = {});
// Run sort based on type of `comparator`.
if (_.isString(this.comparator) || this.comparator.length === 1) { if (_.isString(this.comparator) || this.comparator.length === 1) {
this.models = this.sortBy(this.comparator, this); this.models = this.sortBy(this.comparator, this);
} else { } else {
this.models.sort(_.bind(this.comparator, this)); this.models.sort(_.bind(this.comparator, this));
} }
if (!options || !options.silent) this.trigger('sort', this, options); if (!options.silent) this.trigger('sort', this, options);
return this; return this;
}, },
...@@ -779,11 +755,10 @@ ...@@ -779,11 +755,10 @@
// Smartly update a collection with a change set of models, adding, // Smartly update a collection with a change set of models, adding,
// removing, and merging as necessary. // removing, and merging as necessary.
update: function(models, options) { update: function(models, options) {
options = _.extend({add: true, merge: true, remove: true}, options);
if (options.parse) models = this.parse(models, options);
var model, i, l, existing; var model, i, l, existing;
var add = [], remove = [], modelMap = {}; var add = [], remove = [], modelMap = {};
var idAttr = this.model.prototype.idAttribute;
options = _.extend({add: true, merge: true, remove: true}, options);
if (options.parse) models = this.parse(models);
// Allow a single model (or no argument) to be passed. // Allow a single model (or no argument) to be passed.
if (!_.isArray(models)) models = models ? [models] : []; if (!_.isArray(models)) models = models ? [models] : [];
...@@ -794,7 +769,7 @@ ...@@ -794,7 +769,7 @@
// Determine which models to add and merge, and which to remove. // Determine which models to add and merge, and which to remove.
for (i = 0, l = models.length; i < l; i++) { for (i = 0, l = models.length; i < l; i++) {
model = models[i]; model = models[i];
existing = this.get(model.id || model.cid || model[idAttr]); existing = this.get(model);
if (options.remove && existing) modelMap[existing.cid] = true; if (options.remove && existing) modelMap[existing.cid] = true;
if ((options.add && !existing) || (options.merge && existing)) { if ((options.add && !existing) || (options.merge && existing)) {
add.push(model); add.push(model);
...@@ -818,11 +793,11 @@ ...@@ -818,11 +793,11 @@
// any `add` or `remove` events. Fires `reset` when finished. // any `add` or `remove` events. Fires `reset` when finished.
reset: function(models, options) { reset: function(models, options) {
options || (options = {}); options || (options = {});
if (options.parse) models = this.parse(models); if (options.parse) models = this.parse(models, options);
for (var i = 0, l = this.models.length; i < l; i++) { for (var i = 0, l = this.models.length; i < l; i++) {
this._removeReference(this.models[i]); this._removeReference(this.models[i]);
} }
options.previousModels = this.models; options.previousModels = this.models.slice();
this._reset(); this._reset();
if (models) this.add(models, _.extend({silent: true}, options)); if (models) this.add(models, _.extend({silent: true}, options));
if (!options.silent) this.trigger('reset', this, options); if (!options.silent) this.trigger('reset', this, options);
...@@ -830,14 +805,13 @@ ...@@ -830,14 +805,13 @@
}, },
// Fetch the default set of models for this collection, resetting the // Fetch the default set of models for this collection, resetting the
// collection when they arrive. If `add: true` is passed, appends the // collection when they arrive. If `update: true` is passed, the response
// models to the collection instead of resetting. // data will be passed through the `update` method instead of `reset`.
fetch: function(options) { fetch: function(options) {
options = options ? _.clone(options) : {}; options = options ? _.clone(options) : {};
if (options.parse === void 0) options.parse = true; if (options.parse === void 0) options.parse = true;
var collection = this;
var success = options.success; var success = options.success;
options.success = function(resp, status, xhr) { options.success = function(collection, resp, options) {
var method = options.update ? 'update' : 'reset'; var method = options.update ? 'update' : 'reset';
collection[method](resp, options); collection[method](resp, options);
if (success) success(collection, resp, options); if (success) success(collection, resp, options);
...@@ -849,11 +823,10 @@ ...@@ -849,11 +823,10 @@
// collection immediately, unless `wait: true` is passed, in which case we // collection immediately, unless `wait: true` is passed, in which case we
// wait for the server to agree. // wait for the server to agree.
create: function(model, options) { create: function(model, options) {
var collection = this;
options = options ? _.clone(options) : {}; options = options ? _.clone(options) : {};
model = this._prepareModel(model, options); if (!(model = this._prepareModel(model, options))) return false;
if (!model) return false; if (!options.wait) this.add(model, options);
if (!options.wait) collection.add(model, options); var collection = this;
var success = options.success; var success = options.success;
options.success = function(model, resp, options) { options.success = function(model, resp, options) {
if (options.wait) collection.add(model, options); if (options.wait) collection.add(model, options);
...@@ -865,7 +838,7 @@ ...@@ -865,7 +838,7 @@
// **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. // collection. The default implementation is just to pass it through.
parse: function(resp) { parse: function(resp, options) {
return resp; return resp;
}, },
...@@ -874,19 +847,11 @@ ...@@ -874,19 +847,11 @@
return new this.constructor(this.models); return new this.constructor(this.models);
}, },
// Proxy to _'s chain. Can't be proxied the same way the rest of the
// underscore methods are proxied because it relies on the underscore
// constructor.
chain: function() {
return _(this.models).chain();
},
// Reset all internal state. Called when the collection is reset. // Reset all internal state. Called when the collection is reset.
_reset: function() { _reset: function() {
this.length = 0; this.length = 0;
this.models = []; this.models.length = 0;
this._byId = {}; this._byId = {};
this._byCid = {};
}, },
// Prepare a model or hash of attributes to be added to this collection. // Prepare a model or hash of attributes to be added to this collection.
...@@ -920,6 +885,14 @@ ...@@ -920,6 +885,14 @@
if (model.id != null) this._byId[model.id] = model; if (model.id != null) this._byId[model.id] = model;
} }
this.trigger.apply(this, arguments); this.trigger.apply(this, arguments);
},
sortedIndex: function (model, value, context) {
value || (value = this.comparator);
var iterator = _.isFunction(value) ? value : function(model) {
return model.get(value);
};
return _.sortedIndex(this.models, model, iterator, context);
} }
}); });
...@@ -928,9 +901,9 @@ ...@@ -928,9 +901,9 @@
var methods = ['forEach', 'each', 'map', 'collect', 'reduce', 'foldl', var methods = ['forEach', 'each', 'map', 'collect', 'reduce', 'foldl',
'inject', 'reduceRight', 'foldr', 'find', 'detect', 'filter', 'select', 'inject', 'reduceRight', 'foldr', 'find', 'detect', 'filter', 'select',
'reject', 'every', 'all', 'some', 'any', 'include', 'contains', 'invoke', 'reject', 'every', 'all', 'some', 'any', 'include', 'contains', 'invoke',
'max', 'min', 'sortedIndex', 'toArray', 'size', 'first', 'head', 'take', 'max', 'min', 'toArray', 'size', 'first', 'head', 'take', 'initial', 'rest',
'initial', 'rest', 'tail', 'last', 'without', 'indexOf', 'shuffle', 'tail', 'drop', 'last', 'without', 'indexOf', 'shuffle', 'lastIndexOf',
'lastIndexOf', 'isEmpty']; 'isEmpty', 'chain'];
// Mix in each Underscore method as a proxy to `Collection#models`. // Mix in each Underscore method as a proxy to `Collection#models`.
_.each(methods, function(method) { _.each(methods, function(method) {
...@@ -969,7 +942,7 @@ ...@@ -969,7 +942,7 @@
// Cached regular expressions for matching named param parts and splatted // Cached regular expressions for matching named param parts and splatted
// parts of route strings. // parts of route strings.
var optionalParam = /\((.*?)\)/g; var optionalParam = /\((.*?)\)/g;
var namedParam = /:\w+/g; var namedParam = /(\(\?)?:\w+/g;
var splatParam = /\*\w+/g; var splatParam = /\*\w+/g;
var escapeRegExp = /[\-{}\[\]+?.,\\\^$|#\s]/g; var escapeRegExp = /[\-{}\[\]+?.,\\\^$|#\s]/g;
...@@ -993,6 +966,7 @@ ...@@ -993,6 +966,7 @@
var args = this._extractParameters(route, fragment); var args = this._extractParameters(route, fragment);
callback && callback.apply(this, args); callback && callback.apply(this, args);
this.trigger.apply(this, ['route:' + name].concat(args)); this.trigger.apply(this, ['route:' + name].concat(args));
this.trigger('route', name, args);
Backbone.history.trigger('route', this, name, args); Backbone.history.trigger('route', this, name, args);
}, this)); }, this));
return this; return this;
...@@ -1020,7 +994,9 @@ ...@@ -1020,7 +994,9 @@
_routeToRegExp: function(route) { _routeToRegExp: function(route) {
route = route.replace(escapeRegExp, '\\$&') route = route.replace(escapeRegExp, '\\$&')
.replace(optionalParam, '(?:$1)?') .replace(optionalParam, '(?:$1)?')
.replace(namedParam, '([^\/]+)') .replace(namedParam, function(match, optional){
return optional ? match : '([^\/]+)';
})
.replace(splatParam, '(.*?)'); .replace(splatParam, '(.*?)');
return new RegExp('^' + route + '$'); return new RegExp('^' + route + '$');
}, },
...@@ -1042,7 +1018,7 @@ ...@@ -1042,7 +1018,7 @@
this.handlers = []; this.handlers = [];
_.bindAll(this, 'checkUrl'); _.bindAll(this, 'checkUrl');
// #1653 - Ensure that `History` can be used outside of the browser. // Ensure that `History` can be used outside of the browser.
if (typeof window !== 'undefined') { if (typeof window !== 'undefined') {
this.location = window.location; this.location = window.location;
this.history = window.history; this.history = window.history;
...@@ -1121,9 +1097,9 @@ ...@@ -1121,9 +1097,9 @@
// Depending on whether we're using pushState or hashes, and whether // Depending on whether we're using pushState or hashes, and whether
// 'onhashchange' is supported, determine how we check the URL state. // 'onhashchange' is supported, determine how we check the URL state.
if (this._hasPushState) { if (this._hasPushState) {
Backbone.$(window).bind('popstate', this.checkUrl); Backbone.$(window).on('popstate', this.checkUrl);
} else if (this._wantsHashChange && ('onhashchange' in window) && !oldIE) { } else if (this._wantsHashChange && ('onhashchange' in window) && !oldIE) {
Backbone.$(window).bind('hashchange', this.checkUrl); Backbone.$(window).on('hashchange', this.checkUrl);
} else if (this._wantsHashChange) { } else if (this._wantsHashChange) {
this._checkUrlInterval = setInterval(this.checkUrl, this.interval); this._checkUrlInterval = setInterval(this.checkUrl, this.interval);
} }
...@@ -1155,7 +1131,7 @@ ...@@ -1155,7 +1131,7 @@
// Disable Backbone.history, perhaps temporarily. Not useful in a real app, // Disable Backbone.history, perhaps temporarily. Not useful in a real app,
// but possibly useful for unit testing Routers. // but possibly useful for unit testing Routers.
stop: function() { stop: function() {
Backbone.$(window).unbind('popstate', this.checkUrl).unbind('hashchange', this.checkUrl); Backbone.$(window).off('popstate', this.checkUrl).off('hashchange', this.checkUrl);
clearInterval(this._checkUrlInterval); clearInterval(this._checkUrlInterval);
History.started = false; History.started = false;
}, },
...@@ -1238,7 +1214,7 @@ ...@@ -1238,7 +1214,7 @@
var href = location.href.replace(/(javascript:|#).*$/, ''); var href = location.href.replace(/(javascript:|#).*$/, '');
location.replace(href + '#' + fragment); location.replace(href + '#' + fragment);
} else { } else {
// #1649 - Some browsers require that `hash` contains a leading #. // Some browsers require that `hash` contains a leading #.
location.hash = '#' + fragment; location.hash = '#' + fragment;
} }
} }
...@@ -1298,18 +1274,6 @@ ...@@ -1298,18 +1274,6 @@
return this; return this;
}, },
// For small amounts of DOM Elements, where a full-blown template isn't
// needed, use **make** to manufacture elements, one at a time.
//
// var el = this.make('li', {'class': 'row'}, this.model.escape('title'));
//
make: function(tagName, attributes, content) {
var el = document.createElement(tagName);
if (attributes) Backbone.$(el).attr(attributes);
if (content != null) Backbone.$(el).html(content);
return el;
},
// Change the view's element (`this.el` property), including event // Change the view's element (`this.el` property), including event
// re-delegation. // re-delegation.
setElement: function(element, delegate) { setElement: function(element, delegate) {
...@@ -1347,9 +1311,9 @@ ...@@ -1347,9 +1311,9 @@
method = _.bind(method, this); method = _.bind(method, this);
eventName += '.delegateEvents' + this.cid; eventName += '.delegateEvents' + this.cid;
if (selector === '') { if (selector === '') {
this.$el.bind(eventName, method); this.$el.on(eventName, method);
} else { } else {
this.$el.delegate(selector, eventName, method); this.$el.on(eventName, selector, method);
} }
} }
}, },
...@@ -1358,7 +1322,7 @@ ...@@ -1358,7 +1322,7 @@
// You usually don't need to use this, but may wish to if you have multiple // You usually don't need to use this, but may wish to if you have multiple
// Backbone views attached to the same DOM element. // Backbone views attached to the same DOM element.
undelegateEvents: function() { undelegateEvents: function() {
this.$el.unbind('.delegateEvents' + this.cid); this.$el.off('.delegateEvents' + this.cid);
}, },
// Performs the initial configuration of a View with a set of options. // Performs the initial configuration of a View with a set of options.
...@@ -1379,7 +1343,8 @@ ...@@ -1379,7 +1343,8 @@
var attrs = _.extend({}, _.result(this, 'attributes')); var attrs = _.extend({}, _.result(this, 'attributes'));
if (this.id) attrs.id = _.result(this, 'id'); if (this.id) attrs.id = _.result(this, 'id');
if (this.className) attrs['class'] = _.result(this, 'className'); if (this.className) attrs['class'] = _.result(this, 'className');
this.setElement(this.make(_.result(this, 'tagName'), attrs), false); var $el = Backbone.$('<' + _.result(this, 'tagName') + '>').attr(attrs);
this.setElement($el, false);
} else { } else {
this.setElement(_.result(this, 'el'), false); this.setElement(_.result(this, 'el'), false);
} }
...@@ -1461,19 +1426,19 @@ ...@@ -1461,19 +1426,19 @@
} }
var success = options.success; var success = options.success;
options.success = function(resp, status, xhr) { options.success = function(resp) {
if (success) success(resp, status, xhr); if (success) success(model, resp, options);
model.trigger('sync', model, resp, options); model.trigger('sync', model, resp, options);
}; };
var error = options.error; var error = options.error;
options.error = function(xhr, status, thrown) { options.error = function(xhr) {
if (error) error(model, xhr, options); if (error) error(model, xhr, options);
model.trigger('error', model, xhr, options); model.trigger('error', model, xhr, options);
}; };
// Make the request, allowing the user to override any Ajax options. // Make the request, allowing the user to override any Ajax options.
var xhr = Backbone.ajax(_.extend(params, options)); var xhr = options.xhr = Backbone.ajax(_.extend(params, options));
model.trigger('request', model, xhr, options); model.trigger('request', model, xhr, options);
return xhr; return xhr;
}; };
...@@ -1499,7 +1464,7 @@ ...@@ -1499,7 +1464,7 @@
if (protoProps && _.has(protoProps, 'constructor')) { if (protoProps && _.has(protoProps, 'constructor')) {
child = protoProps.constructor; child = protoProps.constructor;
} else { } else {
child = function(){ parent.apply(this, arguments); }; child = function(){ return parent.apply(this, arguments); };
} }
// Add static properties to the constructor function, if supplied. // Add static properties to the constructor function, if supplied.
......
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
html,
body {
margin: 0;
padding: 0;
}
button {
margin: 0;
padding: 0;
border: 0;
background: none;
font-size: 100%;
vertical-align: baseline;
font-family: inherit;
color: inherit;
-webkit-appearance: none;
/*-moz-appearance: none;*/
-ms-appearance: none;
-o-appearance: none;
appearance: none;
}
body {
font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif;
line-height: 1.4em;
background: #eaeaea url('bg.png');
color: #4d4d4d;
width: 550px;
margin: 0 auto;
-webkit-font-smoothing: antialiased;
-moz-font-smoothing: antialiased;
-ms-font-smoothing: antialiased;
-o-font-smoothing: antialiased;
font-smoothing: antialiased;
}
#todoapp {
background: #fff;
background: rgba(255, 255, 255, 0.9);
margin: 130px 0 40px 0;
border: 1px solid #ccc;
position: relative;
border-top-left-radius: 2px;
border-top-right-radius: 2px;
box-shadow: 0 2px 6px 0 rgba(0, 0, 0, 0.2),
0 25px 50px 0 rgba(0, 0, 0, 0.15);
}
#todoapp:before {
content: '';
border-left: 1px solid #f5d6d6;
border-right: 1px solid #f5d6d6;
width: 2px;
position: absolute;
top: 0;
left: 40px;
height: 100%;
}
#todoapp input::-webkit-input-placeholder {
font-style: italic;
}
#todoapp input:-moz-placeholder {
font-style: italic;
color: #a9a9a9;
}
#todoapp h1 {
position: absolute;
top: -120px;
width: 100%;
font-size: 70px;
font-weight: bold;
text-align: center;
color: #b3b3b3;
color: rgba(255, 255, 255, 0.3);
text-shadow: -1px -1px rgba(0, 0, 0, 0.2);
-webkit-text-rendering: optimizeLegibility;
-moz-text-rendering: optimizeLegibility;
-ms-text-rendering: optimizeLegibility;
-o-text-rendering: optimizeLegibility;
text-rendering: optimizeLegibility;
}
#header {
padding-top: 15px;
border-radius: inherit;
}
#header:before {
content: '';
position: absolute;
top: 0;
right: 0;
left: 0;
height: 15px;
z-index: 2;
border-bottom: 1px solid #6c615c;
background: #8d7d77;
background: -webkit-gradient(linear, left top, left bottom, from(rgba(132, 110, 100, 0.8)),to(rgba(101, 84, 76, 0.8)));
background: -webkit-linear-gradient(top, rgba(132, 110, 100, 0.8), rgba(101, 84, 76, 0.8));
background: -moz-linear-gradient(top, rgba(132, 110, 100, 0.8), rgba(101, 84, 76, 0.8));
background: -o-linear-gradient(top, rgba(132, 110, 100, 0.8), rgba(101, 84, 76, 0.8));
background: -ms-linear-gradient(top, rgba(132, 110, 100, 0.8), rgba(101, 84, 76, 0.8));
background: linear-gradient(top, rgba(132, 110, 100, 0.8), rgba(101, 84, 76, 0.8));
filter: progid:DXImageTransform.Microsoft.gradient(GradientType=0,StartColorStr='#9d8b83', EndColorStr='#847670');
border-top-left-radius: 1px;
border-top-right-radius: 1px;
}
#new-todo,
.edit {
position: relative;
margin: 0;
width: 100%;
font-size: 24px;
font-family: inherit;
line-height: 1.4em;
border: 0;
outline: none;
color: inherit;
padding: 6px;
border: 1px solid #999;
box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2);
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
-ms-box-sizing: border-box;
-o-box-sizing: border-box;
box-sizing: border-box;
-webkit-font-smoothing: antialiased;
-moz-font-smoothing: antialiased;
-ms-font-smoothing: antialiased;
-o-font-smoothing: antialiased;
font-smoothing: antialiased;
}
#new-todo {
padding: 16px 16px 16px 60px;
border: none;
background: rgba(0, 0, 0, 0.02);
z-index: 2;
box-shadow: none;
}
#main {
position: relative;
z-index: 2;
border-top: 1px dotted #adadad;
}
label[for='toggle-all'] {
display: none;
}
#toggle-all {
position: absolute;
top: -42px;
left: -4px;
width: 40px;
text-align: center;
border: none; /* Mobile Safari */
}
#toggle-all:before {
content: '»';
font-size: 28px;
color: #d9d9d9;
padding: 0 25px 7px;
}
#toggle-all:checked:before {
color: #737373;
}
#todo-list {
margin: 0;
padding: 0;
list-style: none;
}
#todo-list li {
position: relative;
font-size: 24px;
border-bottom: 1px dotted #ccc;
}
#todo-list li:last-child {
border-bottom: none;
}
#todo-list li.editing {
border-bottom: none;
padding: 0;
}
#todo-list li.editing .edit {
display: block;
width: 506px;
padding: 13px 17px 12px 17px;
margin: 0 0 0 43px;
}
#todo-list li.editing .view {
display: none;
}
#todo-list li .toggle {
text-align: center;
width: 40px;
/* auto, since non-WebKit browsers doesn't support input styling */
height: auto;
position: absolute;
top: 0;
bottom: 0;
margin: auto 0;
border: none; /* Mobile Safari */
-webkit-appearance: none;
/*-moz-appearance: none;*/
-ms-appearance: none;
-o-appearance: none;
appearance: none;
}
#todo-list li .toggle:after {
content: '✔';
line-height: 43px; /* 40 + a couple of pixels visual adjustment */
font-size: 20px;
color: #d9d9d9;
text-shadow: 0 -1px 0 #bfbfbf;
}
#todo-list li .toggle:checked:after {
color: #85ada7;
text-shadow: 0 1px 0 #669991;
bottom: 1px;
position: relative;
}
#todo-list li label {
word-break: break-word;
padding: 15px;
margin-left: 45px;
display: block;
line-height: 1.2;
-webkit-transition: color 0.4s;
-moz-transition: color 0.4s;
-ms-transition: color 0.4s;
-o-transition: color 0.4s;
transition: color 0.4s;
}
#todo-list li.completed label {
color: #a9a9a9;
text-decoration: line-through;
}
#todo-list li .destroy {
display: none;
position: absolute;
top: 0;
right: 10px;
bottom: 0;
width: 40px;
height: 40px;
margin: auto 0;
font-size: 22px;
color: #a88a8a;
-webkit-transition: all 0.2s;
-moz-transition: all 0.2s;
-ms-transition: all 0.2s;
-o-transition: all 0.2s;
transition: all 0.2s;
}
#todo-list li .destroy:hover {
text-shadow: 0 0 1px #000,
0 0 10px rgba(199, 107, 107, 0.8);
-webkit-transform: scale(1.3);
-moz-transform: scale(1.3);
-ms-transform: scale(1.3);
-o-transform: scale(1.3);
transform: scale(1.3);
}
#todo-list li .destroy:after {
content: '✖';
}
#todo-list li:hover .destroy {
display: block;
}
#todo-list li .edit {
display: none;
}
#todo-list li.editing:last-child {
margin-bottom: -1px;
}
#footer {
color: #777;
padding: 0 15px;
position: absolute;
right: 0;
bottom: -31px;
left: 0;
height: 20px;
z-index: 1;
text-align: center;
}
#footer:before {
content: '';
position: absolute;
right: 0;
bottom: 31px;
left: 0;
height: 50px;
z-index: -1;
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.3),
0 6px 0 -3px rgba(255, 255, 255, 0.8),
0 7px 1px -3px rgba(0, 0, 0, 0.3),
0 43px 0 -6px rgba(255, 255, 255, 0.8),
0 44px 2px -6px rgba(0, 0, 0, 0.2);
}
#todo-count {
float: left;
text-align: left;
}
#filters {
margin: 0;
padding: 0;
list-style: none;
position: absolute;
right: 0;
left: 0;
}
#filters li {
display: inline;
}
#filters li a {
color: #83756f;
margin: 2px;
text-decoration: none;
}
#filters li a.selected {
font-weight: bold;
}
#clear-completed {
float: right;
position: relative;
line-height: 20px;
text-decoration: none;
background: rgba(0, 0, 0, 0.1);
font-size: 11px;
padding: 0 10px;
border-radius: 3px;
box-shadow: 0 -1px 0 0 rgba(0, 0, 0, 0.2);
}
#clear-completed:hover {
background: rgba(0, 0, 0, 0.15);
box-shadow: 0 -1px 0 0 rgba(0, 0, 0, 0.3);
}
#info {
margin: 65px auto 0;
color: #a6a6a6;
font-size: 12px;
text-shadow: 0 1px 0 rgba(255, 255, 255, 0.7);
text-align: center;
}
#info a {
color: inherit;
}
/*
Hack to remove background from Mobile Safari.
Can't use it globally since it destroys checkboxes in Firefox and Opera
*/
@media screen and (-webkit-min-device-pixel-ratio:0) {
#toggle-all,
#todo-list li .toggle {
background: none;
}
#todo-list li .toggle {
height: 40px;
}
#toggle-all {
top: -56px;
left: -15px;
width: 65px;
height: 41px;
-webkit-transform: rotate(90deg);
transform: rotate(90deg);
-webkit-appearance: none;
appearance: none;
}
}
.hidden{
display:none;
}
(function () {
'use strict';
if (location.hostname === 'todomvc.com') {
var _gaq=[['_setAccount','UA-31081062-1'],['_trackPageview']];(function(d,t){var g=d.createElement(t),s=d.getElementsByTagName(t)[0];g.src='//www.google-analytics.com/ga.js';s.parentNode.insertBefore(g,s)}(document,'script'));
}
})();
...@@ -4,10 +4,7 @@ ...@@ -4,10 +4,7 @@
<meta charset="utf-8"> <meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"> <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<title>Backbone.js • TodoMVC</title> <title>Backbone.js • TodoMVC</title>
<link rel="stylesheet" href="../../assets/base.css"> <link rel="stylesheet" href="components/todomvc-common/base.css">
<!--[if IE]>
<script src="../../assets/ie.js"></script>
<![endif]-->
</head> </head>
<body> <body>
<section id="todoapp"> <section id="todoapp">
...@@ -52,11 +49,11 @@ ...@@ -52,11 +49,11 @@
<button id="clear-completed">Clear completed (<%= completed %>)</button> <button id="clear-completed">Clear completed (<%= completed %>)</button>
<% } %> <% } %>
</script> </script>
<script src="../../assets/base.js"></script> <script src="components/todomvc-common/base.js"></script>
<script src="../../assets/jquery.min.js"></script> <script src="components/jquery/jquery.js"></script>
<script src="../../assets/lodash.min.js"></script> <script src="components/lodash/lodash.js"></script>
<script src="js/lib/backbone.js"></script> <script src="components/backbone/backbone.js"></script>
<script src="js/lib/backbone.localStorage.js"></script> <script src="components/backbone.localStorage/backbone.localStorage.js"></script>
<script src="js/models/todo.js"></script> <script src="js/models/todo.js"></script>
<script src="js/collections/todos.js"></script> <script src="js/collections/todos.js"></script>
<script src="js/views/todos.js"></script> <script src="js/views/todos.js"></script>
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment