├── README.md ├── app.js ├── package.json ├── public ├── css │ └── app.css └── js │ ├── app.js │ ├── controllers.js │ ├── directives.js │ ├── filters.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 │ └── services.js ├── routes ├── index.js └── socket.js └── views ├── index.jade ├── layout.jade └── partials ├── .gitignore ├── chatroom.jade └── roomindex.jade /README.md: -------------------------------------------------------------------------------- 1 | # Angular Socket.IO IM Demo 2 | 3 | A simple instant messaging app to demo the [AngularJS Socket.IO Seed](https://github.com/btford/angular-socket-io-seed). [A walkthrough of writing the application is available on my blog](http://briantford.com/blog/angular-socket-io.html). 4 | 5 | ## Running it 6 | 7 | First, grab the dependencies with npm: 8 | 9 | npm install 10 | 11 | Then run the app like so: 12 | 13 | node app.js 14 | 15 | And navigate to `localhost:3000` -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var express = require('express'), 7 | routes = require('./routes'), 8 | socket = require('./routes/socket.js'); 9 | 10 | var app = module.exports = express.createServer(); 11 | 12 | // Hook Socket.io into Express 13 | var io = require('socket.io').listen(app); 14 | 15 | // Configuration 16 | 17 | app.configure(function(){ 18 | app.set('views', __dirname + '/views'); 19 | app.set('view engine', 'jade'); 20 | app.set('view options', { 21 | layout: false 22 | }); 23 | app.use(express.bodyParser()); 24 | app.use(express.methodOverride()); 25 | app.use(express.static(__dirname + '/public')); 26 | app.use(app.router); 27 | }); 28 | 29 | app.configure('development', function(){ 30 | app.use(express.errorHandler({ dumpExceptions: true, showStack: true })); 31 | }); 32 | 33 | app.configure('production', function(){ 34 | app.use(express.errorHandler()); 35 | }); 36 | 37 | // Routes 38 | 39 | app.get('/', routes.index); 40 | app.get('/partials/:name', routes.partials); 41 | 42 | // redirect all others to the index (HTML5 history) 43 | app.get('*', routes.index); 44 | 45 | // Socket.io Communication 46 | 47 | io.sockets.on('connection', socket); 48 | 49 | // Start server 50 | 51 | app.listen(3000, function(){ 52 | console.log("Express server listening on port %d in %s mode", app.address().port, app.settings.env); 53 | }); 54 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-socket-io-im" 3 | , "version": "0.0.1" 4 | , "private": true 5 | , "dependencies": { 6 | "express": "2.5.10" 7 | , "jade": ">= 0.26.1" 8 | , "socket.io": ">= 0.9.6" 9 | } 10 | } -------------------------------------------------------------------------------- /public/css/app.css: -------------------------------------------------------------------------------- 1 | /* app css stylesheet */ 2 | 3 | .overflowable { 4 | height: 240px; 5 | overflow-y: auto; 6 | border: 1px solid #000; 7 | } 8 | 9 | .overflowable p { 10 | margin: 0; 11 | } 12 | 13 | .col { 14 | float: left; 15 | width: 350px; 16 | } 17 | 18 | .clr { 19 | clear: both; 20 | } -------------------------------------------------------------------------------- /public/js/app.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | // Declare app level module which depends on filters, and services 5 | var app = angular.module('myApp', ['myApp.filters', 'myApp.directives']); 6 | -------------------------------------------------------------------------------- /public/js/controllers.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* Controllers */ 4 | 5 | function AppCtrl($scope, socket) { 6 | 7 | // Socket listeners 8 | // ================ 9 | 10 | socket.on('init', function (data) { 11 | $scope.name = data.name; 12 | $scope.users = data.users; 13 | }); 14 | 15 | socket.on('send:message', function (message) { 16 | $scope.messages.push(message); 17 | }); 18 | 19 | socket.on('change:name', function (data) { 20 | changeName(data.oldName, data.newName); 21 | }); 22 | 23 | socket.on('user:join', function (data) { 24 | $scope.messages.push({ 25 | user: 'chatroom', 26 | text: 'User ' + data.name + ' has joined.' 27 | }); 28 | $scope.users.push(data.name); 29 | }); 30 | 31 | // add a message to the conversation when a user disconnects or leaves the room 32 | socket.on('user:left', function (data) { 33 | $scope.messages.push({ 34 | user: 'chatroom', 35 | text: 'User ' + data.name + ' has left.' 36 | }); 37 | var i, user; 38 | for (i = 0; i < $scope.users.length; i++) { 39 | user = $scope.users[i]; 40 | if (user === data.name) { 41 | $scope.users.splice(i, 1); 42 | break; 43 | } 44 | } 45 | }); 46 | 47 | // Private helpers 48 | // =============== 49 | 50 | var changeName = function (oldName, newName) { 51 | // rename user in list of users 52 | var i; 53 | for (i = 0; i < $scope.users.length; i++) { 54 | if ($scope.users[i] === oldName) { 55 | $scope.users[i] = newName; 56 | } 57 | } 58 | 59 | $scope.messages.push({ 60 | user: 'chatroom', 61 | text: 'User ' + oldName + ' is now known as ' + newName + '.' 62 | }); 63 | } 64 | 65 | // Methods published to the scope 66 | // ============================== 67 | 68 | $scope.changeName = function () { 69 | socket.emit('change:name', { 70 | name: $scope.newName 71 | }, function (result) { 72 | if (!result) { 73 | alert('There was an error changing your name'); 74 | } else { 75 | 76 | changeName($scope.name, $scope.newName); 77 | 78 | $scope.name = $scope.newName; 79 | $scope.newName = ''; 80 | } 81 | }); 82 | }; 83 | 84 | $scope.messages = []; 85 | 86 | $scope.sendMessage = function () { 87 | socket.emit('send:message', { 88 | message: $scope.message 89 | }); 90 | 91 | // add the message to our model locally 92 | $scope.messages.push({ 93 | user: $scope.name, 94 | text: $scope.message 95 | }); 96 | 97 | // clear message box 98 | $scope.message = ''; 99 | }; 100 | } 101 | -------------------------------------------------------------------------------- /public/js/directives.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* Directives */ 4 | 5 | 6 | angular.module('myApp.directives', []). 7 | directive('appVersion', ['version', function(version) { 8 | return function(scope, elm, attrs) { 9 | elm.text(version); 10 | }; 11 | }]); 12 | -------------------------------------------------------------------------------- /public/js/filters.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* Filters */ 4 | 5 | angular.module('myApp.filters', []). 6 | filter('interpolate', ['version', function(version) { 7 | return function(text) { 8 | return String(text).replace(/\%VERSION\%/mg, version); 9 | } 10 | }]); 11 | -------------------------------------------------------------------------------- /public/js/lib/angular/angular-cookies.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license AngularJS v1.0.1 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 | -------------------------------------------------------------------------------- /public/js/lib/angular/angular-cookies.min.js: -------------------------------------------------------------------------------- 1 | /* 2 | AngularJS v1.0.1 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 | -------------------------------------------------------------------------------- /public/js/lib/angular/angular-loader.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license AngularJS v1.0.1 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 | -------------------------------------------------------------------------------- /public/js/lib/angular/angular-loader.min.js: -------------------------------------------------------------------------------- 1 | /* 2 | AngularJS v1.0.1 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 | -------------------------------------------------------------------------------- /public/js/lib/angular/angular-resource.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license AngularJS v1.0.1 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 | -------------------------------------------------------------------------------- /public/js/lib/angular/angular-resource.min.js: -------------------------------------------------------------------------------- 1 | /* 2 | AngularJS v1.0.1 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 | -------------------------------------------------------------------------------- /public/js/lib/angular/angular-sanitize.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license AngularJS v1.0.1 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 | * @example 438 | 439 | 440 | 450 |
451 | Snippet: 452 | 453 | 454 | 455 | 456 | 457 | 458 | 459 | 460 | 463 | 466 | 467 | 468 | 469 | 470 | 471 | 472 |
FilterSourceRendered
linky filter 461 |
<div ng-bind-html="snippet | linky">
</div>
462 |
464 |
465 |
no filter
<div ng-bind="snippet">
</div>
473 | 474 | 475 | it('should linkify the snippet with urls', function() { 476 | expect(using('#linky-filter').binding('snippet | linky')). 477 | toBe('Pretty text with some links: ' + 478 | 'http://angularjs.org/, ' + 479 | 'us@somewhere.org, ' + 480 | 'another@somewhere.org, ' + 481 | 'and one more: ftp://127.0.0.1/.'); 482 | }); 483 | 484 | it ('should not linkify snippet without the linky filter', function() { 485 | expect(using('#escaped-html').binding('snippet')). 486 | toBe("Pretty text with some links:\n" + 487 | "http://angularjs.org/,\n" + 488 | "mailto:us@somewhere.org,\n" + 489 | "another@somewhere.org,\n" + 490 | "and one more: ftp://127.0.0.1/."); 491 | }); 492 | 493 | it('should update', function() { 494 | input('snippet').enter('new http://link.'); 495 | expect(using('#linky-filter').binding('snippet | linky')). 496 | toBe('new http://link.'); 497 | expect(using('#escaped-html').binding('snippet')).toBe('new http://link.'); 498 | }); 499 | 500 | 501 | */ 502 | angular.module('ngSanitize').filter('linky', function() { 503 | var LINKY_URL_REGEXP = /((ftp|https?):\/\/|(mailto:)?[A-Za-z0-9._%+-]+@)\S*[^\s\.\;\,\(\)\{\}\<\>]/, 504 | MAILTO_REGEXP = /^mailto:/; 505 | 506 | return function(text) { 507 | if (!text) return text; 508 | var match; 509 | var raw = text; 510 | var html = []; 511 | // TODO(vojta): use $sanitize instead 512 | var writer = htmlSanitizeWriter(html); 513 | var url; 514 | var i; 515 | while ((match = raw.match(LINKY_URL_REGEXP))) { 516 | // We can not end in these as they are sometimes found at the end of the sentence 517 | url = match[0]; 518 | // if we did not match ftp/http/mailto then assume mailto 519 | if (match[2] == match[3]) url = 'mailto:' + url; 520 | i = match.index; 521 | writer.chars(raw.substr(0, i)); 522 | writer.start('a', {href:url}); 523 | writer.chars(match[0].replace(MAILTO_REGEXP, '')); 524 | writer.end('a'); 525 | raw = raw.substring(i + match[0].length); 526 | } 527 | writer.chars(raw); 528 | return html.join(''); 529 | }; 530 | }); 531 | 532 | })(window, window.angular); 533 | -------------------------------------------------------------------------------- /public/js/lib/angular/angular-sanitize.min.js: -------------------------------------------------------------------------------- 1 | /* 2 | AngularJS v1.0.1 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 | -------------------------------------------------------------------------------- /public/js/lib/angular/angular.min.js: -------------------------------------------------------------------------------- 1 | /* 2 | AngularJS v1.0.1 3 | (c) 2010-2012 Google, Inc. http://angularjs.org 4 | License: MIT 5 | */ 6 | (function(T,aa,p){'use strict';function m(b,a,c){var d;if(b)if(M(b))for(d in b)d!="prototype"&&d!="length"&&d!="name"&&b.hasOwnProperty(d)&&a.call(c,b[d],d);else if(b.forEach&&b.forEach!==m)b.forEach(a,c);else if(J(b)&&va(b.length))for(d=0;d=0&&b.splice(c,1);return a}function U(b,a){if(na(b)||b&&b.$evalAsync&&b.$watch)throw z("Can't copy Window or Scope");if(a){if(b=== 10 | a)throw z("Can't copy equivalent objects or arrays");if(K(b)){for(;a.length;)a.pop();for(var c=0;c2?ga.call(arguments,2):[];return M(a)&&!(a instanceof RegExp)?c.length? 12 | function(){return arguments.length?a.apply(b,c.concat(ga.call(arguments,0))):a.apply(b,c)}:function(){return arguments.length?a.apply(b,arguments):a.call(b)}:a}function hc(b,a){var c=a;/^\$+/.test(b)?c=p:na(a)?c="$WINDOW":a&&aa===a?c="$DOCUMENT":a&&a.$evalAsync&&a.$watch&&(c="$SCOPE");return c}function ba(b,a){return JSON.stringify(b,hc,a?" ":null)}function mb(b){return G(b)?JSON.parse(b):b}function Wa(b){b&&b.length!==0?(b=C(""+b),b=!(b=="f"||b=="0"||b=="false"||b=="no"||b=="n"||b=="[]")):b=!1; 13 | return b}function oa(b){b=u(b).clone();try{b.html("")}catch(a){}return u("
").append(b).html().match(/^(<[^>]+>)/)[1].replace(/^<([\w\-]+)/,function(a,b){return"<"+C(b)})}function Xa(b){var a={},c,d;m((b||"").split("&"),function(b){b&&(c=b.split("="),d=decodeURIComponent(c[0]),a[d]=s(c[1])?decodeURIComponent(c[1]):!0)});return a}function nb(b){var a=[];m(b,function(b,d){a.push(Ya(d,!0)+(b===!0?"":"="+Ya(b,!0)))});return a.length?a.join("&"):""}function Za(b){return Ya(b,!0).replace(/%26/gi,"&").replace(/%3D/gi, 14 | "=").replace(/%2B/gi,"+")}function Ya(b,a){return encodeURIComponent(b).replace(/%40/gi,"@").replace(/%3A/gi,":").replace(/%24/g,"$").replace(/%2C/gi,",").replace(a?null:/%20/g,"+")}function ic(b,a){function c(a){a&&d.push(a)}var d=[b],e,g,h=["ng:app","ng-app","x-ng-app","data-ng-app"],f=/\sng[:\-]app(:\s*([\w\d_]+);?)?\s/;m(h,function(a){h[a]=!0;c(aa.getElementById(a));a=a.replace(":","\\:");b.querySelectorAll&&(m(b.querySelectorAll("."+a),c),m(b.querySelectorAll("."+a+"\\:"),c),m(b.querySelectorAll("["+ 15 | a+"]"),c))});m(d,function(a){if(!e){var b=f.exec(" "+a.className+" ");b?(e=a,g=(b[2]||"").replace(/\s+/g,",")):m(a.attributes,function(b){if(!e&&h[b.name])e=a,g=b.value})}});e&&a(e,g?[g]:[])}function ob(b,a){b=u(b);a=a||[];a.unshift(["$provide",function(a){a.value("$rootElement",b)}]);a.unshift("ng");var c=pb(a);c.invoke(["$rootScope","$rootElement","$compile","$injector",function(a,b,c,h){a.$apply(function(){b.data("$injector",h);c(b)(a)})}]);return c}function $a(b,a){a=a||"_";return b.replace(jc, 16 | function(b,d){return(d?a:"")+b.toLowerCase()})}function pa(b,a,c){if(!b)throw new z("Argument '"+(a||"?")+"' is "+(c||"required"));return b}function qa(b,a,c){c&&K(b)&&(b=b[b.length-1]);pa(M(b),a,"not a function, got "+(b&&typeof b=="object"?b.constructor.name||"Object":typeof b));return b}function kc(b){function a(a,b,e){return a[b]||(a[b]=e())}return a(a(b,"angular",Object),"module",function(){var b={};return function(d,e,g){e&&b.hasOwnProperty(d)&&(b[d]=null);return a(b,d,function(){function a(c, 17 | d,e){return function(){b[e||"push"]([c,d,arguments]);return k}}if(!e)throw z("No module: "+d);var b=[],c=[],j=a("$injector","invoke"),k={_invokeQueue:b,_runBlocks:c,requires:e,name:d,provider:a("$provide","provider"),factory:a("$provide","factory"),service:a("$provide","service"),value:a("$provide","value"),constant:a("$provide","constant","unshift"),filter:a("$filterProvider","register"),controller:a("$controllerProvider","register"),directive:a("$compileProvider","directive"),config:j,run:function(a){c.push(a); 18 | return this}};g&&j(g);return k})}})}function qb(b){return b.replace(lc,function(a,b,d,e){return e?d.toUpperCase():d}).replace(mc,"Moz$1")}function ab(b,a){function c(){var e;for(var b=[this],c=a,h,f,i,j,k,l,n;b.length;){h=b.shift();f=0;for(i=h.length;f 
"+b;a.removeChild(a.firstChild);bb(this,a.childNodes);this.remove()}else bb(this,b)}function cb(b){return b.cloneNode(!0)}function ra(b){rb(b);for(var a=0,b=b.childNodes||[];a 21 | -1}function vb(b,a){a&&m(a.split(" "),function(a){b.className=Q((" "+b.className+" ").replace(/[\n\t]/g," ").replace(" "+Q(a)+" "," "))})}function wb(b,a){a&&m(a.split(" "),function(a){if(!Ca(b,a))b.className=Q(b.className+" "+Q(a))})}function bb(b,a){if(a)for(var a=!a.nodeName&&s(a.length)&&!na(a)?a:[a],c=0;c4096&&c.warn("Cookie '"+a+"' possibly not set or overflowed because it was too large ("+d+" > 4096 bytes)!"),V.length>20&&c.warn("Cookie '"+a+"' possibly not set or overflowed because too many cookies were already set ("+ 33 | V.length+" > 20 )")}else{if(i.cookie!==I){I=i.cookie;d=I.split("; ");V={};for(f=0;f0&&(V[unescape(e.substring(0,g))]=unescape(e.substring(g+1)))}return V}};f.defer=function(a,b){var c;o++;c=l(function(){delete r[c];e(a)},b||0);r[c]=!0;return c};f.defer.cancel=function(a){return r[a]?(delete r[a],n(a),e(x),!0):!1}}function vc(){this.$get=["$window","$log","$sniffer","$document",function(b,a,c,d){return new uc(b,d,a,c)}]}function wc(){this.$get=function(){function b(b, 34 | d){function e(a){if(a!=l){if(n){if(n==a)n=a.n}else n=a;g(a.n,a.p);g(a,l);l=a;l.n=null}}function g(a,b){if(a!=b){if(a)a.p=b;if(b)b.n=a}}if(b in a)throw z("cacheId "+b+" taken");var h=0,f=D({},d,{id:b}),i={},j=d&&d.capacity||Number.MAX_VALUE,k={},l=null,n=null;return a[b]={put:function(a,b){var c=k[a]||(k[a]={key:a});e(c);v(b)||(a in i||h++,i[a]=b,h>j&&this.remove(n.key))},get:function(a){var b=k[a];if(b)return e(b),i[a]},remove:function(a){var b=k[a];if(b==l)l=b.p;if(b==n)n=b.n;g(b.n,b.p);delete k[a]; 35 | delete i[a];h--},removeAll:function(){i={};h=0;k={};l=n=null},destroy:function(){k=f=i=null;delete a[b]},info:function(){return D({},f,{size:h})}}}var a={};b.info=function(){var b={};m(a,function(a,e){b[e]=a.info()});return b};b.get=function(b){return a[b]};return b}}function xc(){this.$get=["$cacheFactory",function(b){return b("templates")}]}function Bb(b){var a={},c="Directive",d=/^\s*directive\:\s*([\d\w\-_]+)\s+(.*)$/,e=/(([\d\w\-_]+)(?:\:([^;]+))?;?)/,g="Template must have exactly one root element. was: "; 36 | this.directive=function f(d,e){G(d)?(pa(e,"directive"),a.hasOwnProperty(d)||(a[d]=[],b.factory(d+c,["$injector","$exceptionHandler",function(b,c){var e=[];m(a[d],function(a){try{var f=b.invoke(a);if(M(f))f={compile:B(f)};else if(!f.compile&&f.link)f.compile=B(f.link);f.priority=f.priority||0;f.name=f.name||d;f.require=f.require||f.controller&&f.name;f.restrict=f.restrict||"A";e.push(f)}catch(g){c(g)}});return e}])),a[d].push(e)):m(d,lb(f));return this};this.$get=["$injector","$interpolate","$exceptionHandler", 37 | "$http","$templateCache","$parse","$controller","$rootScope",function(b,i,j,k,l,n,r,o){function w(a,b,c){a instanceof u||(a=u(a));m(a,function(b,c){b.nodeType==3&&(a[c]=u(b).wrap("").parent()[0])});var d=t(a,b,a,c);return function(b,c){pa(b,"scope");var e=c?ta.clone.call(a):a;e.data("$scope",b);q(e,"ng-scope");c&&c(e,b);d&&d(b,e,e);return e}}function q(a,b){try{a.addClass(b)}catch(c){}}function t(a,b,c,d){function e(a,c,d,g){for(var j,i,n,k,l,o=0,r=0,q=f.length;oE.priority)break;if(B=E.scope)N("isolated scope",y,E,F),J(B)&&(q(F,"ng-isolate-scope"),y=E),q(F,"ng-scope"),A=A||E;W=E.name;if(B=E.controller)s=s||{},N("'"+W+"' controller",s[W],E,F),s[W]=E;if(B=E.transclude)N("transclusion",x,E,F),x=E,l=E.priority,B=="element"?($=u(b),F=c.$$element=u("<\!-- "+W+": "+c[W]+" --\>"),b=F[0],Ga(e,u($[0]),b),v=w($,d,l)):($=u(cb(b)).contents(),F.html(""),v=w($,d));if(B=E.template)if(N("template",I,E,F),I=E,$=u("
"+Q(B)+"
").contents(),b=$[0],E.replace){if($.length!= 44 | 1||b.nodeType!==1)throw new z(g+B);Ga(e,F,b);W={$attr:{}};a=a.concat(X(b,a.splice(C+1,a.length-(C+1)),W));L(c,W);H=a.length}else F.html(B);if(E.templateUrl)N("template",I,E,F),I=E,k=V(a.splice(C,a.length-C),k,F,c,e,E.replace,v),H=a.length;else if(E.compile)try{D=E.compile(F,c,v),M(D)?f(null,D):D&&f(D.pre,D.post)}catch(O){j(O,oa(F))}if(E.terminal)k.terminal=!0,l=Math.max(l,E.priority)}k.scope=A&&A.scope;k.transclude=x&&v;return k}function y(d,e,g,i){var n=!1;if(a.hasOwnProperty(e))for(var k,e=b.get(e+ 45 | c),l=0,o=e.length;lk.priority)&&k.restrict.indexOf(g)!=-1)d.push(k),n=!0}catch(r){j(r)}return n}function L(a,b){var c=b.$attr,d=a.$attr,e=a.$$element;m(a,function(d,e){e.charAt(0)!="$"&&(b[e]&&(d+=(e==="style"?";":" ")+b[e]),a.$set(e,d,!0,c[e]))});m(b,function(b,f){f=="class"?(q(e,b),a["class"]=(a["class"]?a["class"]+" ":"")+b):f=="style"?e.attr("style",e.attr("style")+";"+b):f.charAt(0)!="$"&&!a.hasOwnProperty(f)&&(a[f]=b,d[f]=c[f])})}function V(a,b,c,d,e,f,j){var i= 46 | [],n,o,r=c[0],q=a.shift(),w=D({},q,{controller:null,templateUrl:null,transclude:null});c.html("");k.get(q.templateUrl,{cache:l}).success(function(k){var l,q;if(f){q=u("
"+Q(k)+"
").contents();l=q[0];if(q.length!=1||l.nodeType!==1)throw new z(g+k);k={$attr:{}};Ga(e,c,l);X(l,a,k);L(d,k)}else l=r,c.html(k);a.unshift(w);n=A(a,c,d,j);for(o=t(c.contents(),j);i.length;){var m=i.pop(),k=i.pop();q=i.pop();var y=i.pop(),I=l;q!==r&&(I=cb(l),Ga(k,u(q),I));n(function(){b(o,y,I,e,m)},y,I,e,m)}i=null}).error(function(a, 47 | b,c,d){throw z("Failed to load template: "+d.url);});return function(a,c,d,e,f){i?(i.push(c),i.push(d),i.push(e),i.push(f)):n(function(){b(o,c,d,e,f)},c,d,e,f)}}function I(a,b){return b.priority-a.priority}function N(a,b,c,d){if(b)throw z("Multiple directives ["+b.name+", "+c.name+"] asking for "+a+" on: "+oa(d));}function F(a,b){var c=i(b,!0);c&&a.push({priority:0,compile:B(function(a,b){var d=b.parent(),e=d.data("$binding")||[];e.push(c);q(d.data("$binding",e),"ng-binding");a.$watch(c,function(a){b[0].nodeValue= 48 | a})})})}function W(a,b,c,d){var e=i(c,!0);e&&b.push({priority:100,compile:B(function(a,b,c){b=c.$$observers||(c.$$observers={});d==="class"&&(e=i(c[d],!0));c[d]=p;(b[d]||(b[d]=[])).$$inter=!0;(c.$$observers&&c.$$observers[d].$$scope||a).$watch(e,function(a){c.$set(d,a)})})})}function Ga(a,b,c){var d=b[0],e=d.parentNode,f,g;if(a){f=0;for(g=a.length;f0){var e=N[0],f=e.text;if(f==a||f==b||f==c||f==d||!a&&!b&&!c&&!d)return e}return!1}function f(b,c,d,f){return(b=h(b,c,d,f))?(a&&!b.json&&e("is not valid json",b),N.shift(),b):!1}function i(a){f(a)||e("is unexpected, expecting ["+ 66 | a+"]",h())}function j(a,b){return function(c,d){return a(c,d,b)}}function k(a,b,c){return function(d,f){return b(d,f,a,c)}}function l(){for(var a=[];;)if(N.length>0&&!h("}",")",";","]")&&a.push(v()),!f(";"))return a.length==1?a[0]:function(b,c){for(var d,f=0;f","<=",">="))a=k(a,b.fn,q());return a}function t(){for(var a=m(),b;b=f("*","/","%");)a=k(a,b.fn,m());return a}function m(){var a;return f("+")?A():(a=f("-"))?k(V,a.fn,m()):(a=f("!"))?j(a.fn,m()):A()}function A(){var a; 68 | if(f("("))a=v(),i(")");else if(f("["))a=y();else if(f("{"))a=L();else{var b=f();(a=b.fn)||e("not a primary expression",b)}for(var c;b=f("(","[",".");)b.text==="("?(a=u(a,c),c=null):b.text==="["?(c=a,a=da(a)):b.text==="."?(c=a,a=s(a)):e("IMPOSSIBLE");return a}function y(){var a=[];if(g().text!="]"){do a.push(F());while(f(","))}i("]");return function(b,c){for(var d=[],f=0;f1;d++){var e=a.shift(),g=b[e];g||(g={},b[e]=g);b=g}return b[a.shift()]=c}function eb(b,a,c){if(!a)return b;for(var a=a.split("."),d,e=b,g=a.length,h=0;h7),hasEvent:function(c){if(c=="input"&&Z==9)return!1;if(v(a[c])){var e=b.document.createElement("div");a[c]="on"+c in e}return a[c]},csp:!1}}]}function Tc(){this.$get=B(T)}function Mb(b){var a={},c,d,e;if(!b)return a;m(b.split("\n"),function(b){e=b.indexOf(":");c=C(Q(b.substr(0,e))); 89 | d=Q(b.substr(e+1));c&&(a[c]?a[c]+=", "+d:a[c]=d)});return a}function Nb(b){var a=J(b)?b:p;return function(c){a||(a=Mb(b));return c?a[C(c)]||null:a}}function Ob(b,a,c){if(M(c))return c(b,a);m(c,function(c){b=c(b,a)});return b}function Uc(){var b=/^\s*(\[|\{[^\{])/,a=/[\}\]]\s*$/,c=/^\)\]\}',?\n/,d=this.defaults={transformResponse:[function(d){G(d)&&(d=d.replace(c,""),b.test(d)&&a.test(d)&&(d=mb(d,!0)));return d}],transformRequest:[function(a){return J(a)&&Sa.apply(a)!=="[object File]"?ba(a):a}],headers:{common:{Accept:"application/json, text/plain, */*", 90 | "X-Requested-With":"XMLHttpRequest"},post:{"Content-Type":"application/json;charset=utf-8"},put:{"Content-Type":"application/json;charset=utf-8"}}},e=this.responseInterceptors=[];this.$get=["$httpBackend","$browser","$cacheFactory","$rootScope","$q","$injector",function(a,b,c,i,j,k){function l(a){function c(a){var b=D({},a,{data:Ob(a.data,a.headers,f)});return 200<=a.status&&a.status<300?b:j.reject(b)}a.method=la(a.method);var e=a.transformRequest||d.transformRequest,f=a.transformResponse||d.transformResponse, 91 | g=d.headers,g=D({"X-XSRF-TOKEN":b.cookies()["XSRF-TOKEN"]},g.common,g[C(a.method)],a.headers),e=Ob(a.data,Nb(g),e),i;v(a.data)&&delete g["Content-Type"];i=n(a,e,g);i=i.then(c,c);m(w,function(a){i=a(i)});i.success=function(b){i.then(function(c){b(c.data,c.status,c.headers,a)});return i};i.error=function(b){i.then(null,function(c){b(c.data,c.status,c.headers,a)});return i};return i}function n(b,c,d){function e(a,b,c){m&&(200<=a&&a<300?m.put(w,[a,b,Mb(c)]):m.remove(w));f(b,a,c);i.$apply()}function f(a, 92 | c,d){c=Math.max(c,0);(200<=c&&c<300?n.resolve:n.reject)({data:a,status:c,headers:Nb(d),config:b})}function h(){var a=Ua(l.pendingRequests,b);a!==-1&&l.pendingRequests.splice(a,1)}var n=j.defer(),k=n.promise,m,p,w=r(b.url,b.params);l.pendingRequests.push(b);k.then(h,h);b.cache&&b.method=="GET"&&(m=J(b.cache)?b.cache:o);if(m)if(p=m.get(w))if(p.then)return p.then(h,h),p;else K(p)?f(p[1],p[0],U(p[2])):f(p,200,{});else m.put(w,k);p||a(b.method,w,c,e,d,b.timeout,b.withCredentials);return k}function r(a, 93 | b){if(!b)return a;var c=[];dc(b,function(a,b){a==null||a==p||(J(a)&&(a=ba(a)),c.push(encodeURIComponent(b)+"="+encodeURIComponent(a)))});return a+(a.indexOf("?")==-1?"?":"&")+c.join("&")}var o=c("$http"),w=[];m(e,function(a){w.push(G(a)?k.get(a):k.invoke(a))});l.pendingRequests=[];(function(a){m(arguments,function(a){l[a]=function(b,c){return l(D(c||{},{method:a,url:b}))}})})("get","delete","head","jsonp");(function(a){m(arguments,function(a){l[a]=function(b,c,d){return l(D(d||{},{method:a,url:b, 94 | data:c}))}})})("post","put");l.defaults=d;return l}]}function Vc(){this.$get=["$browser","$window","$document",function(b,a,c){return Wc(b,Xc,b.defer,a.angular.callbacks,c[0],a.location.protocol.replace(":",""))}]}function Wc(b,a,c,d,e,g){function h(a,b){var c=e.createElement("script"),d=function(){e.body.removeChild(c);b&&b()};c.type="text/javascript";c.src=a;Z?c.onreadystatechange=function(){/loaded|complete/.test(c.readyState)&&d()}:c.onload=c.onerror=d;e.body.appendChild(c)}return function(e, 95 | i,j,k,l,n,r){function o(a,c,d,e){c=(i.match(Fb)||["",g])[1]=="file"?d?200:404:c;a(c==1223?204:c,d,e);b.$$completeOutstandingRequest(x)}b.$$incOutstandingRequestCount();i=i||b.url();if(C(e)=="jsonp"){var p="_"+(d.counter++).toString(36);d[p]=function(a){d[p].data=a};h(i.replace("JSON_CALLBACK","angular.callbacks."+p),function(){d[p].data?o(k,200,d[p].data):o(k,-2);delete d[p]})}else{var q=new a;q.open(e,i,!0);m(l,function(a,b){a&&q.setRequestHeader(b,a)});var t;q.onreadystatechange=function(){q.readyState== 96 | 4&&o(k,t||q.status,q.responseText,q.getAllResponseHeaders())};if(r)q.withCredentials=!0;q.send(j||"");n>0&&c(function(){t=-1;q.abort()},n)}}}function Yc(){this.$get=function(){return{id:"en-us",NUMBER_FORMATS:{DECIMAL_SEP:".",GROUP_SEP:",",PATTERNS:[{minInt:1,minFrac:0,maxFrac:3,posPre:"",posSuf:"",negPre:"-",negSuf:"",gSize:3,lgSize:3},{minInt:1,minFrac:2,maxFrac:2,posPre:"\u00a4",posSuf:"",negPre:"(\u00a4",negSuf:")",gSize:3,lgSize:3}],CURRENCY_SYM:"$"},DATETIME_FORMATS:{MONTH:"January,February,March,April,May,June,July,August,September,October,November,December".split(","), 97 | SHORTMONTH:"Jan,Feb,Mar,Apr,May,Jun,Jul,Aug,Sep,Oct,Nov,Dec".split(","),DAY:"Sunday,Monday,Tuesday,Wednesday,Thursday,Friday,Saturday".split(","),SHORTDAY:"Sun,Mon,Tue,Wed,Thu,Fri,Sat".split(","),AMPMS:["AM","PM"],medium:"MMM d, y h:mm:ss a","short":"M/d/yy h:mm a",fullDate:"EEEE, MMMM d, y",longDate:"MMMM d, y",mediumDate:"MMM d, y",shortDate:"M/d/yy",mediumTime:"h:mm:ss a",shortTime:"h:mm a"},pluralCat:function(b){return b===1?"one":"other"}}}}function Zc(){this.$get=["$rootScope","$browser","$q", 98 | "$exceptionHandler",function(b,a,c,d){function e(e,f,i){var j=c.defer(),k=j.promise,l=s(i)&&!i,f=a.defer(function(){try{j.resolve(e())}catch(a){j.reject(a),d(a)}l||b.$apply()},f),i=function(){delete g[k.$$timeoutId]};k.$$timeoutId=f;g[f]=j;k.then(i,i);return k}var g={};e.cancel=function(b){return b&&b.$$timeoutId in g?(g[b.$$timeoutId].reject("canceled"),a.defer.cancel(b.$$timeoutId)):!1};return e}]}function Pb(b){function a(a,e){return b.factory(a+c,e)}var c="Filter";this.register=a;this.$get=["$injector", 99 | function(a){return function(b){return a.get(b+c)}}];a("currency",Qb);a("date",Rb);a("filter",$c);a("json",ad);a("limitTo",bd);a("lowercase",cd);a("number",Sb);a("orderBy",Tb);a("uppercase",dd)}function $c(){return function(b,a){if(!(b instanceof Array))return b;var c=[];c.check=function(a){for(var b=0;b 100 | -1;case "object":for(var c in a)if(c.charAt(0)!=="$"&&d(a[c],b))return!0;return!1;case "array":for(c=0;c=k+l)for(var j=h.length-k,n=0;n0||e>-c)e+=c;e===0&&c==-12&&(e=12);return hb(e,a,d)}}function La(b,a){return function(c,d){var e=c["get"+b](),g=la(a?"SHORT"+b:b);return d[g][e]}}function Rb(b){function a(a){var b;if(b=a.match(c)){var a=new Date(0),g=0,h=0;b[9]&&(g=H(b[9]+b[10]),h=H(b[9]+b[11]));a.setUTCFullYear(H(b[1]),H(b[2])-1,H(b[3]));a.setUTCHours(H(b[4]||0)-g,H(b[5]||0)-h,H(b[6]||0),H(b[7]||0))}return a} 104 | var c=/^(\d{4})-?(\d\d)-?(\d\d)(?:T(\d\d)(?::?(\d\d)(?::?(\d\d)(?:\.(\d{3}))?)?)?(Z|([+-])(\d\d):?(\d\d)))?$/;return function(c,e){var g="",h=[],f,i,e=e||"mediumDate",e=b.DATETIME_FORMATS[e]||e;G(c)&&(c=ed.test(c)?H(c):a(c));va(c)&&(c=new Date(c));if(!ma(c))return c;for(;e;)(i=fd.exec(e))?(h=h.concat(ga.call(i,1)),e=h.pop()):(h.push(e),e=null);m(h,function(a){f=gd[a];g+=f?f(c,b.DATETIME_FORMATS):a.replace(/(^'|'$)/g,"").replace(/''/g,"'")});return g}}function ad(){return function(b){return ba(b,!0)}} 105 | function bd(){return function(b,a){if(!(b instanceof Array))return b;var a=H(a),c=[],d,e;if(!b||!(b instanceof Array))return c;a>b.length?a=b.length:a<-b.length&&(a=-b.length);a>0?(d=0,e=a):(d=b.length+a,e=b.length);for(;dl?(d.$setValidity("maxlength",!1),p):(d.$setValidity("maxlength",!0),a)};d.$parsers.push(c);d.$formatters.push(c)}}function ib(b,a){b="ngClass"+b;return R(function(c,d,e){c.$watch(e[b],function(b,e){if(a===!0||c.$index% 111 | 2===a)e&&b!==e&&(J(e)&&!K(e)&&(e=Ta(e,function(a,b){if(a)return b})),d.removeClass(K(e)?e.join(" "):e)),J(b)&&!K(b)&&(b=Ta(b,function(a,b){if(a)return b})),b&&d.addClass(K(b)?b.join(" "):b)},!0)})}var C=function(b){return G(b)?b.toLowerCase():b},la=function(b){return G(b)?b.toUpperCase():b},z=T.Error,Z=H((/msie (\d+)/.exec(C(navigator.userAgent))||[])[1]),u,ha,ga=[].slice,Ra=[].push,Sa=Object.prototype.toString,Yb=T.angular||(T.angular={}),sa,Cb,Y=["0","0","0"];x.$inject=[];ya.$inject=[];Cb=Z<9?function(b){b= 112 | b.nodeName?b:b[0];return b.scopeName&&b.scopeName!="HTML"?la(b.scopeName+":"+b.nodeName):b.nodeName}:function(b){return b.nodeName?b.nodeName:b[0].nodeName};var jc=/[A-Z]/g,hd={full:"1.0.1",major:1,minor:0,dot:1,codeName:"thorium-shielding"},Ba=P.cache={},Aa=P.expando="ng-"+(new Date).getTime(),nc=1,id=T.document.addEventListener?function(b,a,c){b.addEventListener(a,c,!1)}:function(b,a,c){b.attachEvent("on"+a,c)},tb=T.document.removeEventListener?function(b,a,c){b.removeEventListener(a,c,!1)}:function(b, 113 | a,c){b.detachEvent("on"+a,c)},lc=/([\:\-\_]+(.))/g,mc=/^moz([A-Z])/,ta=P.prototype={ready:function(b){function a(){c||(c=!0,b())}var c=!1;this.bind("DOMContentLoaded",a);P(T).bind("load",a)},toString:function(){var b=[];m(this,function(a){b.push(""+a)});return"["+b.join(", ")+"]"},eq:function(b){return b>=0?u(this[b]):u(this[this.length+b])},length:0,push:Ra,sort:[].sort,splice:[].splice},Ea={};m("multiple,selected,checked,disabled,readOnly,required".split(","),function(b){Ea[C(b)]=b});var zb={}; 114 | m("input,select,option,textarea,button,form".split(","),function(b){zb[la(b)]=!0});m({data:ub,inheritedData:Da,scope:function(b){return Da(b,"$scope")},controller:xb,injector:function(b){return Da(b,"$injector")},removeAttr:function(b,a){b.removeAttribute(a)},hasClass:Ca,css:function(b,a,c){a=qb(a);if(s(c))b.style[a]=c;else{var d;Z<=8&&(d=b.currentStyle&&b.currentStyle[a],d===""&&(d="auto"));d=d||b.style[a];Z<=8&&(d=d===""?p:d);return d}},attr:function(b,a,c){var d=C(a);if(Ea[d])if(s(c))c?(b[a]=!0, 115 | b.setAttribute(a,d)):(b[a]=!1,b.removeAttribute(d));else return b[a]||(b.attributes.getNamedItem(a)||x).specified?d:p;else if(s(c))b.setAttribute(a,c);else if(b.getAttribute)return b=b.getAttribute(a,2),b===null?p:b},prop:function(b,a,c){if(s(c))b[a]=c;else return b[a]},text:D(Z<9?function(b,a){if(b.nodeType==1){if(v(a))return b.innerText;b.innerText=a}else{if(v(a))return b.nodeValue;b.nodeValue=a}}:function(b,a){if(v(a))return b.textContent;b.textContent=a},{$dv:""}),val:function(b,a){if(v(a))return b.value; 116 | b.value=a},html:function(b,a){if(v(a))return b.innerHTML;for(var c=0,d=b.childNodes;c":function(a,c,d,e){return d(a,c)>e(a,c)},"<=":function(a,c,d,e){return d(a,c)<=e(a,c)},">=":function(a,c,d,e){return d(a,c)>=e(a,c)}, 124 | "&&":function(a,c,d,e){return d(a,c)&&e(a,c)},"||":function(a,c,d,e){return d(a,c)||e(a,c)},"&":function(a,c,d,e){return d(a,c)&e(a,c)},"|":function(a,c,d,e){return e(a,c)(a,c,d(a,c))},"!":function(a,c,d){return!d(a,c)}},Kc={n:"\n",f:"\u000c",r:"\r",t:"\t",v:"\u000b","'":"'",'"':'"'},gb={},Xc=T.XMLHttpRequest||function(){try{return new ActiveXObject("Msxml2.XMLHTTP.6.0")}catch(a){}try{return new ActiveXObject("Msxml2.XMLHTTP.3.0")}catch(c){}try{return new ActiveXObject("Msxml2.XMLHTTP")}catch(d){}throw new z("This browser does not support XMLHttpRequest."); 125 | };Pb.$inject=["$provide"];Qb.$inject=["$locale"];Sb.$inject=["$locale"];var Vb=".",gd={yyyy:O("FullYear",4),yy:O("FullYear",2,0,!0),y:O("FullYear",1),MMMM:La("Month"),MMM:La("Month",!0),MM:O("Month",2,1),M:O("Month",1,1),dd:O("Date",2),d:O("Date",1),HH:O("Hours",2),H:O("Hours",1),hh:O("Hours",2,-12),h:O("Hours",1,-12),mm:O("Minutes",2),m:O("Minutes",1),ss:O("Seconds",2),s:O("Seconds",1),EEEE:La("Day"),EEE:La("Day",!0),a:function(a,c){return a.getHours()<12?c.AMPMS[0]:c.AMPMS[1]},Z:function(a){a=a.getTimezoneOffset(); 126 | return hb(a/60,2)+hb(Math.abs(a%60),2)}},fd=/((?:[^yMdHhmsaZE']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+|d+|H+|h+|m+|s+|a|Z))(.*)/,ed=/^\d+$/;Rb.$inject=["$locale"];var cd=B(C),dd=B(la);Tb.$inject=["$parse"];var jd=B({restrict:"E",compile:function(a,c){c.href||c.$set("href","");return function(a,c){c.bind("click",function(a){c.attr("href")||a.preventDefault()})}}}),jb={};m(Ea,function(a,c){var d=ea("ng-"+c);jb[d]=function(){return{priority:100,compile:function(){return function(a,g,h){a.$watch(h[d],function(a){h.$set(c, 127 | !!a)})}}}}});m(["src","href"],function(a){var c=ea("ng-"+a);jb[c]=function(){return{priority:99,link:function(d,e,g){g.$observe(c,function(c){g.$set(a,c);Z&&e.prop(a,c)})}}}});var Oa={$addControl:x,$removeControl:x,$setValidity:x,$setDirty:x};Wb.$inject=["$element","$attrs","$scope"];var Ra={name:"form",restrict:"E",controller:Wb,compile:function(){return{pre:function(a,c,d,e){d.action||c.bind("submit",function(a){a.preventDefault()});var g=c.parent().controller("form"),h=d.name||d.ngForm;h&&(a[h]= 128 | e);g&&c.bind("$destroy",function(){g.$removeControl(e);h&&(a[h]=p);D(e,Oa)})}}}},kd=B(Ra),ld=B(D(U(Ra),{restrict:"EAC"})),md=/^(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?$/,nd=/^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,4}$/,od=/^\s*(\-|\+)?(\d+|(\d*(\.\d*)))\s*$/,$b={text:Qa,number:function(a,c,d,e,g,h){Qa(a,c,d,e,g,h);e.$parsers.push(function(a){var c=S(a);return c||od.test(a)?(e.$setValidity("number",!0),a===""?null:c?a:parseFloat(a)):(e.$setValidity("number", 129 | !1),p)});e.$formatters.push(function(a){return S(a)?"":""+a});if(d.min){var f=parseFloat(d.min),a=function(a){return!S(a)&&ai?(e.$setValidity("max",!1),p):(e.$setValidity("max",!0),a)};e.$parsers.push(d);e.$formatters.push(d)}e.$formatters.push(function(a){return S(a)||va(a)?(e.$setValidity("number",!0),a):(e.$setValidity("number",!1), 130 | p)})},url:function(a,c,d,e,g,h){Qa(a,c,d,e,g,h);a=function(a){return S(a)||md.test(a)?(e.$setValidity("url",!0),a):(e.$setValidity("url",!1),p)};e.$formatters.push(a);e.$parsers.push(a)},email:function(a,c,d,e,g,h){Qa(a,c,d,e,g,h);a=function(a){return S(a)||nd.test(a)?(e.$setValidity("email",!0),a):(e.$setValidity("email",!1),p)};e.$formatters.push(a);e.$parsers.push(a)},radio:function(a,c,d,e){v(d.name)&&c.attr("name",wa());c.bind("click",function(){c[0].checked&&a.$apply(function(){e.$setViewValue(d.value)})}); 131 | e.$render=function(){c[0].checked=d.value==e.$viewValue};d.$observe("value",e.$render)},checkbox:function(a,c,d,e){var g=d.ngTrueValue,h=d.ngFalseValue;G(g)||(g=!0);G(h)||(h=!1);c.bind("click",function(){a.$apply(function(){e.$setViewValue(c[0].checked)})});e.$render=function(){c[0].checked=e.$viewValue};e.$formatters.push(function(a){return a===g});e.$parsers.push(function(a){return a?g:h})},hidden:x,button:x,submit:x,reset:x},ac=["$browser","$sniffer",function(a,c){return{restrict:"E",require:"?ngModel", 132 | link:function(d,e,g,h){h&&($b[C(g.type)]||$b.text)(d,e,g,h,c,a)}}}],Na="ng-valid",Ma="ng-invalid",Pa="ng-pristine",Xb="ng-dirty",pd=["$scope","$exceptionHandler","$attrs","$element","$parse",function(a,c,d,e,g){function h(a,c){c=c?"-"+$a(c,"-"):"";e.removeClass((a?Ma:Na)+c).addClass((a?Na:Ma)+c)}this.$modelValue=this.$viewValue=Number.NaN;this.$parsers=[];this.$formatters=[];this.$viewChangeListeners=[];this.$pristine=!0;this.$dirty=!1;this.$valid=!0;this.$invalid=!1;this.$name=d.name;var g=g(d.ngModel), 133 | f=g.assign;if(!f)throw z(Db+d.ngModel+" ("+oa(e)+")");this.$render=x;var i=e.inheritedData("$formController")||Oa,j=0,k=this.$error={};e.addClass(Pa);h(!0);this.$setValidity=function(a,c){if(k[a]!==!c){if(c){if(k[a]&&j--,!j)h(!0),this.$valid=!0,this.$invalid=!1}else h(!1),this.$invalid=!0,this.$valid=!1,j++;k[a]=!c;h(c,a);i.$setValidity(a,c,this)}};this.$setViewValue=function(d){this.$viewValue=d;if(this.$pristine)this.$dirty=!0,this.$pristine=!1,e.removeClass(Pa).addClass(Xb),i.$setDirty();m(this.$parsers, 134 | function(a){d=a(d)});if(this.$modelValue!==d)this.$modelValue=d,f(a,d),m(this.$viewChangeListeners,function(a){try{a()}catch(d){c(d)}})};var l=this;a.$watch(g,function(a){if(l.$modelValue!==a){var c=l.$formatters,d=c.length;for(l.$modelValue=a;d--;)a=c[d](a);if(l.$viewValue!==a)l.$viewValue=a,l.$render()}})}],qd=function(){return{require:["ngModel","^?form"],controller:pd,link:function(a,c,d,e){var g=e[0],h=e[1]||Oa;h.$addControl(g);c.bind("$destroy",function(){h.$removeControl(g)})}}},rd=B({require:"ngModel", 135 | link:function(a,c,d,e){e.$viewChangeListeners.push(function(){a.$eval(d.ngChange)})}}),bc=function(){return{require:"?ngModel",link:function(a,c,d,e){if(e){d.required=!0;var g=function(a){if(d.required&&(S(a)||a===!1))e.$setValidity("required",!1);else return e.$setValidity("required",!0),a};e.$formatters.push(g);e.$parsers.unshift(g);d.$observe("required",function(){g(e.$viewValue)})}}}},sd=function(){return{require:"ngModel",link:function(a,c,d,e){var g=(a=/\/(.*)\//.exec(d.ngList))&&RegExp(a[1])|| 136 | d.ngList||",",h=function(a){var c=[];a&&m(a.split(g),function(a){a&&c.push(Q(a))});return c};e.$parsers.push(h);e.$formatters.push(function(a){return K(a)&&!fa(h(e.$viewValue),a)?a.join(", "):p})}}},td=/^(true|false|\d+)$/,ud=function(){return{priority:100,compile:function(a,c){return td.test(c.ngValue)?function(a,c,g){g.$set("value",a.$eval(g.ngValue))}:function(a,c,g){a.$watch(g.ngValue,function(a){g.$set("value",a,!1)})}}}},vd=R(function(a,c,d){c.addClass("ng-binding").data("$binding",d.ngBind); 137 | a.$watch(d.ngBind,function(a){c.text(a==p?"":a)})}),wd=["$interpolate",function(a){return function(c,d,e){c=a(d.attr(e.$attr.ngBindTemplate));d.addClass("ng-binding").data("$binding",c);e.$observe("ngBindTemplate",function(a){d.text(a)})}}],xd=[function(){return function(a,c,d){c.addClass("ng-binding").data("$binding",d.ngBindHtmlUnsafe);a.$watch(d.ngBindHtmlUnsafe,function(a){c.html(a||"")})}}],yd=ib("",!0),zd=ib("Odd",0),Ad=ib("Even",1),Bd=R({compile:function(a,c){c.$set("ngCloak",p);a.removeClass("ng-cloak")}}), 138 | Cd=[function(){return{scope:!0,controller:"@"}}],Dd=["$sniffer",function(a){return{priority:1E3,compile:function(){a.csp=!0}}}],cc={};m("click dblclick mousedown mouseup mouseover mouseout mousemove mouseenter mouseleave".split(" "),function(a){var c=ea("ng-"+a);cc[c]=["$parse",function(d){return function(e,g,h){var f=d(h[c]);g.bind(C(a),function(a){e.$apply(function(){f(e,{$event:a})})})}}]});var Ed=R(function(a,c,d){c.bind("submit",function(){a.$apply(d.ngSubmit)})}),Fd=["$http","$templateCache", 139 | "$anchorScroll","$compile",function(a,c,d,e){return{restrict:"ECA",terminal:!0,compile:function(g,h){var f=h.ngInclude||h.src,i=h.onload||"",j=h.autoscroll;return function(g,h){var n=0,m,o=function(){m&&(m.$destroy(),m=null);h.html("")};g.$watch(f,function(f){var q=++n;f?a.get(f,{cache:c}).success(function(a){q===n&&(m&&m.$destroy(),m=g.$new(),h.html(a),e(h.contents())(m),s(j)&&(!j||g.$eval(j))&&d(),m.$emit("$includeContentLoaded"),g.$eval(i))}).error(function(){q===n&&o()}):o()})}}}}],Gd=R({compile:function(){return{pre:function(a, 140 | c,d){a.$eval(d.ngInit)}}}}),Hd=R({terminal:!0,priority:1E3}),Id=["$locale","$interpolate",function(a,c){var d=/{}/g;return{restrict:"EA",link:function(e,g,h){var f=h.count,i=g.attr(h.$attr.when),j=h.offset||0,k=e.$eval(i),l={};m(k,function(a,e){l[e]=c(a.replace(d,"{{"+f+"-"+j+"}}"))});e.$watch(function(){var c=parseFloat(e.$eval(f));return isNaN(c)?"":(k[c]||(c=a.pluralCat(c-j)),l[c](e,g,!0))},function(a){g.text(a)})}}}],Jd=R({transclude:"element",priority:1E3,terminal:!0,compile:function(a,c,d){return function(a, 141 | c,h){var f=h.ngRepeat,h=f.match(/^\s*(.+)\s+in\s+(.*)\s*$/),i,j,k;if(!h)throw z("Expected ngRepeat in form of '_item_ in _collection_' but got '"+f+"'.");f=h[1];i=h[2];h=f.match(/^(?:([\$\w]+)|\(([\$\w]+)\s*,\s*([\$\w]+)\))$/);if(!h)throw z("'item' in 'item in collection' should be identifier or (key, value) but got '"+f+"'.");j=h[3]||h[1];k=h[2];var l=new db;a.$watch(function(a){var e,f,h=a.$eval(i),m=fc(h,!0),p,u=new db,A,y,v,s,z=c;if(K(h))v=h||[];else{v=[];for(A in h)h.hasOwnProperty(A)&&A.charAt(0)!= 142 | "$"&&v.push(A);v.sort()}e=0;for(f=v.length;ex;)u.pop().element.remove()}for(;v.length>w;)v.pop()[0].element.remove()}var h;if(!(h=w.match(d)))throw z("Expected ngOptions in form of '_select_ (as _label_)? for (_key_,)?_value_ in _collection_' but got '"+w+"'.");var j=c(h[2]||h[1]),k=h[4]||h[6],l=h[5],m=c(h[3]||""),n=c(h[2]?h[1]:k),r=c(h[7]),v=[[{element:f,label:""}]];q&&(a(q)(e),q.removeClass("ng-scope"),q.remove());f.html("");f.bind("change",function(){e.$apply(function(){var a, 152 | c=r(e)||[],d={},h,i,j,m,q,s;if(o){i=[];m=0;for(s=v.length;m@charset "UTF-8";[ng\\:cloak],[ng-cloak],[data-ng-cloak],[x-ng-cloak],.ng-cloak,.x-ng-cloak{display:none;}ng\\:form{display:block;}'); 158 | -------------------------------------------------------------------------------- /public/js/lib/angular/version.txt: -------------------------------------------------------------------------------- 1 | 1.0.1 2 | -------------------------------------------------------------------------------- /public/js/services.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* Services */ 4 | 5 | 6 | // Demonstrate how to register services 7 | // In this case it is a simple value service. 8 | app.factory('socket', function ($rootScope) { 9 | var socket = io.connect(); 10 | return { 11 | on: function (eventName, callback) { 12 | socket.on(eventName, function () { 13 | var args = arguments; 14 | $rootScope.$apply(function () { 15 | callback.apply(socket, args); 16 | }); 17 | }); 18 | }, 19 | emit: function (eventName, data, callback) { 20 | socket.emit(eventName, data, function () { 21 | var args = arguments; 22 | $rootScope.$apply(function () { 23 | if (callback) { 24 | callback.apply(socket, args); 25 | } 26 | }); 27 | }) 28 | } 29 | }; 30 | }); 31 | -------------------------------------------------------------------------------- /routes/index.js: -------------------------------------------------------------------------------- 1 | 2 | /* 3 | * GET home page. 4 | */ 5 | 6 | exports.index = function(req, res){ 7 | res.render('index'); 8 | }; 9 | 10 | exports.partials = function (req, res) { 11 | var name = req.params.name; 12 | res.render('partials/' + name); 13 | }; -------------------------------------------------------------------------------- /routes/socket.js: -------------------------------------------------------------------------------- 1 | // Keep track of which names are used so that there are no duplicates 2 | var userNames = (function () { 3 | var names = {}; 4 | 5 | var claim = function (name) { 6 | if (!name || names[name]) { 7 | return false; 8 | } else { 9 | names[name] = true; 10 | return true; 11 | } 12 | }; 13 | 14 | // find the lowest unused "guest" name and claim it 15 | var getGuestName = function () { 16 | var name, 17 | nextUserId = 1; 18 | 19 | do { 20 | name = 'Guest ' + nextUserId; 21 | nextUserId += 1; 22 | } while (!claim(name)); 23 | 24 | return name; 25 | }; 26 | 27 | // serialize claimed names as an array 28 | var get = function () { 29 | var res = []; 30 | for (user in names) { 31 | res.push(user); 32 | } 33 | 34 | return res; 35 | }; 36 | 37 | var free = function (name) { 38 | if (names[name]) { 39 | delete names[name]; 40 | } 41 | }; 42 | 43 | return { 44 | claim: claim, 45 | free: free, 46 | get: get, 47 | getGuestName: getGuestName 48 | }; 49 | }()); 50 | 51 | // export function for listening to the socket 52 | module.exports = function (socket) { 53 | var name = userNames.getGuestName(); 54 | 55 | // send the new user their name and a list of users 56 | socket.emit('init', { 57 | name: name, 58 | users: userNames.get() 59 | }); 60 | 61 | // notify other clients that a new user has joined 62 | socket.broadcast.emit('user:join', { 63 | name: name 64 | }); 65 | 66 | // broadcast a user's message to other users 67 | socket.on('send:message', function (data) { 68 | socket.broadcast.emit('send:message', { 69 | user: name, 70 | text: data.message 71 | }); 72 | }); 73 | 74 | // validate a user's name change, and broadcast it on success 75 | socket.on('change:name', function (data, fn) { 76 | if (userNames.claim(data.name)) { 77 | var oldName = name; 78 | userNames.free(oldName); 79 | 80 | name = data.name; 81 | 82 | socket.broadcast.emit('change:name', { 83 | oldName: oldName, 84 | newName: name 85 | }); 86 | 87 | fn(true); 88 | } else { 89 | fn(false); 90 | } 91 | }); 92 | 93 | // clean up when a user leaves, and broadcast it to other users 94 | socket.on('disconnect', function () { 95 | socket.broadcast.emit('user:left', { 96 | name: name 97 | }); 98 | userNames.free(name); 99 | }); 100 | }; 101 | -------------------------------------------------------------------------------- /views/index.jade: -------------------------------------------------------------------------------- 1 | extends layout 2 | 3 | block body 4 | h1 Angular Socket.io IM Demo App 5 | div(ng-controller='AppCtrl') 6 | .col 7 | h3 Messages 8 | .overflowable 9 | p(ng-repeat='message in messages', 10 | ng-class='{alert: message.user == "chatroom"}') {{message.user}}: {{message.text}} 11 | 12 | .col 13 | h3 Users 14 | .overflowable 15 | p(ng-repeat='user in users') {{user}} 16 | 17 | .clr 18 | form(ng-submit='sendMessage()') 19 | | Message: 20 | input(size='60', ng-model='message') 21 | input(type='submit', value='Send') 22 | 23 | .clr 24 | h3 Change your name 25 | p Your current user name is {{name}} 26 | form(ng-submit='changeName()') 27 | input(ng-model='newName') 28 | input(type='submit', value='Change Name') 29 | 30 | script(src='js/lib/angular/angular.js') 31 | script(src='/socket.io/socket.io.js') 32 | script(src='js/app.js') 33 | script(src='js/services.js') 34 | script(src='js/controllers.js') 35 | script(src='js/filters.js') 36 | script(src='js/directives.js') 37 | -------------------------------------------------------------------------------- /views/layout.jade: -------------------------------------------------------------------------------- 1 | doctype html 2 | html(ng-app="myApp") 3 | head 4 | meta(charset='utf8') 5 | base(href='/') 6 | title Angular Socket.io IM Demo App 7 | link(rel='stylesheet', href='/css/app.css') 8 | body 9 | block body 10 | -------------------------------------------------------------------------------- /views/partials/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/btford/angular-socket-io-im/b73268493c26515ece5a5a3158bb3a1d9cf9a26f/views/partials/.gitignore -------------------------------------------------------------------------------- /views/partials/chatroom.jade: -------------------------------------------------------------------------------- 1 | a(href='/') List of Rooms 2 | 3 | h2 Welcome to the "{{room.name}}" chat room 4 | 5 | .col 6 | h3 Messages 7 | .overflowable 8 | p(ng-repeat='message in room.messages', 9 | ng-class='{alert: message.user == "alert"}') {{message.user}}: {{message.text}} 10 | 11 | form(ng-submit='sendMessage()') 12 | input(ng-model='message') 13 | input(type='submit', value='Send') 14 | 15 | .col 16 | h3 Users 17 | .overflowable 18 | p(ng-repeat='user in room.users') {{user}} 19 | -------------------------------------------------------------------------------- /views/partials/roomindex.jade: -------------------------------------------------------------------------------- 1 | h2 Available Chat rooms 2 | ul 3 | li(ng-repeat='(id, room) in rooms') 4 | a(href='/room/{{id}}') {{room.name}} 5 | 6 | form(ng-submit='newRoom()') 7 | input(ng-model='roomName') 8 | input(type='submit', value='Make New Room') 9 | --------------------------------------------------------------------------------