Commit 42cae948 authored by André Ruffert's avatar André Ruffert Committed by Sindre Sorhus

Close #1158 PR: Update `typescript-backbone` to new app UI. Fixes #1110

parent 043a525c
node_modules/*
node_modules/backbone/*
!node_modules/backbone/backbone.js
node_modules/backbone.localstorage/*
!node_modules/backbone.localstorage/backbone.localStorage.js
node_modules/jquery/**
!node_modules/jquery/dist/jquery.js
node_modules/lodash/*
!node_modules/lodash/index.js
node_modules/todomvc-app-css/*
!node_modules/todomvc-app-css/index.css
node_modules/todomvc-common/*
!node_modules/todomvc-common/base.css
!node_modules/todomvc-common/base.js
{
"name": "todomvc-typescript-backbone",
"version": "0.0.0",
"dependencies": {
"todomvc-common": "~0.3.0",
"jquery": "~2.0.0",
"lodash": "~1.2.1"
}
}
This source diff could not be displayed because it is too large. You can view the blob instead.
...@@ -47,7 +47,8 @@ https://github.com/documentcloud/backbone/blob/master/examples/todos/index.html ...@@ -47,7 +47,8 @@ https://github.com/documentcloud/backbone/blob/master/examples/todos/index.html
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<title>TypeScript & Backbone.js • TodoMVC</title> <title>TypeScript & Backbone.js • TodoMVC</title>
<link rel="stylesheet" href="bower_components/todomvc-common/base.css"> <link rel="stylesheet" href="node_modules/todomvc-common/base.css">
<link rel="stylesheet" href="node_modules/todomvc-app-css/index.css">
</head> </head>
<body> <body>
<section id="todoapp"> <section id="todoapp">
...@@ -95,18 +96,16 @@ https://github.com/documentcloud/backbone/blob/master/examples/todos/index.html ...@@ -95,18 +96,16 @@ https://github.com/documentcloud/backbone/blob/master/examples/todos/index.html
<% } %> <% } %>
<% if (done) { %> <% if (done) { %>
<span class="todo-clear"> <span class="todo-clear">
<button id="clear-completed"> <button id="clear-completed">Clear completed (<span class="number-done"><%= done %></span>)</button>
Clear completed (<span class="number-done"><%= done %></span>)
</button>
</span> </span>
<% } %> <% } %>
</script> </script>
<script src="bower_components/todomvc-common/base.js"></script> <script src="node_modules/todomvc-common/base.js"></script>
<script src="bower_components/jquery/jquery.js"></script> <script src="node_modules/jquery/dist/jquery.js"></script>
<script src="bower_components/lodash/lodash.js"></script> <script src="node_modules/lodash/index.js"></script>
<script src="bower_components/backbone/backbone.js"></script> <script src="node_modules/backbone/backbone.js"></script>
<script src="js/libs/backbone/backbone.localStorage.js"></script> <script src="node_modules/backbone.localstorage/backbone.localStorage.js"></script>
<script src="js/app.js"></script> <script src="js/app.js"></script>
</body> </body>
</html> </html>
/**
* Backbone localStorage Adapter
* Version 1.1.0
*
* https://github.com/jeromegn/Backbone.localStorage
*/
(function (root, factory) {
if (typeof define === "function" && define.amd) {
// AMD. Register as an anonymous module.
define(["underscore", "backbone"], function (_, Backbone) {
// Use global variables if the locals are undefined.
return factory(_ || root._, Backbone || root.Backbone);
});
} else {
// RequireJS isn't being used. Assume underscore and backbone are loaded in <script> tags
factory(_, Backbone);
}
}(this, function (_, Backbone) {
// A simple module to replace `Backbone.sync` with *localStorage*-based
// persistence. Models are given GUIDS, and saved into a JSON object. Simple
// as that.
// Hold reference to Underscore.js and Backbone.js in the closure in order
// to make things work even if they are removed from the global namespace
// Generate four random hex digits.
function S4() {
return (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1);
};
// Generate a pseudo-GUID by concatenating random hexadecimal.
function guid() {
return (S4() + S4() + "-" + S4() + "-" + S4() + "-" + S4() + "-" + S4() + S4() + S4());
};
// Our Store is represented by a single JS object in *localStorage*. Create it
// with a meaningful name, like the name you'd give a table.
// window.Store is deprectated, use Backbone.LocalStorage instead
Backbone.LocalStorage = window.Store = function (name) {
this.name = name;
var store = this.localStorage().getItem(this.name);
this.records = (store && store.split(",")) || [];
};
_.extend(Backbone.LocalStorage.prototype, {
// Save the current state of the **Store** to *localStorage*.
save: function () {
this.localStorage().setItem(this.name, this.records.join(","));
},
// Add a model, giving it a (hopefully)-unique GUID, if it doesn't already
// have an id of it's own.
create: function (model) {
if (!model.id) {
model.id = guid();
model.set(model.idAttribute, model.id);
}
this.localStorage().setItem(this.name + "-" + model.id, JSON.stringify(model));
this.records.push(model.id.toString());
this.save();
return this.find(model);
},
// Update a model by replacing its copy in `this.data`.
update: function (model) {
this.localStorage().setItem(this.name + "-" + model.id, JSON.stringify(model));
if (!_.include(this.records, model.id.toString()))
this.records.push(model.id.toString()); this.save();
return this.find(model);
},
// Retrieve a model from `this.data` by id.
find: function (model) {
return this.jsonData(this.localStorage().getItem(this.name + "-" + model.id));
},
// Return the array of all models currently in storage.
findAll: function () {
return _(this.records).chain()
.map(function (id) {
return this.jsonData(this.localStorage().getItem(this.name + "-" + id));
}, this)
.compact()
.value();
},
// Delete a model from `this.data`, returning it.
destroy: function (model) {
if (model.isNew())
return false
this.localStorage().removeItem(this.name + "-" + model.id);
this.records = _.reject(this.records, function (id) {
return id === model.id.toString();
});
this.save();
return model;
},
localStorage: function () {
return localStorage;
},
// fix for "illegal access" error on Android when JSON.parse is passed null
jsonData: function (data) {
return data && JSON.parse(data);
}
});
// localSync delegate to the model or collection's
// *localStorage* property, which should be an instance of `Store`.
// window.Store.sync and Backbone.localSync is deprectated, use Backbone.LocalStorage.sync instead
Backbone.LocalStorage.sync = window.Store.sync = Backbone.localSync = function (method, model, options) {
var store = model.localStorage || model.collection.localStorage;
var resp, errorMessage, syncDfd = $.Deferred && $.Deferred(); //If $ is having Deferred - use it.
try {
switch (method) {
case "read":
resp = model.id != undefined ? store.find(model) : store.findAll();
break;
case "create":
resp = store.create(model);
break;
case "update":
resp = store.update(model);
break;
case "delete":
resp = store.destroy(model);
break;
}
} catch (error) {
if (error.code === DOMException.QUOTA_EXCEEDED_ERR && window.localStorage.length === 0)
errorMessage = "Private browsing is unsupported";
else
errorMessage = error.message;
}
if (resp) {
model.trigger("sync", model, resp, options);
if (options && options.success)
if (Backbone.VERSION === "0.9.10") {
options.success(model, resp, options);
} else {
options.success(resp);
}
if (syncDfd)
syncDfd.resolve(resp);
} else {
errorMessage = errorMessage ? errorMessage
: "Record Not Found";
model.trigger("error", model, errorMessage, options);
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();
};
Backbone.ajaxSync = Backbone.sync;
Backbone.getSyncMethod = function (model) {
if (model.localStorage || (model.collection && model.collection.localStorage)) {
return Backbone.localSync;
}
return Backbone.ajaxSync;
};
// Override 'Backbone.sync' to default to localSync,
// the original 'Backbone.sync' is still available in 'Backbone.ajaxSync'
Backbone.sync = function (method, model, options) {
return Backbone.getSyncMethod(model).apply(this, [method, model, options]);
};
return Backbone.LocalStorage;
}));
\ No newline at end of file
/**
* Backbone localStorage Adapter
* Version 1.1.16
*
* https://github.com/jeromegn/Backbone.localStorage
*/
(function (root, factory) {
if (typeof exports === 'object' && typeof require === 'function') {
module.exports = factory(require("backbone"));
} else if (typeof define === "function" && define.amd) {
// AMD. Register as an anonymous module.
define(["backbone"], function(Backbone) {
// Use global variables if the locals are undefined.
return factory(Backbone || root.Backbone);
});
} else {
factory(Backbone);
}
}(this, function(Backbone) {
// A simple module to replace `Backbone.sync` with *localStorage*-based
// persistence. Models are given GUIDS, and saved into a JSON object. Simple
// as that.
// Generate four random hex digits.
function S4() {
return (((1+Math.random())*0x10000)|0).toString(16).substring(1);
};
// Generate a pseudo-GUID by concatenating random hexadecimal.
function guid() {
return (S4()+S4()+"-"+S4()+"-"+S4()+"-"+S4()+"-"+S4()+S4()+S4());
};
function isObject(item) {
return item === Object(item);
}
function contains(array, item) {
var i = array.length;
while (i--) if (array[i] === item) return true;
return false;
}
function extend(obj, props) {
for (var key in props) obj[key] = props[key]
return obj;
}
function result(object, property) {
if (object == null) return void 0;
var value = object[property];
return (typeof value === 'function') ? object[property]() : value;
}
// Our Store is represented by a single JS object in *localStorage*. Create it
// with a meaningful name, like the name you'd give a table.
// window.Store is deprectated, use Backbone.LocalStorage instead
Backbone.LocalStorage = window.Store = function(name, serializer) {
if( !this.localStorage ) {
throw "Backbone.localStorage: Environment does not support localStorage."
}
this.name = name;
this.serializer = serializer || {
serialize: function(item) {
return isObject(item) ? JSON.stringify(item) : item;
},
// fix for "illegal access" error on Android when JSON.parse is passed null
deserialize: function (data) {
return data && JSON.parse(data);
}
};
var store = this.localStorage().getItem(this.name);
this.records = (store && store.split(",")) || [];
};
extend(Backbone.LocalStorage.prototype, {
// Save the current state of the **Store** to *localStorage*.
save: function() {
this.localStorage().setItem(this.name, this.records.join(","));
},
// Add a model, giving it a (hopefully)-unique GUID, if it doesn't already
// have an id of it's own.
create: function(model) {
if (!model.id && model.id !== 0) {
model.id = guid();
model.set(model.idAttribute, model.id);
}
this.localStorage().setItem(this._itemName(model.id), this.serializer.serialize(model));
this.records.push(model.id.toString());
this.save();
return this.find(model);
},
// Update a model by replacing its copy in `this.data`.
update: function(model) {
this.localStorage().setItem(this._itemName(model.id), this.serializer.serialize(model));
var modelId = model.id.toString();
if (!contains(this.records, modelId)) {
this.records.push(modelId);
this.save();
}
return this.find(model);
},
// Retrieve a model from `this.data` by id.
find: function(model) {
return this.serializer.deserialize(this.localStorage().getItem(this._itemName(model.id)));
},
// Return the array of all models currently in storage.
findAll: function() {
var result = [];
for (var i = 0, id, data; i < this.records.length; i++) {
id = this.records[i];
data = this.serializer.deserialize(this.localStorage().getItem(this._itemName(id)));
if (data != null) result.push(data);
}
return result;
},
// Delete a model from `this.data`, returning it.
destroy: function(model) {
this.localStorage().removeItem(this._itemName(model.id));
var modelId = model.id.toString();
for (var i = 0, id; i < this.records.length; i++) {
if (this.records[i] === modelId) {
this.records.splice(i, 1);
}
}
this.save();
return model;
},
localStorage: function() {
return localStorage;
},
// Clear localStorage for specific collection.
_clear: function() {
var local = this.localStorage(),
itemRe = new RegExp("^" + this.name + "-");
// Remove id-tracking item (e.g., "foo").
local.removeItem(this.name);
// Match all data items (e.g., "foo-ID") and remove.
for (var k in local) {
if (itemRe.test(k)) {
local.removeItem(k);
}
}
this.records.length = 0;
},
// Size of localStorage.
_storageSize: function() {
return this.localStorage().length;
},
_itemName: function(id) {
return this.name+"-"+id;
}
});
// localSync delegate to the model or collection's
// *localStorage* property, which should be an instance of `Store`.
// window.Store.sync and Backbone.localSync is deprecated, use Backbone.LocalStorage.sync instead
Backbone.LocalStorage.sync = window.Store.sync = Backbone.localSync = function(method, model, options) {
var store = result(model, 'localStorage') || result(model.collection, 'localStorage');
var resp, errorMessage;
//If $ is having Deferred - use it.
var syncDfd = Backbone.$ ?
(Backbone.$.Deferred && Backbone.$.Deferred()) :
(Backbone.Deferred && Backbone.Deferred());
try {
switch (method) {
case "read":
resp = model.id != undefined ? store.find(model) : store.findAll();
break;
case "create":
resp = store.create(model);
break;
case "update":
resp = store.update(model);
break;
case "delete":
resp = store.destroy(model);
break;
}
} catch(error) {
if (error.code === 22 && store._storageSize() === 0)
errorMessage = "Private browsing is unsupported";
else
errorMessage = error.message;
}
if (resp) {
if (options && options.success) {
if (Backbone.VERSION === "0.9.10") {
options.success(model, resp, options);
} else {
options.success(resp);
}
}
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();
};
Backbone.ajaxSync = Backbone.sync;
Backbone.getSyncMethod = function(model, options) {
var forceAjaxSync = options && options.ajaxSync;
if(!forceAjaxSync && (result(model, 'localStorage') || result(model.collection, 'localStorage'))) {
return Backbone.localSync;
}
return Backbone.ajaxSync;
};
// Override 'Backbone.sync' to default to localSync,
// the original 'Backbone.sync' is still available in 'Backbone.ajaxSync'
Backbone.sync = function(method, model, options) {
return Backbone.getSyncMethod(model, options).apply(this, [method, model, options]);
};
return Backbone.LocalStorage;
}));
// Backbone.js 1.0.0 // Backbone.js 1.1.2
// (c) 2010-2013 Jeremy Ashkenas, DocumentCloud Inc. // (c) 2010-2014 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors
// Backbone may be freely distributed under the MIT license. // Backbone may be freely distributed under the MIT license.
// For all details and documentation: // For all details and documentation:
// http://backbonejs.org // http://backbonejs.org
(function(){ (function(root, factory) {
// Set up Backbone appropriately for the environment. Start with AMD.
if (typeof define === 'function' && define.amd) {
define(['underscore', 'jquery', 'exports'], function(_, $, exports) {
// Export global even in AMD case in case this script is loaded with
// others that may still expect a global Backbone.
root.Backbone = factory(root, exports, _, $);
});
// Next for Node.js or CommonJS. jQuery may not be needed as a module.
} else if (typeof exports !== 'undefined') {
var _ = require('underscore');
factory(root, exports, _);
// Finally, as a browser global.
} else {
root.Backbone = factory(root, {}, root._, (root.jQuery || root.Zepto || root.ender || root.$));
}
}(this, function(root, Backbone, _, $) {
// Initial Setup // Initial Setup
// ------------- // -------------
// Save a reference to the global object (`window` in the browser, `exports`
// on the server).
var root = this;
// Save the previous value of the `Backbone` variable, so that it can be // Save the previous value of the `Backbone` variable, so that it can be
// restored later on, if `noConflict` is used. // restored later on, if `noConflict` is used.
var previousBackbone = root.Backbone; var previousBackbone = root.Backbone;
...@@ -24,25 +40,12 @@ ...@@ -24,25 +40,12 @@
var slice = array.slice; var slice = array.slice;
var splice = array.splice; var splice = array.splice;
// The top-level namespace. All public Backbone classes and modules will
// be attached to this. Exported for both the browser and the server.
var Backbone;
if (typeof exports !== 'undefined') {
Backbone = exports;
} else {
Backbone = root.Backbone = {};
}
// Current version of the library. Keep in sync with `package.json`. // Current version of the library. Keep in sync with `package.json`.
Backbone.VERSION = '1.0.0'; Backbone.VERSION = '1.1.2';
// Require Underscore, if we're on the server, and it's not already present.
var _ = root._;
if (!_ && (typeof require !== 'undefined')) _ = require('underscore');
// For Backbone's purposes, jQuery, Zepto, Ender, or My Library (kidding) owns // For Backbone's purposes, jQuery, Zepto, Ender, or My Library (kidding) owns
// the `$` variable. // the `$` variable.
Backbone.$ = root.jQuery || root.Zepto || root.ender || root.$; Backbone.$ = $;
// Runs Backbone.js in *noConflict* mode, returning the `Backbone` variable // Runs Backbone.js in *noConflict* mode, returning the `Backbone` variable
// to its previous owner. Returns a reference to this Backbone object. // to its previous owner. Returns a reference to this Backbone object.
...@@ -52,7 +55,7 @@ ...@@ -52,7 +55,7 @@
}; };
// Turn on `emulateHTTP` to support legacy HTTP servers. Setting this option // Turn on `emulateHTTP` to support legacy HTTP servers. Setting this option
// will fake `"PUT"` and `"DELETE"` requests via the `_method` parameter and // will fake `"PATCH"`, `"PUT"` and `"DELETE"` requests via the `_method` parameter and
// set a `X-Http-Method-Override` header. // set a `X-Http-Method-Override` header.
Backbone.emulateHTTP = false; Backbone.emulateHTTP = false;
...@@ -108,10 +111,9 @@ ...@@ -108,10 +111,9 @@
var retain, ev, events, names, i, l, j, k; var retain, ev, events, names, i, l, j, k;
if (!this._events || !eventsApi(this, 'off', name, [callback, context])) return this; if (!this._events || !eventsApi(this, 'off', name, [callback, context])) return this;
if (!name && !callback && !context) { if (!name && !callback && !context) {
this._events = {}; this._events = void 0;
return this; return this;
} }
names = name ? [name] : _.keys(this._events); names = name ? [name] : _.keys(this._events);
for (i = 0, l = names.length; i < l; i++) { for (i = 0, l = names.length; i < l; i++) {
name = names[i]; name = names[i];
...@@ -151,14 +153,15 @@ ...@@ -151,14 +153,15 @@
// 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(obj, name, callback) { stopListening: function(obj, name, callback) {
var listeners = this._listeners; var listeningTo = this._listeningTo;
if (!listeners) return this; if (!listeningTo) return this;
var deleteListener = !name && !callback; var remove = !name && !callback;
if (typeof name === 'object') callback = this; if (!callback && typeof name === 'object') callback = this;
if (obj) (listeners = {})[obj._listenerId] = obj; if (obj) (listeningTo = {})[obj._listenId] = obj;
for (var id in listeners) { for (var id in listeningTo) {
listeners[id].off(name, callback, this); obj = listeningTo[id];
if (deleteListener) delete this._listeners[id]; obj.off(name, callback, this);
if (remove || _.isEmpty(obj._events)) delete this._listeningTo[id];
} }
return this; return this;
} }
...@@ -204,7 +207,7 @@ ...@@ -204,7 +207,7 @@
case 1: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1); return; case 1: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1); return;
case 2: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1, a2); return; case 2: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1, a2); return;
case 3: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1, a2, a3); return; case 3: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1, a2, a3); return;
default: while (++i < l) (ev = events[i]).callback.apply(ev.ctx, args); default: while (++i < l) (ev = events[i]).callback.apply(ev.ctx, args); return;
} }
}; };
...@@ -215,10 +218,10 @@ ...@@ -215,10 +218,10 @@
// listening to. // listening to.
_.each(listenMethods, function(implementation, method) { _.each(listenMethods, function(implementation, method) {
Events[method] = function(obj, name, callback) { Events[method] = function(obj, name, callback) {
var listeners = this._listeners || (this._listeners = {}); var listeningTo = this._listeningTo || (this._listeningTo = {});
var id = obj._listenerId || (obj._listenerId = _.uniqueId('l')); var id = obj._listenId || (obj._listenId = _.uniqueId('l'));
listeners[id] = obj; listeningTo[id] = obj;
if (typeof name === 'object') callback = this; if (!callback && typeof name === 'object') callback = this;
obj[implementation](name, callback, this); obj[implementation](name, callback, this);
return this; return this;
}; };
...@@ -243,24 +246,18 @@ ...@@ -243,24 +246,18 @@
// Create a new model with the specified attributes. A client id (`cid`) // Create a new model with the specified attributes. A client id (`cid`)
// is automatically generated and assigned for you. // is automatically generated and assigned for you.
var Model = Backbone.Model = function(attributes, options) { var Model = Backbone.Model = function(attributes, options) {
var defaults;
var attrs = attributes || {}; var attrs = attributes || {};
options || (options = {}); options || (options = {});
this.cid = _.uniqueId('c'); this.cid = _.uniqueId('c');
this.attributes = {}; this.attributes = {};
_.extend(this, _.pick(options, modelOptions)); if (options.collection) this.collection = options.collection;
if (options.parse) attrs = this.parse(attrs, options) || {}; if (options.parse) attrs = this.parse(attrs, options) || {};
if (defaults = _.result(this, 'defaults')) { attrs = _.defaults({}, attrs, _.result(this, 'defaults'));
attrs = _.defaults({}, attrs, defaults);
}
this.set(attrs, options); this.set(attrs, options);
this.changed = {}; this.changed = {};
this.initialize.apply(this, arguments); this.initialize.apply(this, arguments);
}; };
// A list of options to be attached directly to the model, if provided.
var modelOptions = ['url', 'urlRoot', 'collection'];
// Attach all inheritable methods to the Model prototype. // Attach all inheritable methods to the Model prototype.
_.extend(Model.prototype, Events, { _.extend(Model.prototype, Events, {
...@@ -355,7 +352,7 @@ ...@@ -355,7 +352,7 @@
// Trigger all relevant attribute changes. // Trigger all relevant attribute changes.
if (!silent) { if (!silent) {
if (changes.length) this._pending = true; if (changes.length) this._pending = options;
for (var i = 0, l = changes.length; i < l; i++) { for (var i = 0, l = changes.length; i < l; i++) {
this.trigger('change:' + changes[i], this, current[changes[i]], options); this.trigger('change:' + changes[i], this, current[changes[i]], options);
} }
...@@ -366,6 +363,7 @@ ...@@ -366,6 +363,7 @@
if (changing) return this; if (changing) return this;
if (!silent) { if (!silent) {
while (this._pending) { while (this._pending) {
options = this._pending;
this._pending = false; this._pending = false;
this.trigger('change', this, options); this.trigger('change', this, options);
} }
...@@ -456,13 +454,16 @@ ...@@ -456,13 +454,16 @@
(attrs = {})[key] = val; (attrs = {})[key] = val;
} }
// If we're not waiting and attributes exist, save acts as `set(attr).save(null, opts)`.
if (attrs && (!options || !options.wait) && !this.set(attrs, options)) return false;
options = _.extend({validate: true}, options); options = _.extend({validate: true}, options);
// Do not persist invalid models. // If we're not waiting and attributes exist, save acts as
if (!this._validate(attrs, options)) return false; // `set(attr).save(null, opts)` with validation. Otherwise, check if
// the model will be valid when the attributes, if any, are set.
if (attrs && !options.wait) {
if (!this.set(attrs, options)) return false;
} else {
if (!this._validate(attrs, options)) return false;
}
// Set temporary attributes if `{wait: true}`. // Set temporary attributes if `{wait: true}`.
if (attrs && options.wait) { if (attrs && options.wait) {
...@@ -530,9 +531,12 @@ ...@@ -530,9 +531,12 @@
// using Backbone's restful methods, override this to change the endpoint // using Backbone's restful methods, override this to change the endpoint
// that will be called. // that will be called.
url: function() { url: function() {
var base = _.result(this, 'urlRoot') || _.result(this.collection, 'url') || urlError(); var base =
_.result(this, 'urlRoot') ||
_.result(this.collection, 'url') ||
urlError();
if (this.isNew()) return base; if (this.isNew()) return base;
return base + (base.charAt(base.length - 1) === '/' ? '' : '/') + encodeURIComponent(this.id); return base.replace(/([^\/])$/, '$1/') + encodeURIComponent(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
...@@ -548,7 +552,7 @@ ...@@ -548,7 +552,7 @@
// A model is new if it has never been saved to the server, and lacks an id. // A model is new if it has never been saved to the server, and lacks an id.
isNew: function() { isNew: function() {
return this.id == null; return !this.has(this.idAttribute);
}, },
// Check if the model is currently in a valid state. // Check if the model is currently in a valid state.
...@@ -563,7 +567,7 @@ ...@@ -563,7 +567,7 @@
attrs = _.extend({}, this.attributes, attrs); attrs = _.extend({}, this.attributes, attrs);
var error = this.validationError = this.validate(attrs, options) || null; var error = this.validationError = this.validate(attrs, options) || null;
if (!error) return true; if (!error) return true;
this.trigger('invalid', this, error, _.extend(options || {}, {validationError: error})); this.trigger('invalid', this, error, _.extend(options, {validationError: error}));
return false; return false;
} }
...@@ -596,7 +600,6 @@ ...@@ -596,7 +600,6 @@
// its models in sort order, as they're added and removed. // its models in sort order, as they're added and removed.
var Collection = Backbone.Collection = function(models, options) { var Collection = Backbone.Collection = function(models, options) {
options || (options = {}); options || (options = {});
if (options.url) this.url = options.url;
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._reset(); this._reset();
...@@ -606,7 +609,7 @@ ...@@ -606,7 +609,7 @@
// Default options for `Collection#set`. // Default options for `Collection#set`.
var setOptions = {add: true, remove: true, merge: true}; var setOptions = {add: true, remove: true, merge: true};
var addOptions = {add: true, merge: false, remove: false}; var addOptions = {add: true, remove: false};
// Define the Collection's inheritable methods. // Define the Collection's inheritable methods.
_.extend(Collection.prototype, Events, { _.extend(Collection.prototype, Events, {
...@@ -632,16 +635,17 @@ ...@@ -632,16 +635,17 @@
// Add a model, or list of models to the set. // Add a model, or list of models to the set.
add: function(models, options) { add: function(models, options) {
return this.set(models, _.defaults(options || {}, addOptions)); return this.set(models, _.extend({merge: false}, options, addOptions));
}, },
// Remove a model, or a list of models from the set. // Remove a model, or a list of models from the set.
remove: function(models, options) { remove: function(models, options) {
models = _.isArray(models) ? models.slice() : [models]; var singular = !_.isArray(models);
models = singular ? [models] : _.clone(models);
options || (options = {}); options || (options = {});
var i, l, index, model; 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 = models[i] = this.get(models[i]);
if (!model) continue; if (!model) continue;
delete this._byId[model.id]; delete this._byId[model.id];
delete this._byId[model.cid]; delete this._byId[model.cid];
...@@ -652,9 +656,9 @@ ...@@ -652,9 +656,9 @@
options.index = index; options.index = index;
model.trigger('remove', model, this, options); model.trigger('remove', model, this, options);
} }
this._removeReference(model); this._removeReference(model, options);
} }
return this; return singular ? models[0] : models;
}, },
// Update a collection by `set`-ing a new list of models, adding new ones, // Update a collection by `set`-ing a new list of models, adding new ones,
...@@ -662,43 +666,57 @@ ...@@ -662,43 +666,57 @@
// already exist in the collection, as necessary. Similar to **Model#set**, // already exist in the collection, as necessary. Similar to **Model#set**,
// the core operation for updating the data contained by the collection. // the core operation for updating the data contained by the collection.
set: function(models, options) { set: function(models, options) {
options = _.defaults(options || {}, setOptions); options = _.defaults({}, options, setOptions);
if (options.parse) models = this.parse(models, options); if (options.parse) models = this.parse(models, options);
if (!_.isArray(models)) models = models ? [models] : []; var singular = !_.isArray(models);
var i, l, model, attrs, existing, sort; models = singular ? (models ? [models] : []) : _.clone(models);
var i, l, id, model, attrs, existing, sort;
var at = options.at; var at = options.at;
var targetModel = this.model;
var sortable = this.comparator && (at == null) && options.sort !== false; var sortable = this.comparator && (at == null) && options.sort !== false;
var sortAttr = _.isString(this.comparator) ? this.comparator : null; var sortAttr = _.isString(this.comparator) ? this.comparator : null;
var toAdd = [], toRemove = [], modelMap = {}; var toAdd = [], toRemove = [], modelMap = {};
var add = options.add, merge = options.merge, remove = options.remove;
var order = !sortable && add && remove ? [] : false;
// 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 = 0, l = models.length; i < l; i++) { for (i = 0, l = models.length; i < l; i++) {
if (!(model = this._prepareModel(models[i], options))) continue; attrs = models[i] || {};
if (attrs instanceof Model) {
id = model = attrs;
} else {
id = attrs[targetModel.prototype.idAttribute || '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.get(model)) { if (existing = this.get(id)) {
if (options.remove) modelMap[existing.cid] = true; if (remove) modelMap[existing.cid] = true;
if (options.merge) { if (merge) {
existing.set(model.attributes, options); attrs = attrs === model ? model.attributes : attrs;
if (options.parse) attrs = existing.parse(attrs, options);
existing.set(attrs, options);
if (sortable && !sort && existing.hasChanged(sortAttr)) sort = true; if (sortable && !sort && existing.hasChanged(sortAttr)) sort = true;
} }
models[i] = existing;
// This is a new model, push it to the `toAdd` list. // If this is a new, valid model, push it to the `toAdd` list.
} else if (options.add) { } else if (add) {
model = models[i] = this._prepareModel(attrs, options);
if (!model) continue;
toAdd.push(model); toAdd.push(model);
this._addReference(model, options);
// Listen to added models' events, and index models for lookup by
// `id` and by `cid`.
model.on('all', this._onModelEvent, this);
this._byId[model.cid] = model;
if (model.id != null) this._byId[model.id] = model;
} }
// Do not add multiple models with the same `id`.
model = existing || model;
if (order && (model.isNew() || !modelMap[model.id])) order.push(model);
modelMap[model.id] = true;
} }
// Remove nonexistent models if appropriate. // Remove nonexistent models if appropriate.
if (options.remove) { if (remove) {
for (i = 0, l = this.length; i < l; ++i) { for (i = 0, l = this.length; i < l; ++i) {
if (!modelMap[(model = this.models[i]).cid]) toRemove.push(model); if (!modelMap[(model = this.models[i]).cid]) toRemove.push(model);
} }
...@@ -706,29 +724,35 @@ ...@@ -706,29 +724,35 @@
} }
// 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 (toAdd.length) { if (toAdd.length || (order && order.length)) {
if (sortable) sort = true; if (sortable) sort = true;
this.length += toAdd.length; this.length += toAdd.length;
if (at != null) { if (at != null) {
splice.apply(this.models, [at, 0].concat(toAdd)); for (i = 0, l = toAdd.length; i < l; i++) {
this.models.splice(at + i, 0, toAdd[i]);
}
} else { } else {
push.apply(this.models, toAdd); if (order) this.models.length = 0;
var orderedModels = order || toAdd;
for (i = 0, l = orderedModels.length; i < l; i++) {
this.models.push(orderedModels[i]);
}
} }
} }
// Silently sort the collection if appropriate. // Silently sort the collection if appropriate.
if (sort) this.sort({silent: true}); if (sort) this.sort({silent: true});
if (options.silent) return this; // Unless silenced, it's time to fire all appropriate add/sort events.
if (!options.silent) {
// Trigger `add` events. for (i = 0, l = toAdd.length; i < l; i++) {
for (i = 0, l = toAdd.length; i < l; i++) { (model = toAdd[i]).trigger('add', model, this, options);
(model = toAdd[i]).trigger('add', model, this, options); }
if (sort || (order && order.length)) this.trigger('sort', this, options);
} }
// Trigger `sort` if the collection was sorted. // Return the added (or merged) model (or models).
if (sort) this.trigger('sort', this, options); return singular ? models[0] : models;
return this;
}, },
// 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,
...@@ -738,20 +762,18 @@ ...@@ -738,20 +762,18 @@
reset: function(models, options) { reset: function(models, options) {
options || (options = {}); options || (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);
} }
options.previousModels = this.models; options.previousModels = this.models;
this._reset(); this._reset();
this.add(models, _.extend({silent: true}, options)); models = this.add(models, _.extend({silent: true}, options));
if (!options.silent) this.trigger('reset', this, options); if (!options.silent) this.trigger('reset', this, options);
return this; return models;
}, },
// Add a model to the end of the collection. // Add a model to the end of the collection.
push: function(model, options) { push: function(model, options) {
model = this._prepareModel(model, options); return this.add(model, _.extend({at: this.length}, options));
this.add(model, _.extend({at: this.length}, options));
return model;
}, },
// Remove a model from the end of the collection. // Remove a model from the end of the collection.
...@@ -763,9 +785,7 @@ ...@@ -763,9 +785,7 @@
// Add a model to the beginning of the collection. // Add a model to the beginning of the collection.
unshift: function(model, options) { unshift: function(model, options) {
model = this._prepareModel(model, options); return this.add(model, _.extend({at: 0}, options));
this.add(model, _.extend({at: 0}, options));
return model;
}, },
// Remove a model from the beginning of the collection. // Remove a model from the beginning of the collection.
...@@ -776,14 +796,14 @@ ...@@ -776,14 +796,14 @@
}, },
// Slice out a sub-array of models from the collection. // Slice out a sub-array of models from the collection.
slice: function(begin, end) { slice: function() {
return this.models.slice(begin, end); return slice.apply(this.models, arguments);
}, },
// 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.cid || obj]; return this._byId[obj] || this._byId[obj.id] || this._byId[obj.cid];
}, },
// Get the model at the given index. // Get the model at the given index.
...@@ -827,16 +847,6 @@ ...@@ -827,16 +847,6 @@
return this; return this;
}, },
// Figure out the smallest index at which a model should be inserted so as
// to maintain order.
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);
},
// Pluck an attribute from each model in the collection. // Pluck an attribute from each model in the collection.
pluck: function(attr) { pluck: function(attr) {
return _.invoke(this.models, 'get', attr); return _.invoke(this.models, 'get', attr);
...@@ -869,7 +879,7 @@ ...@@ -869,7 +879,7 @@
if (!options.wait) this.add(model, options); if (!options.wait) this.add(model, options);
var collection = this; var collection = this;
var success = options.success; var success = options.success;
options.success = function(resp) { options.success = function(model, resp) {
if (options.wait) collection.add(model, options); if (options.wait) collection.add(model, options);
if (success) success(model, resp, options); if (success) success(model, resp, options);
}; };
...@@ -899,22 +909,25 @@ ...@@ -899,22 +909,25 @@
// Prepare a hash of attributes (or other model) to be added to this // Prepare a hash of attributes (or other model) to be added to this
// collection. // collection.
_prepareModel: function(attrs, options) { _prepareModel: function(attrs, options) {
if (attrs instanceof Model) { if (attrs instanceof Model) return attrs;
if (!attrs.collection) attrs.collection = this; options = options ? _.clone(options) : {};
return attrs;
}
options || (options = {});
options.collection = this; options.collection = this;
var model = new this.model(attrs, options); var model = new this.model(attrs, options);
if (!model._validate(attrs, options)) { if (!model.validationError) return model;
this.trigger('invalid', this, attrs, options); this.trigger('invalid', this, model.validationError, options);
return false; return false;
} },
return model;
// Internal method to create a model's ties to a collection.
_addReference: function(model, options) {
this._byId[model.cid] = model;
if (model.id != null) this._byId[model.id] = model;
if (!model.collection) model.collection = this;
model.on('all', this._onModelEvent, this);
}, },
// Internal method to sever a model's ties to a collection. // Internal method to sever a model's ties to a collection.
_removeReference: function(model) { _removeReference: function(model, options) {
if (this === model.collection) delete model.collection; if (this === model.collection) delete model.collection;
model.off('all', this._onModelEvent, this); model.off('all', this._onModelEvent, this);
}, },
...@@ -942,8 +955,8 @@ ...@@ -942,8 +955,8 @@
'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', 'toArray', 'size', 'first', 'head', 'take', 'initial', 'rest', 'max', 'min', 'toArray', 'size', 'first', 'head', 'take', 'initial', 'rest',
'tail', 'drop', 'last', 'without', 'indexOf', 'shuffle', 'lastIndexOf', 'tail', 'drop', 'last', 'without', 'difference', 'indexOf', 'shuffle',
'isEmpty', 'chain']; 'lastIndexOf', 'isEmpty', 'chain', 'sample'];
// 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) {
...@@ -955,7 +968,7 @@ ...@@ -955,7 +968,7 @@
}); });
// Underscore methods that take a property name as an argument. // Underscore methods that take a property name as an argument.
var attributeMethods = ['groupBy', 'countBy', 'sortBy']; var attributeMethods = ['groupBy', 'countBy', 'sortBy', 'indexBy'];
// Use attributes instead of properties. // Use attributes instead of properties.
_.each(attributeMethods, function(method) { _.each(attributeMethods, function(method) {
...@@ -982,7 +995,8 @@ ...@@ -982,7 +995,8 @@
// if an existing element is not provided... // if an existing element is not provided...
var View = Backbone.View = function(options) { var View = Backbone.View = function(options) {
this.cid = _.uniqueId('view'); this.cid = _.uniqueId('view');
this._configure(options || {}); options || (options = {});
_.extend(this, _.pick(options, viewOptions));
this._ensureElement(); this._ensureElement();
this.initialize.apply(this, arguments); this.initialize.apply(this, arguments);
this.delegateEvents(); this.delegateEvents();
...@@ -1001,7 +1015,7 @@ ...@@ -1001,7 +1015,7 @@
tagName: 'div', tagName: 'div',
// jQuery delegate for element lookup, scoped to DOM elements within the // jQuery delegate for element lookup, scoped to DOM elements within the
// current view. This should be prefered to global lookups where possible. // current view. This should be preferred to global lookups where possible.
$: function(selector) { $: function(selector) {
return this.$el.find(selector); return this.$el.find(selector);
}, },
...@@ -1041,7 +1055,7 @@ ...@@ -1041,7 +1055,7 @@
// //
// { // {
// 'mousedown .title': 'edit', // 'mousedown .title': 'edit',
// 'click .button': 'save' // 'click .button': 'save',
// 'click .open': function(e) { ... } // 'click .open': function(e) { ... }
// } // }
// //
...@@ -1079,16 +1093,6 @@ ...@@ -1079,16 +1093,6 @@
return this; return this;
}, },
// Performs the initial configuration of a View with a set of options.
// Keys with special meaning *(e.g. model, collection, id, className)* are
// attached directly to the view. See `viewOptions` for an exhaustive
// list.
_configure: function(options) {
if (this.options) options = _.extend({}, _.result(this, 'options'), options);
_.extend(this, _.pick(options, viewOptions));
this.options = options;
},
// Ensure that the View has a DOM element to render into. // Ensure that the View has a DOM element to render into.
// If `this.el` is a string, pass it through `$()`, take the first // If `this.el` is a string, pass it through `$()`, take the first
// matching element, and re-assign it to `el`. Otherwise, create // matching element, and re-assign it to `el`. Otherwise, create
...@@ -1174,8 +1178,7 @@ ...@@ -1174,8 +1178,7 @@
// If we're sending a `PATCH` request, and we're in an old Internet Explorer // If we're sending a `PATCH` request, and we're in an old Internet Explorer
// that still has ActiveX enabled by default, override jQuery to use that // that still has ActiveX enabled by default, override jQuery to use that
// for XHR instead. Remove this line when jQuery supports `PATCH` on IE8. // for XHR instead. Remove this line when jQuery supports `PATCH` on IE8.
if (params.type === 'PATCH' && window.ActiveXObject && if (params.type === 'PATCH' && noXhrPatch) {
!(window.external && window.external.msActiveXFilteringEnabled)) {
params.xhr = function() { params.xhr = function() {
return new ActiveXObject("Microsoft.XMLHTTP"); return new ActiveXObject("Microsoft.XMLHTTP");
}; };
...@@ -1187,6 +1190,10 @@ ...@@ -1187,6 +1190,10 @@
return xhr; return xhr;
}; };
var noXhrPatch =
typeof window !== 'undefined' && !!window.ActiveXObject &&
!(window.XMLHttpRequest && (new XMLHttpRequest).dispatchEvent);
// Map from CRUD to HTTP for our default `Backbone.sync` implementation. // Map from CRUD to HTTP for our default `Backbone.sync` implementation.
var methodMap = { var methodMap = {
'create': 'POST', 'create': 'POST',
...@@ -1244,7 +1251,7 @@ ...@@ -1244,7 +1251,7 @@
var router = this; var router = this;
Backbone.history.route(route, function(fragment) { Backbone.history.route(route, function(fragment) {
var args = router._extractParameters(route, fragment); var args = router._extractParameters(route, fragment);
callback && callback.apply(router, args); router.execute(callback, args);
router.trigger.apply(router, ['route:' + name].concat(args)); router.trigger.apply(router, ['route:' + name].concat(args));
router.trigger('route', name, args); router.trigger('route', name, args);
Backbone.history.trigger('route', router, name, args); Backbone.history.trigger('route', router, name, args);
...@@ -1252,6 +1259,12 @@ ...@@ -1252,6 +1259,12 @@
return this; return this;
}, },
// Execute a route handler with the provided parameters. This is an
// excellent place to do pre-route setup or post-route cleanup.
execute: function(callback, args) {
if (callback) callback.apply(this, args);
},
// Simple proxy to `Backbone.history` to save a fragment into the history. // Simple proxy to `Backbone.history` to save a fragment into the history.
navigate: function(fragment, options) { navigate: function(fragment, options) {
Backbone.history.navigate(fragment, options); Backbone.history.navigate(fragment, options);
...@@ -1275,11 +1288,11 @@ ...@@ -1275,11 +1288,11 @@
_routeToRegExp: function(route) { _routeToRegExp: function(route) {
route = route.replace(escapeRegExp, '\\$&') route = route.replace(escapeRegExp, '\\$&')
.replace(optionalParam, '(?:$1)?') .replace(optionalParam, '(?:$1)?')
.replace(namedParam, function(match, optional){ .replace(namedParam, function(match, optional) {
return optional ? match : '([^\/]+)'; return optional ? match : '([^/?]+)';
}) })
.replace(splatParam, '(.*?)'); .replace(splatParam, '([^?]*?)');
return new RegExp('^' + route + '$'); return new RegExp('^' + route + '(?:\\?([\\s\\S]*))?$');
}, },
// 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
...@@ -1287,7 +1300,9 @@ ...@@ -1287,7 +1300,9 @@
// treated as `null` to normalize cross-browser behavior. // treated as `null` to normalize cross-browser behavior.
_extractParameters: function(route, fragment) { _extractParameters: function(route, fragment) {
var params = route.exec(fragment).slice(1); var params = route.exec(fragment).slice(1);
return _.map(params, function(param) { return _.map(params, function(param, i) {
// Don't decode the search params.
if (i === params.length - 1) return param || null;
return param ? decodeURIComponent(param) : null; return param ? decodeURIComponent(param) : null;
}); });
} }
...@@ -1325,6 +1340,9 @@ ...@@ -1325,6 +1340,9 @@
// Cached regex for removing a trailing slash. // Cached regex for removing a trailing slash.
var trailingSlash = /\/$/; var trailingSlash = /\/$/;
// Cached regex for stripping urls of hash.
var pathStripper = /#.*$/;
// Has the history handling already been started? // Has the history handling already been started?
History.started = false; History.started = false;
...@@ -1335,6 +1353,11 @@ ...@@ -1335,6 +1353,11 @@
// twenty times a second. // twenty times a second.
interval: 50, interval: 50,
// Are we at the app root?
atRoot: function() {
return this.location.pathname.replace(/[^\/]$/, '$&/') === this.root;
},
// Gets the true hash value. Cannot use location.hash directly due to bug // Gets the true hash value. Cannot use location.hash directly due to bug
// in Firefox where location.hash will always be decoded. // in Firefox where location.hash will always be decoded.
getHash: function(window) { getHash: function(window) {
...@@ -1347,9 +1370,9 @@ ...@@ -1347,9 +1370,9 @@
getFragment: function(fragment, forcePushState) { getFragment: function(fragment, forcePushState) {
if (fragment == null) { if (fragment == null) {
if (this._hasPushState || !this._wantsHashChange || forcePushState) { if (this._hasPushState || !this._wantsHashChange || forcePushState) {
fragment = this.location.pathname; fragment = decodeURI(this.location.pathname + this.location.search);
var root = this.root.replace(trailingSlash, ''); var root = this.root.replace(trailingSlash, '');
if (!fragment.indexOf(root)) fragment = fragment.substr(root.length); if (!fragment.indexOf(root)) fragment = fragment.slice(root.length);
} else { } else {
fragment = this.getHash(); fragment = this.getHash();
} }
...@@ -1365,7 +1388,7 @@ ...@@ -1365,7 +1388,7 @@
// Figure out the initial configuration. Do we need an iframe? // Figure out the initial configuration. Do we need an iframe?
// Is pushState desired ... is it available? // Is pushState desired ... is it available?
this.options = _.extend({}, {root: '/'}, this.options, options); this.options = _.extend({root: '/'}, this.options, options);
this.root = this.options.root; this.root = this.options.root;
this._wantsHashChange = this.options.hashChange !== false; this._wantsHashChange = this.options.hashChange !== false;
this._wantsPushState = !!this.options.pushState; this._wantsPushState = !!this.options.pushState;
...@@ -1378,7 +1401,8 @@ ...@@ -1378,7 +1401,8 @@
this.root = ('/' + this.root + '/').replace(rootStripper, '/'); this.root = ('/' + this.root + '/').replace(rootStripper, '/');
if (oldIE && this._wantsHashChange) { if (oldIE && this._wantsHashChange) {
this.iframe = Backbone.$('<iframe src="javascript:0" tabindex="-1" />').hide().appendTo('body')[0].contentWindow; var frame = Backbone.$('<iframe src="javascript:0" tabindex="-1">');
this.iframe = frame.hide().appendTo('body')[0].contentWindow;
this.navigate(fragment); this.navigate(fragment);
} }
...@@ -1396,21 +1420,26 @@ ...@@ -1396,21 +1420,26 @@
// opened by a non-pushState browser. // opened by a non-pushState browser.
this.fragment = fragment; this.fragment = fragment;
var loc = this.location; var loc = this.location;
var atRoot = loc.pathname.replace(/[^\/]$/, '$&/') === this.root;
// If we've started off with a route from a `pushState`-enabled browser,
// but we're currently in a browser that doesn't support it...
if (this._wantsHashChange && this._wantsPushState && !this._hasPushState && !atRoot) {
this.fragment = this.getFragment(null, true);
this.location.replace(this.root + this.location.search + '#' + this.fragment);
// Return immediately as browser will do redirect to new url
return true;
// Or if we've started out with a hash-based route, but we're currently // Transition from hashChange to pushState or vice versa if both are
// in a browser where it could be `pushState`-based instead... // requested.
} else if (this._wantsPushState && this._hasPushState && atRoot && loc.hash) { if (this._wantsHashChange && this._wantsPushState) {
this.fragment = this.getHash().replace(routeStripper, '');
this.history.replaceState({}, document.title, this.root + this.fragment + loc.search); // If we've started off with a route from a `pushState`-enabled
// browser, but we're currently in a browser that doesn't support it...
if (!this._hasPushState && !this.atRoot()) {
this.fragment = this.getFragment(null, true);
this.location.replace(this.root + '#' + this.fragment);
// Return immediately as browser will do redirect to new url
return true;
// Or if we've started out with a hash-based route, but we're currently
// in a browser where it could be `pushState`-based instead...
} else if (this._hasPushState && this.atRoot() && loc.hash) {
this.fragment = this.getHash().replace(routeStripper, '');
this.history.replaceState({}, document.title, this.root + this.fragment);
}
} }
if (!this.options.silent) return this.loadUrl(); if (!this.options.silent) return this.loadUrl();
...@@ -1420,7 +1449,7 @@ ...@@ -1420,7 +1449,7 @@
// but possibly useful for unit testing Routers. // but possibly useful for unit testing Routers.
stop: function() { stop: function() {
Backbone.$(window).off('popstate', this.checkUrl).off('hashchange', this.checkUrl); Backbone.$(window).off('popstate', this.checkUrl).off('hashchange', this.checkUrl);
clearInterval(this._checkUrlInterval); if (this._checkUrlInterval) clearInterval(this._checkUrlInterval);
History.started = false; History.started = false;
}, },
...@@ -1439,21 +1468,20 @@ ...@@ -1439,21 +1468,20 @@
} }
if (current === this.fragment) return false; if (current === this.fragment) return false;
if (this.iframe) this.navigate(current); if (this.iframe) this.navigate(current);
this.loadUrl() || this.loadUrl(this.getHash()); 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, // match, returns `true`. If no defined routes matches the fragment,
// returns `false`. // returns `false`.
loadUrl: function(fragmentOverride) { loadUrl: function(fragment) {
var fragment = this.fragment = this.getFragment(fragmentOverride); fragment = this.fragment = this.getFragment(fragment);
var matched = _.any(this.handlers, function(handler) { return _.any(this.handlers, function(handler) {
if (handler.route.test(fragment)) { if (handler.route.test(fragment)) {
handler.callback(fragment); handler.callback(fragment);
return true; return true;
} }
}); });
return matched;
}, },
// Save a fragment into the hash history, or replace the URL state if the // Save a fragment into the hash history, or replace the URL state if the
...@@ -1465,11 +1493,18 @@ ...@@ -1465,11 +1493,18 @@
// you wish to modify the current URL without adding an entry to the history. // you wish to modify the current URL without adding an entry to the history.
navigate: function(fragment, options) { navigate: function(fragment, options) {
if (!History.started) return false; if (!History.started) return false;
if (!options || options === true) options = {trigger: options}; if (!options || options === true) options = {trigger: !!options};
fragment = this.getFragment(fragment || '');
var url = this.root + (fragment = this.getFragment(fragment || ''));
// Strip the hash for matching.
fragment = fragment.replace(pathStripper, '');
if (this.fragment === fragment) return; if (this.fragment === fragment) return;
this.fragment = fragment; this.fragment = fragment;
var url = this.root + fragment;
// Don't include a trailing slash on the root.
if (fragment === '' && url !== '/') url = url.slice(0, -1);
// If pushState is available, we use it to set the fragment as a real URL. // If pushState is available, we use it to set the fragment as a real URL.
if (this._hasPushState) { if (this._hasPushState) {
...@@ -1492,7 +1527,7 @@ ...@@ -1492,7 +1527,7 @@
} else { } else {
return this.location.assign(url); return this.location.assign(url);
} }
if (options.trigger) this.loadUrl(fragment); if (options.trigger) return this.loadUrl(fragment);
}, },
// Update the hash location, either replacing the current entry, or adding // Update the hash location, either replacing the current entry, or adding
...@@ -1560,7 +1595,7 @@ ...@@ -1560,7 +1595,7 @@
}; };
// Wrap an optional error callback with a fallback error event. // Wrap an optional error callback with a fallback error event.
var wrapError = function (model, options) { var wrapError = function(model, options) {
var error = options.error; var error = options.error;
options.error = function(resp) { options.error = function(resp) {
if (error) error(model, resp, options); if (error) error(model, resp, options);
...@@ -1568,4 +1603,6 @@ ...@@ -1568,4 +1603,6 @@
}; };
}; };
}).call(this); return Backbone;
}));
This source diff could not be displayed because it is too large. You can view the blob instead.
...@@ -12,104 +12,81 @@ button { ...@@ -12,104 +12,81 @@ button {
font-size: 100%; font-size: 100%;
vertical-align: baseline; vertical-align: baseline;
font-family: inherit; font-family: inherit;
font-weight: inherit;
color: inherit; color: inherit;
-webkit-appearance: none; -webkit-appearance: none;
-ms-appearance: none; -ms-appearance: none;
-o-appearance: none;
appearance: none; appearance: none;
-webkit-font-smoothing: antialiased;
-moz-font-smoothing: antialiased;
-ms-font-smoothing: antialiased;
font-smoothing: antialiased;
} }
body { body {
font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif; font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif;
line-height: 1.4em; line-height: 1.4em;
background: #eaeaea url('bg.png'); background: #f5f5f5;
color: #4d4d4d; color: #4d4d4d;
width: 550px; min-width: 230px;
max-width: 550px;
margin: 0 auto; margin: 0 auto;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-font-smoothing: antialiased; -moz-font-smoothing: antialiased;
-ms-font-smoothing: antialiased; -ms-font-smoothing: antialiased;
-o-font-smoothing: antialiased;
font-smoothing: antialiased; font-smoothing: antialiased;
font-weight: 300;
} }
button, button,
input[type="checkbox"] { input[type="checkbox"] {
outline: none; outline: none;
}
.hidden {
display: none;
} }
#todoapp { #todoapp {
background: #fff; background: #fff;
background: rgba(255, 255, 255, 0.9);
margin: 130px 0 40px 0; margin: 130px 0 40px 0;
border: 1px solid #ccc;
position: relative; position: relative;
border-top-left-radius: 2px; box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2),
border-top-right-radius: 2px; 0 25px 50px 0 rgba(0, 0, 0, 0.1);
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 { #todoapp input::-webkit-input-placeholder {
font-style: italic; font-style: italic;
font-weight: 300;
color: #e6e6e6;
} }
#todoapp input::-moz-placeholder { #todoapp input::-moz-placeholder {
font-style: italic; font-style: italic;
color: #a9a9a9; font-weight: 300;
color: #e6e6e6;
}
#todoapp input::input-placeholder {
font-style: italic;
font-weight: 300;
color: #e6e6e6;
} }
#todoapp h1 { #todoapp h1 {
position: absolute; position: absolute;
top: -120px; top: -155px;
width: 100%; width: 100%;
font-size: 70px; font-size: 100px;
font-weight: bold; font-weight: 100;
text-align: center; text-align: center;
color: #b3b3b3; color: rgba(175, 47, 47, 0.15);
color: rgba(255, 255, 255, 0.3);
text-shadow: -1px -1px rgba(0, 0, 0, 0.2);
-webkit-text-rendering: optimizeLegibility; -webkit-text-rendering: optimizeLegibility;
-moz-text-rendering: optimizeLegibility; -moz-text-rendering: optimizeLegibility;
-ms-text-rendering: optimizeLegibility; -ms-text-rendering: optimizeLegibility;
-o-text-rendering: optimizeLegibility;
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: 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, #new-todo,
.edit { .edit {
position: relative; position: relative;
...@@ -117,6 +94,7 @@ input[type="checkbox"] { ...@@ -117,6 +94,7 @@ input[type="checkbox"] {
width: 100%; width: 100%;
font-size: 24px; font-size: 24px;
font-family: inherit; font-family: inherit;
font-weight: inherit;
line-height: 1.4em; line-height: 1.4em;
border: 0; border: 0;
outline: none; outline: none;
...@@ -124,29 +102,25 @@ input[type="checkbox"] { ...@@ -124,29 +102,25 @@ input[type="checkbox"] {
padding: 6px; padding: 6px;
border: 1px solid #999; border: 1px solid #999;
box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2); box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2);
-moz-box-sizing: border-box;
-ms-box-sizing: border-box; -ms-box-sizing: border-box;
-o-box-sizing: border-box;
box-sizing: border-box; box-sizing: border-box;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-font-smoothing: antialiased; -moz-font-smoothing: antialiased;
-ms-font-smoothing: antialiased; -ms-font-smoothing: antialiased;
-o-font-smoothing: antialiased;
font-smoothing: antialiased; font-smoothing: antialiased;
} }
#new-todo { #new-todo {
padding: 16px 16px 16px 60px; padding: 16px 16px 16px 60px;
border: none; border: none;
background: rgba(0, 0, 0, 0.02); background: rgba(0, 0, 0, 0.003);
z-index: 2; box-shadow: inset 0 -2px 1px rgba(0,0,0,0.03);
box-shadow: none;
} }
#main { #main {
position: relative; position: relative;
z-index: 2; z-index: 2;
border-top: 1px dotted #adadad; border-top: 1px solid #e6e6e6;
} }
label[for='toggle-all'] { label[for='toggle-all'] {
...@@ -155,19 +129,19 @@ label[for='toggle-all'] { ...@@ -155,19 +129,19 @@ label[for='toggle-all'] {
#toggle-all { #toggle-all {
position: absolute; position: absolute;
top: -42px; top: -55px;
left: -4px; left: -12px;
width: 40px; width: 60px;
height: 34px;
text-align: center; text-align: center;
/* Mobile Safari */ border: none; /* Mobile Safari */
border: none;
} }
#toggle-all:before { #toggle-all:before {
content: '»'; content: '';
font-size: 28px; font-size: 22px;
color: #d9d9d9; color: #e6e6e6;
padding: 0 25px 7px; padding: 10px 27px 10px 27px;
} }
#toggle-all:checked:before { #toggle-all:checked:before {
...@@ -183,7 +157,7 @@ label[for='toggle-all'] { ...@@ -183,7 +157,7 @@ label[for='toggle-all'] {
#todo-list li { #todo-list li {
position: relative; position: relative;
font-size: 24px; font-size: 24px;
border-bottom: 1px dotted #ccc; border-bottom: 1px solid #ededed;
} }
#todo-list li:last-child { #todo-list li:last-child {
...@@ -215,28 +189,18 @@ label[for='toggle-all'] { ...@@ -215,28 +189,18 @@ label[for='toggle-all'] {
top: 0; top: 0;
bottom: 0; bottom: 0;
margin: auto 0; margin: auto 0;
/* Mobile Safari */ border: none; /* Mobile Safari */
border: none;
-webkit-appearance: none; -webkit-appearance: none;
-ms-appearance: none; -ms-appearance: none;
-o-appearance: none;
appearance: none; appearance: none;
} }
#todo-list li .toggle:after { #todo-list li .toggle:after {
content: '✔'; content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="-10 -18 100 135"><circle cx="50" cy="50" r="50" fill="none" stroke="#ededed" stroke-width="3"/></svg>');
/* 40 + a couple of pixels visual adjustment */
line-height: 43px;
font-size: 20px;
color: #d9d9d9;
text-shadow: 0 -1px 0 #bfbfbf;
} }
#todo-list li .toggle:checked:after { #todo-list li .toggle:checked:after {
color: #85ada7; content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="-10 -18 100 135"><circle cx="50" cy="50" r="50" fill="none" stroke="#bddad5" stroke-width="3"/><path fill="#5dc2af" d="M72 25L42 71 27 56l-4 4 20 20 34-52z"/></svg>');
text-shadow: 0 1px 0 #669991;
bottom: 1px;
position: relative;
} }
#todo-list li label { #todo-list li label {
...@@ -246,12 +210,11 @@ label[for='toggle-all'] { ...@@ -246,12 +210,11 @@ label[for='toggle-all'] {
margin-left: 45px; margin-left: 45px;
display: block; display: block;
line-height: 1.2; line-height: 1.2;
-webkit-transition: color 0.4s;
transition: color 0.4s; transition: color 0.4s;
} }
#todo-list li.completed label { #todo-list li.completed label {
color: #a9a9a9; color: #d9d9d9;
text-decoration: line-through; text-decoration: line-through;
} }
...@@ -264,21 +227,18 @@ label[for='toggle-all'] { ...@@ -264,21 +227,18 @@ label[for='toggle-all'] {
width: 40px; width: 40px;
height: 40px; height: 40px;
margin: auto 0; margin: auto 0;
font-size: 22px; font-size: 30px;
color: #a88a8a; color: #cc9a9a;
-webkit-transition: all 0.2s; margin-bottom: 11px;
transition: all 0.2s; transition: color 0.2s ease-out;
} }
#todo-list li .destroy:hover { #todo-list li .destroy:hover {
text-shadow: 0 0 1px #000, color: #af5b5e;
0 0 10px rgba(199, 107, 107, 0.8);
-webkit-transform: scale(1.3);
transform: scale(1.3);
} }
#todo-list li .destroy:after { #todo-list li .destroy:after {
content: ''; content: '×';
} }
#todo-list li:hover .destroy { #todo-list li:hover .destroy {
...@@ -295,29 +255,25 @@ label[for='toggle-all'] { ...@@ -295,29 +255,25 @@ label[for='toggle-all'] {
#footer { #footer {
color: #777; color: #777;
padding: 0 15px; padding: 10px 15px;
position: absolute;
right: 0;
bottom: -31px;
left: 0;
height: 20px; height: 20px;
z-index: 1;
text-align: center; text-align: center;
border-top: 1px solid #e6e6e6;
} }
#footer:before { #footer:before {
content: ''; content: '';
position: absolute; position: absolute;
right: 0; right: 0;
bottom: 31px; bottom: 0;
left: 0; left: 0;
height: 50px; height: 50px;
z-index: -1; overflow: hidden;
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.3), box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2),
0 6px 0 -3px rgba(255, 255, 255, 0.8), 0 8px 0 -3px #f6f6f6,
0 7px 1px -3px rgba(0, 0, 0, 0.3), 0 9px 1px -3px rgba(0, 0, 0, 0.2),
0 43px 0 -6px rgba(255, 255, 255, 0.8), 0 16px 0 -6px #f6f6f6,
0 44px 2px -6px rgba(0, 0, 0, 0.2); 0 17px 2px -6px rgba(0, 0, 0, 0.2);
} }
#todo-count { #todo-count {
...@@ -325,6 +281,10 @@ label[for='toggle-all'] { ...@@ -325,6 +281,10 @@ label[for='toggle-all'] {
text-align: left; text-align: left;
} }
#todo-count strong {
font-weight: 300;
}
#filters { #filters {
margin: 0; margin: 0;
padding: 0; padding: 0;
...@@ -339,49 +299,72 @@ label[for='toggle-all'] { ...@@ -339,49 +299,72 @@ label[for='toggle-all'] {
} }
#filters li a { #filters li a {
color: #83756f; color: inherit;
margin: 2px; margin: 3px;
padding: 3px 7px;
text-decoration: none; text-decoration: none;
border: 1px solid transparent;
border-radius: 3px;
}
#filters li a.selected,
#filters li a:hover {
border-color: rgba(175, 47, 47, 0.1);
} }
#filters li a.selected { #filters li a.selected {
font-weight: bold; border-color: rgba(175, 47, 47, 0.2);
} }
#clear-completed { #clear-completed,
html #clear-completed:active {
float: right; float: right;
position: relative; position: relative;
line-height: 20px; line-height: 20px;
text-decoration: none; text-decoration: none;
background: rgba(0, 0, 0, 0.1); cursor: pointer;
font-size: 11px; visibility: hidden;
padding: 0 10px; position: relative;
border-radius: 3px; }
box-shadow: 0 -1px 0 0 rgba(0, 0, 0, 0.2);
#clear-completed::after {
visibility: visible;
content: 'Clear completed';
position: absolute;
right: 0;
white-space: nowrap;
} }
#clear-completed:hover { #clear-completed:hover::after {
background: rgba(0, 0, 0, 0.15); text-decoration: underline;
box-shadow: 0 -1px 0 0 rgba(0, 0, 0, 0.3);
} }
#info { #info {
margin: 65px auto 0; margin: 65px auto 0;
color: #a6a6a6; color: #bfbfbf;
font-size: 12px; font-size: 10px;
text-shadow: 0 1px 0 rgba(255, 255, 255, 0.7); text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5);
text-align: center; text-align: center;
} }
#info p {
line-height: 1;
}
#info a { #info a {
color: inherit; color: inherit;
text-decoration: none;
font-weight: 400;
}
#info a:hover {
text-decoration: underline;
} }
/* /*
Hack to remove background from Mobile Safari. Hack to remove background from Mobile Safari.
Can't use it globally since it destroys checkboxes in Firefox and Opera Can't use it globally since it destroys checkboxes in Firefox
*/ */
@media screen and (-webkit-min-device-pixel-ratio:0) { @media screen and (-webkit-min-device-pixel-ratio:0) {
#toggle-all, #toggle-all,
#todo-list li .toggle { #todo-list li .toggle {
...@@ -393,10 +376,6 @@ label[for='toggle-all'] { ...@@ -393,10 +376,6 @@ label[for='toggle-all'] {
} }
#toggle-all { #toggle-all {
top: -56px;
left: -15px;
width: 65px;
height: 41px;
-webkit-transform: rotate(90deg); -webkit-transform: rotate(90deg);
transform: rotate(90deg); transform: rotate(90deg);
-webkit-appearance: none; -webkit-appearance: none;
...@@ -404,151 +383,12 @@ label[for='toggle-all'] { ...@@ -404,151 +383,12 @@ label[for='toggle-all'] {
} }
} }
.hidden { @media (max-width: 430px) {
display: none; #footer {
} height: 50px;
hr {
margin: 20px 0;
border: 0;
border-top: 1px dashed #C5C5C5;
border-bottom: 1px dashed #F7F7F7;
}
.learn a {
font-weight: normal;
text-decoration: none;
color: #b83f45;
}
.learn a:hover {
text-decoration: underline;
color: #787e7e;
}
.learn h3,
.learn h4,
.learn h5 {
margin: 10px 0;
font-weight: 500;
line-height: 1.2;
color: #000;
}
.learn h3 {
font-size: 24px;
}
.learn h4 {
font-size: 18px;
}
.learn h5 {
margin-bottom: 0;
font-size: 14px;
}
.learn ul {
padding: 0;
margin: 0 0 30px 25px;
}
.learn li {
line-height: 20px;
}
.learn p {
font-size: 15px;
font-weight: 300;
line-height: 1.3;
margin-top: 0;
margin-bottom: 0;
}
.quote {
border: none;
margin: 20px 0 60px 0;
}
.quote p {
font-style: italic;
}
.quote p:before {
content: '“';
font-size: 50px;
opacity: .15;
position: absolute;
top: -20px;
left: 3px;
}
.quote p:after {
content: '”';
font-size: 50px;
opacity: .15;
position: absolute;
bottom: -42px;
right: 3px;
}
.quote footer {
position: absolute;
bottom: -40px;
right: 0;
}
.quote footer img {
border-radius: 3px;
}
.quote footer a {
margin-left: 5px;
vertical-align: middle;
}
.speech-bubble {
position: relative;
padding: 10px;
background: rgba(0, 0, 0, .04);
border-radius: 5px;
}
.speech-bubble:after {
content: '';
position: absolute;
top: 100%;
right: 30px;
border: 13px solid transparent;
border-top-color: rgba(0, 0, 0, .04);
}
.learn-bar > .learn {
position: absolute;
width: 272px;
top: 8px;
left: -300px;
padding: 10px;
border-radius: 5px;
background-color: rgba(255, 255, 255, .6);
-webkit-transition-property: left;
transition-property: left;
-webkit-transition-duration: 500ms;
transition-duration: 500ms;
}
@media (min-width: 899px) {
.learn-bar {
width: auto;
margin: 0 0 0 300px;
}
.learn-bar > .learn {
left: 8px;
} }
.learn-bar #todoapp { #filters {
width: 550px; bottom: 10px;
margin: 130px auto 40px auto;
} }
} }
hr {
margin: 20px 0;
border: 0;
border-top: 1px dashed #c5c5c5;
border-bottom: 1px dashed #f7f7f7;
}
.learn a {
font-weight: normal;
text-decoration: none;
color: #b83f45;
}
.learn a:hover {
text-decoration: underline;
color: #787e7e;
}
.learn h3,
.learn h4,
.learn h5 {
margin: 10px 0;
font-weight: 500;
line-height: 1.2;
color: #000;
}
.learn h3 {
font-size: 24px;
}
.learn h4 {
font-size: 18px;
}
.learn h5 {
margin-bottom: 0;
font-size: 14px;
}
.learn ul {
padding: 0;
margin: 0 0 30px 25px;
}
.learn li {
line-height: 20px;
}
.learn p {
font-size: 15px;
font-weight: 300;
line-height: 1.3;
margin-top: 0;
margin-bottom: 0;
}
#issue-count {
display: none;
}
.quote {
border: none;
margin: 20px 0 60px 0;
}
.quote p {
font-style: italic;
}
.quote p:before {
content: '“';
font-size: 50px;
opacity: .15;
position: absolute;
top: -20px;
left: 3px;
}
.quote p:after {
content: '”';
font-size: 50px;
opacity: .15;
position: absolute;
bottom: -42px;
right: 3px;
}
.quote footer {
position: absolute;
bottom: -40px;
right: 0;
}
.quote footer img {
border-radius: 3px;
}
.quote footer a {
margin-left: 5px;
vertical-align: middle;
}
.speech-bubble {
position: relative;
padding: 10px;
background: rgba(0, 0, 0, .04);
border-radius: 5px;
}
.speech-bubble:after {
content: '';
position: absolute;
top: 100%;
right: 30px;
border: 13px solid transparent;
border-top-color: rgba(0, 0, 0, .04);
}
.learn-bar > .learn {
position: absolute;
width: 272px;
top: 8px;
left: -300px;
padding: 10px;
border-radius: 5px;
background-color: rgba(255, 255, 255, .6);
transition-property: left;
transition-duration: 500ms;
}
@media (min-width: 899px) {
.learn-bar {
width: auto;
padding-left: 300px;
}
.learn-bar > .learn {
left: 8px;
}
}
/* global _ */
(function () { (function () {
'use strict'; 'use strict';
/* jshint ignore:start */
// Underscore's Template Module // Underscore's Template Module
// Courtesy of underscorejs.org // Courtesy of underscorejs.org
var _ = (function (_) { var _ = (function (_) {
...@@ -114,6 +116,7 @@ ...@@ -114,6 +116,7 @@
if (location.hostname === 'todomvc.com') { if (location.hostname === 'todomvc.com') {
window._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')); window._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'));
} }
/* jshint ignore:end */
function redirect() { function redirect() {
if (location.hostname === 'tastejs.github.io') { if (location.hostname === 'tastejs.github.io') {
...@@ -175,13 +178,17 @@ ...@@ -175,13 +178,17 @@
if (learnJSON.backend) { if (learnJSON.backend) {
this.frameworkJSON = learnJSON.backend; this.frameworkJSON = learnJSON.backend;
this.frameworkJSON.issueLabel = framework;
this.append({ this.append({
backend: true backend: true
}); });
} else if (learnJSON[framework]) { } else if (learnJSON[framework]) {
this.frameworkJSON = learnJSON[framework]; this.frameworkJSON = learnJSON[framework];
this.frameworkJSON.issueLabel = framework;
this.append(); this.append();
} }
this.fetchIssueCount();
} }
Learn.prototype.append = function (opts) { Learn.prototype.append = function (opts) {
...@@ -212,6 +219,26 @@ ...@@ -212,6 +219,26 @@
document.body.insertAdjacentHTML('afterBegin', aside.outerHTML); document.body.insertAdjacentHTML('afterBegin', aside.outerHTML);
}; };
Learn.prototype.fetchIssueCount = function () {
var issueLink = document.getElementById('issue-count-link');
if (issueLink) {
var url = issueLink.href.replace('https://github.com', 'https://api.github.com/repos');
var xhr = new XMLHttpRequest();
xhr.open('GET', url, true);
xhr.onload = function (e) {
var parsedResponse = JSON.parse(e.target.responseText);
if (parsedResponse instanceof Array) {
var count = parsedResponse.length
if (count !== 0) {
issueLink.innerHTML = 'This app has ' + count + ' open issues';
document.getElementById('issue-count').style.display = 'inline';
}
}
};
xhr.send();
}
};
redirect(); redirect();
getFile('learn.json', Learn); getFile('learn.json', Learn);
})(); })();
{
"private": true,
"dependencies": {
"backbone": "^1.1.2",
"backbone.localstorage": "^1.1.16",
"jquery": "^2.1.3",
"lodash": "^3.1.0",
"todomvc-app-css": "^1.0.0",
"todomvc-common": "^1.0.1"
}
}
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