Commit 3dd6105b authored by Pascal Hartig's avatar Pascal Hartig

Merge branch 'pr/1103'

Close #1103
parents e5adfb0c 189da5c8
node_modules/angular/*
!node_modules/angular/angular.js
node_modules/angularfire/*
node_modules/angularfire/dist/*
!node_modules/angularfire/dist/angularfire.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-angular",
"version": "0.0.0",
"dependencies": {
"angular": "1.2.8",
"angularfire": "~0.5.0",
"todomvc-common": "~0.3.0"
},
"devDependencies": {
"angular-mocks": "1.2.8"
},
"resolutions": {
"angular": "1.2.8"
}
}
/**
* @license AngularJS v1.2.8
* (c) 2010-2014 Google, Inc. http://angularjs.org
* License: MIT
*/
(function(window, angular, undefined) {
'use strict';
/**
* @ngdoc overview
* @name angular.mock
* @description
*
* Namespace from 'angular-mocks.js' which contains testing related code.
*/
angular.mock = {};
/**
* ! This is a private undocumented service !
*
* @name ngMock.$browser
*
* @description
* This service is a mock implementation of {@link ng.$browser}. It provides fake
* implementation for commonly used browser apis that are hard to test, e.g. setTimeout, xhr,
* cookies, etc...
*
* The api of this service is the same as that of the real {@link ng.$browser $browser}, except
* that there are several helper methods available which can be used in tests.
*/
angular.mock.$BrowserProvider = function() {
this.$get = function() {
return new angular.mock.$Browser();
};
};
angular.mock.$Browser = function() {
var self = this;
this.isMock = true;
self.$$url = "http://server/";
self.$$lastUrl = self.$$url; // used by url polling fn
self.pollFns = [];
// TODO(vojta): remove this temporary api
self.$$completeOutstandingRequest = angular.noop;
self.$$incOutstandingRequestCount = angular.noop;
// register url polling fn
self.onUrlChange = function(listener) {
self.pollFns.push(
function() {
if (self.$$lastUrl != self.$$url) {
self.$$lastUrl = self.$$url;
listener(self.$$url);
}
}
);
return listener;
};
self.cookieHash = {};
self.lastCookieHash = {};
self.deferredFns = [];
self.deferredNextId = 0;
self.defer = function(fn, delay) {
delay = delay || 0;
self.deferredFns.push({time:(self.defer.now + delay), fn:fn, id: self.deferredNextId});
self.deferredFns.sort(function(a,b){ return a.time - b.time;});
return self.deferredNextId++;
};
/**
* @name ngMock.$browser#defer.now
* @propertyOf ngMock.$browser
*
* @description
* Current milliseconds mock time.
*/
self.defer.now = 0;
self.defer.cancel = function(deferId) {
var fnIndex;
angular.forEach(self.deferredFns, function(fn, index) {
if (fn.id === deferId) fnIndex = index;
});
if (fnIndex !== undefined) {
self.deferredFns.splice(fnIndex, 1);
return true;
}
return false;
};
/**
* @name ngMock.$browser#defer.flush
* @methodOf ngMock.$browser
*
* @description
* Flushes all pending requests and executes the defer callbacks.
*
* @param {number=} number of milliseconds to flush. See {@link #defer.now}
*/
self.defer.flush = function(delay) {
if (angular.isDefined(delay)) {
self.defer.now += delay;
} else {
if (self.deferredFns.length) {
self.defer.now = self.deferredFns[self.deferredFns.length-1].time;
} else {
throw new Error('No deferred tasks to be flushed');
}
}
while (self.deferredFns.length && self.deferredFns[0].time <= self.defer.now) {
self.deferredFns.shift().fn();
}
};
self.$$baseHref = '';
self.baseHref = function() {
return this.$$baseHref;
};
};
angular.mock.$Browser.prototype = {
/**
* @name ngMock.$browser#poll
* @methodOf ngMock.$browser
*
* @description
* run all fns in pollFns
*/
poll: function poll() {
angular.forEach(this.pollFns, function(pollFn){
pollFn();
});
},
addPollFn: function(pollFn) {
this.pollFns.push(pollFn);
return pollFn;
},
url: function(url, replace) {
if (url) {
this.$$url = url;
return this;
}
return this.$$url;
},
cookies: function(name, value) {
if (name) {
if (angular.isUndefined(value)) {
delete this.cookieHash[name];
} else {
if (angular.isString(value) && //strings only
value.length <= 4096) { //strict cookie storage limits
this.cookieHash[name] = value;
}
}
} else {
if (!angular.equals(this.cookieHash, this.lastCookieHash)) {
this.lastCookieHash = angular.copy(this.cookieHash);
this.cookieHash = angular.copy(this.cookieHash);
}
return this.cookieHash;
}
},
notifyWhenNoOutstandingRequests: function(fn) {
fn();
}
};
/**
* @ngdoc object
* @name ngMock.$exceptionHandlerProvider
*
* @description
* Configures the mock implementation of {@link ng.$exceptionHandler} to rethrow or to log errors
* passed into the `$exceptionHandler`.
*/
/**
* @ngdoc object
* @name ngMock.$exceptionHandler
*
* @description
* Mock implementation of {@link ng.$exceptionHandler} that rethrows or logs errors passed
* into it. See {@link ngMock.$exceptionHandlerProvider $exceptionHandlerProvider} for configuration
* information.
*
*
* <pre>
* describe('$exceptionHandlerProvider', function() {
*
* it('should capture log messages and exceptions', function() {
*
* module(function($exceptionHandlerProvider) {
* $exceptionHandlerProvider.mode('log');
* });
*
* inject(function($log, $exceptionHandler, $timeout) {
* $timeout(function() { $log.log(1); });
* $timeout(function() { $log.log(2); throw 'banana peel'; });
* $timeout(function() { $log.log(3); });
* expect($exceptionHandler.errors).toEqual([]);
* expect($log.assertEmpty());
* $timeout.flush();
* expect($exceptionHandler.errors).toEqual(['banana peel']);
* expect($log.log.logs).toEqual([[1], [2], [3]]);
* });
* });
* });
* </pre>
*/
angular.mock.$ExceptionHandlerProvider = function() {
var handler;
/**
* @ngdoc method
* @name ngMock.$exceptionHandlerProvider#mode
* @methodOf ngMock.$exceptionHandlerProvider
*
* @description
* Sets the logging mode.
*
* @param {string} mode Mode of operation, defaults to `rethrow`.
*
* - `rethrow`: If any errors are passed into the handler in tests, it typically
* means that there is a bug in the application or test, so this mock will
* make these tests fail.
* - `log`: Sometimes it is desirable to test that an error is thrown, for this case the `log`
* mode stores an array of errors in `$exceptionHandler.errors`, to allow later
* assertion of them. See {@link ngMock.$log#assertEmpty assertEmpty()} and
* {@link ngMock.$log#reset reset()}
*/
this.mode = function(mode) {
switch(mode) {
case 'rethrow':
handler = function(e) {
throw e;
};
break;
case 'log':
var errors = [];
handler = function(e) {
if (arguments.length == 1) {
errors.push(e);
} else {
errors.push([].slice.call(arguments, 0));
}
};
handler.errors = errors;
break;
default:
throw new Error("Unknown mode '" + mode + "', only 'log'/'rethrow' modes are allowed!");
}
};
this.$get = function() {
return handler;
};
this.mode('rethrow');
};
/**
* @ngdoc service
* @name ngMock.$log
*
* @description
* Mock implementation of {@link ng.$log} that gathers all logged messages in arrays
* (one array per logging level). These arrays are exposed as `logs` property of each of the
* level-specific log function, e.g. for level `error` the array is exposed as `$log.error.logs`.
*
*/
angular.mock.$LogProvider = function() {
var debug = true;
function concat(array1, array2, index) {
return array1.concat(Array.prototype.slice.call(array2, index));
}
this.debugEnabled = function(flag) {
if (angular.isDefined(flag)) {
debug = flag;
return this;
} else {
return debug;
}
};
this.$get = function () {
var $log = {
log: function() { $log.log.logs.push(concat([], arguments, 0)); },
warn: function() { $log.warn.logs.push(concat([], arguments, 0)); },
info: function() { $log.info.logs.push(concat([], arguments, 0)); },
error: function() { $log.error.logs.push(concat([], arguments, 0)); },
debug: function() {
if (debug) {
$log.debug.logs.push(concat([], arguments, 0));
}
}
};
/**
* @ngdoc method
* @name ngMock.$log#reset
* @methodOf ngMock.$log
*
* @description
* Reset all of the logging arrays to empty.
*/
$log.reset = function () {
/**
* @ngdoc property
* @name ngMock.$log#log.logs
* @propertyOf ngMock.$log
*
* @description
* Array of messages logged using {@link ngMock.$log#log}.
*
* @example
* <pre>
* $log.log('Some Log');
* var first = $log.log.logs.unshift();
* </pre>
*/
$log.log.logs = [];
/**
* @ngdoc property
* @name ngMock.$log#info.logs
* @propertyOf ngMock.$log
*
* @description
* Array of messages logged using {@link ngMock.$log#info}.
*
* @example
* <pre>
* $log.info('Some Info');
* var first = $log.info.logs.unshift();
* </pre>
*/
$log.info.logs = [];
/**
* @ngdoc property
* @name ngMock.$log#warn.logs
* @propertyOf ngMock.$log
*
* @description
* Array of messages logged using {@link ngMock.$log#warn}.
*
* @example
* <pre>
* $log.warn('Some Warning');
* var first = $log.warn.logs.unshift();
* </pre>
*/
$log.warn.logs = [];
/**
* @ngdoc property
* @name ngMock.$log#error.logs
* @propertyOf ngMock.$log
*
* @description
* Array of messages logged using {@link ngMock.$log#error}.
*
* @example
* <pre>
* $log.log('Some Error');
* var first = $log.error.logs.unshift();
* </pre>
*/
$log.error.logs = [];
/**
* @ngdoc property
* @name ngMock.$log#debug.logs
* @propertyOf ngMock.$log
*
* @description
* Array of messages logged using {@link ngMock.$log#debug}.
*
* @example
* <pre>
* $log.debug('Some Error');
* var first = $log.debug.logs.unshift();
* </pre>
*/
$log.debug.logs = [];
};
/**
* @ngdoc method
* @name ngMock.$log#assertEmpty
* @methodOf ngMock.$log
*
* @description
* Assert that the all of the logging methods have no logged messages. If messages present, an
* exception is thrown.
*/
$log.assertEmpty = function() {
var errors = [];
angular.forEach(['error', 'warn', 'info', 'log', 'debug'], function(logLevel) {
angular.forEach($log[logLevel].logs, function(log) {
angular.forEach(log, function (logItem) {
errors.push('MOCK $log (' + logLevel + '): ' + String(logItem) + '\n' +
(logItem.stack || ''));
});
});
});
if (errors.length) {
errors.unshift("Expected $log to be empty! Either a message was logged unexpectedly, or "+
"an expected log message was not checked and removed:");
errors.push('');
throw new Error(errors.join('\n---------\n'));
}
};
$log.reset();
return $log;
};
};
/**
* @ngdoc service
* @name ngMock.$interval
*
* @description
* Mock implementation of the $interval service.
*
* Use {@link ngMock.$interval#methods_flush `$interval.flush(millis)`} to
* move forward by `millis` milliseconds and trigger any functions scheduled to run in that
* time.
*
* @param {function()} fn A function that should be called repeatedly.
* @param {number} delay Number of milliseconds between each function call.
* @param {number=} [count=0] Number of times to repeat. If not set, or 0, will repeat
* indefinitely.
* @param {boolean=} [invokeApply=true] If set to `false` skips model dirty checking, otherwise
* will invoke `fn` within the {@link ng.$rootScope.Scope#methods_$apply $apply} block.
* @returns {promise} A promise which will be notified on each iteration.
*/
angular.mock.$IntervalProvider = function() {
this.$get = ['$rootScope', '$q',
function($rootScope, $q) {
var repeatFns = [],
nextRepeatId = 0,
now = 0;
var $interval = function(fn, delay, count, invokeApply) {
var deferred = $q.defer(),
promise = deferred.promise,
iteration = 0,
skipApply = (angular.isDefined(invokeApply) && !invokeApply);
count = (angular.isDefined(count)) ? count : 0,
promise.then(null, null, fn);
promise.$$intervalId = nextRepeatId;
function tick() {
deferred.notify(iteration++);
if (count > 0 && iteration >= count) {
var fnIndex;
deferred.resolve(iteration);
angular.forEach(repeatFns, function(fn, index) {
if (fn.id === promise.$$intervalId) fnIndex = index;
});
if (fnIndex !== undefined) {
repeatFns.splice(fnIndex, 1);
}
}
if (!skipApply) $rootScope.$apply();
}
repeatFns.push({
nextTime:(now + delay),
delay: delay,
fn: tick,
id: nextRepeatId,
deferred: deferred
});
repeatFns.sort(function(a,b){ return a.nextTime - b.nextTime;});
nextRepeatId++;
return promise;
};
$interval.cancel = function(promise) {
var fnIndex;
angular.forEach(repeatFns, function(fn, index) {
if (fn.id === promise.$$intervalId) fnIndex = index;
});
if (fnIndex !== undefined) {
repeatFns[fnIndex].deferred.reject('canceled');
repeatFns.splice(fnIndex, 1);
return true;
}
return false;
};
/**
* @ngdoc method
* @name ngMock.$interval#flush
* @methodOf ngMock.$interval
* @description
*
* Runs interval tasks scheduled to be run in the next `millis` milliseconds.
*
* @param {number=} millis maximum timeout amount to flush up until.
*
* @return {number} The amount of time moved forward.
*/
$interval.flush = function(millis) {
now += millis;
while (repeatFns.length && repeatFns[0].nextTime <= now) {
var task = repeatFns[0];
task.fn();
task.nextTime += task.delay;
repeatFns.sort(function(a,b){ return a.nextTime - b.nextTime;});
}
return millis;
};
return $interval;
}];
};
/* jshint -W101 */
/* The R_ISO8061_STR regex is never going to fit into the 100 char limit!
* This directive should go inside the anonymous function but a bug in JSHint means that it would
* not be enacted early enough to prevent the warning.
*/
var R_ISO8061_STR = /^(\d{4})-?(\d\d)-?(\d\d)(?:T(\d\d)(?:\:?(\d\d)(?:\:?(\d\d)(?:\.(\d{3}))?)?)?(Z|([+-])(\d\d):?(\d\d)))?$/;
function jsonStringToDate(string) {
var match;
if (match = string.match(R_ISO8061_STR)) {
var date = new Date(0),
tzHour = 0,
tzMin = 0;
if (match[9]) {
tzHour = int(match[9] + match[10]);
tzMin = int(match[9] + match[11]);
}
date.setUTCFullYear(int(match[1]), int(match[2]) - 1, int(match[3]));
date.setUTCHours(int(match[4]||0) - tzHour,
int(match[5]||0) - tzMin,
int(match[6]||0),
int(match[7]||0));
return date;
}
return string;
}
function int(str) {
return parseInt(str, 10);
}
function padNumber(num, digits, trim) {
var neg = '';
if (num < 0) {
neg = '-';
num = -num;
}
num = '' + num;
while(num.length < digits) num = '0' + num;
if (trim)
num = num.substr(num.length - digits);
return neg + num;
}
/**
* @ngdoc object
* @name angular.mock.TzDate
* @description
*
* *NOTE*: this is not an injectable instance, just a globally available mock class of `Date`.
*
* Mock of the Date type which has its timezone specified via constructor arg.
*
* The main purpose is to create Date-like instances with timezone fixed to the specified timezone
* offset, so that we can test code that depends on local timezone settings without dependency on
* the time zone settings of the machine where the code is running.
*
* @param {number} offset Offset of the *desired* timezone in hours (fractions will be honored)
* @param {(number|string)} timestamp Timestamp representing the desired time in *UTC*
*
* @example
* !!!! WARNING !!!!!
* This is not a complete Date object so only methods that were implemented can be called safely.
* To make matters worse, TzDate instances inherit stuff from Date via a prototype.
*
* We do our best to intercept calls to "unimplemented" methods, but since the list of methods is
* incomplete we might be missing some non-standard methods. This can result in errors like:
* "Date.prototype.foo called on incompatible Object".
*
* <pre>
* var newYearInBratislava = new TzDate(-1, '2009-12-31T23:00:00Z');
* newYearInBratislava.getTimezoneOffset() => -60;
* newYearInBratislava.getFullYear() => 2010;
* newYearInBratislava.getMonth() => 0;
* newYearInBratislava.getDate() => 1;
* newYearInBratislava.getHours() => 0;
* newYearInBratislava.getMinutes() => 0;
* newYearInBratislava.getSeconds() => 0;
* </pre>
*
*/
angular.mock.TzDate = function (offset, timestamp) {
var self = new Date(0);
if (angular.isString(timestamp)) {
var tsStr = timestamp;
self.origDate = jsonStringToDate(timestamp);
timestamp = self.origDate.getTime();
if (isNaN(timestamp))
throw {
name: "Illegal Argument",
message: "Arg '" + tsStr + "' passed into TzDate constructor is not a valid date string"
};
} else {
self.origDate = new Date(timestamp);
}
var localOffset = new Date(timestamp).getTimezoneOffset();
self.offsetDiff = localOffset*60*1000 - offset*1000*60*60;
self.date = new Date(timestamp + self.offsetDiff);
self.getTime = function() {
return self.date.getTime() - self.offsetDiff;
};
self.toLocaleDateString = function() {
return self.date.toLocaleDateString();
};
self.getFullYear = function() {
return self.date.getFullYear();
};
self.getMonth = function() {
return self.date.getMonth();
};
self.getDate = function() {
return self.date.getDate();
};
self.getHours = function() {
return self.date.getHours();
};
self.getMinutes = function() {
return self.date.getMinutes();
};
self.getSeconds = function() {
return self.date.getSeconds();
};
self.getMilliseconds = function() {
return self.date.getMilliseconds();
};
self.getTimezoneOffset = function() {
return offset * 60;
};
self.getUTCFullYear = function() {
return self.origDate.getUTCFullYear();
};
self.getUTCMonth = function() {
return self.origDate.getUTCMonth();
};
self.getUTCDate = function() {
return self.origDate.getUTCDate();
};
self.getUTCHours = function() {
return self.origDate.getUTCHours();
};
self.getUTCMinutes = function() {
return self.origDate.getUTCMinutes();
};
self.getUTCSeconds = function() {
return self.origDate.getUTCSeconds();
};
self.getUTCMilliseconds = function() {
return self.origDate.getUTCMilliseconds();
};
self.getDay = function() {
return self.date.getDay();
};
// provide this method only on browsers that already have it
if (self.toISOString) {
self.toISOString = function() {
return padNumber(self.origDate.getUTCFullYear(), 4) + '-' +
padNumber(self.origDate.getUTCMonth() + 1, 2) + '-' +
padNumber(self.origDate.getUTCDate(), 2) + 'T' +
padNumber(self.origDate.getUTCHours(), 2) + ':' +
padNumber(self.origDate.getUTCMinutes(), 2) + ':' +
padNumber(self.origDate.getUTCSeconds(), 2) + '.' +
padNumber(self.origDate.getUTCMilliseconds(), 3) + 'Z';
};
}
//hide all methods not implemented in this mock that the Date prototype exposes
var unimplementedMethods = ['getUTCDay',
'getYear', 'setDate', 'setFullYear', 'setHours', 'setMilliseconds',
'setMinutes', 'setMonth', 'setSeconds', 'setTime', 'setUTCDate', 'setUTCFullYear',
'setUTCHours', 'setUTCMilliseconds', 'setUTCMinutes', 'setUTCMonth', 'setUTCSeconds',
'setYear', 'toDateString', 'toGMTString', 'toJSON', 'toLocaleFormat', 'toLocaleString',
'toLocaleTimeString', 'toSource', 'toString', 'toTimeString', 'toUTCString', 'valueOf'];
angular.forEach(unimplementedMethods, function(methodName) {
self[methodName] = function() {
throw new Error("Method '" + methodName + "' is not implemented in the TzDate mock");
};
});
return self;
};
//make "tzDateInstance instanceof Date" return true
angular.mock.TzDate.prototype = Date.prototype;
/* jshint +W101 */
angular.mock.animate = angular.module('mock.animate', ['ng'])
.config(['$provide', function($provide) {
$provide.decorator('$animate', function($delegate) {
var animate = {
queue : [],
enabled : $delegate.enabled,
flushNext : function(name) {
var tick = animate.queue.shift();
if (!tick) throw new Error('No animation to be flushed');
if(tick.method !== name) {
throw new Error('The next animation is not "' + name +
'", but is "' + tick.method + '"');
}
tick.fn();
return tick;
}
};
angular.forEach(['enter','leave','move','addClass','removeClass'], function(method) {
animate[method] = function() {
var params = arguments;
animate.queue.push({
method : method,
params : params,
element : angular.isElement(params[0]) && params[0],
parent : angular.isElement(params[1]) && params[1],
after : angular.isElement(params[2]) && params[2],
fn : function() {
$delegate[method].apply($delegate, params);
}
});
};
});
return animate;
});
}]);
/**
* @ngdoc function
* @name angular.mock.dump
* @description
*
* *NOTE*: this is not an injectable instance, just a globally available function.
*
* Method for serializing common angular objects (scope, elements, etc..) into strings, useful for
* debugging.
*
* This method is also available on window, where it can be used to display objects on debug
* console.
*
* @param {*} object - any object to turn into string.
* @return {string} a serialized string of the argument
*/
angular.mock.dump = function(object) {
return serialize(object);
function serialize(object) {
var out;
if (angular.isElement(object)) {
object = angular.element(object);
out = angular.element('<div></div>');
angular.forEach(object, function(element) {
out.append(angular.element(element).clone());
});
out = out.html();
} else if (angular.isArray(object)) {
out = [];
angular.forEach(object, function(o) {
out.push(serialize(o));
});
out = '[ ' + out.join(', ') + ' ]';
} else if (angular.isObject(object)) {
if (angular.isFunction(object.$eval) && angular.isFunction(object.$apply)) {
out = serializeScope(object);
} else if (object instanceof Error) {
out = object.stack || ('' + object.name + ': ' + object.message);
} else {
// TODO(i): this prevents methods being logged,
// we should have a better way to serialize objects
out = angular.toJson(object, true);
}
} else {
out = String(object);
}
return out;
}
function serializeScope(scope, offset) {
offset = offset || ' ';
var log = [offset + 'Scope(' + scope.$id + '): {'];
for ( var key in scope ) {
if (Object.prototype.hasOwnProperty.call(scope, key) && !key.match(/^(\$|this)/)) {
log.push(' ' + key + ': ' + angular.toJson(scope[key]));
}
}
var child = scope.$$childHead;
while(child) {
log.push(serializeScope(child, offset + ' '));
child = child.$$nextSibling;
}
log.push('}');
return log.join('\n' + offset);
}
};
/**
* @ngdoc object
* @name ngMock.$httpBackend
* @description
* Fake HTTP backend implementation suitable for unit testing applications that use the
* {@link ng.$http $http service}.
*
* *Note*: For fake HTTP backend implementation suitable for end-to-end testing or backend-less
* development please see {@link ngMockE2E.$httpBackend e2e $httpBackend mock}.
*
* During unit testing, we want our unit tests to run quickly and have no external dependencies so
* we don’t want to send {@link https://developer.mozilla.org/en/xmlhttprequest XHR} or
* {@link http://en.wikipedia.org/wiki/JSONP JSONP} requests to a real server. All we really need is
* to verify whether a certain request has been sent or not, or alternatively just let the
* application make requests, respond with pre-trained responses and assert that the end result is
* what we expect it to be.
*
* This mock implementation can be used to respond with static or dynamic responses via the
* `expect` and `when` apis and their shortcuts (`expectGET`, `whenPOST`, etc).
*
* When an Angular application needs some data from a server, it calls the $http service, which
* sends the request to a real server using $httpBackend service. With dependency injection, it is
* easy to inject $httpBackend mock (which has the same API as $httpBackend) and use it to verify
* the requests and respond with some testing data without sending a request to real server.
*
* There are two ways to specify what test data should be returned as http responses by the mock
* backend when the code under test makes http requests:
*
* - `$httpBackend.expect` - specifies a request expectation
* - `$httpBackend.when` - specifies a backend definition
*
*
* # Request Expectations vs Backend Definitions
*
* Request expectations provide a way to make assertions about requests made by the application and
* to define responses for those requests. The test will fail if the expected requests are not made
* or they are made in the wrong order.
*
* Backend definitions allow you to define a fake backend for your application which doesn't assert
* if a particular request was made or not, it just returns a trained response if a request is made.
* The test will pass whether or not the request gets made during testing.
*
*
* <table class="table">
* <tr><th width="220px"></th><th>Request expectations</th><th>Backend definitions</th></tr>
* <tr>
* <th>Syntax</th>
* <td>.expect(...).respond(...)</td>
* <td>.when(...).respond(...)</td>
* </tr>
* <tr>
* <th>Typical usage</th>
* <td>strict unit tests</td>
* <td>loose (black-box) unit testing</td>
* </tr>
* <tr>
* <th>Fulfills multiple requests</th>
* <td>NO</td>
* <td>YES</td>
* </tr>
* <tr>
* <th>Order of requests matters</th>
* <td>YES</td>
* <td>NO</td>
* </tr>
* <tr>
* <th>Request required</th>
* <td>YES</td>
* <td>NO</td>
* </tr>
* <tr>
* <th>Response required</th>
* <td>optional (see below)</td>
* <td>YES</td>
* </tr>
* </table>
*
* In cases where both backend definitions and request expectations are specified during unit
* testing, the request expectations are evaluated first.
*
* If a request expectation has no response specified, the algorithm will search your backend
* definitions for an appropriate response.
*
* If a request didn't match any expectation or if the expectation doesn't have the response
* defined, the backend definitions are evaluated in sequential order to see if any of them match
* the request. The response from the first matched definition is returned.
*
*
* # Flushing HTTP requests
*
* The $httpBackend used in production, always responds to requests with responses asynchronously.
* If we preserved this behavior in unit testing, we'd have to create async unit tests, which are
* hard to write, follow and maintain. At the same time the testing mock, can't respond
* synchronously because that would change the execution of the code under test. For this reason the
* mock $httpBackend has a `flush()` method, which allows the test to explicitly flush pending
* requests and thus preserving the async api of the backend, while allowing the test to execute
* synchronously.
*
*
* # Unit testing with mock $httpBackend
* The following code shows how to setup and use the mock backend in unit testing a controller.
* First we create the controller under test
*
<pre>
// The controller code
function MyController($scope, $http) {
var authToken;
$http.get('/auth.py').success(function(data, status, headers) {
authToken = headers('A-Token');
$scope.user = data;
});
$scope.saveMessage = function(message) {
var headers = { 'Authorization': authToken };
$scope.status = 'Saving...';
$http.post('/add-msg.py', message, { headers: headers } ).success(function(response) {
$scope.status = '';
}).error(function() {
$scope.status = 'ERROR!';
});
};
}
</pre>
*
* Now we setup the mock backend and create the test specs.
*
<pre>
// testing controller
describe('MyController', function() {
var $httpBackend, $rootScope, createController;
beforeEach(inject(function($injector) {
// Set up the mock http service responses
$httpBackend = $injector.get('$httpBackend');
// backend definition common for all tests
$httpBackend.when('GET', '/auth.py').respond({userId: 'userX'}, {'A-Token': 'xxx'});
// Get hold of a scope (i.e. the root scope)
$rootScope = $injector.get('$rootScope');
// The $controller service is used to create instances of controllers
var $controller = $injector.get('$controller');
createController = function() {
return $controller('MyController', {'$scope' : $rootScope });
};
}));
afterEach(function() {
$httpBackend.verifyNoOutstandingExpectation();
$httpBackend.verifyNoOutstandingRequest();
});
it('should fetch authentication token', function() {
$httpBackend.expectGET('/auth.py');
var controller = createController();
$httpBackend.flush();
});
it('should send msg to server', function() {
var controller = createController();
$httpBackend.flush();
// now you don’t care about the authentication, but
// the controller will still send the request and
// $httpBackend will respond without you having to
// specify the expectation and response for this request
$httpBackend.expectPOST('/add-msg.py', 'message content').respond(201, '');
$rootScope.saveMessage('message content');
expect($rootScope.status).toBe('Saving...');
$httpBackend.flush();
expect($rootScope.status).toBe('');
});
it('should send auth header', function() {
var controller = createController();
$httpBackend.flush();
$httpBackend.expectPOST('/add-msg.py', undefined, function(headers) {
// check if the header was send, if it wasn't the expectation won't
// match the request and the test will fail
return headers['Authorization'] == 'xxx';
}).respond(201, '');
$rootScope.saveMessage('whatever');
$httpBackend.flush();
});
});
</pre>
*/
angular.mock.$HttpBackendProvider = function() {
this.$get = ['$rootScope', createHttpBackendMock];
};
/**
* General factory function for $httpBackend mock.
* Returns instance for unit testing (when no arguments specified):
* - passing through is disabled
* - auto flushing is disabled
*
* Returns instance for e2e testing (when `$delegate` and `$browser` specified):
* - passing through (delegating request to real backend) is enabled
* - auto flushing is enabled
*
* @param {Object=} $delegate Real $httpBackend instance (allow passing through if specified)
* @param {Object=} $browser Auto-flushing enabled if specified
* @return {Object} Instance of $httpBackend mock
*/
function createHttpBackendMock($rootScope, $delegate, $browser) {
var definitions = [],
expectations = [],
responses = [],
responsesPush = angular.bind(responses, responses.push),
copy = angular.copy;
function createResponse(status, data, headers) {
if (angular.isFunction(status)) return status;
return function() {
return angular.isNumber(status)
? [status, data, headers]
: [200, status, data];
};
}
// TODO(vojta): change params to: method, url, data, headers, callback
function $httpBackend(method, url, data, callback, headers, timeout, withCredentials) {
var xhr = new MockXhr(),
expectation = expectations[0],
wasExpected = false;
function prettyPrint(data) {
return (angular.isString(data) || angular.isFunction(data) || data instanceof RegExp)
? data
: angular.toJson(data);
}
function wrapResponse(wrapped) {
if (!$browser && timeout && timeout.then) timeout.then(handleTimeout);
return handleResponse;
function handleResponse() {
var response = wrapped.response(method, url, data, headers);
xhr.$$respHeaders = response[2];
callback(copy(response[0]), copy(response[1]), xhr.getAllResponseHeaders());
}
function handleTimeout() {
for (var i = 0, ii = responses.length; i < ii; i++) {
if (responses[i] === handleResponse) {
responses.splice(i, 1);
callback(-1, undefined, '');
break;
}
}
}
}
if (expectation && expectation.match(method, url)) {
if (!expectation.matchData(data))
throw new Error('Expected ' + expectation + ' with different data\n' +
'EXPECTED: ' + prettyPrint(expectation.data) + '\nGOT: ' + data);
if (!expectation.matchHeaders(headers))
throw new Error('Expected ' + expectation + ' with different headers\n' +
'EXPECTED: ' + prettyPrint(expectation.headers) + '\nGOT: ' +
prettyPrint(headers));
expectations.shift();
if (expectation.response) {
responses.push(wrapResponse(expectation));
return;
}
wasExpected = true;
}
var i = -1, definition;
while ((definition = definitions[++i])) {
if (definition.match(method, url, data, headers || {})) {
if (definition.response) {
// if $browser specified, we do auto flush all requests
($browser ? $browser.defer : responsesPush)(wrapResponse(definition));
} else if (definition.passThrough) {
$delegate(method, url, data, callback, headers, timeout, withCredentials);
} else throw new Error('No response defined !');
return;
}
}
throw wasExpected ?
new Error('No response defined !') :
new Error('Unexpected request: ' + method + ' ' + url + '\n' +
(expectation ? 'Expected ' + expectation : 'No more request expected'));
}
/**
* @ngdoc method
* @name ngMock.$httpBackend#when
* @methodOf ngMock.$httpBackend
* @description
* Creates a new backend definition.
*
* @param {string} method HTTP method.
* @param {string|RegExp} url HTTP url.
* @param {(string|RegExp|function(string))=} data HTTP request body or function that receives
* data string and returns true if the data is as expected.
* @param {(Object|function(Object))=} headers HTTP headers or function that receives http header
* object and returns true if the headers match the current definition.
* @returns {requestHandler} Returns an object with `respond` method that controls how a matched
* request is handled.
*
* - respond –
* `{function([status,] data[, headers])|function(function(method, url, data, headers)}`
* – The respond method takes a set of static data to be returned or a function that can return
* an array containing response status (number), response data (string) and response headers
* (Object).
*/
$httpBackend.when = function(method, url, data, headers) {
var definition = new MockHttpExpectation(method, url, data, headers),
chain = {
respond: function(status, data, headers) {
definition.response = createResponse(status, data, headers);
}
};
if ($browser) {
chain.passThrough = function() {
definition.passThrough = true;
};
}
definitions.push(definition);
return chain;
};
/**
* @ngdoc method
* @name ngMock.$httpBackend#whenGET
* @methodOf ngMock.$httpBackend
* @description
* Creates a new backend definition for GET requests. For more info see `when()`.
*
* @param {string|RegExp} url HTTP url.
* @param {(Object|function(Object))=} headers HTTP headers.
* @returns {requestHandler} Returns an object with `respond` method that control how a matched
* request is handled.
*/
/**
* @ngdoc method
* @name ngMock.$httpBackend#whenHEAD
* @methodOf ngMock.$httpBackend
* @description
* Creates a new backend definition for HEAD requests. For more info see `when()`.
*
* @param {string|RegExp} url HTTP url.
* @param {(Object|function(Object))=} headers HTTP headers.
* @returns {requestHandler} Returns an object with `respond` method that control how a matched
* request is handled.
*/
/**
* @ngdoc method
* @name ngMock.$httpBackend#whenDELETE
* @methodOf ngMock.$httpBackend
* @description
* Creates a new backend definition for DELETE requests. For more info see `when()`.
*
* @param {string|RegExp} url HTTP url.
* @param {(Object|function(Object))=} headers HTTP headers.
* @returns {requestHandler} Returns an object with `respond` method that control how a matched
* request is handled.
*/
/**
* @ngdoc method
* @name ngMock.$httpBackend#whenPOST
* @methodOf ngMock.$httpBackend
* @description
* Creates a new backend definition for POST requests. For more info see `when()`.
*
* @param {string|RegExp} url HTTP url.
* @param {(string|RegExp|function(string))=} data HTTP request body or function that receives
* data string and returns true if the data is as expected.
* @param {(Object|function(Object))=} headers HTTP headers.
* @returns {requestHandler} Returns an object with `respond` method that control how a matched
* request is handled.
*/
/**
* @ngdoc method
* @name ngMock.$httpBackend#whenPUT
* @methodOf ngMock.$httpBackend
* @description
* Creates a new backend definition for PUT requests. For more info see `when()`.
*
* @param {string|RegExp} url HTTP url.
* @param {(string|RegExp|function(string))=} data HTTP request body or function that receives
* data string and returns true if the data is as expected.
* @param {(Object|function(Object))=} headers HTTP headers.
* @returns {requestHandler} Returns an object with `respond` method that control how a matched
* request is handled.
*/
/**
* @ngdoc method
* @name ngMock.$httpBackend#whenJSONP
* @methodOf ngMock.$httpBackend
* @description
* Creates a new backend definition for JSONP requests. For more info see `when()`.
*
* @param {string|RegExp} url HTTP url.
* @returns {requestHandler} Returns an object with `respond` method that control how a matched
* request is handled.
*/
createShortMethods('when');
/**
* @ngdoc method
* @name ngMock.$httpBackend#expect
* @methodOf ngMock.$httpBackend
* @description
* Creates a new request expectation.
*
* @param {string} method HTTP method.
* @param {string|RegExp} url HTTP url.
* @param {(string|RegExp|function(string)|Object)=} data HTTP request body or function that
* receives data string and returns true if the data is as expected, or Object if request body
* is in JSON format.
* @param {(Object|function(Object))=} headers HTTP headers or function that receives http header
* object and returns true if the headers match the current expectation.
* @returns {requestHandler} Returns an object with `respond` method that control how a matched
* request is handled.
*
* - respond –
* `{function([status,] data[, headers])|function(function(method, url, data, headers)}`
* – The respond method takes a set of static data to be returned or a function that can return
* an array containing response status (number), response data (string) and response headers
* (Object).
*/
$httpBackend.expect = function(method, url, data, headers) {
var expectation = new MockHttpExpectation(method, url, data, headers);
expectations.push(expectation);
return {
respond: function(status, data, headers) {
expectation.response = createResponse(status, data, headers);
}
};
};
/**
* @ngdoc method
* @name ngMock.$httpBackend#expectGET
* @methodOf ngMock.$httpBackend
* @description
* Creates a new request expectation for GET requests. For more info see `expect()`.
*
* @param {string|RegExp} url HTTP url.
* @param {Object=} headers HTTP headers.
* @returns {requestHandler} Returns an object with `respond` method that control how a matched
* request is handled. See #expect for more info.
*/
/**
* @ngdoc method
* @name ngMock.$httpBackend#expectHEAD
* @methodOf ngMock.$httpBackend
* @description
* Creates a new request expectation for HEAD requests. For more info see `expect()`.
*
* @param {string|RegExp} url HTTP url.
* @param {Object=} headers HTTP headers.
* @returns {requestHandler} Returns an object with `respond` method that control how a matched
* request is handled.
*/
/**
* @ngdoc method
* @name ngMock.$httpBackend#expectDELETE
* @methodOf ngMock.$httpBackend
* @description
* Creates a new request expectation for DELETE requests. For more info see `expect()`.
*
* @param {string|RegExp} url HTTP url.
* @param {Object=} headers HTTP headers.
* @returns {requestHandler} Returns an object with `respond` method that control how a matched
* request is handled.
*/
/**
* @ngdoc method
* @name ngMock.$httpBackend#expectPOST
* @methodOf ngMock.$httpBackend
* @description
* Creates a new request expectation for POST requests. For more info see `expect()`.
*
* @param {string|RegExp} url HTTP url.
* @param {(string|RegExp|function(string)|Object)=} data HTTP request body or function that
* receives data string and returns true if the data is as expected, or Object if request body
* is in JSON format.
* @param {Object=} headers HTTP headers.
* @returns {requestHandler} Returns an object with `respond` method that control how a matched
* request is handled.
*/
/**
* @ngdoc method
* @name ngMock.$httpBackend#expectPUT
* @methodOf ngMock.$httpBackend
* @description
* Creates a new request expectation for PUT requests. For more info see `expect()`.
*
* @param {string|RegExp} url HTTP url.
* @param {(string|RegExp|function(string)|Object)=} data HTTP request body or function that
* receives data string and returns true if the data is as expected, or Object if request body
* is in JSON format.
* @param {Object=} headers HTTP headers.
* @returns {requestHandler} Returns an object with `respond` method that control how a matched
* request is handled.
*/
/**
* @ngdoc method
* @name ngMock.$httpBackend#expectPATCH
* @methodOf ngMock.$httpBackend
* @description
* Creates a new request expectation for PATCH requests. For more info see `expect()`.
*
* @param {string|RegExp} url HTTP url.
* @param {(string|RegExp|function(string)|Object)=} data HTTP request body or function that
* receives data string and returns true if the data is as expected, or Object if request body
* is in JSON format.
* @param {Object=} headers HTTP headers.
* @returns {requestHandler} Returns an object with `respond` method that control how a matched
* request is handled.
*/
/**
* @ngdoc method
* @name ngMock.$httpBackend#expectJSONP
* @methodOf ngMock.$httpBackend
* @description
* Creates a new request expectation for JSONP requests. For more info see `expect()`.
*
* @param {string|RegExp} url HTTP url.
* @returns {requestHandler} Returns an object with `respond` method that control how a matched
* request is handled.
*/
createShortMethods('expect');
/**
* @ngdoc method
* @name ngMock.$httpBackend#flush
* @methodOf ngMock.$httpBackend
* @description
* Flushes all pending requests using the trained responses.
*
* @param {number=} count Number of responses to flush (in the order they arrived). If undefined,
* all pending requests will be flushed. If there are no pending requests when the flush method
* is called an exception is thrown (as this typically a sign of programming error).
*/
$httpBackend.flush = function(count) {
$rootScope.$digest();
if (!responses.length) throw new Error('No pending request to flush !');
if (angular.isDefined(count)) {
while (count--) {
if (!responses.length) throw new Error('No more pending request to flush !');
responses.shift()();
}
} else {
while (responses.length) {
responses.shift()();
}
}
$httpBackend.verifyNoOutstandingExpectation();
};
/**
* @ngdoc method
* @name ngMock.$httpBackend#verifyNoOutstandingExpectation
* @methodOf ngMock.$httpBackend
* @description
* Verifies that all of the requests defined via the `expect` api were made. If any of the
* requests were not made, verifyNoOutstandingExpectation throws an exception.
*
* Typically, you would call this method following each test case that asserts requests using an
* "afterEach" clause.
*
* <pre>
* afterEach($httpBackend.verifyNoOutstandingExpectation);
* </pre>
*/
$httpBackend.verifyNoOutstandingExpectation = function() {
$rootScope.$digest();
if (expectations.length) {
throw new Error('Unsatisfied requests: ' + expectations.join(', '));
}
};
/**
* @ngdoc method
* @name ngMock.$httpBackend#verifyNoOutstandingRequest
* @methodOf ngMock.$httpBackend
* @description
* Verifies that there are no outstanding requests that need to be flushed.
*
* Typically, you would call this method following each test case that asserts requests using an
* "afterEach" clause.
*
* <pre>
* afterEach($httpBackend.verifyNoOutstandingRequest);
* </pre>
*/
$httpBackend.verifyNoOutstandingRequest = function() {
if (responses.length) {
throw new Error('Unflushed requests: ' + responses.length);
}
};
/**
* @ngdoc method
* @name ngMock.$httpBackend#resetExpectations
* @methodOf ngMock.$httpBackend
* @description
* Resets all request expectations, but preserves all backend definitions. Typically, you would
* call resetExpectations during a multiple-phase test when you want to reuse the same instance of
* $httpBackend mock.
*/
$httpBackend.resetExpectations = function() {
expectations.length = 0;
responses.length = 0;
};
return $httpBackend;
function createShortMethods(prefix) {
angular.forEach(['GET', 'DELETE', 'JSONP'], function(method) {
$httpBackend[prefix + method] = function(url, headers) {
return $httpBackend[prefix](method, url, undefined, headers);
};
});
angular.forEach(['PUT', 'POST', 'PATCH'], function(method) {
$httpBackend[prefix + method] = function(url, data, headers) {
return $httpBackend[prefix](method, url, data, headers);
};
});
}
}
function MockHttpExpectation(method, url, data, headers) {
this.data = data;
this.headers = headers;
this.match = function(m, u, d, h) {
if (method != m) return false;
if (!this.matchUrl(u)) return false;
if (angular.isDefined(d) && !this.matchData(d)) return false;
if (angular.isDefined(h) && !this.matchHeaders(h)) return false;
return true;
};
this.matchUrl = function(u) {
if (!url) return true;
if (angular.isFunction(url.test)) return url.test(u);
return url == u;
};
this.matchHeaders = function(h) {
if (angular.isUndefined(headers)) return true;
if (angular.isFunction(headers)) return headers(h);
return angular.equals(headers, h);
};
this.matchData = function(d) {
if (angular.isUndefined(data)) return true;
if (data && angular.isFunction(data.test)) return data.test(d);
if (data && angular.isFunction(data)) return data(d);
if (data && !angular.isString(data)) return angular.equals(data, angular.fromJson(d));
return data == d;
};
this.toString = function() {
return method + ' ' + url;
};
}
function createMockXhr() {
return new MockXhr();
}
function MockXhr() {
// hack for testing $http, $httpBackend
MockXhr.$$lastInstance = this;
this.open = function(method, url, async) {
this.$$method = method;
this.$$url = url;
this.$$async = async;
this.$$reqHeaders = {};
this.$$respHeaders = {};
};
this.send = function(data) {
this.$$data = data;
};
this.setRequestHeader = function(key, value) {
this.$$reqHeaders[key] = value;
};
this.getResponseHeader = function(name) {
// the lookup must be case insensitive,
// that's why we try two quick lookups first and full scan last
var header = this.$$respHeaders[name];
if (header) return header;
name = angular.lowercase(name);
header = this.$$respHeaders[name];
if (header) return header;
header = undefined;
angular.forEach(this.$$respHeaders, function(headerVal, headerName) {
if (!header && angular.lowercase(headerName) == name) header = headerVal;
});
return header;
};
this.getAllResponseHeaders = function() {
var lines = [];
angular.forEach(this.$$respHeaders, function(value, key) {
lines.push(key + ': ' + value);
});
return lines.join('\n');
};
this.abort = angular.noop;
}
/**
* @ngdoc function
* @name ngMock.$timeout
* @description
*
* This service is just a simple decorator for {@link ng.$timeout $timeout} service
* that adds a "flush" and "verifyNoPendingTasks" methods.
*/
angular.mock.$TimeoutDecorator = function($delegate, $browser) {
/**
* @ngdoc method
* @name ngMock.$timeout#flush
* @methodOf ngMock.$timeout
* @description
*
* Flushes the queue of pending tasks.
*
* @param {number=} delay maximum timeout amount to flush up until
*/
$delegate.flush = function(delay) {
$browser.defer.flush(delay);
};
/**
* @ngdoc method
* @name ngMock.$timeout#verifyNoPendingTasks
* @methodOf ngMock.$timeout
* @description
*
* Verifies that there are no pending tasks that need to be flushed.
*/
$delegate.verifyNoPendingTasks = function() {
if ($browser.deferredFns.length) {
throw new Error('Deferred tasks to flush (' + $browser.deferredFns.length + '): ' +
formatPendingTasksAsString($browser.deferredFns));
}
};
function formatPendingTasksAsString(tasks) {
var result = [];
angular.forEach(tasks, function(task) {
result.push('{id: ' + task.id + ', ' + 'time: ' + task.time + '}');
});
return result.join(', ');
}
return $delegate;
};
/**
*
*/
angular.mock.$RootElementProvider = function() {
this.$get = function() {
return angular.element('<div ng-app></div>');
};
};
/**
* @ngdoc overview
* @name ngMock
* @description
*
* # ngMock
*
* The `ngMock` module providers support to inject and mock Angular services into unit tests.
* In addition, ngMock also extends various core ng services such that they can be
* inspected and controlled in a synchronous manner within test code.
*
* {@installModule mocks}
*
* <div doc-module-components="ngMock"></div>
*
*/
angular.module('ngMock', ['ng']).provider({
$browser: angular.mock.$BrowserProvider,
$exceptionHandler: angular.mock.$ExceptionHandlerProvider,
$log: angular.mock.$LogProvider,
$interval: angular.mock.$IntervalProvider,
$httpBackend: angular.mock.$HttpBackendProvider,
$rootElement: angular.mock.$RootElementProvider
}).config(['$provide', function($provide) {
$provide.decorator('$timeout', angular.mock.$TimeoutDecorator);
}]);
/**
* @ngdoc overview
* @name ngMockE2E
* @description
*
* The `ngMockE2E` is an angular module which contains mocks suitable for end-to-end testing.
* Currently there is only one mock present in this module -
* the {@link ngMockE2E.$httpBackend e2e $httpBackend} mock.
*/
angular.module('ngMockE2E', ['ng']).config(['$provide', function($provide) {
$provide.decorator('$httpBackend', angular.mock.e2e.$httpBackendDecorator);
}]);
/**
* @ngdoc object
* @name ngMockE2E.$httpBackend
* @description
* Fake HTTP backend implementation suitable for end-to-end testing or backend-less development of
* applications that use the {@link ng.$http $http service}.
*
* *Note*: For fake http backend implementation suitable for unit testing please see
* {@link ngMock.$httpBackend unit-testing $httpBackend mock}.
*
* This implementation can be used to respond with static or dynamic responses via the `when` api
* and its shortcuts (`whenGET`, `whenPOST`, etc) and optionally pass through requests to the
* real $httpBackend for specific requests (e.g. to interact with certain remote apis or to fetch
* templates from a webserver).
*
* As opposed to unit-testing, in an end-to-end testing scenario or in scenario when an application
* is being developed with the real backend api replaced with a mock, it is often desirable for
* certain category of requests to bypass the mock and issue a real http request (e.g. to fetch
* templates or static files from the webserver). To configure the backend with this behavior
* use the `passThrough` request handler of `when` instead of `respond`.
*
* Additionally, we don't want to manually have to flush mocked out requests like we do during unit
* testing. For this reason the e2e $httpBackend automatically flushes mocked out requests
* automatically, closely simulating the behavior of the XMLHttpRequest object.
*
* To setup the application to run with this http backend, you have to create a module that depends
* on the `ngMockE2E` and your application modules and defines the fake backend:
*
* <pre>
* myAppDev = angular.module('myAppDev', ['myApp', 'ngMockE2E']);
* myAppDev.run(function($httpBackend) {
* phones = [{name: 'phone1'}, {name: 'phone2'}];
*
* // returns the current list of phones
* $httpBackend.whenGET('/phones').respond(phones);
*
* // adds a new phone to the phones array
* $httpBackend.whenPOST('/phones').respond(function(method, url, data) {
* phones.push(angular.fromJson(data));
* });
* $httpBackend.whenGET(/^\/templates\//).passThrough();
* //...
* });
* </pre>
*
* Afterwards, bootstrap your app with this new module.
*/
/**
* @ngdoc method
* @name ngMockE2E.$httpBackend#when
* @methodOf ngMockE2E.$httpBackend
* @description
* Creates a new backend definition.
*
* @param {string} method HTTP method.
* @param {string|RegExp} url HTTP url.
* @param {(string|RegExp)=} data HTTP request body.
* @param {(Object|function(Object))=} headers HTTP headers or function that receives http header
* object and returns true if the headers match the current definition.
* @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that
* control how a matched request is handled.
*
* - respond –
* `{function([status,] data[, headers])|function(function(method, url, data, headers)}`
* – The respond method takes a set of static data to be returned or a function that can return
* an array containing response status (number), response data (string) and response headers
* (Object).
* - passThrough – `{function()}` – Any request matching a backend definition with `passThrough`
* handler, will be pass through to the real backend (an XHR request will be made to the
* server.
*/
/**
* @ngdoc method
* @name ngMockE2E.$httpBackend#whenGET
* @methodOf ngMockE2E.$httpBackend
* @description
* Creates a new backend definition for GET requests. For more info see `when()`.
*
* @param {string|RegExp} url HTTP url.
* @param {(Object|function(Object))=} headers HTTP headers.
* @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that
* control how a matched request is handled.
*/
/**
* @ngdoc method
* @name ngMockE2E.$httpBackend#whenHEAD
* @methodOf ngMockE2E.$httpBackend
* @description
* Creates a new backend definition for HEAD requests. For more info see `when()`.
*
* @param {string|RegExp} url HTTP url.
* @param {(Object|function(Object))=} headers HTTP headers.
* @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that
* control how a matched request is handled.
*/
/**
* @ngdoc method
* @name ngMockE2E.$httpBackend#whenDELETE
* @methodOf ngMockE2E.$httpBackend
* @description
* Creates a new backend definition for DELETE requests. For more info see `when()`.
*
* @param {string|RegExp} url HTTP url.
* @param {(Object|function(Object))=} headers HTTP headers.
* @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that
* control how a matched request is handled.
*/
/**
* @ngdoc method
* @name ngMockE2E.$httpBackend#whenPOST
* @methodOf ngMockE2E.$httpBackend
* @description
* Creates a new backend definition for POST requests. For more info see `when()`.
*
* @param {string|RegExp} url HTTP url.
* @param {(string|RegExp)=} data HTTP request body.
* @param {(Object|function(Object))=} headers HTTP headers.
* @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that
* control how a matched request is handled.
*/
/**
* @ngdoc method
* @name ngMockE2E.$httpBackend#whenPUT
* @methodOf ngMockE2E.$httpBackend
* @description
* Creates a new backend definition for PUT requests. For more info see `when()`.
*
* @param {string|RegExp} url HTTP url.
* @param {(string|RegExp)=} data HTTP request body.
* @param {(Object|function(Object))=} headers HTTP headers.
* @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that
* control how a matched request is handled.
*/
/**
* @ngdoc method
* @name ngMockE2E.$httpBackend#whenPATCH
* @methodOf ngMockE2E.$httpBackend
* @description
* Creates a new backend definition for PATCH requests. For more info see `when()`.
*
* @param {string|RegExp} url HTTP url.
* @param {(string|RegExp)=} data HTTP request body.
* @param {(Object|function(Object))=} headers HTTP headers.
* @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that
* control how a matched request is handled.
*/
/**
* @ngdoc method
* @name ngMockE2E.$httpBackend#whenJSONP
* @methodOf ngMockE2E.$httpBackend
* @description
* Creates a new backend definition for JSONP requests. For more info see `when()`.
*
* @param {string|RegExp} url HTTP url.
* @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that
* control how a matched request is handled.
*/
angular.mock.e2e = {};
angular.mock.e2e.$httpBackendDecorator =
['$rootScope', '$delegate', '$browser', createHttpBackendMock];
angular.mock.clearDataCache = function() {
var key,
cache = angular.element.cache;
for(key in cache) {
if (Object.prototype.hasOwnProperty.call(cache,key)) {
var handle = cache[key].handle;
handle && angular.element(handle.elem).off();
delete cache[key];
}
}
};
if(window.jasmine || window.mocha) {
var currentSpec = null,
isSpecRunning = function() {
return currentSpec && (window.mocha || currentSpec.queue.running);
};
beforeEach(function() {
currentSpec = this;
});
afterEach(function() {
var injector = currentSpec.$injector;
currentSpec.$injector = null;
currentSpec.$modules = null;
currentSpec = null;
if (injector) {
injector.get('$rootElement').off();
injector.get('$browser').pollFns.length = 0;
}
angular.mock.clearDataCache();
// clean up jquery's fragment cache
angular.forEach(angular.element.fragments, function(val, key) {
delete angular.element.fragments[key];
});
MockXhr.$$lastInstance = null;
angular.forEach(angular.callbacks, function(val, key) {
delete angular.callbacks[key];
});
angular.callbacks.counter = 0;
});
/**
* @ngdoc function
* @name angular.mock.module
* @description
*
* *NOTE*: This function is also published on window for easy access.<br>
*
* This function registers a module configuration code. It collects the configuration information
* which will be used when the injector is created by {@link angular.mock.inject inject}.
*
* See {@link angular.mock.inject inject} for usage example
*
* @param {...(string|Function|Object)} fns any number of modules which are represented as string
* aliases or as anonymous module initialization functions. The modules are used to
* configure the injector. The 'ng' and 'ngMock' modules are automatically loaded. If an
* object literal is passed they will be register as values in the module, the key being
* the module name and the value being what is returned.
*/
window.module = angular.mock.module = function() {
var moduleFns = Array.prototype.slice.call(arguments, 0);
return isSpecRunning() ? workFn() : workFn;
/////////////////////
function workFn() {
if (currentSpec.$injector) {
throw new Error('Injector already created, can not register a module!');
} else {
var modules = currentSpec.$modules || (currentSpec.$modules = []);
angular.forEach(moduleFns, function(module) {
if (angular.isObject(module) && !angular.isArray(module)) {
modules.push(function($provide) {
angular.forEach(module, function(value, key) {
$provide.value(key, value);
});
});
} else {
modules.push(module);
}
});
}
}
};
/**
* @ngdoc function
* @name angular.mock.inject
* @description
*
* *NOTE*: This function is also published on window for easy access.<br>
*
* The inject function wraps a function into an injectable function. The inject() creates new
* instance of {@link AUTO.$injector $injector} per test, which is then used for
* resolving references.
*
*
* ## Resolving References (Underscore Wrapping)
* Often, we would like to inject a reference once, in a `beforeEach()` block and reuse this
* in multiple `it()` clauses. To be able to do this we must assign the reference to a variable
* that is declared in the scope of the `describe()` block. Since we would, most likely, want
* the variable to have the same name of the reference we have a problem, since the parameter
* to the `inject()` function would hide the outer variable.
*
* To help with this, the injected parameters can, optionally, be enclosed with underscores.
* These are ignored by the injector when the reference name is resolved.
*
* For example, the parameter `_myService_` would be resolved as the reference `myService`.
* Since it is available in the function body as _myService_, we can then assign it to a variable
* defined in an outer scope.
*
* ```
* // Defined out reference variable outside
* var myService;
*
* // Wrap the parameter in underscores
* beforeEach( inject( function(_myService_){
* myService = _myService_;
* }));
*
* // Use myService in a series of tests.
* it('makes use of myService', function() {
* myService.doStuff();
* });
*
* ```
*
* See also {@link angular.mock.module angular.mock.module}
*
* ## Example
* Example of what a typical jasmine tests looks like with the inject method.
* <pre>
*
* angular.module('myApplicationModule', [])
* .value('mode', 'app')
* .value('version', 'v1.0.1');
*
*
* describe('MyApp', function() {
*
* // You need to load modules that you want to test,
* // it loads only the "ng" module by default.
* beforeEach(module('myApplicationModule'));
*
*
* // inject() is used to inject arguments of all given functions
* it('should provide a version', inject(function(mode, version) {
* expect(version).toEqual('v1.0.1');
* expect(mode).toEqual('app');
* }));
*
*
* // The inject and module method can also be used inside of the it or beforeEach
* it('should override a version and test the new version is injected', function() {
* // module() takes functions or strings (module aliases)
* module(function($provide) {
* $provide.value('version', 'overridden'); // override version here
* });
*
* inject(function(version) {
* expect(version).toEqual('overridden');
* });
* });
* });
*
* </pre>
*
* @param {...Function} fns any number of functions which will be injected using the injector.
*/
var ErrorAddingDeclarationLocationStack = function(e, errorForStack) {
this.message = e.message;
this.name = e.name;
if (e.line) this.line = e.line;
if (e.sourceId) this.sourceId = e.sourceId;
if (e.stack && errorForStack)
this.stack = e.stack + '\n' + errorForStack.stack;
if (e.stackArray) this.stackArray = e.stackArray;
};
ErrorAddingDeclarationLocationStack.prototype.toString = Error.prototype.toString;
window.inject = angular.mock.inject = function() {
var blockFns = Array.prototype.slice.call(arguments, 0);
var errorForStack = new Error('Declaration Location');
return isSpecRunning() ? workFn() : workFn;
/////////////////////
function workFn() {
var modules = currentSpec.$modules || [];
modules.unshift('ngMock');
modules.unshift('ng');
var injector = currentSpec.$injector;
if (!injector) {
injector = currentSpec.$injector = angular.injector(modules);
}
for(var i = 0, ii = blockFns.length; i < ii; i++) {
try {
/* jshint -W040 *//* Jasmine explicitly provides a `this` object when calling functions */
injector.invoke(blockFns[i] || angular.noop, this);
/* jshint +W040 */
} catch (e) {
if (e.stack && errorForStack) {
throw new ErrorAddingDeclarationLocationStack(e, errorForStack);
}
throw e;
} finally {
errorForStack = null;
}
}
}
};
}
})(window, window.angular);
{
"name": "angular",
"version": "1.2.8",
"main": "./angular.js",
"dependencies": {
}
}
// AngularFire is an officially supported AngularJS binding for Firebase.
// The bindings let you associate a Firebase URL with a model (or set of
// models), and they will be transparently kept in sync across all clients
// currently using your app. The 2-way data binding offered by AngularJS works
// as normal, except that the changes are also sent to all other clients
// instead of just a server.
//
// AngularFire 0.5.0
// http://angularfire.com
// License: MIT
"use strict";
var AngularFire, AngularFireAuth;
// Define the `firebase` module under which all AngularFire services will live.
angular.module("firebase", []).value("Firebase", Firebase);
// Define the `$firebase` service that provides synchronization methods.
angular.module("firebase").factory("$firebase", ["$q", "$parse", "$timeout",
function($q, $parse, $timeout) {
// The factory returns an object containing the value of the data at
// the Firebase location provided, as well as several methods. It
// takes a single argument:
//
// * `ref`: A Firebase reference. Queries or limits may be applied.
return function(ref) {
var af = new AngularFire($q, $parse, $timeout, ref);
return af.construct();
};
}
]);
// Define the `orderByPriority` filter that sorts objects returned by
// $firebase in the order of priority. Priority is defined by Firebase,
// for more info see: https://www.firebase.com/docs/ordered-data.html
angular.module("firebase").filter("orderByPriority", function() {
return function(input) {
if (!input.$getIndex || typeof input.$getIndex != "function") {
// If input is an object, map it to an array for the time being.
var type = Object.prototype.toString.call(input);
if (typeof input == "object" && type == "[object Object]") {
var ret = [];
for (var prop in input) {
if (input.hasOwnProperty(prop)) {
ret.push(input[prop]);
}
}
return ret;
}
return input;
}
var sorted = [];
var index = input.$getIndex();
if (index.length <= 0) {
return input;
}
for (var i = 0; i < index.length; i++) {
var val = input[index[i]];
if (val) {
val.$id = index[i];
sorted.push(val);
}
}
return sorted;
};
});
// The `AngularFire` object that implements synchronization.
AngularFire = function($q, $parse, $timeout, ref) {
this._q = $q;
this._bound = false;
this._loaded = false;
this._parse = $parse;
this._timeout = $timeout;
this._index = [];
this._onChange = [];
this._onLoaded = [];
if (typeof ref == "string") {
throw new Error("Please provide a Firebase reference instead " +
"of a URL, eg: new Firebase(url)");
}
this._fRef = ref;
};
AngularFire.prototype = {
// This function is called by the factory to create a new explicit sync
// point between a particular model and a Firebase location.
construct: function() {
var self = this;
var object = {};
// Establish a 3-way data binding (implicit sync) with the specified
// Firebase location and a model on $scope. To be used from a controller
// to automatically synchronize *all* local changes. It take two arguments:
//
// * `$scope`: The scope with which the bound model is associated.
// * `name` : The name of the model.
//
// This function also returns a promise, which when resolve will be
// provided an `unbind` method, a function which you can call to stop
// watching the local model for changes.
object.$bind = function(scope, name) {
return self._bind(scope, name);
};
// Add an object to the remote data. Adding an object is the
// equivalent of calling `push()` on a Firebase reference. It takes
// up to two arguments:
//
// * `item`: The object or primitive to add.
// * `cb` : An optional callback function to be invoked when the
// item is added to the Firebase server. It will be called
// with an Error object if one occurred, null otherwise.
//
// This function returns a Firebase reference to the newly added object
// or primitive. The key name can be extracted using `ref.name()`.
object.$add = function(item, cb) {
var ref;
if (typeof item == "object") {
ref = self._fRef.ref().push(self._parseObject(item), cb);
} else {
ref = self._fRef.ref().push(item, cb);
}
return ref;
};
// Save the current state of the object (or a child) to the remote.
// Takes a single optional argument:
//
// * `key`: Specify a child key to save the data for. If no key is
// specified, the entire object's current state will be saved.
object.$save = function(key) {
if (key) {
self._fRef.ref().child(key).set(self._parseObject(self._object[key]));
} else {
self._fRef.ref().set(self._parseObject(self._object));
}
};
// Set the current state of the object to the specified value. Calling
// this is the equivalent of calling `set()` on a Firebase reference.
object.$set = function(newValue) {
self._fRef.ref().set(newValue);
};
// Remove this object from the remote data. Calling this is the equivalent
// of calling `remove()` on a Firebase reference. This function takes a
// single optional argument:
//
// * `key`: Specify a child key to remove. If no key is specified, the
// entire object will be removed from the remote data store.
object.$remove = function(key) {
if (key) {
self._fRef.ref().child(key).remove();
} else {
self._fRef.ref().remove();
}
};
// Get an AngularFire wrapper for a named child.
object.$child = function(key) {
var af = new AngularFire(
self._q, self._parse, self._timeout, self._fRef.ref().child(key)
);
return af.construct();
};
// Attach an event handler for when the object is changed. You can attach
// handlers for the following events:
//
// - "change": The provided function will be called whenever the local
// object is modified because the remote data was updated.
// - "loaded": This function will be called *once*, when the initial
// data has been loaded. 'object' will be an empty object ({})
// until this function is called.
object.$on = function(type, callback) {
switch (type) {
case "change":
self._onChange.push(callback);
break;
case "loaded":
self._onLoaded.push(callback);
break;
default:
throw new Error("Invalid event type " + type + " specified");
}
};
// Return the current index, which is a list of key names in an array,
// ordered by their Firebase priority.
object.$getIndex = function() {
return angular.copy(self._index);
};
self._object = object;
self._getInitialValue();
return self._object;
},
// This function is responsible for fetching the initial data for the
// given reference. If the data returned from the server is an object or
// array, we'll attach appropriate child event handlers. If the value is
// a primitive, we'll continue to watch for value changes.
_getInitialValue: function() {
var self = this;
var gotInitialValue = function(snapshot) {
var value = snapshot.val();
if (value === null) {
// NULLs are handled specially. If there's a 3-way data binding
// on a local primitive, then update that, otherwise switch to object
// binding using child events.
if (self._bound) {
var local = self._parseObject(self._parse(self._name)(self._scope));
switch (typeof local) {
// Primitive defaults.
case "string":
case "undefined":
value = "";
break;
case "number":
value = 0;
break;
case "boolean":
value = false;
break;
}
}
}
switch (typeof value) {
// For primitive values, simply update the object returned.
case "string":
case "number":
case "boolean":
self._updatePrimitive(value);
break;
// For arrays and objects, switch to child methods.
case "object":
self._getChildValues();
self._fRef.off("value", gotInitialValue);
break;
default:
throw new Error("Unexpected type from remote data " + typeof value);
}
// Call handlers for the "loaded" event.
self._loaded = true;
self._broadcastEvent("loaded", value);
};
self._fRef.on("value", gotInitialValue);
},
// This function attaches child events for object and array types.
_getChildValues: function() {
var self = this;
// Store the priority of the current property as "$priority". Changing
// the value of this property will also update the priority of the
// object (see _parseObject).
function _processSnapshot(snapshot, prevChild) {
var key = snapshot.name();
var val = snapshot.val();
// If the item already exists in the index, remove it first.
var curIdx = self._index.indexOf(key);
if (curIdx !== -1) {
self._index.splice(curIdx, 1);
}
// Update index. This is used by $getIndex and orderByPriority.
if (prevChild) {
var prevIdx = self._index.indexOf(prevChild);
self._index.splice(prevIdx + 1, 0, key);
} else {
self._index.unshift(key);
}
// Update local model with priority field, if needed.
if (snapshot.getPriority() !== null) {
val.$priority = snapshot.getPriority();
}
self._updateModel(key, val);
}
self._fRef.on("child_added", _processSnapshot);
self._fRef.on("child_moved", _processSnapshot);
self._fRef.on("child_changed", _processSnapshot);
self._fRef.on("child_removed", function(snapshot) {
// Remove from index.
var key = snapshot.name();
var idx = self._index.indexOf(key);
self._index.splice(idx, 1);
// Remove from local model.
self._updateModel(key, null);
});
},
// Called whenever there is a remote change. Applies them to the local
// model for both explicit and implicit sync modes.
_updateModel: function(key, value) {
var self = this;
self._timeout(function() {
if (value == null) {
delete self._object[key];
} else {
self._object[key] = value;
}
// Call change handlers.
self._broadcastEvent("change");
// If there is an implicit binding, also update the local model.
if (!self._bound) {
return;
}
var current = self._object;
var local = self._parse(self._name)(self._scope);
// If remote value matches local value, don't do anything, otherwise
// apply the change.
if (!angular.equals(current, local)) {
self._parse(self._name).assign(self._scope, angular.copy(current));
}
});
},
// Called whenever there is a remote change for a primitive value.
_updatePrimitive: function(value) {
var self = this;
self._timeout(function() {
// Primitive values are represented as a special object {$value: value}.
// Only update if the remote value is different from the local value.
if (!self._object.$value || !angular.equals(self._object.$value, value)) {
self._object.$value = value;
}
// Call change handlers.
self._broadcastEvent("change");
// If there's an implicit binding, simply update the local scope model.
if (self._bound) {
var local = self._parseObject(self._parse(self._name)(self._scope));
if (!angular.equals(local, value)) {
self._parse(self._name).assign(self._scope, value);
}
}
});
},
// If event handlers for a specified event were attached, call them.
_broadcastEvent: function(evt, param) {
var cbs;
switch (evt) {
case "change":
cbs = this._onChange;
break;
case "loaded":
cbs = this._onLoaded;
break;
default:
cbs = [];
break;
}
if (cbs.length > 0) {
for (var i = 0; i < cbs.length; i++) {
if (typeof cbs[i] == "function") {
cbs[i](param);
}
}
}
},
// This function creates a 3-way binding between the provided scope model
// and Firebase. All changes made to the local model are saved to Firebase
// and changes to the remote data automatically appear on the local model.
_bind: function(scope, name) {
var self = this;
var deferred = self._q.defer();
// _updateModel or _updatePrimitive will take care of updating the local
// model if _bound is set to true.
self._name = name;
self._bound = true;
self._scope = scope;
// If the local model is an object, call an update to set local values.
var local = self._parse(name)(scope);
if (local !== undefined && typeof local == "object") {
self._fRef.update(self._parseObject(local));
}
// We're responsible for setting up scope.$watch to reflect local changes
// on the Firebase data.
var unbind = scope.$watch(name, function() {
// If the new local value matches the current remote value, we don't
// trigger a remote update.
var local = self._parseObject(self._parse(name)(scope));
if (self._object.$value && angular.equals(local, self._object.$value)) {
return;
} else if (angular.equals(local, self._object)) {
return;
}
// If the local model is undefined or the remote data hasn't been
// loaded yet, don't update.
if (local === undefined || !self._loaded) {
return;
}
// Use update if limits are in effect, set if not.
if (self._fRef.set) {
self._fRef.set(local);
} else {
self._fRef.ref().update(local);
}
}, true);
// When the scope is destroyed, unbind automatically.
scope.$on("$destroy", function() {
unbind();
});
// Once we receive the initial value, resolve the promise.
self._fRef.once("value", function() {
deferred.resolve(unbind);
});
return deferred.promise;
},
// Parse a local model, removing all properties beginning with "$" and
// converting $priority to ".priority".
_parseObject: function(obj) {
function _findReplacePriority(item) {
for (var prop in item) {
if (item.hasOwnProperty(prop)) {
if (prop == "$priority") {
item[".priority"] = item.$priority;
delete item.$priority;
} else if (typeof item[prop] == "object") {
_findReplacePriority(item[prop]);
}
}
}
return item;
}
// We use toJson/fromJson to remove $$hashKey and others. Can be replaced
// by angular.copy, but only for later versions of AngularJS.
var newObj = _findReplacePriority(angular.copy(obj));
return angular.fromJson(angular.toJson(newObj));
}
};
// Defines the `$firebaseAuth` service that provides authentication support
// for AngularFire.
angular.module("firebase").factory("$firebaseAuth", [
"$q", "$timeout", "$injector", "$rootScope", "$location",
function($q, $t, $i, $rs, $l) {
// The factory returns an object containing the authentication state
// of the current user. This service takes 2 arguments:
//
// * `ref` : A Firebase reference.
// * `options`: An object that may contain the following options:
//
// * `path` : The path to which the user will be redirected if the
// authRequired property was set to true in the
// $routeProvider, and the user isn't logged in.
// * `simple` : $firebaseAuth requires inclusion of the
// firebase-simple-login.js file by default. If this
// value is set to false, this requirement is waived,
// but only custom login functionality will be enabled.
// * `callback`: A function that will be called when there is a change
// in authentication state.
//
// The returned object has the following properties:
//
// * `user`: Set to "null" if the user is currently logged out. This value
// will be changed to an object when the user successfully logs in. This
// object will contain details of the logged in user. The exact
// properties will vary based on the method used to login, but will at
// a minimum contain the `id` and `provider` properties.
//
// The returned object will also have the following methods available:
// $login(), $logout() and $createUser().
return function(ref, options) {
var auth = new AngularFireAuth($q, $t, $i, $rs, $l, ref, options);
return auth.construct();
};
}
]);
AngularFireAuth = function($q, $t, $i, $rs, $l, ref, options) {
this._q = $q;
this._timeout = $t;
this._injector = $i;
this._location = $l;
this._rootScope = $rs;
// Check if '$route' is present, use if available.
this._route = null;
if (this._injector.has("$route")) {
this._route = this._injector.get("$route");
}
// Setup options and callback.
this._cb = function(){};
this._options = options || {};
if (this._options.callback && typeof this._options.callback === "function") {
this._cb = options.callback;
}
this._deferred = null;
this._redirectTo = null;
this._authenticated = false;
if (typeof ref == "string") {
throw new Error("Please provide a Firebase reference instead " +
"of a URL, eg: new Firebase(url)");
}
this._fRef = ref;
};
AngularFireAuth.prototype = {
construct: function() {
var self = this;
var object = {
user: null,
$login: self.login.bind(self),
$logout: self.logout.bind(self),
$createUser: self.createUser.bind(self)
};
if (self._options.path && self._route !== null) {
// Check if the current page requires authentication.
if (self._route.current) {
self._authRequiredRedirect(self._route.current, self._options.path);
}
// Set up a handler for all future route changes, so we can check
// if authentication is required.
self._rootScope.$on("$routeChangeStart", function(e, next) {
self._authRequiredRedirect(next, self._options.path);
});
}
// If Simple Login is disabled, simply return.
self._object = object;
if (self._options.simple === false) {
return;
}
// Initialize Simple Login.
if (!window.FirebaseSimpleLogin) {
var err = new Error("FirebaseSimpleLogin undefined, " +
"did you include firebase-simple-login.js?");
self._rootScope.$broadcast("$firebaseAuth:error", err);
return;
}
var client = new FirebaseSimpleLogin(self._fRef, function(err, user) {
self._cb(err, user);
if (err) {
if (self._deferred) {
self._deferred.reject(err);
self._deferred = null;
}
self._rootScope.$broadcast("$firebaseAuth:error", err);
} else if (user) {
if (self._deferred) {
self._deferred.resolve(user);
self._deferred = null;
}
self._loggedIn(user);
} else {
self._loggedOut();
}
});
self._authClient = client;
return self._object;
},
// The login method takes a provider (for Simple Login) or a token
// (for Custom Login) and authenticates the Firebase URL with which
// the service was initialized. This method returns a promise, which will
// be resolved when the login succeeds (and rejected when an error occurs).
login: function(tokenOrProvider, options) {
var self = this;
var deferred = self._q.defer();
switch (tokenOrProvider) {
case "github":
case "persona":
case "twitter":
case "facebook":
case "password":
case "anonymous":
if (!self._authClient) {
var err = new Error("Simple Login not initialized");
deferred.reject(err);
self._rootScope.$broadcast("$firebaseAuth:error", err);
} else {
self._deferred = deferred;
self._authClient.login(tokenOrProvider, options);
}
break;
// A token was provided, so initialize custom login.
default:
try {
// Extract claims and update user auth state to include them.
var claims = self._deconstructJWT(tokenOrProvider);
self._fRef.auth(tokenOrProvider, function(err) {
if (err) {
deferred.reject(err);
self._rootScope.$broadcast("$firebaseAuth:error", err);
} else {
self._deferred = deferred;
self._loggedIn(claims);
}
});
} catch(e) {
deferred.reject(e);
self._rootScope.$broadcast("$firebaseAuth:error", e);
}
}
return deferred.promise;
},
// Unauthenticate the Firebase reference.
logout: function() {
if (this._authClient) {
this._authClient.logout();
} else {
this._fRef.unauth();
this._loggedOut();
}
},
// Creates a user for Firebase Simple Login.
// Function 'cb' receives an error as the first argument and a
// Simple Login user object as the second argument. Pass noLogin=true
// if you don't want the newly created user to also be logged in.
createUser: function(email, password, cb, noLogin) {
var self = this;
self._authClient.createUser(email, password, function(err, user) {
try {
if (err) {
self._rootScope.$broadcast("$firebaseAuth:error", err);
} else {
if (!noLogin) {
self.login("password", {email: email, password: password});
}
}
} catch(e) {
self._rootScope.$broadcast("$firebaseAuth:error", e);
}
if (cb) {
self._timeout(function(){
cb(err, user);
});
}
});
},
// Changes the password for a Firebase Simple Login user.
// Take an email, old password and new password as three mandatory arguments.
// An optional callback may be specified to be notified when the password
// has been changed successfully.
changePassword: function(email, old, np, cb) {
var self = this;
self._authClient.changePassword(email, old, np, function(err, user) {
if (err) {
self._rootScope.$broadcast("$firebaseAuth:error", err);
}
if (cb) {
self._timeout(function() {
cb(err, user);
});
}
});
},
// Common function to trigger a login event on the root scope.
_loggedIn: function(user) {
var self = this;
self._timeout(function() {
self._object.user = user;
self._authenticated = true;
self._rootScope.$broadcast("$firebaseAuth:login", user);
if (self._redirectTo) {
self._location.replace();
self._location.path(self._redirectTo);
self._redirectTo = null;
}
});
},
// Common function to trigger a logout event on the root scope.
_loggedOut: function() {
var self = this;
self._timeout(function() {
self._object.user = null;
self._authenticated = false;
self._rootScope.$broadcast("$firebaseAuth:logout");
});
},
// A function to check whether the current path requires authentication,
// and if so, whether a redirect to a login page is needed.
_authRequiredRedirect: function(route, path) {
if (route.authRequired && !this._authenticated){
if (route.pathTo === undefined) {
this._redirectTo = this._location.path();
} else {
this._redirectTo = route.pathTo === path ? "/" : route.pathTo;
}
this._location.replace();
this._location.path(path);
}
},
// Helper function to decode Base64 (polyfill for window.btoa on IE).
// From: https://github.com/mshang/base64-js/blob/master/base64.js
_decodeBase64: function(str) {
var char_set =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
var output = ""; // final output
var buf = ""; // binary buffer
var bits = 8;
for (var i = 0; i < str.length; ++i) {
if (str[i] == "=") {
break;
}
var c_num = char_set.indexOf(str.charAt(i));
if (c_num == -1) {
throw new Error("Not base64.");
}
var c_bin = c_num.toString(2);
while (c_bin.length < 6) {
c_bin = "0" + c_bin;
}
buf += c_bin;
while (buf.length >= bits) {
var octet = buf.slice(0, bits);
buf = buf.slice(bits);
output += String.fromCharCode(parseInt(octet, 2));
}
}
return output;
},
// Helper function to extract claims from a JWT. Does *not* verify the
// validity of the token.
_deconstructJWT: function(token) {
var segments = token.split(".");
if (!segments instanceof Array || segments.length !== 3) {
throw new Error("Invalid JWT");
}
var decoded = "";
var claims = segments[1];
if (window.atob) {
decoded = window.atob(claims);
} else {
decoded = this._decodeBase64(claims);
}
return JSON.parse(decodeURIComponent(escape(decoded)));
}
};
......@@ -3,7 +3,8 @@
<head>
<meta charset="utf-8">
<title>Firebase &amp; AngularJS • 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">
<style>[ng-cloak] { display: none; }</style>
</head>
<body>
......@@ -20,12 +21,12 @@
<ul id="todo-list">
<li ng-repeat="(id, todo) in todos | todoFilter" ng-class="{completed: todo.completed, editing: todo == editedTodo}">
<div class="view">
<input class="toggle" type="checkbox" ng-model="todo.completed" ng-change="todos.$save(id)">
<label ng-dblclick="editTodo(id)">{{todo.title}}</label>
<button class="destroy" ng-click="removeTodo(id)"></button>
<input class="toggle" type="checkbox" ng-model="todo.completed" ng-change="todos.$save(todo)">
<label ng-dblclick="editTodo(todo)">{{todo.title}}</label>
<button class="destroy" ng-click="removeTodo(todo)"></button>
</div>
<form ng-submit="doneEditing(id)">
<input class="edit" ng-model="todo.title" todo-escape="revertEditing(id)" todo-blur="doneEditing(id)" todo-focus="todo == editedTodo">
<form ng-submit="doneEditing(todo)">
<input class="edit" ng-model="todo.title" todo-escape="revertEditing(todo)" todo-blur="doneEditing(todo)" todo-focus="todo == editedTodo">
</form>
</li>
</ul>
......@@ -53,15 +54,16 @@
<p>Credits:
<a href="http://twitter.com/cburgdorf">Christoph Burgdorf</a>,
<a href="http://ericbidelman.com">Eric Bidelman</a>,
<a href="http://twitter.com/_davideast">David East</a>,
<a href="http://jacobmumm.com">Jacob Mumm</a> and
<a href="http://igorminar.com">Igor Minar</a>
</p>
<p>Part of <a href="http://todomvc.com">TodoMVC</a></p>
</footer>
<script src="https://cdn.firebase.com/v0/firebase.js"></script>
<script src="bower_components/todomvc-common/base.js"></script>
<script src="bower_components/angular/angular.js"></script>
<script src="bower_components/angularfire/angularfire.js"></script>
<script src="//cdn.firebase.com/js/client/2.2.2/firebase.js"></script>
<script src="node_modules/todomvc-common/base.js"></script>
<script src="node_modules/angular/angular.js"></script>
<script src="node_modules/angularfire/dist/angularfire.js"></script>
<script src="js/app.js"></script>
<script src="js/controllers/todoCtrl.js"></script>
<script src="js/directives/todoFocus.js"></script>
......
......@@ -3,18 +3,22 @@
/**
* The main controller for the app. The controller:
* - retrieves and persists the model via the $firebase service
* - retrieves and persists the model via the $firebaseArray service
* - exposes the model to the template and provides event handlers
*/
todomvc.controller('TodoCtrl', function TodoCtrl($scope, $location, $firebase) {
var url = 'https://todomvc-angular.firebaseio.com/';
todomvc.controller('TodoCtrl', function TodoCtrl($scope, $location, $firebaseArray) {
var url = 'https://todomvc-angular.firebaseio.com/todos';
var fireRef = new Firebase(url);
// Bind the todos to the firebase provider.
$scope.todos = $firebaseArray(fireRef);
$scope.newTodo = '';
$scope.editedTodo = null;
$scope.$watch('todos', function () {
var total = 0;
var remaining = 0;
$scope.todos.$getIndex().forEach(function (index) {
var todo = $scope.todos[index];
$scope.todos.forEach(function (todo) {
// Skip invalid entries so they don't break the entire app.
if (!todo || !todo.title) {
return;
......@@ -43,59 +47,47 @@ todomvc.controller('TodoCtrl', function TodoCtrl($scope, $location, $firebase) {
$scope.newTodo = '';
};
$scope.editTodo = function (id) {
$scope.editedTodo = $scope.todos[id];
$scope.editTodo = function (todo) {
$scope.editedTodo = todo;
$scope.originalTodo = angular.extend({}, $scope.editedTodo);
};
$scope.doneEditing = function (id) {
$scope.doneEditing = function (todo) {
$scope.editedTodo = null;
var title = $scope.todos[id].title.trim();
var title = todo.title.trim();
if (title) {
$scope.todos.$save(id);
$scope.todos.$save(todo);
} else {
$scope.removeTodo(id);
$scope.removeTodo(todo);
}
};
$scope.revertEditing = function (id) {
$scope.todos[id] = $scope.originalTodo;
$scope.doneEditing(id);
};
$scope.removeTodo = function (id) {
$scope.todos.$remove(id);
$scope.revertEditing = function (todo) {
todo.title = $scope.originalTodo.title;
$scope.doneEditing(todo);
};
$scope.toggleCompleted = function (id) {
var todo = $scope.todos[id];
todo.completed = !todo.completed;
$scope.todos.$save(id);
$scope.removeTodo = function (todo) {
$scope.todos.$remove(todo);
};
$scope.clearCompletedTodos = function () {
angular.forEach($scope.todos.$getIndex(), function (index) {
if ($scope.todos[index].completed) {
$scope.todos.$remove(index);
$scope.todos.forEach(function(todo) {
if(todo.completed) {
$scope.removeTodo(todo);
}
});
};
$scope.markAll = function (allCompleted) {
angular.forEach($scope.todos.$getIndex(), function (index) {
$scope.todos[index].completed = !allCompleted;
$scope.todos.forEach(function(todo) {
todo.completed = allCompleted;
$scope.todos.$save(todo);
});
$scope.todos.$save();
};
$scope.newTodo = '';
$scope.editedTodo = null;
if ($location.path() === '') {
$location.path('/');
}
$scope.location = $location;
// Bind the todos to the firebase provider.
$scope.todos = $firebase(fireRef);
});
/*!
* AngularFire is the officially supported AngularJS binding for Firebase. Firebase
* is a full backend so you don't need servers to build your Angular app. AngularFire
* provides you with the $firebase service which allows you to easily keep your $scope
* variables in sync with your Firebase backend.
*
* AngularFire 1.0.0
* https://github.com/firebase/angularfire/
* Date: 03/04/2015
* License: MIT
*/
(function(exports) {
"use strict";
// Define the `firebase` module under which all AngularFire
// services will live.
angular.module("firebase", [])
//todo use $window
.value("Firebase", exports.Firebase)
// used in conjunction with firebaseUtils.debounce function, this is the
// amount of time we will wait for additional records before triggering
// Angular's digest scope to dirty check and re-render DOM elements. A
// larger number here significantly improves performance when working with
// big data sets that are frequently changing in the DOM, but delays the
// speed at which each record is rendered in real-time. A number less than
// 100ms will usually be optimal.
.value('firebaseBatchDelay', 50 /* milliseconds */);
})(window);
(function() {
'use strict';
/**
* Creates and maintains a synchronized list of data. This is a pseudo-read-only array. One should
* not call splice(), push(), pop(), et al directly on this array, but should instead use the
* $remove and $add methods.
*
* It is acceptable to .sort() this array, but it is important to use this in conjunction with
* $watch(), so that it will be re-sorted any time the server data changes. Examples of this are
* included in the $watch documentation.
*
* Internally, the $firebase object depends on this class to provide several $$ (i.e. protected)
* methods, which it invokes to notify the array whenever a change has been made at the server:
* $$added - called whenever a child_added event occurs
* $$updated - called whenever a child_changed event occurs
* $$moved - called whenever a child_moved event occurs
* $$removed - called whenever a child_removed event occurs
* $$error - called when listeners are canceled due to a security error
* $$process - called immediately after $$added/$$updated/$$moved/$$removed
* (assuming that these methods do not abort by returning false or null)
* to splice/manipulate the array and invoke $$notify
*
* Additionally, these methods may be of interest to devs extending this class:
* $$notify - triggers notifications to any $watch listeners, called by $$process
* $$getKey - determines how to look up a record's key (returns $id by default)
*
* Instead of directly modifying this class, one should generally use the $extend
* method to add or change how methods behave. $extend modifies the prototype of
* the array class by returning a clone of $firebaseArray.
*
* <pre><code>
* var ExtendedArray = $firebaseArray.$extend({
* // add a new method to the prototype
* foo: function() { return 'bar'; },
*
* // change how records are created
* $$added: function(snap, prevChild) {
* return new Widget(snap, prevChild);
* },
*
* // change how records are updated
* $$updated: function(snap) {
* return this.$getRecord(snap.key()).update(snap);
* }
* });
*
* var list = new ExtendedArray(ref);
* </code></pre>
*/
angular.module('firebase').factory('$firebaseArray', ["$log", "$firebaseUtils",
function($log, $firebaseUtils) {
/**
* This constructor should probably never be called manually. It is used internally by
* <code>$firebase.$asArray()</code>.
*
* @param {Firebase} ref
* @returns {Array}
* @constructor
*/
function FirebaseArray(ref) {
if( !(this instanceof FirebaseArray) ) {
return new FirebaseArray(ref);
}
var self = this;
this._observers = [];
this.$list = [];
this._ref = ref;
this._sync = new ArraySyncManager(this);
$firebaseUtils.assertValidRef(ref, 'Must pass a valid Firebase reference ' +
'to $firebaseArray (not a string or URL)');
// indexCache is a weak hashmap (a lazy list) of keys to array indices,
// items are not guaranteed to stay up to date in this list (since the data
// array can be manually edited without calling the $ methods) and it should
// always be used with skepticism regarding whether it is accurate
// (see $indexFor() below for proper usage)
this._indexCache = {};
// Array.isArray will not work on objects which extend the Array class.
// So instead of extending the Array class, we just return an actual array.
// However, it's still possible to extend FirebaseArray and have the public methods
// appear on the array object. We do this by iterating the prototype and binding
// any method that is not prefixed with an underscore onto the final array.
$firebaseUtils.getPublicMethods(self, function(fn, key) {
self.$list[key] = fn.bind(self);
});
this._sync.init(this.$list);
return this.$list;
}
FirebaseArray.prototype = {
/**
* Create a new record with a unique ID and add it to the end of the array.
* This should be used instead of Array.prototype.push, since those changes will not be
* synchronized with the server.
*
* Any value, including a primitive, can be added in this way. Note that when the record
* is created, the primitive value would be stored in $value (records are always objects
* by default).
*
* Returns a future which is resolved when the data has successfully saved to the server.
* The resolve callback will be passed a Firebase ref representing the new data element.
*
* @param data
* @returns a promise resolved after data is added
*/
$add: function(data) {
this._assertNotDestroyed('$add');
var def = $firebaseUtils.defer();
var ref = this.$ref().ref().push();
ref.set($firebaseUtils.toJSON(data), $firebaseUtils.makeNodeResolver(def));
return def.promise.then(function() {
return ref;
});
},
/**
* Pass either an item in the array or the index of an item and it will be saved back
* to Firebase. While the array is read-only and its structure should not be changed,
* it is okay to modify properties on the objects it contains and then save those back
* individually.
*
* Returns a future which is resolved when the data has successfully saved to the server.
* The resolve callback will be passed a Firebase ref representing the saved element.
* If passed an invalid index or an object which is not a record in this array,
* the promise will be rejected.
*
* @param {int|object} indexOrItem
* @returns a promise resolved after data is saved
*/
$save: function(indexOrItem) {
this._assertNotDestroyed('$save');
var self = this;
var item = self._resolveItem(indexOrItem);
var key = self.$keyAt(item);
if( key !== null ) {
var ref = self.$ref().ref().child(key);
var data = $firebaseUtils.toJSON(item);
return $firebaseUtils.doSet(ref, data).then(function() {
self.$$notify('child_changed', key);
return ref;
});
}
else {
return $firebaseUtils.reject('Invalid record; could determine key for '+indexOrItem);
}
},
/**
* Pass either an existing item in this array or the index of that item and it will
* be removed both locally and in Firebase. This should be used in place of
* Array.prototype.splice for removing items out of the array, as calling splice
* will not update the value on the server.
*
* Returns a future which is resolved when the data has successfully removed from the
* server. The resolve callback will be passed a Firebase ref representing the deleted
* element. If passed an invalid index or an object which is not a record in this array,
* the promise will be rejected.
*
* @param {int|object} indexOrItem
* @returns a promise which resolves after data is removed
*/
$remove: function(indexOrItem) {
this._assertNotDestroyed('$remove');
var key = this.$keyAt(indexOrItem);
if( key !== null ) {
var ref = this.$ref().ref().child(key);
return $firebaseUtils.doRemove(ref).then(function() {
return ref;
});
}
else {
return $firebaseUtils.reject('Invalid record; could not determine key for '+indexOrItem);
}
},
/**
* Given an item in this array or the index of an item in the array, this returns the
* Firebase key (record.$id) for that record. If passed an invalid key or an item which
* does not exist in this array, it will return null.
*
* @param {int|object} indexOrItem
* @returns {null|string}
*/
$keyAt: function(indexOrItem) {
var item = this._resolveItem(indexOrItem);
return this.$$getKey(item);
},
/**
* The inverse of $keyAt, this method takes a Firebase key (record.$id) and returns the
* index in the array where that record is stored. If the record is not in the array,
* this method returns -1.
*
* @param {String} key
* @returns {int} -1 if not found
*/
$indexFor: function(key) {
var self = this;
var cache = self._indexCache;
// evaluate whether our key is cached and, if so, whether it is up to date
if( !cache.hasOwnProperty(key) || self.$keyAt(cache[key]) !== key ) {
// update the hashmap
var pos = self.$list.findIndex(function(rec) { return self.$$getKey(rec) === key; });
if( pos !== -1 ) {
cache[key] = pos;
}
}
return cache.hasOwnProperty(key)? cache[key] : -1;
},
/**
* The loaded method is invoked after the initial batch of data arrives from the server.
* When this resolves, all data which existed prior to calling $asArray() is now cached
* locally in the array.
*
* As a shortcut is also possible to pass resolve/reject methods directly into this
* method just as they would be passed to .then()
*
* @param {Function} [resolve]
* @param {Function} [reject]
* @returns a promise
*/
$loaded: function(resolve, reject) {
var promise = this._sync.ready();
if( arguments.length ) {
// allow this method to be called just like .then
// by passing any arguments on to .then
promise = promise.then.call(promise, resolve, reject);
}
return promise;
},
/**
* @returns {Firebase} the original Firebase ref used to create this object.
*/
$ref: function() { return this._ref; },
/**
* Listeners passed into this method are notified whenever a new change (add, updated,
* move, remove) is received from the server. Each invocation is sent an object
* containing <code>{ type: 'added|updated|moved|removed', key: 'key_of_item_affected'}</code>
*
* Additionally, added and moved events receive a prevChild parameter, containing the
* key of the item before this one in the array.
*
* This method returns a function which can be invoked to stop observing events.
*
* @param {Function} cb
* @param {Object} [context]
* @returns {Function} used to stop observing
*/
$watch: function(cb, context) {
var list = this._observers;
list.push([cb, context]);
// an off function for cancelling the listener
return function() {
var i = list.findIndex(function(parts) {
return parts[0] === cb && parts[1] === context;
});
if( i > -1 ) {
list.splice(i, 1);
}
};
},
/**
* Informs $firebase to stop sending events and clears memory being used
* by this array (delete's its local content).
*/
$destroy: function(err) {
if( !this._isDestroyed ) {
this._isDestroyed = true;
this._sync.destroy(err);
this.$list.length = 0;
$log.debug('destroy called for FirebaseArray: '+this.$ref().ref().toString());
}
},
/**
* Returns the record for a given Firebase key (record.$id). If the record is not found
* then returns null.
*
* @param {string} key
* @returns {Object|null} a record in this array
*/
$getRecord: function(key) {
var i = this.$indexFor(key);
return i > -1? this.$list[i] : null;
},
/**
* Called to inform the array when a new item has been added at the server.
* This method should return the record (an object) that will be passed into $$process
* along with the add event. Alternately, the record will be skipped if this method returns
* a falsey value.
*
* @param {object} snap a Firebase snapshot
* @param {string} prevChild
* @return {object} the record to be inserted into the array
* @protected
*/
$$added: function(snap/*, prevChild*/) {
// check to make sure record does not exist
var i = this.$indexFor($firebaseUtils.getKey(snap));
if( i === -1 ) {
// parse data and create record
var rec = snap.val();
if( !angular.isObject(rec) ) {
rec = { $value: rec };
}
rec.$id = $firebaseUtils.getKey(snap);
rec.$priority = snap.getPriority();
$firebaseUtils.applyDefaults(rec, this.$$defaults);
return rec;
}
return false;
},
/**
* Called whenever an item is removed at the server.
* This method does not physically remove the objects, but instead
* returns a boolean indicating whether it should be removed (and
* taking any other desired actions before the remove completes).
*
* @param {object} snap a Firebase snapshot
* @return {boolean} true if item should be removed
* @protected
*/
$$removed: function(snap) {
return this.$indexFor($firebaseUtils.getKey(snap)) > -1;
},
/**
* Called whenever an item is changed at the server.
* This method should apply the changes, including changes to data
* and to $priority, and then return true if any changes were made.
*
* If this method returns false, then $$process will not be invoked,
* which means that $$notify will not take place and no $watch events
* will be triggered.
*
* @param {object} snap a Firebase snapshot
* @return {boolean} true if any data changed
* @protected
*/
$$updated: function(snap) {
var changed = false;
var rec = this.$getRecord($firebaseUtils.getKey(snap));
if( angular.isObject(rec) ) {
// apply changes to the record
changed = $firebaseUtils.updateRec(rec, snap);
$firebaseUtils.applyDefaults(rec, this.$$defaults);
}
return changed;
},
/**
* Called whenever an item changes order (moves) on the server.
* This method should set $priority to the updated value and return true if
* the record should actually be moved. It should not actually apply the move
* operation.
*
* If this method returns false, then the record will not be moved in the array
* and no $watch listeners will be notified. (When true, $$process is invoked
* which invokes $$notify)
*
* @param {object} snap a Firebase snapshot
* @param {string} prevChild
* @protected
*/
$$moved: function(snap/*, prevChild*/) {
var rec = this.$getRecord($firebaseUtils.getKey(snap));
if( angular.isObject(rec) ) {
rec.$priority = snap.getPriority();
return true;
}
return false;
},
/**
* Called whenever a security error or other problem causes the listeners to become
* invalid. This is generally an unrecoverable error.
*
* @param {Object} err which will have a `code` property and possibly a `message`
* @protected
*/
$$error: function(err) {
$log.error(err);
this.$destroy(err);
},
/**
* Returns ID for a given record
* @param {object} rec
* @returns {string||null}
* @protected
*/
$$getKey: function(rec) {
return angular.isObject(rec)? rec.$id : null;
},
/**
* Handles placement of recs in the array, sending notifications,
* and other internals. Called by the synchronization process
* after $$added, $$updated, $$moved, and $$removed return a truthy value.
*
* @param {string} event one of child_added, child_removed, child_moved, or child_changed
* @param {object} rec
* @param {string} [prevChild]
* @protected
*/
$$process: function(event, rec, prevChild) {
var key = this.$$getKey(rec);
var changed = false;
var curPos;
switch(event) {
case 'child_added':
curPos = this.$indexFor(key);
break;
case 'child_moved':
curPos = this.$indexFor(key);
this._spliceOut(key);
break;
case 'child_removed':
// remove record from the array
changed = this._spliceOut(key) !== null;
break;
case 'child_changed':
changed = true;
break;
default:
throw new Error('Invalid event type: ' + event);
}
if( angular.isDefined(curPos) ) {
// add it to the array
changed = this._addAfter(rec, prevChild) !== curPos;
}
if( changed ) {
// send notifications to anybody monitoring $watch
this.$$notify(event, key, prevChild);
}
return changed;
},
/**
* Used to trigger notifications for listeners registered using $watch. This method is
* typically invoked internally by the $$process method.
*
* @param {string} event
* @param {string} key
* @param {string} [prevChild]
* @protected
*/
$$notify: function(event, key, prevChild) {
var eventData = {event: event, key: key};
if( angular.isDefined(prevChild) ) {
eventData.prevChild = prevChild;
}
angular.forEach(this._observers, function(parts) {
parts[0].call(parts[1], eventData);
});
},
/**
* Used to insert a new record into the array at a specific position. If prevChild is
* null, is inserted first, if prevChild is not found, it is inserted last, otherwise,
* it goes immediately after prevChild.
*
* @param {object} rec
* @param {string|null} prevChild
* @private
*/
_addAfter: function(rec, prevChild) {
var i;
if( prevChild === null ) {
i = 0;
}
else {
i = this.$indexFor(prevChild)+1;
if( i === 0 ) { i = this.$list.length; }
}
this.$list.splice(i, 0, rec);
this._indexCache[this.$$getKey(rec)] = i;
return i;
},
/**
* Removes a record from the array by calling splice. If the item is found
* this method returns it. Otherwise, this method returns null.
*
* @param {string} key
* @returns {object|null}
* @private
*/
_spliceOut: function(key) {
var i = this.$indexFor(key);
if( i > -1 ) {
delete this._indexCache[key];
return this.$list.splice(i, 1)[0];
}
return null;
},
/**
* Resolves a variable which may contain an integer or an item that exists in this array.
* Returns the item or null if it does not exist.
*
* @param indexOrItem
* @returns {*}
* @private
*/
_resolveItem: function(indexOrItem) {
var list = this.$list;
if( angular.isNumber(indexOrItem) && indexOrItem >= 0 && list.length >= indexOrItem ) {
return list[indexOrItem];
}
else if( angular.isObject(indexOrItem) ) {
// it must be an item in this array; it's not sufficient for it just to have
// a $id or even a $id that is in the array, it must be an actual record
// the fastest way to determine this is to use $getRecord (to avoid iterating all recs)
// and compare the two
var key = this.$$getKey(indexOrItem);
var rec = this.$getRecord(key);
return rec === indexOrItem? rec : null;
}
return null;
},
/**
* Throws an error if $destroy has been called. Should be used for any function
* which tries to write data back to $firebase.
* @param {string} method
* @private
*/
_assertNotDestroyed: function(method) {
if( this._isDestroyed ) {
throw new Error('Cannot call ' + method + ' method on a destroyed $firebaseArray object');
}
}
};
/**
* This method allows FirebaseArray to be inherited by child classes. Methods passed into this
* function will be added onto the array's prototype. They can override existing methods as
* well.
*
* In addition to passing additional methods, it is also possible to pass in a class function.
* The prototype on that class function will be preserved, and it will inherit from
* FirebaseArray. It's also possible to do both, passing a class to inherit and additional
* methods to add onto the prototype.
*
* <pre><code>
* var ExtendedArray = $firebaseArray.$extend({
* // add a method onto the prototype that sums all items in the array
* getSum: function() {
* var ct = 0;
* angular.forEach(this.$list, function(rec) { ct += rec.x; });
* return ct;
* }
* });
*
* // use our new factory in place of $firebaseArray
* var list = new ExtendedArray(ref);
* </code></pre>
*
* @param {Function} [ChildClass] a child class which should inherit FirebaseArray
* @param {Object} [methods] a list of functions to add onto the prototype
* @returns {Function} a child class suitable for use with $firebase (this will be ChildClass if provided)
* @static
*/
FirebaseArray.$extend = function(ChildClass, methods) {
if( arguments.length === 1 && angular.isObject(ChildClass) ) {
methods = ChildClass;
ChildClass = function() { return FirebaseArray.apply(this, arguments); };
}
return $firebaseUtils.inherit(ChildClass, FirebaseArray, methods);
};
function ArraySyncManager(firebaseArray) {
function destroy(err) {
if( !sync.isDestroyed ) {
sync.isDestroyed = true;
var ref = firebaseArray.$ref();
ref.off('child_added', created);
ref.off('child_moved', moved);
ref.off('child_changed', updated);
ref.off('child_removed', removed);
firebaseArray = null;
initComplete(err||'destroyed');
}
}
function init($list) {
var ref = firebaseArray.$ref();
// listen for changes at the Firebase instance
ref.on('child_added', created, error);
ref.on('child_moved', moved, error);
ref.on('child_changed', updated, error);
ref.on('child_removed', removed, error);
// determine when initial load is completed
ref.once('value', function(snap) {
if (angular.isArray(snap.val())) {
$log.warn('Storing data using array indices in Firebase can result in unexpected behavior. See https://www.firebase.com/docs/web/guide/understanding-data.html#section-arrays-in-firebase for more information.');
}
initComplete(null, $list);
}, initComplete);
}
// call initComplete(), do not call this directly
function _initComplete(err, result) {
if( !isResolved ) {
isResolved = true;
if( err ) { def.reject(err); }
else { def.resolve(result); }
}
}
var def = $firebaseUtils.defer();
var batch = $firebaseUtils.batch();
var created = batch(function(snap, prevChild) {
var rec = firebaseArray.$$added(snap, prevChild);
if( rec ) {
firebaseArray.$$process('child_added', rec, prevChild);
}
});
var updated = batch(function(snap) {
var rec = firebaseArray.$getRecord($firebaseUtils.getKey(snap));
if( rec ) {
var changed = firebaseArray.$$updated(snap);
if( changed ) {
firebaseArray.$$process('child_changed', rec);
}
}
});
var moved = batch(function(snap, prevChild) {
var rec = firebaseArray.$getRecord($firebaseUtils.getKey(snap));
if( rec ) {
var confirmed = firebaseArray.$$moved(snap, prevChild);
if( confirmed ) {
firebaseArray.$$process('child_moved', rec, prevChild);
}
}
});
var removed = batch(function(snap) {
var rec = firebaseArray.$getRecord($firebaseUtils.getKey(snap));
if( rec ) {
var confirmed = firebaseArray.$$removed(snap);
if( confirmed ) {
firebaseArray.$$process('child_removed', rec);
}
}
});
var isResolved = false;
var error = batch(function(err) {
_initComplete(err);
firebaseArray.$$error(err);
});
var initComplete = batch(_initComplete);
var sync = {
destroy: destroy,
isDestroyed: false,
init: init,
ready: function() { return def.promise; }
};
return sync;
}
return FirebaseArray;
}
]);
/** @deprecated */
angular.module('firebase').factory('$FirebaseArray', ['$log', '$firebaseArray',
function($log, $firebaseArray) {
return function() {
$log.warn('$FirebaseArray has been renamed. Use $firebaseArray instead.');
return $firebaseArray.apply(null, arguments);
};
}
]);
})();
(function() {
'use strict';
var FirebaseAuth;
// Define a service which provides user authentication and management.
angular.module('firebase').factory('$firebaseAuth', [
'$q', '$firebaseUtils', '$log', function($q, $firebaseUtils, $log) {
/**
* This factory returns an object allowing you to manage the client's authentication state.
*
* @param {Firebase} ref A Firebase reference to authenticate.
* @return {object} An object containing methods for authenticating clients, retrieving
* authentication state, and managing users.
*/
return function(ref) {
var auth = new FirebaseAuth($q, $firebaseUtils, $log, ref);
return auth.construct();
};
}
]);
FirebaseAuth = function($q, $firebaseUtils, $log, ref) {
this._q = $q;
this._utils = $firebaseUtils;
this._log = $log;
if (typeof ref === 'string') {
throw new Error('Please provide a Firebase reference instead of a URL when creating a `$firebaseAuth` object.');
}
this._ref = ref;
};
FirebaseAuth.prototype = {
construct: function() {
this._object = {
// Authentication methods
$authWithCustomToken: this.authWithCustomToken.bind(this),
$authAnonymously: this.authAnonymously.bind(this),
$authWithPassword: this.authWithPassword.bind(this),
$authWithOAuthPopup: this.authWithOAuthPopup.bind(this),
$authWithOAuthRedirect: this.authWithOAuthRedirect.bind(this),
$authWithOAuthToken: this.authWithOAuthToken.bind(this),
$unauth: this.unauth.bind(this),
// Authentication state methods
$onAuth: this.onAuth.bind(this),
$getAuth: this.getAuth.bind(this),
$requireAuth: this.requireAuth.bind(this),
$waitForAuth: this.waitForAuth.bind(this),
// User management methods
$createUser: this.createUser.bind(this),
$changePassword: this.changePassword.bind(this),
$changeEmail: this.changeEmail.bind(this),
$removeUser: this.removeUser.bind(this),
$resetPassword: this.resetPassword.bind(this)
};
return this._object;
},
/********************/
/* Authentication */
/********************/
/**
* Authenticates the Firebase reference with a custom authentication token.
*
* @param {string} authToken An authentication token or a Firebase Secret. A Firebase Secret
* should only be used for authenticating a server process and provides full read / write
* access to the entire Firebase.
* @param {Object} [options] An object containing optional client arguments, such as configuring
* session persistence.
* @return {Promise<Object>} A promise fulfilled with an object containing authentication data.
*/
authWithCustomToken: function(authToken, options) {
var deferred = this._q.defer();
try {
this._ref.authWithCustomToken(authToken, this._utils.makeNodeResolver(deferred), options);
} catch (error) {
deferred.reject(error);
}
return deferred.promise;
},
/**
* Authenticates the Firebase reference anonymously.
*
* @param {Object} [options] An object containing optional client arguments, such as configuring
* session persistence.
* @return {Promise<Object>} A promise fulfilled with an object containing authentication data.
*/
authAnonymously: function(options) {
var deferred = this._q.defer();
try {
this._ref.authAnonymously(this._utils.makeNodeResolver(deferred), options);
} catch (error) {
deferred.reject(error);
}
return deferred.promise;
},
/**
* Authenticates the Firebase reference with an email/password user.
*
* @param {Object} credentials An object containing email and password attributes corresponding
* to the user account.
* @param {Object} [options] An object containing optional client arguments, such as configuring
* session persistence.
* @return {Promise<Object>} A promise fulfilled with an object containing authentication data.
*/
authWithPassword: function(credentials, options) {
var deferred = this._q.defer();
try {
this._ref.authWithPassword(credentials, this._utils.makeNodeResolver(deferred), options);
} catch (error) {
deferred.reject(error);
}
return deferred.promise;
},
/**
* Authenticates the Firebase reference with the OAuth popup flow.
*
* @param {string} provider The unique string identifying the OAuth provider to authenticate
* with, e.g. google.
* @param {Object} [options] An object containing optional client arguments, such as configuring
* session persistence.
* @return {Promise<Object>} A promise fulfilled with an object containing authentication data.
*/
authWithOAuthPopup: function(provider, options) {
var deferred = this._q.defer();
try {
this._ref.authWithOAuthPopup(provider, this._utils.makeNodeResolver(deferred), options);
} catch (error) {
deferred.reject(error);
}
return deferred.promise;
},
/**
* Authenticates the Firebase reference with the OAuth redirect flow.
*
* @param {string} provider The unique string identifying the OAuth provider to authenticate
* with, e.g. google.
* @param {Object} [options] An object containing optional client arguments, such as configuring
* session persistence.
* @return {Promise<Object>} A promise fulfilled with an object containing authentication data.
*/
authWithOAuthRedirect: function(provider, options) {
var deferred = this._q.defer();
try {
this._ref.authWithOAuthRedirect(provider, this._utils.makeNodeResolver(deferred), options);
} catch (error) {
deferred.reject(error);
}
return deferred.promise;
},
/**
* Authenticates the Firebase reference with an OAuth token.
*
* @param {string} provider The unique string identifying the OAuth provider to authenticate
* with, e.g. google.
* @param {string|Object} credentials Either a string, such as an OAuth 2.0 access token, or an
* Object of key / value pairs, such as a set of OAuth 1.0a credentials.
* @param {Object} [options] An object containing optional client arguments, such as configuring
* session persistence.
* @return {Promise<Object>} A promise fulfilled with an object containing authentication data.
*/
authWithOAuthToken: function(provider, credentials, options) {
var deferred = this._q.defer();
try {
this._ref.authWithOAuthToken(provider, credentials, this._utils.makeNodeResolver(deferred), options);
} catch (error) {
deferred.reject(error);
}
return deferred.promise;
},
/**
* Unauthenticates the Firebase reference.
*/
unauth: function() {
if (this.getAuth() !== null) {
this._ref.unauth();
}
},
/**************************/
/* Authentication State */
/**************************/
/**
* Asynchronously fires the provided callback with the current authentication data every time
* the authentication data changes. It also fires as soon as the authentication data is
* retrieved from the server.
*
* @param {function} callback A callback that fires when the client's authenticate state
* changes. If authenticated, the callback will be passed an object containing authentication
* data according to the provider used to authenticate. Otherwise, it will be passed null.
* @param {string} [context] If provided, this object will be used as this when calling your
* callback.
* @return {function} A function which can be used to deregister the provided callback.
*/
onAuth: function(callback, context) {
var self = this;
var fn = this._utils.debounce(callback, context, 0);
this._ref.onAuth(fn);
// Return a method to detach the `onAuth()` callback.
return function() {
self._ref.offAuth(fn);
};
},
/**
* Synchronously retrieves the current authentication data.
*
* @return {Object} The client's authentication data.
*/
getAuth: function() {
return this._ref.getAuth();
},
/**
* Helper onAuth() callback method for the two router-related methods.
*
* @param {boolean} rejectIfAuthDataIsNull Determines if the returned promise should be
* resolved or rejected upon an unauthenticated client.
* @return {Promise<Object>} A promise fulfilled with the client's authentication state or
* rejected if the client is unauthenticated and rejectIfAuthDataIsNull is true.
*/
_routerMethodOnAuthPromise: function(rejectIfAuthDataIsNull) {
var ref = this._ref;
return this._utils.promise(function(resolve,reject){
function callback(authData) {
// Turn off this onAuth() callback since we just needed to get the authentication data once.
ref.offAuth(callback);
if (authData !== null) {
resolve(authData);
return;
}
else if (rejectIfAuthDataIsNull) {
reject("AUTH_REQUIRED");
return;
}
else {
resolve(null);
return;
}
}
ref.onAuth(callback);
});
},
/**
* Utility method which can be used in a route's resolve() method to require that a route has
* a logged in client.
*
* @returns {Promise<Object>} A promise fulfilled with the client's current authentication
* state or rejected if the client is not authenticated.
*/
requireAuth: function() {
return this._routerMethodOnAuthPromise(true);
},
/**
* Utility method which can be used in a route's resolve() method to grab the current
* authentication data.
*
* @returns {Promise<Object|null>} A promise fulfilled with the client's current authentication
* state, which will be null if the client is not authenticated.
*/
waitForAuth: function() {
return this._routerMethodOnAuthPromise(false);
},
/*********************/
/* User Management */
/*********************/
/**
* Creates a new email/password user. Note that this function only creates the user, if you
* wish to log in as the newly created user, call $authWithPassword() after the promise for
* this method has been resolved.
*
* @param {Object} credentials An object containing the email and password of the user to create.
* @return {Promise<Object>} A promise fulfilled with the user object, which contains the
* uid of the created user.
*/
createUser: function(credentials) {
var deferred = this._q.defer();
// Throw an error if they are trying to pass in separate string arguments
if (typeof credentials === "string") {
throw new Error("$createUser() expects an object containing 'email' and 'password', but got a string.");
}
try {
this._ref.createUser(credentials, this._utils.makeNodeResolver(deferred));
} catch (error) {
deferred.reject(error);
}
return deferred.promise;
},
/**
* Changes the password for an email/password user.
*
* @param {Object} credentials An object containing the email, old password, and new password of
* the user whose password is to change.
* @return {Promise<>} An empty promise fulfilled once the password change is complete.
*/
changePassword: function(credentials) {
var deferred = this._q.defer();
// Throw an error if they are trying to pass in separate string arguments
if (typeof credentials === "string") {
throw new Error("$changePassword() expects an object containing 'email', 'oldPassword', and 'newPassword', but got a string.");
}
try {
this._ref.changePassword(credentials, this._utils.makeNodeResolver(deferred));
} catch (error) {
deferred.reject(error);
}
return deferred.promise;
},
/**
* Changes the email for an email/password user.
*
* @param {Object} credentials An object containing the old email, new email, and password of
* the user whose email is to change.
* @return {Promise<>} An empty promise fulfilled once the email change is complete.
*/
changeEmail: function(credentials) {
if (typeof this._ref.changeEmail !== 'function') {
throw new Error("$changeEmail() expects an object containing 'oldEmail', 'newEmail', and 'password', but got a string.");
}
var deferred = this._q.defer();
try {
this._ref.changeEmail(credentials, this._utils.makeNodeResolver(deferred));
} catch (error) {
deferred.reject(error);
}
return deferred.promise;
},
/**
* Removes an email/password user.
*
* @param {Object} credentials An object containing the email and password of the user to remove.
* @return {Promise<>} An empty promise fulfilled once the user is removed.
*/
removeUser: function(credentials) {
var deferred = this._q.defer();
// Throw an error if they are trying to pass in separate string arguments
if (typeof credentials === "string") {
throw new Error("$removeUser() expects an object containing 'email' and 'password', but got a string.");
}
try {
this._ref.removeUser(credentials, this._utils.makeNodeResolver(deferred));
} catch (error) {
deferred.reject(error);
}
return deferred.promise;
},
/**
* Sends a password reset email to an email/password user.
*
* @param {Object} credentials An object containing the email of the user to send a reset
* password email to.
* @return {Promise<>} An empty promise fulfilled once the reset password email is sent.
*/
resetPassword: function(credentials) {
var deferred = this._q.defer();
// Throw an error if they are trying to pass in a string argument
if (typeof credentials === "string") {
throw new Error("$resetPassword() expects an object containing 'email', but got a string.");
}
try {
this._ref.resetPassword(credentials, this._utils.makeNodeResolver(deferred));
} catch (error) {
deferred.reject(error);
}
return deferred.promise;
}
};
})();
(function() {
'use strict';
/**
* Creates and maintains a synchronized object, with 2-way bindings between Angular and Firebase.
*
* Implementations of this class are contracted to provide the following internal methods,
* which are used by the synchronization process and 3-way bindings:
* $$updated - called whenever a change occurs (a value event from Firebase)
* $$error - called when listeners are canceled due to a security error
* $$notify - called to update $watch listeners and trigger updates to 3-way bindings
* $ref - called to obtain the underlying Firebase reference
*
* Instead of directly modifying this class, one should generally use the $extend
* method to add or change how methods behave:
*
* <pre><code>
* var ExtendedObject = $firebaseObject.$extend({
* // add a new method to the prototype
* foo: function() { return 'bar'; },
* });
*
* var obj = new ExtendedObject(ref);
* </code></pre>
*/
angular.module('firebase').factory('$firebaseObject', [
'$parse', '$firebaseUtils', '$log',
function($parse, $firebaseUtils, $log) {
/**
* Creates a synchronized object with 2-way bindings between Angular and Firebase.
*
* @param {Firebase} ref
* @returns {FirebaseObject}
* @constructor
*/
function FirebaseObject(ref) {
if( !(this instanceof FirebaseObject) ) {
return new FirebaseObject(ref);
}
// These are private config props and functions used internally
// they are collected here to reduce clutter in console.log and forEach
this.$$conf = {
// synchronizes data to Firebase
sync: new ObjectSyncManager(this, ref),
// stores the Firebase ref
ref: ref,
// synchronizes $scope variables with this object
binding: new ThreeWayBinding(this),
// stores observers registered with $watch
listeners: []
};
// this bit of magic makes $$conf non-enumerable and non-configurable
// and non-writable (its properties are still writable but the ref cannot be replaced)
// we redundantly assign it above so the IDE can relax
Object.defineProperty(this, '$$conf', {
value: this.$$conf
});
this.$id = $firebaseUtils.getKey(ref.ref());
this.$priority = null;
$firebaseUtils.applyDefaults(this, this.$$defaults);
// start synchronizing data with Firebase
this.$$conf.sync.init();
}
FirebaseObject.prototype = {
/**
* Saves all data on the FirebaseObject back to Firebase.
* @returns a promise which will resolve after the save is completed.
*/
$save: function () {
var self = this;
var ref = self.$ref();
var data = $firebaseUtils.toJSON(self);
return $firebaseUtils.doSet(ref, data).then(function() {
self.$$notify();
return self.$ref();
});
},
/**
* Removes all keys from the FirebaseObject and also removes
* the remote data from the server.
*
* @returns a promise which will resolve after the op completes
*/
$remove: function() {
var self = this;
$firebaseUtils.trimKeys(self, {});
self.$value = null;
return $firebaseUtils.doRemove(self.$ref()).then(function() {
self.$$notify();
return self.$ref();
});
},
/**
* The loaded method is invoked after the initial batch of data arrives from the server.
* When this resolves, all data which existed prior to calling $asObject() is now cached
* locally in the object.
*
* As a shortcut is also possible to pass resolve/reject methods directly into this
* method just as they would be passed to .then()
*
* @param {Function} resolve
* @param {Function} reject
* @returns a promise which resolves after initial data is downloaded from Firebase
*/
$loaded: function(resolve, reject) {
var promise = this.$$conf.sync.ready();
if (arguments.length) {
// allow this method to be called just like .then
// by passing any arguments on to .then
promise = promise.then.call(promise, resolve, reject);
}
return promise;
},
/**
* @returns {Firebase} the original Firebase instance used to create this object.
*/
$ref: function () {
return this.$$conf.ref;
},
/**
* Creates a 3-way data sync between this object, the Firebase server, and a
* scope variable. This means that any changes made to the scope variable are
* pushed to Firebase, and vice versa.
*
* If scope emits a $destroy event, the binding is automatically severed. Otherwise,
* it is possible to unbind the scope variable by using the `unbind` function
* passed into the resolve method.
*
* Can only be bound to one scope variable at a time. If a second is attempted,
* the promise will be rejected with an error.
*
* @param {object} scope
* @param {string} varName
* @returns a promise which resolves to an unbind method after data is set in scope
*/
$bindTo: function (scope, varName) {
var self = this;
return self.$loaded().then(function () {
return self.$$conf.binding.bindTo(scope, varName);
});
},
/**
* Listeners passed into this method are notified whenever a new change is received
* from the server. Each invocation is sent an object containing
* <code>{ type: 'updated', key: 'my_firebase_id' }</code>
*
* This method returns an unbind function that can be used to detach the listener.
*
* @param {Function} cb
* @param {Object} [context]
* @returns {Function} invoke to stop observing events
*/
$watch: function (cb, context) {
var list = this.$$conf.listeners;
list.push([cb, context]);
// an off function for cancelling the listener
return function () {
var i = list.findIndex(function (parts) {
return parts[0] === cb && parts[1] === context;
});
if (i > -1) {
list.splice(i, 1);
}
};
},
/**
* Informs $firebase to stop sending events and clears memory being used
* by this object (delete's its local content).
*/
$destroy: function(err) {
var self = this;
if (!self.$isDestroyed) {
self.$isDestroyed = true;
self.$$conf.sync.destroy(err);
self.$$conf.binding.destroy();
$firebaseUtils.each(self, function (v, k) {
delete self[k];
});
}
},
/**
* Called by $firebase whenever an item is changed at the server.
* This method must exist on any objectFactory passed into $firebase.
*
* It should return true if any changes were made, otherwise `$$notify` will
* not be invoked.
*
* @param {object} snap a Firebase snapshot
* @return {boolean} true if any changes were made.
*/
$$updated: function (snap) {
// applies new data to this object
var changed = $firebaseUtils.updateRec(this, snap);
// applies any defaults set using $$defaults
$firebaseUtils.applyDefaults(this, this.$$defaults);
// returning true here causes $$notify to be triggered
return changed;
},
/**
* Called whenever a security error or other problem causes the listeners to become
* invalid. This is generally an unrecoverable error.
* @param {Object} err which will have a `code` property and possibly a `message`
*/
$$error: function (err) {
// prints an error to the console (via Angular's logger)
$log.error(err);
// frees memory and cancels any remaining listeners
this.$destroy(err);
},
/**
* Called internally by $bindTo when data is changed in $scope.
* Should apply updates to this record but should not call
* notify().
*/
$$scopeUpdated: function(newData) {
// we use a one-directional loop to avoid feedback with 3-way bindings
// since set() is applied locally anyway, this is still performant
var def = $firebaseUtils.defer();
this.$ref().set($firebaseUtils.toJSON(newData), $firebaseUtils.makeNodeResolver(def));
return def.promise;
},
/**
* Updates any bound scope variables and
* notifies listeners registered with $watch
*/
$$notify: function() {
var self = this, list = this.$$conf.listeners.slice();
// be sure to do this after setting up data and init state
angular.forEach(list, function (parts) {
parts[0].call(parts[1], {event: 'value', key: self.$id});
});
},
/**
* Overrides how Angular.forEach iterates records on this object so that only
* fields stored in Firebase are part of the iteration. To include meta fields like
* $id and $priority in the iteration, utilize for(key in obj) instead.
*/
forEach: function(iterator, context) {
return $firebaseUtils.each(this, iterator, context);
}
};
/**
* This method allows FirebaseObject to be copied into a new factory. Methods passed into this
* function will be added onto the object's prototype. They can override existing methods as
* well.
*
* In addition to passing additional methods, it is also possible to pass in a class function.
* The prototype on that class function will be preserved, and it will inherit from
* FirebaseObject. It's also possible to do both, passing a class to inherit and additional
* methods to add onto the prototype.
*
* Once a factory is obtained by this method, it can be passed into $firebase as the
* `objectFactory` parameter:
*
* <pre><code>
* var MyFactory = $firebaseObject.$extend({
* // add a method onto the prototype that prints a greeting
* getGreeting: function() {
* return 'Hello ' + this.first_name + ' ' + this.last_name + '!';
* }
* });
*
* // use our new factory in place of $firebaseObject
* var obj = $firebase(ref, {objectFactory: MyFactory}).$asObject();
* </code></pre>
*
* @param {Function} [ChildClass] a child class which should inherit FirebaseObject
* @param {Object} [methods] a list of functions to add onto the prototype
* @returns {Function} a new factory suitable for use with $firebase
*/
FirebaseObject.$extend = function(ChildClass, methods) {
if( arguments.length === 1 && angular.isObject(ChildClass) ) {
methods = ChildClass;
ChildClass = function() { FirebaseObject.apply(this, arguments); };
}
return $firebaseUtils.inherit(ChildClass, FirebaseObject, methods);
};
/**
* Creates a three-way data binding on a scope variable.
*
* @param {FirebaseObject} rec
* @returns {*}
* @constructor
*/
function ThreeWayBinding(rec) {
this.subs = [];
this.scope = null;
this.key = null;
this.rec = rec;
}
ThreeWayBinding.prototype = {
assertNotBound: function(varName) {
if( this.scope ) {
var msg = 'Cannot bind to ' + varName + ' because this instance is already bound to ' +
this.key + '; one binding per instance ' +
'(call unbind method or create another FirebaseObject instance)';
$log.error(msg);
return $firebaseUtils.reject(msg);
}
},
bindTo: function(scope, varName) {
function _bind(self) {
var sending = false;
var parsed = $parse(varName);
var rec = self.rec;
self.scope = scope;
self.varName = varName;
function equals(scopeValue) {
return angular.equals(scopeValue, rec) &&
scopeValue.$priority === rec.$priority &&
scopeValue.$value === rec.$value;
}
function setScope(rec) {
parsed.assign(scope, $firebaseUtils.scopeData(rec));
}
var send = $firebaseUtils.debounce(function(val) {
var scopeData = $firebaseUtils.scopeData(val);
rec.$$scopeUpdated(scopeData)
['finally'](function() {
sending = false;
if(!scopeData.hasOwnProperty('$value')){
delete rec.$value;
delete parsed(scope).$value;
}
}
);
}, 50, 500);
var scopeUpdated = function(newVal) {
newVal = newVal[0];
if( !equals(newVal) ) {
sending = true;
send(newVal);
}
};
var recUpdated = function() {
if( !sending && !equals(parsed(scope)) ) {
setScope(rec);
}
};
// $watch will not check any vars prefixed with $, so we
// manually check $priority and $value using this method
function watchExp(){
var obj = parsed(scope);
return [obj, obj.$priority, obj.$value];
}
setScope(rec);
self.subs.push(scope.$on('$destroy', self.unbind.bind(self)));
// monitor scope for any changes
self.subs.push(scope.$watch(watchExp, scopeUpdated, true));
// monitor the object for changes
self.subs.push(rec.$watch(recUpdated));
return self.unbind.bind(self);
}
return this.assertNotBound(varName) || _bind(this);
},
unbind: function() {
if( this.scope ) {
angular.forEach(this.subs, function(unbind) {
unbind();
});
this.subs = [];
this.scope = null;
this.key = null;
}
},
destroy: function() {
this.unbind();
this.rec = null;
}
};
function ObjectSyncManager(firebaseObject, ref) {
function destroy(err) {
if( !sync.isDestroyed ) {
sync.isDestroyed = true;
ref.off('value', applyUpdate);
firebaseObject = null;
initComplete(err||'destroyed');
}
}
function init() {
ref.on('value', applyUpdate, error);
ref.once('value', function(snap) {
if (angular.isArray(snap.val())) {
$log.warn('Storing data using array indices in Firebase can result in unexpected behavior. See https://www.firebase.com/docs/web/guide/understanding-data.html#section-arrays-in-firebase for more information. Also note that you probably wanted $firebaseArray and not $firebaseObject.');
}
initComplete(null);
}, initComplete);
}
// call initComplete(); do not call this directly
function _initComplete(err) {
if( !isResolved ) {
isResolved = true;
if( err ) { def.reject(err); }
else { def.resolve(firebaseObject); }
}
}
var isResolved = false;
var def = $firebaseUtils.defer();
var batch = $firebaseUtils.batch();
var applyUpdate = batch(function(snap) {
var changed = firebaseObject.$$updated(snap);
if( changed ) {
// notifies $watch listeners and
// updates $scope if bound to a variable
firebaseObject.$$notify();
}
});
var error = batch(firebaseObject.$$error, firebaseObject);
var initComplete = batch(_initComplete);
var sync = {
isDestroyed: false,
destroy: destroy,
init: init,
ready: function() { return def.promise; }
};
return sync;
}
return FirebaseObject;
}
]);
/** @deprecated */
angular.module('firebase').factory('$FirebaseObject', ['$log', '$firebaseObject',
function($log, $firebaseObject) {
return function() {
$log.warn('$FirebaseObject has been renamed. Use $firebaseObject instead.');
return $firebaseObject.apply(null, arguments);
};
}
]);
})();
(function() {
'use strict';
angular.module("firebase")
/** @deprecated */
.factory("$firebase", function() {
return function() {
throw new Error('$firebase has been removed. You may instantiate $firebaseArray and $firebaseObject ' +
'directly now. For simple write operations, just use the Firebase ref directly. ' +
'See the AngularFire 1.0.0 changelog for details: https://www.firebase.com/docs/web/libraries/angular/changelog.html');
};
});
})();
'use strict';
// Shim Array.indexOf for IE compatibility.
if (!Array.prototype.indexOf) {
Array.prototype.indexOf = function (searchElement, fromIndex) {
if (this === undefined || this === null) {
throw new TypeError("'this' is null or not defined");
}
// Hack to convert object.length to a UInt32
// jshint -W016
var length = this.length >>> 0;
fromIndex = +fromIndex || 0;
// jshint +W016
if (Math.abs(fromIndex) === Infinity) {
fromIndex = 0;
}
if (fromIndex < 0) {
fromIndex += length;
if (fromIndex < 0) {
fromIndex = 0;
}
}
for (;fromIndex < length; fromIndex++) {
if (this[fromIndex] === searchElement) {
return fromIndex;
}
}
return -1;
};
}
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/bind
if (!Function.prototype.bind) {
Function.prototype.bind = function (oThis) {
if (typeof this !== "function") {
// closest thing possible to the ECMAScript 5
// internal IsCallable function
throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable");
}
var aArgs = Array.prototype.slice.call(arguments, 1),
fToBind = this,
fNOP = function () {},
fBound = function () {
return fToBind.apply(this instanceof fNOP && oThis
? this
: oThis,
aArgs.concat(Array.prototype.slice.call(arguments)));
};
fNOP.prototype = this.prototype;
fBound.prototype = new fNOP();
return fBound;
};
}
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/findIndex
if (!Array.prototype.findIndex) {
Object.defineProperty(Array.prototype, 'findIndex', {
enumerable: false,
configurable: true,
writable: true,
value: function(predicate) {
if (this == null) {
throw new TypeError('Array.prototype.find called on null or undefined');
}
if (typeof predicate !== 'function') {
throw new TypeError('predicate must be a function');
}
var list = Object(this);
var length = list.length >>> 0;
var thisArg = arguments[1];
var value;
for (var i = 0; i < length; i++) {
if (i in list) {
value = list[i];
if (predicate.call(thisArg, value, i, list)) {
return i;
}
}
}
return -1;
}
});
}
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/create
if (typeof Object.create != 'function') {
(function () {
var F = function () {};
Object.create = function (o) {
if (arguments.length > 1) {
throw new Error('Second argument not supported');
}
if (o === null) {
throw new Error('Cannot set a null [[Prototype]]');
}
if (typeof o != 'object') {
throw new TypeError('Argument must be an object');
}
F.prototype = o;
return new F();
};
})();
}
// From https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/keys
if (!Object.keys) {
Object.keys = (function () {
'use strict';
var hasOwnProperty = Object.prototype.hasOwnProperty,
hasDontEnumBug = !({toString: null}).propertyIsEnumerable('toString'),
dontEnums = [
'toString',
'toLocaleString',
'valueOf',
'hasOwnProperty',
'isPrototypeOf',
'propertyIsEnumerable',
'constructor'
],
dontEnumsLength = dontEnums.length;
return function (obj) {
if (typeof obj !== 'object' && (typeof obj !== 'function' || obj === null)) {
throw new TypeError('Object.keys called on non-object');
}
var result = [], prop, i;
for (prop in obj) {
if (hasOwnProperty.call(obj, prop)) {
result.push(prop);
}
}
if (hasDontEnumBug) {
for (i = 0; i < dontEnumsLength; i++) {
if (hasOwnProperty.call(obj, dontEnums[i])) {
result.push(dontEnums[i]);
}
}
}
return result;
};
}());
}
// http://ejohn.org/blog/objectgetprototypeof/
if ( typeof Object.getPrototypeOf !== "function" ) {
if ( typeof "test".__proto__ === "object" ) {
Object.getPrototypeOf = function(object){
return object.__proto__;
};
} else {
Object.getPrototypeOf = function(object){
// May break if the constructor has been tampered with
return object.constructor.prototype;
};
}
}
(function() {
'use strict';
angular.module('firebase')
.factory('$firebaseConfig', ["$firebaseArray", "$firebaseObject", "$injector",
function($firebaseArray, $firebaseObject, $injector) {
return function(configOpts) {
// make a copy we can modify
var opts = angular.extend({}, configOpts);
// look up factories if passed as string names
if( typeof opts.objectFactory === 'string' ) {
opts.objectFactory = $injector.get(opts.objectFactory);
}
if( typeof opts.arrayFactory === 'string' ) {
opts.arrayFactory = $injector.get(opts.arrayFactory);
}
// extend defaults and return
return angular.extend({
arrayFactory: $firebaseArray,
objectFactory: $firebaseObject
}, opts);
};
}
])
.factory('$firebaseUtils', ["$q", "$timeout", "firebaseBatchDelay",
function($q, $timeout, firebaseBatchDelay) {
// ES6 style promises polyfill for angular 1.2.x
// Copied from angular 1.3.x implementation: https://github.com/angular/angular.js/blob/v1.3.5/src/ng/q.js#L539
function Q(resolver) {
if (!angular.isFunction(resolver)) {
throw new Error('missing resolver function');
}
var deferred = $q.defer();
function resolveFn(value) {
deferred.resolve(value);
}
function rejectFn(reason) {
deferred.reject(reason);
}
resolver(resolveFn, rejectFn);
return deferred.promise;
}
var utils = {
/**
* Returns a function which, each time it is invoked, will pause for `wait`
* milliseconds before invoking the original `fn` instance. If another
* request is received in that time, it resets `wait` up until `maxWait` is
* reached.
*
* Unlike a debounce function, once wait is received, all items that have been
* queued will be invoked (not just once per execution). It is acceptable to use 0,
* which means to batch all synchronously queued items.
*
* The batch function actually returns a wrap function that should be called on each
* method that is to be batched.
*
* <pre><code>
* var total = 0;
* var batchWrapper = batch(10, 100);
* var fn1 = batchWrapper(function(x) { return total += x; });
* var fn2 = batchWrapper(function() { console.log(total); });
* fn1(10);
* fn2();
* fn1(10);
* fn2();
* console.log(total); // 0 (nothing invoked yet)
* // after 10ms will log "10" and then "20"
* </code></pre>
*
* @param {int} wait number of milliseconds to pause before sending out after each invocation
* @param {int} maxWait max milliseconds to wait before sending out, defaults to wait * 10 or 100
* @returns {Function}
*/
batch: function(wait, maxWait) {
wait = typeof('wait') === 'number'? wait : firebaseBatchDelay;
if( !maxWait ) { maxWait = wait*10 || 100; }
var queue = [];
var start;
var cancelTimer;
var runScheduledForNextTick;
// returns `fn` wrapped in a function that queues up each call event to be
// invoked later inside fo runNow()
function createBatchFn(fn, context) {
if( typeof(fn) !== 'function' ) {
throw new Error('Must provide a function to be batched. Got '+fn);
}
return function() {
var args = Array.prototype.slice.call(arguments, 0);
queue.push([fn, context, args]);
resetTimer();
};
}
// clears the current wait timer and creates a new one
// however, if maxWait is exceeded, calls runNow() on the next tick.
function resetTimer() {
if( cancelTimer ) {
cancelTimer();
cancelTimer = null;
}
if( start && Date.now() - start > maxWait ) {
if(!runScheduledForNextTick){
runScheduledForNextTick = true;
utils.compile(runNow);
}
}
else {
if( !start ) { start = Date.now(); }
cancelTimer = utils.wait(runNow, wait);
}
}
// Clears the queue and invokes all of the functions awaiting notification
function runNow() {
cancelTimer = null;
start = null;
runScheduledForNextTick = false;
var copyList = queue.slice(0);
queue = [];
angular.forEach(copyList, function(parts) {
parts[0].apply(parts[1], parts[2]);
});
}
return createBatchFn;
},
/**
* A rudimentary debounce method
* @param {function} fn the function to debounce
* @param {object} [ctx] the `this` context to set in fn
* @param {int} wait number of milliseconds to pause before sending out after each invocation
* @param {int} [maxWait] max milliseconds to wait before sending out, defaults to wait * 10 or 100
*/
debounce: function(fn, ctx, wait, maxWait) {
var start, cancelTimer, args, runScheduledForNextTick;
if( typeof(ctx) === 'number' ) {
maxWait = wait;
wait = ctx;
ctx = null;
}
if( typeof wait !== 'number' ) {
throw new Error('Must provide a valid integer for wait. Try 0 for a default');
}
if( typeof(fn) !== 'function' ) {
throw new Error('Must provide a valid function to debounce');
}
if( !maxWait ) { maxWait = wait*10 || 100; }
// clears the current wait timer and creates a new one
// however, if maxWait is exceeded, calls runNow() on the next tick.
function resetTimer() {
if( cancelTimer ) {
cancelTimer();
cancelTimer = null;
}
if( start && Date.now() - start > maxWait ) {
if(!runScheduledForNextTick){
runScheduledForNextTick = true;
utils.compile(runNow);
}
}
else {
if( !start ) { start = Date.now(); }
cancelTimer = utils.wait(runNow, wait);
}
}
// Clears the queue and invokes the debounced function with the most recent arguments
function runNow() {
cancelTimer = null;
start = null;
runScheduledForNextTick = false;
fn.apply(ctx, args);
}
function debounced() {
args = Array.prototype.slice.call(arguments, 0);
resetTimer();
}
debounced.running = function() {
return start > 0;
};
return debounced;
},
assertValidRef: function(ref, msg) {
if( !angular.isObject(ref) ||
typeof(ref.ref) !== 'function' ||
typeof(ref.ref().transaction) !== 'function' ) {
throw new Error(msg || 'Invalid Firebase reference');
}
},
// http://stackoverflow.com/questions/7509831/alternative-for-the-deprecated-proto
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/create
inherit: function(ChildClass, ParentClass, methods) {
var childMethods = ChildClass.prototype;
ChildClass.prototype = Object.create(ParentClass.prototype);
ChildClass.prototype.constructor = ChildClass; // restoring proper constructor for child class
angular.forEach(Object.keys(childMethods), function(k) {
ChildClass.prototype[k] = childMethods[k];
});
if( angular.isObject(methods) ) {
angular.extend(ChildClass.prototype, methods);
}
return ChildClass;
},
getPrototypeMethods: function(inst, iterator, context) {
var methods = {};
var objProto = Object.getPrototypeOf({});
var proto = angular.isFunction(inst) && angular.isObject(inst.prototype)?
inst.prototype : Object.getPrototypeOf(inst);
while(proto && proto !== objProto) {
for (var key in proto) {
// we only invoke each key once; if a super is overridden it's skipped here
if (proto.hasOwnProperty(key) && !methods.hasOwnProperty(key)) {
methods[key] = true;
iterator.call(context, proto[key], key, proto);
}
}
proto = Object.getPrototypeOf(proto);
}
},
getPublicMethods: function(inst, iterator, context) {
utils.getPrototypeMethods(inst, function(m, k) {
if( typeof(m) === 'function' && k.charAt(0) !== '_' ) {
iterator.call(context, m, k);
}
});
},
defer: $q.defer,
reject: $q.reject,
resolve: $q.when,
//TODO: Remove false branch and use only angular implementation when we drop angular 1.2.x support.
promise: angular.isFunction($q) ? $q : Q,
makeNodeResolver:function(deferred){
return function(err,result){
if(err === null){
if(arguments.length > 2){
result = Array.prototype.slice.call(arguments,1);
}
deferred.resolve(result);
}
else {
deferred.reject(err);
}
};
},
wait: function(fn, wait) {
var to = $timeout(fn, wait||0);
return function() {
if( to ) {
$timeout.cancel(to);
to = null;
}
};
},
compile: function(fn) {
return $timeout(fn||function() {});
},
deepCopy: function(obj) {
if( !angular.isObject(obj) ) { return obj; }
var newCopy = angular.isArray(obj) ? obj.slice() : angular.extend({}, obj);
for (var key in newCopy) {
if (newCopy.hasOwnProperty(key)) {
if (angular.isObject(newCopy[key])) {
newCopy[key] = utils.deepCopy(newCopy[key]);
}
}
}
return newCopy;
},
trimKeys: function(dest, source) {
utils.each(dest, function(v,k) {
if( !source.hasOwnProperty(k) ) {
delete dest[k];
}
});
},
scopeData: function(dataOrRec) {
var data = {
$id: dataOrRec.$id,
$priority: dataOrRec.$priority
};
var hasPublicProp = false;
utils.each(dataOrRec, function(v,k) {
hasPublicProp = true;
data[k] = utils.deepCopy(v);
});
if(!hasPublicProp && dataOrRec.hasOwnProperty('$value')){
data.$value = dataOrRec.$value;
}
return data;
},
updateRec: function(rec, snap) {
var data = snap.val();
var oldData = angular.extend({}, rec);
// deal with primitives
if( !angular.isObject(data) ) {
rec.$value = data;
data = {};
}
else {
delete rec.$value;
}
// apply changes: remove old keys, insert new data, set priority
utils.trimKeys(rec, data);
angular.extend(rec, data);
rec.$priority = snap.getPriority();
return !angular.equals(oldData, rec) ||
oldData.$value !== rec.$value ||
oldData.$priority !== rec.$priority;
},
applyDefaults: function(rec, defaults) {
if( angular.isObject(defaults) ) {
angular.forEach(defaults, function(v,k) {
if( !rec.hasOwnProperty(k) ) {
rec[k] = v;
}
});
}
return rec;
},
dataKeys: function(obj) {
var out = [];
utils.each(obj, function(v,k) {
out.push(k);
});
return out;
},
each: function(obj, iterator, context) {
if(angular.isObject(obj)) {
for (var k in obj) {
if (obj.hasOwnProperty(k)) {
var c = k.charAt(0);
if( c !== '_' && c !== '$' && c !== '.' ) {
iterator.call(context, obj[k], k, obj);
}
}
}
}
else if(angular.isArray(obj)) {
for(var i = 0, len = obj.length; i < len; i++) {
iterator.call(context, obj[i], i, obj);
}
}
return obj;
},
/**
* A utility for retrieving a Firebase reference or DataSnapshot's
* key name. This is backwards-compatible with `name()` from Firebase
* 1.x.x and `key()` from Firebase 2.0.0+. Once support for Firebase
* 1.x.x is dropped in AngularFire, this helper can be removed.
*/
getKey: function(refOrSnapshot) {
return (typeof refOrSnapshot.key === 'function') ? refOrSnapshot.key() : refOrSnapshot.name();
},
/**
* A utility for converting records to JSON objects
* which we can save into Firebase. It asserts valid
* keys and strips off any items prefixed with $.
*
* If the rec passed into this method has a toJSON()
* method, that will be used in place of the custom
* functionality here.
*
* @param rec
* @returns {*}
*/
toJSON: function(rec) {
var dat;
if( !angular.isObject(rec) ) {
rec = {$value: rec};
}
if (angular.isFunction(rec.toJSON)) {
dat = rec.toJSON();
}
else {
dat = {};
utils.each(rec, function (v, k) {
dat[k] = stripDollarPrefixedKeys(v);
});
}
if( angular.isDefined(rec.$value) && Object.keys(dat).length === 0 && rec.$value !== null ) {
dat['.value'] = rec.$value;
}
if( angular.isDefined(rec.$priority) && Object.keys(dat).length > 0 && rec.$priority !== null ) {
dat['.priority'] = rec.$priority;
}
angular.forEach(dat, function(v,k) {
if (k.match(/[.$\[\]#\/]/) && k !== '.value' && k !== '.priority' ) {
throw new Error('Invalid key ' + k + ' (cannot contain .$[]#)');
}
else if( angular.isUndefined(v) ) {
throw new Error('Key '+k+' was undefined. Cannot pass undefined in JSON. Use null instead.');
}
});
return dat;
},
doSet: function(ref, data) {
var def = utils.defer();
if( angular.isFunction(ref.set) || !angular.isObject(data) ) {
// this is not a query, just do a flat set
ref.set(data, utils.makeNodeResolver(def));
}
else {
var dataCopy = angular.extend({}, data);
// this is a query, so we will replace all the elements
// of this query with the value provided, but not blow away
// the entire Firebase path
ref.once('value', function(snap) {
snap.forEach(function(ss) {
if( !dataCopy.hasOwnProperty(utils.getKey(ss)) ) {
dataCopy[utils.getKey(ss)] = null;
}
});
ref.ref().update(dataCopy, utils.makeNodeResolver(def));
}, function(err) {
def.reject(err);
});
}
return def.promise;
},
doRemove: function(ref) {
var def = utils.defer();
if( angular.isFunction(ref.remove) ) {
// ref is not a query, just do a flat remove
ref.remove(utils.makeNodeResolver(def));
}
else {
// ref is a query so let's only remove the
// items in the query and not the entire path
ref.once('value', function(snap) {
var promises = [];
snap.forEach(function(ss) {
var d = utils.defer();
promises.push(d.promise);
ss.ref().remove(utils.makeNodeResolver(def));
});
utils.allPromises(promises)
.then(function() {
def.resolve(ref);
},
function(err){
def.reject(err);
}
);
}, function(err) {
def.reject(err);
});
}
return def.promise;
},
/**
* AngularFire version number.
*/
VERSION: '1.0.0',
batchDelay: firebaseBatchDelay,
allPromises: $q.all.bind($q)
};
return utils;
}
]);
function stripDollarPrefixedKeys(data) {
if( !angular.isObject(data) ) { return data; }
var out = angular.isArray(data)? [] : {};
angular.forEach(data, function(v,k) {
if(typeof k !== 'string' || k.charAt(0) !== '$') {
out[k] = stripDollarPrefixedKeys(v);
}
});
return out;
}
})();
......@@ -12,25 +12,27 @@ button {
font-size: 100%;
vertical-align: baseline;
font-family: inherit;
font-weight: inherit;
color: inherit;
-webkit-appearance: none;
-ms-appearance: none;
-o-appearance: none;
appearance: none;
-webkit-font-smoothing: antialiased;
-moz-font-smoothing: antialiased;
font-smoothing: antialiased;
}
body {
font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif;
line-height: 1.4em;
background: #eaeaea url('bg.png');
background: #f5f5f5;
color: #4d4d4d;
width: 550px;
min-width: 230px;
max-width: 550px;
margin: 0 auto;
-webkit-font-smoothing: antialiased;
-moz-font-smoothing: antialiased;
-ms-font-smoothing: antialiased;
-o-font-smoothing: antialiased;
font-smoothing: antialiased;
font-weight: 300;
}
button,
......@@ -38,78 +40,49 @@ input[type="checkbox"] {
outline: none;
}
.hidden {
display: none;
}
#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%;
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2),
0 25px 50px 0 rgba(0, 0, 0, 0.1);
}
#todoapp input::-webkit-input-placeholder {
font-style: italic;
font-weight: 300;
color: #e6e6e6;
}
#todoapp input::-moz-placeholder {
font-style: italic;
color: #a9a9a9;
font-weight: 300;
color: #e6e6e6;
}
#todoapp input::input-placeholder {
font-style: italic;
font-weight: 300;
color: #e6e6e6;
}
#todoapp h1 {
position: absolute;
top: -120px;
top: -155px;
width: 100%;
font-size: 70px;
font-weight: bold;
font-size: 100px;
font-weight: 100;
text-align: center;
color: #b3b3b3;
color: rgba(255, 255, 255, 0.3);
text-shadow: -1px -1px rgba(0, 0, 0, 0.2);
color: rgba(175, 47, 47, 0.15);
-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: 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;
......@@ -117,6 +90,7 @@ input[type="checkbox"] {
width: 100%;
font-size: 24px;
font-family: inherit;
font-weight: inherit;
line-height: 1.4em;
border: 0;
outline: none;
......@@ -124,29 +98,23 @@ input[type="checkbox"] {
padding: 6px;
border: 1px solid #999;
box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2);
-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;
background: rgba(0, 0, 0, 0.003);
box-shadow: inset 0 -2px 1px rgba(0,0,0,0.03);
}
#main {
position: relative;
z-index: 2;
border-top: 1px dotted #adadad;
border-top: 1px solid #e6e6e6;
}
label[for='toggle-all'] {
......@@ -155,19 +123,19 @@ label[for='toggle-all'] {
#toggle-all {
position: absolute;
top: -42px;
left: -4px;
width: 40px;
top: -55px;
left: -12px;
width: 60px;
height: 34px;
text-align: center;
/* Mobile Safari */
border: none;
border: none; /* Mobile Safari */
}
#toggle-all:before {
content: '»';
font-size: 28px;
color: #d9d9d9;
padding: 0 25px 7px;
content: '';
font-size: 22px;
color: #e6e6e6;
padding: 10px 27px 10px 27px;
}
#toggle-all:checked:before {
......@@ -183,7 +151,7 @@ label[for='toggle-all'] {
#todo-list li {
position: relative;
font-size: 24px;
border-bottom: 1px dotted #ccc;
border-bottom: 1px solid #ededed;
}
#todo-list li:last-child {
......@@ -215,28 +183,17 @@ label[for='toggle-all'] {
top: 0;
bottom: 0;
margin: auto 0;
/* Mobile Safari */
border: none;
border: none; /* Mobile Safari */
-webkit-appearance: none;
-ms-appearance: none;
-o-appearance: none;
appearance: none;
}
#todo-list li .toggle:after {
content: '✔';
/* 40 + a couple of pixels visual adjustment */
line-height: 43px;
font-size: 20px;
color: #d9d9d9;
text-shadow: 0 -1px 0 #bfbfbf;
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>');
}
#todo-list li .toggle:checked:after {
color: #85ada7;
text-shadow: 0 1px 0 #669991;
bottom: 1px;
position: relative;
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>');
}
#todo-list li label {
......@@ -246,12 +203,11 @@ label[for='toggle-all'] {
margin-left: 45px;
display: block;
line-height: 1.2;
-webkit-transition: color 0.4s;
transition: color 0.4s;
}
#todo-list li.completed label {
color: #a9a9a9;
color: #d9d9d9;
text-decoration: line-through;
}
......@@ -264,21 +220,18 @@ label[for='toggle-all'] {
width: 40px;
height: 40px;
margin: auto 0;
font-size: 22px;
color: #a88a8a;
-webkit-transition: all 0.2s;
transition: all 0.2s;
font-size: 30px;
color: #cc9a9a;
margin-bottom: 11px;
transition: color 0.2s ease-out;
}
#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);
transform: scale(1.3);
color: #af5b5e;
}
#todo-list li .destroy:after {
content: '';
content: '×';
}
#todo-list li:hover .destroy {
......@@ -295,29 +248,25 @@ label[for='toggle-all'] {
#footer {
color: #777;
padding: 0 15px;
position: absolute;
right: 0;
bottom: -31px;
left: 0;
padding: 10px 15px;
height: 20px;
z-index: 1;
text-align: center;
border-top: 1px solid #e6e6e6;
}
#footer:before {
content: '';
position: absolute;
right: 0;
bottom: 31px;
bottom: 0;
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);
overflow: hidden;
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2),
0 8px 0 -3px #f6f6f6,
0 9px 1px -3px rgba(0, 0, 0, 0.2),
0 16px 0 -6px #f6f6f6,
0 17px 2px -6px rgba(0, 0, 0, 0.2);
}
#todo-count {
......@@ -325,6 +274,10 @@ label[for='toggle-all'] {
text-align: left;
}
#todo-count strong {
font-weight: 300;
}
#filters {
margin: 0;
padding: 0;
......@@ -339,49 +292,73 @@ label[for='toggle-all'] {
}
#filters li a {
color: #83756f;
margin: 2px;
color: inherit;
margin: 3px;
padding: 3px 7px;
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 {
font-weight: bold;
border-color: rgba(175, 47, 47, 0.2);
}
#clear-completed {
#clear-completed,
html #clear-completed:active {
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);
cursor: pointer;
visibility: hidden;
position: relative;
}
#clear-completed::after {
visibility: visible;
content: 'Clear completed';
position: absolute;
top: 0;
right: 0;
white-space: nowrap;
}
#clear-completed:hover {
background: rgba(0, 0, 0, 0.15);
box-shadow: 0 -1px 0 0 rgba(0, 0, 0, 0.3);
#clear-completed:hover::after {
text-decoration: underline;
}
#info {
margin: 65px auto 0;
color: #a6a6a6;
font-size: 12px;
text-shadow: 0 1px 0 rgba(255, 255, 255, 0.7);
color: #bfbfbf;
font-size: 10px;
text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5);
text-align: center;
}
#info p {
line-height: 1;
}
#info a {
color: inherit;
text-decoration: none;
font-weight: 400;
}
#info a:hover {
text-decoration: underline;
}
/*
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) {
#toggle-all,
#todo-list li .toggle {
......@@ -393,10 +370,6 @@ label[for='toggle-all'] {
}
#toggle-all {
top: -56px;
left: -15px;
width: 65px;
height: 41px;
-webkit-transform: rotate(90deg);
transform: rotate(90deg);
-webkit-appearance: none;
......@@ -404,151 +377,12 @@ label[for='toggle-all'] {
}
}
.hidden {
display: none;
}
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;
@media (max-width: 430px) {
#footer {
height: 50px;
}
.learn-bar #todoapp {
width: 550px;
margin: 130px auto 40px auto;
#filters {
bottom: 10px;
}
}
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 () {
'use strict';
/* jshint ignore:start */
// Underscore's Template Module
// Courtesy of underscorejs.org
var _ = (function (_) {
......@@ -114,6 +116,7 @@
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'));
}
/* jshint ignore:end */
function redirect() {
if (location.hostname === 'tastejs.github.io') {
......@@ -175,13 +178,17 @@
if (learnJSON.backend) {
this.frameworkJSON = learnJSON.backend;
this.frameworkJSON.issueLabel = framework;
this.append({
backend: true
});
} else if (learnJSON[framework]) {
this.frameworkJSON = learnJSON[framework];
this.frameworkJSON.issueLabel = framework;
this.append();
}
this.fetchIssueCount();
}
Learn.prototype.append = function (opts) {
......@@ -212,6 +219,26 @@
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();
getFile('learn.json', Learn);
})();
{
"private": true,
"dependencies": {
"angular": "^1.3.15",
"angularfire": "^1.0.0",
"todomvc-app-css": "^1.0.1",
"todomvc-common": "^1.0.1"
}
}
......@@ -13,10 +13,10 @@ Here are some links you may find helpful:
* [Tutorial](https://www.firebase.com/tutorial/)
* [Documentation & Examples](https://www.firebase.com/docs/)
* [API Reference](https://www.firebase.com/docs/javascript/firebase/)
* [API Reference](https://www.firebase.com/docs/web)
* [Blog](https://www.firebase.com/blog/)
* [Firebase on Github](http://firebase.github.io)
* [AngularJS bindings for Firebase](http://github.com/firebase/angularFire)
* [AngularJS bindings for Firebase](https://www.firebase.com/docs/web/libraries/angular/)
Get help from other AngularJS users:
......
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