├── views ├── footer.ejs ├── body.ejs ├── 404.ejs ├── header.ejs ├── 500.ejs ├── layout.ejs └── home │ └── index.ejs ├── assets ├── js │ └── app.coffee ├── templates │ ├── robots.txt │ ├── favicon.ico │ └── views │ │ ├── remote │ │ └── test.html │ │ ├── test.ejs │ │ ├── partials │ │ ├── partial1.ejs │ │ └── partial2.ejs │ │ ├── error404.ejs │ │ ├── users │ │ ├── new.ejs │ │ ├── edit.ejs │ │ ├── show.ejs │ │ ├── index.ejs │ │ └── partials │ │ │ ├── _edit_form.ejs │ │ │ └── _new_form.ejs │ │ ├── home.ejs │ │ └── login.ejs ├── styles │ ├── main.css │ ├── application.css │ └── bootstrap-responsive.min.css └── application │ ├── controllers │ ├── constants.coffee │ ├── main.coffee │ ├── navbar.coffee │ ├── login.coffee │ └── users.coffee │ ├── services │ ├── users.js │ ├── socket.coffee │ └── session.js │ ├── directives │ ├── passwordValidate.js │ └── gravatar.js │ └── application.js ├── app.js ├── config ├── views.js ├── locales │ └── english.js ├── bootstrap.js ├── assets.js ├── local.ex.js ├── application.js ├── policies.js ├── adapters.js └── routes.js ├── public ├── favicon.ico ├── images │ └── glyphicons-halflings.png ├── robots.txt └── js │ └── vendor │ ├── angular-cookies.js │ ├── underscore-min.js │ ├── angular-resource.js │ ├── angular-sanitize.js │ ├── bootstrap.min.js │ └── socket.io.min.js ├── api ├── policies │ └── authenticated.js ├── controllers │ ├── EchoController.js │ ├── SessionController.js │ ├── UserController.js │ └── TemplateController.js └── models │ └── User.js ├── .gitignore ├── package.json └── README.md /views/footer.ejs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/js/app.coffee: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | require('sails').lift(); -------------------------------------------------------------------------------- /config/views.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | viewEngine: 'ejs' 3 | }; -------------------------------------------------------------------------------- /assets/templates/robots.txt: -------------------------------------------------------------------------------- 1 | # robotstxt.org 2 | 3 | User-agent: * 4 | -------------------------------------------------------------------------------- /assets/styles/main.css: -------------------------------------------------------------------------------- 1 | /* Will be compiled down to a single stylesheet with your sass files */ -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/levid/angular-sails-socketio-mongo-demo/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /config/locales/english.js: -------------------------------------------------------------------------------- 1 | /* 2 | * English string set 3 | * 4 | */ 5 | var english = { 6 | 7 | }; 8 | module.exports = english; -------------------------------------------------------------------------------- /assets/templates/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/levid/angular-sails-socketio-mongo-demo/HEAD/assets/templates/favicon.ico -------------------------------------------------------------------------------- /assets/templates/views/remote/test.html: -------------------------------------------------------------------------------- 1 |

My partial to be returned by the TemplateController when I hit /template/find/test.html

2 | -------------------------------------------------------------------------------- /public/images/glyphicons-halflings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/levid/angular-sails-socketio-mongo-demo/HEAD/public/images/glyphicons-halflings.png -------------------------------------------------------------------------------- /assets/templates/views/test.ejs: -------------------------------------------------------------------------------- 1 |
2 |

This is my angular partial returned by the TemplateController so we don't have to do inlining of partials

3 |
4 | -------------------------------------------------------------------------------- /assets/templates/views/partials/partial1.ejs: -------------------------------------------------------------------------------- 1 | 5 | -------------------------------------------------------------------------------- /assets/templates/views/partials/partial2.ejs: -------------------------------------------------------------------------------- 1 | 5 | -------------------------------------------------------------------------------- /assets/application/controllers/constants.coffee: -------------------------------------------------------------------------------- 1 | "use strict" 2 | 3 | ### 4 | Defines application-wide key value pairs 5 | ### 6 | Application.Constants.constant "configuration", { 7 | 8 | } -------------------------------------------------------------------------------- /assets/templates/views/error404.ejs: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /config/bootstrap.js: -------------------------------------------------------------------------------- 1 | // App bootstrap 2 | // Code to run before launching the app 3 | // 4 | // Make sure you call cb() when you're finished. 5 | module.exports.bootstrap = function (cb) { 6 | cb(); 7 | }; -------------------------------------------------------------------------------- /views/body.ejs: -------------------------------------------------------------------------------- 1 |
2 |
3 |

{{message}}

4 |
5 | 6 |
7 |
8 |
-------------------------------------------------------------------------------- /assets/application/controllers/main.coffee: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | class MainCtrl 4 | constructor: () -> 5 | Application.Controllers.controller "MainCtrl", ["$scope", ($scope) -> 6 | $scope.foo = "booyah" 7 | ] 8 | 9 | window.MainCtrl = new MainCtrl() -------------------------------------------------------------------------------- /assets/application/controllers/navbar.coffee: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | Application.Controllers.controller "NavbarController", ["$scope", "$location", ($scope, $location) -> 4 | $scope.getClass = (path) -> 5 | if $location.path().substr(0, path.length) is path 6 | true 7 | else 8 | false 9 | ] -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # The robots.txt file is used to control how search engines index your live URLs. 2 | # See http://www.robotstxt.org/wc/norobots.html for more information. 3 | # 4 | # To prevent search engines from seeing the site altogether, uncomment the next two lines: 5 | # User-Agent: * 6 | # Disallow: / 7 | -------------------------------------------------------------------------------- /assets/templates/views/users/new.ejs: -------------------------------------------------------------------------------- 1 | 11 | 12 | -------------------------------------------------------------------------------- /assets/templates/views/users/edit.ejs: -------------------------------------------------------------------------------- 1 | 11 | 12 | -------------------------------------------------------------------------------- /api/policies/authenticated.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Allow any authenticated user. 3 | */ 4 | module.exports = function (req,res,ok) { 5 | 6 | // User is allowed, proceed to controller 7 | if (req.session.authenticated) { 8 | return ok(); 9 | } 10 | 11 | // User is not allowed 12 | else { 13 | return res.redirect('/login'); 14 | // return res.send("You are not permitted to perform this action.",403); 15 | } 16 | }; -------------------------------------------------------------------------------- /api/controllers/EchoController.js: -------------------------------------------------------------------------------- 1 | /*--------------------- 2 | :: Echo 3 | -> controller 4 | ---------------------*/ 5 | var EchoController = { 6 | 7 | index: function (req,res) { 8 | // Get the value of a parameter 9 | var param = req.param('message'); 10 | 11 | // Send a JSON response 12 | res.json({ 13 | success: true, 14 | message: param 15 | }); 16 | } 17 | 18 | }; 19 | module.exports = EchoController; -------------------------------------------------------------------------------- /config/assets.js: -------------------------------------------------------------------------------- 1 | // Asset rack configuration 2 | module.exports.assets = { 3 | 4 | // A list of directories, in order, which will be recursively parsed for css, javascript, and templates 5 | // and then can be automatically injected in your layout/views via the view partials: 6 | // ( assets.css(), assets.js() and assets.templateLibrary() ) 7 | sequence: [ 8 | 'assets/application', 9 | 'assets/mixins', 10 | 'assets/js', 11 | 'assets/styles', 12 | 'assets/templates' 13 | ] 14 | }; 15 | -------------------------------------------------------------------------------- /config/local.ex.js: -------------------------------------------------------------------------------- 1 | // Local configuration 2 | // 3 | // Included in the .gitignore by default, 4 | // this is where you include configuration overrides for your local system 5 | // or for a production deployment. 6 | 7 | 8 | // For example, to use port 80 on the local machine, override the `port` config 9 | // module.exports.port = 80; 10 | 11 | // or to keep your db credentials out of the repo, but to use them on the local machine 12 | // override the `modelDefaults` config 13 | // module.exports.modelDefaults = { database: 'foo', user: 'bar', password: 'baZ'} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ######################## 2 | # sails 3 | ######################## 4 | .sails 5 | .waterline 6 | .rigging 7 | .tmp 8 | 9 | 10 | ######################## 11 | # node.js / npm 12 | ######################## 13 | lib-cov 14 | *.seed 15 | *.log 16 | *.csv 17 | *.dat 18 | *.out 19 | *.pid 20 | *.gz 21 | 22 | pids 23 | logs 24 | results 25 | 26 | npm-debug.log 27 | 28 | 29 | ######################## 30 | # misc / editors 31 | ######################## 32 | *~ 33 | *# 34 | .DS_STORE 35 | .netbeans 36 | nbproject 37 | .idea 38 | 39 | 40 | ######################## 41 | # local config 42 | ######################## 43 | config/local.js 44 | -------------------------------------------------------------------------------- /api/models/User.js: -------------------------------------------------------------------------------- 1 | /*--------------------- 2 | :: User 3 | -> model 4 | ---------------------*/ 5 | 6 | var SALT_WORK_FACTOR = 10; 7 | 8 | module.exports = { 9 | 10 | attributes : { 11 | 12 | // Simple attribute: 13 | // name: 'STRING', 14 | 15 | // Or for more flexibility: 16 | // phoneNumber: { 17 | // type: 'STRING', 18 | // defaultValue: '555-555-5555' 19 | // } 20 | 21 | name: { 22 | type: 'STRING' 23 | }, 24 | 25 | 26 | email: { 27 | type: 'STRING' 28 | }, 29 | 30 | password: { 31 | type: 'String', 32 | required: true, 33 | index: { unique: true } 34 | } 35 | } 36 | 37 | }; -------------------------------------------------------------------------------- /assets/application/services/users.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | Application.Services.service("userService", ["ngResource"]).factory("User", function ($resource) { 4 | // We need to add an update method 5 | return $resource( 6 | "/user/:id", {id: "@id" }, { 7 | update: { method: 'PUT'} 8 | } 9 | ); 10 | 11 | // return $resource( 12 | // "/user/:id", { id: "@id" }, { 13 | // index: { method: 'GET', isArray: true }, 14 | // new: { method: 'GET' }, 15 | // create: { method: 'POST' }, 16 | // show: { method: 'GET' }, 17 | // edit: { method: 'GET' }, 18 | // update: { method: 'PUT' }, 19 | // destroy: { method: 'DELETE' } 20 | // } 21 | // ); 22 | 23 | }); -------------------------------------------------------------------------------- /config/application.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 3 | // Name of the application (used as default ) 4 | appName: "Sails Application", 5 | 6 | // Port this Sails application will live on 7 | port: 1337, 8 | 9 | // The environment the app is deployed in 10 | // (`development` or `production`) 11 | // 12 | // In `production` mode, all css and js are bundled up and minified 13 | // And your views and templates are cached in-memory. Gzip is also used. 14 | // The downside? Harder to debug, and the server takes longer to start. 15 | environment: 'development', 16 | 17 | // Logger 18 | // Valid `level` configs: 19 | // 20 | // - error 21 | // - warn 22 | // - debug 23 | // - info 24 | // - verbose 25 | // 26 | log: { 27 | level: 'verbose' 28 | } 29 | }; -------------------------------------------------------------------------------- /assets/templates/views/users/show.ejs: -------------------------------------------------------------------------------- 1 | <script type="text/ng-template" id="views/users/show.html"> 2 | <div class="users show view"> 3 | <h1>User</h1> 4 | 5 | <div data-ng-controller="UsersController"> 6 | <fieldset> 7 | <legend>User Information</legend> 8 | <p> 9 | <b>Id:</b> 10 | {{user.id}} 11 | </p> 12 | 13 | <p> 14 | <b>Name:</b> 15 | {{user.name}} 16 | </p> 17 | 18 | <p> 19 | <b>Email:</b> 20 | {{user.email}} 21 | </p> 22 | 23 | <a class="btn btn-info" ng-href="#/users/edit/{{user.id}}">Edit</a> 24 | <a class="btn btn-primary" ng-href="#/users">Back</a> 25 | </fieldset> 26 | </div> 27 | </div> 28 | </script> 29 | 30 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "isaac-portfolio-2013", 3 | "private": true, 4 | "version": "0.0.0-8", 5 | "description": "a Sails application", 6 | "dependencies": { 7 | "sails": "0.8.895", 8 | "angular": "0.0.4", 9 | "coffee-script": "1.6.2", 10 | "hamljs": "0.6.1", 11 | "sails-mysql": "0.7.8", 12 | "sails-mongo": "0.0.3", 13 | "socket.io": "0.9.14", 14 | "bcrypt": "0.7.5" 15 | }, 16 | "scripts": { 17 | "start": "node .app.js", 18 | "debug": "node debug .app.js" 19 | }, 20 | "main": ".app.js", 21 | "repository": "", 22 | "author": "Isaac Wooten", 23 | "license": "MIT", 24 | "engines": { 25 | "node": "0.8.x" 26 | }, 27 | "subdomain": "levid-isaac-portfolio-2013" 28 | } 29 | -------------------------------------------------------------------------------- /config/policies.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Policy defines middleware that is run before each controller/controller. 3 | * Any policy dropped into the /middleware directory is made globally available through sails.middleware 4 | * Below, use the string name of the middleware 5 | */ 6 | module.exports.policies = { 7 | 8 | // Default policy (allow public access) 9 | '*': true, 10 | 11 | userController: { 12 | '*' : 'authenticated' 13 | }, 14 | 15 | usersController: { 16 | '*' : 'authenticated' 17 | }, 18 | 19 | sessionController : { 20 | '*' : true 21 | } 22 | 23 | /** Example mapping: 24 | someController: { 25 | 26 | // Apply the "authenticated" policy to all actions 27 | '*': 'authenticated', 28 | 29 | // For someAction, apply 'somePolicy' instead 30 | someAction: 'somePolicy' 31 | } 32 | */ 33 | }; -------------------------------------------------------------------------------- /assets/application/directives/passwordValidate.js: -------------------------------------------------------------------------------- 1 | Application.Directives.directive('passwordValidate', function() { 2 | return { 3 | require: 'ngModel', 4 | link: function(scope, elm, attrs, ctrl) { 5 | ctrl.$parsers.unshift(function(viewValue) { 6 | 7 | scope.pwdValidLength = (viewValue && viewValue.length >= 8 ? 'valid' : undefined); 8 | scope.pwdHasLetter = (viewValue && /[A-z]/.test(viewValue)) ? 'valid' : undefined; 9 | scope.pwdHasNumber = (viewValue && /\d/.test(viewValue)) ? 'valid' : undefined; 10 | 11 | if(scope.pwdValidLength && scope.pwdHasLetter && scope.pwdHasNumber) { 12 | ctrl.$setValidity('pwd', true); 13 | return viewValue; 14 | } else { 15 | ctrl.$setValidity('pwd', false); 16 | return undefined; 17 | } 18 | 19 | }); 20 | } 21 | }; 22 | }); -------------------------------------------------------------------------------- /api/controllers/SessionController.js: -------------------------------------------------------------------------------- 1 | /*--------------------- 2 | :: Session 3 | -> controller 4 | ---------------------*/ 5 | var bcrypt = require('bcrypt'); 6 | 7 | var SessionController = { 8 | 9 | login: function(req,res) { 10 | // @todo validate / sanitize e-mail & password length 11 | // @todo csrf protection 12 | var user = User.find({ 13 | email: req.param('email') 14 | }).done(function(err,user){ 15 | var err_msg = "Invalid username or password"; 16 | if(err) { 17 | return res.json({ error: err.message },500); 18 | } 19 | if(!user || !bcrypt.compareSync(req.param('password'),user.password)) { 20 | return res.json({ authorized: false }); 21 | } 22 | return res.json({ authorized: true, user: user }); 23 | }); 24 | }, 25 | logout: function(req,res) { 26 | return res.json({ authorized: false }); 27 | } 28 | 29 | }; 30 | module.exports = SessionController; -------------------------------------------------------------------------------- /api/controllers/UserController.js: -------------------------------------------------------------------------------- 1 | /*--------------------- 2 | :: User 3 | -> controller 4 | ---------------------*/ 5 | var bcrypt = require('bcrypt') 6 | , SALT_WORK_FACTOR = 10 7 | , MIN_PASSWORD_LENGTH = 8; 8 | 9 | var UserController = { 10 | create: function(req,res) { 11 | try { 12 | if(!req.param('password') || req.param('password').length < MIN_PASSWORD_LENGTH) { 13 | throw new Error("password not sent or doesn't meet length requirement ("+MIN_PASSWORD_LENGTH+" chars)"); 14 | } 15 | 16 | function createUser(hash) { 17 | User.create({ 18 | name: req.param('name'), 19 | email: req.param('email'), 20 | password: hash 21 | }).done(function(err,user){ 22 | if(err) throw err; 23 | res.json(user); 24 | }); 25 | }; 26 | 27 | bcrypt.hash(req.param('password'),SALT_WORK_FACTOR,function(err, hash){ 28 | if(err) throw err; 29 | createUser(hash); 30 | }); 31 | 32 | } catch(e) { 33 | return res.json({ error : e.message },500); 34 | } 35 | } 36 | }; 37 | module.exports = UserController; -------------------------------------------------------------------------------- /assets/templates/views/home.ejs: -------------------------------------------------------------------------------- 1 | <script type="text/ng-template" id="views/home.html"> 2 | 3 | <h1 id="header"> 4 | <a href="/">New Sails App</a> 5 | </h1> 6 | <div id="content"> 7 | <h2>It works!</h2> 8 | 9 | <p>You've successfully installed the Sails Framework.</p> 10 | 11 | <p>Sails is a modern, realtime MVC framework for the Node.js platform. <br/> <span class="highlight">It helps you build apps fast.</span></p> 12 | 13 | <p> 14 | <a href="https://github.com/balderdashy/sails/wiki" class="button"><span>Documentation</span></a> 15 | <a href="https://github.com/balderdashy/sails/wiki/Changelog" class="button"><span>Changelog</span></a> 16 | </p> 17 | </div> 18 | 19 | <div id="footer"> 20 | <a target="_blank" href="http://sailsjs.com" class="copyright">Built with Sails.js</a> 21 | </div> 22 | 23 | <br /> 24 | <div> 25 | <label for"test"><h4>Test Angular Bindings</h4></label> 26 | <input id="test" type="text" ng-model="data.message"> 27 | <h1>{{data.message}}</h1> 28 | </div> 29 | 30 | </script> 31 | -------------------------------------------------------------------------------- /assets/application/controllers/login.coffee: -------------------------------------------------------------------------------- 1 | "use strict" 2 | 3 | angular.module("application").controller "LoginCtrl", ["$rootScope", "$scope", "$location", "SessionService", ($rootScope, $scope, $location, SessionService) -> 4 | 5 | init = -> 6 | $scope.user = SessionService.getUser() 7 | 8 | loginHandler = (res) -> 9 | if SessionService.authorized(res) 10 | $scope.message = "Authorized!" 11 | $rootScope.user = SessionService.getUser() 12 | $scope.user = SessionService.getUser() 13 | $location.path "/users" 14 | else 15 | $scope.message = "Invalid username or password!" 16 | 17 | logoutHandler = (res) -> 18 | $scope.message = "Logged Out!" 19 | user = { 20 | name: '' 21 | email: '' 22 | } 23 | $scope.user = user 24 | $rootScope.user = user 25 | $location.path "/login" 26 | 27 | errorHandler = (err) -> 28 | $scope.message = "Error! " + err 29 | 30 | $scope.login = -> 31 | SessionService.login $scope.user, loginHandler, errorHandler 32 | 33 | $scope.logout = -> 34 | SessionService.logout $scope.user, logoutHandler, errorHandler 35 | 36 | $scope.showMessage = -> 37 | $scope.message and $scope.message.length 38 | 39 | init() 40 | 41 | ] -------------------------------------------------------------------------------- /config/adapters.js: -------------------------------------------------------------------------------- 1 | // Configure installed adapters 2 | // If you define an attribute in your model definition, 3 | // it will override anything from this global config. 4 | module.exports.adapters = { 5 | 6 | // If you leave the adapter config unspecified 7 | // in a model definition, 'default' will be used. 8 | 'default': 'mongo', 9 | 10 | // In-memory adapter for DEVELOPMENT ONLY 11 | // (data is NOT preserved when the server shuts down) 12 | memory: { 13 | module: 'sails-dirty', 14 | inMemory: true 15 | }, 16 | 17 | // Persistent adapter for DEVELOPMENT ONLY 18 | // (data IS preserved when the server shuts down) 19 | // PLEASE NOTE: disk adapter not compatible with node v0.10.0 currently 20 | // because of limitations in node-dirty 21 | // See https://github.com/felixge/node-dirty/issues/34 22 | disk: { 23 | module: 'sails-dirty', 24 | filePath: './.tmp/dirty.db', 25 | inMemory: false 26 | }, 27 | 28 | // MySQL is the world's most popular relational database. 29 | // Learn more: http://en.wikipedia.org/wiki/MySQL 30 | mysql: { 31 | module : 'sails-mysql', 32 | host : 'YOUR_MYSQL_SERVER_HOSTNAME_OR_IP_ADDRESS', 33 | user : 'YOUR_MYSQL_USER', 34 | password : 'YOUR_MYSQL_PASSWORD', 35 | database : 'YOUR_MYSQL_DB' 36 | }, 37 | 38 | mongo: { 39 | module : 'sails-mongo', 40 | url : 'mongodb://localhost:27017/YOUR_DB_NAME' 41 | } 42 | }; -------------------------------------------------------------------------------- /assets/templates/views/login.ejs: -------------------------------------------------------------------------------- 1 | <script type="text/ng-template" id="views/login.html"> 2 | 3 | <h1>Sign In</h1> 4 | 5 | <form id="login-form" ng-submit="login()" name="loginForm" class="css-form" novalidate> 6 | <fieldset> 7 | <legend>User Information</legend> 8 | 9 | <div class="alert alert-error" data-ng-show="showMessage(message)"> 10 | <button type="button" class="close" data-dismiss="alert">×</button> 11 | {{message}} 12 | </div> 13 | 14 | <div class="control-group"> 15 | <label class="control-label" for="inputEmail"> 16 | <span class="required">*</span> 17 | Email Address 18 | </label> 19 | <div class="controls"> 20 | <input type="email" ng-model="user.email" required autofocus /> 21 | <span class="error" ng-show="myForm.list.$error.REQUIRED">Required!</span> 22 | </div> 23 | </div> 24 | 25 | <div class="control-group"> 26 | <label class="control-label" for="inputPassword"> 27 | <span class="required">*</span> 28 | Password 29 | </label> 30 | <div class="controls"> 31 | <input type="password" ng-model="user.password" required /> 32 | </div> 33 | </div> 34 | 35 | <div class="control-group"> 36 | <div class="controls"> 37 | <label class="checkbox"> 38 | <input type="checkbox"> Remember me 39 | </label> 40 | <div class="form-actions"> 41 | <button type="submit" class="btn btn-inverse" ng-disabled="loginForm.$invalid">Sign In</button> 42 | </div> 43 | </div> 44 | </div> 45 | 46 | </fieldset> 47 | </form> 48 | </script> -------------------------------------------------------------------------------- /assets/application/services/socket.coffee: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | # Socket service 4 | Application.Services.factory "$socket", ["$rootScope", "User", ($rootScope, User) -> 5 | 6 | # $socket listeners 7 | # ================ 8 | 9 | 10 | $socket = io.connect("http://localhost:1336") 11 | $socket.on "connect", (stream) -> 12 | console.log "someone connected!" 13 | 14 | $socket.on 'connectedUsers', (data) -> 15 | console.log "connected users: ", data 16 | $rootScope.$apply -> 17 | $rootScope.connectedUsers = data 18 | 19 | $socket.on 'news', (data) -> 20 | console.log data 21 | $socket.emit 'my other event', 22 | my: 'data' 23 | 24 | $socket.on "onUserAdded", (user) -> 25 | scope = UsersController.getScope() # Get the scope of the UsersController 26 | user = User.get user 27 | console.log "onUserAdded called", user 28 | # scope.$apply -> 29 | scope.users.push user 30 | 31 | $socket.on "onUserUpdated", (user) -> 32 | scope = UsersController.getScope() # Get the scope of the UsersController 33 | console.log "onUserUpdated called", user 34 | users = User.query() 35 | # scope.$apply -> 36 | scope.users = users 37 | 38 | $socket.on "onUserDeleted", (user) -> 39 | scope = UsersController.getScope() # Get the scope of the UsersController 40 | console.log "onUserDeleted called", user 41 | scope.$apply -> 42 | scope.users = $.grep(scope.users, (o, i) -> 43 | o.id is user.id 44 | , true) 45 | 46 | $socket.on "disconnect", (stream) -> 47 | console.log "someone disconnected" 48 | 49 | $socket.removeListener "connect" 50 | $socket.removeListener "news" 51 | $socket.removeListener "onUserAdded" 52 | $socket.removeListener "onUserDeleted" 53 | ] -------------------------------------------------------------------------------- /assets/application/services/session.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('application.services') 4 | .factory('SessionService',['$resource', function($resource){ 5 | 6 | var service = $resource('/session/:param',{},{ 7 | 'login': { 8 | method: 'POST' 9 | }, 10 | 'logout': { 11 | method: 'DELETE' 12 | } 13 | }); 14 | 15 | var _user = { authorized: false }; 16 | 17 | function getUser() { 18 | return _user; 19 | } 20 | 21 | function authorized(){ 22 | return _user.authorized === true; 23 | } 24 | 25 | function unauthorized(){ 26 | return _user.authorized === false; 27 | } 28 | 29 | function login(newUser,resultHandler,errorHandler) { 30 | service.login( 31 | newUser, 32 | function(res){ 33 | _user = (res.user || {}); 34 | _user.authorized = res.authorized; 35 | if(angular.isFunction(resultHandler)) { 36 | resultHandler(res); 37 | } 38 | }, 39 | function(err){ 40 | if(angular.isFunction(errorHandler)){ 41 | errorHandler(err); 42 | } 43 | } 44 | ); 45 | } 46 | 47 | function logout(user,resultHandler,errorHandler){ 48 | service.logout( 49 | user, 50 | function(res){ 51 | _user = (res.user || {}); 52 | _user.authorized = res.authorized; 53 | if(angular.isFunction(resultHandler)) { 54 | resultHandler(res); 55 | } 56 | }, 57 | function(err){ 58 | if(angular.isFunction(errorHandler)){ 59 | errorHandler(err); 60 | } 61 | } 62 | ); 63 | } 64 | 65 | return { 66 | login: login, 67 | logout: logout, 68 | authorized: authorized, 69 | getUser: getUser 70 | }; 71 | 72 | }]); -------------------------------------------------------------------------------- /views/404.ejs: -------------------------------------------------------------------------------- 1 | <h1>Page not found</h1> 2 | 3 | <h2>The view you were trying to reach does not exist.</h2> 4 | 5 | 6 | <style type="text/css"> 7 | 8 | /* Styles included inline since you'll probably be deleting this page anyway */ 9 | body{font:14px Helvetica,Arial,sans-serif}html{background:#e0e0e0}p{margin:3% 1% 3% 0}a{color:#00b7ff}input{padding:.5em;width:50%}h1{font-size:28px;font-weight:bold;margin:20px 0 15px;color:#444;text-shadow:0 0 5px rgba(150,150,150,0.6)}h2{color:#444;text-shadow:0 0 5px rgba(150,150,150,0.6);font-size:18px;font-weight:bold;margin:10px 0 10px}h3{font-size:18px;font-weight:bold;margin:10px 0 10px;color:#eee;text-shadow:0 0 5px rgba(0,0,0,0.6)}#controllers li a{background:#eee;border-radius:8px;padding:.5em 1em;margin-bottom:.3em;display:block;width:50%}#topbar{width:100%;padding:0 2em;background:black;position:fixed;top:0;left:0;z-index:100;box-shadow:0 2px 2px rgba(100,100,100,0.5)}#branding{margin:0;text-shadow:none;padding:12px 0 0;height:35px;font-size:19px;font-weight:normal;color:white;width:100px;float:left}#branding a{color:white}#topbar{overflow:auto}#topbar li{float:left}#topbar li a{color:#aaa;font-size:.94em;height:30px;padding:17px 1.5em 0;display:block}#topbar li a:hover{color:#ccc}#topbar li a.selected{color:#ddd;background:#444;font-weight:bold;font-size:.94em;height:30px;padding:17px 1.5em 0;display:block}#topbar-dropdown{float:right;margin-top:15px;margin-right:45px}#content{background:#c0c0c0;padding:50px 20px 30px;box-shadow:0 3px 1px rgba(150,150,150,0.2)}img.spinner{padding:.5em;margin:0 auto;display:block}#footer{padding:1em 0 3em;color:#999;font-size:.85em}#footer .copyright{color:#777;margin-left:2em;opacity:.8}#footer .copyright:hover{opacity:1;text-decoration:underline}input.error{border-color:red}span.error{display:block;font-size:85%;color:red}.mobile-only{display:none}@media screen and (max-width:980px){input.ui-list-search{width:45%;padding-top:7px;padding-bottom:7px}}@media screen and (max-width:480px){.widescreen-nav{display:none}a.create-node{margin-bottom:1em}input.ui-list-search{width:auto;display:block;float:none!important;margin-left:0;margin-right:0;margin-top:1em;clear:both}.mobile-only{display:block}} 10 | </style> -------------------------------------------------------------------------------- /views/header.ejs: -------------------------------------------------------------------------------- 1 | <div class="navbar navbar-inverse navbar-fixed-top"> 2 | <div class="navbar-inner"> 3 | 4 | <div class="container-fluid"> 5 | <button type="button" class="btn btn-navbar" data-toggle="collapse" data-target=".nav-collapse"> 6 | <span class="icon-bar"></span> 7 | <span class="icon-bar"></span> 8 | <span class="icon-bar"></span> 9 | </button> 10 | 11 | <gravatar email="user.email" data-ng-model="user" size="200" class="img-circle pull-left" style="width: 40px; height: 40px"></gravatar> 12 | <a class="brand" href="#"> 13 | Angular Sails Socket.io Demo 14 | <span data-ng-show="connectedUsers.count">( 15 | <ng-pluralize count="connectedUsers.count" when="{'0': 'None viewing', 'one': '1 viewing', 'other': '{} viewing'}"></ng-pluralize> 16 | )</span> 17 | </a> 18 | 19 | <div class="nav-collapse collapse"> 20 | <div class="authStatus"> 21 | <p class="navbar-text pull-right" data-ng-show="user"> 22 | Logged in as <a ng-href="#/users/show/{{user.id}}" target="_self" class="navbar-link">{{ user.name }}</a> 23 |  |  <a href="" ng-controller="LoginCtrl" ng-click="logout()" class="navbar-link">Sign out</a> 24 | </p> 25 | 26 | <p class="navbar-text pull-right" data-ng-hide="user"> 27 | <a ng-href="#/login" class="navbar-link">Sign in</a> 28 | </p> 29 | </div> 30 | 31 | <div data-ng-controller="UsersController"></div> 32 | 33 | <ul class="nav" data-ng-controller="NavbarController"> 34 | <li data-ng-class="{'active':getClass('/')}"><a ng-href="#/">Home</a></li> 35 | <li data-ng-class="{'active':getClass('/view1')}"><a ng-href="#/view1">View1</a></li> 36 | <li data-ng-class="{'active':getClass('/view2')}"><a ng-href="#/view2">View2</a></li> 37 | <li data-ng-class="{'active':getClass('/users')}"><a ng-href="#/users">Users</a></li> 38 | <li data-ng-class="{'active':getClass('/remotepartial')}"><a ng-href="#/remotepartial">Remote Partial</a></li> 39 | </ul> 40 | </div> 41 | </div> 42 | 43 | </div> 44 | </div> -------------------------------------------------------------------------------- /assets/templates/views/users/index.ejs: -------------------------------------------------------------------------------- 1 | <script type="text/ng-template" id="views/users/index.html"> 2 | <div class="users view"> 3 | <h1>Users</h1> 4 | 5 | <div data-ng-controller="UsersController"> 6 | <fieldset> 7 | <legend>Total Users: {{ users.length }}</legend> 8 | <table class="table table-hover table-striped table-bordered usersTable"> 9 | <thead> 10 | <tr> 11 | <th data-ng-click="setOrder('id')">ID</th> 12 | <th data-ng-click="setOrder('name')">Name</th> 13 | <th data-ng-click="setOrder('email')">Email</th> 14 | <th colspan="3" style="text-align: center;">Actions</th> 15 | </tr> 16 | </thead> 17 | <tbody> 18 | <tr data-ng-hide="users && users.length > 0" class="error"> 19 | <td colspan="6"> 20 | <div class="text-center"><strong>No users found</strong></div> 21 | </td> 22 | </tr> 23 | <tr data-ng-repeat="user in users | orderBy:orderby:reverse"> 24 | <td> 25 | {{ user.id }} 26 | </td> 27 | <td> 28 | {{ user.name }} 29 | </td> 30 | <td> 31 | {{ user.email }} 32 | </td> 33 | <td style="text-align: center;"> 34 | <a class="btn btn-inverse" ng-href="#/users/show/{{user.id}}">Show</a> 35 | </td> 36 | <td style="text-align: center;"> 37 | <a class="btn btn-info" ng-href="#/users/edit/{{user.id}}">Edit</a> 38 | </td> 39 | <td style="text-align: center;"> 40 | <button class="btn btn-danger" data-ng-click="deleteUser(user)">X</button> 41 | </td> 42 | </tr> 43 | </tbody> 44 | </table> 45 | 46 | 47 | <a class="btn btn-primary" ng-href="#/users/new">New User</a> 48 | </div> 49 | </div> 50 | </div> 51 | </script> 52 | 53 | -------------------------------------------------------------------------------- /views/500.ejs: -------------------------------------------------------------------------------- 1 | <h1>Error</h1> 2 | 3 | 4 | <% _.each(errors, function (error) { %> 5 | 6 | <code> 7 | <%= error %> 8 | </code> 9 | 10 | <% }) %> 11 | 12 | 13 | <style type="text/css"> 14 | 15 | /* Styles included inline since you'll probably be deleting this page anyway */ 16 | body{font:14px Helvetica,Arial,sans-serif}html{background:#e0e0e0}p{margin:3% 1% 3% 0}a{color:#00b7ff}input{padding:.5em;width:50%}h1{font-size:28px;font-weight:bold;margin:20px 0 15px;color:#444;text-shadow:0 0 5px rgba(150,150,150,0.6)}h2{color:#444;text-shadow:0 0 5px rgba(150,150,150,0.6);font-size:18px;font-weight:bold;margin:10px 0 10px}h3{font-size:18px;font-weight:bold;margin:10px 0 10px;color:#eee;text-shadow:0 0 5px rgba(0,0,0,0.6)}#controllers li a{background:#eee;border-radius:8px;padding:.5em 1em;margin-bottom:.3em;display:block;width:50%}#topbar{width:100%;padding:0 2em;background:black;position:fixed;top:0;left:0;z-index:100;box-shadow:0 2px 2px rgba(100,100,100,0.5)}#branding{margin:0;text-shadow:none;padding:12px 0 0;height:35px;font-size:19px;font-weight:normal;color:white;width:100px;float:left}#branding a{color:white}#topbar{overflow:auto}#topbar li{float:left}#topbar li a{color:#aaa;font-size:.94em;height:30px;padding:17px 1.5em 0;display:block}#topbar li a:hover{color:#ccc}#topbar li a.selected{color:#ddd;background:#444;font-weight:bold;font-size:.94em;height:30px;padding:17px 1.5em 0;display:block}#topbar-dropdown{float:right;margin-top:15px;margin-right:45px}#content{background:#c0c0c0;padding:50px 20px 30px;box-shadow:0 3px 1px rgba(150,150,150,0.2)}img.spinner{padding:.5em;margin:0 auto;display:block}#footer{padding:1em 0 3em;color:#999;font-size:.85em}#footer .copyright{color:#777;margin-left:2em;opacity:.8}#footer .copyright:hover{opacity:1;text-decoration:underline}input.error{border-color:red}span.error{display:block;font-size:85%;color:red}.mobile-only{display:none}@media screen and (max-width:980px){input.ui-list-search{width:45%;padding-top:7px;padding-bottom:7px}}@media screen and (max-width:480px){.widescreen-nav{display:none}a.create-node{margin-bottom:1em}input.ui-list-search{width:auto;display:block;float:none!important;margin-left:0;margin-right:0;margin-top:1em;clear:both}.mobile-only{display:block}} 17 | </style> -------------------------------------------------------------------------------- /views/layout.ejs: -------------------------------------------------------------------------------- 1 | <!DOCTYPE html> 2 | <html lang="en" ng-app="application"> 3 | <head> 4 | <title><%- title %> 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | <%- assets.css() %> 18 | 19 | 20 | 21 | 22 | 23 | <%- partial('header')%> 24 | <%- partial('body')%> 25 | <%- partial('footer')%> 26 | 27 | 28 | 34 | 35 | 36 | 37 | <%- assets.templateLibrary() %> 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | <%- assets.js() %> 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Angular + Sails + Socket.io + MongoDB Demo 2 | 3 | A simple CRUD example of using AngularJS with SailsJS. It uses Angular Resource to interface with the Sails REST back-end Api 4 | and also includes some socket.io interaction on the front-end. There is some basic authentication and also 5 | a few nifty directives for loading gravatars via email address and form password validation. Enjoy! 6 | 7 | Install 8 | ------------------ 9 | 10 | git clone git@github.com:levid/angular-sails-socketio-mongo-demo.git 11 | cd angular-sails-socketio-mongo-demo 12 | npm install 13 | 14 | You will want to configure your Mongo DB in config/adapters.js 15 | 16 | mongo: { 17 | module : 'sails-mongo', 18 | url : 'mongodb://localhost:27017/YOUR_DB_NAME' 19 | } 20 | 21 | Then to start the server on port 1337 run: 22 | 23 | sails lift 24 | 25 | Partials (inlined) 26 | ------------------ 27 | 28 | Partials can either be inlined into the main html page by dumping partials into the assets/templates folder, 29 | your partial should look like this: 30 | 31 | 35 | 36 | You can also create sub folders within the templates folder to keep things more organized. 37 | Just make sure to reference the path relative to your templates folder like this: 38 | 39 | 43 | 44 | then include the line below in your main html body (this will concatenate and inject all the partials into the page): 45 | 46 | <%- assets.templateLibrary() %> 47 | 48 | Your when statement in your angular routeprovider would look like this : 49 | 50 | when('/view1', {templateUrl: 'partial1.html'}). 51 | 52 | where partial1.html would be the id specified in your partial 53 | 54 | Partials (remote) 55 | ----------------- 56 | 57 | Partials can also be served from the server by dumping your plain html partials into assets/templates/partials 58 | (note though that if you include the assets.templateLibrary() line from above - the partials in this folder 59 | will still be injected into the page) 60 | 61 | Your when statement in your angular routeprovider - in this case - would look like this : 62 | 63 | when('/view1', {templateUrl: '/template/find/partial1.html'}). 64 | 65 | (this uses the api/controller/TemplateController.js to serve up a partial by name) 66 | -------------------------------------------------------------------------------- /api/controllers/TemplateController.js: -------------------------------------------------------------------------------- 1 | /*--------------------- 2 | :: Template 3 | -> controller 4 | ---------------------*/ 5 | module.exports = { 6 | index: function (req,res) { 7 | var listOfAssetSourcePaths = sails.config.assets.sequence; 8 | 9 | var htmlString = ""; 10 | async.each(listOfAssetSourcePaths, function (path,cb) { 11 | require('fs').readFile(path,function (err, contents) { 12 | if (err) return cb(err); 13 | htmlString += contents; 14 | }); 15 | }, function (err) { 16 | if (err) return res.send(err,500); 17 | 18 | res.contentType('text/html'); 19 | res.send(htmlString); 20 | }); 21 | }, 22 | 23 | find: function(req,res) { 24 | var tpl = req.param('id'); 25 | console.log('looking for template' + tpl); 26 | require('fs').readFile('assets/templates/views/remote/'+tpl,function (err, contents) { 27 | if (err){ 28 | console.log(err); 29 | res.contentType('text/html'); 30 | res.send(''); 31 | } 32 | else { 33 | res.contentType('text/html'); 34 | res.send(contents); 35 | } 36 | }); 37 | } 38 | }; 39 | 40 | var io = require('socket.io').listen(1336); 41 | io.sockets.on('connection', function (socket) { 42 | var numberOfSockets = Object.keys(io.connected).length; 43 | socket.emit('connectedUsers', { count: numberOfSockets }); 44 | socket.broadcast.emit('connectedUsers', { count: numberOfSockets }); 45 | socket.setMaxListeners(0); 46 | 47 | socket.emit('news', { hello: 'world' }); 48 | socket.emit('init', { data: 'what' }); 49 | socket.on('my other event', function (data) { 50 | console.log("serverEventData: " + data); 51 | }); 52 | 53 | socket.on('deleteUser', function(user){ 54 | console.log("onUserDeleted broadcasted"); 55 | socket.broadcast.emit('onUserDeleted', user); 56 | }); 57 | 58 | socket.on('addUser', function(user){ 59 | console.log("onUserAdded broadcasted"); 60 | socket.broadcast.emit('onUserAdded', user); 61 | }); 62 | 63 | socket.on('updateUser', function(user){ 64 | console.log("onUserUpdated broadcasted"); 65 | socket.broadcast.emit('onUserUpdated', user); 66 | }); 67 | 68 | socket.on('disconnect', function () { 69 | socket.emit('connectedUsers', { count: numberOfSockets-1 }); 70 | socket.broadcast.emit('connectedUsers', { count: numberOfSockets-1 }); 71 | }); 72 | 73 | socket.removeListener("connect", function(){}); 74 | socket.removeListener("deleteUser", function(){}); 75 | socket.removeListener("addUser", function(){}); 76 | }); -------------------------------------------------------------------------------- /config/routes.js: -------------------------------------------------------------------------------- 1 | // Routes 2 | // ********************* 3 | // 4 | // This table routes urls to controllers/actions. 5 | // 6 | // If the URL is not specified here, the default route for a URL is: /:controller/:action/:id 7 | // where :controller, :action, and the :id request parameter are derived from the url 8 | // 9 | // If :action is not specified, Sails will redirect to the appropriate action 10 | // based on the HTTP verb: (using REST/Backbone conventions) 11 | // 12 | // GET: /:controller/read/:id 13 | // POST: /:controller/create 14 | // PUT: /:controller/update/:id 15 | // DELETE: /:controller/destroy/:id 16 | // 17 | // If the requested controller/action doesn't exist: 18 | // - if a view exists ( /views/:controller/:action.ejs ), Sails will render that view 19 | // - if no view exists, but a model exists, Sails will automatically generate a 20 | // JSON API for the model which matches :controller. 21 | // - if no view OR model exists, Sails will respond with a 404. 22 | // 23 | module.exports.routes = { 24 | 25 | // To route the home page to the "index" action of the "home" controller: 26 | '/' : { 27 | controller : 'home' 28 | }, 29 | 30 | '/login' : { 31 | controller : 'home' 32 | }, 33 | 34 | 'post /session' : { 35 | controller: 'session', 36 | action : 'login' 37 | }, 38 | 39 | 'delete /session' : { 40 | controller: 'session', 41 | action : 'logout' 42 | } 43 | 44 | // If you want to set up a route only for a particular HTTP method/verb 45 | // (GET, POST, PUT, DELETE) you can specify the verb before the path: 46 | // 'post /signup': { 47 | // controller : 'user', 48 | // action : 'signup' 49 | // } 50 | 51 | // Keep in mind default routes exist for each of your controllers 52 | // So if you have a UserController with an action called "juggle" 53 | // a route will be automatically exist mapping it to /user/juggle. 54 | // 55 | // Additionally, unless you override them, new controllers will have 56 | // create(), find(), findAll(), update(), and destroy() actions, 57 | // and routes will exist for them as follows: 58 | /* 59 | 60 | // Standard RESTful routing 61 | // (if index is not defined, findAll will be used) 62 | 'get /user': { 63 | controller : 'user', 64 | action : 'index' 65 | } 66 | 'get /user/:id': { 67 | controller : 'user', 68 | action : 'find' 69 | } 70 | 'post /user': { 71 | controller : 'user', 72 | action : 'create' 73 | } 74 | 'put /user/:id': { 75 | controller : 'user', 76 | action : 'update' 77 | } 78 | 'delete /user/:id': { 79 | controller : 'user', 80 | action : 'destroy' 81 | } 82 | */ 83 | }; -------------------------------------------------------------------------------- /assets/templates/views/users/partials/_edit_form.ejs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/templates/views/users/partials/_new_form.ejs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /views/home/index.ejs: -------------------------------------------------------------------------------- 1 |

2 | New Sails App 3 |

4 |
5 |

It works!

6 | 7 |

You've successfully installed the Sails Framework.

8 | 9 |

Sails is a modern, realtime MVC framework for the Node.js platform.
It helps you build apps fast.

10 | 11 |

12 | Documentation 13 | Changelog 14 |

15 |
16 | 17 | 20 | 21 | -------------------------------------------------------------------------------- /assets/application/application.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /** 4 | * The application file bootstraps the angular app by initializing the main module and 5 | * creating namespaces and moduled for controllers, filters, services, and directives. 6 | */ 7 | 8 | var Application = Application || {}; 9 | 10 | Application.Constants = angular.module('application.constants', []); 11 | Application.Services = angular.module('application.services', []); 12 | Application.Controllers = angular.module('application.controllers', []); 13 | Application.Filters = angular.module('application.filters', []); 14 | Application.Directives = angular.module('application.directives', []); 15 | 16 | angular.module('application', ['ngResource', 'application.filters', 'application.services', 'application.directives', 'application.constants', 'application.controllers']). 17 | config(['$routeProvider', '$locationProvider', function($routeProvider, $locationProvider) { 18 | 19 | // $locationProvider.html5Mode(true).hashPrefix('!') 20 | 21 | $routeProvider. 22 | when('/', { 23 | templateUrl: 'views/home.html' 24 | }). 25 | when('/view1', { 26 | templateUrl: 'views/partials/partial1.html' 27 | }). 28 | when('/view2', { 29 | templateUrl: 'views/partials/partial2.html' 30 | }). 31 | when('/remotepartial', { 32 | templateUrl: 'template/find/test.html' 33 | }). 34 | when('/login',{ 35 | templateUrl: 'views/login.html', 36 | controller: 'LoginCtrl' 37 | }). 38 | when('/logout',{ 39 | templateUrl: 'views/login.html', 40 | controller: 'LoginCtrl' 41 | }). 42 | when('/users', { 43 | templateUrl: 'views/users/index.html', 44 | controller: 'UsersController.index', 45 | action: 'index' // optional action for CRUD methods 46 | }). 47 | when('/users/new', { 48 | templateUrl: 'views/users/new.html', 49 | controller: 'UsersController.new', 50 | action: 'new' // optional action for CRUD methods 51 | }). 52 | when('/users/show/:id', { 53 | templateUrl: 'views/users/show.html', 54 | controller: 'UsersController.show', 55 | action: 'show' // optional action for CRUD methods 56 | }). 57 | when('/users/edit/:id', { 58 | templateUrl: 'views/users/edit.html', 59 | controller: 'UsersController.edit', 60 | action: 'edit' // optional action for CRUD methods 61 | }). 62 | when('/users/delete/:id', { 63 | templateUrl: 'views/users/index.html', 64 | controller: 'UsersController.delete', 65 | action: 'delete' // optional action for CRUD methods 66 | }). 67 | otherwise({ 68 | templateUrl: 'views/error404.html' 69 | // redirectTo: '/login' 70 | }); 71 | 72 | }]).run(function($rootScope, $route){ 73 | // Bind the `$routeChangeSuccess` event on the rootScope, so that we dont need to bind in individual controllers. 74 | $rootScope.$on('$routeChangeSuccess', function(currentRoute, previousRoute) { 75 | // This will set the custom property that we have defined while configuring the routes. 76 | if($route.current.action && $route.current.action.length > 0){ 77 | $rootScope.action = $route.current.action ; 78 | } 79 | }); 80 | }); -------------------------------------------------------------------------------- /assets/styles/application.css: -------------------------------------------------------------------------------- 1 | /* app css stylesheet */ 2 | @charset "UTF-8"; 3 | 4 | body { 5 | padding-top: 60px; 6 | padding-bottom: 40px; 7 | } 8 | .sidebar-nav { 9 | padding: 9px 0; 10 | } 11 | 12 | @media (max-width: 980px) { 13 | /* Enable use of floated navbar text */ 14 | .navbar-text.pull-right { 15 | float: none; 16 | padding-left: 5px; 17 | padding-right: 5px; 18 | } 19 | } 20 | 21 | .img-circle { 22 | padding: 0px 5px 0px 0px; 23 | position: relative; 24 | z-index: 50; 25 | } 26 | 27 | .css-form input.ng-invalid.ng-dirty { 28 | background-color: #f2dede; 29 | } 30 | 31 | .css-form input.ng-valid.ng-dirty { 32 | background-color: #dff0d8; 33 | } 34 | 35 | .required { 36 | color: red; 37 | } 38 | 39 | 40 | .input-help { 41 | display: none; 42 | position: relative; 43 | z-index: 100; 44 | top: 0px; 45 | left: 0px; 46 | width: 200px; 47 | padding:10px; 48 | background:#fefefe; 49 | font-size:.875em; 50 | border-radius:5px; 51 | box-shadow:0 1px 3px #aaa; 52 | border:1px solid #ddd; 53 | opacity: 0.9; 54 | } 55 | 56 | .input-help h4 { 57 | margin:0; 58 | padding:0; 59 | font-weight: normal; 60 | font-size: 1.1em; 61 | } 62 | 63 | /* Always hide the input help when it's pristine */ 64 | input.ng-pristine + .input-help { 65 | display: none; 66 | } 67 | 68 | /* Hide the invalid box while the input has focus */ 69 | .ng-invalid:focus + .input-help { 70 | display: none; 71 | } 72 | 73 | /* Show a blue border while an input has focus, make sure it overrides everything else */ 74 | /* Overriding Twitter Bootstrap cuz I don't agree we need to alarm the user while they're typing */ 75 | input:focus { 76 | color: black !important; 77 | border-color: rgba(82, 168, 236, 0.8) !important; 78 | -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6) !important; 79 | -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6) !important; 80 | box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6) !important; 81 | } 82 | 83 | 84 | /* Show green border when stuff has been typed in, and its valid */ 85 | .ng-dirty.ng-valid { 86 | border-color:#3a7d34; 87 | } 88 | 89 | /* Show red border when stuff has been typed in, but its invalid */ 90 | .ng-dirty.ng-invalid { 91 | border-color:#ec3f41; 92 | } 93 | 94 | /* Show the help box once it has focus */ 95 | .immediate-help:focus + .input-help { 96 | display: block; 97 | } 98 | 99 | /* Immediate help should be red when pristine */ 100 | .immediate-help.ng-pristine:focus + .input-help { 101 | border-color:#ec3f41; 102 | } 103 | .immediate-help.ng-pristine:focus + .input-help::before { 104 | color:#ec3f41; 105 | } 106 | 107 | /* Help hould be green when input is valid */ 108 | .ng-valid + .input-help { 109 | border-color:#3a7d34; 110 | } 111 | .ng-valid + .input-help::before { 112 | color:#3a7d34; 113 | } 114 | 115 | /* Help should show and be red when invalid */ 116 | .ng-invalid + .input-help { 117 | display: block; 118 | border-color: #ec3f41; 119 | } 120 | .ng-invalid + .input-help::before { 121 | color: #ec3f41; 122 | } 123 | 124 | /* Style input help requirement bullets */ 125 | .input-help ul { 126 | list-style: none; 127 | margin: 10px 0 0 0; 128 | } 129 | 130 | /* Default each bullet to be invalid with a red cross and text */ 131 | .input-help li { 132 | padding-left: 22px; 133 | line-height: 24px; 134 | color:#ec3f41; 135 | background:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAA1CAYAAABIkmvkAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAAJwAAACcBKgmRTwAAABZ0RVh0Q3JlYXRpb24gVGltZQAxMC8wOS8xMlhq+BkAAAAcdEVYdFNvZnR3YXJlAEFkb2JlIEZpcmV3b3JrcyBDUzVxteM2AAAEA0lEQVRIie2WW2jbVRzHv//zT5rkn0ub61zaNdEiPqTC9EFRhtgJk63qg3Wr0806pswNiYgDUXxwyryCEB8UBevtaSCCDHQoboKyoVvVzfRmL2napU0mrdbl8s//dy4+dM1M28n64FsPnIdz+XzO75zfOXA0pRRWU7o/uS9FxOc+3/vlIQBgq4F3fHxvKuIPJ9cFwi9uTXU8BwDa1Uaw/aN7UusCkWRbPI5yxcTI2Bgy49kXrkrwwIedqYg/nGyLXwsJiYHBYWTGs7Cq5Kpt4cA3PXft+2rX40vhrt7OVLgplIzHYuBKoH9gCKMjGVE1LdfJl86YDAAOfN2ziZP4NODyv9/z2fanFuH7P9iWCjcFk/FYK4QSGLgEk0WeUy/3mQCgPXFs9xbBRW883NrssDvQN3hWcOLPEPGWiD94MBaPQymBoaERjI9mBSfu+fHwL+biItpjR3e6JFfloDeAaGQ9SpUycvlp6ExHJBKGYsDvgyMYH81KTsL90yuX4VoWdh3pMqSQpWBjAC3RZkgpYEkCFDA8NIqJ0UlFxI3Tr/5aB9elsau305BcloKBAFpjLeBSYGRwDBNjk4oTN06/dnYZXCcAgK1vbzYkl6VwOATihOzYlOLEjTOvn1sRXiYAgDsP32YIKUuWaXFOwtP3xrnqleAVBQBwy/M3GZy4+PnN3/4TvqJgNWVVj2lNsCZYE6wJ1gRrgv9dYAMAHHw2Bl2fUEpBVavtLPVW/78nVR/Zk4CupzVHA6zChSOK0yHv0S8GFyK4BMPhAJxOgLE03/9kYhE2dz+agKaldY8bDaEQ7D5ft7Roy+UIlCooy5LQdaZ5vVBEgGmmrT172yVxaIylmdcDm9cHc2oK1Zm8kETvLAo0pRRk8mmnEqKouVw68zVCzP8F/uccFHHoXi/sjT6Y53Mw83mhOHn8J7416wQAwPftd0ouiswwdJu/CRASkBKQAmYuBzNfWIC/O173W6llwfbeu6Yi8tDsrAQJYGICyGQAIWDO5KUkaxlcJwAASdSmaWAQHCACOAc4h6YzJi1qWymNNUHlwYcT0JDWXQbACYhGgeh6gHM4Ghuh2/R0YePNiaUCTSmFcvdDCY1paZvhht3nQ2VmGmahICSR5vQHmDt6DcozeZSnp2FdLLZHhwdq94SVd+xMaJqWtrkM2L1uVHILpy0t8igidymXExfHMzBCQbhCIdga7Onz8etqkdgkUYTZbYCSqORmULlQEIq4J3jyexMA8jdu9BRzuaKyLN3udkNjDEqICID+2hbm797Wwez24/T3vJTE3aFTP9Sd9vT1NziVEMUGr1c35+Y2b5jKnqgNKqWglMLspjs6/rj1dudie2mdao07J5s3dCzt/werJTyI1yYqpQAAAABJRU5ErkJggg==) no-repeat 2px -34px; 136 | } 137 | 138 | /* Set to green check and text when valid */ 139 | .input-help li.valid { 140 | color:#3a7d34; 141 | background-position: 2px 6px; 142 | } 143 | 144 | /* Set submit button */ 145 | form .btn, form.ng-valid .btn[disabled] { 146 | display: none; 147 | } 148 | form.ng-invalid .btn[disabled], form.ng-valid .btn { 149 | display: inline-block; 150 | } 151 | 152 | 153 | 154 | -------------------------------------------------------------------------------- /public/js/vendor/angular-cookies.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license AngularJS v1.0.6 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 | 30 | 38 | 39 | 40 | */ 41 | factory('$cookies', ['$rootScope', '$browser', function ($rootScope, $browser) { 42 | var cookies = {}, 43 | lastCookies = {}, 44 | lastBrowserCookies, 45 | runEval = false, 46 | copy = angular.copy, 47 | isUndefined = angular.isUndefined; 48 | 49 | //creates a poller fn that copies all cookies from the $browser to service & inits the service 50 | $browser.addPollFn(function() { 51 | var currentCookies = $browser.cookies(); 52 | if (lastBrowserCookies != currentCookies) { //relies on browser.cookies() impl 53 | lastBrowserCookies = currentCookies; 54 | copy(currentCookies, lastCookies); 55 | copy(currentCookies, cookies); 56 | if (runEval) $rootScope.$apply(); 57 | } 58 | })(); 59 | 60 | runEval = true; 61 | 62 | //at the end of each eval, push cookies 63 | //TODO: this should happen before the "delayed" watches fire, because if some cookies are not 64 | // strings or browser refuses to store some cookies, we update the model in the push fn. 65 | $rootScope.$watch(push); 66 | 67 | return cookies; 68 | 69 | 70 | /** 71 | * Pushes all the cookies from the service to the browser and verifies if all cookies were stored. 72 | */ 73 | function push() { 74 | var name, 75 | value, 76 | browserCookies, 77 | updated; 78 | 79 | //delete any cookies deleted in $cookies 80 | for (name in lastCookies) { 81 | if (isUndefined(cookies[name])) { 82 | $browser.cookies(name, undefined); 83 | } 84 | } 85 | 86 | //update all cookies updated in $cookies 87 | for(name in cookies) { 88 | value = cookies[name]; 89 | if (!angular.isString(value)) { 90 | if (angular.isDefined(lastCookies[name])) { 91 | cookies[name] = lastCookies[name]; 92 | } else { 93 | delete cookies[name]; 94 | } 95 | } else if (value !== lastCookies[name]) { 96 | $browser.cookies(name, value); 97 | updated = true; 98 | } 99 | } 100 | 101 | //verify what was actually stored 102 | if (updated){ 103 | updated = false; 104 | browserCookies = $browser.cookies(); 105 | 106 | for (name in cookies) { 107 | if (cookies[name] !== browserCookies[name]) { 108 | //delete or reset all cookies that the browser dropped from $cookies 109 | if (isUndefined(browserCookies[name])) { 110 | delete cookies[name]; 111 | } else { 112 | cookies[name] = browserCookies[name]; 113 | } 114 | updated = true; 115 | } 116 | } 117 | } 118 | } 119 | }]). 120 | 121 | 122 | /** 123 | * @ngdoc object 124 | * @name ngCookies.$cookieStore 125 | * @requires $cookies 126 | * 127 | * @description 128 | * Provides a key-value (string-object) storage, that is backed by session cookies. 129 | * Objects put or retrieved from this storage are automatically serialized or 130 | * deserialized by angular's toJson/fromJson. 131 | * @example 132 | */ 133 | factory('$cookieStore', ['$cookies', function($cookies) { 134 | 135 | return { 136 | /** 137 | * @ngdoc method 138 | * @name ngCookies.$cookieStore#get 139 | * @methodOf ngCookies.$cookieStore 140 | * 141 | * @description 142 | * Returns the value of given cookie key 143 | * 144 | * @param {string} key Id to use for lookup. 145 | * @returns {Object} Deserialized cookie value. 146 | */ 147 | get: function(key) { 148 | return angular.fromJson($cookies[key]); 149 | }, 150 | 151 | /** 152 | * @ngdoc method 153 | * @name ngCookies.$cookieStore#put 154 | * @methodOf ngCookies.$cookieStore 155 | * 156 | * @description 157 | * Sets a value for given cookie key 158 | * 159 | * @param {string} key Id for the `value`. 160 | * @param {Object} value Value to be stored. 161 | */ 162 | put: function(key, value) { 163 | $cookies[key] = angular.toJson(value); 164 | }, 165 | 166 | /** 167 | * @ngdoc method 168 | * @name ngCookies.$cookieStore#remove 169 | * @methodOf ngCookies.$cookieStore 170 | * 171 | * @description 172 | * Remove given cookie 173 | * 174 | * @param {string} key Id of the key-value pair to delete. 175 | */ 176 | remove: function(key) { 177 | delete $cookies[key]; 178 | } 179 | }; 180 | 181 | }]); 182 | 183 | 184 | })(window, window.angular); -------------------------------------------------------------------------------- /assets/application/controllers/users.coffee: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | Application.Controllers.controller "UsersController", ["$rootScope", "$scope", "$location", "$socket", "User", "SessionService", "$route", "$routeParams", ($rootScope, $scope, $location, $socket, User, SessionService, $route, $routeParams) -> 4 | 5 | # Users class that is accessible by the window object 6 | # 7 | # @todo add a notifications handler 8 | # @todo add a proper error handler 9 | # 10 | class UsersController 11 | 12 | #### It's always nice to have a constructor to keep things organized 13 | # 14 | constructor: () -> 15 | @initScopedMethods() 16 | 17 | #### The index action 18 | # 19 | # @param [Object] $scope 20 | # 21 | # - The $scope object must be passed in to the method 22 | # since it is a public static method 23 | # 24 | index: ($scope) -> 25 | console.log "#{$rootScope.action} action called" 26 | $scope.users = User.query((success) -> 27 | console.log success 28 | , (error) -> 29 | console.log error 30 | ) 31 | 32 | 33 | #### The new action 34 | # 35 | # @param [Object] $scope 36 | # 37 | # - The $scope object must be passed in to the method 38 | # since it is a public static method 39 | # 40 | new: ($scope) -> 41 | console.log "#{$rootScope.action} action called" 42 | # $scope.user = User.get( 43 | # action: "new" 44 | # , (resource) -> 45 | # console.log resource 46 | # , (response) -> 47 | # console.log response 48 | # ) 49 | 50 | 51 | #### The edit action 52 | # 53 | # @param [Object] $scope 54 | # 55 | # - The $scope object must be passed in to the method 56 | # since it is a public static method 57 | # 58 | edit: ($scope) -> 59 | console.log "#{$rootScope.action} action called" 60 | if $routeParams.id 61 | $scope.user = User.get( 62 | id: $routeParams.id 63 | action: "edit" 64 | , (success) -> 65 | console.log success 66 | , (error) -> 67 | console.log error 68 | ) 69 | 70 | #### The show action 71 | # 72 | # @param [Object] $scope 73 | # 74 | # - The $scope object must be passed in to the method 75 | # since it is a public static method 76 | # 77 | show: ($scope) -> 78 | console.log "#{$rootScope.action} action called" 79 | if $routeParams.id 80 | $scope.user = User.get( 81 | id: $routeParams.id 82 | , (success) -> 83 | console.log success 84 | , (error) -> 85 | console.log error 86 | ) 87 | 88 | #### The create action 89 | # 90 | # @param [Object] $scope 91 | # 92 | # - The $scope object must be passed in to the method 93 | # since it is a public static method 94 | # 95 | # create: ($scope) -> 96 | # console.log "#{$rootScope.action} action called" 97 | # $scope.user = User.save {}, data, ((success) -> 98 | # console.log success 99 | # ), (error) -> 100 | # console.log error 101 | 102 | #### The update action 103 | # 104 | # @param [Object] $scope 105 | # 106 | # - The $scope object must be passed in to the method 107 | # since it is a public static method 108 | # 109 | # update: ($scope) -> 110 | # console.log "#{$rootScope.action} action called" 111 | # if $routeParams.id 112 | # $scope.user = User.update 113 | # id: $routeParams.id 114 | # , $routeParams.data, ((success) -> 115 | # console.log success 116 | # ), (error) -> 117 | # console.log error 118 | 119 | #### The delete action 120 | # 121 | # @param [Object] $scope 122 | # 123 | # - The $scope object must be passed in to the method 124 | # since it is a public static method 125 | # 126 | # delete: ($scope) -> 127 | # console.log "#{$rootScope.action} action called" 128 | # $scope.users = User.query() 129 | # $scope.user = User.delete( 130 | # id: $routeParams.id 131 | # , (resource) -> 132 | # console.log resource 133 | # , (response) -> 134 | # console.log response 135 | # ) 136 | 137 | 138 | #### Get current scope 139 | # 140 | # This method acts as a public static method to grab the 141 | # current $scope outside of this class. 142 | # (ex: socket.coffee) 143 | # 144 | getScope: () -> 145 | return $scope 146 | 147 | #### Scoped methods 148 | # 149 | # These are helper methods accessible to the angular user views 150 | # 151 | initScopedMethods: () -> 152 | $scope.showMessage = -> 153 | $scope.message && $scope.message.length 154 | 155 | $scope.getUser = (user) -> 156 | user = User.get user 157 | user 158 | 159 | $scope.getAuthenticatedUser = -> 160 | user = SessionService.getUser() 161 | user 162 | 163 | $scope.setOrder = (orderby) -> 164 | if orderby is $scope.orderby 165 | $scope.reverse = !$scope.reverse 166 | $scope.orderby = orderby 167 | 168 | $scope.addUser = -> 169 | user = new User 170 | name: $scope.inputData.name if $scope.inputData.name.length 171 | email: $scope.inputData.email if $scope.inputData.email.length 172 | password: $scope.inputData.password if $scope.inputData.password.length 173 | 174 | User.save(user, 175 | success = (data, status, headers, config) -> 176 | $scope.message = "New user added!" 177 | $scope.status = 200 178 | $socket.emit "addUser", data # Broadcast to connected subscribers that a user has been added 179 | $location.path('/users') # Redirect to users index 180 | , error = (data, status, headers, config) -> 181 | $scope.message = data.errors 182 | $scope.status = status 183 | ) 184 | 185 | $scope.updateUser = (user) -> 186 | user = { 187 | id: $scope.user.id 188 | name: $scope.user.name 189 | email: $scope.user.email 190 | password: $scope.inputData.password 191 | } 192 | 193 | User.update($scope.user, 194 | success = (data, status, headers, config) -> 195 | $scope.message = "User updated!" 196 | $scope.status = 200 197 | $socket.emit "updateUser", data # Broadcast to connected subscribers that a user has been updated 198 | $location.path('/users') # Redirect to users index 199 | $rootScope.user = data 200 | 201 | , error = (data, status, headers, config) -> 202 | $scope.message = data.errors 203 | $scope.status = status 204 | ) 205 | 206 | $scope.deleteUser = (user) -> 207 | r = confirm("Are you sure?"); 208 | if r is true 209 | User.delete( 210 | id: user.id 211 | , (success) -> 212 | console.log success 213 | $scope.users = _.difference($scope.users, user) 214 | $socket.emit "deleteUser", user # Broadcast to connected subscribers that a user has been deleted 215 | , (error) -> 216 | console.log error 217 | ) 218 | false 219 | 220 | window.UsersController = new UsersController() 221 | 222 | ] -------------------------------------------------------------------------------- /assets/application/directives/gravatar.js: -------------------------------------------------------------------------------- 1 | // A simple directive to display a gravatar image given an email 2 | Application.Directives.directive('gravatar', ['md5', function(md5) { 3 | 4 | return { 5 | restrict: 'E', 6 | template: '', 7 | replace: true, 8 | scope: { 9 | email: '=', 10 | size: '=', 11 | defaultImage: '=', 12 | forceDefault: '=' 13 | }, 14 | link: function(scope, element, attrs) { 15 | scope.options = {}; 16 | scope.$watch('email', function(email) { 17 | if ( email ) { 18 | scope.hash = md5(email.trim().toLowerCase()); 19 | } 20 | }); 21 | scope.$watch('size', function(size) { 22 | scope.options.s = (angular.isNumber(size)) ? size : undefined; 23 | generateParams(); 24 | }); 25 | scope.$watch('forceDefault', function(forceDefault) { 26 | scope.options.f = forceDefault ? 'y' : undefined; 27 | generateParams(); 28 | }); 29 | scope.$watch('defaultImage', function(defaultImage) { 30 | scope.options.d = defaultImage ? defaultImage : undefined; 31 | generateParams(); 32 | }); 33 | function generateParams() { 34 | var options = []; 35 | scope.getParams = ''; 36 | angular.forEach(scope.options, function(value, key) { 37 | if ( value ) { 38 | options.push(key + '=' + encodeURIComponent(value)); 39 | } 40 | }); 41 | if ( options.length > 0 ) { 42 | scope.getParams = '?' + options.join('&'); 43 | } 44 | } 45 | } 46 | }; 47 | }]) 48 | 49 | .factory('md5', function() { 50 | function md5cycle(x, k) { 51 | var a = x[0], 52 | b = x[1], 53 | c = x[2], 54 | d = x[3]; 55 | 56 | a = ff(a, b, c, d, k[0], 7, -680876936); 57 | d = ff(d, a, b, c, k[1], 12, -389564586); 58 | c = ff(c, d, a, b, k[2], 17, 606105819); 59 | b = ff(b, c, d, a, k[3], 22, -1044525330); 60 | a = ff(a, b, c, d, k[4], 7, -176418897); 61 | d = ff(d, a, b, c, k[5], 12, 1200080426); 62 | c = ff(c, d, a, b, k[6], 17, -1473231341); 63 | b = ff(b, c, d, a, k[7], 22, -45705983); 64 | a = ff(a, b, c, d, k[8], 7, 1770035416); 65 | d = ff(d, a, b, c, k[9], 12, -1958414417); 66 | c = ff(c, d, a, b, k[10], 17, -42063); 67 | b = ff(b, c, d, a, k[11], 22, -1990404162); 68 | a = ff(a, b, c, d, k[12], 7, 1804603682); 69 | d = ff(d, a, b, c, k[13], 12, -40341101); 70 | c = ff(c, d, a, b, k[14], 17, -1502002290); 71 | b = ff(b, c, d, a, k[15], 22, 1236535329); 72 | 73 | a = gg(a, b, c, d, k[1], 5, -165796510); 74 | d = gg(d, a, b, c, k[6], 9, -1069501632); 75 | c = gg(c, d, a, b, k[11], 14, 643717713); 76 | b = gg(b, c, d, a, k[0], 20, -373897302); 77 | a = gg(a, b, c, d, k[5], 5, -701558691); 78 | d = gg(d, a, b, c, k[10], 9, 38016083); 79 | c = gg(c, d, a, b, k[15], 14, -660478335); 80 | b = gg(b, c, d, a, k[4], 20, -405537848); 81 | a = gg(a, b, c, d, k[9], 5, 568446438); 82 | d = gg(d, a, b, c, k[14], 9, -1019803690); 83 | c = gg(c, d, a, b, k[3], 14, -187363961); 84 | b = gg(b, c, d, a, k[8], 20, 1163531501); 85 | a = gg(a, b, c, d, k[13], 5, -1444681467); 86 | d = gg(d, a, b, c, k[2], 9, -51403784); 87 | c = gg(c, d, a, b, k[7], 14, 1735328473); 88 | b = gg(b, c, d, a, k[12], 20, -1926607734); 89 | 90 | a = hh(a, b, c, d, k[5], 4, -378558); 91 | d = hh(d, a, b, c, k[8], 11, -2022574463); 92 | c = hh(c, d, a, b, k[11], 16, 1839030562); 93 | b = hh(b, c, d, a, k[14], 23, -35309556); 94 | a = hh(a, b, c, d, k[1], 4, -1530992060); 95 | d = hh(d, a, b, c, k[4], 11, 1272893353); 96 | c = hh(c, d, a, b, k[7], 16, -155497632); 97 | b = hh(b, c, d, a, k[10], 23, -1094730640); 98 | a = hh(a, b, c, d, k[13], 4, 681279174); 99 | d = hh(d, a, b, c, k[0], 11, -358537222); 100 | c = hh(c, d, a, b, k[3], 16, -722521979); 101 | b = hh(b, c, d, a, k[6], 23, 76029189); 102 | a = hh(a, b, c, d, k[9], 4, -640364487); 103 | d = hh(d, a, b, c, k[12], 11, -421815835); 104 | c = hh(c, d, a, b, k[15], 16, 530742520); 105 | b = hh(b, c, d, a, k[2], 23, -995338651); 106 | 107 | a = ii(a, b, c, d, k[0], 6, -198630844); 108 | d = ii(d, a, b, c, k[7], 10, 1126891415); 109 | c = ii(c, d, a, b, k[14], 15, -1416354905); 110 | b = ii(b, c, d, a, k[5], 21, -57434055); 111 | a = ii(a, b, c, d, k[12], 6, 1700485571); 112 | d = ii(d, a, b, c, k[3], 10, -1894986606); 113 | c = ii(c, d, a, b, k[10], 15, -1051523); 114 | b = ii(b, c, d, a, k[1], 21, -2054922799); 115 | a = ii(a, b, c, d, k[8], 6, 1873313359); 116 | d = ii(d, a, b, c, k[15], 10, -30611744); 117 | c = ii(c, d, a, b, k[6], 15, -1560198380); 118 | b = ii(b, c, d, a, k[13], 21, 1309151649); 119 | a = ii(a, b, c, d, k[4], 6, -145523070); 120 | d = ii(d, a, b, c, k[11], 10, -1120210379); 121 | c = ii(c, d, a, b, k[2], 15, 718787259); 122 | b = ii(b, c, d, a, k[9], 21, -343485551); 123 | 124 | x[0] = add32(a, x[0]); 125 | x[1] = add32(b, x[1]); 126 | x[2] = add32(c, x[2]); 127 | x[3] = add32(d, x[3]); 128 | 129 | } 130 | 131 | function cmn(q, a, b, x, s, t) { 132 | a = add32(add32(a, q), add32(x, t)); 133 | return add32((a << s) | (a >>> (32 - s)), b); 134 | } 135 | 136 | function ff(a, b, c, d, x, s, t) { 137 | return cmn((b & c) | ((~b) & d), a, b, x, s, t); 138 | } 139 | 140 | function gg(a, b, c, d, x, s, t) { 141 | return cmn((b & d) | (c & (~d)), a, b, x, s, t); 142 | } 143 | 144 | function hh(a, b, c, d, x, s, t) { 145 | return cmn(b ^ c ^ d, a, b, x, s, t); 146 | } 147 | 148 | function ii(a, b, c, d, x, s, t) { 149 | return cmn(c ^ (b | (~d)), a, b, x, s, t); 150 | } 151 | 152 | function md51(s) { 153 | txt = ''; 154 | var n = s.length, 155 | state = [1732584193, -271733879, -1732584194, 271733878], 156 | i; 157 | for (i = 64; i <= s.length; i += 64) { 158 | md5cycle(state, md5blk(s.substring(i - 64, i))); 159 | } 160 | s = s.substring(i - 64); 161 | var tail = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; 162 | for (i = 0; i < s.length; i++) { 163 | tail[i >> 2] |= s.charCodeAt(i) << ((i % 4) << 3); 164 | } 165 | tail[i >> 2] |= 0x80 << ((i % 4) << 3); 166 | if (i > 55) { 167 | md5cycle(state, tail); 168 | for (i = 0; i < 16; i++) { 169 | tail[i] = 0; 170 | } 171 | } 172 | tail[14] = n * 8; 173 | md5cycle(state, tail); 174 | return state; 175 | } 176 | 177 | /* there needs to be support for Unicode here, 178 | * unless we pretend that we can redefine the MD-5 179 | * algorithm for multi-byte characters (perhaps 180 | * by adding every four 16-bit characters and 181 | * shortening the sum to 32 bits). Otherwise 182 | * I suggest performing MD-5 as if every character 183 | * was two bytes--e.g., 0040 0025 = @%--but then 184 | * how will an ordinary MD-5 sum be matched? 185 | * There is no way to standardize text to something 186 | * like UTF-8 before transformation; speed cost is 187 | * utterly prohibitive. The JavaScript standard 188 | * itself needs to look at this: it should start 189 | * providing access to strings as preformed UTF-8 190 | * 8-bit unsigned value arrays. 191 | */ 192 | 193 | function md5blk(s) { /* I figured global was faster. */ 194 | var md5blks = [], 195 | i; /* Andy King said do it this way. */ 196 | for (i = 0; i < 64; i += 4) { 197 | md5blks[i >> 2] = s.charCodeAt(i) + (s.charCodeAt(i + 1) << 8) + (s.charCodeAt(i + 2) << 16) + (s.charCodeAt(i + 3) << 24); 198 | } 199 | return md5blks; 200 | } 201 | 202 | var hex_chr = '0123456789abcdef'.split(''); 203 | 204 | function rhex(n) { 205 | var s = '', j = 0; 206 | for (; j < 4; j++) { 207 | s += hex_chr[(n >> (j * 8 + 4)) & 0x0F] + hex_chr[(n >> (j * 8)) & 0x0F]; 208 | } 209 | return s; 210 | } 211 | 212 | function hex(x) { 213 | for (var i = 0; i < x.length; i++) { 214 | x[i] = rhex(x[i]); 215 | } 216 | return x.join(''); 217 | } 218 | 219 | function md5(s) { 220 | return hex(md51(s)); 221 | } 222 | 223 | /* this function is much faster, 224 | so if possible we use it. Some IEs 225 | are the only ones I know of that 226 | need the idiotic second function, 227 | generated by an if clause. */ 228 | 229 | add32 = function(a, b) { 230 | return (a + b) & 0xFFFFFFFF; 231 | }; 232 | 233 | if (md5('hello') !== '5d41402abc4b2a76b9719d911017c592') { 234 | add32 = function (x, y) { 235 | var lsw = (x & 0xFFFF) + (y & 0xFFFF), 236 | msw = (x >> 16) + (y >> 16) + (lsw >> 16); 237 | return (msw << 16) | (lsw & 0xFFFF); 238 | }; 239 | } 240 | 241 | return md5; 242 | }); -------------------------------------------------------------------------------- /public/js/vendor/underscore-min.js: -------------------------------------------------------------------------------- 1 | (function(){var n=this,t=n._,r={},e=Array.prototype,u=Object.prototype,i=Function.prototype,a=e.push,o=e.slice,c=e.concat,l=u.toString,f=u.hasOwnProperty,s=e.forEach,p=e.map,h=e.reduce,v=e.reduceRight,d=e.filter,g=e.every,m=e.some,y=e.indexOf,b=e.lastIndexOf,x=Array.isArray,_=Object.keys,j=i.bind,w=function(n){return n instanceof w?n:this instanceof w?(this._wrapped=n,void 0):new w(n)};"undefined"!=typeof exports?("undefined"!=typeof module&&module.exports&&(exports=module.exports=w),exports._=w):n._=w,w.VERSION="1.4.4";var A=w.each=w.forEach=function(n,t,e){if(null!=n)if(s&&n.forEach===s)n.forEach(t,e);else if(n.length===+n.length){for(var u=0,i=n.length;i>u;u++)if(t.call(e,n[u],u,n)===r)return}else for(var a in n)if(w.has(n,a)&&t.call(e,n[a],a,n)===r)return};w.map=w.collect=function(n,t,r){var e=[];return null==n?e:p&&n.map===p?n.map(t,r):(A(n,function(n,u,i){e[e.length]=t.call(r,n,u,i)}),e)};var O="Reduce of empty array with no initial value";w.reduce=w.foldl=w.inject=function(n,t,r,e){var u=arguments.length>2;if(null==n&&(n=[]),h&&n.reduce===h)return e&&(t=w.bind(t,e)),u?n.reduce(t,r):n.reduce(t);if(A(n,function(n,i,a){u?r=t.call(e,r,n,i,a):(r=n,u=!0)}),!u)throw new TypeError(O);return r},w.reduceRight=w.foldr=function(n,t,r,e){var u=arguments.length>2;if(null==n&&(n=[]),v&&n.reduceRight===v)return e&&(t=w.bind(t,e)),u?n.reduceRight(t,r):n.reduceRight(t);var i=n.length;if(i!==+i){var a=w.keys(n);i=a.length}if(A(n,function(o,c,l){c=a?a[--i]:--i,u?r=t.call(e,r,n[c],c,l):(r=n[c],u=!0)}),!u)throw new TypeError(O);return r},w.find=w.detect=function(n,t,r){var e;return E(n,function(n,u,i){return t.call(r,n,u,i)?(e=n,!0):void 0}),e},w.filter=w.select=function(n,t,r){var e=[];return null==n?e:d&&n.filter===d?n.filter(t,r):(A(n,function(n,u,i){t.call(r,n,u,i)&&(e[e.length]=n)}),e)},w.reject=function(n,t,r){return w.filter(n,function(n,e,u){return!t.call(r,n,e,u)},r)},w.every=w.all=function(n,t,e){t||(t=w.identity);var u=!0;return null==n?u:g&&n.every===g?n.every(t,e):(A(n,function(n,i,a){return(u=u&&t.call(e,n,i,a))?void 0:r}),!!u)};var E=w.some=w.any=function(n,t,e){t||(t=w.identity);var u=!1;return null==n?u:m&&n.some===m?n.some(t,e):(A(n,function(n,i,a){return u||(u=t.call(e,n,i,a))?r:void 0}),!!u)};w.contains=w.include=function(n,t){return null==n?!1:y&&n.indexOf===y?n.indexOf(t)!=-1:E(n,function(n){return n===t})},w.invoke=function(n,t){var r=o.call(arguments,2),e=w.isFunction(t);return w.map(n,function(n){return(e?t:n[t]).apply(n,r)})},w.pluck=function(n,t){return w.map(n,function(n){return n[t]})},w.where=function(n,t,r){return w.isEmpty(t)?r?null:[]:w[r?"find":"filter"](n,function(n){for(var r in t)if(t[r]!==n[r])return!1;return!0})},w.findWhere=function(n,t){return w.where(n,t,!0)},w.max=function(n,t,r){if(!t&&w.isArray(n)&&n[0]===+n[0]&&65535>n.length)return Math.max.apply(Math,n);if(!t&&w.isEmpty(n))return-1/0;var e={computed:-1/0,value:-1/0};return A(n,function(n,u,i){var a=t?t.call(r,n,u,i):n;a>=e.computed&&(e={value:n,computed:a})}),e.value},w.min=function(n,t,r){if(!t&&w.isArray(n)&&n[0]===+n[0]&&65535>n.length)return Math.min.apply(Math,n);if(!t&&w.isEmpty(n))return 1/0;var e={computed:1/0,value:1/0};return A(n,function(n,u,i){var a=t?t.call(r,n,u,i):n;e.computed>a&&(e={value:n,computed:a})}),e.value},w.shuffle=function(n){var t,r=0,e=[];return A(n,function(n){t=w.random(r++),e[r-1]=e[t],e[t]=n}),e};var k=function(n){return w.isFunction(n)?n:function(t){return t[n]}};w.sortBy=function(n,t,r){var e=k(t);return w.pluck(w.map(n,function(n,t,u){return{value:n,index:t,criteria:e.call(r,n,t,u)}}).sort(function(n,t){var r=n.criteria,e=t.criteria;if(r!==e){if(r>e||r===void 0)return 1;if(e>r||e===void 0)return-1}return n.indexi;){var o=i+a>>>1;u>r.call(e,n[o])?i=o+1:a=o}return i},w.toArray=function(n){return n?w.isArray(n)?o.call(n):n.length===+n.length?w.map(n,w.identity):w.values(n):[]},w.size=function(n){return null==n?0:n.length===+n.length?n.length:w.keys(n).length},w.first=w.head=w.take=function(n,t,r){return null==n?void 0:null==t||r?n[0]:o.call(n,0,t)},w.initial=function(n,t,r){return o.call(n,0,n.length-(null==t||r?1:t))},w.last=function(n,t,r){return null==n?void 0:null==t||r?n[n.length-1]:o.call(n,Math.max(n.length-t,0))},w.rest=w.tail=w.drop=function(n,t,r){return o.call(n,null==t||r?1:t)},w.compact=function(n){return w.filter(n,w.identity)};var R=function(n,t,r){return A(n,function(n){w.isArray(n)?t?a.apply(r,n):R(n,t,r):r.push(n)}),r};w.flatten=function(n,t){return R(n,t,[])},w.without=function(n){return w.difference(n,o.call(arguments,1))},w.uniq=w.unique=function(n,t,r,e){w.isFunction(t)&&(e=r,r=t,t=!1);var u=r?w.map(n,r,e):n,i=[],a=[];return A(u,function(r,e){(t?e&&a[a.length-1]===r:w.contains(a,r))||(a.push(r),i.push(n[e]))}),i},w.union=function(){return w.uniq(c.apply(e,arguments))},w.intersection=function(n){var t=o.call(arguments,1);return w.filter(w.uniq(n),function(n){return w.every(t,function(t){return w.indexOf(t,n)>=0})})},w.difference=function(n){var t=c.apply(e,o.call(arguments,1));return w.filter(n,function(n){return!w.contains(t,n)})},w.zip=function(){for(var n=o.call(arguments),t=w.max(w.pluck(n,"length")),r=Array(t),e=0;t>e;e++)r[e]=w.pluck(n,""+e);return r},w.object=function(n,t){if(null==n)return{};for(var r={},e=0,u=n.length;u>e;e++)t?r[n[e]]=t[e]:r[n[e][0]]=n[e][1];return r},w.indexOf=function(n,t,r){if(null==n)return-1;var e=0,u=n.length;if(r){if("number"!=typeof r)return e=w.sortedIndex(n,t),n[e]===t?e:-1;e=0>r?Math.max(0,u+r):r}if(y&&n.indexOf===y)return n.indexOf(t,r);for(;u>e;e++)if(n[e]===t)return e;return-1},w.lastIndexOf=function(n,t,r){if(null==n)return-1;var e=null!=r;if(b&&n.lastIndexOf===b)return e?n.lastIndexOf(t,r):n.lastIndexOf(t);for(var u=e?r:n.length;u--;)if(n[u]===t)return u;return-1},w.range=function(n,t,r){1>=arguments.length&&(t=n||0,n=0),r=arguments[2]||1;for(var e=Math.max(Math.ceil((t-n)/r),0),u=0,i=Array(e);e>u;)i[u++]=n,n+=r;return i},w.bind=function(n,t){if(n.bind===j&&j)return j.apply(n,o.call(arguments,1));var r=o.call(arguments,2);return function(){return n.apply(t,r.concat(o.call(arguments)))}},w.partial=function(n){var t=o.call(arguments,1);return function(){return n.apply(this,t.concat(o.call(arguments)))}},w.bindAll=function(n){var t=o.call(arguments,1);return 0===t.length&&(t=w.functions(n)),A(t,function(t){n[t]=w.bind(n[t],n)}),n},w.memoize=function(n,t){var r={};return t||(t=w.identity),function(){var e=t.apply(this,arguments);return w.has(r,e)?r[e]:r[e]=n.apply(this,arguments)}},w.delay=function(n,t){var r=o.call(arguments,2);return setTimeout(function(){return n.apply(null,r)},t)},w.defer=function(n){return w.delay.apply(w,[n,1].concat(o.call(arguments,1)))},w.throttle=function(n,t){var r,e,u,i,a=0,o=function(){a=new Date,u=null,i=n.apply(r,e)};return function(){var c=new Date,l=t-(c-a);return r=this,e=arguments,0>=l?(clearTimeout(u),u=null,a=c,i=n.apply(r,e)):u||(u=setTimeout(o,l)),i}},w.debounce=function(n,t,r){var e,u;return function(){var i=this,a=arguments,o=function(){e=null,r||(u=n.apply(i,a))},c=r&&!e;return clearTimeout(e),e=setTimeout(o,t),c&&(u=n.apply(i,a)),u}},w.once=function(n){var t,r=!1;return function(){return r?t:(r=!0,t=n.apply(this,arguments),n=null,t)}},w.wrap=function(n,t){return function(){var r=[n];return a.apply(r,arguments),t.apply(this,r)}},w.compose=function(){var n=arguments;return function(){for(var t=arguments,r=n.length-1;r>=0;r--)t=[n[r].apply(this,t)];return t[0]}},w.after=function(n,t){return 0>=n?t():function(){return 1>--n?t.apply(this,arguments):void 0}},w.keys=_||function(n){if(n!==Object(n))throw new TypeError("Invalid object");var t=[];for(var r in n)w.has(n,r)&&(t[t.length]=r);return t},w.values=function(n){var t=[];for(var r in n)w.has(n,r)&&t.push(n[r]);return t},w.pairs=function(n){var t=[];for(var r in n)w.has(n,r)&&t.push([r,n[r]]);return t},w.invert=function(n){var t={};for(var r in n)w.has(n,r)&&(t[n[r]]=r);return t},w.functions=w.methods=function(n){var t=[];for(var r in n)w.isFunction(n[r])&&t.push(r);return t.sort()},w.extend=function(n){return A(o.call(arguments,1),function(t){if(t)for(var r in t)n[r]=t[r]}),n},w.pick=function(n){var t={},r=c.apply(e,o.call(arguments,1));return A(r,function(r){r in n&&(t[r]=n[r])}),t},w.omit=function(n){var t={},r=c.apply(e,o.call(arguments,1));for(var u in n)w.contains(r,u)||(t[u]=n[u]);return t},w.defaults=function(n){return A(o.call(arguments,1),function(t){if(t)for(var r in t)null==n[r]&&(n[r]=t[r])}),n},w.clone=function(n){return w.isObject(n)?w.isArray(n)?n.slice():w.extend({},n):n},w.tap=function(n,t){return t(n),n};var I=function(n,t,r,e){if(n===t)return 0!==n||1/n==1/t;if(null==n||null==t)return n===t;n instanceof w&&(n=n._wrapped),t instanceof w&&(t=t._wrapped);var u=l.call(n);if(u!=l.call(t))return!1;switch(u){case"[object String]":return n==t+"";case"[object Number]":return n!=+n?t!=+t:0==n?1/n==1/t:n==+t;case"[object Date]":case"[object Boolean]":return+n==+t;case"[object RegExp]":return n.source==t.source&&n.global==t.global&&n.multiline==t.multiline&&n.ignoreCase==t.ignoreCase}if("object"!=typeof n||"object"!=typeof t)return!1;for(var i=r.length;i--;)if(r[i]==n)return e[i]==t;r.push(n),e.push(t);var a=0,o=!0;if("[object Array]"==u){if(a=n.length,o=a==t.length)for(;a--&&(o=I(n[a],t[a],r,e)););}else{var c=n.constructor,f=t.constructor;if(c!==f&&!(w.isFunction(c)&&c instanceof c&&w.isFunction(f)&&f instanceof f))return!1;for(var s in n)if(w.has(n,s)&&(a++,!(o=w.has(t,s)&&I(n[s],t[s],r,e))))break;if(o){for(s in t)if(w.has(t,s)&&!a--)break;o=!a}}return r.pop(),e.pop(),o};w.isEqual=function(n,t){return I(n,t,[],[])},w.isEmpty=function(n){if(null==n)return!0;if(w.isArray(n)||w.isString(n))return 0===n.length;for(var t in n)if(w.has(n,t))return!1;return!0},w.isElement=function(n){return!(!n||1!==n.nodeType)},w.isArray=x||function(n){return"[object Array]"==l.call(n)},w.isObject=function(n){return n===Object(n)},A(["Arguments","Function","String","Number","Date","RegExp"],function(n){w["is"+n]=function(t){return l.call(t)=="[object "+n+"]"}}),w.isArguments(arguments)||(w.isArguments=function(n){return!(!n||!w.has(n,"callee"))}),"function"!=typeof/./&&(w.isFunction=function(n){return"function"==typeof n}),w.isFinite=function(n){return isFinite(n)&&!isNaN(parseFloat(n))},w.isNaN=function(n){return w.isNumber(n)&&n!=+n},w.isBoolean=function(n){return n===!0||n===!1||"[object Boolean]"==l.call(n)},w.isNull=function(n){return null===n},w.isUndefined=function(n){return n===void 0},w.has=function(n,t){return f.call(n,t)},w.noConflict=function(){return n._=t,this},w.identity=function(n){return n},w.times=function(n,t,r){for(var e=Array(n),u=0;n>u;u++)e[u]=t.call(r,u);return e},w.random=function(n,t){return null==t&&(t=n,n=0),n+Math.floor(Math.random()*(t-n+1))};var M={escape:{"&":"&","<":"<",">":">",'"':""","'":"'","/":"/"}};M.unescape=w.invert(M.escape);var S={escape:RegExp("["+w.keys(M.escape).join("")+"]","g"),unescape:RegExp("("+w.keys(M.unescape).join("|")+")","g")};w.each(["escape","unescape"],function(n){w[n]=function(t){return null==t?"":(""+t).replace(S[n],function(t){return M[n][t]})}}),w.result=function(n,t){if(null==n)return null;var r=n[t];return w.isFunction(r)?r.call(n):r},w.mixin=function(n){A(w.functions(n),function(t){var r=w[t]=n[t];w.prototype[t]=function(){var n=[this._wrapped];return a.apply(n,arguments),D.call(this,r.apply(w,n))}})};var N=0;w.uniqueId=function(n){var t=++N+"";return n?n+t:t},w.templateSettings={evaluate:/<%([\s\S]+?)%>/g,interpolate:/<%=([\s\S]+?)%>/g,escape:/<%-([\s\S]+?)%>/g};var T=/(.)^/,q={"'":"'","\\":"\\","\r":"r","\n":"n"," ":"t","\u2028":"u2028","\u2029":"u2029"},B=/\\|'|\r|\n|\t|\u2028|\u2029/g;w.template=function(n,t,r){var e;r=w.defaults({},r,w.templateSettings);var u=RegExp([(r.escape||T).source,(r.interpolate||T).source,(r.evaluate||T).source].join("|")+"|$","g"),i=0,a="__p+='";n.replace(u,function(t,r,e,u,o){return a+=n.slice(i,o).replace(B,function(n){return"\\"+q[n]}),r&&(a+="'+\n((__t=("+r+"))==null?'':_.escape(__t))+\n'"),e&&(a+="'+\n((__t=("+e+"))==null?'':__t)+\n'"),u&&(a+="';\n"+u+"\n__p+='"),i=o+t.length,t}),a+="';\n",r.variable||(a="with(obj||{}){\n"+a+"}\n"),a="var __t,__p='',__j=Array.prototype.join,"+"print=function(){__p+=__j.call(arguments,'');};\n"+a+"return __p;\n";try{e=Function(r.variable||"obj","_",a)}catch(o){throw o.source=a,o}if(t)return e(t,w);var c=function(n){return e.call(this,n,w)};return c.source="function("+(r.variable||"obj")+"){\n"+a+"}",c},w.chain=function(n){return w(n).chain()};var D=function(n){return this._chain?w(n).chain():n};w.mixin(w),A(["pop","push","reverse","shift","sort","splice","unshift"],function(n){var t=e[n];w.prototype[n]=function(){var r=this._wrapped;return t.apply(r,arguments),"shift"!=n&&"splice"!=n||0!==r.length||delete r[0],D.call(this,r)}}),A(["concat","join","slice"],function(n){var t=e[n];w.prototype[n]=function(){return D.call(this,t.apply(this._wrapped,arguments))}}),w.extend(w.prototype,{chain:function(){return this._chain=!0,this},value:function(){return this._wrapped}})}).call(this); -------------------------------------------------------------------------------- /assets/styles/bootstrap-responsive.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap Responsive v2.3.1 3 | * 4 | * Copyright 2012 Twitter, Inc 5 | * Licensed under the Apache License v2.0 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Designed and built with all the love in the world @twitter by @mdo and @fat. 9 | */ 10 | .clearfix{*zoom:1;}.clearfix:before,.clearfix:after{display:table;content:"";line-height:0;} 11 | .clearfix:after{clear:both;} 12 | .hide-text{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0;} 13 | .input-block-level{display:block;width:100%;min-height:30px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;} 14 | @-ms-viewport{width:device-width;}.hidden{display:none;visibility:hidden;} 15 | .visible-phone{display:none !important;} 16 | .visible-tablet{display:none !important;} 17 | .hidden-desktop{display:none !important;} 18 | .visible-desktop{display:inherit !important;} 19 | @media (min-width:768px) and (max-width:979px){.hidden-desktop{display:inherit !important;} .visible-desktop{display:none !important ;} .visible-tablet{display:inherit !important;} .hidden-tablet{display:none !important;}}@media (max-width:767px){.hidden-desktop{display:inherit !important;} .visible-desktop{display:none !important;} .visible-phone{display:inherit !important;} .hidden-phone{display:none !important;}}.visible-print{display:none !important;} 20 | @media print{.visible-print{display:inherit !important;} .hidden-print{display:none !important;}}@media (max-width:767px){body{padding-left:20px;padding-right:20px;} .navbar-fixed-top,.navbar-fixed-bottom,.navbar-static-top{margin-left:-20px;margin-right:-20px;} .container-fluid{padding:0;} .dl-horizontal dt{float:none;clear:none;width:auto;text-align:left;} .dl-horizontal dd{margin-left:0;} .container{width:auto;} .row-fluid{width:100%;} .row,.thumbnails{margin-left:0;} .thumbnails>li{float:none;margin-left:0;} [class*="span"],.uneditable-input[class*="span"],.row-fluid [class*="span"]{float:none;display:block;width:100%;margin-left:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;} .span12,.row-fluid .span12{width:100%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;} .row-fluid [class*="offset"]:first-child{margin-left:0;} .input-large,.input-xlarge,.input-xxlarge,input[class*="span"],select[class*="span"],textarea[class*="span"],.uneditable-input{display:block;width:100%;min-height:30px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;} .input-prepend input,.input-append input,.input-prepend input[class*="span"],.input-append input[class*="span"]{display:inline-block;width:auto;} .controls-row [class*="span"]+[class*="span"]{margin-left:0;} .modal{position:fixed;top:20px;left:20px;right:20px;width:auto;margin:0;}.modal.fade{top:-100px;} .modal.fade.in{top:20px;}}@media (max-width:480px){.nav-collapse{-webkit-transform:translate3d(0, 0, 0);} .page-header h1 small{display:block;line-height:20px;} input[type="checkbox"],input[type="radio"]{border:1px solid #ccc;} .form-horizontal .control-label{float:none;width:auto;padding-top:0;text-align:left;} .form-horizontal .controls{margin-left:0;} .form-horizontal .control-list{padding-top:0;} .form-horizontal .form-actions{padding-left:10px;padding-right:10px;} .media .pull-left,.media .pull-right{float:none;display:block;margin-bottom:10px;} .media-object{margin-right:0;margin-left:0;} .modal{top:10px;left:10px;right:10px;} .modal-header .close{padding:10px;margin:-10px;} .carousel-caption{position:static;}}@media (min-width:768px) and (max-width:979px){.row{margin-left:-20px;*zoom:1;}.row:before,.row:after{display:table;content:"";line-height:0;} .row:after{clear:both;} [class*="span"]{float:left;min-height:1px;margin-left:20px;} .container,.navbar-static-top .container,.navbar-fixed-top .container,.navbar-fixed-bottom .container{width:724px;} .span12{width:724px;} .span11{width:662px;} .span10{width:600px;} .span9{width:538px;} .span8{width:476px;} .span7{width:414px;} .span6{width:352px;} .span5{width:290px;} .span4{width:228px;} .span3{width:166px;} .span2{width:104px;} .span1{width:42px;} .offset12{margin-left:764px;} .offset11{margin-left:702px;} .offset10{margin-left:640px;} .offset9{margin-left:578px;} .offset8{margin-left:516px;} .offset7{margin-left:454px;} .offset6{margin-left:392px;} .offset5{margin-left:330px;} .offset4{margin-left:268px;} .offset3{margin-left:206px;} .offset2{margin-left:144px;} .offset1{margin-left:82px;} .row-fluid{width:100%;*zoom:1;}.row-fluid:before,.row-fluid:after{display:table;content:"";line-height:0;} .row-fluid:after{clear:both;} .row-fluid [class*="span"]{display:block;width:100%;min-height:30px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;float:left;margin-left:2.7624309392265194%;*margin-left:2.709239449864817%;} .row-fluid [class*="span"]:first-child{margin-left:0;} .row-fluid .controls-row [class*="span"]+[class*="span"]{margin-left:2.7624309392265194%;} .row-fluid .span12{width:100%;*width:99.94680851063829%;} .row-fluid .span11{width:91.43646408839778%;*width:91.38327259903608%;} .row-fluid .span10{width:82.87292817679558%;*width:82.81973668743387%;} .row-fluid .span9{width:74.30939226519337%;*width:74.25620077583166%;} .row-fluid .span8{width:65.74585635359117%;*width:65.69266486422946%;} .row-fluid .span7{width:57.18232044198895%;*width:57.12912895262725%;} .row-fluid .span6{width:48.61878453038674%;*width:48.56559304102504%;} .row-fluid .span5{width:40.05524861878453%;*width:40.00205712942283%;} .row-fluid .span4{width:31.491712707182323%;*width:31.43852121782062%;} .row-fluid .span3{width:22.92817679558011%;*width:22.87498530621841%;} .row-fluid .span2{width:14.3646408839779%;*width:14.311449394616199%;} .row-fluid .span1{width:5.801104972375691%;*width:5.747913483013988%;} .row-fluid .offset12{margin-left:105.52486187845304%;*margin-left:105.41847889972962%;} .row-fluid .offset12:first-child{margin-left:102.76243093922652%;*margin-left:102.6560479605031%;} .row-fluid .offset11{margin-left:96.96132596685082%;*margin-left:96.8549429881274%;} .row-fluid .offset11:first-child{margin-left:94.1988950276243%;*margin-left:94.09251204890089%;} .row-fluid .offset10{margin-left:88.39779005524862%;*margin-left:88.2914070765252%;} .row-fluid .offset10:first-child{margin-left:85.6353591160221%;*margin-left:85.52897613729868%;} .row-fluid .offset9{margin-left:79.8342541436464%;*margin-left:79.72787116492299%;} .row-fluid .offset9:first-child{margin-left:77.07182320441989%;*margin-left:76.96544022569647%;} .row-fluid .offset8{margin-left:71.2707182320442%;*margin-left:71.16433525332079%;} .row-fluid .offset8:first-child{margin-left:68.50828729281768%;*margin-left:68.40190431409427%;} .row-fluid .offset7{margin-left:62.70718232044199%;*margin-left:62.600799341718584%;} .row-fluid .offset7:first-child{margin-left:59.94475138121547%;*margin-left:59.838368402492065%;} .row-fluid .offset6{margin-left:54.14364640883978%;*margin-left:54.037263430116376%;} .row-fluid .offset6:first-child{margin-left:51.38121546961326%;*margin-left:51.27483249088986%;} .row-fluid .offset5{margin-left:45.58011049723757%;*margin-left:45.47372751851417%;} .row-fluid .offset5:first-child{margin-left:42.81767955801105%;*margin-left:42.71129657928765%;} .row-fluid .offset4{margin-left:37.01657458563536%;*margin-left:36.91019160691196%;} .row-fluid .offset4:first-child{margin-left:34.25414364640884%;*margin-left:34.14776066768544%;} .row-fluid .offset3{margin-left:28.45303867403315%;*margin-left:28.346655695309746%;} .row-fluid .offset3:first-child{margin-left:25.69060773480663%;*margin-left:25.584224756083227%;} .row-fluid .offset2{margin-left:19.88950276243094%;*margin-left:19.783119783707537%;} .row-fluid .offset2:first-child{margin-left:17.12707182320442%;*margin-left:17.02068884448102%;} .row-fluid .offset1{margin-left:11.32596685082873%;*margin-left:11.219583872105325%;} .row-fluid .offset1:first-child{margin-left:8.56353591160221%;*margin-left:8.457152932878806%;} input,textarea,.uneditable-input{margin-left:0;} .controls-row [class*="span"]+[class*="span"]{margin-left:20px;} input.span12,textarea.span12,.uneditable-input.span12{width:710px;} input.span11,textarea.span11,.uneditable-input.span11{width:648px;} input.span10,textarea.span10,.uneditable-input.span10{width:586px;} input.span9,textarea.span9,.uneditable-input.span9{width:524px;} input.span8,textarea.span8,.uneditable-input.span8{width:462px;} input.span7,textarea.span7,.uneditable-input.span7{width:400px;} input.span6,textarea.span6,.uneditable-input.span6{width:338px;} input.span5,textarea.span5,.uneditable-input.span5{width:276px;} input.span4,textarea.span4,.uneditable-input.span4{width:214px;} input.span3,textarea.span3,.uneditable-input.span3{width:152px;} input.span2,textarea.span2,.uneditable-input.span2{width:90px;} input.span1,textarea.span1,.uneditable-input.span1{width:28px;}}@media (min-width:1200px){.row{margin-left:-30px;*zoom:1;}.row:before,.row:after{display:table;content:"";line-height:0;} .row:after{clear:both;} [class*="span"]{float:left;min-height:1px;margin-left:30px;} .container,.navbar-static-top .container,.navbar-fixed-top .container,.navbar-fixed-bottom .container{width:1170px;} .span12{width:1170px;} .span11{width:1070px;} .span10{width:970px;} .span9{width:870px;} .span8{width:770px;} .span7{width:670px;} .span6{width:570px;} .span5{width:470px;} .span4{width:370px;} .span3{width:270px;} .span2{width:170px;} .span1{width:70px;} .offset12{margin-left:1230px;} .offset11{margin-left:1130px;} .offset10{margin-left:1030px;} .offset9{margin-left:930px;} .offset8{margin-left:830px;} .offset7{margin-left:730px;} .offset6{margin-left:630px;} .offset5{margin-left:530px;} .offset4{margin-left:430px;} .offset3{margin-left:330px;} .offset2{margin-left:230px;} .offset1{margin-left:130px;} .row-fluid{width:100%;*zoom:1;}.row-fluid:before,.row-fluid:after{display:table;content:"";line-height:0;} .row-fluid:after{clear:both;} .row-fluid [class*="span"]{display:block;width:100%;min-height:30px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;float:left;margin-left:2.564102564102564%;*margin-left:2.5109110747408616%;} .row-fluid [class*="span"]:first-child{margin-left:0;} .row-fluid .controls-row [class*="span"]+[class*="span"]{margin-left:2.564102564102564%;} .row-fluid .span12{width:100%;*width:99.94680851063829%;} .row-fluid .span11{width:91.45299145299145%;*width:91.39979996362975%;} .row-fluid .span10{width:82.90598290598291%;*width:82.8527914166212%;} .row-fluid .span9{width:74.35897435897436%;*width:74.30578286961266%;} .row-fluid .span8{width:65.81196581196582%;*width:65.75877432260411%;} .row-fluid .span7{width:57.26495726495726%;*width:57.21176577559556%;} .row-fluid .span6{width:48.717948717948715%;*width:48.664757228587014%;} .row-fluid .span5{width:40.17094017094017%;*width:40.11774868157847%;} .row-fluid .span4{width:31.623931623931625%;*width:31.570740134569924%;} .row-fluid .span3{width:23.076923076923077%;*width:23.023731587561375%;} .row-fluid .span2{width:14.52991452991453%;*width:14.476723040552828%;} .row-fluid .span1{width:5.982905982905983%;*width:5.929714493544281%;} .row-fluid .offset12{margin-left:105.12820512820512%;*margin-left:105.02182214948171%;} .row-fluid .offset12:first-child{margin-left:102.56410256410257%;*margin-left:102.45771958537915%;} .row-fluid .offset11{margin-left:96.58119658119658%;*margin-left:96.47481360247316%;} .row-fluid .offset11:first-child{margin-left:94.01709401709402%;*margin-left:93.91071103837061%;} .row-fluid .offset10{margin-left:88.03418803418803%;*margin-left:87.92780505546462%;} .row-fluid .offset10:first-child{margin-left:85.47008547008548%;*margin-left:85.36370249136206%;} .row-fluid .offset9{margin-left:79.48717948717949%;*margin-left:79.38079650845607%;} .row-fluid .offset9:first-child{margin-left:76.92307692307693%;*margin-left:76.81669394435352%;} .row-fluid .offset8{margin-left:70.94017094017094%;*margin-left:70.83378796144753%;} .row-fluid .offset8:first-child{margin-left:68.37606837606839%;*margin-left:68.26968539734497%;} .row-fluid .offset7{margin-left:62.393162393162385%;*margin-left:62.28677941443899%;} .row-fluid .offset7:first-child{margin-left:59.82905982905982%;*margin-left:59.72267685033642%;} .row-fluid .offset6{margin-left:53.84615384615384%;*margin-left:53.739770867430444%;} .row-fluid .offset6:first-child{margin-left:51.28205128205128%;*margin-left:51.175668303327875%;} .row-fluid .offset5{margin-left:45.299145299145295%;*margin-left:45.1927623204219%;} .row-fluid .offset5:first-child{margin-left:42.73504273504273%;*margin-left:42.62865975631933%;} .row-fluid .offset4{margin-left:36.75213675213675%;*margin-left:36.645753773413354%;} .row-fluid .offset4:first-child{margin-left:34.18803418803419%;*margin-left:34.081651209310785%;} .row-fluid .offset3{margin-left:28.205128205128204%;*margin-left:28.0987452264048%;} .row-fluid .offset3:first-child{margin-left:25.641025641025642%;*margin-left:25.53464266230224%;} .row-fluid .offset2{margin-left:19.65811965811966%;*margin-left:19.551736679396257%;} .row-fluid .offset2:first-child{margin-left:17.094017094017094%;*margin-left:16.98763411529369%;} .row-fluid .offset1{margin-left:11.11111111111111%;*margin-left:11.004728132387708%;} .row-fluid .offset1:first-child{margin-left:8.547008547008547%;*margin-left:8.440625568285142%;} input,textarea,.uneditable-input{margin-left:0;} .controls-row [class*="span"]+[class*="span"]{margin-left:30px;} input.span12,textarea.span12,.uneditable-input.span12{width:1156px;} input.span11,textarea.span11,.uneditable-input.span11{width:1056px;} input.span10,textarea.span10,.uneditable-input.span10{width:956px;} input.span9,textarea.span9,.uneditable-input.span9{width:856px;} input.span8,textarea.span8,.uneditable-input.span8{width:756px;} input.span7,textarea.span7,.uneditable-input.span7{width:656px;} input.span6,textarea.span6,.uneditable-input.span6{width:556px;} input.span5,textarea.span5,.uneditable-input.span5{width:456px;} input.span4,textarea.span4,.uneditable-input.span4{width:356px;} input.span3,textarea.span3,.uneditable-input.span3{width:256px;} input.span2,textarea.span2,.uneditable-input.span2{width:156px;} input.span1,textarea.span1,.uneditable-input.span1{width:56px;} .thumbnails{margin-left:-30px;} .thumbnails>li{margin-left:30px;} .row-fluid .thumbnails{margin-left:0;}}@media (max-width:979px){body{padding-top:0;} .navbar-fixed-top,.navbar-fixed-bottom{position:static;} .navbar-fixed-top{margin-bottom:20px;} .navbar-fixed-bottom{margin-top:20px;} .navbar-fixed-top .navbar-inner,.navbar-fixed-bottom .navbar-inner{padding:5px;} .navbar .container{width:auto;padding:0;} .navbar .brand{padding-left:10px;padding-right:10px;margin:0 0 0 -5px;} .nav-collapse{clear:both;} .nav-collapse .nav{float:none;margin:0 0 10px;} .nav-collapse .nav>li{float:none;} .nav-collapse .nav>li>a{margin-bottom:2px;} .nav-collapse .nav>.divider-vertical{display:none;} .nav-collapse .nav .nav-header{color:#777777;text-shadow:none;} .nav-collapse .nav>li>a,.nav-collapse .dropdown-menu a{padding:9px 15px;font-weight:bold;color:#777777;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;} .nav-collapse .btn{padding:4px 10px 4px;font-weight:normal;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;} .nav-collapse .dropdown-menu li+li a{margin-bottom:2px;} .nav-collapse .nav>li>a:hover,.nav-collapse .nav>li>a:focus,.nav-collapse .dropdown-menu a:hover,.nav-collapse .dropdown-menu a:focus{background-color:#f2f2f2;} .navbar-inverse .nav-collapse .nav>li>a,.navbar-inverse .nav-collapse .dropdown-menu a{color:#999999;} .navbar-inverse .nav-collapse .nav>li>a:hover,.navbar-inverse .nav-collapse .nav>li>a:focus,.navbar-inverse .nav-collapse .dropdown-menu a:hover,.navbar-inverse .nav-collapse .dropdown-menu a:focus{background-color:#111111;} .nav-collapse.in .btn-group{margin-top:5px;padding:0;} .nav-collapse .dropdown-menu{position:static;top:auto;left:auto;float:none;display:none;max-width:none;margin:0 15px;padding:0;background-color:transparent;border:none;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none;} .nav-collapse .open>.dropdown-menu{display:block;} .nav-collapse .dropdown-menu:before,.nav-collapse .dropdown-menu:after{display:none;} .nav-collapse .dropdown-menu .divider{display:none;} .nav-collapse .nav>li>.dropdown-menu:before,.nav-collapse .nav>li>.dropdown-menu:after{display:none;} .nav-collapse .navbar-form,.nav-collapse .navbar-search{float:none;padding:10px 15px;margin:10px 0;border-top:1px solid #f2f2f2;border-bottom:1px solid #f2f2f2;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.1), 0 1px 0 rgba(255,255,255,.1);-moz-box-shadow:inset 0 1px 0 rgba(255,255,255,.1), 0 1px 0 rgba(255,255,255,.1);box-shadow:inset 0 1px 0 rgba(255,255,255,.1), 0 1px 0 rgba(255,255,255,.1);} .navbar-inverse .nav-collapse .navbar-form,.navbar-inverse .nav-collapse .navbar-search{border-top-color:#111111;border-bottom-color:#111111;} .navbar .nav-collapse .nav.pull-right{float:none;margin-left:0;} .nav-collapse,.nav-collapse.collapse{overflow:hidden;height:0;} .navbar .btn-navbar{display:block;} .navbar-static .navbar-inner{padding-left:10px;padding-right:10px;}}@media (min-width:980px){.nav-collapse.collapse{height:auto !important;overflow:visible !important;}} 21 | -------------------------------------------------------------------------------- /public/js/vendor/angular-resource.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license AngularJS v1.0.6 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 | * # Installation 28 | * To use $resource make sure you have included the `angular-resource.js` that comes in Angular 29 | * package. You can also find this file on Google CDN, bower as well as at 30 | * {@link http://code.angularjs.org/ code.angularjs.org}. 31 | * 32 | * Finally load the module in your application: 33 | * 34 | * angular.module('app', ['ngResource']); 35 | * 36 | * and you are ready to get started! 37 | * 38 | * @param {string} url A parameterized URL template with parameters prefixed by `:` as in 39 | * `/user/:username`. If you are using a URL with a port number (e.g. 40 | * `http://example.com:8080/api`), you'll need to escape the colon character before the port 41 | * number, like this: `$resource('http://example.com\\:8080/api')`. 42 | * 43 | * @param {Object=} paramDefaults Default values for `url` parameters. These can be overridden in 44 | * `actions` methods. 45 | * 46 | * Each key value in the parameter object is first bound to url template if present and then any 47 | * excess keys are appended to the url search query after the `?`. 48 | * 49 | * Given a template `/path/:verb` and parameter `{verb:'greet', salutation:'Hello'}` results in 50 | * URL `/path/greet?salutation=Hello`. 51 | * 52 | * If the parameter value is prefixed with `@` then the value of that parameter is extracted from 53 | * the data object (useful for non-GET operations). 54 | * 55 | * @param {Object.=} actions Hash with declaration of custom action that should extend the 56 | * default set of resource actions. The declaration should be created in the following format: 57 | * 58 | * {action1: {method:?, params:?, isArray:?}, 59 | * action2: {method:?, params:?, isArray:?}, 60 | * ...} 61 | * 62 | * Where: 63 | * 64 | * - `action` – {string} – The name of action. This name becomes the name of the method on your 65 | * resource object. 66 | * - `method` – {string} – HTTP request method. Valid methods are: `GET`, `POST`, `PUT`, `DELETE`, 67 | * and `JSONP` 68 | * - `params` – {object=} – Optional set of pre-bound parameters for this action. 69 | * - isArray – {boolean=} – If true then the returned object for this action is an array, see 70 | * `returns` section. 71 | * 72 | * @returns {Object} A resource "class" object with methods for the default set of resource actions 73 | * optionally extended with custom `actions`. The default set contains these actions: 74 | * 75 | * { 'get': {method:'GET'}, 76 | * 'save': {method:'POST'}, 77 | * 'query': {method:'GET', isArray:true}, 78 | * 'remove': {method:'DELETE'}, 79 | * 'delete': {method:'DELETE'} }; 80 | * 81 | * Calling these methods invoke an {@link ng.$http} with the specified http method, 82 | * destination and parameters. When the data is returned from the server then the object is an 83 | * instance of the resource class. The actions `save`, `remove` and `delete` are available on it 84 | * as methods with the `$` prefix. This allows you to easily perform CRUD operations (create, 85 | * read, update, delete) on server-side data like this: 86 | *
 87 |         var User = $resource('/user/:userId', {userId:'@id'});
 88 |         var user = User.get({userId:123}, function() {
 89 |           user.abc = true;
 90 |           user.$save();
 91 |         });
 92 |      
93 | * 94 | * It is important to realize that invoking a $resource object method immediately returns an 95 | * empty reference (object or array depending on `isArray`). Once the data is returned from the 96 | * server the existing reference is populated with the actual data. This is a useful trick since 97 | * usually the resource is assigned to a model which is then rendered by the view. Having an empty 98 | * object results in no rendering, once the data arrives from the server then the object is 99 | * populated with the data and the view automatically re-renders itself showing the new data. This 100 | * means that in most case one never has to write a callback function for the action methods. 101 | * 102 | * The action methods on the class object or instance object can be invoked with the following 103 | * parameters: 104 | * 105 | * - HTTP GET "class" actions: `Resource.action([parameters], [success], [error])` 106 | * - non-GET "class" actions: `Resource.action([parameters], postData, [success], [error])` 107 | * - non-GET instance actions: `instance.$action([parameters], [success], [error])` 108 | * 109 | * 110 | * @example 111 | * 112 | * # Credit card resource 113 | * 114 | *
115 |      // Define CreditCard class
116 |      var CreditCard = $resource('/user/:userId/card/:cardId',
117 |       {userId:123, cardId:'@id'}, {
118 |        charge: {method:'POST', params:{charge:true}}
119 |       });
120 | 
121 |      // We can retrieve a collection from the server
122 |      var cards = CreditCard.query(function() {
123 |        // GET: /user/123/card
124 |        // server returns: [ {id:456, number:'1234', name:'Smith'} ];
125 | 
126 |        var card = cards[0];
127 |        // each item is an instance of CreditCard
128 |        expect(card instanceof CreditCard).toEqual(true);
129 |        card.name = "J. Smith";
130 |        // non GET methods are mapped onto the instances
131 |        card.$save();
132 |        // POST: /user/123/card/456 {id:456, number:'1234', name:'J. Smith'}
133 |        // server returns: {id:456, number:'1234', name: 'J. Smith'};
134 | 
135 |        // our custom method is mapped as well.
136 |        card.$charge({amount:9.99});
137 |        // POST: /user/123/card/456?amount=9.99&charge=true {id:456, number:'1234', name:'J. Smith'}
138 |      });
139 | 
140 |      // we can create an instance as well
141 |      var newCard = new CreditCard({number:'0123'});
142 |      newCard.name = "Mike Smith";
143 |      newCard.$save();
144 |      // POST: /user/123/card {number:'0123', name:'Mike Smith'}
145 |      // server returns: {id:789, number:'01234', name: 'Mike Smith'};
146 |      expect(newCard.id).toEqual(789);
147 |  * 
148 | * 149 | * The object returned from this function execution is a resource "class" which has "static" method 150 | * for each action in the definition. 151 | * 152 | * Calling these methods invoke `$http` on the `url` template with the given `method` and `params`. 153 | * When the data is returned from the server then the object is an instance of the resource type and 154 | * all of the non-GET methods are available with `$` prefix. This allows you to easily support CRUD 155 | * operations (create, read, update, delete) on server-side data. 156 | 157 |
158 |      var User = $resource('/user/:userId', {userId:'@id'});
159 |      var user = User.get({userId:123}, function() {
160 |        user.abc = true;
161 |        user.$save();
162 |      });
163 |    
164 | * 165 | * It's worth noting that the success callback for `get`, `query` and other method gets passed 166 | * in the response that came from the server as well as $http header getter function, so one 167 | * could rewrite the above example and get access to http headers as: 168 | * 169 |
170 |      var User = $resource('/user/:userId', {userId:'@id'});
171 |      User.get({userId:123}, function(u, getResponseHeaders){
172 |        u.abc = true;
173 |        u.$save(function(u, putResponseHeaders) {
174 |          //u => saved user object
175 |          //putResponseHeaders => $http header getter
176 |        });
177 |      });
178 |    
179 | 180 | * # Buzz client 181 | 182 | Let's look at what a buzz client created with the `$resource` service looks like: 183 | 184 | 185 | 205 | 206 |
207 | 208 | 209 |
210 |
211 |

212 | 213 | {{item.actor.name}} 214 | Expand replies: {{item.links.replies[0].count}} 215 |

216 | {{item.object.content | html}} 217 |
218 | 219 | {{reply.actor.name}}: {{reply.content | html}} 220 |
221 |
222 |
223 |
224 | 225 | 226 |
227 | */ 228 | angular.module('ngResource', ['ng']). 229 | factory('$resource', ['$http', '$parse', function($http, $parse) { 230 | var DEFAULT_ACTIONS = { 231 | 'get': {method:'GET'}, 232 | 'save': {method:'POST'}, 233 | 'query': {method:'GET', isArray:true}, 234 | 'remove': {method:'DELETE'}, 235 | 'delete': {method:'DELETE'} 236 | }; 237 | var noop = angular.noop, 238 | forEach = angular.forEach, 239 | extend = angular.extend, 240 | copy = angular.copy, 241 | isFunction = angular.isFunction, 242 | getter = function(obj, path) { 243 | return $parse(path)(obj); 244 | }; 245 | 246 | /** 247 | * We need our custom method because encodeURIComponent is too aggressive and doesn't follow 248 | * http://www.ietf.org/rfc/rfc3986.txt with regards to the character set (pchar) allowed in path 249 | * segments: 250 | * segment = *pchar 251 | * pchar = unreserved / pct-encoded / sub-delims / ":" / "@" 252 | * pct-encoded = "%" HEXDIG HEXDIG 253 | * unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" 254 | * sub-delims = "!" / "$" / "&" / "'" / "(" / ")" 255 | * / "*" / "+" / "," / ";" / "=" 256 | */ 257 | function encodeUriSegment(val) { 258 | return encodeUriQuery(val, true). 259 | replace(/%26/gi, '&'). 260 | replace(/%3D/gi, '='). 261 | replace(/%2B/gi, '+'); 262 | } 263 | 264 | 265 | /** 266 | * This method is intended for encoding *key* or *value* parts of query component. We need a custom 267 | * method becuase encodeURIComponent is too agressive and encodes stuff that doesn't have to be 268 | * encoded per http://tools.ietf.org/html/rfc3986: 269 | * query = *( pchar / "/" / "?" ) 270 | * pchar = unreserved / pct-encoded / sub-delims / ":" / "@" 271 | * unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" 272 | * pct-encoded = "%" HEXDIG HEXDIG 273 | * sub-delims = "!" / "$" / "&" / "'" / "(" / ")" 274 | * / "*" / "+" / "," / ";" / "=" 275 | */ 276 | function encodeUriQuery(val, pctEncodeSpaces) { 277 | return encodeURIComponent(val). 278 | replace(/%40/gi, '@'). 279 | replace(/%3A/gi, ':'). 280 | replace(/%24/g, '$'). 281 | replace(/%2C/gi, ','). 282 | replace(/%20/g, (pctEncodeSpaces ? '%20' : '+')); 283 | } 284 | 285 | function Route(template, defaults) { 286 | this.template = template = template + '#'; 287 | this.defaults = defaults || {}; 288 | var urlParams = this.urlParams = {}; 289 | forEach(template.split(/\W/), function(param){ 290 | if (param && (new RegExp("(^|[^\\\\]):" + param + "\\W").test(template))) { 291 | urlParams[param] = true; 292 | } 293 | }); 294 | this.template = template.replace(/\\:/g, ':'); 295 | } 296 | 297 | Route.prototype = { 298 | url: function(params) { 299 | var self = this, 300 | url = this.template, 301 | val, 302 | encodedVal; 303 | 304 | params = params || {}; 305 | forEach(this.urlParams, function(_, urlParam){ 306 | val = params.hasOwnProperty(urlParam) ? params[urlParam] : self.defaults[urlParam]; 307 | if (angular.isDefined(val) && val !== null) { 308 | encodedVal = encodeUriSegment(val); 309 | url = url.replace(new RegExp(":" + urlParam + "(\\W)", "g"), encodedVal + "$1"); 310 | } else { 311 | url = url.replace(new RegExp("(\/?):" + urlParam + "(\\W)", "g"), function(match, 312 | leadingSlashes, tail) { 313 | if (tail.charAt(0) == '/') { 314 | return tail; 315 | } else { 316 | return leadingSlashes + tail; 317 | } 318 | }); 319 | } 320 | }); 321 | url = url.replace(/\/?#$/, ''); 322 | var query = []; 323 | forEach(params, function(value, key){ 324 | if (!self.urlParams[key]) { 325 | query.push(encodeUriQuery(key) + '=' + encodeUriQuery(value)); 326 | } 327 | }); 328 | query.sort(); 329 | url = url.replace(/\/*$/, ''); 330 | return url + (query.length ? '?' + query.join('&') : ''); 331 | } 332 | }; 333 | 334 | 335 | function ResourceFactory(url, paramDefaults, actions) { 336 | var route = new Route(url); 337 | 338 | actions = extend({}, DEFAULT_ACTIONS, actions); 339 | 340 | function extractParams(data, actionParams){ 341 | var ids = {}; 342 | actionParams = extend({}, paramDefaults, actionParams); 343 | forEach(actionParams, function(value, key){ 344 | ids[key] = value.charAt && value.charAt(0) == '@' ? getter(data, value.substr(1)) : value; 345 | }); 346 | return ids; 347 | } 348 | 349 | function Resource(value){ 350 | copy(value || {}, this); 351 | } 352 | 353 | forEach(actions, function(action, name) { 354 | action.method = angular.uppercase(action.method); 355 | var hasBody = action.method == 'POST' || action.method == 'PUT' || action.method == 'PATCH'; 356 | Resource[name] = function(a1, a2, a3, a4) { 357 | var params = {}; 358 | var data; 359 | var success = noop; 360 | var error = null; 361 | switch(arguments.length) { 362 | case 4: 363 | error = a4; 364 | success = a3; 365 | //fallthrough 366 | case 3: 367 | case 2: 368 | if (isFunction(a2)) { 369 | if (isFunction(a1)) { 370 | success = a1; 371 | error = a2; 372 | break; 373 | } 374 | 375 | success = a2; 376 | error = a3; 377 | //fallthrough 378 | } else { 379 | params = a1; 380 | data = a2; 381 | success = a3; 382 | break; 383 | } 384 | case 1: 385 | if (isFunction(a1)) success = a1; 386 | else if (hasBody) data = a1; 387 | else params = a1; 388 | break; 389 | case 0: break; 390 | default: 391 | throw "Expected between 0-4 arguments [params, data, success, error], got " + 392 | arguments.length + " arguments."; 393 | } 394 | 395 | var value = this instanceof Resource ? this : (action.isArray ? [] : new Resource(data)); 396 | $http({ 397 | method: action.method, 398 | url: route.url(extend({}, extractParams(data, action.params || {}), params)), 399 | data: data 400 | }).then(function(response) { 401 | var data = response.data; 402 | 403 | if (data) { 404 | if (action.isArray) { 405 | value.length = 0; 406 | forEach(data, function(item) { 407 | value.push(new Resource(item)); 408 | }); 409 | } else { 410 | copy(data, value); 411 | } 412 | } 413 | (success||noop)(value, response.headers); 414 | }, error); 415 | 416 | return value; 417 | }; 418 | 419 | 420 | Resource.prototype['$' + name] = function(a1, a2, a3) { 421 | var params = extractParams(this), 422 | success = noop, 423 | error; 424 | 425 | switch(arguments.length) { 426 | case 3: params = a1; success = a2; error = a3; break; 427 | case 2: 428 | case 1: 429 | if (isFunction(a1)) { 430 | success = a1; 431 | error = a2; 432 | } else { 433 | params = a1; 434 | success = a2 || noop; 435 | } 436 | case 0: break; 437 | default: 438 | throw "Expected between 1-3 arguments [params, success, error], got " + 439 | arguments.length + " arguments."; 440 | } 441 | var data = hasBody ? this : undefined; 442 | Resource[name].call(this, params, data, success, error); 443 | }; 444 | }); 445 | 446 | Resource.bind = function(additionalParamDefaults){ 447 | return ResourceFactory(url, extend({}, paramDefaults, additionalParamDefaults), actions); 448 | }; 449 | 450 | return Resource; 451 | } 452 | 453 | return ResourceFactory; 454 | }]); 455 | 456 | 457 | })(window, window.angular); -------------------------------------------------------------------------------- /public/js/vendor/angular-sanitize.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license AngularJS v1.0.6 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 ngBindHtmlWatchAction(value) { 420 | value = $sanitize(value); 421 | element.html(value || ''); 422 | }); 423 | }; 424 | }]); 425 | 426 | /** 427 | * @ngdoc filter 428 | * @name ngSanitize.filter:linky 429 | * @function 430 | * 431 | * @description 432 | * Finds links in text input and turns them into html links. Supports http/https/ftp/mailto and 433 | * plain email address links. 434 | * 435 | * @param {string} text Input text. 436 | * @returns {string} Html-linkified text. 437 | * 438 | * @usage 439 | 440 | * 441 | * @example 442 | 443 | 444 | 454 |
455 | Snippet: 456 | 457 | 458 | 459 | 460 | 461 | 462 | 463 | 464 | 467 | 470 | 471 | 472 | 473 | 474 | 475 | 476 |
FilterSourceRendered
linky filter 465 |
<div ng-bind-html="snippet | linky">
</div>
466 |
468 |
469 |
no filter
<div ng-bind="snippet">
</div>
477 | 478 | 479 | it('should linkify the snippet with urls', function() { 480 | expect(using('#linky-filter').binding('snippet | linky')). 481 | toBe('Pretty text with some links: ' + 482 | 'http://angularjs.org/, ' + 483 | 'us@somewhere.org, ' + 484 | 'another@somewhere.org, ' + 485 | 'and one more: ftp://127.0.0.1/.'); 486 | }); 487 | 488 | it ('should not linkify snippet without the linky filter', function() { 489 | expect(using('#escaped-html').binding('snippet')). 490 | toBe("Pretty text with some links:\n" + 491 | "http://angularjs.org/,\n" + 492 | "mailto:us@somewhere.org,\n" + 493 | "another@somewhere.org,\n" + 494 | "and one more: ftp://127.0.0.1/."); 495 | }); 496 | 497 | it('should update', function() { 498 | input('snippet').enter('new http://link.'); 499 | expect(using('#linky-filter').binding('snippet | linky')). 500 | toBe('new http://link.'); 501 | expect(using('#escaped-html').binding('snippet')).toBe('new http://link.'); 502 | }); 503 | 504 | 505 | */ 506 | angular.module('ngSanitize').filter('linky', function() { 507 | var LINKY_URL_REGEXP = /((ftp|https?):\/\/|(mailto:)?[A-Za-z0-9._%+-]+@)\S*[^\s\.\;\,\(\)\{\}\<\>]/, 508 | MAILTO_REGEXP = /^mailto:/; 509 | 510 | return function(text) { 511 | if (!text) return text; 512 | var match; 513 | var raw = text; 514 | var html = []; 515 | // TODO(vojta): use $sanitize instead 516 | var writer = htmlSanitizeWriter(html); 517 | var url; 518 | var i; 519 | while ((match = raw.match(LINKY_URL_REGEXP))) { 520 | // We can not end in these as they are sometimes found at the end of the sentence 521 | url = match[0]; 522 | // if we did not match ftp/http/mailto then assume mailto 523 | if (match[2] == match[3]) url = 'mailto:' + url; 524 | i = match.index; 525 | writer.chars(raw.substr(0, i)); 526 | writer.start('a', {href:url}); 527 | writer.chars(match[0].replace(MAILTO_REGEXP, '')); 528 | writer.end('a'); 529 | raw = raw.substring(i + match[0].length); 530 | } 531 | writer.chars(raw); 532 | return html.join(''); 533 | }; 534 | }); 535 | 536 | 537 | })(window, window.angular); -------------------------------------------------------------------------------- /public/js/vendor/bootstrap.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Bootstrap.js by @fat & @mdo 3 | * plugins: bootstrap-transition.js, bootstrap-modal.js, bootstrap-dropdown.js, bootstrap-scrollspy.js, bootstrap-tab.js, bootstrap-tooltip.js, bootstrap-popover.js, bootstrap-affix.js, bootstrap-alert.js, bootstrap-button.js, bootstrap-collapse.js, bootstrap-carousel.js, bootstrap-typeahead.js 4 | * Copyright 2012 Twitter, Inc. 5 | * http://www.apache.org/licenses/LICENSE-2.0.txt 6 | */ 7 | !function(a){a(function(){a.support.transition=function(){var a=function(){var a=document.createElement("bootstrap"),b={WebkitTransition:"webkitTransitionEnd",MozTransition:"transitionend",OTransition:"oTransitionEnd otransitionend",transition:"transitionend"},c;for(c in b)if(a.style[c]!==undefined)return b[c]}();return a&&{end:a}}()})}(window.jQuery),!function(a){var b=function(b,c){this.options=c,this.$element=a(b).delegate('[data-dismiss="modal"]',"click.dismiss.modal",a.proxy(this.hide,this)),this.options.remote&&this.$element.find(".modal-body").load(this.options.remote)};b.prototype={constructor:b,toggle:function(){return this[this.isShown?"hide":"show"]()},show:function(){var b=this,c=a.Event("show");this.$element.trigger(c);if(this.isShown||c.isDefaultPrevented())return;this.isShown=!0,this.escape(),this.backdrop(function(){var c=a.support.transition&&b.$element.hasClass("fade");b.$element.parent().length||b.$element.appendTo(document.body),b.$element.show(),c&&b.$element[0].offsetWidth,b.$element.addClass("in").attr("aria-hidden",!1),b.enforceFocus(),c?b.$element.one(a.support.transition.end,function(){b.$element.focus().trigger("shown")}):b.$element.focus().trigger("shown")})},hide:function(b){b&&b.preventDefault();var c=this;b=a.Event("hide"),this.$element.trigger(b);if(!this.isShown||b.isDefaultPrevented())return;this.isShown=!1,this.escape(),a(document).off("focusin.modal"),this.$element.removeClass("in").attr("aria-hidden",!0),a.support.transition&&this.$element.hasClass("fade")?this.hideWithTransition():this.hideModal()},enforceFocus:function(){var b=this;a(document).on("focusin.modal",function(a){b.$element[0]!==a.target&&!b.$element.has(a.target).length&&b.$element.focus()})},escape:function(){var a=this;this.isShown&&this.options.keyboard?this.$element.on("keyup.dismiss.modal",function(b){b.which==27&&a.hide()}):this.isShown||this.$element.off("keyup.dismiss.modal")},hideWithTransition:function(){var b=this,c=setTimeout(function(){b.$element.off(a.support.transition.end),b.hideModal()},500);this.$element.one(a.support.transition.end,function(){clearTimeout(c),b.hideModal()})},hideModal:function(){var a=this;this.$element.hide(),this.backdrop(function(){a.removeBackdrop(),a.$element.trigger("hidden")})},removeBackdrop:function(){this.$backdrop&&this.$backdrop.remove(),this.$backdrop=null},backdrop:function(b){var c=this,d=this.$element.hasClass("fade")?"fade":"";if(this.isShown&&this.options.backdrop){var e=a.support.transition&&d;this.$backdrop=a('