├── .gitignore ├── Gruntfile.coffee ├── LICENSE ├── README.md ├── angular-parse.js ├── bower.json ├── example.js ├── example ├── coffee │ └── app.coffee ├── css │ └── style.css ├── index.html ├── js │ └── app.js ├── lib │ └── bootstrap │ │ ├── css │ │ ├── bootstrap-responsive.css │ │ └── bootstrap.css │ │ ├── img │ │ ├── glyphicons-halflings-white.png │ │ └── glyphicons-halflings.png │ │ └── js │ │ └── bootstrap.js ├── parse │ ├── cloud │ │ └── main.js │ ├── config │ │ └── global.json │ └── src │ │ └── main.coffee └── partials │ ├── detail.html │ ├── form.html │ ├── list.html │ ├── register.html │ └── sign-in.html ├── karma.conf.js ├── lib └── angular │ ├── angular-cookies.js │ ├── angular-cookies.min.js │ ├── angular-loader.js │ ├── angular-loader.min.js │ ├── angular-resource.js │ ├── angular-resource.min.js │ ├── angular-sanitize.js │ ├── angular-sanitize.min.js │ ├── angular.js │ ├── angular.min.js │ └── version.txt ├── package.json ├── src └── angular-parse.coffee └── test ├── support ├── angular-mocks.js ├── angular-scenario.js └── jasmine.async.js └── unit ├── authSpec.coffee └── modelSpec.coffee /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .idea/ 3 | npm-debug.log -------------------------------------------------------------------------------- /Gruntfile.coffee: -------------------------------------------------------------------------------- 1 | module.exports = (grunt) -> 2 | # Project configuration. 3 | grunt.initConfig 4 | relativePath: '' 5 | 6 | coffee: 7 | main: 8 | files: [ 9 | expand: true 10 | cwd: 'src/' 11 | src: ['**/*.coffee'] 12 | dest: '.' 13 | ext: '.js' 14 | , 15 | expand: true 16 | cwd: 'example/coffee' 17 | src: ['**/*.coffee'] 18 | dest: 'example/js' 19 | ext: '.js' 20 | ] 21 | 22 | karma: 23 | options: 24 | configFile: 'karma.conf.js' 25 | unit: 26 | background: true 27 | single: 28 | singleRun: true 29 | 30 | connect: 31 | main: 32 | options: 33 | port: 9001 34 | base: 'build/' 35 | 36 | watch: 37 | main: 38 | options: 39 | livereload: false 40 | files: ['src/**/*.coffee', 'test/**/*.coffee'] 41 | tasks: ['coffee', 'karma:unit:run'] 42 | 43 | grunt.loadNpmTasks name for name of grunt.file.readJSON('package.json').devDependencies when name[0..5] is 'grunt-' 44 | 45 | grunt.registerTask 'default', ['coffee', 'karma:unit', 'watch:main'] 46 | grunt.registerTask 'test', ['karma:single'] 47 | grunt.registerTask "parse-deploy", -> 48 | done = @async() 49 | grunt.utils.spawn 50 | cmd: "parse" 51 | args: ["deploy"] 52 | opts: 53 | cwd: "./example/parse" 54 | , -> done() -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 Jim Hoskins 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a 4 | copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included 12 | in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 15 | OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 17 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 18 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 19 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 20 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Parse for AngularJS 2 | 3 | _This is pre-alpha/actively developed. There are no guarantees of 4 | stability, but you are welcome to play around and submit issues_ 5 | 6 | angular-parse is an [AngularJS](http://angularjs.org) module for 7 | interacting with the [Parse](http://parse.com) [REST 8 | API](https://parse.com/docs/rest). It *does not* utlize the [Parse 9 | JavaScript API](https://parse.com/docs/js_guide) but instead is built 10 | from (mostly) scratch. The reason is the existing Parse JavaScript API 11 | is not ideal for AngularJS applications. 12 | 13 | # Why Angular-Parse 14 | 15 | There are a few things that are not ideal about the existing Parse 16 | JavaScript API in AngularJS. The existing API is modeled after [Backbone 17 | Models](http://backbonejs.org/#Model) and the main problem is setters 18 | are used instead of object properties. `instance.set('property', 'value')` 19 | doesn't really fit well with things like `ng-model` 20 | 21 | Instead, angular-parse is based loosely on [Spine 22 | Models](http://spinejs.com/docs/models) where properties directly 23 | defined on the object are used. To facilitate this, when defining a 24 | model, it is "configured" by supplying the class name (as defined in 25 | Parse) as well as which properties are part of that class. 26 | 27 | Angular-parse also uses promises for any methods making network calls. 28 | 29 | ## Getting started 30 | 31 | Include the JavaScript file 32 | 33 | ```html 34 | 35 | 36 | ``` 37 | 38 | Make sure to add `"Parse"` as a dependency of your main module 39 | 40 | ```javascript 41 | var app = angular.module("YourApp", ["Parse"]) 42 | ``` 43 | 44 | Angular-parse also requires you provide the value "ParseConfig" as an 45 | object with the following format 46 | 47 | ```javascript 48 | app.config(function (ParseProvider) { 49 | ParseProvider.initialize("PARSE_APPLICATION_ID", "PARSE_REST_API_KEY"); 50 | }); 51 | ``` 52 | 53 | ## Defining Models 54 | 55 | You can define models by extending Parse.Model. You must call configure 56 | on the class and pass it the Parse class name, and the name of any 57 | attributes of that class 58 | 59 | Using CoffeeScript: 60 | ```coffeescript 61 | app.factory 'Car', (Parse) -> 62 | class Car extends Parse.model 63 | @configure "Car", "make", "model", "year" 64 | 65 | @customClassMethod: (arg) -> 66 | # add custom class methods like this 67 | 68 | customInstanceMethod: (arg) -> 69 | # add custom instance methods like this 70 | ``` 71 | 72 | Using JavaScript: 73 | ```javascript 74 | // Not implemented yet, sorry 75 | ``` 76 | 77 | ## Using Models 78 | 79 | A model acts much the same as a normal JavaScript object with a 80 | constructor 81 | 82 | ### Creating a new instance 83 | 84 | You can create a new instance by using `new`. Any attributes passed in 85 | will be set on the instance. This does not save it to parse, that must 86 | be done with `.save()`. The save method returns a promise, which is 87 | fulfilled with the instance itself. 88 | 89 | ```javascript 90 | var car = new Car({ 91 | make: "Scion", 92 | model: "xB", 93 | year: 2008 94 | }); 95 | 96 | car.isNew() === true; 97 | car.objectId == null; 98 | 99 | car.save().then(function (_car) { 100 | _car === car; 101 | car.isNew() === false; 102 | car.objectId === "...aParseId"; 103 | car.createdAt === "...aDateString"; 104 | car.updatedAt === "...aDateString"; 105 | } 106 | ``` 107 | 108 | If the object has an objectId, it will be updated properly, and will not 109 | create a new instance. `save()` can be used either for new or existing 110 | records. 111 | 112 | ### Getting an instance By Id 113 | 114 | The `find` method on your model class takes an objectId, and returns a 115 | promise that will be fulfilled with your instance if it exists. 116 | 117 | 118 | ```javascript 119 | Car.find("someObjectId").then(function (car) { 120 | car.objectId === "someObjectId"; 121 | }) 122 | ``` 123 | 124 | ### Destroying an instance 125 | 126 | The destroy method on an instance will destroy it set destroyed to true 127 | and set the item's objectId to null 128 | 129 | ```javascript 130 | Car.find("someObjectId").then(function (car) { 131 | car.objectId === "someObjectId"; 132 | 133 | car.destroy().then(function (_car) { 134 | car === _car; 135 | car.destroyed === true; 136 | car.isNew() === true; 137 | car.objectId === null; 138 | }) 139 | }) 140 | ``` 141 | 142 | ### Defining a custom user class 143 | 144 | A simple User class is provided to you. However, you can subclass it: 145 | 146 | ```coffeescript 147 | angular.module('Parse').factory 'ParseCustomUser', (ParseDefaultUser) -> 148 | class CustomUser extends ParseDefaultUser 149 | @configure 'users', 'username', 'password', 'property' 150 | ``` 151 | 152 | In this manner, all User instances returned by the Parse methods 153 | will be of your custom class. 154 | 155 | ### Contributing 156 | 157 | Pull requests and issues are welcome. 158 | -------------------------------------------------------------------------------- /angular-parse.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | var CONFIG, module, 3 | __hasProp = {}.hasOwnProperty, 4 | __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }, 5 | __slice = [].slice, 6 | __extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }; 7 | 8 | module = angular.module('Parse', []); 9 | 10 | CONFIG = {}; 11 | 12 | module.factory('persist', function($q, $window) { 13 | var persist, store; 14 | store = $window.localStorage; 15 | return persist = { 16 | get: function(keys) { 17 | var key, result, _i, _len; 18 | if (!angular.isArray(keys)) { 19 | keys = [keys]; 20 | } 21 | result = {}; 22 | for (_i = 0, _len = keys.length; _i < _len; _i++) { 23 | key = keys[_i]; 24 | if (store.key(key)) { 25 | result[key] = store.getItem(key); 26 | } else { 27 | result[key] = void 0; 28 | } 29 | } 30 | return result; 31 | }, 32 | set: function(obj) { 33 | var key, val; 34 | for (key in obj) { 35 | if (!__hasProp.call(obj, key)) continue; 36 | val = obj[key]; 37 | store.setItem(key, val); 38 | } 39 | return true; 40 | }, 41 | remove: function(keys) { 42 | var key, _i, _len; 43 | if (!angular.isArray(keys)) { 44 | keys = [keys]; 45 | } 46 | for (_i = 0, _len = keys.length; _i < _len; _i++) { 47 | key = keys[_i]; 48 | localStorage.removeItem(key); 49 | } 50 | return true; 51 | } 52 | }; 53 | }); 54 | 55 | module.factory('ParseUtils', function($http, $window) { 56 | var Parse; 57 | return Parse = { 58 | BaseUrl: "https://api.parse.com/1", 59 | _request: function(method, path, data, params) { 60 | var headers, id, klass, _ref; 61 | if (angular.isArray(path)) { 62 | klass = path[0], id = path[1]; 63 | path = "" + (klass.pathBase()) + "/" + id; 64 | } else if (path.className) { 65 | path = "" + (path.pathBase()); 66 | } else if (path.objectId && ((_ref = path.constructor) != null ? _ref.className : void 0)) { 67 | path = "" + (path.constructor.pathBase()) + "/" + path.objectId; 68 | } 69 | headers = { 70 | "X-Parse-Application-Id": CONFIG.applicationId, 71 | "X-Parse-REST-API-KEY": CONFIG.apiKey, 72 | "Content-Type": "application/json" 73 | }; 74 | if ($window.localStorage.key('PARSE_SESSION_TOKEN')) { 75 | headers["X-Parse-Session-Token"] = $window.localStorage.getItem('PARSE_SESSION_TOKEN'); 76 | } 77 | return $http({ 78 | method: method, 79 | url: this.BaseUrl + path, 80 | data: data, 81 | params: params, 82 | headers: headers 83 | }); 84 | }, 85 | func: function(name) { 86 | return function(data) { 87 | return Parse.callFunction(name, data); 88 | }; 89 | }, 90 | callFunction: function(name, data) { 91 | return Parse._request("POST", "/functions/" + name, data).then(function(r) { 92 | return r.data.result; 93 | }); 94 | } 95 | }; 96 | }); 97 | 98 | module.factory('ParseAuth', function(persist, ParseUser, ParseUtils, $q) { 99 | var auth; 100 | return auth = { 101 | sessionToken: null, 102 | currentUser: null, 103 | _login: function(user) { 104 | var info; 105 | auth.currentUser = user; 106 | auth.sessionToken = user.sessionToken; 107 | info = user.attributes(); 108 | info.objectId = user.objectId; 109 | persist.set({ 110 | PARSE_USER_INFO: JSON.stringify(info), 111 | PARSE_SESSION_TOKEN: user.sessionToken 112 | }); 113 | return user; 114 | }, 115 | resumeSession: function() { 116 | var deferred, e, results, sessionToken, user, userAttrs; 117 | results = persist.get(['PARSE_SESSION_TOKEN', 'PARSE_USER_INFO']); 118 | userAttrs = results.PARSE_USER_INFO; 119 | sessionToken = results.PARSE_SESSION_TOKEN; 120 | deferred = $q.defer(); 121 | if (userAttrs && sessionToken) { 122 | try { 123 | user = new ParseUser(JSON.parse(userAttrs)); 124 | auth.currentUser = user; 125 | auth.sessionToken = sessionToken; 126 | deferred.resolve(user.refresh()); 127 | } catch (_error) { 128 | e = _error; 129 | deferred.reject('User attributes not parseable'); 130 | } 131 | } else { 132 | deferred.reject('User attributes or Session Token not found'); 133 | } 134 | return deferred.promise; 135 | }, 136 | register: function(username, password) { 137 | return new ParseUser({ 138 | username: username, 139 | password: password 140 | }).save().then(function(user) { 141 | return auth._login(user); 142 | }); 143 | }, 144 | login: function(username, password) { 145 | return ParseUtils._request("GET", "/login", null, { 146 | username: username, 147 | password: password 148 | }).then(function(response) { 149 | return auth._login(new ParseUser(response.data)); 150 | }); 151 | }, 152 | logout: function() { 153 | persist.remove(['PARSE_SESSION_TOKEN', 'PARSE_USER_INFO']); 154 | auth.currentUser = null; 155 | return auth.sessionToken = null; 156 | } 157 | }; 158 | }); 159 | 160 | module.factory('ParseModel', function(ParseUtils) { 161 | var Model; 162 | return Model = (function() { 163 | Model.pathBase = function() { 164 | return "/classes/" + this.className; 165 | }; 166 | 167 | Model.find = function(id, params) { 168 | var _this = this; 169 | return ParseUtils._request('GET', "/classes/" + this.className + "/" + id, null, params).then(function(response) { 170 | return new _this(response.data); 171 | }); 172 | }; 173 | 174 | Model.query = function(params) { 175 | var _this = this; 176 | return ParseUtils._request('GET', this, null, params).then(function(response) { 177 | var item, _i, _len, _ref, _results; 178 | _ref = response.data.results; 179 | _results = []; 180 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 181 | item = _ref[_i]; 182 | _results.push(new _this(item)); 183 | } 184 | return _results; 185 | }); 186 | }; 187 | 188 | Model.configure = function() { 189 | var attributes, name; 190 | name = arguments[0], attributes = 2 <= arguments.length ? __slice.call(arguments, 1) : []; 191 | this.className = name; 192 | return this.attributes = attributes; 193 | }; 194 | 195 | function Model(data) { 196 | this.isDirty = __bind(this.isDirty, this); 197 | this._saveCache = __bind(this._saveCache, this); 198 | this.encodeParse = __bind(this.encodeParse, this); 199 | this.attributes = __bind(this.attributes, this); 200 | this.destroy = __bind(this.destroy, this); 201 | this.update = __bind(this.update, this); 202 | this.create = __bind(this.create, this); 203 | this.refresh = __bind(this.refresh, this); 204 | this.save = __bind(this.save, this); 205 | this.isNew = __bind(this.isNew, this); 206 | var key, value; 207 | for (key in data) { 208 | value = data[key]; 209 | this[key] = value; 210 | } 211 | this._saveCache(); 212 | } 213 | 214 | Model.prototype.isNew = function() { 215 | return !this.objectId; 216 | }; 217 | 218 | Model.prototype.save = function() { 219 | if (this.isNew()) { 220 | return this.create(); 221 | } else { 222 | return this.update(); 223 | } 224 | }; 225 | 226 | Model.prototype.refresh = function() { 227 | var _this = this; 228 | return ParseUtils._request('GET', this).then(function(response) { 229 | var key, value, _ref; 230 | _ref = response.data; 231 | for (key in _ref) { 232 | if (!__hasProp.call(_ref, key)) continue; 233 | value = _ref[key]; 234 | _this[key] = value; 235 | } 236 | return _this; 237 | }); 238 | }; 239 | 240 | Model.prototype.create = function() { 241 | var _this = this; 242 | return ParseUtils._request('POST', this.constructor, this.encodeParse()).then(function(response) { 243 | var token; 244 | _this.objectId = response.data.objectId; 245 | _this.createdAt = response.data.createdAt; 246 | if (token = response.data.sessionToken) { 247 | _this.sessionToken = token; 248 | } 249 | _this._saveCache(); 250 | return _this; 251 | }); 252 | }; 253 | 254 | Model.prototype.update = function() { 255 | var _this = this; 256 | return ParseUtils._request('PUT', this, this.encodeParse()).then(function(response) { 257 | _this.updatedAt = response.data.updatedAt; 258 | _this._saveCache(); 259 | return _this; 260 | }); 261 | }; 262 | 263 | Model.prototype.destroy = function() { 264 | var _this = this; 265 | return ParseUtils._request('DELETE', this).then(function(response) { 266 | _this.objectId = null; 267 | return _this; 268 | }); 269 | }; 270 | 271 | Model.prototype.attributes = function() { 272 | var key, result, _i, _len, _ref; 273 | result = {}; 274 | _ref = this.constructor.attributes; 275 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 276 | key = _ref[_i]; 277 | result[key] = this[key]; 278 | } 279 | return result; 280 | }; 281 | 282 | Model.prototype.encodeParse = function() { 283 | var key, obj, result, _i, _len, _ref, _ref1; 284 | result = {}; 285 | _ref = this.constructor.attributes; 286 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 287 | key = _ref[_i]; 288 | if (key in this) { 289 | obj = this[key]; 290 | if ((obj != null) && obj.objectId && ((_ref1 = obj.constructor) != null ? _ref1.className : void 0)) { 291 | obj = { 292 | __type: "Pointer", 293 | className: obj.constructor.className, 294 | objectId: obj.objectId 295 | }; 296 | } 297 | result[key] = obj; 298 | } 299 | } 300 | return result; 301 | }; 302 | 303 | Model.prototype._saveCache = function() { 304 | return this._cache = angular.copy(this.encodeParse()); 305 | }; 306 | 307 | Model.prototype.isDirty = function() { 308 | return !angular.equals(this._cache, this.encodeParse()); 309 | }; 310 | 311 | return Model; 312 | 313 | })(); 314 | }); 315 | 316 | module.factory('ParseDefaultUser', function(ParseModel) { 317 | var User, _ref; 318 | return User = (function(_super) { 319 | __extends(User, _super); 320 | 321 | function User() { 322 | _ref = User.__super__.constructor.apply(this, arguments); 323 | return _ref; 324 | } 325 | 326 | User.configure('users', 'username', 'password'); 327 | 328 | User.pathBase = function() { 329 | return "/users"; 330 | }; 331 | 332 | User.prototype.save = function() { 333 | var _this = this; 334 | return User.__super__.save.call(this).then(function(user) { 335 | delete user.password; 336 | return user; 337 | }); 338 | }; 339 | 340 | return User; 341 | 342 | })(ParseModel); 343 | }); 344 | 345 | module.factory('ParseUser', function(ParseDefaultUser, ParseCustomUser) { 346 | if ((ParseCustomUser != null) && (new ParseCustomUser instanceof ParseDefaultUser)) { 347 | return ParseCustomUser; 348 | } else { 349 | return ParseDefaultUser; 350 | } 351 | }); 352 | 353 | module.provider('Parse', function() { 354 | return { 355 | initialize: function(applicationId, apiKey) { 356 | CONFIG.apiKey = apiKey; 357 | return CONFIG.applicationId = applicationId; 358 | }, 359 | $get: function(ParseModel, ParseUser, ParseAuth, ParseUtils) { 360 | return { 361 | BaseUrl: ParseUtils.BaseUrl, 362 | Model: ParseModel, 363 | User: ParseUser, 364 | auth: ParseAuth 365 | }; 366 | } 367 | }; 368 | }); 369 | 370 | angular.module('Parse').factory('ParseCustomUser', function(ParseDefaultUser) { 371 | return ParseDefaultUser; 372 | }); 373 | 374 | }).call(this); 375 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-parse", 3 | "version": "0.3.0", 4 | "main": [ 5 | "angular-parse.js" 6 | ], 7 | "dependencies": { 8 | }, 9 | "install": { 10 | "path": "lib" 11 | } 12 | } -------------------------------------------------------------------------------- /example.js: -------------------------------------------------------------------------------- 1 | var app = angular.module('parseExample', ['parse']); 2 | -------------------------------------------------------------------------------- /example/coffee/app.coffee: -------------------------------------------------------------------------------- 1 | app = angular.module 'Forum', ['Parse'] 2 | 3 | app.run (Parse) -> 4 | Parse.auth.resumeSession() 5 | 6 | app.config (ParseProvider, $routeProvider) -> 7 | ParseProvider.initialize( 8 | "x58FvFYxi4SDIRdJck4hFVv1huo8F409UnfERfUU", 9 | "ixgyh7GKI1eLSU7PYrDuuOEh31CAlYmCXS7tuJ5p" 10 | ) 11 | findPostById = (Post, $route) -> 12 | if id = $route.current.params.id 13 | Post.find(id, include: "author") 14 | else 15 | new Post 16 | 17 | 18 | $routeProvider 19 | .when("/", controller: "PostList", templateUrl: "partials/list.html") 20 | .when("/register", 21 | controller: "RegisterCtrl" 22 | templateUrl: "partials/register.html" 23 | ) 24 | .when("/sign-in", 25 | controller: "SignInCtrl" 26 | templateUrl: "partials/sign-in.html" 27 | ) 28 | .when("/new-post", 29 | controller: "PostForm" 30 | templateUrl: "partials/form.html" 31 | resolve: 32 | $post: findPostById 33 | ) 34 | .when("/posts/:id", 35 | controller: "PostDetail" 36 | templateUrl: "partials/detail.html" 37 | resolve: 38 | $post: findPostById 39 | ) 40 | .when("/edit-post/:id", 41 | controller: "PostForm" 42 | templateUrl: "partials/form.html" 43 | resolve: 44 | $post: findPostById 45 | ) 46 | .otherwise(redirectTo: "/") 47 | 48 | 49 | app.factory 'Post', (Parse) -> 50 | class Post extends Parse.Model 51 | @configure 'Post', 'title', 'body', 'author', 'tags', 'commentCount' 52 | 53 | app.factory 'Comment', (Parse) -> 54 | class Comment extends Parse.Model 55 | @configure 'Comment', 'author', 'post', 'body' 56 | 57 | 58 | app.controller 'PostList', ($scope, Post) -> 59 | $scope.load = -> 60 | Post.query({include: 'author'}).then (posts) -> 61 | $scope.posts = posts 62 | 63 | $scope.destroy = (post) -> 64 | post.destroy().then -> $scope.load() 65 | 66 | $scope.load() 67 | 68 | app.controller 'PostDetail', ($scope, $routeParams, $post, Comment) -> 69 | $scope.post = $post 70 | $scope.comments = [] 71 | 72 | loadComments = -> 73 | Comment.query( 74 | where: 75 | post: 76 | __type: 'Pointer' 77 | className: 'Post' 78 | objectId: $post.objectId 79 | include: 'author' 80 | ).then (comments) -> 81 | $scope.comments = comments 82 | console.log comments 83 | , -> 84 | console.log arguments 85 | 86 | $scope.$on 'new-comment', loadComments 87 | loadComments() 88 | 89 | app.controller 'PostForm', ($scope, $location, Post) -> 90 | $scope.post = new Post 91 | $scope.hideForm = true 92 | 93 | $scope.save = -> 94 | $scope.post?.save().then (post) -> 95 | console.log post 96 | $location.path("/posts/#{post.objectId}") 97 | , (res) -> 98 | console.log res 99 | 100 | app.controller 'CommentForm', ($scope, Comment) -> 101 | $scope.comment = new Comment(post: $scope.post) 102 | console.log $scope.comment 103 | 104 | $scope.save = -> 105 | $scope.comment.save().then -> 106 | console.log 'S', arguments 107 | $scope.$emit 'new-comment' 108 | $scope.comment = new Comment(post: $scope.post) 109 | $scope.numRows = 1 110 | , -> 111 | console.log 'F', arguments 112 | 113 | 114 | app.controller 'AuthCtrl', ($scope, Parse) -> 115 | $scope.auth = Parse.auth 116 | $scope.signout = -> 117 | Parse.auth.logout() 118 | 119 | app.controller 'RegisterCtrl', ($location, $window, $scope, Parse) -> 120 | $scope.auth = Parse.auth 121 | $scope.user = {} 122 | $scope.errorMessage = null 123 | 124 | $scope.register = (user) -> 125 | if user.password isnt user.passwordConfirm 126 | return $scope.errorMessage = "Passwords must match" 127 | 128 | unless user.username and user.password 129 | return $scope.errorMessage = 'Please supply a username and password' 130 | 131 | Parse.auth.register(user.username, user.password).then -> 132 | $location.path("/") 133 | , (err) -> 134 | $scope.errorMessage = err.data.error 135 | 136 | 137 | app.controller 'SignInCtrl', ($location, $window, $scope, Parse) -> 138 | $scope.auth = Parse.auth 139 | $scope.user = {} 140 | $scope.errorMessage = null 141 | 142 | $scope.signin = (user) -> 143 | unless user.username and user.password 144 | return $scope.errorMessage = 'Please supply a username and password' 145 | 146 | Parse.auth.login(user.username, user.password).then -> 147 | console.log 'in', arguments 148 | $location.path("/") 149 | , (err) -> 150 | console.log 'out', arguments 151 | $scope.errorMessage = err.data.error 152 | 153 | -------------------------------------------------------------------------------- /example/css/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | background: #E3E3E3; 3 | } 4 | 5 | body > .navbar { 6 | margin-bottom: 50px; 7 | } 8 | 9 | #main { 10 | background: #FFF; 11 | padding: 10px; 12 | margin: 10px -10px; 13 | box-shadow: 0 0 1px #000; 14 | } 15 | 16 | .header { 17 | border-bottom: solid 1px #CCC; 18 | padding-bottom: 10px; 19 | } 20 | 21 | .new-form { 22 | background: #E0E0E7; 23 | padding: 10px; 24 | margin: 10px -10px; 25 | padding-bottom: -10px; 26 | } 27 | 28 | .post .title { 29 | font-size: 2em; 30 | } 31 | 32 | .comment-count { 33 | font-size: 2em; 34 | font-weight: bold; 35 | text-align: center; 36 | } 37 | 38 | form { 39 | margin: 0; 40 | } 41 | 42 | form input.input-xxlarge, 43 | form textarea.input-xxlarge { 44 | width: 680px; 45 | font-size: 24px; 46 | line-height: 24px; 47 | height: auto; 48 | } 49 | 50 | 51 | .post { 52 | margin-bottom: 20px; 53 | } 54 | 55 | -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Parse Example 5 | 6 | 7 | 8 | 9 | 34 | 35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 | Sign In 44 | 45 |
46 | {{errorMessage}} 47 |
48 | 49 | 50 | 51 | 52 | 53 | 54 |
55 | 56 | or 57 | Register 58 |
59 |
60 |
61 |
62 |
63 |
64 |

Signed In

65 |

{{auth.currentUser.username}}

66 | 67 |
68 |
69 |
70 |
71 |
72 | 73 | 74 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /example/js/app.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | var app, 3 | __hasProp = {}.hasOwnProperty, 4 | __extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }; 5 | 6 | app = angular.module('Forum', ['Parse']); 7 | 8 | app.run(function(Parse) { 9 | return Parse.auth.resumeSession(); 10 | }); 11 | 12 | app.config(function(ParseProvider, $routeProvider) { 13 | var findPostById; 14 | ParseProvider.initialize("x58FvFYxi4SDIRdJck4hFVv1huo8F409UnfERfUU", "ixgyh7GKI1eLSU7PYrDuuOEh31CAlYmCXS7tuJ5p"); 15 | findPostById = function(Post, $route) { 16 | var id; 17 | if (id = $route.current.params.id) { 18 | return Post.find(id, { 19 | include: "author" 20 | }); 21 | } else { 22 | return new Post; 23 | } 24 | }; 25 | return $routeProvider.when("/", { 26 | controller: "PostList", 27 | templateUrl: "partials/list.html" 28 | }).when("/register", { 29 | controller: "RegisterCtrl", 30 | templateUrl: "partials/register.html" 31 | }).when("/sign-in", { 32 | controller: "SignInCtrl", 33 | templateUrl: "partials/sign-in.html" 34 | }).when("/new-post", { 35 | controller: "PostForm", 36 | templateUrl: "partials/form.html", 37 | resolve: { 38 | $post: findPostById 39 | } 40 | }).when("/posts/:id", { 41 | controller: "PostDetail", 42 | templateUrl: "partials/detail.html", 43 | resolve: { 44 | $post: findPostById 45 | } 46 | }).when("/edit-post/:id", { 47 | controller: "PostForm", 48 | templateUrl: "partials/form.html", 49 | resolve: { 50 | $post: findPostById 51 | } 52 | }).otherwise({ 53 | redirectTo: "/" 54 | }); 55 | }); 56 | 57 | app.factory('Post', function(Parse) { 58 | var Post, _ref; 59 | return Post = (function(_super) { 60 | __extends(Post, _super); 61 | 62 | function Post() { 63 | _ref = Post.__super__.constructor.apply(this, arguments); 64 | return _ref; 65 | } 66 | 67 | Post.configure('Post', 'title', 'body', 'author', 'tags', 'commentCount'); 68 | 69 | return Post; 70 | 71 | })(Parse.Model); 72 | }); 73 | 74 | app.factory('Comment', function(Parse) { 75 | var Comment, _ref; 76 | return Comment = (function(_super) { 77 | __extends(Comment, _super); 78 | 79 | function Comment() { 80 | _ref = Comment.__super__.constructor.apply(this, arguments); 81 | return _ref; 82 | } 83 | 84 | Comment.configure('Comment', 'author', 'post', 'body'); 85 | 86 | return Comment; 87 | 88 | })(Parse.Model); 89 | }); 90 | 91 | app.controller('PostList', function($scope, Post) { 92 | $scope.load = function() { 93 | return Post.query({ 94 | include: 'author' 95 | }).then(function(posts) { 96 | return $scope.posts = posts; 97 | }); 98 | }; 99 | $scope.destroy = function(post) { 100 | return post.destroy().then(function() { 101 | return $scope.load(); 102 | }); 103 | }; 104 | return $scope.load(); 105 | }); 106 | 107 | app.controller('PostDetail', function($scope, $routeParams, $post, Comment) { 108 | var loadComments; 109 | $scope.post = $post; 110 | $scope.comments = []; 111 | loadComments = function() { 112 | return Comment.query({ 113 | where: { 114 | post: { 115 | __type: 'Pointer', 116 | className: 'Post', 117 | objectId: $post.objectId 118 | } 119 | }, 120 | include: 'author' 121 | }).then(function(comments) { 122 | $scope.comments = comments; 123 | return console.log(comments); 124 | }, function() { 125 | return console.log(arguments); 126 | }); 127 | }; 128 | $scope.$on('new-comment', loadComments); 129 | return loadComments(); 130 | }); 131 | 132 | app.controller('PostForm', function($scope, $location, Post) { 133 | $scope.post = new Post; 134 | $scope.hideForm = true; 135 | return $scope.save = function() { 136 | var _ref; 137 | return (_ref = $scope.post) != null ? _ref.save().then(function(post) { 138 | console.log(post); 139 | return $location.path("/posts/" + post.objectId); 140 | }, function(res) { 141 | return console.log(res); 142 | }) : void 0; 143 | }; 144 | }); 145 | 146 | app.controller('CommentForm', function($scope, Comment) { 147 | $scope.comment = new Comment({ 148 | post: $scope.post 149 | }); 150 | console.log($scope.comment); 151 | return $scope.save = function() { 152 | return $scope.comment.save().then(function() { 153 | console.log('S', arguments); 154 | $scope.$emit('new-comment'); 155 | $scope.comment = new Comment({ 156 | post: $scope.post 157 | }); 158 | return $scope.numRows = 1; 159 | }, function() { 160 | return console.log('F', arguments); 161 | }); 162 | }; 163 | }); 164 | 165 | app.controller('AuthCtrl', function($scope, Parse) { 166 | $scope.auth = Parse.auth; 167 | return $scope.signout = function() { 168 | return Parse.auth.logout(); 169 | }; 170 | }); 171 | 172 | app.controller('RegisterCtrl', function($location, $window, $scope, Parse) { 173 | $scope.auth = Parse.auth; 174 | $scope.user = {}; 175 | $scope.errorMessage = null; 176 | return $scope.register = function(user) { 177 | if (user.password !== user.passwordConfirm) { 178 | return $scope.errorMessage = "Passwords must match"; 179 | } 180 | if (!(user.username && user.password)) { 181 | return $scope.errorMessage = 'Please supply a username and password'; 182 | } 183 | return Parse.auth.register(user.username, user.password).then(function() { 184 | return $location.path("/"); 185 | }, function(err) { 186 | return $scope.errorMessage = err.data.error; 187 | }); 188 | }; 189 | }); 190 | 191 | app.controller('SignInCtrl', function($location, $window, $scope, Parse) { 192 | $scope.auth = Parse.auth; 193 | $scope.user = {}; 194 | $scope.errorMessage = null; 195 | return $scope.signin = function(user) { 196 | if (!(user.username && user.password)) { 197 | return $scope.errorMessage = 'Please supply a username and password'; 198 | } 199 | return Parse.auth.login(user.username, user.password).then(function() { 200 | console.log('in', arguments); 201 | return $location.path("/"); 202 | }, function(err) { 203 | console.log('out', arguments); 204 | return $scope.errorMessage = err.data.error; 205 | }); 206 | }; 207 | }); 208 | 209 | }).call(this); 210 | -------------------------------------------------------------------------------- /example/lib/bootstrap/css/bootstrap-responsive.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap Responsive v2.2.2 3 | * 4 | * Copyright 2012 Twitter, Inc 5 | * Licensed under the Apache License v2.0 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Designed and built with all the love in the world @twitter by @mdo and @fat. 9 | */ 10 | 11 | @-ms-viewport { 12 | width: device-width; 13 | } 14 | 15 | .clearfix { 16 | *zoom: 1; 17 | } 18 | 19 | .clearfix:before, 20 | .clearfix:after { 21 | display: table; 22 | line-height: 0; 23 | content: ""; 24 | } 25 | 26 | .clearfix:after { 27 | clear: both; 28 | } 29 | 30 | .hide-text { 31 | font: 0/0 a; 32 | color: transparent; 33 | text-shadow: none; 34 | background-color: transparent; 35 | border: 0; 36 | } 37 | 38 | .input-block-level { 39 | display: block; 40 | width: 100%; 41 | min-height: 30px; 42 | -webkit-box-sizing: border-box; 43 | -moz-box-sizing: border-box; 44 | box-sizing: border-box; 45 | } 46 | 47 | .hidden { 48 | display: none; 49 | visibility: hidden; 50 | } 51 | 52 | .visible-phone { 53 | display: none !important; 54 | } 55 | 56 | .visible-tablet { 57 | display: none !important; 58 | } 59 | 60 | .hidden-desktop { 61 | display: none !important; 62 | } 63 | 64 | .visible-desktop { 65 | display: inherit !important; 66 | } 67 | 68 | @media (min-width: 768px) and (max-width: 979px) { 69 | .hidden-desktop { 70 | display: inherit !important; 71 | } 72 | .visible-desktop { 73 | display: none !important ; 74 | } 75 | .visible-tablet { 76 | display: inherit !important; 77 | } 78 | .hidden-tablet { 79 | display: none !important; 80 | } 81 | } 82 | 83 | @media (max-width: 767px) { 84 | .hidden-desktop { 85 | display: inherit !important; 86 | } 87 | .visible-desktop { 88 | display: none !important; 89 | } 90 | .visible-phone { 91 | display: inherit !important; 92 | } 93 | .hidden-phone { 94 | display: none !important; 95 | } 96 | } 97 | 98 | @media (min-width: 1200px) { 99 | .row { 100 | margin-left: -30px; 101 | *zoom: 1; 102 | } 103 | .row:before, 104 | .row:after { 105 | display: table; 106 | line-height: 0; 107 | content: ""; 108 | } 109 | .row:after { 110 | clear: both; 111 | } 112 | [class*="span"] { 113 | float: left; 114 | min-height: 1px; 115 | margin-left: 30px; 116 | } 117 | .container, 118 | .navbar-static-top .container, 119 | .navbar-fixed-top .container, 120 | .navbar-fixed-bottom .container { 121 | width: 1170px; 122 | } 123 | .span12 { 124 | width: 1170px; 125 | } 126 | .span11 { 127 | width: 1070px; 128 | } 129 | .span10 { 130 | width: 970px; 131 | } 132 | .span9 { 133 | width: 870px; 134 | } 135 | .span8 { 136 | width: 770px; 137 | } 138 | .span7 { 139 | width: 670px; 140 | } 141 | .span6 { 142 | width: 570px; 143 | } 144 | .span5 { 145 | width: 470px; 146 | } 147 | .span4 { 148 | width: 370px; 149 | } 150 | .span3 { 151 | width: 270px; 152 | } 153 | .span2 { 154 | width: 170px; 155 | } 156 | .span1 { 157 | width: 70px; 158 | } 159 | .offset12 { 160 | margin-left: 1230px; 161 | } 162 | .offset11 { 163 | margin-left: 1130px; 164 | } 165 | .offset10 { 166 | margin-left: 1030px; 167 | } 168 | .offset9 { 169 | margin-left: 930px; 170 | } 171 | .offset8 { 172 | margin-left: 830px; 173 | } 174 | .offset7 { 175 | margin-left: 730px; 176 | } 177 | .offset6 { 178 | margin-left: 630px; 179 | } 180 | .offset5 { 181 | margin-left: 530px; 182 | } 183 | .offset4 { 184 | margin-left: 430px; 185 | } 186 | .offset3 { 187 | margin-left: 330px; 188 | } 189 | .offset2 { 190 | margin-left: 230px; 191 | } 192 | .offset1 { 193 | margin-left: 130px; 194 | } 195 | .row-fluid { 196 | width: 100%; 197 | *zoom: 1; 198 | } 199 | .row-fluid:before, 200 | .row-fluid:after { 201 | display: table; 202 | line-height: 0; 203 | content: ""; 204 | } 205 | .row-fluid:after { 206 | clear: both; 207 | } 208 | .row-fluid [class*="span"] { 209 | display: block; 210 | float: left; 211 | width: 100%; 212 | min-height: 30px; 213 | margin-left: 2.564102564102564%; 214 | *margin-left: 2.5109110747408616%; 215 | -webkit-box-sizing: border-box; 216 | -moz-box-sizing: border-box; 217 | box-sizing: border-box; 218 | } 219 | .row-fluid [class*="span"]:first-child { 220 | margin-left: 0; 221 | } 222 | .row-fluid .controls-row [class*="span"] + [class*="span"] { 223 | margin-left: 2.564102564102564%; 224 | } 225 | .row-fluid .span12 { 226 | width: 100%; 227 | *width: 99.94680851063829%; 228 | } 229 | .row-fluid .span11 { 230 | width: 91.45299145299145%; 231 | *width: 91.39979996362975%; 232 | } 233 | .row-fluid .span10 { 234 | width: 82.90598290598291%; 235 | *width: 82.8527914166212%; 236 | } 237 | .row-fluid .span9 { 238 | width: 74.35897435897436%; 239 | *width: 74.30578286961266%; 240 | } 241 | .row-fluid .span8 { 242 | width: 65.81196581196582%; 243 | *width: 65.75877432260411%; 244 | } 245 | .row-fluid .span7 { 246 | width: 57.26495726495726%; 247 | *width: 57.21176577559556%; 248 | } 249 | .row-fluid .span6 { 250 | width: 48.717948717948715%; 251 | *width: 48.664757228587014%; 252 | } 253 | .row-fluid .span5 { 254 | width: 40.17094017094017%; 255 | *width: 40.11774868157847%; 256 | } 257 | .row-fluid .span4 { 258 | width: 31.623931623931625%; 259 | *width: 31.570740134569924%; 260 | } 261 | .row-fluid .span3 { 262 | width: 23.076923076923077%; 263 | *width: 23.023731587561375%; 264 | } 265 | .row-fluid .span2 { 266 | width: 14.52991452991453%; 267 | *width: 14.476723040552828%; 268 | } 269 | .row-fluid .span1 { 270 | width: 5.982905982905983%; 271 | *width: 5.929714493544281%; 272 | } 273 | .row-fluid .offset12 { 274 | margin-left: 105.12820512820512%; 275 | *margin-left: 105.02182214948171%; 276 | } 277 | .row-fluid .offset12:first-child { 278 | margin-left: 102.56410256410257%; 279 | *margin-left: 102.45771958537915%; 280 | } 281 | .row-fluid .offset11 { 282 | margin-left: 96.58119658119658%; 283 | *margin-left: 96.47481360247316%; 284 | } 285 | .row-fluid .offset11:first-child { 286 | margin-left: 94.01709401709402%; 287 | *margin-left: 93.91071103837061%; 288 | } 289 | .row-fluid .offset10 { 290 | margin-left: 88.03418803418803%; 291 | *margin-left: 87.92780505546462%; 292 | } 293 | .row-fluid .offset10:first-child { 294 | margin-left: 85.47008547008548%; 295 | *margin-left: 85.36370249136206%; 296 | } 297 | .row-fluid .offset9 { 298 | margin-left: 79.48717948717949%; 299 | *margin-left: 79.38079650845607%; 300 | } 301 | .row-fluid .offset9:first-child { 302 | margin-left: 76.92307692307693%; 303 | *margin-left: 76.81669394435352%; 304 | } 305 | .row-fluid .offset8 { 306 | margin-left: 70.94017094017094%; 307 | *margin-left: 70.83378796144753%; 308 | } 309 | .row-fluid .offset8:first-child { 310 | margin-left: 68.37606837606839%; 311 | *margin-left: 68.26968539734497%; 312 | } 313 | .row-fluid .offset7 { 314 | margin-left: 62.393162393162385%; 315 | *margin-left: 62.28677941443899%; 316 | } 317 | .row-fluid .offset7:first-child { 318 | margin-left: 59.82905982905982%; 319 | *margin-left: 59.72267685033642%; 320 | } 321 | .row-fluid .offset6 { 322 | margin-left: 53.84615384615384%; 323 | *margin-left: 53.739770867430444%; 324 | } 325 | .row-fluid .offset6:first-child { 326 | margin-left: 51.28205128205128%; 327 | *margin-left: 51.175668303327875%; 328 | } 329 | .row-fluid .offset5 { 330 | margin-left: 45.299145299145295%; 331 | *margin-left: 45.1927623204219%; 332 | } 333 | .row-fluid .offset5:first-child { 334 | margin-left: 42.73504273504273%; 335 | *margin-left: 42.62865975631933%; 336 | } 337 | .row-fluid .offset4 { 338 | margin-left: 36.75213675213675%; 339 | *margin-left: 36.645753773413354%; 340 | } 341 | .row-fluid .offset4:first-child { 342 | margin-left: 34.18803418803419%; 343 | *margin-left: 34.081651209310785%; 344 | } 345 | .row-fluid .offset3 { 346 | margin-left: 28.205128205128204%; 347 | *margin-left: 28.0987452264048%; 348 | } 349 | .row-fluid .offset3:first-child { 350 | margin-left: 25.641025641025642%; 351 | *margin-left: 25.53464266230224%; 352 | } 353 | .row-fluid .offset2 { 354 | margin-left: 19.65811965811966%; 355 | *margin-left: 19.551736679396257%; 356 | } 357 | .row-fluid .offset2:first-child { 358 | margin-left: 17.094017094017094%; 359 | *margin-left: 16.98763411529369%; 360 | } 361 | .row-fluid .offset1 { 362 | margin-left: 11.11111111111111%; 363 | *margin-left: 11.004728132387708%; 364 | } 365 | .row-fluid .offset1:first-child { 366 | margin-left: 8.547008547008547%; 367 | *margin-left: 8.440625568285142%; 368 | } 369 | input, 370 | textarea, 371 | .uneditable-input { 372 | margin-left: 0; 373 | } 374 | .controls-row [class*="span"] + [class*="span"] { 375 | margin-left: 30px; 376 | } 377 | input.span12, 378 | textarea.span12, 379 | .uneditable-input.span12 { 380 | width: 1156px; 381 | } 382 | input.span11, 383 | textarea.span11, 384 | .uneditable-input.span11 { 385 | width: 1056px; 386 | } 387 | input.span10, 388 | textarea.span10, 389 | .uneditable-input.span10 { 390 | width: 956px; 391 | } 392 | input.span9, 393 | textarea.span9, 394 | .uneditable-input.span9 { 395 | width: 856px; 396 | } 397 | input.span8, 398 | textarea.span8, 399 | .uneditable-input.span8 { 400 | width: 756px; 401 | } 402 | input.span7, 403 | textarea.span7, 404 | .uneditable-input.span7 { 405 | width: 656px; 406 | } 407 | input.span6, 408 | textarea.span6, 409 | .uneditable-input.span6 { 410 | width: 556px; 411 | } 412 | input.span5, 413 | textarea.span5, 414 | .uneditable-input.span5 { 415 | width: 456px; 416 | } 417 | input.span4, 418 | textarea.span4, 419 | .uneditable-input.span4 { 420 | width: 356px; 421 | } 422 | input.span3, 423 | textarea.span3, 424 | .uneditable-input.span3 { 425 | width: 256px; 426 | } 427 | input.span2, 428 | textarea.span2, 429 | .uneditable-input.span2 { 430 | width: 156px; 431 | } 432 | input.span1, 433 | textarea.span1, 434 | .uneditable-input.span1 { 435 | width: 56px; 436 | } 437 | .thumbnails { 438 | margin-left: -30px; 439 | } 440 | .thumbnails > li { 441 | margin-left: 30px; 442 | } 443 | .row-fluid .thumbnails { 444 | margin-left: 0; 445 | } 446 | } 447 | 448 | @media (min-width: 768px) and (max-width: 979px) { 449 | .row { 450 | margin-left: -20px; 451 | *zoom: 1; 452 | } 453 | .row:before, 454 | .row:after { 455 | display: table; 456 | line-height: 0; 457 | content: ""; 458 | } 459 | .row:after { 460 | clear: both; 461 | } 462 | [class*="span"] { 463 | float: left; 464 | min-height: 1px; 465 | margin-left: 20px; 466 | } 467 | .container, 468 | .navbar-static-top .container, 469 | .navbar-fixed-top .container, 470 | .navbar-fixed-bottom .container { 471 | width: 724px; 472 | } 473 | .span12 { 474 | width: 724px; 475 | } 476 | .span11 { 477 | width: 662px; 478 | } 479 | .span10 { 480 | width: 600px; 481 | } 482 | .span9 { 483 | width: 538px; 484 | } 485 | .span8 { 486 | width: 476px; 487 | } 488 | .span7 { 489 | width: 414px; 490 | } 491 | .span6 { 492 | width: 352px; 493 | } 494 | .span5 { 495 | width: 290px; 496 | } 497 | .span4 { 498 | width: 228px; 499 | } 500 | .span3 { 501 | width: 166px; 502 | } 503 | .span2 { 504 | width: 104px; 505 | } 506 | .span1 { 507 | width: 42px; 508 | } 509 | .offset12 { 510 | margin-left: 764px; 511 | } 512 | .offset11 { 513 | margin-left: 702px; 514 | } 515 | .offset10 { 516 | margin-left: 640px; 517 | } 518 | .offset9 { 519 | margin-left: 578px; 520 | } 521 | .offset8 { 522 | margin-left: 516px; 523 | } 524 | .offset7 { 525 | margin-left: 454px; 526 | } 527 | .offset6 { 528 | margin-left: 392px; 529 | } 530 | .offset5 { 531 | margin-left: 330px; 532 | } 533 | .offset4 { 534 | margin-left: 268px; 535 | } 536 | .offset3 { 537 | margin-left: 206px; 538 | } 539 | .offset2 { 540 | margin-left: 144px; 541 | } 542 | .offset1 { 543 | margin-left: 82px; 544 | } 545 | .row-fluid { 546 | width: 100%; 547 | *zoom: 1; 548 | } 549 | .row-fluid:before, 550 | .row-fluid:after { 551 | display: table; 552 | line-height: 0; 553 | content: ""; 554 | } 555 | .row-fluid:after { 556 | clear: both; 557 | } 558 | .row-fluid [class*="span"] { 559 | display: block; 560 | float: left; 561 | width: 100%; 562 | min-height: 30px; 563 | margin-left: 2.7624309392265194%; 564 | *margin-left: 2.709239449864817%; 565 | -webkit-box-sizing: border-box; 566 | -moz-box-sizing: border-box; 567 | box-sizing: border-box; 568 | } 569 | .row-fluid [class*="span"]:first-child { 570 | margin-left: 0; 571 | } 572 | .row-fluid .controls-row [class*="span"] + [class*="span"] { 573 | margin-left: 2.7624309392265194%; 574 | } 575 | .row-fluid .span12 { 576 | width: 100%; 577 | *width: 99.94680851063829%; 578 | } 579 | .row-fluid .span11 { 580 | width: 91.43646408839778%; 581 | *width: 91.38327259903608%; 582 | } 583 | .row-fluid .span10 { 584 | width: 82.87292817679558%; 585 | *width: 82.81973668743387%; 586 | } 587 | .row-fluid .span9 { 588 | width: 74.30939226519337%; 589 | *width: 74.25620077583166%; 590 | } 591 | .row-fluid .span8 { 592 | width: 65.74585635359117%; 593 | *width: 65.69266486422946%; 594 | } 595 | .row-fluid .span7 { 596 | width: 57.18232044198895%; 597 | *width: 57.12912895262725%; 598 | } 599 | .row-fluid .span6 { 600 | width: 48.61878453038674%; 601 | *width: 48.56559304102504%; 602 | } 603 | .row-fluid .span5 { 604 | width: 40.05524861878453%; 605 | *width: 40.00205712942283%; 606 | } 607 | .row-fluid .span4 { 608 | width: 31.491712707182323%; 609 | *width: 31.43852121782062%; 610 | } 611 | .row-fluid .span3 { 612 | width: 22.92817679558011%; 613 | *width: 22.87498530621841%; 614 | } 615 | .row-fluid .span2 { 616 | width: 14.3646408839779%; 617 | *width: 14.311449394616199%; 618 | } 619 | .row-fluid .span1 { 620 | width: 5.801104972375691%; 621 | *width: 5.747913483013988%; 622 | } 623 | .row-fluid .offset12 { 624 | margin-left: 105.52486187845304%; 625 | *margin-left: 105.41847889972962%; 626 | } 627 | .row-fluid .offset12:first-child { 628 | margin-left: 102.76243093922652%; 629 | *margin-left: 102.6560479605031%; 630 | } 631 | .row-fluid .offset11 { 632 | margin-left: 96.96132596685082%; 633 | *margin-left: 96.8549429881274%; 634 | } 635 | .row-fluid .offset11:first-child { 636 | margin-left: 94.1988950276243%; 637 | *margin-left: 94.09251204890089%; 638 | } 639 | .row-fluid .offset10 { 640 | margin-left: 88.39779005524862%; 641 | *margin-left: 88.2914070765252%; 642 | } 643 | .row-fluid .offset10:first-child { 644 | margin-left: 85.6353591160221%; 645 | *margin-left: 85.52897613729868%; 646 | } 647 | .row-fluid .offset9 { 648 | margin-left: 79.8342541436464%; 649 | *margin-left: 79.72787116492299%; 650 | } 651 | .row-fluid .offset9:first-child { 652 | margin-left: 77.07182320441989%; 653 | *margin-left: 76.96544022569647%; 654 | } 655 | .row-fluid .offset8 { 656 | margin-left: 71.2707182320442%; 657 | *margin-left: 71.16433525332079%; 658 | } 659 | .row-fluid .offset8:first-child { 660 | margin-left: 68.50828729281768%; 661 | *margin-left: 68.40190431409427%; 662 | } 663 | .row-fluid .offset7 { 664 | margin-left: 62.70718232044199%; 665 | *margin-left: 62.600799341718584%; 666 | } 667 | .row-fluid .offset7:first-child { 668 | margin-left: 59.94475138121547%; 669 | *margin-left: 59.838368402492065%; 670 | } 671 | .row-fluid .offset6 { 672 | margin-left: 54.14364640883978%; 673 | *margin-left: 54.037263430116376%; 674 | } 675 | .row-fluid .offset6:first-child { 676 | margin-left: 51.38121546961326%; 677 | *margin-left: 51.27483249088986%; 678 | } 679 | .row-fluid .offset5 { 680 | margin-left: 45.58011049723757%; 681 | *margin-left: 45.47372751851417%; 682 | } 683 | .row-fluid .offset5:first-child { 684 | margin-left: 42.81767955801105%; 685 | *margin-left: 42.71129657928765%; 686 | } 687 | .row-fluid .offset4 { 688 | margin-left: 37.01657458563536%; 689 | *margin-left: 36.91019160691196%; 690 | } 691 | .row-fluid .offset4:first-child { 692 | margin-left: 34.25414364640884%; 693 | *margin-left: 34.14776066768544%; 694 | } 695 | .row-fluid .offset3 { 696 | margin-left: 28.45303867403315%; 697 | *margin-left: 28.346655695309746%; 698 | } 699 | .row-fluid .offset3:first-child { 700 | margin-left: 25.69060773480663%; 701 | *margin-left: 25.584224756083227%; 702 | } 703 | .row-fluid .offset2 { 704 | margin-left: 19.88950276243094%; 705 | *margin-left: 19.783119783707537%; 706 | } 707 | .row-fluid .offset2:first-child { 708 | margin-left: 17.12707182320442%; 709 | *margin-left: 17.02068884448102%; 710 | } 711 | .row-fluid .offset1 { 712 | margin-left: 11.32596685082873%; 713 | *margin-left: 11.219583872105325%; 714 | } 715 | .row-fluid .offset1:first-child { 716 | margin-left: 8.56353591160221%; 717 | *margin-left: 8.457152932878806%; 718 | } 719 | input, 720 | textarea, 721 | .uneditable-input { 722 | margin-left: 0; 723 | } 724 | .controls-row [class*="span"] + [class*="span"] { 725 | margin-left: 20px; 726 | } 727 | input.span12, 728 | textarea.span12, 729 | .uneditable-input.span12 { 730 | width: 710px; 731 | } 732 | input.span11, 733 | textarea.span11, 734 | .uneditable-input.span11 { 735 | width: 648px; 736 | } 737 | input.span10, 738 | textarea.span10, 739 | .uneditable-input.span10 { 740 | width: 586px; 741 | } 742 | input.span9, 743 | textarea.span9, 744 | .uneditable-input.span9 { 745 | width: 524px; 746 | } 747 | input.span8, 748 | textarea.span8, 749 | .uneditable-input.span8 { 750 | width: 462px; 751 | } 752 | input.span7, 753 | textarea.span7, 754 | .uneditable-input.span7 { 755 | width: 400px; 756 | } 757 | input.span6, 758 | textarea.span6, 759 | .uneditable-input.span6 { 760 | width: 338px; 761 | } 762 | input.span5, 763 | textarea.span5, 764 | .uneditable-input.span5 { 765 | width: 276px; 766 | } 767 | input.span4, 768 | textarea.span4, 769 | .uneditable-input.span4 { 770 | width: 214px; 771 | } 772 | input.span3, 773 | textarea.span3, 774 | .uneditable-input.span3 { 775 | width: 152px; 776 | } 777 | input.span2, 778 | textarea.span2, 779 | .uneditable-input.span2 { 780 | width: 90px; 781 | } 782 | input.span1, 783 | textarea.span1, 784 | .uneditable-input.span1 { 785 | width: 28px; 786 | } 787 | } 788 | 789 | @media (max-width: 767px) { 790 | body { 791 | padding-right: 20px; 792 | padding-left: 20px; 793 | } 794 | .navbar-fixed-top, 795 | .navbar-fixed-bottom, 796 | .navbar-static-top { 797 | margin-right: -20px; 798 | margin-left: -20px; 799 | } 800 | .container-fluid { 801 | padding: 0; 802 | } 803 | .dl-horizontal dt { 804 | float: none; 805 | width: auto; 806 | clear: none; 807 | text-align: left; 808 | } 809 | .dl-horizontal dd { 810 | margin-left: 0; 811 | } 812 | .container { 813 | width: auto; 814 | } 815 | .row-fluid { 816 | width: 100%; 817 | } 818 | .row, 819 | .thumbnails { 820 | margin-left: 0; 821 | } 822 | .thumbnails > li { 823 | float: none; 824 | margin-left: 0; 825 | } 826 | [class*="span"], 827 | .uneditable-input[class*="span"], 828 | .row-fluid [class*="span"] { 829 | display: block; 830 | float: none; 831 | width: 100%; 832 | margin-left: 0; 833 | -webkit-box-sizing: border-box; 834 | -moz-box-sizing: border-box; 835 | box-sizing: border-box; 836 | } 837 | .span12, 838 | .row-fluid .span12 { 839 | width: 100%; 840 | -webkit-box-sizing: border-box; 841 | -moz-box-sizing: border-box; 842 | box-sizing: border-box; 843 | } 844 | .row-fluid [class*="offset"]:first-child { 845 | margin-left: 0; 846 | } 847 | .input-large, 848 | .input-xlarge, 849 | .input-xxlarge, 850 | input[class*="span"], 851 | select[class*="span"], 852 | textarea[class*="span"], 853 | .uneditable-input { 854 | display: block; 855 | width: 100%; 856 | min-height: 30px; 857 | -webkit-box-sizing: border-box; 858 | -moz-box-sizing: border-box; 859 | box-sizing: border-box; 860 | } 861 | .input-prepend input, 862 | .input-append input, 863 | .input-prepend input[class*="span"], 864 | .input-append input[class*="span"] { 865 | display: inline-block; 866 | width: auto; 867 | } 868 | .controls-row [class*="span"] + [class*="span"] { 869 | margin-left: 0; 870 | } 871 | .modal { 872 | position: fixed; 873 | top: 20px; 874 | right: 20px; 875 | left: 20px; 876 | width: auto; 877 | margin: 0; 878 | } 879 | .modal.fade { 880 | top: -100px; 881 | } 882 | .modal.fade.in { 883 | top: 20px; 884 | } 885 | } 886 | 887 | @media (max-width: 480px) { 888 | .nav-collapse { 889 | -webkit-transform: translate3d(0, 0, 0); 890 | } 891 | .page-header h1 small { 892 | display: block; 893 | line-height: 20px; 894 | } 895 | input[type="checkbox"], 896 | input[type="radio"] { 897 | border: 1px solid #ccc; 898 | } 899 | .form-horizontal .control-label { 900 | float: none; 901 | width: auto; 902 | padding-top: 0; 903 | text-align: left; 904 | } 905 | .form-horizontal .controls { 906 | margin-left: 0; 907 | } 908 | .form-horizontal .control-list { 909 | padding-top: 0; 910 | } 911 | .form-horizontal .form-actions { 912 | padding-right: 10px; 913 | padding-left: 10px; 914 | } 915 | .media .pull-left, 916 | .media .pull-right { 917 | display: block; 918 | float: none; 919 | margin-bottom: 10px; 920 | } 921 | .media-object { 922 | margin-right: 0; 923 | margin-left: 0; 924 | } 925 | .modal { 926 | top: 10px; 927 | right: 10px; 928 | left: 10px; 929 | } 930 | .modal-header .close { 931 | padding: 10px; 932 | margin: -10px; 933 | } 934 | .carousel-caption { 935 | position: static; 936 | } 937 | } 938 | 939 | @media (max-width: 979px) { 940 | body { 941 | padding-top: 0; 942 | } 943 | .navbar-fixed-top, 944 | .navbar-fixed-bottom { 945 | position: static; 946 | } 947 | .navbar-fixed-top { 948 | margin-bottom: 20px; 949 | } 950 | .navbar-fixed-bottom { 951 | margin-top: 20px; 952 | } 953 | .navbar-fixed-top .navbar-inner, 954 | .navbar-fixed-bottom .navbar-inner { 955 | padding: 5px; 956 | } 957 | .navbar .container { 958 | width: auto; 959 | padding: 0; 960 | } 961 | .navbar .brand { 962 | padding-right: 10px; 963 | padding-left: 10px; 964 | margin: 0 0 0 -5px; 965 | } 966 | .nav-collapse { 967 | clear: both; 968 | } 969 | .nav-collapse .nav { 970 | float: none; 971 | margin: 0 0 10px; 972 | } 973 | .nav-collapse .nav > li { 974 | float: none; 975 | } 976 | .nav-collapse .nav > li > a { 977 | margin-bottom: 2px; 978 | } 979 | .nav-collapse .nav > .divider-vertical { 980 | display: none; 981 | } 982 | .nav-collapse .nav .nav-header { 983 | color: #777777; 984 | text-shadow: none; 985 | } 986 | .nav-collapse .nav > li > a, 987 | .nav-collapse .dropdown-menu a { 988 | padding: 9px 15px; 989 | font-weight: bold; 990 | color: #777777; 991 | -webkit-border-radius: 3px; 992 | -moz-border-radius: 3px; 993 | border-radius: 3px; 994 | } 995 | .nav-collapse .btn { 996 | padding: 4px 10px 4px; 997 | font-weight: normal; 998 | -webkit-border-radius: 4px; 999 | -moz-border-radius: 4px; 1000 | border-radius: 4px; 1001 | } 1002 | .nav-collapse .dropdown-menu li + li a { 1003 | margin-bottom: 2px; 1004 | } 1005 | .nav-collapse .nav > li > a:hover, 1006 | .nav-collapse .dropdown-menu a:hover { 1007 | background-color: #f2f2f2; 1008 | } 1009 | .navbar-inverse .nav-collapse .nav > li > a, 1010 | .navbar-inverse .nav-collapse .dropdown-menu a { 1011 | color: #999999; 1012 | } 1013 | .navbar-inverse .nav-collapse .nav > li > a:hover, 1014 | .navbar-inverse .nav-collapse .dropdown-menu a:hover { 1015 | background-color: #111111; 1016 | } 1017 | .nav-collapse.in .btn-group { 1018 | padding: 0; 1019 | margin-top: 5px; 1020 | } 1021 | .nav-collapse .dropdown-menu { 1022 | position: static; 1023 | top: auto; 1024 | left: auto; 1025 | display: none; 1026 | float: none; 1027 | max-width: none; 1028 | padding: 0; 1029 | margin: 0 15px; 1030 | background-color: transparent; 1031 | border: none; 1032 | -webkit-border-radius: 0; 1033 | -moz-border-radius: 0; 1034 | border-radius: 0; 1035 | -webkit-box-shadow: none; 1036 | -moz-box-shadow: none; 1037 | box-shadow: none; 1038 | } 1039 | .nav-collapse .open > .dropdown-menu { 1040 | display: block; 1041 | } 1042 | .nav-collapse .dropdown-menu:before, 1043 | .nav-collapse .dropdown-menu:after { 1044 | display: none; 1045 | } 1046 | .nav-collapse .dropdown-menu .divider { 1047 | display: none; 1048 | } 1049 | .nav-collapse .nav > li > .dropdown-menu:before, 1050 | .nav-collapse .nav > li > .dropdown-menu:after { 1051 | display: none; 1052 | } 1053 | .nav-collapse .navbar-form, 1054 | .nav-collapse .navbar-search { 1055 | float: none; 1056 | padding: 10px 15px; 1057 | margin: 10px 0; 1058 | border-top: 1px solid #f2f2f2; 1059 | border-bottom: 1px solid #f2f2f2; 1060 | -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1); 1061 | -moz-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1); 1062 | box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1); 1063 | } 1064 | .navbar-inverse .nav-collapse .navbar-form, 1065 | .navbar-inverse .nav-collapse .navbar-search { 1066 | border-top-color: #111111; 1067 | border-bottom-color: #111111; 1068 | } 1069 | .navbar .nav-collapse .nav.pull-right { 1070 | float: none; 1071 | margin-left: 0; 1072 | } 1073 | .nav-collapse, 1074 | .nav-collapse.collapse { 1075 | height: 0; 1076 | overflow: hidden; 1077 | } 1078 | .navbar .btn-navbar { 1079 | display: block; 1080 | } 1081 | .navbar-static .navbar-inner { 1082 | padding-right: 10px; 1083 | padding-left: 10px; 1084 | } 1085 | } 1086 | 1087 | @media (min-width: 980px) { 1088 | .nav-collapse.collapse { 1089 | height: auto !important; 1090 | overflow: visible !important; 1091 | } 1092 | } 1093 | -------------------------------------------------------------------------------- /example/lib/bootstrap/img/glyphicons-halflings-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jimrhoskins/angular-parse/fac11b2c7d29020c4ddabf2504841f968be25bd0/example/lib/bootstrap/img/glyphicons-halflings-white.png -------------------------------------------------------------------------------- /example/lib/bootstrap/img/glyphicons-halflings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jimrhoskins/angular-parse/fac11b2c7d29020c4ddabf2504841f968be25bd0/example/lib/bootstrap/img/glyphicons-halflings.png -------------------------------------------------------------------------------- /example/parse/cloud/main.js: -------------------------------------------------------------------------------- 1 | 2 | Parse.Cloud.define("hello", function(request, response) { 3 | return response.success("Hello coffee!"); 4 | }); 5 | 6 | Parse.Cloud.beforeSave('Post', function(req, res) { 7 | var post, user; 8 | post = req.object; 9 | user = req.user; 10 | if (!user) { 11 | return res.error("You must be signed in to post."); 12 | } 13 | if (!post.get("title").length) { 14 | return res.error("You must include a title"); 15 | } 16 | if (!post.get("body").length) { 17 | return res.error("You must include a body"); 18 | } 19 | post.set('commentCount', post.get('commentCount') || 0); 20 | post.set('author', user); 21 | return res.success(); 22 | }); 23 | 24 | Parse.Cloud.beforeSave('Comment', function(req, res) { 25 | var comment, user; 26 | comment = req.object; 27 | user = req.user; 28 | if (!user) { 29 | return res.error("You must be signed in to post."); 30 | } 31 | if (!comment.get("body").length) { 32 | return res.error("You must include a body"); 33 | } 34 | comment.set('author', user); 35 | return res.success(); 36 | }); 37 | -------------------------------------------------------------------------------- /example/parse/config/global.json: -------------------------------------------------------------------------------- 1 | { 2 | "applications": { 3 | "Forum": { 4 | "applicationId": "x58FvFYxi4SDIRdJck4hFVv1huo8F409UnfERfUU", 5 | "masterKey": "NdmAC7MEaW1v8wzYyO6z8hqiyvmqPIcJKgeargKj" 6 | }, 7 | "_default": { 8 | "link": "Forum" 9 | } 10 | }, 11 | "global": { 12 | "parseVersion": "1.1.15" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /example/parse/src/main.coffee: -------------------------------------------------------------------------------- 1 | 2 | Parse.Cloud.define "hello", (request, response) -> 3 | response.success("Hello coffee!") 4 | 5 | Parse.Cloud.beforeSave 'Post', (req, res) -> 6 | post = req.object 7 | user = req.user 8 | 9 | unless user 10 | return res.error "You must be signed in to post." 11 | 12 | unless post.get("title").length 13 | return res.error "You must include a title" 14 | 15 | unless post.get("body").length 16 | return res.error "You must include a body" 17 | 18 | post.set('commentCount', post.get('commentCount') || 0) 19 | post.set('author', user) 20 | 21 | res.success() 22 | 23 | Parse.Cloud.beforeSave 'Comment', (req, res) -> 24 | comment = req.object 25 | user = req.user 26 | 27 | unless user 28 | return res.error "You must be signed in to post." 29 | 30 | unless comment.get("body").length 31 | return res.error "You must include a body" 32 | 33 | comment.set('author', user) 34 | 35 | res.success() 36 | 37 | -------------------------------------------------------------------------------- /example/partials/detail.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | « Forum 4 |
5 | 6 |

{{post.title}}

7 |
8 | Asked by {{post.author.username}} 9 |
10 | {{post.body}} 11 |
12 | 13 | 14 | {{tag}} 15 | 16 | 17 |

Comments

18 |
19 |
20 | 27 |
28 | 29 |
30 |
31 |
32 |
33 |
Avatar
34 |
35 |
{{comment.author.username}}
36 | {{comment.body}} 37 |
38 |
39 |
40 | -------------------------------------------------------------------------------- /example/partials/form.html: -------------------------------------------------------------------------------- 1 |
2 | 9 | 10 |
11 | 17 | 18 | 25 | 26 | 27 |
28 | 29 |
30 |
31 |
32 | -------------------------------------------------------------------------------- /example/partials/list.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

Forum

4 |
5 |
6 | 7 | 11 |
12 |
13 | Avatar 14 |
15 |
16 | 17 | {{post.title}} 18 | 19 |
20 | Posted by 21 | {{post.author.username}} 22 |
23 |
24 |
25 | {{post.commentCount}} 26 |
27 |
28 | {{cars}} 29 |
30 | 31 | -------------------------------------------------------------------------------- /example/partials/register.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |

Register

5 |
6 |
7 | Error: {{errorMessage}} 8 |
9 | 10 |
11 | 12 |
13 | 14 |
15 |
16 | 17 |
18 | 19 |
20 | 21 |
22 |
23 | 24 |
25 | 26 |
27 | 29 |
30 |
31 | 32 | 33 |
34 | 35 |
36 |
37 | 38 |
39 |
40 | -------------------------------------------------------------------------------- /example/partials/sign-in.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |

Sign In

5 |
6 |
7 | Error: {{errorMessage}} 8 |
9 | 10 |
11 | 12 |
13 | 14 |
15 |
16 | 17 |
18 | 19 |
20 | 21 |
22 |
23 | 24 | 25 |
26 | 27 |
28 |
29 | 30 |
31 |
32 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Testacular configuration 2 | // Generated on Tue Dec 18 2012 20:42:55 GMT-0500 (EST) 3 | 4 | 5 | // base path, that will be used to resolve files and exclude 6 | basePath = '.'; 7 | 8 | preprocessors = { 9 | "**/*.coffee": "coffee" 10 | } 11 | 12 | // list of files / patterns to load in the browser 13 | files = [ 14 | JASMINE, 15 | JASMINE_ADAPTER, 16 | 'lib/angular/angular.js', 17 | 'lib/angular/angular-*.js', 18 | 'test/support/angular-mocks.js', 19 | 'test/support/jasmine.async.js', 20 | 'angular-parse.js', 21 | 'test/unit/**/*.coffee' 22 | ]; 23 | 24 | 25 | // list of files to exclude 26 | exclude = [ 27 | 28 | ]; 29 | 30 | 31 | // test results reporter to use 32 | // possible values: 'dots', 'progress', 'junit' 33 | reporters = ['progress']; 34 | 35 | 36 | // web server port 37 | port = 9101; 38 | 39 | 40 | // cli runner port 41 | runnerPort = 9100; 42 | 43 | 44 | // enable / disable colors in the output (reporters and logs) 45 | colors = true; 46 | 47 | 48 | // level of logging 49 | // possible values: LOG_DISABLE || LOG_ERROR || LOG_WARN || LOG_INFO || LOG_DEBUG 50 | logLevel = LOG_INFO; 51 | 52 | 53 | // enable / disable watching file and executing tests whenever any file changes 54 | autoWatch = true; 55 | 56 | 57 | // Start these browsers, currently available: 58 | // - Chrome 59 | // - ChromeCanary 60 | // - Firefox 61 | // - Opera 62 | // - Safari (only Mac) 63 | // - PhantomJS 64 | // - IE (only Windows) 65 | browsers = ['PhantomJS']; 66 | 67 | 68 | // If browser does not capture in given timeout [ms], kill it 69 | captureTimeout = 5000; 70 | 71 | 72 | // Continuous Integration mode 73 | // if true, it capture browsers, run tests and exit 74 | singleRun = false; 75 | -------------------------------------------------------------------------------- /lib/angular/angular-cookies.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license AngularJS v1.0.2 3 | * (c) 2010-2012 Google, Inc. http://angularjs.org 4 | * License: MIT 5 | */ 6 | (function(window, angular, undefined) { 7 | 'use strict'; 8 | 9 | /** 10 | * @ngdoc overview 11 | * @name ngCookies 12 | */ 13 | 14 | 15 | angular.module('ngCookies', ['ng']). 16 | /** 17 | * @ngdoc object 18 | * @name ngCookies.$cookies 19 | * @requires $browser 20 | * 21 | * @description 22 | * Provides read/write access to browser's cookies. 23 | * 24 | * Only a simple Object is exposed and by adding or removing properties to/from 25 | * this object, new cookies are created/deleted at the end of current $eval. 26 | * 27 | * @example 28 | */ 29 | factory('$cookies', ['$rootScope', '$browser', function ($rootScope, $browser) { 30 | var cookies = {}, 31 | lastCookies = {}, 32 | lastBrowserCookies, 33 | runEval = false, 34 | copy = angular.copy, 35 | isUndefined = angular.isUndefined; 36 | 37 | //creates a poller fn that copies all cookies from the $browser to service & inits the service 38 | $browser.addPollFn(function() { 39 | var currentCookies = $browser.cookies(); 40 | if (lastBrowserCookies != currentCookies) { //relies on browser.cookies() impl 41 | lastBrowserCookies = currentCookies; 42 | copy(currentCookies, lastCookies); 43 | copy(currentCookies, cookies); 44 | if (runEval) $rootScope.$apply(); 45 | } 46 | })(); 47 | 48 | runEval = true; 49 | 50 | //at the end of each eval, push cookies 51 | //TODO: this should happen before the "delayed" watches fire, because if some cookies are not 52 | // strings or browser refuses to store some cookies, we update the model in the push fn. 53 | $rootScope.$watch(push); 54 | 55 | return cookies; 56 | 57 | 58 | /** 59 | * Pushes all the cookies from the service to the browser and verifies if all cookies were stored. 60 | */ 61 | function push() { 62 | var name, 63 | value, 64 | browserCookies, 65 | updated; 66 | 67 | //delete any cookies deleted in $cookies 68 | for (name in lastCookies) { 69 | if (isUndefined(cookies[name])) { 70 | $browser.cookies(name, undefined); 71 | } 72 | } 73 | 74 | //update all cookies updated in $cookies 75 | for(name in cookies) { 76 | value = cookies[name]; 77 | if (!angular.isString(value)) { 78 | if (angular.isDefined(lastCookies[name])) { 79 | cookies[name] = lastCookies[name]; 80 | } else { 81 | delete cookies[name]; 82 | } 83 | } else if (value !== lastCookies[name]) { 84 | $browser.cookies(name, value); 85 | updated = true; 86 | } 87 | } 88 | 89 | //verify what was actually stored 90 | if (updated){ 91 | updated = false; 92 | browserCookies = $browser.cookies(); 93 | 94 | for (name in cookies) { 95 | if (cookies[name] !== browserCookies[name]) { 96 | //delete or reset all cookies that the browser dropped from $cookies 97 | if (isUndefined(browserCookies[name])) { 98 | delete cookies[name]; 99 | } else { 100 | cookies[name] = browserCookies[name]; 101 | } 102 | updated = true; 103 | } 104 | } 105 | } 106 | } 107 | }]). 108 | 109 | 110 | /** 111 | * @ngdoc object 112 | * @name ngCookies.$cookieStore 113 | * @requires $cookies 114 | * 115 | * @description 116 | * Provides a key-value (string-object) storage, that is backed by session cookies. 117 | * Objects put or retrieved from this storage are automatically serialized or 118 | * deserialized by angular's toJson/fromJson. 119 | * @example 120 | */ 121 | factory('$cookieStore', ['$cookies', function($cookies) { 122 | 123 | return { 124 | /** 125 | * @ngdoc method 126 | * @name ngCookies.$cookieStore#get 127 | * @methodOf ngCookies.$cookieStore 128 | * 129 | * @description 130 | * Returns the value of given cookie key 131 | * 132 | * @param {string} key Id to use for lookup. 133 | * @returns {Object} Deserialized cookie value. 134 | */ 135 | get: function(key) { 136 | return angular.fromJson($cookies[key]); 137 | }, 138 | 139 | /** 140 | * @ngdoc method 141 | * @name ngCookies.$cookieStore#put 142 | * @methodOf ngCookies.$cookieStore 143 | * 144 | * @description 145 | * Sets a value for given cookie key 146 | * 147 | * @param {string} key Id for the `value`. 148 | * @param {Object} value Value to be stored. 149 | */ 150 | put: function(key, value) { 151 | $cookies[key] = angular.toJson(value); 152 | }, 153 | 154 | /** 155 | * @ngdoc method 156 | * @name ngCookies.$cookieStore#remove 157 | * @methodOf ngCookies.$cookieStore 158 | * 159 | * @description 160 | * Remove given cookie 161 | * 162 | * @param {string} key Id of the key-value pair to delete. 163 | */ 164 | remove: function(key) { 165 | delete $cookies[key]; 166 | } 167 | }; 168 | 169 | }]); 170 | 171 | })(window, window.angular); 172 | -------------------------------------------------------------------------------- /lib/angular/angular-cookies.min.js: -------------------------------------------------------------------------------- 1 | /* 2 | AngularJS v1.0.2 3 | (c) 2010-2012 Google, Inc. http://angularjs.org 4 | License: MIT 5 | */ 6 | (function(m,f,l){'use strict';f.module("ngCookies",["ng"]).factory("$cookies",["$rootScope","$browser",function(d,c){var b={},g={},h,i=!1,j=f.copy,k=f.isUndefined;c.addPollFn(function(){var a=c.cookies();h!=a&&(h=a,j(a,g),j(a,b),i&&d.$apply())})();i=!0;d.$watch(function(){var a,e,d;for(a in g)k(b[a])&&c.cookies(a,l);for(a in b)e=b[a],f.isString(e)?e!==g[a]&&(c.cookies(a,e),d=!0):f.isDefined(g[a])?b[a]=g[a]:delete b[a];if(d)for(a in e=c.cookies(),b)b[a]!==e[a]&&(k(e[a])?delete b[a]:b[a]=e[a])});return b}]).factory("$cookieStore", 7 | ["$cookies",function(d){return{get:function(c){return f.fromJson(d[c])},put:function(c,b){d[c]=f.toJson(b)},remove:function(c){delete d[c]}}}])})(window,window.angular); 8 | -------------------------------------------------------------------------------- /lib/angular/angular-loader.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license AngularJS v1.0.2 3 | * (c) 2010-2012 Google, Inc. http://angularjs.org 4 | * License: MIT 5 | */ 6 | 7 | ( 8 | 9 | /** 10 | * @ngdoc interface 11 | * @name angular.Module 12 | * @description 13 | * 14 | * Interface for configuring angular {@link angular.module modules}. 15 | */ 16 | 17 | function setupModuleLoader(window) { 18 | 19 | function ensure(obj, name, factory) { 20 | return obj[name] || (obj[name] = factory()); 21 | } 22 | 23 | return ensure(ensure(window, 'angular', Object), 'module', function() { 24 | /** @type {Object.} */ 25 | var modules = {}; 26 | 27 | /** 28 | * @ngdoc function 29 | * @name angular.module 30 | * @description 31 | * 32 | * The `angular.module` is a global place for creating and registering Angular modules. All 33 | * modules (angular core or 3rd party) that should be available to an application must be 34 | * registered using this mechanism. 35 | * 36 | * 37 | * # Module 38 | * 39 | * A module is a collocation of services, directives, filters, and configure information. Module 40 | * is used to configure the {@link AUTO.$injector $injector}. 41 | * 42 | *
 43 |      * // Create a new module
 44 |      * var myModule = angular.module('myModule', []);
 45 |      *
 46 |      * // register a new service
 47 |      * myModule.value('appName', 'MyCoolApp');
 48 |      *
 49 |      * // configure existing services inside initialization blocks.
 50 |      * myModule.config(function($locationProvider) {
 51 | 'use strict';
 52 |      *   // Configure existing providers
 53 |      *   $locationProvider.hashPrefix('!');
 54 |      * });
 55 |      * 
56 | * 57 | * Then you can create an injector and load your modules like this: 58 | * 59 | *
 60 |      * var injector = angular.injector(['ng', 'MyModule'])
 61 |      * 
62 | * 63 | * However it's more likely that you'll just use 64 | * {@link ng.directive:ngApp ngApp} or 65 | * {@link angular.bootstrap} to simplify this process for you. 66 | * 67 | * @param {!string} name The name of the module to create or retrieve. 68 | * @param {Array.=} requires If specified then new module is being created. If unspecified then the 69 | * the module is being retrieved for further configuration. 70 | * @param {Function} configFn Option configuration function for the module. Same as 71 | * {@link angular.Module#config Module#config()}. 72 | * @returns {module} new module with the {@link angular.Module} api. 73 | */ 74 | return function module(name, requires, configFn) { 75 | if (requires && modules.hasOwnProperty(name)) { 76 | modules[name] = null; 77 | } 78 | return ensure(modules, name, function() { 79 | if (!requires) { 80 | throw Error('No module: ' + name); 81 | } 82 | 83 | /** @type {!Array.>} */ 84 | var invokeQueue = []; 85 | 86 | /** @type {!Array.} */ 87 | var runBlocks = []; 88 | 89 | var config = invokeLater('$injector', 'invoke'); 90 | 91 | /** @type {angular.Module} */ 92 | var moduleInstance = { 93 | // Private state 94 | _invokeQueue: invokeQueue, 95 | _runBlocks: runBlocks, 96 | 97 | /** 98 | * @ngdoc property 99 | * @name angular.Module#requires 100 | * @propertyOf angular.Module 101 | * @returns {Array.} List of module names which must be loaded before this module. 102 | * @description 103 | * Holds the list of modules which the injector will load before the current module is loaded. 104 | */ 105 | requires: requires, 106 | 107 | /** 108 | * @ngdoc property 109 | * @name angular.Module#name 110 | * @propertyOf angular.Module 111 | * @returns {string} Name of the module. 112 | * @description 113 | */ 114 | name: name, 115 | 116 | 117 | /** 118 | * @ngdoc method 119 | * @name angular.Module#provider 120 | * @methodOf angular.Module 121 | * @param {string} name service name 122 | * @param {Function} providerType Construction function for creating new instance of the service. 123 | * @description 124 | * See {@link AUTO.$provide#provider $provide.provider()}. 125 | */ 126 | provider: invokeLater('$provide', 'provider'), 127 | 128 | /** 129 | * @ngdoc method 130 | * @name angular.Module#factory 131 | * @methodOf angular.Module 132 | * @param {string} name service name 133 | * @param {Function} providerFunction Function for creating new instance of the service. 134 | * @description 135 | * See {@link AUTO.$provide#factory $provide.factory()}. 136 | */ 137 | factory: invokeLater('$provide', 'factory'), 138 | 139 | /** 140 | * @ngdoc method 141 | * @name angular.Module#service 142 | * @methodOf angular.Module 143 | * @param {string} name service name 144 | * @param {Function} constructor A constructor function that will be instantiated. 145 | * @description 146 | * See {@link AUTO.$provide#service $provide.service()}. 147 | */ 148 | service: invokeLater('$provide', 'service'), 149 | 150 | /** 151 | * @ngdoc method 152 | * @name angular.Module#value 153 | * @methodOf angular.Module 154 | * @param {string} name service name 155 | * @param {*} object Service instance object. 156 | * @description 157 | * See {@link AUTO.$provide#value $provide.value()}. 158 | */ 159 | value: invokeLater('$provide', 'value'), 160 | 161 | /** 162 | * @ngdoc method 163 | * @name angular.Module#constant 164 | * @methodOf angular.Module 165 | * @param {string} name constant name 166 | * @param {*} object Constant value. 167 | * @description 168 | * Because the constant are fixed, they get applied before other provide methods. 169 | * See {@link AUTO.$provide#constant $provide.constant()}. 170 | */ 171 | constant: invokeLater('$provide', 'constant', 'unshift'), 172 | 173 | /** 174 | * @ngdoc method 175 | * @name angular.Module#filter 176 | * @methodOf angular.Module 177 | * @param {string} name Filter name. 178 | * @param {Function} filterFactory Factory function for creating new instance of filter. 179 | * @description 180 | * See {@link ng.$filterProvider#register $filterProvider.register()}. 181 | */ 182 | filter: invokeLater('$filterProvider', 'register'), 183 | 184 | /** 185 | * @ngdoc method 186 | * @name angular.Module#controller 187 | * @methodOf angular.Module 188 | * @param {string} name Controller name. 189 | * @param {Function} constructor Controller constructor function. 190 | * @description 191 | * See {@link ng.$controllerProvider#register $controllerProvider.register()}. 192 | */ 193 | controller: invokeLater('$controllerProvider', 'register'), 194 | 195 | /** 196 | * @ngdoc method 197 | * @name angular.Module#directive 198 | * @methodOf angular.Module 199 | * @param {string} name directive name 200 | * @param {Function} directiveFactory Factory function for creating new instance of 201 | * directives. 202 | * @description 203 | * See {@link ng.$compileProvider#directive $compileProvider.directive()}. 204 | */ 205 | directive: invokeLater('$compileProvider', 'directive'), 206 | 207 | /** 208 | * @ngdoc method 209 | * @name angular.Module#config 210 | * @methodOf angular.Module 211 | * @param {Function} configFn Execute this function on module load. Useful for service 212 | * configuration. 213 | * @description 214 | * Use this method to register work which needs to be performed on module loading. 215 | */ 216 | config: config, 217 | 218 | /** 219 | * @ngdoc method 220 | * @name angular.Module#run 221 | * @methodOf angular.Module 222 | * @param {Function} initializationFn Execute this function after injector creation. 223 | * Useful for application initialization. 224 | * @description 225 | * Use this method to register work which needs to be performed when the injector with 226 | * with the current module is finished loading. 227 | */ 228 | run: function(block) { 229 | runBlocks.push(block); 230 | return this; 231 | } 232 | }; 233 | 234 | if (configFn) { 235 | config(configFn); 236 | } 237 | 238 | return moduleInstance; 239 | 240 | /** 241 | * @param {string} provider 242 | * @param {string} method 243 | * @param {String=} insertMethod 244 | * @returns {angular.Module} 245 | */ 246 | function invokeLater(provider, method, insertMethod) { 247 | return function() { 248 | invokeQueue[insertMethod || 'push']([provider, method, arguments]); 249 | return moduleInstance; 250 | } 251 | } 252 | }); 253 | }; 254 | }); 255 | 256 | } 257 | )(window); 258 | 259 | /** 260 | * Closure compiler type information 261 | * 262 | * @typedef { { 263 | * requires: !Array., 264 | * invokeQueue: !Array.>, 265 | * 266 | * service: function(string, Function):angular.Module, 267 | * factory: function(string, Function):angular.Module, 268 | * value: function(string, *):angular.Module, 269 | * 270 | * filter: function(string, Function):angular.Module, 271 | * 272 | * init: function(Function):angular.Module 273 | * } } 274 | */ 275 | angular.Module; 276 | 277 | -------------------------------------------------------------------------------- /lib/angular/angular-loader.min.js: -------------------------------------------------------------------------------- 1 | /* 2 | AngularJS v1.0.2 3 | (c) 2010-2012 Google, Inc. http://angularjs.org 4 | License: MIT 5 | */ 6 | (function(i){'use strict';function d(c,b,e){return c[b]||(c[b]=e())}return d(d(i,"angular",Object),"module",function(){var c={};return function(b,e,f){e&&c.hasOwnProperty(b)&&(c[b]=null);return d(c,b,function(){function a(a,b,d){return function(){c[d||"push"]([a,b,arguments]);return g}}if(!e)throw Error("No module: "+b);var c=[],d=[],h=a("$injector","invoke"),g={_invokeQueue:c,_runBlocks:d,requires:e,name:b,provider:a("$provide","provider"),factory:a("$provide","factory"),service:a("$provide","service"), 7 | value:a("$provide","value"),constant:a("$provide","constant","unshift"),filter:a("$filterProvider","register"),controller:a("$controllerProvider","register"),directive:a("$compileProvider","directive"),config:h,run:function(a){d.push(a);return this}};f&&h(f);return g})}})})(window); 8 | -------------------------------------------------------------------------------- /lib/angular/angular-resource.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license AngularJS v1.0.2 3 | * (c) 2010-2012 Google, Inc. http://angularjs.org 4 | * License: MIT 5 | */ 6 | (function(window, angular, undefined) { 7 | 'use strict'; 8 | 9 | /** 10 | * @ngdoc overview 11 | * @name ngResource 12 | * @description 13 | */ 14 | 15 | /** 16 | * @ngdoc object 17 | * @name ngResource.$resource 18 | * @requires $http 19 | * 20 | * @description 21 | * A factory which creates a resource object that lets you interact with 22 | * [RESTful](http://en.wikipedia.org/wiki/Representational_State_Transfer) server-side data sources. 23 | * 24 | * The returned resource object has action methods which provide high-level behaviors without 25 | * the need to interact with the low level {@link ng.$http $http} service. 26 | * 27 | * @param {string} url A parameterized URL template with parameters prefixed by `:` as in 28 | * `/user/:username`. 29 | * 30 | * @param {Object=} paramDefaults Default values for `url` parameters. These can be overridden in 31 | * `actions` methods. 32 | * 33 | * Each key value in the parameter object is first bound to url template if present and then any 34 | * excess keys are appended to the url search query after the `?`. 35 | * 36 | * Given a template `/path/:verb` and parameter `{verb:'greet', salutation:'Hello'}` results in 37 | * URL `/path/greet?salutation=Hello`. 38 | * 39 | * If the parameter value is prefixed with `@` then the value of that parameter is extracted from 40 | * the data object (useful for non-GET operations). 41 | * 42 | * @param {Object.=} actions Hash with declaration of custom action that should extend the 43 | * default set of resource actions. The declaration should be created in the following format: 44 | * 45 | * {action1: {method:?, params:?, isArray:?}, 46 | * action2: {method:?, params:?, isArray:?}, 47 | * ...} 48 | * 49 | * Where: 50 | * 51 | * - `action` – {string} – The name of action. This name becomes the name of the method on your 52 | * resource object. 53 | * - `method` – {string} – HTTP request method. Valid methods are: `GET`, `POST`, `PUT`, `DELETE`, 54 | * and `JSONP` 55 | * - `params` – {object=} – Optional set of pre-bound parameters for this action. 56 | * - isArray – {boolean=} – If true then the returned object for this action is an array, see 57 | * `returns` section. 58 | * 59 | * @returns {Object} A resource "class" object with methods for the default set of resource actions 60 | * optionally extended with custom `actions`. The default set contains these actions: 61 | * 62 | * { 'get': {method:'GET'}, 63 | * 'save': {method:'POST'}, 64 | * 'query': {method:'GET', isArray:true}, 65 | * 'remove': {method:'DELETE'}, 66 | * 'delete': {method:'DELETE'} }; 67 | * 68 | * Calling these methods invoke an {@link ng.$http} with the specified http method, 69 | * destination and parameters. When the data is returned from the server then the object is an 70 | * instance of the resource class `save`, `remove` and `delete` actions are available on it as 71 | * methods with the `$` prefix. This allows you to easily perform CRUD operations (create, read, 72 | * update, delete) on server-side data like this: 73 | *
 74 |         var User = $resource('/user/:userId', {userId:'@id'});
 75 |         var user = User.get({userId:123}, function() {
 76 |           user.abc = true;
 77 |           user.$save();
 78 |         });
 79 |      
80 | * 81 | * It is important to realize that invoking a $resource object method immediately returns an 82 | * empty reference (object or array depending on `isArray`). Once the data is returned from the 83 | * server the existing reference is populated with the actual data. This is a useful trick since 84 | * usually the resource is assigned to a model which is then rendered by the view. Having an empty 85 | * object results in no rendering, once the data arrives from the server then the object is 86 | * populated with the data and the view automatically re-renders itself showing the new data. This 87 | * means that in most case one never has to write a callback function for the action methods. 88 | * 89 | * The action methods on the class object or instance object can be invoked with the following 90 | * parameters: 91 | * 92 | * - HTTP GET "class" actions: `Resource.action([parameters], [success], [error])` 93 | * - non-GET "class" actions: `Resource.action([parameters], postData, [success], [error])` 94 | * - non-GET instance actions: `instance.$action([parameters], [success], [error])` 95 | * 96 | * 97 | * @example 98 | * 99 | * # Credit card resource 100 | * 101 | *
102 |      // Define CreditCard class
103 |      var CreditCard = $resource('/user/:userId/card/:cardId',
104 |       {userId:123, cardId:'@id'}, {
105 |        charge: {method:'POST', params:{charge:true}}
106 |       });
107 | 
108 |      // We can retrieve a collection from the server
109 |      var cards = CreditCard.query(function() {
110 |        // GET: /user/123/card
111 |        // server returns: [ {id:456, number:'1234', name:'Smith'} ];
112 | 
113 |        var card = cards[0];
114 |        // each item is an instance of CreditCard
115 |        expect(card instanceof CreditCard).toEqual(true);
116 |        card.name = "J. Smith";
117 |        // non GET methods are mapped onto the instances
118 |        card.$save();
119 |        // POST: /user/123/card/456 {id:456, number:'1234', name:'J. Smith'}
120 |        // server returns: {id:456, number:'1234', name: 'J. Smith'};
121 | 
122 |        // our custom method is mapped as well.
123 |        card.$charge({amount:9.99});
124 |        // POST: /user/123/card/456?amount=9.99&charge=true {id:456, number:'1234', name:'J. Smith'}
125 |      });
126 | 
127 |      // we can create an instance as well
128 |      var newCard = new CreditCard({number:'0123'});
129 |      newCard.name = "Mike Smith";
130 |      newCard.$save();
131 |      // POST: /user/123/card {number:'0123', name:'Mike Smith'}
132 |      // server returns: {id:789, number:'01234', name: 'Mike Smith'};
133 |      expect(newCard.id).toEqual(789);
134 |  * 
135 | * 136 | * The object returned from this function execution is a resource "class" which has "static" method 137 | * for each action in the definition. 138 | * 139 | * Calling these methods invoke `$http` on the `url` template with the given `method` and `params`. 140 | * When the data is returned from the server then the object is an instance of the resource type and 141 | * all of the non-GET methods are available with `$` prefix. This allows you to easily support CRUD 142 | * operations (create, read, update, delete) on server-side data. 143 | 144 |
145 |      var User = $resource('/user/:userId', {userId:'@id'});
146 |      var user = User.get({userId:123}, function() {
147 |        user.abc = true;
148 |        user.$save();
149 |      });
150 |    
151 | * 152 | * It's worth noting that the success callback for `get`, `query` and other method gets passed 153 | * in the response that came from the server as well as $http header getter function, so one 154 | * could rewrite the above example and get access to http headers as: 155 | * 156 |
157 |      var User = $resource('/user/:userId', {userId:'@id'});
158 |      User.get({userId:123}, function(u, getResponseHeaders){
159 |        u.abc = true;
160 |        u.$save(function(u, putResponseHeaders) {
161 |          //u => saved user object
162 |          //putResponseHeaders => $http header getter
163 |        });
164 |      });
165 |    
166 | 167 | * # Buzz client 168 | 169 | Let's look at what a buzz client created with the `$resource` service looks like: 170 | 171 | 172 | 192 | 193 |
194 | 195 | 196 |
197 |
198 |

199 | 200 | {{item.actor.name}} 201 | Expand replies: {{item.links.replies[0].count}} 202 |

203 | {{item.object.content | html}} 204 |
205 | 206 | {{reply.actor.name}}: {{reply.content | html}} 207 |
208 |
209 |
210 |
211 | 212 | 213 |
214 | */ 215 | angular.module('ngResource', ['ng']). 216 | factory('$resource', ['$http', '$parse', function($http, $parse) { 217 | var DEFAULT_ACTIONS = { 218 | 'get': {method:'GET'}, 219 | 'save': {method:'POST'}, 220 | 'query': {method:'GET', isArray:true}, 221 | 'remove': {method:'DELETE'}, 222 | 'delete': {method:'DELETE'} 223 | }; 224 | var noop = angular.noop, 225 | forEach = angular.forEach, 226 | extend = angular.extend, 227 | copy = angular.copy, 228 | isFunction = angular.isFunction, 229 | getter = function(obj, path) { 230 | return $parse(path)(obj); 231 | }; 232 | 233 | /** 234 | * We need our custom mehtod because encodeURIComponent is too agressive and doesn't follow 235 | * http://www.ietf.org/rfc/rfc3986.txt with regards to the character set (pchar) allowed in path 236 | * segments: 237 | * segment = *pchar 238 | * pchar = unreserved / pct-encoded / sub-delims / ":" / "@" 239 | * pct-encoded = "%" HEXDIG HEXDIG 240 | * unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" 241 | * sub-delims = "!" / "$" / "&" / "'" / "(" / ")" 242 | * / "*" / "+" / "," / ";" / "=" 243 | */ 244 | function encodeUriSegment(val) { 245 | return encodeUriQuery(val, true). 246 | replace(/%26/gi, '&'). 247 | replace(/%3D/gi, '='). 248 | replace(/%2B/gi, '+'); 249 | } 250 | 251 | 252 | /** 253 | * This method is intended for encoding *key* or *value* parts of query component. We need a custom 254 | * method becuase encodeURIComponent is too agressive and encodes stuff that doesn't have to be 255 | * encoded per http://tools.ietf.org/html/rfc3986: 256 | * query = *( pchar / "/" / "?" ) 257 | * pchar = unreserved / pct-encoded / sub-delims / ":" / "@" 258 | * unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" 259 | * pct-encoded = "%" HEXDIG HEXDIG 260 | * sub-delims = "!" / "$" / "&" / "'" / "(" / ")" 261 | * / "*" / "+" / "," / ";" / "=" 262 | */ 263 | function encodeUriQuery(val, pctEncodeSpaces) { 264 | return encodeURIComponent(val). 265 | replace(/%40/gi, '@'). 266 | replace(/%3A/gi, ':'). 267 | replace(/%24/g, '$'). 268 | replace(/%2C/gi, ','). 269 | replace((pctEncodeSpaces ? null : /%20/g), '+'); 270 | } 271 | 272 | function Route(template, defaults) { 273 | this.template = template = template + '#'; 274 | this.defaults = defaults || {}; 275 | var urlParams = this.urlParams = {}; 276 | forEach(template.split(/\W/), function(param){ 277 | if (param && template.match(new RegExp("[^\\\\]:" + param + "\\W"))) { 278 | urlParams[param] = true; 279 | } 280 | }); 281 | this.template = template.replace(/\\:/g, ':'); 282 | } 283 | 284 | Route.prototype = { 285 | url: function(params) { 286 | var self = this, 287 | url = this.template, 288 | encodedVal; 289 | 290 | params = params || {}; 291 | forEach(this.urlParams, function(_, urlParam){ 292 | encodedVal = encodeUriSegment(params[urlParam] || self.defaults[urlParam] || ""); 293 | url = url.replace(new RegExp(":" + urlParam + "(\\W)"), encodedVal + "$1"); 294 | }); 295 | url = url.replace(/\/?#$/, ''); 296 | var query = []; 297 | forEach(params, function(value, key){ 298 | if (!self.urlParams[key]) { 299 | query.push(encodeUriQuery(key) + '=' + encodeUriQuery(value)); 300 | } 301 | }); 302 | query.sort(); 303 | url = url.replace(/\/*$/, ''); 304 | return url + (query.length ? '?' + query.join('&') : ''); 305 | } 306 | }; 307 | 308 | 309 | function ResourceFactory(url, paramDefaults, actions) { 310 | var route = new Route(url); 311 | 312 | actions = extend({}, DEFAULT_ACTIONS, actions); 313 | 314 | function extractParams(data){ 315 | var ids = {}; 316 | forEach(paramDefaults || {}, function(value, key){ 317 | ids[key] = value.charAt && value.charAt(0) == '@' ? getter(data, value.substr(1)) : value; 318 | }); 319 | return ids; 320 | } 321 | 322 | function Resource(value){ 323 | copy(value || {}, this); 324 | } 325 | 326 | forEach(actions, function(action, name) { 327 | var hasBody = action.method == 'POST' || action.method == 'PUT' || action.method == 'PATCH'; 328 | Resource[name] = function(a1, a2, a3, a4) { 329 | var params = {}; 330 | var data; 331 | var success = noop; 332 | var error = null; 333 | switch(arguments.length) { 334 | case 4: 335 | error = a4; 336 | success = a3; 337 | //fallthrough 338 | case 3: 339 | case 2: 340 | if (isFunction(a2)) { 341 | if (isFunction(a1)) { 342 | success = a1; 343 | error = a2; 344 | break; 345 | } 346 | 347 | success = a2; 348 | error = a3; 349 | //fallthrough 350 | } else { 351 | params = a1; 352 | data = a2; 353 | success = a3; 354 | break; 355 | } 356 | case 1: 357 | if (isFunction(a1)) success = a1; 358 | else if (hasBody) data = a1; 359 | else params = a1; 360 | break; 361 | case 0: break; 362 | default: 363 | throw "Expected between 0-4 arguments [params, data, success, error], got " + 364 | arguments.length + " arguments."; 365 | } 366 | 367 | var value = this instanceof Resource ? this : (action.isArray ? [] : new Resource(data)); 368 | $http({ 369 | method: action.method, 370 | url: route.url(extend({}, extractParams(data), action.params || {}, params)), 371 | data: data 372 | }).then(function(response) { 373 | var data = response.data; 374 | 375 | if (data) { 376 | if (action.isArray) { 377 | value.length = 0; 378 | forEach(data, function(item) { 379 | value.push(new Resource(item)); 380 | }); 381 | } else { 382 | copy(data, value); 383 | } 384 | } 385 | (success||noop)(value, response.headers); 386 | }, error); 387 | 388 | return value; 389 | }; 390 | 391 | 392 | Resource.bind = function(additionalParamDefaults){ 393 | return ResourceFactory(url, extend({}, paramDefaults, additionalParamDefaults), actions); 394 | }; 395 | 396 | 397 | Resource.prototype['$' + name] = function(a1, a2, a3) { 398 | var params = extractParams(this), 399 | success = noop, 400 | error; 401 | 402 | switch(arguments.length) { 403 | case 3: params = a1; success = a2; error = a3; break; 404 | case 2: 405 | case 1: 406 | if (isFunction(a1)) { 407 | success = a1; 408 | error = a2; 409 | } else { 410 | params = a1; 411 | success = a2 || noop; 412 | } 413 | case 0: break; 414 | default: 415 | throw "Expected between 1-3 arguments [params, success, error], got " + 416 | arguments.length + " arguments."; 417 | } 418 | var data = hasBody ? this : undefined; 419 | Resource[name].call(this, params, data, success, error); 420 | }; 421 | }); 422 | return Resource; 423 | } 424 | 425 | return ResourceFactory; 426 | }]); 427 | 428 | })(window, window.angular); 429 | -------------------------------------------------------------------------------- /lib/angular/angular-resource.min.js: -------------------------------------------------------------------------------- 1 | /* 2 | AngularJS v1.0.2 3 | (c) 2010-2012 Google, Inc. http://angularjs.org 4 | License: MIT 5 | */ 6 | (function(A,f,u){'use strict';f.module("ngResource",["ng"]).factory("$resource",["$http","$parse",function(v,w){function g(b,c){return encodeURIComponent(b).replace(/%40/gi,"@").replace(/%3A/gi,":").replace(/%24/g,"$").replace(/%2C/gi,",").replace(c?null:/%20/g,"+")}function l(b,c){this.template=b+="#";this.defaults=c||{};var a=this.urlParams={};j(b.split(/\W/),function(c){c&&b.match(RegExp("[^\\\\]:"+c+"\\W"))&&(a[c]=!0)});this.template=b.replace(/\\:/g,":")}function s(b,c,a){function f(d){var b= 7 | {};j(c||{},function(a,x){var m;a.charAt&&a.charAt(0)=="@"?(m=a.substr(1),m=w(m)(d)):m=a;b[x]=m});return b}function e(a){t(a||{},this)}var y=new l(b),a=r({},z,a);j(a,function(d,g){var l=d.method=="POST"||d.method=="PUT"||d.method=="PATCH";e[g]=function(a,b,c,g){var i={},h,k=o,p=null;switch(arguments.length){case 4:p=g,k=c;case 3:case 2:if(q(b)){if(q(a)){k=a;p=b;break}k=b;p=c}else{i=a;h=b;k=c;break}case 1:q(a)?k=a:l?h=a:i=a;break;case 0:break;default:throw"Expected between 0-4 arguments [params, data, success, error], got "+ 8 | arguments.length+" arguments.";}var n=this instanceof e?this:d.isArray?[]:new e(h);v({method:d.method,url:y.url(r({},f(h),d.params||{},i)),data:h}).then(function(a){var b=a.data;if(b)d.isArray?(n.length=0,j(b,function(a){n.push(new e(a))})):t(b,n);(k||o)(n,a.headers)},p);return n};e.bind=function(d){return s(b,r({},c,d),a)};e.prototype["$"+g]=function(a,b,d){var c=f(this),i=o,h;switch(arguments.length){case 3:c=a;i=b;h=d;break;case 2:case 1:q(a)?(i=a,h=b):(c=a,i=b||o);case 0:break;default:throw"Expected between 1-3 arguments [params, success, error], got "+ 9 | arguments.length+" arguments.";}e[g].call(this,c,l?this:u,i,h)}});return e}var z={get:{method:"GET"},save:{method:"POST"},query:{method:"GET",isArray:!0},remove:{method:"DELETE"},"delete":{method:"DELETE"}},o=f.noop,j=f.forEach,r=f.extend,t=f.copy,q=f.isFunction;l.prototype={url:function(b){var c=this,a=this.template,f,b=b||{};j(this.urlParams,function(e,d){f=g(b[d]||c.defaults[d]||"",!0).replace(/%26/gi,"&").replace(/%3D/gi,"=").replace(/%2B/gi,"+");a=a.replace(RegExp(":"+d+"(\\W)"),f+"$1")});var a= 10 | a.replace(/\/?#$/,""),e=[];j(b,function(a,b){c.urlParams[b]||e.push(g(b)+"="+g(a))});e.sort();a=a.replace(/\/*$/,"");return a+(e.length?"?"+e.join("&"):"")}};return s}])})(window,window.angular); 11 | -------------------------------------------------------------------------------- /lib/angular/angular-sanitize.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license AngularJS v1.0.2 3 | * (c) 2010-2012 Google, Inc. http://angularjs.org 4 | * License: MIT 5 | */ 6 | (function(window, angular, undefined) { 7 | 'use strict'; 8 | 9 | /** 10 | * @ngdoc overview 11 | * @name ngSanitize 12 | * @description 13 | */ 14 | 15 | /* 16 | * HTML Parser By Misko Hevery (misko@hevery.com) 17 | * based on: HTML Parser By John Resig (ejohn.org) 18 | * Original code by Erik Arvidsson, Mozilla Public License 19 | * http://erik.eae.net/simplehtmlparser/simplehtmlparser.js 20 | * 21 | * // Use like so: 22 | * htmlParser(htmlString, { 23 | * start: function(tag, attrs, unary) {}, 24 | * end: function(tag) {}, 25 | * chars: function(text) {}, 26 | * comment: function(text) {} 27 | * }); 28 | * 29 | */ 30 | 31 | 32 | /** 33 | * @ngdoc service 34 | * @name ngSanitize.$sanitize 35 | * @function 36 | * 37 | * @description 38 | * The input is sanitized by parsing the html into tokens. All safe tokens (from a whitelist) are 39 | * then serialized back to properly escaped html string. This means that no unsafe input can make 40 | * it into the returned string, however, since our parser is more strict than a typical browser 41 | * parser, it's possible that some obscure input, which would be recognized as valid HTML by a 42 | * browser, won't make it through the sanitizer. 43 | * 44 | * @param {string} html Html input. 45 | * @returns {string} Sanitized html. 46 | * 47 | * @example 48 | 49 | 50 | 58 |
59 | Snippet: 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 71 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 |
FilterSourceRendered
html filter 69 |
<div ng-bind-html="snippet">
</div>
70 |
72 |
73 |
no filter
<div ng-bind="snippet">
</div>
unsafe html filter
<div ng-bind-html-unsafe="snippet">
</div>
86 |
87 |
88 | 89 | it('should sanitize the html snippet ', function() { 90 | expect(using('#html-filter').element('div').html()). 91 | toBe('

an html\nclick here\nsnippet

'); 92 | }); 93 | 94 | it('should escape snippet without any filter', function() { 95 | expect(using('#escaped-html').element('div').html()). 96 | toBe("<p style=\"color:blue\">an html\n" + 97 | "<em onmouseover=\"this.textContent='PWN3D!'\">click here</em>\n" + 98 | "snippet</p>"); 99 | }); 100 | 101 | it('should inline raw snippet if filtered as unsafe', function() { 102 | expect(using('#html-unsafe-filter').element("div").html()). 103 | toBe("

an html\n" + 104 | "click here\n" + 105 | "snippet

"); 106 | }); 107 | 108 | it('should update', function() { 109 | input('snippet').enter('new text'); 110 | expect(using('#html-filter').binding('snippet')).toBe('new text'); 111 | expect(using('#escaped-html').element('div').html()).toBe("new <b>text</b>"); 112 | expect(using('#html-unsafe-filter').binding("snippet")).toBe('new text'); 113 | }); 114 |
115 |
116 | */ 117 | var $sanitize = function(html) { 118 | var buf = []; 119 | htmlParser(html, htmlSanitizeWriter(buf)); 120 | return buf.join(''); 121 | }; 122 | 123 | 124 | // Regular Expressions for parsing tags and attributes 125 | var START_TAG_REGEXP = /^<\s*([\w:-]+)((?:\s+[\w:-]+(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*)\s*(\/?)\s*>/, 126 | END_TAG_REGEXP = /^<\s*\/\s*([\w:-]+)[^>]*>/, 127 | ATTR_REGEXP = /([\w:-]+)(?:\s*=\s*(?:(?:"((?:[^"])*)")|(?:'((?:[^'])*)')|([^>\s]+)))?/g, 128 | BEGIN_TAG_REGEXP = /^/g, 131 | CDATA_REGEXP = //g, 132 | URI_REGEXP = /^((ftp|https?):\/\/|mailto:|#)/, 133 | NON_ALPHANUMERIC_REGEXP = /([^\#-~| |!])/g; // Match everything outside of normal chars and " (quote character) 134 | 135 | 136 | // Good source of info about elements and attributes 137 | // http://dev.w3.org/html5/spec/Overview.html#semantics 138 | // http://simon.html5.org/html-elements 139 | 140 | // Safe Void Elements - HTML5 141 | // http://dev.w3.org/html5/spec/Overview.html#void-elements 142 | var voidElements = makeMap("area,br,col,hr,img,wbr"); 143 | 144 | // Elements that you can, intentionally, leave open (and which close themselves) 145 | // http://dev.w3.org/html5/spec/Overview.html#optional-tags 146 | var optionalEndTagBlockElements = makeMap("colgroup,dd,dt,li,p,tbody,td,tfoot,th,thead,tr"), 147 | optionalEndTagInlineElements = makeMap("rp,rt"), 148 | optionalEndTagElements = angular.extend({}, optionalEndTagInlineElements, optionalEndTagBlockElements); 149 | 150 | // Safe Block Elements - HTML5 151 | var blockElements = angular.extend({}, optionalEndTagBlockElements, makeMap("address,article,aside," + 152 | "blockquote,caption,center,del,dir,div,dl,figure,figcaption,footer,h1,h2,h3,h4,h5,h6," + 153 | "header,hgroup,hr,ins,map,menu,nav,ol,pre,script,section,table,ul")); 154 | 155 | // Inline Elements - HTML5 156 | var inlineElements = angular.extend({}, optionalEndTagInlineElements, makeMap("a,abbr,acronym,b,bdi,bdo," + 157 | "big,br,cite,code,del,dfn,em,font,i,img,ins,kbd,label,map,mark,q,ruby,rp,rt,s,samp,small," + 158 | "span,strike,strong,sub,sup,time,tt,u,var")); 159 | 160 | 161 | // Special Elements (can contain anything) 162 | var specialElements = makeMap("script,style"); 163 | 164 | var validElements = angular.extend({}, voidElements, blockElements, inlineElements, optionalEndTagElements); 165 | 166 | //Attributes that have href and hence need to be sanitized 167 | var uriAttrs = makeMap("background,cite,href,longdesc,src,usemap"); 168 | var validAttrs = angular.extend({}, uriAttrs, makeMap( 169 | 'abbr,align,alt,axis,bgcolor,border,cellpadding,cellspacing,class,clear,'+ 170 | 'color,cols,colspan,compact,coords,dir,face,headers,height,hreflang,hspace,'+ 171 | 'ismap,lang,language,nohref,nowrap,rel,rev,rows,rowspan,rules,'+ 172 | 'scope,scrolling,shape,span,start,summary,target,title,type,'+ 173 | 'valign,value,vspace,width')); 174 | 175 | function makeMap(str) { 176 | var obj = {}, items = str.split(','), i; 177 | for (i = 0; i < items.length; i++) obj[items[i]] = true; 178 | return obj; 179 | } 180 | 181 | 182 | /** 183 | * @example 184 | * htmlParser(htmlString, { 185 | * start: function(tag, attrs, unary) {}, 186 | * end: function(tag) {}, 187 | * chars: function(text) {}, 188 | * comment: function(text) {} 189 | * }); 190 | * 191 | * @param {string} html string 192 | * @param {object} handler 193 | */ 194 | function htmlParser( html, handler ) { 195 | var index, chars, match, stack = [], last = html; 196 | stack.last = function() { return stack[ stack.length - 1 ]; }; 197 | 198 | while ( html ) { 199 | chars = true; 200 | 201 | // Make sure we're not in a script or style element 202 | if ( !stack.last() || !specialElements[ stack.last() ] ) { 203 | 204 | // Comment 205 | if ( html.indexOf(""); 207 | 208 | if ( index >= 0 ) { 209 | if (handler.comment) handler.comment( html.substring( 4, index ) ); 210 | html = html.substring( index + 3 ); 211 | chars = false; 212 | } 213 | 214 | // end tag 215 | } else if ( BEGING_END_TAGE_REGEXP.test(html) ) { 216 | match = html.match( END_TAG_REGEXP ); 217 | 218 | if ( match ) { 219 | html = html.substring( match[0].length ); 220 | match[0].replace( END_TAG_REGEXP, parseEndTag ); 221 | chars = false; 222 | } 223 | 224 | // start tag 225 | } else if ( BEGIN_TAG_REGEXP.test(html) ) { 226 | match = html.match( START_TAG_REGEXP ); 227 | 228 | if ( match ) { 229 | html = html.substring( match[0].length ); 230 | match[0].replace( START_TAG_REGEXP, parseStartTag ); 231 | chars = false; 232 | } 233 | } 234 | 235 | if ( chars ) { 236 | index = html.indexOf("<"); 237 | 238 | var text = index < 0 ? html : html.substring( 0, index ); 239 | html = index < 0 ? "" : html.substring( index ); 240 | 241 | if (handler.chars) handler.chars( decodeEntities(text) ); 242 | } 243 | 244 | } else { 245 | html = html.replace(new RegExp("(.*)<\\s*\\/\\s*" + stack.last() + "[^>]*>", 'i'), function(all, text){ 246 | text = text. 247 | replace(COMMENT_REGEXP, "$1"). 248 | replace(CDATA_REGEXP, "$1"); 249 | 250 | if (handler.chars) handler.chars( decodeEntities(text) ); 251 | 252 | return ""; 253 | }); 254 | 255 | parseEndTag( "", stack.last() ); 256 | } 257 | 258 | if ( html == last ) { 259 | throw "Parse Error: " + html; 260 | } 261 | last = html; 262 | } 263 | 264 | // Clean up any remaining tags 265 | parseEndTag(); 266 | 267 | function parseStartTag( tag, tagName, rest, unary ) { 268 | tagName = angular.lowercase(tagName); 269 | if ( blockElements[ tagName ] ) { 270 | while ( stack.last() && inlineElements[ stack.last() ] ) { 271 | parseEndTag( "", stack.last() ); 272 | } 273 | } 274 | 275 | if ( optionalEndTagElements[ tagName ] && stack.last() == tagName ) { 276 | parseEndTag( "", tagName ); 277 | } 278 | 279 | unary = voidElements[ tagName ] || !!unary; 280 | 281 | if ( !unary ) 282 | stack.push( tagName ); 283 | 284 | var attrs = {}; 285 | 286 | rest.replace(ATTR_REGEXP, function(match, name, doubleQuotedValue, singleQoutedValue, unqoutedValue) { 287 | var value = doubleQuotedValue 288 | || singleQoutedValue 289 | || unqoutedValue 290 | || ''; 291 | 292 | attrs[name] = decodeEntities(value); 293 | }); 294 | if (handler.start) handler.start( tagName, attrs, unary ); 295 | } 296 | 297 | function parseEndTag( tag, tagName ) { 298 | var pos = 0, i; 299 | tagName = angular.lowercase(tagName); 300 | if ( tagName ) 301 | // Find the closest opened tag of the same type 302 | for ( pos = stack.length - 1; pos >= 0; pos-- ) 303 | if ( stack[ pos ] == tagName ) 304 | break; 305 | 306 | if ( pos >= 0 ) { 307 | // Close all the open elements, up the stack 308 | for ( i = stack.length - 1; i >= pos; i-- ) 309 | if (handler.end) handler.end( stack[ i ] ); 310 | 311 | // Remove the open elements from the stack 312 | stack.length = pos; 313 | } 314 | } 315 | } 316 | 317 | /** 318 | * decodes all entities into regular string 319 | * @param value 320 | * @returns {string} A string with decoded entities. 321 | */ 322 | var hiddenPre=document.createElement("pre"); 323 | function decodeEntities(value) { 324 | hiddenPre.innerHTML=value.replace(//g, '>'); 343 | } 344 | 345 | /** 346 | * create an HTML/XML writer which writes to buffer 347 | * @param {Array} buf use buf.jain('') to get out sanitized html string 348 | * @returns {object} in the form of { 349 | * start: function(tag, attrs, unary) {}, 350 | * end: function(tag) {}, 351 | * chars: function(text) {}, 352 | * comment: function(text) {} 353 | * } 354 | */ 355 | function htmlSanitizeWriter(buf){ 356 | var ignore = false; 357 | var out = angular.bind(buf, buf.push); 358 | return { 359 | start: function(tag, attrs, unary){ 360 | tag = angular.lowercase(tag); 361 | if (!ignore && specialElements[tag]) { 362 | ignore = tag; 363 | } 364 | if (!ignore && validElements[tag] == true) { 365 | out('<'); 366 | out(tag); 367 | angular.forEach(attrs, function(value, key){ 368 | var lkey=angular.lowercase(key); 369 | if (validAttrs[lkey]==true && (uriAttrs[lkey]!==true || value.match(URI_REGEXP))) { 370 | out(' '); 371 | out(key); 372 | out('="'); 373 | out(encodeEntities(value)); 374 | out('"'); 375 | } 376 | }); 377 | out(unary ? '/>' : '>'); 378 | } 379 | }, 380 | end: function(tag){ 381 | tag = angular.lowercase(tag); 382 | if (!ignore && validElements[tag] == true) { 383 | out(''); 386 | } 387 | if (tag == ignore) { 388 | ignore = false; 389 | } 390 | }, 391 | chars: function(chars){ 392 | if (!ignore) { 393 | out(encodeEntities(chars)); 394 | } 395 | } 396 | }; 397 | } 398 | 399 | 400 | // define ngSanitize module and register $sanitize service 401 | angular.module('ngSanitize', []).value('$sanitize', $sanitize); 402 | 403 | /** 404 | * @ngdoc directive 405 | * @name ngSanitize.directive:ngBindHtml 406 | * 407 | * @description 408 | * Creates a binding that will sanitize the result of evaluating the `expression` with the 409 | * {@link ngSanitize.$sanitize $sanitize} service and innerHTML the result into the current element. 410 | * 411 | * See {@link ngSanitize.$sanitize $sanitize} docs for examples. 412 | * 413 | * @element ANY 414 | * @param {expression} ngBindHtml {@link guide/expression Expression} to evaluate. 415 | */ 416 | angular.module('ngSanitize').directive('ngBindHtml', ['$sanitize', function($sanitize) { 417 | return function(scope, element, attr) { 418 | element.addClass('ng-binding').data('$binding', attr.ngBindHtml); 419 | scope.$watch(attr.ngBindHtml, function(value) { 420 | value = $sanitize(value); 421 | element.html(value || ''); 422 | }); 423 | }; 424 | }]); 425 | /** 426 | * @ngdoc filter 427 | * @name ngSanitize.filter:linky 428 | * @function 429 | * 430 | * @description 431 | * Finds links in text input and turns them into html links. Supports http/https/ftp/mailto and 432 | * plain email address links. 433 | * 434 | * @param {string} text Input text. 435 | * @returns {string} Html-linkified text. 436 | * 437 | * @usage 438 | 439 | * 440 | * @example 441 | 442 | 443 | 453 |
454 | Snippet: 455 | 456 | 457 | 458 | 459 | 460 | 461 | 462 | 463 | 466 | 469 | 470 | 471 | 472 | 473 | 474 | 475 |
FilterSourceRendered
linky filter 464 |
<div ng-bind-html="snippet | linky">
</div>
465 |
467 |
468 |
no filter
<div ng-bind="snippet">
</div>
476 | 477 | 478 | it('should linkify the snippet with urls', function() { 479 | expect(using('#linky-filter').binding('snippet | linky')). 480 | toBe('Pretty text with some links: ' + 481 | 'http://angularjs.org/, ' + 482 | 'us@somewhere.org, ' + 483 | 'another@somewhere.org, ' + 484 | 'and one more: ftp://127.0.0.1/.'); 485 | }); 486 | 487 | it ('should not linkify snippet without the linky filter', function() { 488 | expect(using('#escaped-html').binding('snippet')). 489 | toBe("Pretty text with some links:\n" + 490 | "http://angularjs.org/,\n" + 491 | "mailto:us@somewhere.org,\n" + 492 | "another@somewhere.org,\n" + 493 | "and one more: ftp://127.0.0.1/."); 494 | }); 495 | 496 | it('should update', function() { 497 | input('snippet').enter('new http://link.'); 498 | expect(using('#linky-filter').binding('snippet | linky')). 499 | toBe('new http://link.'); 500 | expect(using('#escaped-html').binding('snippet')).toBe('new http://link.'); 501 | }); 502 | 503 | 504 | */ 505 | angular.module('ngSanitize').filter('linky', function() { 506 | var LINKY_URL_REGEXP = /((ftp|https?):\/\/|(mailto:)?[A-Za-z0-9._%+-]+@)\S*[^\s\.\;\,\(\)\{\}\<\>]/, 507 | MAILTO_REGEXP = /^mailto:/; 508 | 509 | return function(text) { 510 | if (!text) return text; 511 | var match; 512 | var raw = text; 513 | var html = []; 514 | // TODO(vojta): use $sanitize instead 515 | var writer = htmlSanitizeWriter(html); 516 | var url; 517 | var i; 518 | while ((match = raw.match(LINKY_URL_REGEXP))) { 519 | // We can not end in these as they are sometimes found at the end of the sentence 520 | url = match[0]; 521 | // if we did not match ftp/http/mailto then assume mailto 522 | if (match[2] == match[3]) url = 'mailto:' + url; 523 | i = match.index; 524 | writer.chars(raw.substr(0, i)); 525 | writer.start('a', {href:url}); 526 | writer.chars(match[0].replace(MAILTO_REGEXP, '')); 527 | writer.end('a'); 528 | raw = raw.substring(i + match[0].length); 529 | } 530 | writer.chars(raw); 531 | return html.join(''); 532 | }; 533 | }); 534 | 535 | })(window, window.angular); 536 | -------------------------------------------------------------------------------- /lib/angular/angular-sanitize.min.js: -------------------------------------------------------------------------------- 1 | /* 2 | AngularJS v1.0.2 3 | (c) 2010-2012 Google, Inc. http://angularjs.org 4 | License: MIT 5 | */ 6 | (function(I,g){'use strict';function i(a){var d={},a=a.split(","),b;for(b=0;b=0;e--)if(f[e]==b)break;if(e>=0){for(c=f.length-1;c>=e;c--)d.end&&d.end(f[c]);f.length= 7 | e}}var c,h,f=[],j=a;for(f.last=function(){return f[f.length-1]};a;){h=!0;if(!f.last()||!q[f.last()]){if(a.indexOf("<\!--")===0)c=a.indexOf("--\>"),c>=0&&(d.comment&&d.comment(a.substring(4,c)),a=a.substring(c+3),h=!1);else if(B.test(a)){if(c=a.match(r))a=a.substring(c[0].length),c[0].replace(r,e),h=!1}else if(C.test(a)&&(c=a.match(s)))a=a.substring(c[0].length),c[0].replace(s,b),h=!1;h&&(c=a.indexOf("<"),h=c<0?a:a.substring(0,c),a=c<0?"":a.substring(c),d.chars&&d.chars(k(h)))}else a=a.replace(RegExp("(.*)<\\s*\\/\\s*"+ 8 | f.last()+"[^>]*>","i"),function(b,a){a=a.replace(D,"$1").replace(E,"$1");d.chars&&d.chars(k(a));return""}),e("",f.last());if(a==j)throw"Parse Error: "+a;j=a}e()}function k(a){l.innerHTML=a.replace(//g,">")}function u(a){var d=!1,b=g.bind(a,a.push);return{start:function(a,c,h){a=g.lowercase(a);!d&&q[a]&&(d=a);!d&&v[a]== 9 | !0&&(b("<"),b(a),g.forEach(c,function(a,c){var e=g.lowercase(c);if(G[e]==!0&&(w[e]!==!0||a.match(H)))b(" "),b(c),b('="'),b(t(a)),b('"')}),b(h?"/>":">"))},end:function(a){a=g.lowercase(a);!d&&v[a]==!0&&(b(""));a==d&&(d=!1)},chars:function(a){d||b(t(a))}}}var s=/^<\s*([\w:-]+)((?:\s+[\w:-]+(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*)\s*(\/?)\s*>/,r=/^<\s*\/\s*([\w:-]+)[^>]*>/,A=/([\w:-]+)(?:\s*=\s*(?:(?:"((?:[^"])*)")|(?:'((?:[^'])*)')|([^>\s]+)))?/g,C=/^/g, 10 | E=//g,H=/^((ftp|https?):\/\/|mailto:|#)/,F=/([^\#-~| |!])/g,p=i("area,br,col,hr,img,wbr"),x=i("colgroup,dd,dt,li,p,tbody,td,tfoot,th,thead,tr"),y=i("rp,rt"),o=g.extend({},y,x),m=g.extend({},x,i("address,article,aside,blockquote,caption,center,del,dir,div,dl,figure,figcaption,footer,h1,h2,h3,h4,h5,h6,header,hgroup,hr,ins,map,menu,nav,ol,pre,script,section,table,ul")),n=g.extend({},y,i("a,abbr,acronym,b,bdi,bdo,big,br,cite,code,del,dfn,em,font,i,img,ins,kbd,label,map,mark,q,ruby,rp,rt,s,samp,small,span,strike,strong,sub,sup,time,tt,u,var")), 11 | q=i("script,style"),v=g.extend({},p,m,n,o),w=i("background,cite,href,longdesc,src,usemap"),G=g.extend({},w,i("abbr,align,alt,axis,bgcolor,border,cellpadding,cellspacing,class,clear,color,cols,colspan,compact,coords,dir,face,headers,height,hreflang,hspace,ismap,lang,language,nohref,nowrap,rel,rev,rows,rowspan,rules,scope,scrolling,shape,span,start,summary,target,title,type,valign,value,vspace,width")),l=document.createElement("pre");g.module("ngSanitize",[]).value("$sanitize",function(a){var d=[]; 12 | z(a,u(d));return d.join("")});g.module("ngSanitize").directive("ngBindHtml",["$sanitize",function(a){return function(d,b,e){b.addClass("ng-binding").data("$binding",e.ngBindHtml);d.$watch(e.ngBindHtml,function(c){c=a(c);b.html(c||"")})}}]);g.module("ngSanitize").filter("linky",function(){var a=/((ftp|https?):\/\/|(mailto:)?[A-Za-z0-9._%+-]+@)\S*[^\s\.\;\,\(\)\{\}\<\>]/,d=/^mailto:/;return function(b){if(!b)return b;for(var e=b,c=[],h=u(c),f,g;b=e.match(a);)f=b[0],b[2]==b[3]&&(f="mailto:"+f),g=b.index, 13 | h.chars(e.substr(0,g)),h.start("a",{href:f}),h.chars(b[0].replace(d,"")),h.end("a"),e=e.substring(g+b[0].length);h.chars(e);return c.join("")}})})(window,window.angular); 14 | -------------------------------------------------------------------------------- /lib/angular/version.txt: -------------------------------------------------------------------------------- 1 | 1.0.2 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-parse", 3 | "version": "0.3.0", 4 | "description": "AngularJS interface to Parse", 5 | "main": "angular-parse.js", 6 | "directories": { 7 | "example": "example" 8 | }, 9 | "scripts": { 10 | "test": "echo \"Error: no test specified\" && exit 1" 11 | }, 12 | "license": "BSD", 13 | "repository": { 14 | "type": "git", 15 | "url": "git://github.com/jimrhoskins/angular-parse.git" 16 | }, 17 | "devDependencies": { 18 | "grunt": "~0.4.1", 19 | "grunt-contrib-coffee": "~0.7.0", 20 | "grunt-contrib-watch": "~0.5.1", 21 | "grunt-karma": "~0.4.4" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/angular-parse.coffee: -------------------------------------------------------------------------------- 1 | module = angular.module 'Parse', [] 2 | 3 | CONFIG = {} 4 | 5 | module.factory 'persist', ($q, $window) -> 6 | store = $window.localStorage 7 | 8 | persist = 9 | get: (keys) -> 10 | keys = [keys] unless angular.isArray keys 11 | result = {} 12 | for key in keys 13 | if store.key key 14 | result[key] = store.getItem key 15 | else 16 | result[key] = undefined 17 | result 18 | 19 | set: (obj) -> 20 | for own key, val of obj 21 | store.setItem key, val 22 | true 23 | 24 | remove: (keys) -> 25 | keys = [keys] unless angular.isArray keys 26 | for key in keys 27 | localStorage.removeItem key 28 | true 29 | 30 | module.factory 'ParseUtils', ($http, $window) -> 31 | Parse = 32 | BaseUrl: "https://api.parse.com/1" 33 | 34 | _request: (method, path, data, params) -> 35 | 36 | if angular.isArray path 37 | [klass, id] = path 38 | path = "#{klass.pathBase()}/#{id}" 39 | else if path.className 40 | path = "#{path.pathBase()}" 41 | else if path.objectId and path.constructor?.className 42 | path = "#{path.constructor.pathBase()}/#{path.objectId}" 43 | 44 | headers = 45 | "X-Parse-Application-Id": CONFIG.applicationId 46 | "X-Parse-REST-API-KEY" : CONFIG.apiKey 47 | "Content-Type" : "application/json" 48 | 49 | if $window.localStorage.key('PARSE_SESSION_TOKEN') 50 | headers["X-Parse-Session-Token"] = $window.localStorage.getItem('PARSE_SESSION_TOKEN') 51 | 52 | $http 53 | method: method 54 | url: @BaseUrl + path 55 | data: data 56 | params: params 57 | headers: headers 58 | 59 | func: (name) -> 60 | (data) -> Parse.callFunction name, data 61 | 62 | callFunction: (name, data) -> 63 | Parse._request("POST", "/functions/#{name}", data).then (r) -> 64 | r.data.result 65 | 66 | module.factory 'ParseAuth', (persist, ParseUser, ParseUtils, $q) -> 67 | auth = 68 | sessionToken: null 69 | currentUser: null 70 | 71 | _login: (user) -> 72 | auth.currentUser = user 73 | auth.sessionToken = user.sessionToken 74 | info = user.attributes() 75 | info.objectId = user.objectId 76 | persist.set 77 | PARSE_USER_INFO: JSON.stringify(info) 78 | PARSE_SESSION_TOKEN: user.sessionToken 79 | user 80 | 81 | resumeSession: -> 82 | results = persist.get(['PARSE_SESSION_TOKEN', 'PARSE_USER_INFO']) 83 | userAttrs = results.PARSE_USER_INFO 84 | sessionToken = results.PARSE_SESSION_TOKEN 85 | deferred = $q.defer(); 86 | if userAttrs and sessionToken 87 | try 88 | user = new ParseUser(JSON.parse(userAttrs)) 89 | auth.currentUser = user 90 | auth.sessionToken = sessionToken 91 | deferred.resolve(user.refresh()) 92 | catch e 93 | deferred.reject('User attributes not parseable') 94 | else 95 | deferred.reject('User attributes or Session Token not found') 96 | return deferred.promise 97 | 98 | register: (username, password) -> 99 | new ParseUser( 100 | username: username 101 | password: password 102 | ).save().then (user) -> 103 | auth._login(user) 104 | 105 | login: (username, password) -> 106 | ParseUtils._request("GET", "/login", null, { 107 | username: username 108 | password: password 109 | }) 110 | .then (response) -> 111 | auth._login( new ParseUser(response.data)) 112 | 113 | logout: -> 114 | persist.remove ['PARSE_SESSION_TOKEN', 'PARSE_USER_INFO'] 115 | auth.currentUser = null 116 | auth.sessionToken = null 117 | 118 | module.factory 'ParseModel', (ParseUtils) -> 119 | class Model 120 | @pathBase: -> 121 | "/classes/#{@className}" 122 | 123 | @find: (id, params) -> 124 | ParseUtils._request('GET', "/classes/#{@className}/#{id}", null, params) 125 | .then (response) => 126 | new @(response.data) 127 | 128 | @query: (params) -> 129 | ParseUtils._request('GET', @, null, params) 130 | .then (response) => 131 | for item in response.data.results 132 | new @(item) 133 | 134 | @configure: (name, attributes...) -> 135 | @className = name 136 | @attributes = attributes 137 | 138 | constructor: (data) -> 139 | for key, value of data 140 | @[key] = value 141 | @_saveCache() 142 | 143 | isNew: => 144 | !@objectId 145 | 146 | save: => 147 | if @isNew() 148 | @create() 149 | else 150 | @update() 151 | 152 | refresh: => 153 | ParseUtils._request('GET', @).then (response) => 154 | for own key, value of response.data 155 | @[key] = value 156 | @ 157 | 158 | create: => 159 | ParseUtils._request('POST', @constructor, @encodeParse()) 160 | .then (response) => 161 | @objectId = response.data.objectId 162 | @createdAt = response.data.createdAt 163 | if token = response.data.sessionToken 164 | @sessionToken = token 165 | @_saveCache() 166 | return @ 167 | 168 | update: => 169 | ParseUtils._request('PUT', @, @encodeParse()) 170 | .then (response) => 171 | @updatedAt = response.data.updatedAt 172 | @_saveCache() 173 | return @ 174 | 175 | destroy: => 176 | ParseUtils._request('DELETE', @) 177 | .then (response) => 178 | @objectId = null 179 | return @ 180 | 181 | attributes: => 182 | result = {} 183 | for key in @constructor.attributes 184 | result[key] = @[key] 185 | result 186 | 187 | encodeParse: => 188 | result = {} 189 | for key in @constructor.attributes 190 | if key of this 191 | obj = @[key] 192 | 193 | if obj? and obj.objectId and obj.constructor?.className 194 | # Pointer 195 | obj = { 196 | __type: "Pointer" 197 | className: obj.constructor.className 198 | objectId: obj.objectId 199 | } 200 | 201 | result[key] = obj 202 | 203 | result 204 | 205 | _saveCache: => 206 | @_cache = angular.copy @encodeParse() 207 | 208 | isDirty: => 209 | not angular.equals @_cache, @encodeParse() 210 | 211 | module.factory 'ParseDefaultUser', (ParseModel) -> 212 | class User extends ParseModel 213 | @configure 'users', 'username', 'password' 214 | @pathBase: -> "/users" 215 | 216 | save: -> 217 | super().then (user) => 218 | delete user.password 219 | user 220 | 221 | module.factory 'ParseUser', (ParseDefaultUser, ParseCustomUser) -> 222 | if ParseCustomUser? and (new ParseCustomUser instanceof ParseDefaultUser) 223 | return ParseCustomUser 224 | else 225 | return ParseDefaultUser 226 | 227 | module.provider 'Parse', -> 228 | return { 229 | initialize: (applicationId, apiKey) -> 230 | CONFIG.apiKey = apiKey 231 | CONFIG.applicationId = applicationId 232 | 233 | $get: (ParseModel, ParseUser, ParseAuth, ParseUtils) -> 234 | BaseUrl: ParseUtils.BaseUrl 235 | Model: ParseModel 236 | User: ParseUser 237 | auth: ParseAuth 238 | } 239 | 240 | angular.module('Parse').factory 'ParseCustomUser', (ParseDefaultUser) -> 241 | ParseDefaultUser -------------------------------------------------------------------------------- /test/support/angular-mocks.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * @license AngularJS v1.0.2 4 | * (c) 2010-2012 Google, Inc. http://angularjs.org 5 | * License: MIT 6 | * 7 | * TODO(vojta): wrap whole file into closure during build 8 | */ 9 | 10 | /** 11 | * @ngdoc overview 12 | * @name angular.mock 13 | * @description 14 | * 15 | * Namespace from 'angular-mocks.js' which contains testing related code. 16 | */ 17 | angular.mock = {}; 18 | 19 | /** 20 | * ! This is a private undocumented service ! 21 | * 22 | * @name ngMock.$browser 23 | * 24 | * @description 25 | * This service is a mock implementation of {@link ng.$browser}. It provides fake 26 | * implementation for commonly used browser apis that are hard to test, e.g. setTimeout, xhr, 27 | * cookies, etc... 28 | * 29 | * The api of this service is the same as that of the real {@link ng.$browser $browser}, except 30 | * that there are several helper methods available which can be used in tests. 31 | */ 32 | angular.mock.$BrowserProvider = function() { 33 | this.$get = function(){ 34 | return new angular.mock.$Browser(); 35 | }; 36 | }; 37 | 38 | angular.mock.$Browser = function() { 39 | var self = this; 40 | 41 | this.isMock = true; 42 | self.$$url = "http://server/"; 43 | self.$$lastUrl = self.$$url; // used by url polling fn 44 | self.pollFns = []; 45 | 46 | // TODO(vojta): remove this temporary api 47 | self.$$completeOutstandingRequest = angular.noop; 48 | self.$$incOutstandingRequestCount = angular.noop; 49 | 50 | 51 | // register url polling fn 52 | 53 | self.onUrlChange = function(listener) { 54 | self.pollFns.push( 55 | function() { 56 | if (self.$$lastUrl != self.$$url) { 57 | self.$$lastUrl = self.$$url; 58 | listener(self.$$url); 59 | } 60 | } 61 | ); 62 | 63 | return listener; 64 | }; 65 | 66 | self.cookieHash = {}; 67 | self.lastCookieHash = {}; 68 | self.deferredFns = []; 69 | self.deferredNextId = 0; 70 | 71 | self.defer = function(fn, delay) { 72 | delay = delay || 0; 73 | self.deferredFns.push({time:(self.defer.now + delay), fn:fn, id: self.deferredNextId}); 74 | self.deferredFns.sort(function(a,b){ return a.time - b.time;}); 75 | return self.deferredNextId++; 76 | }; 77 | 78 | 79 | self.defer.now = 0; 80 | 81 | 82 | self.defer.cancel = function(deferId) { 83 | var fnIndex; 84 | 85 | angular.forEach(self.deferredFns, function(fn, index) { 86 | if (fn.id === deferId) fnIndex = index; 87 | }); 88 | 89 | if (fnIndex !== undefined) { 90 | self.deferredFns.splice(fnIndex, 1); 91 | return true; 92 | } 93 | 94 | return false; 95 | }; 96 | 97 | 98 | /** 99 | * @name ngMock.$browser#defer.flush 100 | * @methodOf ngMock.$browser 101 | * 102 | * @description 103 | * Flushes all pending requests and executes the defer callbacks. 104 | * 105 | * @param {number=} number of milliseconds to flush. See {@link #defer.now} 106 | */ 107 | self.defer.flush = function(delay) { 108 | if (angular.isDefined(delay)) { 109 | self.defer.now += delay; 110 | } else { 111 | if (self.deferredFns.length) { 112 | self.defer.now = self.deferredFns[self.deferredFns.length-1].time; 113 | } else { 114 | throw Error('No deferred tasks to be flushed'); 115 | } 116 | } 117 | 118 | while (self.deferredFns.length && self.deferredFns[0].time <= self.defer.now) { 119 | self.deferredFns.shift().fn(); 120 | } 121 | }; 122 | /** 123 | * @name ngMock.$browser#defer.now 124 | * @propertyOf ngMock.$browser 125 | * 126 | * @description 127 | * Current milliseconds mock time. 128 | */ 129 | 130 | self.$$baseHref = ''; 131 | self.baseHref = function() { 132 | return this.$$baseHref; 133 | }; 134 | }; 135 | angular.mock.$Browser.prototype = { 136 | 137 | /** 138 | * @name ngMock.$browser#poll 139 | * @methodOf ngMock.$browser 140 | * 141 | * @description 142 | * run all fns in pollFns 143 | */ 144 | poll: function poll() { 145 | angular.forEach(this.pollFns, function(pollFn){ 146 | pollFn(); 147 | }); 148 | }, 149 | 150 | addPollFn: function(pollFn) { 151 | this.pollFns.push(pollFn); 152 | return pollFn; 153 | }, 154 | 155 | url: function(url, replace) { 156 | if (url) { 157 | this.$$url = url; 158 | return this; 159 | } 160 | 161 | return this.$$url; 162 | }, 163 | 164 | cookies: function(name, value) { 165 | if (name) { 166 | if (value == undefined) { 167 | delete this.cookieHash[name]; 168 | } else { 169 | if (angular.isString(value) && //strings only 170 | value.length <= 4096) { //strict cookie storage limits 171 | this.cookieHash[name] = value; 172 | } 173 | } 174 | } else { 175 | if (!angular.equals(this.cookieHash, this.lastCookieHash)) { 176 | this.lastCookieHash = angular.copy(this.cookieHash); 177 | this.cookieHash = angular.copy(this.cookieHash); 178 | } 179 | return this.cookieHash; 180 | } 181 | }, 182 | 183 | notifyWhenNoOutstandingRequests: function(fn) { 184 | fn(); 185 | } 186 | }; 187 | 188 | 189 | /** 190 | * @ngdoc object 191 | * @name ngMock.$exceptionHandlerProvider 192 | * 193 | * @description 194 | * Configures the mock implementation of {@link ng.$exceptionHandler} to rethrow or to log errors passed 195 | * into the `$exceptionHandler`. 196 | */ 197 | 198 | /** 199 | * @ngdoc object 200 | * @name ngMock.$exceptionHandler 201 | * 202 | * @description 203 | * Mock implementation of {@link ng.$exceptionHandler} that rethrows or logs errors passed 204 | * into it. See {@link ngMock.$exceptionHandlerProvider $exceptionHandlerProvider} for configuration 205 | * information. 206 | */ 207 | 208 | angular.mock.$ExceptionHandlerProvider = function() { 209 | var handler; 210 | 211 | /** 212 | * @ngdoc method 213 | * @name ngMock.$exceptionHandlerProvider#mode 214 | * @methodOf ngMock.$exceptionHandlerProvider 215 | * 216 | * @description 217 | * Sets the logging mode. 218 | * 219 | * @param {string} mode Mode of operation, defaults to `rethrow`. 220 | * 221 | * - `rethrow`: If any errors are are passed into the handler in tests, it typically 222 | * means that there is a bug in the application or test, so this mock will 223 | * make these tests fail. 224 | * - `log`: Sometimes it is desirable to test that an error is throw, for this case the `log` mode stores the 225 | * error and allows later assertion of it. 226 | * See {@link ngMock.$log#assertEmpty assertEmpty()} and 227 | * {@link ngMock.$log#reset reset()} 228 | */ 229 | this.mode = function(mode) { 230 | switch(mode) { 231 | case 'rethrow': 232 | handler = function(e) { 233 | throw e; 234 | }; 235 | break; 236 | case 'log': 237 | var errors = []; 238 | 239 | handler = function(e) { 240 | if (arguments.length == 1) { 241 | errors.push(e); 242 | } else { 243 | errors.push([].slice.call(arguments, 0)); 244 | } 245 | }; 246 | 247 | handler.errors = errors; 248 | break; 249 | default: 250 | throw Error("Unknown mode '" + mode + "', only 'log'/'rethrow' modes are allowed!"); 251 | } 252 | }; 253 | 254 | this.$get = function() { 255 | return handler; 256 | }; 257 | 258 | this.mode('rethrow'); 259 | }; 260 | 261 | 262 | /** 263 | * @ngdoc service 264 | * @name ngMock.$log 265 | * 266 | * @description 267 | * Mock implementation of {@link ng.$log} that gathers all logged messages in arrays 268 | * (one array per logging level). These arrays are exposed as `logs` property of each of the 269 | * level-specific log function, e.g. for level `error` the array is exposed as `$log.error.logs`. 270 | * 271 | */ 272 | angular.mock.$LogProvider = function() { 273 | 274 | function concat(array1, array2, index) { 275 | return array1.concat(Array.prototype.slice.call(array2, index)); 276 | } 277 | 278 | 279 | this.$get = function () { 280 | var $log = { 281 | log: function() { $log.log.logs.push(concat([], arguments, 0)); }, 282 | warn: function() { $log.warn.logs.push(concat([], arguments, 0)); }, 283 | info: function() { $log.info.logs.push(concat([], arguments, 0)); }, 284 | error: function() { $log.error.logs.push(concat([], arguments, 0)); } 285 | }; 286 | 287 | /** 288 | * @ngdoc method 289 | * @name ngMock.$log#reset 290 | * @methodOf ngMock.$log 291 | * 292 | * @description 293 | * Reset all of the logging arrays to empty. 294 | */ 295 | $log.reset = function () { 296 | /** 297 | * @ngdoc property 298 | * @name ngMock.$log#log.logs 299 | * @propertyOf ngMock.$log 300 | * 301 | * @description 302 | * Array of logged messages. 303 | */ 304 | $log.log.logs = []; 305 | /** 306 | * @ngdoc property 307 | * @name ngMock.$log#warn.logs 308 | * @propertyOf ngMock.$log 309 | * 310 | * @description 311 | * Array of logged messages. 312 | */ 313 | $log.warn.logs = []; 314 | /** 315 | * @ngdoc property 316 | * @name ngMock.$log#info.logs 317 | * @propertyOf ngMock.$log 318 | * 319 | * @description 320 | * Array of logged messages. 321 | */ 322 | $log.info.logs = []; 323 | /** 324 | * @ngdoc property 325 | * @name ngMock.$log#error.logs 326 | * @propertyOf ngMock.$log 327 | * 328 | * @description 329 | * Array of logged messages. 330 | */ 331 | $log.error.logs = []; 332 | }; 333 | 334 | /** 335 | * @ngdoc method 336 | * @name ngMock.$log#assertEmpty 337 | * @methodOf ngMock.$log 338 | * 339 | * @description 340 | * Assert that the all of the logging methods have no logged messages. If messages present, an exception is thrown. 341 | */ 342 | $log.assertEmpty = function() { 343 | var errors = []; 344 | angular.forEach(['error', 'warn', 'info', 'log'], function(logLevel) { 345 | angular.forEach($log[logLevel].logs, function(log) { 346 | angular.forEach(log, function (logItem) { 347 | errors.push('MOCK $log (' + logLevel + '): ' + String(logItem) + '\n' + (logItem.stack || '')); 348 | }); 349 | }); 350 | }); 351 | if (errors.length) { 352 | errors.unshift("Expected $log to be empty! Either a message was logged unexpectedly, or an expected " + 353 | "log message was not checked and removed:"); 354 | errors.push(''); 355 | throw new Error(errors.join('\n---------\n')); 356 | } 357 | }; 358 | 359 | $log.reset(); 360 | return $log; 361 | }; 362 | }; 363 | 364 | 365 | (function() { 366 | var R_ISO8061_STR = /^(\d{4})-?(\d\d)-?(\d\d)(?:T(\d\d)(?:\:?(\d\d)(?:\:?(\d\d)(?:\.(\d{3}))?)?)?(Z|([+-])(\d\d):?(\d\d)))?$/; 367 | 368 | function jsonStringToDate(string){ 369 | var match; 370 | if (match = string.match(R_ISO8061_STR)) { 371 | var date = new Date(0), 372 | tzHour = 0, 373 | tzMin = 0; 374 | if (match[9]) { 375 | tzHour = int(match[9] + match[10]); 376 | tzMin = int(match[9] + match[11]); 377 | } 378 | date.setUTCFullYear(int(match[1]), int(match[2]) - 1, int(match[3])); 379 | date.setUTCHours(int(match[4]||0) - tzHour, int(match[5]||0) - tzMin, int(match[6]||0), int(match[7]||0)); 380 | return date; 381 | } 382 | return string; 383 | } 384 | 385 | function int(str) { 386 | return parseInt(str, 10); 387 | } 388 | 389 | function padNumber(num, digits, trim) { 390 | var neg = ''; 391 | if (num < 0) { 392 | neg = '-'; 393 | num = -num; 394 | } 395 | num = '' + num; 396 | while(num.length < digits) num = '0' + num; 397 | if (trim) 398 | num = num.substr(num.length - digits); 399 | return neg + num; 400 | } 401 | 402 | 403 | /** 404 | * @ngdoc object 405 | * @name angular.mock.TzDate 406 | * @description 407 | * 408 | * *NOTE*: this is not an injectable instance, just a globally available mock class of `Date`. 409 | * 410 | * Mock of the Date type which has its timezone specified via constroctor arg. 411 | * 412 | * The main purpose is to create Date-like instances with timezone fixed to the specified timezone 413 | * offset, so that we can test code that depends on local timezone settings without dependency on 414 | * the time zone settings of the machine where the code is running. 415 | * 416 | * @param {number} offset Offset of the *desired* timezone in hours (fractions will be honored) 417 | * @param {(number|string)} timestamp Timestamp representing the desired time in *UTC* 418 | * 419 | * @example 420 | * !!!! WARNING !!!!! 421 | * This is not a complete Date object so only methods that were implemented can be called safely. 422 | * To make matters worse, TzDate instances inherit stuff from Date via a prototype. 423 | * 424 | * We do our best to intercept calls to "unimplemented" methods, but since the list of methods is 425 | * incomplete we might be missing some non-standard methods. This can result in errors like: 426 | * "Date.prototype.foo called on incompatible Object". 427 | * 428 | *
 429 |    * var newYearInBratislava = new TzDate(-1, '2009-12-31T23:00:00Z');
 430 |    * newYearInBratislava.getTimezoneOffset() => -60;
 431 |    * newYearInBratislava.getFullYear() => 2010;
 432 |    * newYearInBratislava.getMonth() => 0;
 433 |    * newYearInBratislava.getDate() => 1;
 434 |    * newYearInBratislava.getHours() => 0;
 435 |    * newYearInBratislava.getMinutes() => 0;
 436 |    * 
437 | * 438 | */ 439 | angular.mock.TzDate = function (offset, timestamp) { 440 | var self = new Date(0); 441 | if (angular.isString(timestamp)) { 442 | var tsStr = timestamp; 443 | 444 | self.origDate = jsonStringToDate(timestamp); 445 | 446 | timestamp = self.origDate.getTime(); 447 | if (isNaN(timestamp)) 448 | throw { 449 | name: "Illegal Argument", 450 | message: "Arg '" + tsStr + "' passed into TzDate constructor is not a valid date string" 451 | }; 452 | } else { 453 | self.origDate = new Date(timestamp); 454 | } 455 | 456 | var localOffset = new Date(timestamp).getTimezoneOffset(); 457 | self.offsetDiff = localOffset*60*1000 - offset*1000*60*60; 458 | self.date = new Date(timestamp + self.offsetDiff); 459 | 460 | self.getTime = function() { 461 | return self.date.getTime() - self.offsetDiff; 462 | }; 463 | 464 | self.toLocaleDateString = function() { 465 | return self.date.toLocaleDateString(); 466 | }; 467 | 468 | self.getFullYear = function() { 469 | return self.date.getFullYear(); 470 | }; 471 | 472 | self.getMonth = function() { 473 | return self.date.getMonth(); 474 | }; 475 | 476 | self.getDate = function() { 477 | return self.date.getDate(); 478 | }; 479 | 480 | self.getHours = function() { 481 | return self.date.getHours(); 482 | }; 483 | 484 | self.getMinutes = function() { 485 | return self.date.getMinutes(); 486 | }; 487 | 488 | self.getSeconds = function() { 489 | return self.date.getSeconds(); 490 | }; 491 | 492 | self.getTimezoneOffset = function() { 493 | return offset * 60; 494 | }; 495 | 496 | self.getUTCFullYear = function() { 497 | return self.origDate.getUTCFullYear(); 498 | }; 499 | 500 | self.getUTCMonth = function() { 501 | return self.origDate.getUTCMonth(); 502 | }; 503 | 504 | self.getUTCDate = function() { 505 | return self.origDate.getUTCDate(); 506 | }; 507 | 508 | self.getUTCHours = function() { 509 | return self.origDate.getUTCHours(); 510 | }; 511 | 512 | self.getUTCMinutes = function() { 513 | return self.origDate.getUTCMinutes(); 514 | }; 515 | 516 | self.getUTCSeconds = function() { 517 | return self.origDate.getUTCSeconds(); 518 | }; 519 | 520 | self.getUTCMilliseconds = function() { 521 | return self.origDate.getUTCMilliseconds(); 522 | }; 523 | 524 | self.getDay = function() { 525 | return self.date.getDay(); 526 | }; 527 | 528 | // provide this method only on browsers that already have it 529 | if (self.toISOString) { 530 | self.toISOString = function() { 531 | return padNumber(self.origDate.getUTCFullYear(), 4) + '-' + 532 | padNumber(self.origDate.getUTCMonth() + 1, 2) + '-' + 533 | padNumber(self.origDate.getUTCDate(), 2) + 'T' + 534 | padNumber(self.origDate.getUTCHours(), 2) + ':' + 535 | padNumber(self.origDate.getUTCMinutes(), 2) + ':' + 536 | padNumber(self.origDate.getUTCSeconds(), 2) + '.' + 537 | padNumber(self.origDate.getUTCMilliseconds(), 3) + 'Z' 538 | } 539 | } 540 | 541 | //hide all methods not implemented in this mock that the Date prototype exposes 542 | var unimplementedMethods = ['getMilliseconds', 'getUTCDay', 543 | 'getYear', 'setDate', 'setFullYear', 'setHours', 'setMilliseconds', 544 | 'setMinutes', 'setMonth', 'setSeconds', 'setTime', 'setUTCDate', 'setUTCFullYear', 545 | 'setUTCHours', 'setUTCMilliseconds', 'setUTCMinutes', 'setUTCMonth', 'setUTCSeconds', 546 | 'setYear', 'toDateString', 'toGMTString', 'toJSON', 'toLocaleFormat', 'toLocaleString', 547 | 'toLocaleTimeString', 'toSource', 'toString', 'toTimeString', 'toUTCString', 'valueOf']; 548 | 549 | angular.forEach(unimplementedMethods, function(methodName) { 550 | self[methodName] = function() { 551 | throw Error("Method '" + methodName + "' is not implemented in the TzDate mock"); 552 | }; 553 | }); 554 | 555 | return self; 556 | }; 557 | 558 | //make "tzDateInstance instanceof Date" return true 559 | angular.mock.TzDate.prototype = Date.prototype; 560 | })(); 561 | 562 | 563 | /** 564 | * @ngdoc function 565 | * @name angular.mock.debug 566 | * @description 567 | * 568 | * *NOTE*: this is not an injectable instance, just a globally available function. 569 | * 570 | * Method for serializing common angular objects (scope, elements, etc..) into strings, useful for debugging. 571 | * 572 | * This method is also available on window, where it can be used to display objects on debug console. 573 | * 574 | * @param {*} object - any object to turn into string. 575 | * @return {string} a serialized string of the argument 576 | */ 577 | angular.mock.dump = function(object) { 578 | return serialize(object); 579 | 580 | function serialize(object) { 581 | var out; 582 | 583 | if (angular.isElement(object)) { 584 | object = angular.element(object); 585 | out = angular.element('
'); 586 | angular.forEach(object, function(element) { 587 | out.append(angular.element(element).clone()); 588 | }); 589 | out = out.html(); 590 | } else if (angular.isArray(object)) { 591 | out = []; 592 | angular.forEach(object, function(o) { 593 | out.push(serialize(o)); 594 | }); 595 | out = '[ ' + out.join(', ') + ' ]'; 596 | } else if (angular.isObject(object)) { 597 | if (angular.isFunction(object.$eval) && angular.isFunction(object.$apply)) { 598 | out = serializeScope(object); 599 | } else if (object instanceof Error) { 600 | out = object.stack || ('' + object.name + ': ' + object.message); 601 | } else { 602 | out = angular.toJson(object, true); 603 | } 604 | } else { 605 | out = String(object); 606 | } 607 | 608 | return out; 609 | } 610 | 611 | function serializeScope(scope, offset) { 612 | offset = offset || ' '; 613 | var log = [offset + 'Scope(' + scope.$id + '): {']; 614 | for ( var key in scope ) { 615 | if (scope.hasOwnProperty(key) && !key.match(/^(\$|this)/)) { 616 | log.push(' ' + key + ': ' + angular.toJson(scope[key])); 617 | } 618 | } 619 | var child = scope.$$childHead; 620 | while(child) { 621 | log.push(serializeScope(child, offset + ' ')); 622 | child = child.$$nextSibling; 623 | } 624 | log.push('}'); 625 | return log.join('\n' + offset); 626 | } 627 | }; 628 | 629 | /** 630 | * @ngdoc object 631 | * @name ngMock.$httpBackend 632 | * @description 633 | * Fake HTTP backend implementation suitable for unit testing application that use the 634 | * {@link ng.$http $http service}. 635 | * 636 | * *Note*: For fake http backend implementation suitable for end-to-end testing or backend-less 637 | * development please see {@link ngMockE2E.$httpBackend e2e $httpBackend mock}. 638 | * 639 | * During unit testing, we want our unit tests to run quickly and have no external dependencies so 640 | * we don’t want to send {@link https://developer.mozilla.org/en/xmlhttprequest XHR} or 641 | * {@link http://en.wikipedia.org/wiki/JSONP JSONP} requests to a real server. All we really need is 642 | * to verify whether a certain request has been sent or not, or alternatively just let the 643 | * application make requests, respond with pre-trained responses and assert that the end result is 644 | * what we expect it to be. 645 | * 646 | * This mock implementation can be used to respond with static or dynamic responses via the 647 | * `expect` and `when` apis and their shortcuts (`expectGET`, `whenPOST`, etc). 648 | * 649 | * When an Angular application needs some data from a server, it calls the $http service, which 650 | * sends the request to a real server using $httpBackend service. With dependency injection, it is 651 | * easy to inject $httpBackend mock (which has the same API as $httpBackend) and use it to verify 652 | * the requests and respond with some testing data without sending a request to real server. 653 | * 654 | * There are two ways to specify what test data should be returned as http responses by the mock 655 | * backend when the code under test makes http requests: 656 | * 657 | * - `$httpBackend.expect` - specifies a request expectation 658 | * - `$httpBackend.when` - specifies a backend definition 659 | * 660 | * 661 | * # Request Expectations vs Backend Definitions 662 | * 663 | * Request expectations provide a way to make assertions about requests made by the application and 664 | * to define responses for those requests. The test will fail if the expected requests are not made 665 | * or they are made in the wrong order. 666 | * 667 | * Backend definitions allow you to define a fake backend for your application which doesn't assert 668 | * if a particular request was made or not, it just returns a trained response if a request is made. 669 | * The test will pass whether or not the request gets made during testing. 670 | * 671 | * 672 | * 673 | * 674 | * 675 | * 676 | * 677 | * 678 | * 679 | * 680 | * 681 | * 682 | * 683 | * 684 | * 685 | * 686 | * 687 | * 688 | * 689 | * 690 | * 691 | * 692 | * 693 | * 694 | * 695 | * 696 | * 697 | * 698 | * 699 | * 700 | * 701 | * 702 | * 703 | * 704 | *
Request expectationsBackend definitions
Syntax.expect(...).respond(...).when(...).respond(...)
Typical usagestrict unit testsloose (black-box) unit testing
Fulfills multiple requestsNOYES
Order of requests mattersYESNO
Request requiredYESNO
Response requiredoptional (see below)YES
705 | * 706 | * In cases where both backend definitions and request expectations are specified during unit 707 | * testing, the request expectations are evaluated first. 708 | * 709 | * If a request expectation has no response specified, the algorithm will search your backend 710 | * definitions for an appropriate response. 711 | * 712 | * If a request didn't match any expectation or if the expectation doesn't have the response 713 | * defined, the backend definitions are evaluated in sequential order to see if any of them match 714 | * the request. The response from the first matched definition is returned. 715 | * 716 | * 717 | * # Flushing HTTP requests 718 | * 719 | * The $httpBackend used in production, always responds to requests with responses asynchronously. 720 | * If we preserved this behavior in unit testing, we'd have to create async unit tests, which are 721 | * hard to write, follow and maintain. At the same time the testing mock, can't respond 722 | * synchronously because that would change the execution of the code under test. For this reason the 723 | * mock $httpBackend has a `flush()` method, which allows the test to explicitly flush pending 724 | * requests and thus preserving the async api of the backend, while allowing the test to execute 725 | * synchronously. 726 | * 727 | * 728 | * # Unit testing with mock $httpBackend 729 | * 730 | *
 731 |    // controller
 732 |    function MyController($scope, $http) {
 733 |      $http.get('/auth.py').success(function(data) {
 734 |        $scope.user = data;
 735 |      });
 736 | 
 737 |      this.saveMessage = function(message) {
 738 |        $scope.status = 'Saving...';
 739 |        $http.post('/add-msg.py', message).success(function(response) {
 740 |          $scope.status = '';
 741 |        }).error(function() {
 742 |          $scope.status = 'ERROR!';
 743 |        });
 744 |      };
 745 |    }
 746 | 
 747 |    // testing controller
 748 |    var $http;
 749 | 
 750 |    beforeEach(inject(function($injector) {
 751 |      $httpBackend = $injector.get('$httpBackend');
 752 | 
 753 |      // backend definition common for all tests
 754 |      $httpBackend.when('GET', '/auth.py').respond({userId: 'userX'}, {'A-Token': 'xxx'});
 755 |    }));
 756 | 
 757 | 
 758 |    afterEach(function() {
 759 |      $httpBackend.verifyNoOutstandingExpectation();
 760 |      $httpBackend.verifyNoOutstandingRequest();
 761 |    });
 762 | 
 763 | 
 764 |    it('should fetch authentication token', function() {
 765 |      $httpBackend.expectGET('/auth.py');
 766 |      var controller = scope.$new(MyController);
 767 |      $httpBackend.flush();
 768 |    });
 769 | 
 770 | 
 771 |    it('should send msg to server', function() {
 772 |      // now you don’t care about the authentication, but
 773 |      // the controller will still send the request and
 774 |      // $httpBackend will respond without you having to
 775 |      // specify the expectation and response for this request
 776 |      $httpBackend.expectPOST('/add-msg.py', 'message content').respond(201, '');
 777 | 
 778 |      var controller = scope.$new(MyController);
 779 |      $httpBackend.flush();
 780 |      controller.saveMessage('message content');
 781 |      expect(controller.status).toBe('Saving...');
 782 |      $httpBackend.flush();
 783 |      expect(controller.status).toBe('');
 784 |    });
 785 | 
 786 | 
 787 |    it('should send auth header', function() {
 788 |      $httpBackend.expectPOST('/add-msg.py', undefined, function(headers) {
 789 |        // check if the header was send, if it wasn't the expectation won't
 790 |        // match the request and the test will fail
 791 |        return headers['Authorization'] == 'xxx';
 792 |      }).respond(201, '');
 793 | 
 794 |      var controller = scope.$new(MyController);
 795 |      controller.saveMessage('whatever');
 796 |      $httpBackend.flush();
 797 |    });
 798 |    
799 | */ 800 | angular.mock.$HttpBackendProvider = function() { 801 | this.$get = [createHttpBackendMock]; 802 | }; 803 | 804 | /** 805 | * General factory function for $httpBackend mock. 806 | * Returns instance for unit testing (when no arguments specified): 807 | * - passing through is disabled 808 | * - auto flushing is disabled 809 | * 810 | * Returns instance for e2e testing (when `$delegate` and `$browser` specified): 811 | * - passing through (delegating request to real backend) is enabled 812 | * - auto flushing is enabled 813 | * 814 | * @param {Object=} $delegate Real $httpBackend instance (allow passing through if specified) 815 | * @param {Object=} $browser Auto-flushing enabled if specified 816 | * @return {Object} Instance of $httpBackend mock 817 | */ 818 | function createHttpBackendMock($delegate, $browser) { 819 | var definitions = [], 820 | expectations = [], 821 | responses = [], 822 | responsesPush = angular.bind(responses, responses.push); 823 | 824 | function createResponse(status, data, headers) { 825 | if (angular.isFunction(status)) return status; 826 | 827 | return function() { 828 | return angular.isNumber(status) 829 | ? [status, data, headers] 830 | : [200, status, data]; 831 | }; 832 | } 833 | 834 | // TODO(vojta): change params to: method, url, data, headers, callback 835 | function $httpBackend(method, url, data, callback, headers) { 836 | var xhr = new MockXhr(), 837 | expectation = expectations[0], 838 | wasExpected = false; 839 | 840 | function prettyPrint(data) { 841 | return (angular.isString(data) || angular.isFunction(data) || data instanceof RegExp) 842 | ? data 843 | : angular.toJson(data); 844 | } 845 | 846 | if (expectation && expectation.match(method, url)) { 847 | if (!expectation.matchData(data)) 848 | throw Error('Expected ' + expectation + ' with different data\n' + 849 | 'EXPECTED: ' + prettyPrint(expectation.data) + '\nGOT: ' + data); 850 | 851 | if (!expectation.matchHeaders(headers)) 852 | throw Error('Expected ' + expectation + ' with different headers\n' + 853 | 'EXPECTED: ' + prettyPrint(expectation.headers) + '\nGOT: ' + 854 | prettyPrint(headers)); 855 | 856 | expectations.shift(); 857 | 858 | if (expectation.response) { 859 | responses.push(function() { 860 | var response = expectation.response(method, url, data, headers); 861 | xhr.$$respHeaders = response[2]; 862 | callback(response[0], response[1], xhr.getAllResponseHeaders()); 863 | }); 864 | return; 865 | } 866 | wasExpected = true; 867 | } 868 | 869 | var i = -1, definition; 870 | while ((definition = definitions[++i])) { 871 | if (definition.match(method, url, data, headers || {})) { 872 | if (definition.response) { 873 | // if $browser specified, we do auto flush all requests 874 | ($browser ? $browser.defer : responsesPush)(function() { 875 | var response = definition.response(method, url, data, headers); 876 | xhr.$$respHeaders = response[2]; 877 | callback(response[0], response[1], xhr.getAllResponseHeaders()); 878 | }); 879 | } else if (definition.passThrough) { 880 | $delegate(method, url, data, callback, headers); 881 | } else throw Error('No response defined !'); 882 | return; 883 | } 884 | } 885 | throw wasExpected ? 886 | Error('No response defined !') : 887 | Error('Unexpected request: ' + method + ' ' + url + '\n' + 888 | (expectation ? 'Expected ' + expectation : 'No more request expected')); 889 | } 890 | 891 | /** 892 | * @ngdoc method 893 | * @name ngMock.$httpBackend#when 894 | * @methodOf ngMock.$httpBackend 895 | * @description 896 | * Creates a new backend definition. 897 | * 898 | * @param {string} method HTTP method. 899 | * @param {string|RegExp} url HTTP url. 900 | * @param {(string|RegExp)=} data HTTP request body. 901 | * @param {(Object|function(Object))=} headers HTTP headers or function that receives http header 902 | * object and returns true if the headers match the current definition. 903 | * @returns {requestHandler} Returns an object with `respond` method that control how a matched 904 | * request is handled. 905 | * 906 | * - respond – `{function([status,] data[, headers])|function(function(method, url, data, headers)}` 907 | * – The respond method takes a set of static data to be returned or a function that can return 908 | * an array containing response status (number), response data (string) and response headers 909 | * (Object). 910 | */ 911 | $httpBackend.when = function(method, url, data, headers) { 912 | var definition = new MockHttpExpectation(method, url, data, headers), 913 | chain = { 914 | respond: function(status, data, headers) { 915 | definition.response = createResponse(status, data, headers); 916 | } 917 | }; 918 | 919 | if ($browser) { 920 | chain.passThrough = function() { 921 | definition.passThrough = true; 922 | }; 923 | } 924 | 925 | definitions.push(definition); 926 | return chain; 927 | }; 928 | 929 | /** 930 | * @ngdoc method 931 | * @name ngMock.$httpBackend#whenGET 932 | * @methodOf ngMock.$httpBackend 933 | * @description 934 | * Creates a new backend definition for GET requests. For more info see `when()`. 935 | * 936 | * @param {string|RegExp} url HTTP url. 937 | * @param {(Object|function(Object))=} headers HTTP headers. 938 | * @returns {requestHandler} Returns an object with `respond` method that control how a matched 939 | * request is handled. 940 | */ 941 | 942 | /** 943 | * @ngdoc method 944 | * @name ngMock.$httpBackend#whenHEAD 945 | * @methodOf ngMock.$httpBackend 946 | * @description 947 | * Creates a new backend definition for HEAD requests. For more info see `when()`. 948 | * 949 | * @param {string|RegExp} url HTTP url. 950 | * @param {(Object|function(Object))=} headers HTTP headers. 951 | * @returns {requestHandler} Returns an object with `respond` method that control how a matched 952 | * request is handled. 953 | */ 954 | 955 | /** 956 | * @ngdoc method 957 | * @name ngMock.$httpBackend#whenDELETE 958 | * @methodOf ngMock.$httpBackend 959 | * @description 960 | * Creates a new backend definition for DELETE requests. For more info see `when()`. 961 | * 962 | * @param {string|RegExp} url HTTP url. 963 | * @param {(Object|function(Object))=} headers HTTP headers. 964 | * @returns {requestHandler} Returns an object with `respond` method that control how a matched 965 | * request is handled. 966 | */ 967 | 968 | /** 969 | * @ngdoc method 970 | * @name ngMock.$httpBackend#whenPOST 971 | * @methodOf ngMock.$httpBackend 972 | * @description 973 | * Creates a new backend definition for POST requests. For more info see `when()`. 974 | * 975 | * @param {string|RegExp} url HTTP url. 976 | * @param {(string|RegExp)=} data HTTP request body. 977 | * @param {(Object|function(Object))=} headers HTTP headers. 978 | * @returns {requestHandler} Returns an object with `respond` method that control how a matched 979 | * request is handled. 980 | */ 981 | 982 | /** 983 | * @ngdoc method 984 | * @name ngMock.$httpBackend#whenPUT 985 | * @methodOf ngMock.$httpBackend 986 | * @description 987 | * Creates a new backend definition for PUT requests. For more info see `when()`. 988 | * 989 | * @param {string|RegExp} url HTTP url. 990 | * @param {(string|RegExp)=} data HTTP request body. 991 | * @param {(Object|function(Object))=} headers HTTP headers. 992 | * @returns {requestHandler} Returns an object with `respond` method that control how a matched 993 | * request is handled. 994 | */ 995 | 996 | /** 997 | * @ngdoc method 998 | * @name ngMock.$httpBackend#whenJSONP 999 | * @methodOf ngMock.$httpBackend 1000 | * @description 1001 | * Creates a new backend definition for JSONP requests. For more info see `when()`. 1002 | * 1003 | * @param {string|RegExp} url HTTP url. 1004 | * @returns {requestHandler} Returns an object with `respond` method that control how a matched 1005 | * request is handled. 1006 | */ 1007 | createShortMethods('when'); 1008 | 1009 | 1010 | /** 1011 | * @ngdoc method 1012 | * @name ngMock.$httpBackend#expect 1013 | * @methodOf ngMock.$httpBackend 1014 | * @description 1015 | * Creates a new request expectation. 1016 | * 1017 | * @param {string} method HTTP method. 1018 | * @param {string|RegExp} url HTTP url. 1019 | * @param {(string|RegExp)=} data HTTP request body. 1020 | * @param {(Object|function(Object))=} headers HTTP headers or function that receives http header 1021 | * object and returns true if the headers match the current expectation. 1022 | * @returns {requestHandler} Returns an object with `respond` method that control how a matched 1023 | * request is handled. 1024 | * 1025 | * - respond – `{function([status,] data[, headers])|function(function(method, url, data, headers)}` 1026 | * – The respond method takes a set of static data to be returned or a function that can return 1027 | * an array containing response status (number), response data (string) and response headers 1028 | * (Object). 1029 | */ 1030 | $httpBackend.expect = function(method, url, data, headers) { 1031 | var expectation = new MockHttpExpectation(method, url, data, headers); 1032 | expectations.push(expectation); 1033 | return { 1034 | respond: function(status, data, headers) { 1035 | expectation.response = createResponse(status, data, headers); 1036 | } 1037 | }; 1038 | }; 1039 | 1040 | 1041 | /** 1042 | * @ngdoc method 1043 | * @name ngMock.$httpBackend#expectGET 1044 | * @methodOf ngMock.$httpBackend 1045 | * @description 1046 | * Creates a new request expectation for GET requests. For more info see `expect()`. 1047 | * 1048 | * @param {string|RegExp} url HTTP url. 1049 | * @param {Object=} headers HTTP headers. 1050 | * @returns {requestHandler} Returns an object with `respond` method that control how a matched 1051 | * request is handled. See #expect for more info. 1052 | */ 1053 | 1054 | /** 1055 | * @ngdoc method 1056 | * @name ngMock.$httpBackend#expectHEAD 1057 | * @methodOf ngMock.$httpBackend 1058 | * @description 1059 | * Creates a new request expectation for HEAD requests. For more info see `expect()`. 1060 | * 1061 | * @param {string|RegExp} url HTTP url. 1062 | * @param {Object=} headers HTTP headers. 1063 | * @returns {requestHandler} Returns an object with `respond` method that control how a matched 1064 | * request is handled. 1065 | */ 1066 | 1067 | /** 1068 | * @ngdoc method 1069 | * @name ngMock.$httpBackend#expectDELETE 1070 | * @methodOf ngMock.$httpBackend 1071 | * @description 1072 | * Creates a new request expectation for DELETE requests. For more info see `expect()`. 1073 | * 1074 | * @param {string|RegExp} url HTTP url. 1075 | * @param {Object=} headers HTTP headers. 1076 | * @returns {requestHandler} Returns an object with `respond` method that control how a matched 1077 | * request is handled. 1078 | */ 1079 | 1080 | /** 1081 | * @ngdoc method 1082 | * @name ngMock.$httpBackend#expectPOST 1083 | * @methodOf ngMock.$httpBackend 1084 | * @description 1085 | * Creates a new request expectation for POST requests. For more info see `expect()`. 1086 | * 1087 | * @param {string|RegExp} url HTTP url. 1088 | * @param {(string|RegExp)=} data HTTP request body. 1089 | * @param {Object=} headers HTTP headers. 1090 | * @returns {requestHandler} Returns an object with `respond` method that control how a matched 1091 | * request is handled. 1092 | */ 1093 | 1094 | /** 1095 | * @ngdoc method 1096 | * @name ngMock.$httpBackend#expectPUT 1097 | * @methodOf ngMock.$httpBackend 1098 | * @description 1099 | * Creates a new request expectation for PUT requests. For more info see `expect()`. 1100 | * 1101 | * @param {string|RegExp} url HTTP url. 1102 | * @param {(string|RegExp)=} data HTTP request body. 1103 | * @param {Object=} headers HTTP headers. 1104 | * @returns {requestHandler} Returns an object with `respond` method that control how a matched 1105 | * request is handled. 1106 | */ 1107 | 1108 | /** 1109 | * @ngdoc method 1110 | * @name ngMock.$httpBackend#expectPATCH 1111 | * @methodOf ngMock.$httpBackend 1112 | * @description 1113 | * Creates a new request expectation for PATCH requests. For more info see `expect()`. 1114 | * 1115 | * @param {string|RegExp} url HTTP url. 1116 | * @param {(string|RegExp)=} data HTTP request body. 1117 | * @param {Object=} headers HTTP headers. 1118 | * @returns {requestHandler} Returns an object with `respond` method that control how a matched 1119 | * request is handled. 1120 | */ 1121 | 1122 | /** 1123 | * @ngdoc method 1124 | * @name ngMock.$httpBackend#expectJSONP 1125 | * @methodOf ngMock.$httpBackend 1126 | * @description 1127 | * Creates a new request expectation for JSONP requests. For more info see `expect()`. 1128 | * 1129 | * @param {string|RegExp} url HTTP url. 1130 | * @returns {requestHandler} Returns an object with `respond` method that control how a matched 1131 | * request is handled. 1132 | */ 1133 | createShortMethods('expect'); 1134 | 1135 | 1136 | /** 1137 | * @ngdoc method 1138 | * @name ngMock.$httpBackend#flush 1139 | * @methodOf ngMock.$httpBackend 1140 | * @description 1141 | * Flushes all pending requests using the trained responses. 1142 | * 1143 | * @param {number=} count Number of responses to flush (in the order they arrived). If undefined, 1144 | * all pending requests will be flushed. If there are no pending requests when the flush method 1145 | * is called an exception is thrown (as this typically a sign of programming error). 1146 | */ 1147 | $httpBackend.flush = function(count) { 1148 | if (!responses.length) throw Error('No pending request to flush !'); 1149 | 1150 | if (angular.isDefined(count)) { 1151 | while (count--) { 1152 | if (!responses.length) throw Error('No more pending request to flush !'); 1153 | responses.shift()(); 1154 | } 1155 | } else { 1156 | while (responses.length) { 1157 | responses.shift()(); 1158 | } 1159 | } 1160 | $httpBackend.verifyNoOutstandingExpectation(); 1161 | }; 1162 | 1163 | 1164 | /** 1165 | * @ngdoc method 1166 | * @name ngMock.$httpBackend#verifyNoOutstandingExpectation 1167 | * @methodOf ngMock.$httpBackend 1168 | * @description 1169 | * Verifies that all of the requests defined via the `expect` api were made. If any of the 1170 | * requests were not made, verifyNoOutstandingExpectation throws an exception. 1171 | * 1172 | * Typically, you would call this method following each test case that asserts requests using an 1173 | * "afterEach" clause. 1174 | * 1175 | *
1176 |    *   afterEach($httpBackend.verifyExpectations);
1177 |    * 
1178 | */ 1179 | $httpBackend.verifyNoOutstandingExpectation = function() { 1180 | if (expectations.length) { 1181 | throw Error('Unsatisfied requests: ' + expectations.join(', ')); 1182 | } 1183 | }; 1184 | 1185 | 1186 | /** 1187 | * @ngdoc method 1188 | * @name ngMock.$httpBackend#verifyNoOutstandingRequest 1189 | * @methodOf ngMock.$httpBackend 1190 | * @description 1191 | * Verifies that there are no outstanding requests that need to be flushed. 1192 | * 1193 | * Typically, you would call this method following each test case that asserts requests using an 1194 | * "afterEach" clause. 1195 | * 1196 | *
1197 |    *   afterEach($httpBackend.verifyNoOutstandingRequest);
1198 |    * 
1199 | */ 1200 | $httpBackend.verifyNoOutstandingRequest = function() { 1201 | if (responses.length) { 1202 | throw Error('Unflushed requests: ' + responses.length); 1203 | } 1204 | }; 1205 | 1206 | 1207 | /** 1208 | * @ngdoc method 1209 | * @name ngMock.$httpBackend#resetExpectations 1210 | * @methodOf ngMock.$httpBackend 1211 | * @description 1212 | * Resets all request expectations, but preserves all backend definitions. Typically, you would 1213 | * call resetExpectations during a multiple-phase test when you want to reuse the same instance of 1214 | * $httpBackend mock. 1215 | */ 1216 | $httpBackend.resetExpectations = function() { 1217 | expectations.length = 0; 1218 | responses.length = 0; 1219 | }; 1220 | 1221 | return $httpBackend; 1222 | 1223 | 1224 | function createShortMethods(prefix) { 1225 | angular.forEach(['GET', 'DELETE', 'JSONP'], function(method) { 1226 | $httpBackend[prefix + method] = function(url, headers) { 1227 | return $httpBackend[prefix](method, url, undefined, headers) 1228 | } 1229 | }); 1230 | 1231 | angular.forEach(['PUT', 'POST', 'PATCH'], function(method) { 1232 | $httpBackend[prefix + method] = function(url, data, headers) { 1233 | return $httpBackend[prefix](method, url, data, headers) 1234 | } 1235 | }); 1236 | } 1237 | } 1238 | 1239 | function MockHttpExpectation(method, url, data, headers) { 1240 | 1241 | this.data = data; 1242 | this.headers = headers; 1243 | 1244 | this.match = function(m, u, d, h) { 1245 | if (method != m) return false; 1246 | if (!this.matchUrl(u)) return false; 1247 | if (angular.isDefined(d) && !this.matchData(d)) return false; 1248 | if (angular.isDefined(h) && !this.matchHeaders(h)) return false; 1249 | return true; 1250 | }; 1251 | 1252 | this.matchUrl = function(u) { 1253 | if (!url) return true; 1254 | if (angular.isFunction(url.test)) return url.test(u); 1255 | return url == u; 1256 | }; 1257 | 1258 | this.matchHeaders = function(h) { 1259 | if (angular.isUndefined(headers)) return true; 1260 | if (angular.isFunction(headers)) return headers(h); 1261 | return angular.equals(headers, h); 1262 | }; 1263 | 1264 | this.matchData = function(d) { 1265 | if (angular.isUndefined(data)) return true; 1266 | if (data && angular.isFunction(data.test)) return data.test(d); 1267 | if (data && !angular.isString(data)) return angular.toJson(data) == d; 1268 | return data == d; 1269 | }; 1270 | 1271 | this.toString = function() { 1272 | return method + ' ' + url; 1273 | }; 1274 | } 1275 | 1276 | function MockXhr() { 1277 | 1278 | // hack for testing $http, $httpBackend 1279 | MockXhr.$$lastInstance = this; 1280 | 1281 | this.open = function(method, url, async) { 1282 | this.$$method = method; 1283 | this.$$url = url; 1284 | this.$$async = async; 1285 | this.$$reqHeaders = {}; 1286 | this.$$respHeaders = {}; 1287 | }; 1288 | 1289 | this.send = function(data) { 1290 | this.$$data = data; 1291 | }; 1292 | 1293 | this.setRequestHeader = function(key, value) { 1294 | this.$$reqHeaders[key] = value; 1295 | }; 1296 | 1297 | this.getResponseHeader = function(name) { 1298 | // the lookup must be case insensitive, that's why we try two quick lookups and full scan at last 1299 | var header = this.$$respHeaders[name]; 1300 | if (header) return header; 1301 | 1302 | name = angular.lowercase(name); 1303 | header = this.$$respHeaders[name]; 1304 | if (header) return header; 1305 | 1306 | header = undefined; 1307 | angular.forEach(this.$$respHeaders, function(headerVal, headerName) { 1308 | if (!header && angular.lowercase(headerName) == name) header = headerVal; 1309 | }); 1310 | return header; 1311 | }; 1312 | 1313 | this.getAllResponseHeaders = function() { 1314 | var lines = []; 1315 | 1316 | angular.forEach(this.$$respHeaders, function(value, key) { 1317 | lines.push(key + ': ' + value); 1318 | }); 1319 | return lines.join('\n'); 1320 | }; 1321 | 1322 | this.abort = angular.noop; 1323 | } 1324 | 1325 | 1326 | /** 1327 | * @ngdoc function 1328 | * @name ngMock.$timeout 1329 | * @description 1330 | * 1331 | * This service is just a simple decorator for {@link ng.$timeout $timeout} service 1332 | * that adds a "flush" method. 1333 | */ 1334 | 1335 | /** 1336 | * @ngdoc method 1337 | * @name ngMock.$timeout#flush 1338 | * @methodOf ngMock.$timeout 1339 | * @description 1340 | * 1341 | * Flushes the queue of pending tasks. 1342 | */ 1343 | 1344 | /** 1345 | * 1346 | */ 1347 | angular.mock.$RootElementProvider = function() { 1348 | this.$get = function() { 1349 | return angular.element('
'); 1350 | } 1351 | }; 1352 | 1353 | /** 1354 | * @ngdoc overview 1355 | * @name ngMock 1356 | * @description 1357 | * 1358 | * The `ngMock` is an angular module which is used with `ng` module and adds unit-test configuration as well as useful 1359 | * mocks to the {@link AUTO.$injector $injector}. 1360 | */ 1361 | angular.module('ngMock', ['ng']).provider({ 1362 | $browser: angular.mock.$BrowserProvider, 1363 | $exceptionHandler: angular.mock.$ExceptionHandlerProvider, 1364 | $log: angular.mock.$LogProvider, 1365 | $httpBackend: angular.mock.$HttpBackendProvider, 1366 | $rootElement: angular.mock.$RootElementProvider 1367 | }).config(function($provide) { 1368 | $provide.decorator('$timeout', function($delegate, $browser) { 1369 | $delegate.flush = function() { 1370 | $browser.defer.flush(); 1371 | }; 1372 | return $delegate; 1373 | }); 1374 | }); 1375 | 1376 | 1377 | /** 1378 | * @ngdoc overview 1379 | * @name ngMockE2E 1380 | * @description 1381 | * 1382 | * The `ngMockE2E` is an angular module which contains mocks suitable for end-to-end testing. 1383 | * Currently there is only one mock present in this module - 1384 | * the {@link ngMockE2E.$httpBackend e2e $httpBackend} mock. 1385 | */ 1386 | angular.module('ngMockE2E', ['ng']).config(function($provide) { 1387 | $provide.decorator('$httpBackend', angular.mock.e2e.$httpBackendDecorator); 1388 | }); 1389 | 1390 | /** 1391 | * @ngdoc object 1392 | * @name ngMockE2E.$httpBackend 1393 | * @description 1394 | * Fake HTTP backend implementation suitable for end-to-end testing or backend-less development of 1395 | * applications that use the {@link ng.$http $http service}. 1396 | * 1397 | * *Note*: For fake http backend implementation suitable for unit testing please see 1398 | * {@link ngMock.$httpBackend unit-testing $httpBackend mock}. 1399 | * 1400 | * This implementation can be used to respond with static or dynamic responses via the `when` api 1401 | * and its shortcuts (`whenGET`, `whenPOST`, etc) and optionally pass through requests to the 1402 | * real $httpBackend for specific requests (e.g. to interact with certain remote apis or to fetch 1403 | * templates from a webserver). 1404 | * 1405 | * As opposed to unit-testing, in an end-to-end testing scenario or in scenario when an application 1406 | * is being developed with the real backend api replaced with a mock, it is often desirable for 1407 | * certain category of requests to bypass the mock and issue a real http request (e.g. to fetch 1408 | * templates or static files from the webserver). To configure the backend with this behavior 1409 | * use the `passThrough` request handler of `when` instead of `respond`. 1410 | * 1411 | * Additionally, we don't want to manually have to flush mocked out requests like we do during unit 1412 | * testing. For this reason the e2e $httpBackend automatically flushes mocked out requests 1413 | * automatically, closely simulating the behavior of the XMLHttpRequest object. 1414 | * 1415 | * To setup the application to run with this http backend, you have to create a module that depends 1416 | * on the `ngMockE2E` and your application modules and defines the fake backend: 1417 | * 1418 | *
1419 |  *   myAppDev = angular.module('myAppDev', ['myApp', 'ngMockE2E']);
1420 |  *   myAppDev.run(function($httpBackend) {
1421 |  *     phones = [{name: 'phone1'}, {name: 'phone2'}];
1422 |  *
1423 |  *     // returns the current list of phones
1424 |  *     $httpBackend.whenGET('/phones').respond(phones);
1425 |  *
1426 |  *     // adds a new phone to the phones array
1427 |  *     $httpBackend.whenPOST('/phones').respond(function(method, url, data) {
1428 |  *       phones.push(angular.fromJSON(data));
1429 |  *     });
1430 |  *     $httpBackend.whenGET(/^\/templates\//).passThrough();
1431 |  *     //...
1432 |  *   });
1433 |  * 
1434 | * 1435 | * Afterwards, bootstrap your app with this new module. 1436 | */ 1437 | 1438 | /** 1439 | * @ngdoc method 1440 | * @name ngMockE2E.$httpBackend#when 1441 | * @methodOf ngMockE2E.$httpBackend 1442 | * @description 1443 | * Creates a new backend definition. 1444 | * 1445 | * @param {string} method HTTP method. 1446 | * @param {string|RegExp} url HTTP url. 1447 | * @param {(string|RegExp)=} data HTTP request body. 1448 | * @param {(Object|function(Object))=} headers HTTP headers or function that receives http header 1449 | * object and returns true if the headers match the current definition. 1450 | * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that 1451 | * control how a matched request is handled. 1452 | * 1453 | * - respond – `{function([status,] data[, headers])|function(function(method, url, data, headers)}` 1454 | * – The respond method takes a set of static data to be returned or a function that can return 1455 | * an array containing response status (number), response data (string) and response headers 1456 | * (Object). 1457 | * - passThrough – `{function()}` – Any request matching a backend definition with `passThrough` 1458 | * handler, will be pass through to the real backend (an XHR request will be made to the 1459 | * server. 1460 | */ 1461 | 1462 | /** 1463 | * @ngdoc method 1464 | * @name ngMockE2E.$httpBackend#whenGET 1465 | * @methodOf ngMockE2E.$httpBackend 1466 | * @description 1467 | * Creates a new backend definition for GET requests. For more info see `when()`. 1468 | * 1469 | * @param {string|RegExp} url HTTP url. 1470 | * @param {(Object|function(Object))=} headers HTTP headers. 1471 | * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that 1472 | * control how a matched request is handled. 1473 | */ 1474 | 1475 | /** 1476 | * @ngdoc method 1477 | * @name ngMockE2E.$httpBackend#whenHEAD 1478 | * @methodOf ngMockE2E.$httpBackend 1479 | * @description 1480 | * Creates a new backend definition for HEAD requests. For more info see `when()`. 1481 | * 1482 | * @param {string|RegExp} url HTTP url. 1483 | * @param {(Object|function(Object))=} headers HTTP headers. 1484 | * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that 1485 | * control how a matched request is handled. 1486 | */ 1487 | 1488 | /** 1489 | * @ngdoc method 1490 | * @name ngMockE2E.$httpBackend#whenDELETE 1491 | * @methodOf ngMockE2E.$httpBackend 1492 | * @description 1493 | * Creates a new backend definition for DELETE requests. For more info see `when()`. 1494 | * 1495 | * @param {string|RegExp} url HTTP url. 1496 | * @param {(Object|function(Object))=} headers HTTP headers. 1497 | * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that 1498 | * control how a matched request is handled. 1499 | */ 1500 | 1501 | /** 1502 | * @ngdoc method 1503 | * @name ngMockE2E.$httpBackend#whenPOST 1504 | * @methodOf ngMockE2E.$httpBackend 1505 | * @description 1506 | * Creates a new backend definition for POST requests. For more info see `when()`. 1507 | * 1508 | * @param {string|RegExp} url HTTP url. 1509 | * @param {(string|RegExp)=} data HTTP request body. 1510 | * @param {(Object|function(Object))=} headers HTTP headers. 1511 | * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that 1512 | * control how a matched request is handled. 1513 | */ 1514 | 1515 | /** 1516 | * @ngdoc method 1517 | * @name ngMockE2E.$httpBackend#whenPUT 1518 | * @methodOf ngMockE2E.$httpBackend 1519 | * @description 1520 | * Creates a new backend definition for PUT requests. For more info see `when()`. 1521 | * 1522 | * @param {string|RegExp} url HTTP url. 1523 | * @param {(string|RegExp)=} data HTTP request body. 1524 | * @param {(Object|function(Object))=} headers HTTP headers. 1525 | * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that 1526 | * control how a matched request is handled. 1527 | */ 1528 | 1529 | /** 1530 | * @ngdoc method 1531 | * @name ngMockE2E.$httpBackend#whenPATCH 1532 | * @methodOf ngMockE2E.$httpBackend 1533 | * @description 1534 | * Creates a new backend definition for PATCH requests. For more info see `when()`. 1535 | * 1536 | * @param {string|RegExp} url HTTP url. 1537 | * @param {(string|RegExp)=} data HTTP request body. 1538 | * @param {(Object|function(Object))=} headers HTTP headers. 1539 | * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that 1540 | * control how a matched request is handled. 1541 | */ 1542 | 1543 | /** 1544 | * @ngdoc method 1545 | * @name ngMockE2E.$httpBackend#whenJSONP 1546 | * @methodOf ngMockE2E.$httpBackend 1547 | * @description 1548 | * Creates a new backend definition for JSONP requests. For more info see `when()`. 1549 | * 1550 | * @param {string|RegExp} url HTTP url. 1551 | * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that 1552 | * control how a matched request is handled. 1553 | */ 1554 | angular.mock.e2e = {}; 1555 | angular.mock.e2e.$httpBackendDecorator = ['$delegate', '$browser', createHttpBackendMock]; 1556 | 1557 | 1558 | angular.mock.clearDataCache = function() { 1559 | var key, 1560 | cache = angular.element.cache; 1561 | 1562 | for(key in cache) { 1563 | if (cache.hasOwnProperty(key)) { 1564 | var handle = cache[key].handle; 1565 | 1566 | handle && angular.element(handle.elem).unbind(); 1567 | delete cache[key]; 1568 | } 1569 | } 1570 | }; 1571 | 1572 | 1573 | window.jstestdriver && (function(window) { 1574 | /** 1575 | * Global method to output any number of objects into JSTD console. Useful for debugging. 1576 | */ 1577 | window.dump = function() { 1578 | var args = []; 1579 | angular.forEach(arguments, function(arg) { 1580 | args.push(angular.mock.dump(arg)); 1581 | }); 1582 | jstestdriver.console.log.apply(jstestdriver.console, args); 1583 | if (window.console) { 1584 | window.console.log.apply(window.console, args); 1585 | } 1586 | }; 1587 | })(window); 1588 | 1589 | 1590 | window.jasmine && (function(window) { 1591 | 1592 | afterEach(function() { 1593 | var spec = getCurrentSpec(); 1594 | var injector = spec.$injector; 1595 | 1596 | spec.$injector = null; 1597 | spec.$modules = null; 1598 | 1599 | if (injector) { 1600 | injector.get('$rootElement').unbind(); 1601 | injector.get('$browser').pollFns.length = 0; 1602 | } 1603 | 1604 | angular.mock.clearDataCache(); 1605 | 1606 | // clean up jquery's fragment cache 1607 | angular.forEach(angular.element.fragments, function(val, key) { 1608 | delete angular.element.fragments[key]; 1609 | }); 1610 | 1611 | MockXhr.$$lastInstance = null; 1612 | 1613 | angular.forEach(angular.callbacks, function(val, key) { 1614 | delete angular.callbacks[key]; 1615 | }); 1616 | angular.callbacks.counter = 0; 1617 | }); 1618 | 1619 | function getCurrentSpec() { 1620 | return jasmine.getEnv().currentSpec; 1621 | } 1622 | 1623 | function isSpecRunning() { 1624 | var spec = getCurrentSpec(); 1625 | return spec && spec.queue.running; 1626 | } 1627 | 1628 | /** 1629 | * @ngdoc function 1630 | * @name angular.mock.module 1631 | * @description 1632 | * 1633 | * *NOTE*: This is function is also published on window for easy access.
1634 | * *NOTE*: Only available with {@link http://pivotal.github.com/jasmine/ jasmine}. 1635 | * 1636 | * This function registers a module configuration code. It collects the configuration information 1637 | * which will be used when the injector is created by {@link angular.mock.inject inject}. 1638 | * 1639 | * See {@link angular.mock.inject inject} for usage example 1640 | * 1641 | * @param {...(string|Function)} fns any number of modules which are represented as string 1642 | * aliases or as anonymous module initialization functions. The modules are used to 1643 | * configure the injector. The 'ng' and 'ngMock' modules are automatically loaded. 1644 | */ 1645 | window.module = angular.mock.module = function() { 1646 | var moduleFns = Array.prototype.slice.call(arguments, 0); 1647 | return isSpecRunning() ? workFn() : workFn; 1648 | ///////////////////// 1649 | function workFn() { 1650 | var spec = getCurrentSpec(); 1651 | if (spec.$injector) { 1652 | throw Error('Injector already created, can not register a module!'); 1653 | } else { 1654 | var modules = spec.$modules || (spec.$modules = []); 1655 | angular.forEach(moduleFns, function(module) { 1656 | modules.push(module); 1657 | }); 1658 | } 1659 | } 1660 | }; 1661 | 1662 | /** 1663 | * @ngdoc function 1664 | * @name angular.mock.inject 1665 | * @description 1666 | * 1667 | * *NOTE*: This is function is also published on window for easy access.
1668 | * *NOTE*: Only available with {@link http://pivotal.github.com/jasmine/ jasmine}. 1669 | * 1670 | * The inject function wraps a function into an injectable function. The inject() creates new 1671 | * instance of {@link AUTO.$injector $injector} per test, which is then used for 1672 | * resolving references. 1673 | * 1674 | * See also {@link angular.mock.module module} 1675 | * 1676 | * Example of what a typical jasmine tests looks like with the inject method. 1677 | *
1678 |    *
1679 |    *   angular.module('myApplicationModule', [])
1680 |    *       .value('mode', 'app')
1681 |    *       .value('version', 'v1.0.1');
1682 |    *
1683 |    *
1684 |    *   describe('MyApp', function() {
1685 |    *
1686 |    *     // You need to load modules that you want to test,
1687 |    *     // it loads only the "ng" module by default.
1688 |    *     beforeEach(module('myApplicationModule'));
1689 |    *
1690 |    *
1691 |    *     // inject() is used to inject arguments of all given functions
1692 |    *     it('should provide a version', inject(function(mode, version) {
1693 |    *       expect(version).toEqual('v1.0.1');
1694 |    *       expect(mode).toEqual('app');
1695 |    *     }));
1696 |    *
1697 |    *
1698 |    *     // The inject and module method can also be used inside of the it or beforeEach
1699 |    *     it('should override a version and test the new version is injected', function() {
1700 |    *       // module() takes functions or strings (module aliases)
1701 |    *       module(function($provide) {
1702 |    *         $provide.value('version', 'overridden'); // override version here
1703 |    *       });
1704 |    *
1705 |    *       inject(function(version) {
1706 |    *         expect(version).toEqual('overridden');
1707 |    *       });
1708 |    *     ));
1709 |    *   });
1710 |    *
1711 |    * 
1712 | * 1713 | * @param {...Function} fns any number of functions which will be injected using the injector. 1714 | */ 1715 | window.inject = angular.mock.inject = function() { 1716 | var blockFns = Array.prototype.slice.call(arguments, 0); 1717 | var errorForStack = new Error('Declaration Location'); 1718 | return isSpecRunning() ? workFn() : workFn; 1719 | ///////////////////// 1720 | function workFn() { 1721 | var spec = getCurrentSpec(); 1722 | var modules = spec.$modules || []; 1723 | modules.unshift('ngMock'); 1724 | modules.unshift('ng'); 1725 | var injector = spec.$injector; 1726 | if (!injector) { 1727 | injector = spec.$injector = angular.injector(modules); 1728 | } 1729 | for(var i = 0, ii = blockFns.length; i < ii; i++) { 1730 | try { 1731 | injector.invoke(blockFns[i] || angular.noop, this); 1732 | } catch (e) { 1733 | if(e.stack) e.stack += '\n' + errorForStack.stack; 1734 | throw e; 1735 | } finally { 1736 | errorForStack = null; 1737 | } 1738 | } 1739 | } 1740 | }; 1741 | })(window); 1742 | -------------------------------------------------------------------------------- /test/support/jasmine.async.js: -------------------------------------------------------------------------------- 1 | // Jasmine.Async, v0.1.0 2 | // Copyright (c)2012 Muted Solutions, LLC. All Rights Reserved. 3 | // Distributed under MIT license 4 | // http://github.com/derickbailey/jasmine.async 5 | this.AsyncSpec = (function(global){ 6 | 7 | // Private Methods 8 | // --------------- 9 | 10 | function runAsync(block){ 11 | return function(){ 12 | var done = false; 13 | var complete = function(){ done = true; }; 14 | 15 | runs(function(){ 16 | block(complete); 17 | }); 18 | 19 | waitsFor(function(){ 20 | return done; 21 | }); 22 | }; 23 | } 24 | 25 | // Constructor Function 26 | // -------------------- 27 | 28 | function AsyncSpec(spec){ 29 | this.spec = spec; 30 | } 31 | 32 | // Public API 33 | // ---------- 34 | 35 | AsyncSpec.prototype.beforeEach = function(block){ 36 | this.spec.beforeEach(runAsync(block)); 37 | }; 38 | 39 | AsyncSpec.prototype.afterEach = function(block){ 40 | this.spec.afterEach(runAsync(block)); 41 | }; 42 | 43 | AsyncSpec.prototype.it = function(description, block){ 44 | // For some reason, `it` is not attached to the current 45 | // test suite, so it has to be called from the global 46 | // context. 47 | global.it(description, runAsync(block)); 48 | }; 49 | 50 | return AsyncSpec; 51 | })(this); -------------------------------------------------------------------------------- /test/unit/authSpec.coffee: -------------------------------------------------------------------------------- 1 | describe 'auth', -> 2 | backend = null 3 | signedHeaders = null 4 | url = null 5 | 6 | afterEach inject ($window) -> 7 | $window.localStorage.clear() 8 | 9 | 10 | beforeEach -> 11 | angular.module('ParseSpec', ['Parse']).config (ParseProvider) -> 12 | ParseProvider.initialize 'appId', 'apiKey' 13 | 14 | module 'ParseSpec' 15 | 16 | inject (Parse, $injector) -> 17 | # Helper method for matching API URLS 18 | url = (path) -> 19 | "#{Parse.BaseUrl}#{path}" 20 | 21 | backend = $injector.get('$httpBackend') 22 | 23 | signedHeaders = (headers) -> 24 | headers["X-Parse-Application-Id"] == 'appId' and 25 | headers["X-Parse-REST-API-KEY"] == "apiKey" 26 | 27 | afterEach -> 28 | backend.verifyNoOutstandingRequest() 29 | backend.verifyNoOutstandingExpectation() 30 | 31 | it 'has an auth property', inject (Parse) -> 32 | expect(Parse.auth).not.toBeUndefined() 33 | 34 | describe 'registering', -> 35 | 36 | beforeEach inject (Parse, $window) -> 37 | $window.localStorage = {} 38 | backend.expectPOST( 39 | url("/users"), 40 | {username: "johndoe", password: 'foobar'} 41 | signedHeaders 42 | ).respond( 43 | { 44 | "createdAt": "2011-11-07T20:58:34.448Z" 45 | "objectId": "g7y9tkhB7O" 46 | "sessionToken": "sessionTok" 47 | } 48 | ) 49 | 50 | Parse.auth.register("johndoe", "foobar") 51 | backend.flush() 52 | 53 | it 'sets the session token', inject (Parse) -> 54 | expect(Parse.auth.sessionToken).toBe('sessionTok') 55 | 56 | it 'sets the current user', inject (Parse) -> 57 | user = Parse.auth.currentUser 58 | expect(user.objectId).toBe('g7y9tkhB7O') 59 | 60 | it 'clears the password on registation', inject ($window, Parse) -> 61 | user = Parse.auth.currentUser 62 | expect(user.password).toBeUndefined() 63 | 64 | 65 | it 'stores sessionId to localStorage', inject ($window) -> 66 | expect($window.localStorage.PARSE_SESSION_TOKEN).toBe 'sessionTok' 67 | 68 | it 'stores user to localStorage', inject ($window, Parse) -> 69 | info = Parse.auth.currentUser.attributes() 70 | info.objectId = Parse.auth.currentUser.objectId 71 | expect($window.localStorage.PARSE_USER_INFO).toBe JSON.stringify(info) 72 | 73 | describe 'logging out', -> 74 | beforeEach inject (Parse, $window) -> 75 | Parse.auth._login(user = new Parse.User( 76 | username: 'foo', 77 | sessionToken: 'sessionTok' 78 | )) 79 | 80 | expect(Parse.auth.currentUser.username).toBe 'foo' 81 | expect($window.localStorage.PARSE_USER_INFO).toBeTruthy() 82 | expect($window.localStorage.PARSE_SESSION_TOKEN).toBeTruthy() 83 | 84 | Parse.auth.logout() 85 | 86 | it 'clears localstorage sessionToken', inject ($window) -> 87 | expect($window.localStorage.PARSE_SESSION_TOKEN).toBeUndefined() 88 | 89 | it 'clears localstorage userInfo', inject ($window) -> 90 | expect($window.localStorage.PARSE_USER_INFO).toBeUndefined() 91 | 92 | it 'clears currentUser', inject (Parse) -> 93 | expect(Parse.auth.currentUser).toBeNull() 94 | 95 | it 'clears sessionToken', inject (Parse) -> 96 | expect(Parse.auth.sessionToken).toBeNull() 97 | 98 | 99 | describe 'resumeSession', -> 100 | describe 'with session data', -> 101 | user = null 102 | beforeEach inject (Parse, $window) -> 103 | user = new Parse.User 104 | username: 'foobar' 105 | sessionToken: 'sessTok' 106 | $window.localStorage.setItem('PARSE_USER_INFO', JSON.stringify(user.attributes())) 107 | $window.localStorage.setItem('PARSE_SESSION_TOKEN', user.sessionToken) 108 | 109 | it 'exists', inject (Parse) -> 110 | Parse.auth.resumeSession() 111 | 112 | it 'sets the currentUser', inject (Parse) -> 113 | Parse.auth.resumeSession() 114 | expect(Parse.auth.currentUser.username).toBe 'foobar' 115 | 116 | it 'sets the sessionToken', inject (Parse) -> 117 | Parse.auth.resumeSession() 118 | expect(Parse.auth.sessionToken).toBe 'sessTok' 119 | 120 | describe 'without session data', -> 121 | it 'doesnt set the currentUser', inject (Parse) -> 122 | Parse.auth.resumeSession() 123 | expect(Parse.auth.currentUser).toBeNull() 124 | 125 | it 'doesnt set the sessionToken', inject (Parse) -> 126 | Parse.auth.resumeSession() 127 | expect(Parse.auth.sessionToken).toBeNull() 128 | -------------------------------------------------------------------------------- /test/unit/modelSpec.coffee: -------------------------------------------------------------------------------- 1 | includes = (expected) -> 2 | x = -> 3 | x.test = (str) -> 4 | try 5 | data = JSON.parse str 6 | for key, value of expected 7 | return false unless data[key] == value 8 | return true 9 | catch e 10 | return false 11 | x.toString = -> 12 | "Include #{angular.toJson expected}" 13 | x 14 | 15 | describe 'Model', -> 16 | url = null 17 | backend = null 18 | signedHeaders = null 19 | 20 | Car = null 21 | 22 | beforeEach -> 23 | angular.module('ParseSpec', ['Parse']).config (ParseProvider) -> 24 | ParseProvider.initialize 'appId', 'apiKey' 25 | 26 | module 'ParseSpec' 27 | 28 | inject ($injector, Parse) -> 29 | url = (path) -> 30 | "#{Parse.BaseUrl}#{path}" 31 | 32 | backend = $injector.get('$httpBackend') 33 | 34 | signedHeaders = (headers) -> 35 | headers["X-Parse-Application-Id"] == 'appId' and 36 | headers["X-Parse-REST-API-KEY"] == "apiKey" 37 | 38 | class Car extends Parse.Model 39 | @configure 'Car', 'make', 'model', 'year', 'parts' 40 | 41 | afterEach -> 42 | backend.verifyNoOutstandingExpectation() 43 | backend.verifyNoOutstandingRequest() 44 | 45 | 46 | it 'exists', inject (Parse) -> 47 | expect(Parse.Model).not.toBeNull() 48 | 49 | it 'allows configuration', inject (Parse) -> 50 | class Car extends Parse.Model 51 | @configure 'Car', 'make', 'model', 'color' 52 | 53 | expect(Car.className).toEqual 'Car' 54 | expect(Car.attributes).toEqual ['make', 'model', 'color'] 55 | 56 | describe 'create', -> 57 | 58 | it 'calls POST on collection', inject () -> 59 | car = new Car 60 | make: "Toyota" 61 | year: 2005 62 | model: "Camry" 63 | 64 | backend.expectPOST( 65 | url("/classes/Car"), 66 | JSON.stringify(car.attributes()) 67 | ).respond 68 | createdAt: "2011-08-20T02:06:57.931Z" 69 | objectId: "foobarbaz" 70 | 71 | car.save().then (c) -> 72 | expect(c).toBe(car) 73 | 74 | backend.flush() 75 | expect(car.objectId).toEqual('foobarbaz') 76 | expect(car.isNew()).toEqual(false) 77 | 78 | describe 'update', -> 79 | 80 | it 'calls PUTS on a resource', inject () -> 81 | car = new Car 82 | make: "Toyota" 83 | year: 2005 84 | model: "Camry" 85 | objectId: 'existingID123' 86 | 87 | backend.expectPUT( 88 | url("/classes/Car/existingID123"), 89 | includes(car.attributes()), 90 | signedHeaders 91 | 92 | ).respond 93 | updatedAt: "2012-08-20T02:06:57.931Z" 94 | 95 | car.save().then (c) -> 96 | expect(c).toBe(car) 97 | 98 | backend.flush() 99 | expect(car.updatedAt).toEqual('2012-08-20T02:06:57.931Z') 100 | expect(car.isNew()).toEqual(false) 101 | 102 | describe 'isDirty', -> 103 | 104 | it 'is not dirty upon creation', -> 105 | car = new Car 106 | make: "Toyota" 107 | year: 2005 108 | model: "Camry" 109 | objectId: 'existingID123' 110 | parts: ['engine', 'chassis'] 111 | 112 | expect(car.isDirty()).toBe(false) 113 | 114 | it 'is dirty upon updating', -> 115 | car = new Car 116 | make: "Toyota" 117 | year: 2005 118 | model: "Camry" 119 | objectId: 'existingID123' 120 | parts: ['engine', 'chassis'] 121 | 122 | car.model = "Corolla" 123 | expect(car.isDirty()).toBe(true) 124 | 125 | it 'is not dirty when returning to old state', -> 126 | car = new Car 127 | make: "Toyota" 128 | year: 2005 129 | model: "Camry" 130 | objectId: 'existingID123' 131 | parts: ['engine', 'chassis'] 132 | 133 | car.model = "Corolla" 134 | expect(car.isDirty()).toBe(true) 135 | car.model = "Camry" 136 | expect(car.isDirty()).toBe(false) 137 | 138 | car.parts.push('tires') 139 | expect(car.isDirty()).toBe(true) 140 | car.parts.splice(2, 1) 141 | expect(car.isDirty()).toBe(false) 142 | 143 | it 'updates its cache on save', -> 144 | car = new Car 145 | make: "Toyota" 146 | year: 2005 147 | model: "Camry" 148 | objectId: 'existingID123' 149 | parts: ['engine', 'chassis'] 150 | 151 | car.model = "Corolla" 152 | expect(car.isDirty()).toBe(true) 153 | backend.expectPUT( 154 | url("/classes/Car/existingID123"), 155 | car.attributes(), 156 | signedHeaders 157 | 158 | ).respond 159 | updatedAt: "2012-08-20T02:06:57.931Z" 160 | 161 | car.save().then (c) -> 162 | expect(c).toBe(car) 163 | expect(car.isDirty()).toBe(false) 164 | 165 | backend.flush() 166 | 167 | describe 'destroy', -> 168 | 169 | it 'calls DELETE on a resource', inject () -> 170 | car = new Car 171 | make: "Toyota" 172 | year: 2005 173 | model: "Camry" 174 | objectId: 'existingID123' 175 | 176 | backend.expectDELETE( 177 | url("/classes/Car/existingID123"), 178 | signedHeaders 179 | ).respond 180 | createdAt: "2011-08-20T02:06:57.931Z" 181 | objectId: "foobarbaz" 182 | 183 | car.destroy().then (c) -> 184 | expect(c.isNew()).toBe(true) 185 | 186 | backend.flush() 187 | 188 | describe 'find', -> 189 | it 'returns by id', -> 190 | backend.expectGET( 191 | url("/classes/Car/objID123") 192 | signedHeaders 193 | ).respond 194 | objectId: "objID123" 195 | createdAt: "2011-08-20T02:06:57.931Z" 196 | updatedAt: "2011-08-20T02:06:57.931Z" 197 | make: "Scion" 198 | model: "xB" 199 | year: 2008 200 | 201 | Car.find('objID123').then (car) -> 202 | expect(car.isNew()).toBe false 203 | expect(car.objectId).toEqual 'objID123' 204 | expect(car.make).toBe 'Scion' 205 | expect(car.model).toBe 'xB' 206 | expect(car.year).toBe 2008 207 | 208 | backend.flush() 209 | 210 | describe 'query', -> 211 | it 'queries the api', -> 212 | backend.expectGET( 213 | url("/classes/Car") 214 | signedHeaders 215 | ).respond 216 | results: [ 217 | { 218 | objectId: "objID1" 219 | createdAt: "2011-08-20T02:06:57.931Z" 220 | updatedAt: "2011-08-20T02:06:57.931Z" 221 | make: "Scion" 222 | model: "xB" 223 | year: 2008 224 | } 225 | { 226 | objectId: "objID2" 227 | createdAt: "2011-08-20T02:06:57.931Z" 228 | updatedAt: "2011-08-20T02:06:57.931Z" 229 | make: "Toyota" 230 | model: "Camry" 231 | year: 2005 232 | } 233 | ] 234 | Car.query().then (cars) -> 235 | expect(cars.length).toBe 2 236 | expect(cars[0].isNew()).toBe false 237 | expect(cars[0].objectId).toBe "objID1" 238 | expect(cars[1].objectId).toBe "objID2" 239 | expect(cars[1].model).toBe "Camry" 240 | 241 | backend.flush() 242 | 243 | describe 'User', -> 244 | 245 | it 'should not have custom property', -> 246 | angular.module('ParseSpec', ['Parse']).config (ParseProvider) -> 247 | ParseProvider.initialize 'appId', 'apiKey' 248 | 249 | module 'ParseSpec' 250 | 251 | inject (ParseUser) -> 252 | expect(ParseUser).toBeDefined() 253 | expect(ParseUser.attributes).toContain('username') 254 | expect(ParseUser.attributes).not.toContain('property') 255 | 256 | it 'should have property when extended', -> 257 | angular.module('Parse').factory 'ParseCustomUser', (ParseDefaultUser) -> 258 | class CustomUser extends ParseDefaultUser 259 | @configure 'users', 'username', 'password', 'property' 260 | 261 | angular.module('ParseSpec', ['Parse']).config (ParseProvider) -> 262 | ParseProvider.initialize 'appId', 'apiKey' 263 | 264 | module 'ParseSpec' 265 | 266 | inject (ParseUser) -> 267 | expect(ParseUser).toBeDefined() 268 | expect(ParseUser.attributes).toContain('username') 269 | expect(ParseUser.attributes).toContain('property') --------------------------------------------------------------------------------