├── LICENSE.md ├── README.md ├── app └── poscontroller.js ├── docs └── quick-talk.pdf ├── gulpfile.js ├── models └── user.js ├── package-lock.json ├── package.json ├── public ├── .DS_Store ├── css │ ├── bootstrap.css │ ├── pos.css │ └── style.css ├── fonts │ ├── glyphicons-halflings-regular.eot │ ├── glyphicons-halflings-regular.svg │ ├── glyphicons-halflings-regular.ttf │ ├── glyphicons-halflings-regular.woff │ └── glyphicons-halflings-regular.woff2 ├── js │ └── bootstrap.js └── poscontroller.js ├── routes ├── index.js └── users.js ├── server.js └── views ├── .DS_Store ├── index.handlebars ├── layouts └── layout.handlebars ├── login.handlebars └── register.handlebars /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 David Kim 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OpenPOS Project 2 | 3 | Copyright (c) 2017 David Kim 4 | 5 | This work is available under the "MIT License". Please see the file 'LICENSE' in this distribution for license terms. 6 | 7 | ## Week 4 Update 8 | 9 | Basic framework for the POS and backend setup is complete. Routed user authentication and login to the main page, which contains the POS browser interface. I still need to complete the README.md and the database functionality which would allow each user to maintain their own POS system populated with their own saved settings. I also need to re-setup gulp to automate installation procedures. CSS also needs modifying to facilitate a better UI experience. 10 | 11 | ## Description 12 | OpenPOS is an open source, cloud-based Point-Of-Sale System. OpenPOS uses the MEAN stack, a full-stack JavaScript framework: 13 | 14 | ### [Node.js](https://nodejs.org/) 15 | 16 | Node.js is an open source, JavaScript runtime environment for executing server-side JavaScript code. The platform is built on Google Chrome's V8 JavaScript engine. It is highly scalable and developer friendly nature. In a nutshell, Node.js is the core backend platform / web framework. 17 | 18 | ### [Express.js](http://expressjs.com/) 19 | 20 | Express.js is an open source, JavaScript development framework that provides a robust set of web and mobile application features for Node.js. It provides URL routing among other various functionalities. In a nutshell, Express.js supplements the backend web framework. 21 | 22 | ### [AngularJS](https://angularjs.org/) 23 | 24 | AngularJS is an open source, JavaScript framework with the core goal of simplification. It excels at building dynamic, single page applications (SPAs) while supporting the Model View Controller (MVC) programming paradigm. In a nutshell, AngularJS takes care of the frontend framework. 25 | 26 | ### [MongoDB](https://www.mongodb.com/) 27 | 28 | MongoDB is an open source, cross-platform document-oriented NoSQL database program. It uses JSON-like documents with dynamic schemas (BSON) to persist data. MongoDB is built for scalability, high availability and performance from a single server deployment to large complex multi-site infrastructures. 29 | 30 | ### [Mongoose](http://mongoosejs.com) 31 | 32 | Mongoose provides a straight-forward, schema-based solution to model your application data. It includes built-in type casting, validation, query building, business logic hooks and more, out of the box. 33 | 34 | ### [Passport](http://passportjs.org) 35 | 36 | Passport is authentication middleware for Node.js. Extremely flexible and modular, Passport can be unobtrusively dropped in to any Express-based web application. A comprehensive set of strategies support authentication using a username and password, Facebook, Twitter, and more. 37 | 38 | ### [Gulp.js](https://gulpjs.com/) 39 | 40 | Gulp is a command line task runner utilizing the Node.js platform. It runs custom defined repetitious tasks and manages process automation. 41 | 42 | ### [Browsersync](https://www.browsersync.io/) 43 | 44 | Browsersync is an automation tool that synchronizes file changes and interactions across many devices. This allows for faster development and better application testing procedures. 45 | 46 | ### [Handlebars.js](https://www.npmjs.com/package/handlebars) 47 | 48 | Handlebars.js is an extension to the Mustache templating language created by Chris Wanstrath. Handlebars.js and Mustache are both logicless templating languages that keep the view and the code separated like we all know they should be. 49 | 50 | ## Prerequisites 51 | ### Node.js & NPM Installation 52 | 53 | [Debian and Ubuntu based Linux distributions](https://nodejs.org/en/download/package-manager/#debian-and-ubuntu-based-linux-distributions) 54 | 55 | [macOS](https://nodejs.org/en/download/package-manager/#macos) 56 | 57 | [Windows](https://nodejs.org/en/download/package-manager/#windows) 58 | 59 | 60 | ### MongoDB Installation 61 | 62 | https://docs.mongodb.com/manual/installation/ 63 | 64 | ### MongoDB Atlas Setup (Optional) 65 | 66 | [Create a free sandbox](https://www.mongodb.com/cloud/atlas) 67 | 68 | ## Quick Start 69 | 70 | Clone the repo 71 | ``` 72 | $ git clone https://github.com/kimdj/OpenPOS.git 73 | ``` 74 | 75 | Change directory to the repo 76 | ``` 77 | $ cd ./OpenPOS 78 | ``` 79 | 80 | Install dependencies 81 | ``` 82 | $ npm install 83 | ``` 84 | 85 | If you're using a local MongoDB instance, start the service: 86 | ``` 87 | $ mongod --dbpath /data/db 88 | ``` 89 | 90 | Or, if you're using MongoDB Atlas, connect to the database: 91 | ``` 92 | $ mongo "mongodb://openposcluster-shard-00-00-zb2uf.mongodb.net:27017, openposcluster-shard-00-01-zb2uf.mongodb.net:27017, openposcluster-shard-00-02-zb2uf.mongodb.net:27017/test?replicaSet=OpenPOSCluster-shard-0" --authenticationDatabase admin --ssl --username --password 93 | ``` 94 | 95 | Start the server 96 | ``` 97 | $ gulp 98 | ``` 99 | 100 | Or, start the web app 101 | ``` 102 | $ node server.js 103 | ``` 104 | 105 | ## Contribute 106 | 107 | If you'd like to contribute to this project, please refer to https://github.com/kimdj/OpenPOS/issues/. 108 | 109 | ## Credits 110 | 111 | [AngularJS POS Demo](http://embed.plnkr.co/I6XAHz/) 112 | [loginapp](https://github.com/bradtraversy/loginapp) 113 | 114 | ## Contact 115 | 116 | E-mail: kim.david.j@gmail.com 117 | 118 | 119 | ## License 120 | 121 | [The MIT License](LICENSE.md) 122 | -------------------------------------------------------------------------------- /app/poscontroller.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 David Kim 2 | // This program is licensed under the "MIT License". 3 | // Please see the file COPYING in the source 4 | // distribution of this software for license terms. 5 | 6 | var app = angular.module('OpenPOS', []); 7 | 8 | // modify how Angular interpolates {{foo}} -> {[{bar}]} to avoid clashing with Handlebars syntax 9 | app.config(function ($interpolateProvider) { 10 | $interpolateProvider.startSymbol('[['); 11 | $interpolateProvider.endSymbol(']]'); 12 | }); 13 | 14 | // main controller 15 | app.controller('PosController', function ($scope, $http) { 16 | 17 | $scope.drinks = []; 18 | $scope.foods = []; 19 | $scope.other = []; 20 | 21 | $scope.categories = [ 22 | { 23 | 'category': 'Foods' 24 | }, 25 | { 26 | 'category': 'Drinks' 27 | }, 28 | { 29 | 'category': 'Other' 30 | } 31 | ] 32 | 33 | $scope.selectedCategory = ''; 34 | $scope.order = []; 35 | $scope.new = {}; 36 | $scope.totOrders = 0; 37 | 38 | $scope.addToOrder = function (item, qty) { 39 | var flag = 0; 40 | if ($scope.order.length > 0) { 41 | for (var i = 0; i < $scope.order.length; i++) { 42 | if (item.id === $scope.order[i].id) { 43 | item.qty += qty; 44 | flag = 1; 45 | break; 46 | } 47 | } 48 | if (flag === 0) { 49 | item.qty = 1; 50 | } 51 | if (item.qty < 2) { 52 | $scope.order.push(item); 53 | } 54 | } else { 55 | item.qty = qty; 56 | $scope.order.push(item); 57 | } 58 | }; 59 | 60 | $scope.removeOneEntity = function (item) { 61 | for (var i = 0; i < $scope.order.length; i++) { 62 | if (item.id === $scope.order[i].id) { 63 | item.qty -= 1; 64 | if (item.qty === 0) { 65 | $scope.order.splice(i, 1); 66 | } 67 | } 68 | } 69 | }; 70 | 71 | $scope.removeItem = function (item) { 72 | for (var i = 0; i < $scope.order.length; i++) { 73 | if (item.id === $scope.order[i].id) { 74 | $scope.order.splice(i, 1); 75 | } 76 | } 77 | }; 78 | 79 | $scope.getTotal = function () { 80 | var tot = 0; 81 | for (var i = 0; i < $scope.order.length; i++) { 82 | tot += ($scope.order[i].price * $scope.order[i].qty) 83 | } 84 | return tot; 85 | }; 86 | 87 | $scope.clearOrder = function () { 88 | $scope.order = []; 89 | }; 90 | 91 | $scope.getDate = function () { 92 | var today = new Date(); 93 | var mm = today.getMonth() + 1; 94 | var dd = today.getDate(); 95 | var yyyy = today.getFullYear(); 96 | 97 | var date = dd + "/" + mm + "/" + yyyy 98 | 99 | return date 100 | }; 101 | 102 | $scope.checkout = function (index) { 103 | alert($scope.getDate() + " - Order Number: " + ($scope.totOrders + 1) + "\n\nOrder amount: $" + $scope.getTotal().toFixed(2) + "\n\nPayment received. Thanks."); 104 | $scope.order = []; 105 | $scope.totOrders += 1; 106 | }; 107 | 108 | var refresh = function () { 109 | $http.get('/productlist').success(function (response) { 110 | $scope.productlist = response; 111 | $scope.product = ""; 112 | 113 | angular.forEach(response, function (item, key) { 114 | if (item.category === "Foods") { 115 | $scope.foods.push({ 116 | id: item._id, 117 | name: item.name, 118 | price: item.price 119 | }); 120 | } else if (item.category === "Drinks") { 121 | $scope.drinks.push({ 122 | id: item._id, 123 | name: item.name, 124 | price: item.price 125 | }); 126 | } else { 127 | $scope.other.push({ 128 | id: item._id, 129 | name: item.name, 130 | price: item.price 131 | }); 132 | } 133 | }); 134 | }); 135 | }; 136 | }); 137 | 138 | 139 | function AppCtrl($scope, $http) { 140 | 141 | var refresh = function () { 142 | console.log("Refreshing menu"); 143 | 144 | $http.get('/productlist').success(function (response) { 145 | 146 | // clear all the buttons from the Menu Panel 147 | $scope.foods.length = 0; 148 | $scope.drinks.length = 0; 149 | $scope.other.length = 0; 150 | 151 | $scope.productlist = response; 152 | $scope.product = ""; 153 | 154 | angular.forEach(response, function (item, key) { 155 | 156 | if (item.category === "Foods") { 157 | $scope.foods.push({ 158 | id: item._id, 159 | name: item.name, 160 | price: item.price 161 | }); 162 | } else if (item.category === "Drinks") { 163 | $scope.drinks.push({ 164 | id: item._id, 165 | name: item.name, 166 | price: item.price 167 | }); 168 | } else { 169 | $scope.other.push({ 170 | id: item._id, 171 | name: item.name, 172 | price: item.price 173 | }); 174 | } 175 | }); 176 | }); 177 | }; 178 | 179 | // do an initial refresh after the user logs in 180 | refresh(); 181 | 182 | $scope.addProduct = function () { 183 | var nameStr = $scope.product.name; 184 | var priceStr = $scope.product.price; 185 | var priceRegex = /^((\d{0,3}(,\d{3})+)|\d+)(\.\d{2})?$/; // allow valid currency values only 186 | 187 | if (nameStr.length > 36) { 188 | alert("Item name can be a maximum of 36 characters long."); 189 | } else if (!priceRegex.test(priceStr)) { 190 | alert("Please enter a valid price."); 191 | } else { 192 | $scope.product.category = $scope.selectedCategory; 193 | $scope.product.user = $scope.uname; 194 | console.log($scope.product); 195 | $http.post('/productlist', $scope.product).success(function (response) { 196 | console.log('addProduct successful!'); 197 | refresh(); // refresh the Menu Panel 198 | }); 199 | } 200 | }; 201 | 202 | $scope.remove = function (id) { 203 | console.log(id); 204 | $http.delete('/productlist/' + id).success(function (response) { 205 | console.log('remove successful!'); 206 | refresh(); // refresh the Menu Panel 207 | }); 208 | }; 209 | } 210 | 211 | function TimeCtrl($scope, $timeout) { 212 | $scope.clock = "loading clock..."; 213 | $scope.tickInterval = 1000 // ms 214 | 215 | var tick = function () { 216 | $scope.clock = Date.now() // get the current time 217 | $timeout(tick, $scope.tickInterval); // reset the timer 218 | } 219 | 220 | // start the timer 221 | $timeout(tick, $scope.tickInterval); 222 | } 223 | -------------------------------------------------------------------------------- /docs/quick-talk.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kimdj/OpenPOS/f1f4d12452e5cefa7b3a96c2e3edb114edd31cf2/docs/quick-talk.pdf -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 David Kim 2 | // This program is licensed under the "MIT License". 3 | // Please see the file COPYING in the source 4 | // distribution of this software for license terms. 5 | 6 | var gulp = require('gulp'); 7 | var gutil = require('gulp-util'); 8 | var browserSync = require('browser-sync'); 9 | var reload = browserSync.reload; 10 | var nodemon = require('gulp-nodemon'); 11 | 12 | /* 13 | // start a local mongodb instance, then start the server 14 | var exec = require('child_process').exec; 15 | 16 | gulp.task('mongod', function (cb) { 17 | exec('mongod --dbpath /data/db', function (err, stdout, stderr) { 18 | console.log('Starting mongod'); 19 | console.log(stdout); 20 | console.log(stderr); 21 | cb(err); 22 | }); 23 | }) 24 | 25 | gulp.task('nodemon', ['mongod'], function (cb) { 26 | var callbackCalled = false; 27 | return nodemon({ 28 | script: './server.js' 29 | }).on('start', function () { 30 | if (!callbackCalled) { 31 | callbackCalled = true; 32 | cb(); 33 | } 34 | }); 35 | }); 36 | */ 37 | 38 | // start the server 39 | gulp.task('nodemon', function (cb) { 40 | var callbackCalled = false; 41 | return nodemon({ 42 | script: './server.js' 43 | }).on('start', function () { 44 | if (!callbackCalled) { 45 | callbackCalled = true; 46 | cb(); 47 | } 48 | }); 49 | }); 50 | 51 | // start browsersync 52 | var bs1 = browserSync.create("proxy1"); 53 | gulp.task('browser-sync', ['nodemon'], function () { 54 | bs1.init({ 55 | proxy: "http://localhost:3000", 56 | port: 3010, 57 | ui: { 58 | port: 3011 59 | }, 60 | browser: ["google chrome", "safari"], 61 | open: false 62 | }); 63 | }); 64 | 65 | // default task 66 | gulp.task('default', ['browser-sync'], function () { 67 | gulp.watch(["./views/*.handlebars"], reload); 68 | }); 69 | -------------------------------------------------------------------------------- /models/user.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 David Kim 2 | // This program is licensed under the "MIT License". 3 | // Please see the file COPYING in the source 4 | // distribution of this software for license terms. 5 | 6 | var mongoose = require('mongoose'); 7 | var bcrypt = require('bcryptjs'); 8 | 9 | // user schema 10 | var UserSchema = mongoose.Schema({ 11 | username: { 12 | type: String, 13 | index: true 14 | }, 15 | password: { 16 | type: String 17 | }, 18 | email: { 19 | type: String 20 | }, 21 | name: { 22 | type: String 23 | } 24 | }); 25 | 26 | // expose the user schema as a module 27 | var User = module.exports = mongoose.model('User', UserSchema); 28 | 29 | module.exports.createUser = function (newUser, callback) { 30 | bcrypt.genSalt(10, function (err, salt) { 31 | bcrypt.hash(newUser.password, salt, function (err, hash) { 32 | newUser.password = hash; 33 | newUser.save(callback); 34 | }); 35 | }); 36 | } 37 | 38 | module.exports.getUserByUsername = function (username, callback) { 39 | var query = { 40 | username: username 41 | }; 42 | User.findOne(query, callback); 43 | } 44 | 45 | module.exports.getUserById = function (id, callback) { 46 | User.findById(id, callback); 47 | } 48 | 49 | module.exports.comparePassword = function (candidatePassword, hash, callback) { 50 | bcrypt.compare(candidatePassword, hash, function (err, isMatch) { 51 | if (err) throw err; 52 | callback(null, isMatch); 53 | }); 54 | } 55 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "OpenPOS", 3 | "version": "1.0.0", 4 | "description": "Simple, Open Source POS System", 5 | "main": "server.js", 6 | "dependencies": { 7 | "bcryptjs": "^2.4.3", 8 | "body-parser": "^1.17.2", 9 | "connect-flash": "^0.1.1", 10 | "cookie-parser": "^1.4.3", 11 | "express": "^4.15.4", 12 | "express-handlebars": "^3.0.0", 13 | "express-messages": "^1.0.1", 14 | "express-session": "^1.15.5", 15 | "express-validator": "^3.2.1", 16 | "gulp": "^3.9.1", 17 | "gulp-autoprefixer": "^4.0.0", 18 | "gulp-cli": "^1.4.0", 19 | "gulp-concat": "^2.6.1", 20 | "gulp-minify-css": "^1.2.4", 21 | "gulp-nodemon": "^2.2.1", 22 | "gulp-plumber": "^1.1.0", 23 | "gulp-rename": "^1.2.2", 24 | "gulp-sass": "^3.1.0", 25 | "gulp-uglify": "^3.0.0", 26 | "mongodb": "^2.2.31", 27 | "mongoose": "4.10.8", 28 | "passport": "^0.3.2", 29 | "passport-http": "^0.3.0", 30 | "passport-local": "^1.0.0" 31 | }, 32 | "devDependencies": { 33 | "browser-sync": "^2.18.13", 34 | "browserify": "^14.4.0" 35 | }, 36 | "scripts": { 37 | "start": "node server.js", 38 | "test": "echo \"Error: no test specified\" && exit 1" 39 | }, 40 | "repository": { 41 | "type": "git", 42 | "url": "https://github.com/kimdj/OpenPOS.git" 43 | }, 44 | "author": "David Kim", 45 | "license": "MIT", 46 | "bugs": { 47 | "url": "https://github.com/kimdj/OpenPOS" 48 | }, 49 | "homepage": "https://github.com/kimdj/OpenPOS" 50 | } 51 | -------------------------------------------------------------------------------- /public/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kimdj/OpenPOS/f1f4d12452e5cefa7b3a96c2e3edb114edd31cf2/public/.DS_Store -------------------------------------------------------------------------------- /public/css/pos.css: -------------------------------------------------------------------------------- 1 | .text-info { 2 | border-left-style: solid; 3 | border-left-width: 5px; 4 | border-left-color: blue; 5 | padding: 5px 5px 5px 10px; 6 | font-size: 110%; 7 | } 8 | 9 | .box { 10 | border-radius: 5px; 11 | box-shadow: 1px 2px 10px silver; 12 | padding: 10px; 13 | } 14 | 15 | .buttons { 16 | padding: 20px; 17 | } 18 | 19 | .btn-marginTop { 20 | margin-top: -1px; 21 | } 22 | 23 | .container { 24 | margin: 0 auto; 25 | } 26 | 27 | .btn-pos { 28 | padding: 20px 31px; 29 | font-size: 20px; 30 | border-radius: 10px; 31 | margin-top: 1px; 32 | margin-right: 1px; 33 | margin-bottom: 1px; 34 | margin-left: 1px; 35 | width: 200px !important; 36 | height: 110px !important; 37 | text-align: center; 38 | } 39 | 40 | btn-summary { 41 | margin-right: 5px; 42 | } 43 | 44 | btn-marginTop { 45 | margin-top: 5px; 46 | } 47 | 48 | .badge-left { 49 | float: none 50 | } 51 | 52 | .list-group-item>.badge { 53 | float: none; 54 | } 55 | -------------------------------------------------------------------------------- /public/css/style.css: -------------------------------------------------------------------------------- 1 | /* Space out content a bit */ 2 | 3 | body { 4 | padding-top: 20px; 5 | padding-bottom: 20px; 6 | } 7 | 8 | 9 | /* Everything but the jumbotron gets side spacing for mobile first views */ 10 | 11 | .header, 12 | .marketing, 13 | .footer { 14 | padding-right: 15px; 15 | padding-left: 15px; 16 | } 17 | 18 | 19 | /* Custom page header */ 20 | 21 | .header { 22 | padding-bottom: 20px; 23 | border-bottom: 1px solid #e5e5e5; 24 | } 25 | 26 | 27 | /* Make the masthead heading the same height as the navigation */ 28 | 29 | .header h3 { 30 | margin-top: 0; 31 | margin-bottom: 0; 32 | line-height: 40px; 33 | } 34 | 35 | 36 | /* Custom page footer */ 37 | 38 | .footer { 39 | padding-top: 19px; 40 | color: #777; 41 | border-top: 1px solid #e5e5e5; 42 | } 43 | 44 | 45 | /* Customize container */ 46 | 47 | @media (min-width: 768px) { 48 | .container { 49 | max-width: 730px; 50 | } 51 | } 52 | 53 | .container-narrow>hr { 54 | margin: 30px 0; 55 | } 56 | 57 | 58 | /* Main marketing message and sign up button */ 59 | 60 | .jumbotron { 61 | text-align: center; 62 | border-bottom: 1px solid #e5e5e5; 63 | } 64 | 65 | .jumbotron .btn { 66 | padding: 14px 24px; 67 | font-size: 21px; 68 | } 69 | 70 | 71 | /* Supporting marketing content */ 72 | 73 | .marketing { 74 | margin: 40px 0; 75 | } 76 | 77 | .marketing p+h4 { 78 | margin-top: 28px; 79 | } 80 | 81 | 82 | /* Responsive: Portrait tablets and up */ 83 | 84 | @media screen and (min-width: 768px) { 85 | /* Remove the padding we set earlier */ 86 | .header, 87 | .marketing, 88 | .footer { 89 | padding-right: 0; 90 | padding-left: 0; 91 | } 92 | /* Space out the masthead */ 93 | .header { 94 | margin-bottom: 30px; 95 | } 96 | /* Remove the bottom border on the jumbotron for visual effect */ 97 | .jumbotron { 98 | border-bottom: 0; 99 | } 100 | } 101 | 102 | .page-header { 103 | margin-top: 0; 104 | } 105 | 106 | footer { 107 | margin-top: 40px; 108 | } 109 | 110 | /* Angular's ng-cloak */ 111 | [ng\:cloak], [ng-cloak], [data-ng-cloak], [x-ng-cloak], .ng-cloak, .x-ng-cloak { 112 | display: none !important; 113 | } -------------------------------------------------------------------------------- /public/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kimdj/OpenPOS/f1f4d12452e5cefa7b3a96c2e3edb114edd31cf2/public/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /public/fonts/glyphicons-halflings-regular.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | -------------------------------------------------------------------------------- /public/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kimdj/OpenPOS/f1f4d12452e5cefa7b3a96c2e3edb114edd31cf2/public/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /public/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kimdj/OpenPOS/f1f4d12452e5cefa7b3a96c2e3edb114edd31cf2/public/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /public/fonts/glyphicons-halflings-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kimdj/OpenPOS/f1f4d12452e5cefa7b3a96c2e3edb114edd31cf2/public/fonts/glyphicons-halflings-regular.woff2 -------------------------------------------------------------------------------- /public/js/bootstrap.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v3.3.6 (http://getbootstrap.com) 3 | * Copyright 2011-2015 Twitter, Inc. 4 | * Licensed under the MIT license 5 | */ 6 | 7 | if (typeof jQuery === 'undefined') { 8 | throw new Error('Bootstrap\'s JavaScript requires jQuery') 9 | } 10 | 11 | + 12 | function ($) { 13 | 'use strict'; 14 | var version = $.fn.jquery.split(' ')[0].split('.') 15 | if ((version[0] < 2 && version[1] < 9) || (version[0] == 1 && version[1] == 9 && version[2] < 1) || (version[0] > 2)) { 16 | throw new Error('Bootstrap\'s JavaScript requires jQuery version 1.9.1 or higher, but lower than version 3') 17 | } 18 | }(jQuery); 19 | 20 | /* ======================================================================== 21 | * Bootstrap: transition.js v3.3.6 22 | * http://getbootstrap.com/javascript/#transitions 23 | * ======================================================================== 24 | * Copyright 2011-2015 Twitter, Inc. 25 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 26 | * ======================================================================== */ 27 | 28 | 29 | + 30 | function ($) { 31 | 'use strict'; 32 | 33 | // CSS TRANSITION SUPPORT (Shoutout: http://www.modernizr.com/) 34 | // ============================================================ 35 | 36 | function transitionEnd() { 37 | var el = document.createElement('bootstrap') 38 | 39 | var transEndEventNames = { 40 | WebkitTransition: 'webkitTransitionEnd', 41 | MozTransition: 'transitionend', 42 | OTransition: 'oTransitionEnd otransitionend', 43 | transition: 'transitionend' 44 | } 45 | 46 | for (var name in transEndEventNames) { 47 | if (el.style[name] !== undefined) { 48 | return { 49 | end: transEndEventNames[name] 50 | } 51 | } 52 | } 53 | 54 | return false // explicit for ie8 ( ._.) 55 | } 56 | 57 | // http://blog.alexmaccaw.com/css-transitions 58 | $.fn.emulateTransitionEnd = function (duration) { 59 | var called = false 60 | var $el = this 61 | $(this).one('bsTransitionEnd', function () { 62 | called = true 63 | }) 64 | var callback = function () { 65 | if (!called) $($el).trigger($.support.transition.end) 66 | } 67 | setTimeout(callback, duration) 68 | return this 69 | } 70 | 71 | $(function () { 72 | $.support.transition = transitionEnd() 73 | 74 | if (!$.support.transition) return 75 | 76 | $.event.special.bsTransitionEnd = { 77 | bindType: $.support.transition.end, 78 | delegateType: $.support.transition.end, 79 | handle: function (e) { 80 | if ($(e.target).is(this)) return e.handleObj.handler.apply(this, arguments) 81 | } 82 | } 83 | }) 84 | 85 | }(jQuery); 86 | 87 | /* ======================================================================== 88 | * Bootstrap: alert.js v3.3.6 89 | * http://getbootstrap.com/javascript/#alerts 90 | * ======================================================================== 91 | * Copyright 2011-2015 Twitter, Inc. 92 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 93 | * ======================================================================== */ 94 | 95 | 96 | + 97 | function ($) { 98 | 'use strict'; 99 | 100 | // ALERT CLASS DEFINITION 101 | // ====================== 102 | 103 | var dismiss = '[data-dismiss="alert"]' 104 | var Alert = function (el) { 105 | $(el).on('click', dismiss, this.close) 106 | } 107 | 108 | Alert.VERSION = '3.3.6' 109 | 110 | Alert.TRANSITION_DURATION = 150 111 | 112 | Alert.prototype.close = function (e) { 113 | var $this = $(this) 114 | var selector = $this.attr('data-target') 115 | 116 | if (!selector) { 117 | selector = $this.attr('href') 118 | selector = selector && selector.replace(/.*(?=#[^\s]*$)/, '') // strip for ie7 119 | } 120 | 121 | var $parent = $(selector) 122 | 123 | if (e) e.preventDefault() 124 | 125 | if (!$parent.length) { 126 | $parent = $this.closest('.alert') 127 | } 128 | 129 | $parent.trigger(e = $.Event('close.bs.alert')) 130 | 131 | if (e.isDefaultPrevented()) return 132 | 133 | $parent.removeClass('in') 134 | 135 | function removeElement() { 136 | // detach from parent, fire event then clean up data 137 | $parent.detach().trigger('closed.bs.alert').remove() 138 | } 139 | 140 | $.support.transition && $parent.hasClass('fade') ? 141 | $parent 142 | .one('bsTransitionEnd', removeElement) 143 | .emulateTransitionEnd(Alert.TRANSITION_DURATION) : 144 | removeElement() 145 | } 146 | 147 | 148 | // ALERT PLUGIN DEFINITION 149 | // ======================= 150 | 151 | function Plugin(option) { 152 | return this.each(function () { 153 | var $this = $(this) 154 | var data = $this.data('bs.alert') 155 | 156 | if (!data) $this.data('bs.alert', (data = new Alert(this))) 157 | if (typeof option == 'string') data[option].call($this) 158 | }) 159 | } 160 | 161 | var old = $.fn.alert 162 | 163 | $.fn.alert = Plugin 164 | $.fn.alert.Constructor = Alert 165 | 166 | 167 | // ALERT NO CONFLICT 168 | // ================= 169 | 170 | $.fn.alert.noConflict = function () { 171 | $.fn.alert = old 172 | return this 173 | } 174 | 175 | 176 | // ALERT DATA-API 177 | // ============== 178 | 179 | $(document).on('click.bs.alert.data-api', dismiss, Alert.prototype.close) 180 | 181 | }(jQuery); 182 | 183 | /* ======================================================================== 184 | * Bootstrap: button.js v3.3.6 185 | * http://getbootstrap.com/javascript/#buttons 186 | * ======================================================================== 187 | * Copyright 2011-2015 Twitter, Inc. 188 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 189 | * ======================================================================== */ 190 | 191 | 192 | + 193 | function ($) { 194 | 'use strict'; 195 | 196 | // BUTTON PUBLIC CLASS DEFINITION 197 | // ============================== 198 | 199 | var Button = function (element, options) { 200 | this.$element = $(element) 201 | this.options = $.extend({}, Button.DEFAULTS, options) 202 | this.isLoading = false 203 | } 204 | 205 | Button.VERSION = '3.3.6' 206 | 207 | Button.DEFAULTS = { 208 | loadingText: 'loading...' 209 | } 210 | 211 | Button.prototype.setState = function (state) { 212 | var d = 'disabled' 213 | var $el = this.$element 214 | var val = $el.is('input') ? 'val' : 'html' 215 | var data = $el.data() 216 | 217 | state += 'Text' 218 | 219 | if (data.resetText == null) $el.data('resetText', $el[val]()) 220 | 221 | // push to event loop to allow forms to submit 222 | setTimeout($.proxy(function () { 223 | $el[val](data[state] == null ? this.options[state] : data[state]) 224 | 225 | if (state == 'loadingText') { 226 | this.isLoading = true 227 | $el.addClass(d).attr(d, d) 228 | } else if (this.isLoading) { 229 | this.isLoading = false 230 | $el.removeClass(d).removeAttr(d) 231 | } 232 | }, this), 0) 233 | } 234 | 235 | Button.prototype.toggle = function () { 236 | var changed = true 237 | var $parent = this.$element.closest('[data-toggle="buttons"]') 238 | 239 | if ($parent.length) { 240 | var $input = this.$element.find('input') 241 | if ($input.prop('type') == 'radio') { 242 | if ($input.prop('checked')) changed = false 243 | $parent.find('.active').removeClass('active') 244 | this.$element.addClass('active') 245 | } else if ($input.prop('type') == 'checkbox') { 246 | if (($input.prop('checked')) !== this.$element.hasClass('active')) changed = false 247 | this.$element.toggleClass('active') 248 | } 249 | $input.prop('checked', this.$element.hasClass('active')) 250 | if (changed) $input.trigger('change') 251 | } else { 252 | this.$element.attr('aria-pressed', !this.$element.hasClass('active')) 253 | this.$element.toggleClass('active') 254 | } 255 | } 256 | 257 | 258 | // BUTTON PLUGIN DEFINITION 259 | // ======================== 260 | 261 | function Plugin(option) { 262 | return this.each(function () { 263 | var $this = $(this) 264 | var data = $this.data('bs.button') 265 | var options = typeof option == 'object' && option 266 | 267 | if (!data) $this.data('bs.button', (data = new Button(this, options))) 268 | 269 | if (option == 'toggle') data.toggle() 270 | else if (option) data.setState(option) 271 | }) 272 | } 273 | 274 | var old = $.fn.button 275 | 276 | $.fn.button = Plugin 277 | $.fn.button.Constructor = Button 278 | 279 | 280 | // BUTTON NO CONFLICT 281 | // ================== 282 | 283 | $.fn.button.noConflict = function () { 284 | $.fn.button = old 285 | return this 286 | } 287 | 288 | 289 | // BUTTON DATA-API 290 | // =============== 291 | 292 | $(document) 293 | .on('click.bs.button.data-api', '[data-toggle^="button"]', function (e) { 294 | var $btn = $(e.target) 295 | if (!$btn.hasClass('btn')) $btn = $btn.closest('.btn') 296 | Plugin.call($btn, 'toggle') 297 | if (!($(e.target).is('input[type="radio"]') || $(e.target).is('input[type="checkbox"]'))) e.preventDefault() 298 | }) 299 | .on('focus.bs.button.data-api blur.bs.button.data-api', '[data-toggle^="button"]', function (e) { 300 | $(e.target).closest('.btn').toggleClass('focus', /^focus(in)?$/.test(e.type)) 301 | }) 302 | 303 | }(jQuery); 304 | 305 | /* ======================================================================== 306 | * Bootstrap: carousel.js v3.3.6 307 | * http://getbootstrap.com/javascript/#carousel 308 | * ======================================================================== 309 | * Copyright 2011-2015 Twitter, Inc. 310 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 311 | * ======================================================================== */ 312 | 313 | 314 | + 315 | function ($) { 316 | 'use strict'; 317 | 318 | // CAROUSEL CLASS DEFINITION 319 | // ========================= 320 | 321 | var Carousel = function (element, options) { 322 | this.$element = $(element) 323 | this.$indicators = this.$element.find('.carousel-indicators') 324 | this.options = options 325 | this.paused = null 326 | this.sliding = null 327 | this.interval = null 328 | this.$active = null 329 | this.$items = null 330 | 331 | this.options.keyboard && this.$element.on('keydown.bs.carousel', $.proxy(this.keydown, this)) 332 | 333 | this.options.pause == 'hover' && !('ontouchstart' in document.documentElement) && this.$element 334 | .on('mouseenter.bs.carousel', $.proxy(this.pause, this)) 335 | .on('mouseleave.bs.carousel', $.proxy(this.cycle, this)) 336 | } 337 | 338 | Carousel.VERSION = '3.3.6' 339 | 340 | Carousel.TRANSITION_DURATION = 600 341 | 342 | Carousel.DEFAULTS = { 343 | interval: 5000, 344 | pause: 'hover', 345 | wrap: true, 346 | keyboard: true 347 | } 348 | 349 | Carousel.prototype.keydown = function (e) { 350 | if (/input|textarea/i.test(e.target.tagName)) return 351 | switch (e.which) { 352 | case 37: 353 | this.prev(); 354 | break 355 | case 39: 356 | this.next(); 357 | break 358 | default: 359 | return 360 | } 361 | 362 | e.preventDefault() 363 | } 364 | 365 | Carousel.prototype.cycle = function (e) { 366 | e || (this.paused = false) 367 | 368 | this.interval && clearInterval(this.interval) 369 | 370 | this.options.interval && 371 | !this.paused && 372 | (this.interval = setInterval($.proxy(this.next, this), this.options.interval)) 373 | 374 | return this 375 | } 376 | 377 | Carousel.prototype.getItemIndex = function (item) { 378 | this.$items = item.parent().children('.item') 379 | return this.$items.index(item || this.$active) 380 | } 381 | 382 | Carousel.prototype.getItemForDirection = function (direction, active) { 383 | var activeIndex = this.getItemIndex(active) 384 | var willWrap = (direction == 'prev' && activeIndex === 0) || 385 | (direction == 'next' && activeIndex == (this.$items.length - 1)) 386 | if (willWrap && !this.options.wrap) return active 387 | var delta = direction == 'prev' ? -1 : 1 388 | var itemIndex = (activeIndex + delta) % this.$items.length 389 | return this.$items.eq(itemIndex) 390 | } 391 | 392 | Carousel.prototype.to = function (pos) { 393 | var that = this 394 | var activeIndex = this.getItemIndex(this.$active = this.$element.find('.item.active')) 395 | 396 | if (pos > (this.$items.length - 1) || pos < 0) return 397 | 398 | if (this.sliding) return this.$element.one('slid.bs.carousel', function () { 399 | that.to(pos) 400 | }) // yes, "slid" 401 | if (activeIndex == pos) return this.pause().cycle() 402 | 403 | return this.slide(pos > activeIndex ? 'next' : 'prev', this.$items.eq(pos)) 404 | } 405 | 406 | Carousel.prototype.pause = function (e) { 407 | e || (this.paused = true) 408 | 409 | if (this.$element.find('.next, .prev').length && $.support.transition) { 410 | this.$element.trigger($.support.transition.end) 411 | this.cycle(true) 412 | } 413 | 414 | this.interval = clearInterval(this.interval) 415 | 416 | return this 417 | } 418 | 419 | Carousel.prototype.next = function () { 420 | if (this.sliding) return 421 | return this.slide('next') 422 | } 423 | 424 | Carousel.prototype.prev = function () { 425 | if (this.sliding) return 426 | return this.slide('prev') 427 | } 428 | 429 | Carousel.prototype.slide = function (type, next) { 430 | var $active = this.$element.find('.item.active') 431 | var $next = next || this.getItemForDirection(type, $active) 432 | var isCycling = this.interval 433 | var direction = type == 'next' ? 'left' : 'right' 434 | var that = this 435 | 436 | if ($next.hasClass('active')) return (this.sliding = false) 437 | 438 | var relatedTarget = $next[0] 439 | var slideEvent = $.Event('slide.bs.carousel', { 440 | relatedTarget: relatedTarget, 441 | direction: direction 442 | }) 443 | this.$element.trigger(slideEvent) 444 | if (slideEvent.isDefaultPrevented()) return 445 | 446 | this.sliding = true 447 | 448 | isCycling && this.pause() 449 | 450 | if (this.$indicators.length) { 451 | this.$indicators.find('.active').removeClass('active') 452 | var $nextIndicator = $(this.$indicators.children()[this.getItemIndex($next)]) 453 | $nextIndicator && $nextIndicator.addClass('active') 454 | } 455 | 456 | var slidEvent = $.Event('slid.bs.carousel', { 457 | relatedTarget: relatedTarget, 458 | direction: direction 459 | }) // yes, "slid" 460 | if ($.support.transition && this.$element.hasClass('slide')) { 461 | $next.addClass(type) 462 | $next[0].offsetWidth // force reflow 463 | $active.addClass(direction) 464 | $next.addClass(direction) 465 | $active 466 | .one('bsTransitionEnd', function () { 467 | $next.removeClass([type, direction].join(' ')).addClass('active') 468 | $active.removeClass(['active', direction].join(' ')) 469 | that.sliding = false 470 | setTimeout(function () { 471 | that.$element.trigger(slidEvent) 472 | }, 0) 473 | }) 474 | .emulateTransitionEnd(Carousel.TRANSITION_DURATION) 475 | } else { 476 | $active.removeClass('active') 477 | $next.addClass('active') 478 | this.sliding = false 479 | this.$element.trigger(slidEvent) 480 | } 481 | 482 | isCycling && this.cycle() 483 | 484 | return this 485 | } 486 | 487 | 488 | // CAROUSEL PLUGIN DEFINITION 489 | // ========================== 490 | 491 | function Plugin(option) { 492 | return this.each(function () { 493 | var $this = $(this) 494 | var data = $this.data('bs.carousel') 495 | var options = $.extend({}, Carousel.DEFAULTS, $this.data(), typeof option == 'object' && option) 496 | var action = typeof option == 'string' ? option : options.slide 497 | 498 | if (!data) $this.data('bs.carousel', (data = new Carousel(this, options))) 499 | if (typeof option == 'number') data.to(option) 500 | else if (action) data[action]() 501 | else if (options.interval) data.pause().cycle() 502 | }) 503 | } 504 | 505 | var old = $.fn.carousel 506 | 507 | $.fn.carousel = Plugin 508 | $.fn.carousel.Constructor = Carousel 509 | 510 | 511 | // CAROUSEL NO CONFLICT 512 | // ==================== 513 | 514 | $.fn.carousel.noConflict = function () { 515 | $.fn.carousel = old 516 | return this 517 | } 518 | 519 | 520 | // CAROUSEL DATA-API 521 | // ================= 522 | 523 | var clickHandler = function (e) { 524 | var href 525 | var $this = $(this) 526 | var $target = $($this.attr('data-target') || (href = $this.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '')) // strip for ie7 527 | if (!$target.hasClass('carousel')) return 528 | var options = $.extend({}, $target.data(), $this.data()) 529 | var slideIndex = $this.attr('data-slide-to') 530 | if (slideIndex) options.interval = false 531 | 532 | Plugin.call($target, options) 533 | 534 | if (slideIndex) { 535 | $target.data('bs.carousel').to(slideIndex) 536 | } 537 | 538 | e.preventDefault() 539 | } 540 | 541 | $(document) 542 | .on('click.bs.carousel.data-api', '[data-slide]', clickHandler) 543 | .on('click.bs.carousel.data-api', '[data-slide-to]', clickHandler) 544 | 545 | $(window).on('load', function () { 546 | $('[data-ride="carousel"]').each(function () { 547 | var $carousel = $(this) 548 | Plugin.call($carousel, $carousel.data()) 549 | }) 550 | }) 551 | 552 | }(jQuery); 553 | 554 | /* ======================================================================== 555 | * Bootstrap: collapse.js v3.3.6 556 | * http://getbootstrap.com/javascript/#collapse 557 | * ======================================================================== 558 | * Copyright 2011-2015 Twitter, Inc. 559 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 560 | * ======================================================================== */ 561 | 562 | 563 | + 564 | function ($) { 565 | 'use strict'; 566 | 567 | // COLLAPSE PUBLIC CLASS DEFINITION 568 | // ================================ 569 | 570 | var Collapse = function (element, options) { 571 | this.$element = $(element) 572 | this.options = $.extend({}, Collapse.DEFAULTS, options) 573 | this.$trigger = $('[data-toggle="collapse"][href="#' + element.id + '"],' + 574 | '[data-toggle="collapse"][data-target="#' + element.id + '"]') 575 | this.transitioning = null 576 | 577 | if (this.options.parent) { 578 | this.$parent = this.getParent() 579 | } else { 580 | this.addAriaAndCollapsedClass(this.$element, this.$trigger) 581 | } 582 | 583 | if (this.options.toggle) this.toggle() 584 | } 585 | 586 | Collapse.VERSION = '3.3.6' 587 | 588 | Collapse.TRANSITION_DURATION = 350 589 | 590 | Collapse.DEFAULTS = { 591 | toggle: true 592 | } 593 | 594 | Collapse.prototype.dimension = function () { 595 | var hasWidth = this.$element.hasClass('width') 596 | return hasWidth ? 'width' : 'height' 597 | } 598 | 599 | Collapse.prototype.show = function () { 600 | if (this.transitioning || this.$element.hasClass('in')) return 601 | 602 | var activesData 603 | var actives = this.$parent && this.$parent.children('.panel').children('.in, .collapsing') 604 | 605 | if (actives && actives.length) { 606 | activesData = actives.data('bs.collapse') 607 | if (activesData && activesData.transitioning) return 608 | } 609 | 610 | var startEvent = $.Event('show.bs.collapse') 611 | this.$element.trigger(startEvent) 612 | if (startEvent.isDefaultPrevented()) return 613 | 614 | if (actives && actives.length) { 615 | Plugin.call(actives, 'hide') 616 | activesData || actives.data('bs.collapse', null) 617 | } 618 | 619 | var dimension = this.dimension() 620 | 621 | this.$element 622 | .removeClass('collapse') 623 | .addClass('collapsing')[dimension](0) 624 | .attr('aria-expanded', true) 625 | 626 | this.$trigger 627 | .removeClass('collapsed') 628 | .attr('aria-expanded', true) 629 | 630 | this.transitioning = 1 631 | 632 | var complete = function () { 633 | this.$element 634 | .removeClass('collapsing') 635 | .addClass('collapse in')[dimension]('') 636 | this.transitioning = 0 637 | this.$element 638 | .trigger('shown.bs.collapse') 639 | } 640 | 641 | if (!$.support.transition) return complete.call(this) 642 | 643 | var scrollSize = $.camelCase(['scroll', dimension].join('-')) 644 | 645 | this.$element 646 | .one('bsTransitionEnd', $.proxy(complete, this)) 647 | .emulateTransitionEnd(Collapse.TRANSITION_DURATION)[dimension](this.$element[0][scrollSize]) 648 | } 649 | 650 | Collapse.prototype.hide = function () { 651 | if (this.transitioning || !this.$element.hasClass('in')) return 652 | 653 | var startEvent = $.Event('hide.bs.collapse') 654 | this.$element.trigger(startEvent) 655 | if (startEvent.isDefaultPrevented()) return 656 | 657 | var dimension = this.dimension() 658 | 659 | this.$element[dimension](this.$element[dimension]())[0].offsetHeight 660 | 661 | this.$element 662 | .addClass('collapsing') 663 | .removeClass('collapse in') 664 | .attr('aria-expanded', false) 665 | 666 | this.$trigger 667 | .addClass('collapsed') 668 | .attr('aria-expanded', false) 669 | 670 | this.transitioning = 1 671 | 672 | var complete = function () { 673 | this.transitioning = 0 674 | this.$element 675 | .removeClass('collapsing') 676 | .addClass('collapse') 677 | .trigger('hidden.bs.collapse') 678 | } 679 | 680 | if (!$.support.transition) return complete.call(this) 681 | 682 | this.$element[dimension](0) 683 | .one('bsTransitionEnd', $.proxy(complete, this)) 684 | .emulateTransitionEnd(Collapse.TRANSITION_DURATION) 685 | } 686 | 687 | Collapse.prototype.toggle = function () { 688 | this[this.$element.hasClass('in') ? 'hide' : 'show']() 689 | } 690 | 691 | Collapse.prototype.getParent = function () { 692 | return $(this.options.parent) 693 | .find('[data-toggle="collapse"][data-parent="' + this.options.parent + '"]') 694 | .each($.proxy(function (i, element) { 695 | var $element = $(element) 696 | this.addAriaAndCollapsedClass(getTargetFromTrigger($element), $element) 697 | }, this)) 698 | .end() 699 | } 700 | 701 | Collapse.prototype.addAriaAndCollapsedClass = function ($element, $trigger) { 702 | var isOpen = $element.hasClass('in') 703 | 704 | $element.attr('aria-expanded', isOpen) 705 | $trigger 706 | .toggleClass('collapsed', !isOpen) 707 | .attr('aria-expanded', isOpen) 708 | } 709 | 710 | function getTargetFromTrigger($trigger) { 711 | var href 712 | var target = $trigger.attr('data-target') || 713 | (href = $trigger.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '') // strip for ie7 714 | 715 | return $(target) 716 | } 717 | 718 | 719 | // COLLAPSE PLUGIN DEFINITION 720 | // ========================== 721 | 722 | function Plugin(option) { 723 | return this.each(function () { 724 | var $this = $(this) 725 | var data = $this.data('bs.collapse') 726 | var options = $.extend({}, Collapse.DEFAULTS, $this.data(), typeof option == 'object' && option) 727 | 728 | if (!data && options.toggle && /show|hide/.test(option)) options.toggle = false 729 | if (!data) $this.data('bs.collapse', (data = new Collapse(this, options))) 730 | if (typeof option == 'string') data[option]() 731 | }) 732 | } 733 | 734 | var old = $.fn.collapse 735 | 736 | $.fn.collapse = Plugin 737 | $.fn.collapse.Constructor = Collapse 738 | 739 | 740 | // COLLAPSE NO CONFLICT 741 | // ==================== 742 | 743 | $.fn.collapse.noConflict = function () { 744 | $.fn.collapse = old 745 | return this 746 | } 747 | 748 | 749 | // COLLAPSE DATA-API 750 | // ================= 751 | 752 | $(document).on('click.bs.collapse.data-api', '[data-toggle="collapse"]', function (e) { 753 | var $this = $(this) 754 | 755 | if (!$this.attr('data-target')) e.preventDefault() 756 | 757 | var $target = getTargetFromTrigger($this) 758 | var data = $target.data('bs.collapse') 759 | var option = data ? 'toggle' : $this.data() 760 | 761 | Plugin.call($target, option) 762 | }) 763 | 764 | }(jQuery); 765 | 766 | /* ======================================================================== 767 | * Bootstrap: dropdown.js v3.3.6 768 | * http://getbootstrap.com/javascript/#dropdowns 769 | * ======================================================================== 770 | * Copyright 2011-2015 Twitter, Inc. 771 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 772 | * ======================================================================== */ 773 | 774 | 775 | + 776 | function ($) { 777 | 'use strict'; 778 | 779 | // DROPDOWN CLASS DEFINITION 780 | // ========================= 781 | 782 | var backdrop = '.dropdown-backdrop' 783 | var toggle = '[data-toggle="dropdown"]' 784 | var Dropdown = function (element) { 785 | $(element).on('click.bs.dropdown', this.toggle) 786 | } 787 | 788 | Dropdown.VERSION = '3.3.6' 789 | 790 | function getParent($this) { 791 | var selector = $this.attr('data-target') 792 | 793 | if (!selector) { 794 | selector = $this.attr('href') 795 | selector = selector && /#[A-Za-z]/.test(selector) && selector.replace(/.*(?=#[^\s]*$)/, '') // strip for ie7 796 | } 797 | 798 | var $parent = selector && $(selector) 799 | 800 | return $parent && $parent.length ? $parent : $this.parent() 801 | } 802 | 803 | function clearMenus(e) { 804 | if (e && e.which === 3) return 805 | $(backdrop).remove() 806 | $(toggle).each(function () { 807 | var $this = $(this) 808 | var $parent = getParent($this) 809 | var relatedTarget = { 810 | relatedTarget: this 811 | } 812 | 813 | if (!$parent.hasClass('open')) return 814 | 815 | if (e && e.type == 'click' && /input|textarea/i.test(e.target.tagName) && $.contains($parent[0], e.target)) return 816 | 817 | $parent.trigger(e = $.Event('hide.bs.dropdown', relatedTarget)) 818 | 819 | if (e.isDefaultPrevented()) return 820 | 821 | $this.attr('aria-expanded', 'false') 822 | $parent.removeClass('open').trigger($.Event('hidden.bs.dropdown', relatedTarget)) 823 | }) 824 | } 825 | 826 | Dropdown.prototype.toggle = function (e) { 827 | var $this = $(this) 828 | 829 | if ($this.is('.disabled, :disabled')) return 830 | 831 | var $parent = getParent($this) 832 | var isActive = $parent.hasClass('open') 833 | 834 | clearMenus() 835 | 836 | if (!isActive) { 837 | if ('ontouchstart' in document.documentElement && !$parent.closest('.navbar-nav').length) { 838 | // if mobile we use a backdrop because click events don't delegate 839 | $(document.createElement('div')) 840 | .addClass('dropdown-backdrop') 841 | .insertAfter($(this)) 842 | .on('click', clearMenus) 843 | } 844 | 845 | var relatedTarget = { 846 | relatedTarget: this 847 | } 848 | $parent.trigger(e = $.Event('show.bs.dropdown', relatedTarget)) 849 | 850 | if (e.isDefaultPrevented()) return 851 | 852 | $this 853 | .trigger('focus') 854 | .attr('aria-expanded', 'true') 855 | 856 | $parent 857 | .toggleClass('open') 858 | .trigger($.Event('shown.bs.dropdown', relatedTarget)) 859 | } 860 | 861 | return false 862 | } 863 | 864 | Dropdown.prototype.keydown = function (e) { 865 | if (!/(38|40|27|32)/.test(e.which) || /input|textarea/i.test(e.target.tagName)) return 866 | 867 | var $this = $(this) 868 | 869 | e.preventDefault() 870 | e.stopPropagation() 871 | 872 | if ($this.is('.disabled, :disabled')) return 873 | 874 | var $parent = getParent($this) 875 | var isActive = $parent.hasClass('open') 876 | 877 | if (!isActive && e.which != 27 || isActive && e.which == 27) { 878 | if (e.which == 27) $parent.find(toggle).trigger('focus') 879 | return $this.trigger('click') 880 | } 881 | 882 | var desc = ' li:not(.disabled):visible a' 883 | var $items = $parent.find('.dropdown-menu' + desc) 884 | 885 | if (!$items.length) return 886 | 887 | var index = $items.index(e.target) 888 | 889 | if (e.which == 38 && index > 0) index-- // up 890 | if (e.which == 40 && index < $items.length - 1) index++ // down 891 | if (!~index) index = 0 892 | 893 | $items.eq(index).trigger('focus') 894 | } 895 | 896 | 897 | // DROPDOWN PLUGIN DEFINITION 898 | // ========================== 899 | 900 | function Plugin(option) { 901 | return this.each(function () { 902 | var $this = $(this) 903 | var data = $this.data('bs.dropdown') 904 | 905 | if (!data) $this.data('bs.dropdown', (data = new Dropdown(this))) 906 | if (typeof option == 'string') data[option].call($this) 907 | }) 908 | } 909 | 910 | var old = $.fn.dropdown 911 | 912 | $.fn.dropdown = Plugin 913 | $.fn.dropdown.Constructor = Dropdown 914 | 915 | 916 | // DROPDOWN NO CONFLICT 917 | // ==================== 918 | 919 | $.fn.dropdown.noConflict = function () { 920 | $.fn.dropdown = old 921 | return this 922 | } 923 | 924 | 925 | // APPLY TO STANDARD DROPDOWN ELEMENTS 926 | // =================================== 927 | 928 | $(document) 929 | .on('click.bs.dropdown.data-api', clearMenus) 930 | .on('click.bs.dropdown.data-api', '.dropdown form', function (e) { 931 | e.stopPropagation() 932 | }) 933 | .on('click.bs.dropdown.data-api', toggle, Dropdown.prototype.toggle) 934 | .on('keydown.bs.dropdown.data-api', toggle, Dropdown.prototype.keydown) 935 | .on('keydown.bs.dropdown.data-api', '.dropdown-menu', Dropdown.prototype.keydown) 936 | 937 | }(jQuery); 938 | 939 | /* ======================================================================== 940 | * Bootstrap: modal.js v3.3.6 941 | * http://getbootstrap.com/javascript/#modals 942 | * ======================================================================== 943 | * Copyright 2011-2015 Twitter, Inc. 944 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 945 | * ======================================================================== */ 946 | 947 | 948 | + 949 | function ($) { 950 | 'use strict'; 951 | 952 | // MODAL CLASS DEFINITION 953 | // ====================== 954 | 955 | var Modal = function (element, options) { 956 | this.options = options 957 | this.$body = $(document.body) 958 | this.$element = $(element) 959 | this.$dialog = this.$element.find('.modal-dialog') 960 | this.$backdrop = null 961 | this.isShown = null 962 | this.originalBodyPad = null 963 | this.scrollbarWidth = 0 964 | this.ignoreBackdropClick = false 965 | 966 | if (this.options.remote) { 967 | this.$element 968 | .find('.modal-content') 969 | .load(this.options.remote, $.proxy(function () { 970 | this.$element.trigger('loaded.bs.modal') 971 | }, this)) 972 | } 973 | } 974 | 975 | Modal.VERSION = '3.3.6' 976 | 977 | Modal.TRANSITION_DURATION = 300 978 | Modal.BACKDROP_TRANSITION_DURATION = 150 979 | 980 | Modal.DEFAULTS = { 981 | backdrop: true, 982 | keyboard: true, 983 | show: true 984 | } 985 | 986 | Modal.prototype.toggle = function (_relatedTarget) { 987 | return this.isShown ? this.hide() : this.show(_relatedTarget) 988 | } 989 | 990 | Modal.prototype.show = function (_relatedTarget) { 991 | var that = this 992 | var e = $.Event('show.bs.modal', { 993 | relatedTarget: _relatedTarget 994 | }) 995 | 996 | this.$element.trigger(e) 997 | 998 | if (this.isShown || e.isDefaultPrevented()) return 999 | 1000 | this.isShown = true 1001 | 1002 | this.checkScrollbar() 1003 | this.setScrollbar() 1004 | this.$body.addClass('modal-open') 1005 | 1006 | this.escape() 1007 | this.resize() 1008 | 1009 | this.$element.on('click.dismiss.bs.modal', '[data-dismiss="modal"]', $.proxy(this.hide, this)) 1010 | 1011 | this.$dialog.on('mousedown.dismiss.bs.modal', function () { 1012 | that.$element.one('mouseup.dismiss.bs.modal', function (e) { 1013 | if ($(e.target).is(that.$element)) that.ignoreBackdropClick = true 1014 | }) 1015 | }) 1016 | 1017 | this.backdrop(function () { 1018 | var transition = $.support.transition && that.$element.hasClass('fade') 1019 | 1020 | if (!that.$element.parent().length) { 1021 | that.$element.appendTo(that.$body) // don't move modals dom position 1022 | } 1023 | 1024 | that.$element 1025 | .show() 1026 | .scrollTop(0) 1027 | 1028 | that.adjustDialog() 1029 | 1030 | if (transition) { 1031 | that.$element[0].offsetWidth // force reflow 1032 | } 1033 | 1034 | that.$element.addClass('in') 1035 | 1036 | that.enforceFocus() 1037 | 1038 | var e = $.Event('shown.bs.modal', { 1039 | relatedTarget: _relatedTarget 1040 | }) 1041 | 1042 | transition ? 1043 | that.$dialog // wait for modal to slide in 1044 | .one('bsTransitionEnd', function () { 1045 | that.$element.trigger('focus').trigger(e) 1046 | }) 1047 | .emulateTransitionEnd(Modal.TRANSITION_DURATION) : 1048 | that.$element.trigger('focus').trigger(e) 1049 | }) 1050 | } 1051 | 1052 | Modal.prototype.hide = function (e) { 1053 | if (e) e.preventDefault() 1054 | 1055 | e = $.Event('hide.bs.modal') 1056 | 1057 | this.$element.trigger(e) 1058 | 1059 | if (!this.isShown || e.isDefaultPrevented()) return 1060 | 1061 | this.isShown = false 1062 | 1063 | this.escape() 1064 | this.resize() 1065 | 1066 | $(document).off('focusin.bs.modal') 1067 | 1068 | this.$element 1069 | .removeClass('in') 1070 | .off('click.dismiss.bs.modal') 1071 | .off('mouseup.dismiss.bs.modal') 1072 | 1073 | this.$dialog.off('mousedown.dismiss.bs.modal') 1074 | 1075 | $.support.transition && this.$element.hasClass('fade') ? 1076 | this.$element 1077 | .one('bsTransitionEnd', $.proxy(this.hideModal, this)) 1078 | .emulateTransitionEnd(Modal.TRANSITION_DURATION) : 1079 | this.hideModal() 1080 | } 1081 | 1082 | Modal.prototype.enforceFocus = function () { 1083 | $(document) 1084 | .off('focusin.bs.modal') // guard against infinite focus loop 1085 | .on('focusin.bs.modal', $.proxy(function (e) { 1086 | if (this.$element[0] !== e.target && !this.$element.has(e.target).length) { 1087 | this.$element.trigger('focus') 1088 | } 1089 | }, this)) 1090 | } 1091 | 1092 | Modal.prototype.escape = function () { 1093 | if (this.isShown && this.options.keyboard) { 1094 | this.$element.on('keydown.dismiss.bs.modal', $.proxy(function (e) { 1095 | e.which == 27 && this.hide() 1096 | }, this)) 1097 | } else if (!this.isShown) { 1098 | this.$element.off('keydown.dismiss.bs.modal') 1099 | } 1100 | } 1101 | 1102 | Modal.prototype.resize = function () { 1103 | if (this.isShown) { 1104 | $(window).on('resize.bs.modal', $.proxy(this.handleUpdate, this)) 1105 | } else { 1106 | $(window).off('resize.bs.modal') 1107 | } 1108 | } 1109 | 1110 | Modal.prototype.hideModal = function () { 1111 | var that = this 1112 | this.$element.hide() 1113 | this.backdrop(function () { 1114 | that.$body.removeClass('modal-open') 1115 | that.resetAdjustments() 1116 | that.resetScrollbar() 1117 | that.$element.trigger('hidden.bs.modal') 1118 | }) 1119 | } 1120 | 1121 | Modal.prototype.removeBackdrop = function () { 1122 | this.$backdrop && this.$backdrop.remove() 1123 | this.$backdrop = null 1124 | } 1125 | 1126 | Modal.prototype.backdrop = function (callback) { 1127 | var that = this 1128 | var animate = this.$element.hasClass('fade') ? 'fade' : '' 1129 | 1130 | if (this.isShown && this.options.backdrop) { 1131 | var doAnimate = $.support.transition && animate 1132 | 1133 | this.$backdrop = $(document.createElement('div')) 1134 | .addClass('modal-backdrop ' + animate) 1135 | .appendTo(this.$body) 1136 | 1137 | this.$element.on('click.dismiss.bs.modal', $.proxy(function (e) { 1138 | if (this.ignoreBackdropClick) { 1139 | this.ignoreBackdropClick = false 1140 | return 1141 | } 1142 | if (e.target !== e.currentTarget) return 1143 | this.options.backdrop == 'static' ? 1144 | this.$element[0].focus() : 1145 | this.hide() 1146 | }, this)) 1147 | 1148 | if (doAnimate) this.$backdrop[0].offsetWidth // force reflow 1149 | 1150 | this.$backdrop.addClass('in') 1151 | 1152 | if (!callback) return 1153 | 1154 | doAnimate ? 1155 | this.$backdrop 1156 | .one('bsTransitionEnd', callback) 1157 | .emulateTransitionEnd(Modal.BACKDROP_TRANSITION_DURATION) : 1158 | callback() 1159 | 1160 | } else if (!this.isShown && this.$backdrop) { 1161 | this.$backdrop.removeClass('in') 1162 | 1163 | var callbackRemove = function () { 1164 | that.removeBackdrop() 1165 | callback && callback() 1166 | } 1167 | $.support.transition && this.$element.hasClass('fade') ? 1168 | this.$backdrop 1169 | .one('bsTransitionEnd', callbackRemove) 1170 | .emulateTransitionEnd(Modal.BACKDROP_TRANSITION_DURATION) : 1171 | callbackRemove() 1172 | 1173 | } else if (callback) { 1174 | callback() 1175 | } 1176 | } 1177 | 1178 | // these following methods are used to handle overflowing modals 1179 | 1180 | Modal.prototype.handleUpdate = function () { 1181 | this.adjustDialog() 1182 | } 1183 | 1184 | Modal.prototype.adjustDialog = function () { 1185 | var modalIsOverflowing = this.$element[0].scrollHeight > document.documentElement.clientHeight 1186 | 1187 | this.$element.css({ 1188 | paddingLeft: !this.bodyIsOverflowing && modalIsOverflowing ? this.scrollbarWidth : '', 1189 | paddingRight: this.bodyIsOverflowing && !modalIsOverflowing ? this.scrollbarWidth : '' 1190 | }) 1191 | } 1192 | 1193 | Modal.prototype.resetAdjustments = function () { 1194 | this.$element.css({ 1195 | paddingLeft: '', 1196 | paddingRight: '' 1197 | }) 1198 | } 1199 | 1200 | Modal.prototype.checkScrollbar = function () { 1201 | var fullWindowWidth = window.innerWidth 1202 | if (!fullWindowWidth) { // workaround for missing window.innerWidth in IE8 1203 | var documentElementRect = document.documentElement.getBoundingClientRect() 1204 | fullWindowWidth = documentElementRect.right - Math.abs(documentElementRect.left) 1205 | } 1206 | this.bodyIsOverflowing = document.body.clientWidth < fullWindowWidth 1207 | this.scrollbarWidth = this.measureScrollbar() 1208 | } 1209 | 1210 | Modal.prototype.setScrollbar = function () { 1211 | var bodyPad = parseInt((this.$body.css('padding-right') || 0), 10) 1212 | this.originalBodyPad = document.body.style.paddingRight || '' 1213 | if (this.bodyIsOverflowing) this.$body.css('padding-right', bodyPad + this.scrollbarWidth) 1214 | } 1215 | 1216 | Modal.prototype.resetScrollbar = function () { 1217 | this.$body.css('padding-right', this.originalBodyPad) 1218 | } 1219 | 1220 | Modal.prototype.measureScrollbar = function () { // thx walsh 1221 | var scrollDiv = document.createElement('div') 1222 | scrollDiv.className = 'modal-scrollbar-measure' 1223 | this.$body.append(scrollDiv) 1224 | var scrollbarWidth = scrollDiv.offsetWidth - scrollDiv.clientWidth 1225 | this.$body[0].removeChild(scrollDiv) 1226 | return scrollbarWidth 1227 | } 1228 | 1229 | 1230 | // MODAL PLUGIN DEFINITION 1231 | // ======================= 1232 | 1233 | function Plugin(option, _relatedTarget) { 1234 | return this.each(function () { 1235 | var $this = $(this) 1236 | var data = $this.data('bs.modal') 1237 | var options = $.extend({}, Modal.DEFAULTS, $this.data(), typeof option == 'object' && option) 1238 | 1239 | if (!data) $this.data('bs.modal', (data = new Modal(this, options))) 1240 | if (typeof option == 'string') data[option](_relatedTarget) 1241 | else if (options.show) data.show(_relatedTarget) 1242 | }) 1243 | } 1244 | 1245 | var old = $.fn.modal 1246 | 1247 | $.fn.modal = Plugin 1248 | $.fn.modal.Constructor = Modal 1249 | 1250 | 1251 | // MODAL NO CONFLICT 1252 | // ================= 1253 | 1254 | $.fn.modal.noConflict = function () { 1255 | $.fn.modal = old 1256 | return this 1257 | } 1258 | 1259 | 1260 | // MODAL DATA-API 1261 | // ============== 1262 | 1263 | $(document).on('click.bs.modal.data-api', '[data-toggle="modal"]', function (e) { 1264 | var $this = $(this) 1265 | var href = $this.attr('href') 1266 | var $target = $($this.attr('data-target') || (href && href.replace(/.*(?=#[^\s]+$)/, ''))) // strip for ie7 1267 | var option = $target.data('bs.modal') ? 'toggle' : $.extend({ 1268 | remote: !/#/.test(href) && href 1269 | }, $target.data(), $this.data()) 1270 | 1271 | if ($this.is('a')) e.preventDefault() 1272 | 1273 | $target.one('show.bs.modal', function (showEvent) { 1274 | if (showEvent.isDefaultPrevented()) return // only register focus restorer if modal will actually get shown 1275 | $target.one('hidden.bs.modal', function () { 1276 | $this.is(':visible') && $this.trigger('focus') 1277 | }) 1278 | }) 1279 | Plugin.call($target, option, this) 1280 | }) 1281 | 1282 | }(jQuery); 1283 | 1284 | /* ======================================================================== 1285 | * Bootstrap: tooltip.js v3.3.6 1286 | * http://getbootstrap.com/javascript/#tooltip 1287 | * Inspired by the original jQuery.tipsy by Jason Frame 1288 | * ======================================================================== 1289 | * Copyright 2011-2015 Twitter, Inc. 1290 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 1291 | * ======================================================================== */ 1292 | 1293 | 1294 | + 1295 | function ($) { 1296 | 'use strict'; 1297 | 1298 | // TOOLTIP PUBLIC CLASS DEFINITION 1299 | // =============================== 1300 | 1301 | var Tooltip = function (element, options) { 1302 | this.type = null 1303 | this.options = null 1304 | this.enabled = null 1305 | this.timeout = null 1306 | this.hoverState = null 1307 | this.$element = null 1308 | this.inState = null 1309 | 1310 | this.init('tooltip', element, options) 1311 | } 1312 | 1313 | Tooltip.VERSION = '3.3.6' 1314 | 1315 | Tooltip.TRANSITION_DURATION = 150 1316 | 1317 | Tooltip.DEFAULTS = { 1318 | animation: true, 1319 | placement: 'top', 1320 | selector: false, 1321 | template: '', 1322 | trigger: 'hover focus', 1323 | title: '', 1324 | delay: 0, 1325 | html: false, 1326 | container: false, 1327 | viewport: { 1328 | selector: 'body', 1329 | padding: 0 1330 | } 1331 | } 1332 | 1333 | Tooltip.prototype.init = function (type, element, options) { 1334 | this.enabled = true 1335 | this.type = type 1336 | this.$element = $(element) 1337 | this.options = this.getOptions(options) 1338 | this.$viewport = this.options.viewport && $($.isFunction(this.options.viewport) ? this.options.viewport.call(this, this.$element) : (this.options.viewport.selector || this.options.viewport)) 1339 | this.inState = { 1340 | click: false, 1341 | hover: false, 1342 | focus: false 1343 | } 1344 | 1345 | if (this.$element[0] instanceof document.constructor && !this.options.selector) { 1346 | throw new Error('`selector` option must be specified when initializing ' + this.type + ' on the window.document object!') 1347 | } 1348 | 1349 | var triggers = this.options.trigger.split(' ') 1350 | 1351 | for (var i = triggers.length; i--;) { 1352 | var trigger = triggers[i] 1353 | 1354 | if (trigger == 'click') { 1355 | this.$element.on('click.' + this.type, this.options.selector, $.proxy(this.toggle, this)) 1356 | } else if (trigger != 'manual') { 1357 | var eventIn = trigger == 'hover' ? 'mouseenter' : 'focusin' 1358 | var eventOut = trigger == 'hover' ? 'mouseleave' : 'focusout' 1359 | 1360 | this.$element.on(eventIn + '.' + this.type, this.options.selector, $.proxy(this.enter, this)) 1361 | this.$element.on(eventOut + '.' + this.type, this.options.selector, $.proxy(this.leave, this)) 1362 | } 1363 | } 1364 | 1365 | this.options.selector ? 1366 | (this._options = $.extend({}, this.options, { 1367 | trigger: 'manual', 1368 | selector: '' 1369 | })) : 1370 | this.fixTitle() 1371 | } 1372 | 1373 | Tooltip.prototype.getDefaults = function () { 1374 | return Tooltip.DEFAULTS 1375 | } 1376 | 1377 | Tooltip.prototype.getOptions = function (options) { 1378 | options = $.extend({}, this.getDefaults(), this.$element.data(), options) 1379 | 1380 | if (options.delay && typeof options.delay == 'number') { 1381 | options.delay = { 1382 | show: options.delay, 1383 | hide: options.delay 1384 | } 1385 | } 1386 | 1387 | return options 1388 | } 1389 | 1390 | Tooltip.prototype.getDelegateOptions = function () { 1391 | var options = {} 1392 | var defaults = this.getDefaults() 1393 | 1394 | this._options && $.each(this._options, function (key, value) { 1395 | if (defaults[key] != value) options[key] = value 1396 | }) 1397 | 1398 | return options 1399 | } 1400 | 1401 | Tooltip.prototype.enter = function (obj) { 1402 | var self = obj instanceof this.constructor ? 1403 | obj : $(obj.currentTarget).data('bs.' + this.type) 1404 | 1405 | if (!self) { 1406 | self = new this.constructor(obj.currentTarget, this.getDelegateOptions()) 1407 | $(obj.currentTarget).data('bs.' + this.type, self) 1408 | } 1409 | 1410 | if (obj instanceof $.Event) { 1411 | self.inState[obj.type == 'focusin' ? 'focus' : 'hover'] = true 1412 | } 1413 | 1414 | if (self.tip().hasClass('in') || self.hoverState == 'in') { 1415 | self.hoverState = 'in' 1416 | return 1417 | } 1418 | 1419 | clearTimeout(self.timeout) 1420 | 1421 | self.hoverState = 'in' 1422 | 1423 | if (!self.options.delay || !self.options.delay.show) return self.show() 1424 | 1425 | self.timeout = setTimeout(function () { 1426 | if (self.hoverState == 'in') self.show() 1427 | }, self.options.delay.show) 1428 | } 1429 | 1430 | Tooltip.prototype.isInStateTrue = function () { 1431 | for (var key in this.inState) { 1432 | if (this.inState[key]) return true 1433 | } 1434 | 1435 | return false 1436 | } 1437 | 1438 | Tooltip.prototype.leave = function (obj) { 1439 | var self = obj instanceof this.constructor ? 1440 | obj : $(obj.currentTarget).data('bs.' + this.type) 1441 | 1442 | if (!self) { 1443 | self = new this.constructor(obj.currentTarget, this.getDelegateOptions()) 1444 | $(obj.currentTarget).data('bs.' + this.type, self) 1445 | } 1446 | 1447 | if (obj instanceof $.Event) { 1448 | self.inState[obj.type == 'focusout' ? 'focus' : 'hover'] = false 1449 | } 1450 | 1451 | if (self.isInStateTrue()) return 1452 | 1453 | clearTimeout(self.timeout) 1454 | 1455 | self.hoverState = 'out' 1456 | 1457 | if (!self.options.delay || !self.options.delay.hide) return self.hide() 1458 | 1459 | self.timeout = setTimeout(function () { 1460 | if (self.hoverState == 'out') self.hide() 1461 | }, self.options.delay.hide) 1462 | } 1463 | 1464 | Tooltip.prototype.show = function () { 1465 | var e = $.Event('show.bs.' + this.type) 1466 | 1467 | if (this.hasContent() && this.enabled) { 1468 | this.$element.trigger(e) 1469 | 1470 | var inDom = $.contains(this.$element[0].ownerDocument.documentElement, this.$element[0]) 1471 | if (e.isDefaultPrevented() || !inDom) return 1472 | var that = this 1473 | 1474 | var $tip = this.tip() 1475 | 1476 | var tipId = this.getUID(this.type) 1477 | 1478 | this.setContent() 1479 | $tip.attr('id', tipId) 1480 | this.$element.attr('aria-describedby', tipId) 1481 | 1482 | if (this.options.animation) $tip.addClass('fade') 1483 | 1484 | var placement = typeof this.options.placement == 'function' ? 1485 | this.options.placement.call(this, $tip[0], this.$element[0]) : 1486 | this.options.placement 1487 | 1488 | var autoToken = /\s?auto?\s?/i 1489 | var autoPlace = autoToken.test(placement) 1490 | if (autoPlace) placement = placement.replace(autoToken, '') || 'top' 1491 | 1492 | $tip 1493 | .detach() 1494 | .css({ 1495 | top: 0, 1496 | left: 0, 1497 | display: 'block' 1498 | }) 1499 | .addClass(placement) 1500 | .data('bs.' + this.type, this) 1501 | 1502 | this.options.container ? $tip.appendTo(this.options.container) : $tip.insertAfter(this.$element) 1503 | this.$element.trigger('inserted.bs.' + this.type) 1504 | 1505 | var pos = this.getPosition() 1506 | var actualWidth = $tip[0].offsetWidth 1507 | var actualHeight = $tip[0].offsetHeight 1508 | 1509 | if (autoPlace) { 1510 | var orgPlacement = placement 1511 | var viewportDim = this.getPosition(this.$viewport) 1512 | 1513 | placement = placement == 'bottom' && pos.bottom + actualHeight > viewportDim.bottom ? 'top' : 1514 | placement == 'top' && pos.top - actualHeight < viewportDim.top ? 'bottom' : 1515 | placement == 'right' && pos.right + actualWidth > viewportDim.width ? 'left' : 1516 | placement == 'left' && pos.left - actualWidth < viewportDim.left ? 'right' : 1517 | placement 1518 | 1519 | $tip 1520 | .removeClass(orgPlacement) 1521 | .addClass(placement) 1522 | } 1523 | 1524 | var calculatedOffset = this.getCalculatedOffset(placement, pos, actualWidth, actualHeight) 1525 | 1526 | this.applyPlacement(calculatedOffset, placement) 1527 | 1528 | var complete = function () { 1529 | var prevHoverState = that.hoverState 1530 | that.$element.trigger('shown.bs.' + that.type) 1531 | that.hoverState = null 1532 | 1533 | if (prevHoverState == 'out') that.leave(that) 1534 | } 1535 | 1536 | $.support.transition && this.$tip.hasClass('fade') ? 1537 | $tip 1538 | .one('bsTransitionEnd', complete) 1539 | .emulateTransitionEnd(Tooltip.TRANSITION_DURATION) : 1540 | complete() 1541 | } 1542 | } 1543 | 1544 | Tooltip.prototype.applyPlacement = function (offset, placement) { 1545 | var $tip = this.tip() 1546 | var width = $tip[0].offsetWidth 1547 | var height = $tip[0].offsetHeight 1548 | 1549 | // manually read margins because getBoundingClientRect includes difference 1550 | var marginTop = parseInt($tip.css('margin-top'), 10) 1551 | var marginLeft = parseInt($tip.css('margin-left'), 10) 1552 | 1553 | // we must check for NaN for ie 8/9 1554 | if (isNaN(marginTop)) marginTop = 0 1555 | if (isNaN(marginLeft)) marginLeft = 0 1556 | 1557 | offset.top += marginTop 1558 | offset.left += marginLeft 1559 | 1560 | // $.fn.offset doesn't round pixel values 1561 | // so we use setOffset directly with our own function B-0 1562 | $.offset.setOffset($tip[0], $.extend({ 1563 | using: function (props) { 1564 | $tip.css({ 1565 | top: Math.round(props.top), 1566 | left: Math.round(props.left) 1567 | }) 1568 | } 1569 | }, offset), 0) 1570 | 1571 | $tip.addClass('in') 1572 | 1573 | // check to see if placing tip in new offset caused the tip to resize itself 1574 | var actualWidth = $tip[0].offsetWidth 1575 | var actualHeight = $tip[0].offsetHeight 1576 | 1577 | if (placement == 'top' && actualHeight != height) { 1578 | offset.top = offset.top + height - actualHeight 1579 | } 1580 | 1581 | var delta = this.getViewportAdjustedDelta(placement, offset, actualWidth, actualHeight) 1582 | 1583 | if (delta.left) offset.left += delta.left 1584 | else offset.top += delta.top 1585 | 1586 | var isVertical = /top|bottom/.test(placement) 1587 | var arrowDelta = isVertical ? delta.left * 2 - width + actualWidth : delta.top * 2 - height + actualHeight 1588 | var arrowOffsetPosition = isVertical ? 'offsetWidth' : 'offsetHeight' 1589 | 1590 | $tip.offset(offset) 1591 | this.replaceArrow(arrowDelta, $tip[0][arrowOffsetPosition], isVertical) 1592 | } 1593 | 1594 | Tooltip.prototype.replaceArrow = function (delta, dimension, isVertical) { 1595 | this.arrow() 1596 | .css(isVertical ? 'left' : 'top', 50 * (1 - delta / dimension) + '%') 1597 | .css(isVertical ? 'top' : 'left', '') 1598 | } 1599 | 1600 | Tooltip.prototype.setContent = function () { 1601 | var $tip = this.tip() 1602 | var title = this.getTitle() 1603 | 1604 | $tip.find('.tooltip-inner')[this.options.html ? 'html' : 'text'](title) 1605 | $tip.removeClass('fade in top bottom left right') 1606 | } 1607 | 1608 | Tooltip.prototype.hide = function (callback) { 1609 | var that = this 1610 | var $tip = $(this.$tip) 1611 | var e = $.Event('hide.bs.' + this.type) 1612 | 1613 | function complete() { 1614 | if (that.hoverState != 'in') $tip.detach() 1615 | that.$element 1616 | .removeAttr('aria-describedby') 1617 | .trigger('hidden.bs.' + that.type) 1618 | callback && callback() 1619 | } 1620 | 1621 | this.$element.trigger(e) 1622 | 1623 | if (e.isDefaultPrevented()) return 1624 | 1625 | $tip.removeClass('in') 1626 | 1627 | $.support.transition && $tip.hasClass('fade') ? 1628 | $tip 1629 | .one('bsTransitionEnd', complete) 1630 | .emulateTransitionEnd(Tooltip.TRANSITION_DURATION) : 1631 | complete() 1632 | 1633 | this.hoverState = null 1634 | 1635 | return this 1636 | } 1637 | 1638 | Tooltip.prototype.fixTitle = function () { 1639 | var $e = this.$element 1640 | if ($e.attr('title') || typeof $e.attr('data-original-title') != 'string') { 1641 | $e.attr('data-original-title', $e.attr('title') || '').attr('title', '') 1642 | } 1643 | } 1644 | 1645 | Tooltip.prototype.hasContent = function () { 1646 | return this.getTitle() 1647 | } 1648 | 1649 | Tooltip.prototype.getPosition = function ($element) { 1650 | $element = $element || this.$element 1651 | 1652 | var el = $element[0] 1653 | var isBody = el.tagName == 'BODY' 1654 | 1655 | var elRect = el.getBoundingClientRect() 1656 | if (elRect.width == null) { 1657 | // width and height are missing in IE8, so compute them manually; see https://github.com/twbs/bootstrap/issues/14093 1658 | elRect = $.extend({}, elRect, { 1659 | width: elRect.right - elRect.left, 1660 | height: elRect.bottom - elRect.top 1661 | }) 1662 | } 1663 | var elOffset = isBody ? { 1664 | top: 0, 1665 | left: 0 1666 | } : $element.offset() 1667 | var scroll = { 1668 | scroll: isBody ? document.documentElement.scrollTop || document.body.scrollTop : $element.scrollTop() 1669 | } 1670 | var outerDims = isBody ? { 1671 | width: $(window).width(), 1672 | height: $(window).height() 1673 | } : null 1674 | 1675 | return $.extend({}, elRect, scroll, outerDims, elOffset) 1676 | } 1677 | 1678 | Tooltip.prototype.getCalculatedOffset = function (placement, pos, actualWidth, actualHeight) { 1679 | return placement == 'bottom' ? { 1680 | top: pos.top + pos.height, 1681 | left: pos.left + pos.width / 2 - actualWidth / 2 1682 | } : 1683 | placement == 'top' ? { 1684 | top: pos.top - actualHeight, 1685 | left: pos.left + pos.width / 2 - actualWidth / 2 1686 | } : 1687 | placement == 'left' ? { 1688 | top: pos.top + pos.height / 2 - actualHeight / 2, 1689 | left: pos.left - actualWidth 1690 | } : 1691 | /* placement == 'right' */ 1692 | { 1693 | top: pos.top + pos.height / 2 - actualHeight / 2, 1694 | left: pos.left + pos.width 1695 | } 1696 | 1697 | } 1698 | 1699 | Tooltip.prototype.getViewportAdjustedDelta = function (placement, pos, actualWidth, actualHeight) { 1700 | var delta = { 1701 | top: 0, 1702 | left: 0 1703 | } 1704 | if (!this.$viewport) return delta 1705 | 1706 | var viewportPadding = this.options.viewport && this.options.viewport.padding || 0 1707 | var viewportDimensions = this.getPosition(this.$viewport) 1708 | 1709 | if (/right|left/.test(placement)) { 1710 | var topEdgeOffset = pos.top - viewportPadding - viewportDimensions.scroll 1711 | var bottomEdgeOffset = pos.top + viewportPadding - viewportDimensions.scroll + actualHeight 1712 | if (topEdgeOffset < viewportDimensions.top) { // top overflow 1713 | delta.top = viewportDimensions.top - topEdgeOffset 1714 | } else if (bottomEdgeOffset > viewportDimensions.top + viewportDimensions.height) { // bottom overflow 1715 | delta.top = viewportDimensions.top + viewportDimensions.height - bottomEdgeOffset 1716 | } 1717 | } else { 1718 | var leftEdgeOffset = pos.left - viewportPadding 1719 | var rightEdgeOffset = pos.left + viewportPadding + actualWidth 1720 | if (leftEdgeOffset < viewportDimensions.left) { // left overflow 1721 | delta.left = viewportDimensions.left - leftEdgeOffset 1722 | } else if (rightEdgeOffset > viewportDimensions.right) { // right overflow 1723 | delta.left = viewportDimensions.left + viewportDimensions.width - rightEdgeOffset 1724 | } 1725 | } 1726 | 1727 | return delta 1728 | } 1729 | 1730 | Tooltip.prototype.getTitle = function () { 1731 | var title 1732 | var $e = this.$element 1733 | var o = this.options 1734 | 1735 | title = $e.attr('data-original-title') || 1736 | (typeof o.title == 'function' ? o.title.call($e[0]) : o.title) 1737 | 1738 | return title 1739 | } 1740 | 1741 | Tooltip.prototype.getUID = function (prefix) { 1742 | do prefix += ~~(Math.random() * 1000000) 1743 | while (document.getElementById(prefix)) 1744 | return prefix 1745 | } 1746 | 1747 | Tooltip.prototype.tip = function () { 1748 | if (!this.$tip) { 1749 | this.$tip = $(this.options.template) 1750 | if (this.$tip.length != 1) { 1751 | throw new Error(this.type + ' `template` option must consist of exactly 1 top-level element!') 1752 | } 1753 | } 1754 | return this.$tip 1755 | } 1756 | 1757 | Tooltip.prototype.arrow = function () { 1758 | return (this.$arrow = this.$arrow || this.tip().find('.tooltip-arrow')) 1759 | } 1760 | 1761 | Tooltip.prototype.enable = function () { 1762 | this.enabled = true 1763 | } 1764 | 1765 | Tooltip.prototype.disable = function () { 1766 | this.enabled = false 1767 | } 1768 | 1769 | Tooltip.prototype.toggleEnabled = function () { 1770 | this.enabled = !this.enabled 1771 | } 1772 | 1773 | Tooltip.prototype.toggle = function (e) { 1774 | var self = this 1775 | if (e) { 1776 | self = $(e.currentTarget).data('bs.' + this.type) 1777 | if (!self) { 1778 | self = new this.constructor(e.currentTarget, this.getDelegateOptions()) 1779 | $(e.currentTarget).data('bs.' + this.type, self) 1780 | } 1781 | } 1782 | 1783 | if (e) { 1784 | self.inState.click = !self.inState.click 1785 | if (self.isInStateTrue()) self.enter(self) 1786 | else self.leave(self) 1787 | } else { 1788 | self.tip().hasClass('in') ? self.leave(self) : self.enter(self) 1789 | } 1790 | } 1791 | 1792 | Tooltip.prototype.destroy = function () { 1793 | var that = this 1794 | clearTimeout(this.timeout) 1795 | this.hide(function () { 1796 | that.$element.off('.' + that.type).removeData('bs.' + that.type) 1797 | if (that.$tip) { 1798 | that.$tip.detach() 1799 | } 1800 | that.$tip = null 1801 | that.$arrow = null 1802 | that.$viewport = null 1803 | }) 1804 | } 1805 | 1806 | 1807 | // TOOLTIP PLUGIN DEFINITION 1808 | // ========================= 1809 | 1810 | function Plugin(option) { 1811 | return this.each(function () { 1812 | var $this = $(this) 1813 | var data = $this.data('bs.tooltip') 1814 | var options = typeof option == 'object' && option 1815 | 1816 | if (!data && /destroy|hide/.test(option)) return 1817 | if (!data) $this.data('bs.tooltip', (data = new Tooltip(this, options))) 1818 | if (typeof option == 'string') data[option]() 1819 | }) 1820 | } 1821 | 1822 | var old = $.fn.tooltip 1823 | 1824 | $.fn.tooltip = Plugin 1825 | $.fn.tooltip.Constructor = Tooltip 1826 | 1827 | 1828 | // TOOLTIP NO CONFLICT 1829 | // =================== 1830 | 1831 | $.fn.tooltip.noConflict = function () { 1832 | $.fn.tooltip = old 1833 | return this 1834 | } 1835 | 1836 | }(jQuery); 1837 | 1838 | /* ======================================================================== 1839 | * Bootstrap: popover.js v3.3.6 1840 | * http://getbootstrap.com/javascript/#popovers 1841 | * ======================================================================== 1842 | * Copyright 2011-2015 Twitter, Inc. 1843 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 1844 | * ======================================================================== */ 1845 | 1846 | 1847 | + 1848 | function ($) { 1849 | 'use strict'; 1850 | 1851 | // POPOVER PUBLIC CLASS DEFINITION 1852 | // =============================== 1853 | 1854 | var Popover = function (element, options) { 1855 | this.init('popover', element, options) 1856 | } 1857 | 1858 | if (!$.fn.tooltip) throw new Error('Popover requires tooltip.js') 1859 | 1860 | Popover.VERSION = '3.3.6' 1861 | 1862 | Popover.DEFAULTS = $.extend({}, $.fn.tooltip.Constructor.DEFAULTS, { 1863 | placement: 'right', 1864 | trigger: 'click', 1865 | content: '', 1866 | template: '' 1867 | }) 1868 | 1869 | 1870 | // NOTE: POPOVER EXTENDS tooltip.js 1871 | // ================================ 1872 | 1873 | Popover.prototype = $.extend({}, $.fn.tooltip.Constructor.prototype) 1874 | 1875 | Popover.prototype.constructor = Popover 1876 | 1877 | Popover.prototype.getDefaults = function () { 1878 | return Popover.DEFAULTS 1879 | } 1880 | 1881 | Popover.prototype.setContent = function () { 1882 | var $tip = this.tip() 1883 | var title = this.getTitle() 1884 | var content = this.getContent() 1885 | 1886 | $tip.find('.popover-title')[this.options.html ? 'html' : 'text'](title) 1887 | $tip.find('.popover-content').children().detach().end()[ // we use append for html objects to maintain js events 1888 | this.options.html ? (typeof content == 'string' ? 'html' : 'append') : 'text' 1889 | ](content) 1890 | 1891 | $tip.removeClass('fade top bottom left right in') 1892 | 1893 | // IE8 doesn't accept hiding via the `:empty` pseudo selector, we have to do 1894 | // this manually by checking the contents. 1895 | if (!$tip.find('.popover-title').html()) $tip.find('.popover-title').hide() 1896 | } 1897 | 1898 | Popover.prototype.hasContent = function () { 1899 | return this.getTitle() || this.getContent() 1900 | } 1901 | 1902 | Popover.prototype.getContent = function () { 1903 | var $e = this.$element 1904 | var o = this.options 1905 | 1906 | return $e.attr('data-content') || 1907 | (typeof o.content == 'function' ? 1908 | o.content.call($e[0]) : 1909 | o.content) 1910 | } 1911 | 1912 | Popover.prototype.arrow = function () { 1913 | return (this.$arrow = this.$arrow || this.tip().find('.arrow')) 1914 | } 1915 | 1916 | 1917 | // POPOVER PLUGIN DEFINITION 1918 | // ========================= 1919 | 1920 | function Plugin(option) { 1921 | return this.each(function () { 1922 | var $this = $(this) 1923 | var data = $this.data('bs.popover') 1924 | var options = typeof option == 'object' && option 1925 | 1926 | if (!data && /destroy|hide/.test(option)) return 1927 | if (!data) $this.data('bs.popover', (data = new Popover(this, options))) 1928 | if (typeof option == 'string') data[option]() 1929 | }) 1930 | } 1931 | 1932 | var old = $.fn.popover 1933 | 1934 | $.fn.popover = Plugin 1935 | $.fn.popover.Constructor = Popover 1936 | 1937 | 1938 | // POPOVER NO CONFLICT 1939 | // =================== 1940 | 1941 | $.fn.popover.noConflict = function () { 1942 | $.fn.popover = old 1943 | return this 1944 | } 1945 | 1946 | }(jQuery); 1947 | 1948 | /* ======================================================================== 1949 | * Bootstrap: scrollspy.js v3.3.6 1950 | * http://getbootstrap.com/javascript/#scrollspy 1951 | * ======================================================================== 1952 | * Copyright 2011-2015 Twitter, Inc. 1953 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 1954 | * ======================================================================== */ 1955 | 1956 | 1957 | + 1958 | function ($) { 1959 | 'use strict'; 1960 | 1961 | // SCROLLSPY CLASS DEFINITION 1962 | // ========================== 1963 | 1964 | function ScrollSpy(element, options) { 1965 | this.$body = $(document.body) 1966 | this.$scrollElement = $(element).is(document.body) ? $(window) : $(element) 1967 | this.options = $.extend({}, ScrollSpy.DEFAULTS, options) 1968 | this.selector = (this.options.target || '') + ' .nav li > a' 1969 | this.offsets = [] 1970 | this.targets = [] 1971 | this.activeTarget = null 1972 | this.scrollHeight = 0 1973 | 1974 | this.$scrollElement.on('scroll.bs.scrollspy', $.proxy(this.process, this)) 1975 | this.refresh() 1976 | this.process() 1977 | } 1978 | 1979 | ScrollSpy.VERSION = '3.3.6' 1980 | 1981 | ScrollSpy.DEFAULTS = { 1982 | offset: 10 1983 | } 1984 | 1985 | ScrollSpy.prototype.getScrollHeight = function () { 1986 | return this.$scrollElement[0].scrollHeight || Math.max(this.$body[0].scrollHeight, document.documentElement.scrollHeight) 1987 | } 1988 | 1989 | ScrollSpy.prototype.refresh = function () { 1990 | var that = this 1991 | var offsetMethod = 'offset' 1992 | var offsetBase = 0 1993 | 1994 | this.offsets = [] 1995 | this.targets = [] 1996 | this.scrollHeight = this.getScrollHeight() 1997 | 1998 | if (!$.isWindow(this.$scrollElement[0])) { 1999 | offsetMethod = 'position' 2000 | offsetBase = this.$scrollElement.scrollTop() 2001 | } 2002 | 2003 | this.$body 2004 | .find(this.selector) 2005 | .map(function () { 2006 | var $el = $(this) 2007 | var href = $el.data('target') || $el.attr('href') 2008 | var $href = /^#./.test(href) && $(href) 2009 | 2010 | return ($href && 2011 | $href.length && 2012 | $href.is(':visible') && 2013 | [[$href[offsetMethod]().top + offsetBase, href]]) || null 2014 | }) 2015 | .sort(function (a, b) { 2016 | return a[0] - b[0] 2017 | }) 2018 | .each(function () { 2019 | that.offsets.push(this[0]) 2020 | that.targets.push(this[1]) 2021 | }) 2022 | } 2023 | 2024 | ScrollSpy.prototype.process = function () { 2025 | var scrollTop = this.$scrollElement.scrollTop() + this.options.offset 2026 | var scrollHeight = this.getScrollHeight() 2027 | var maxScroll = this.options.offset + scrollHeight - this.$scrollElement.height() 2028 | var offsets = this.offsets 2029 | var targets = this.targets 2030 | var activeTarget = this.activeTarget 2031 | var i 2032 | 2033 | if (this.scrollHeight != scrollHeight) { 2034 | this.refresh() 2035 | } 2036 | 2037 | if (scrollTop >= maxScroll) { 2038 | return activeTarget != (i = targets[targets.length - 1]) && this.activate(i) 2039 | } 2040 | 2041 | if (activeTarget && scrollTop < offsets[0]) { 2042 | this.activeTarget = null 2043 | return this.clear() 2044 | } 2045 | 2046 | for (i = offsets.length; i--;) { 2047 | activeTarget != targets[i] && 2048 | scrollTop >= offsets[i] && 2049 | (offsets[i + 1] === undefined || scrollTop < offsets[i + 1]) && 2050 | this.activate(targets[i]) 2051 | } 2052 | } 2053 | 2054 | ScrollSpy.prototype.activate = function (target) { 2055 | this.activeTarget = target 2056 | 2057 | this.clear() 2058 | 2059 | var selector = this.selector + 2060 | '[data-target="' + target + '"],' + 2061 | this.selector + '[href="' + target + '"]' 2062 | 2063 | var active = $(selector) 2064 | .parents('li') 2065 | .addClass('active') 2066 | 2067 | if (active.parent('.dropdown-menu').length) { 2068 | active = active 2069 | .closest('li.dropdown') 2070 | .addClass('active') 2071 | } 2072 | 2073 | active.trigger('activate.bs.scrollspy') 2074 | } 2075 | 2076 | ScrollSpy.prototype.clear = function () { 2077 | $(this.selector) 2078 | .parentsUntil(this.options.target, '.active') 2079 | .removeClass('active') 2080 | } 2081 | 2082 | 2083 | // SCROLLSPY PLUGIN DEFINITION 2084 | // =========================== 2085 | 2086 | function Plugin(option) { 2087 | return this.each(function () { 2088 | var $this = $(this) 2089 | var data = $this.data('bs.scrollspy') 2090 | var options = typeof option == 'object' && option 2091 | 2092 | if (!data) $this.data('bs.scrollspy', (data = new ScrollSpy(this, options))) 2093 | if (typeof option == 'string') data[option]() 2094 | }) 2095 | } 2096 | 2097 | var old = $.fn.scrollspy 2098 | 2099 | $.fn.scrollspy = Plugin 2100 | $.fn.scrollspy.Constructor = ScrollSpy 2101 | 2102 | 2103 | // SCROLLSPY NO CONFLICT 2104 | // ===================== 2105 | 2106 | $.fn.scrollspy.noConflict = function () { 2107 | $.fn.scrollspy = old 2108 | return this 2109 | } 2110 | 2111 | 2112 | // SCROLLSPY DATA-API 2113 | // ================== 2114 | 2115 | $(window).on('load.bs.scrollspy.data-api', function () { 2116 | $('[data-spy="scroll"]').each(function () { 2117 | var $spy = $(this) 2118 | Plugin.call($spy, $spy.data()) 2119 | }) 2120 | }) 2121 | 2122 | }(jQuery); 2123 | 2124 | /* ======================================================================== 2125 | * Bootstrap: tab.js v3.3.6 2126 | * http://getbootstrap.com/javascript/#tabs 2127 | * ======================================================================== 2128 | * Copyright 2011-2015 Twitter, Inc. 2129 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 2130 | * ======================================================================== */ 2131 | 2132 | 2133 | + 2134 | function ($) { 2135 | 'use strict'; 2136 | 2137 | // TAB CLASS DEFINITION 2138 | // ==================== 2139 | 2140 | var Tab = function (element) { 2141 | // jscs:disable requireDollarBeforejQueryAssignment 2142 | this.element = $(element) 2143 | // jscs:enable requireDollarBeforejQueryAssignment 2144 | } 2145 | 2146 | Tab.VERSION = '3.3.6' 2147 | 2148 | Tab.TRANSITION_DURATION = 150 2149 | 2150 | Tab.prototype.show = function () { 2151 | var $this = this.element 2152 | var $ul = $this.closest('ul:not(.dropdown-menu)') 2153 | var selector = $this.data('target') 2154 | 2155 | if (!selector) { 2156 | selector = $this.attr('href') 2157 | selector = selector && selector.replace(/.*(?=#[^\s]*$)/, '') // strip for ie7 2158 | } 2159 | 2160 | if ($this.parent('li').hasClass('active')) return 2161 | 2162 | var $previous = $ul.find('.active:last a') 2163 | var hideEvent = $.Event('hide.bs.tab', { 2164 | relatedTarget: $this[0] 2165 | }) 2166 | var showEvent = $.Event('show.bs.tab', { 2167 | relatedTarget: $previous[0] 2168 | }) 2169 | 2170 | $previous.trigger(hideEvent) 2171 | $this.trigger(showEvent) 2172 | 2173 | if (showEvent.isDefaultPrevented() || hideEvent.isDefaultPrevented()) return 2174 | 2175 | var $target = $(selector) 2176 | 2177 | this.activate($this.closest('li'), $ul) 2178 | this.activate($target, $target.parent(), function () { 2179 | $previous.trigger({ 2180 | type: 'hidden.bs.tab', 2181 | relatedTarget: $this[0] 2182 | }) 2183 | $this.trigger({ 2184 | type: 'shown.bs.tab', 2185 | relatedTarget: $previous[0] 2186 | }) 2187 | }) 2188 | } 2189 | 2190 | Tab.prototype.activate = function (element, container, callback) { 2191 | var $active = container.find('> .active') 2192 | var transition = callback && 2193 | $.support.transition && 2194 | ($active.length && $active.hasClass('fade') || !!container.find('> .fade').length) 2195 | 2196 | function next() { 2197 | $active 2198 | .removeClass('active') 2199 | .find('> .dropdown-menu > .active') 2200 | .removeClass('active') 2201 | .end() 2202 | .find('[data-toggle="tab"]') 2203 | .attr('aria-expanded', false) 2204 | 2205 | element 2206 | .addClass('active') 2207 | .find('[data-toggle="tab"]') 2208 | .attr('aria-expanded', true) 2209 | 2210 | if (transition) { 2211 | element[0].offsetWidth // reflow for transition 2212 | element.addClass('in') 2213 | } else { 2214 | element.removeClass('fade') 2215 | } 2216 | 2217 | if (element.parent('.dropdown-menu').length) { 2218 | element 2219 | .closest('li.dropdown') 2220 | .addClass('active') 2221 | .end() 2222 | .find('[data-toggle="tab"]') 2223 | .attr('aria-expanded', true) 2224 | } 2225 | 2226 | callback && callback() 2227 | } 2228 | 2229 | $active.length && transition ? 2230 | $active 2231 | .one('bsTransitionEnd', next) 2232 | .emulateTransitionEnd(Tab.TRANSITION_DURATION) : 2233 | next() 2234 | 2235 | $active.removeClass('in') 2236 | } 2237 | 2238 | 2239 | // TAB PLUGIN DEFINITION 2240 | // ===================== 2241 | 2242 | function Plugin(option) { 2243 | return this.each(function () { 2244 | var $this = $(this) 2245 | var data = $this.data('bs.tab') 2246 | 2247 | if (!data) $this.data('bs.tab', (data = new Tab(this))) 2248 | if (typeof option == 'string') data[option]() 2249 | }) 2250 | } 2251 | 2252 | var old = $.fn.tab 2253 | 2254 | $.fn.tab = Plugin 2255 | $.fn.tab.Constructor = Tab 2256 | 2257 | 2258 | // TAB NO CONFLICT 2259 | // =============== 2260 | 2261 | $.fn.tab.noConflict = function () { 2262 | $.fn.tab = old 2263 | return this 2264 | } 2265 | 2266 | 2267 | // TAB DATA-API 2268 | // ============ 2269 | 2270 | var clickHandler = function (e) { 2271 | e.preventDefault() 2272 | Plugin.call($(this), 'show') 2273 | } 2274 | 2275 | $(document) 2276 | .on('click.bs.tab.data-api', '[data-toggle="tab"]', clickHandler) 2277 | .on('click.bs.tab.data-api', '[data-toggle="pill"]', clickHandler) 2278 | 2279 | }(jQuery); 2280 | 2281 | /* ======================================================================== 2282 | * Bootstrap: affix.js v3.3.6 2283 | * http://getbootstrap.com/javascript/#affix 2284 | * ======================================================================== 2285 | * Copyright 2011-2015 Twitter, Inc. 2286 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 2287 | * ======================================================================== */ 2288 | 2289 | 2290 | + 2291 | function ($) { 2292 | 'use strict'; 2293 | 2294 | // AFFIX CLASS DEFINITION 2295 | // ====================== 2296 | 2297 | var Affix = function (element, options) { 2298 | this.options = $.extend({}, Affix.DEFAULTS, options) 2299 | 2300 | this.$target = $(this.options.target) 2301 | .on('scroll.bs.affix.data-api', $.proxy(this.checkPosition, this)) 2302 | .on('click.bs.affix.data-api', $.proxy(this.checkPositionWithEventLoop, this)) 2303 | 2304 | this.$element = $(element) 2305 | this.affixed = null 2306 | this.unpin = null 2307 | this.pinnedOffset = null 2308 | 2309 | this.checkPosition() 2310 | } 2311 | 2312 | Affix.VERSION = '3.3.6' 2313 | 2314 | Affix.RESET = 'affix affix-top affix-bottom' 2315 | 2316 | Affix.DEFAULTS = { 2317 | offset: 0, 2318 | target: window 2319 | } 2320 | 2321 | Affix.prototype.getState = function (scrollHeight, height, offsetTop, offsetBottom) { 2322 | var scrollTop = this.$target.scrollTop() 2323 | var position = this.$element.offset() 2324 | var targetHeight = this.$target.height() 2325 | 2326 | if (offsetTop != null && this.affixed == 'top') return scrollTop < offsetTop ? 'top' : false 2327 | 2328 | if (this.affixed == 'bottom') { 2329 | if (offsetTop != null) return (scrollTop + this.unpin <= position.top) ? false : 'bottom' 2330 | return (scrollTop + targetHeight <= scrollHeight - offsetBottom) ? false : 'bottom' 2331 | } 2332 | 2333 | var initializing = this.affixed == null 2334 | var colliderTop = initializing ? scrollTop : position.top 2335 | var colliderHeight = initializing ? targetHeight : height 2336 | 2337 | if (offsetTop != null && scrollTop <= offsetTop) return 'top' 2338 | if (offsetBottom != null && (colliderTop + colliderHeight >= scrollHeight - offsetBottom)) return 'bottom' 2339 | 2340 | return false 2341 | } 2342 | 2343 | Affix.prototype.getPinnedOffset = function () { 2344 | if (this.pinnedOffset) return this.pinnedOffset 2345 | this.$element.removeClass(Affix.RESET).addClass('affix') 2346 | var scrollTop = this.$target.scrollTop() 2347 | var position = this.$element.offset() 2348 | return (this.pinnedOffset = position.top - scrollTop) 2349 | } 2350 | 2351 | Affix.prototype.checkPositionWithEventLoop = function () { 2352 | setTimeout($.proxy(this.checkPosition, this), 1) 2353 | } 2354 | 2355 | Affix.prototype.checkPosition = function () { 2356 | if (!this.$element.is(':visible')) return 2357 | 2358 | var height = this.$element.height() 2359 | var offset = this.options.offset 2360 | var offsetTop = offset.top 2361 | var offsetBottom = offset.bottom 2362 | var scrollHeight = Math.max($(document).height(), $(document.body).height()) 2363 | 2364 | if (typeof offset != 'object') offsetBottom = offsetTop = offset 2365 | if (typeof offsetTop == 'function') offsetTop = offset.top(this.$element) 2366 | if (typeof offsetBottom == 'function') offsetBottom = offset.bottom(this.$element) 2367 | 2368 | var affix = this.getState(scrollHeight, height, offsetTop, offsetBottom) 2369 | 2370 | if (this.affixed != affix) { 2371 | if (this.unpin != null) this.$element.css('top', '') 2372 | 2373 | var affixType = 'affix' + (affix ? '-' + affix : '') 2374 | var e = $.Event(affixType + '.bs.affix') 2375 | 2376 | this.$element.trigger(e) 2377 | 2378 | if (e.isDefaultPrevented()) return 2379 | 2380 | this.affixed = affix 2381 | this.unpin = affix == 'bottom' ? this.getPinnedOffset() : null 2382 | 2383 | this.$element 2384 | .removeClass(Affix.RESET) 2385 | .addClass(affixType) 2386 | .trigger(affixType.replace('affix', 'affixed') + '.bs.affix') 2387 | } 2388 | 2389 | if (affix == 'bottom') { 2390 | this.$element.offset({ 2391 | top: scrollHeight - height - offsetBottom 2392 | }) 2393 | } 2394 | } 2395 | 2396 | 2397 | // AFFIX PLUGIN DEFINITION 2398 | // ======================= 2399 | 2400 | function Plugin(option) { 2401 | return this.each(function () { 2402 | var $this = $(this) 2403 | var data = $this.data('bs.affix') 2404 | var options = typeof option == 'object' && option 2405 | 2406 | if (!data) $this.data('bs.affix', (data = new Affix(this, options))) 2407 | if (typeof option == 'string') data[option]() 2408 | }) 2409 | } 2410 | 2411 | var old = $.fn.affix 2412 | 2413 | $.fn.affix = Plugin 2414 | $.fn.affix.Constructor = Affix 2415 | 2416 | 2417 | // AFFIX NO CONFLICT 2418 | // ================= 2419 | 2420 | $.fn.affix.noConflict = function () { 2421 | $.fn.affix = old 2422 | return this 2423 | } 2424 | 2425 | 2426 | // AFFIX DATA-API 2427 | // ============== 2428 | 2429 | $(window).on('load', function () { 2430 | $('[data-spy="affix"]').each(function () { 2431 | var $spy = $(this) 2432 | var data = $spy.data() 2433 | 2434 | data.offset = data.offset || {} 2435 | 2436 | if (data.offsetBottom != null) data.offset.bottom = data.offsetBottom 2437 | if (data.offsetTop != null) data.offset.top = data.offsetTop 2438 | 2439 | Plugin.call($spy, data) 2440 | }) 2441 | }) 2442 | 2443 | }(jQuery); 2444 | -------------------------------------------------------------------------------- /public/poscontroller.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 David Kim 2 | // This program is licensed under the "MIT License". 3 | // Please see the file COPYING in the source 4 | // distribution of this software for license terms. 5 | 6 | var app = angular.module('OpenPOS', []); 7 | 8 | // change Angular's {{foo}} -> {[{bar}]} to avoid clashing with Handlebars syntax 9 | app.config(function ($interpolateProvider) { 10 | $interpolateProvider.startSymbol('[['); 11 | $interpolateProvider.endSymbol(']]'); 12 | }); 13 | 14 | // main controller 15 | app.controller('PosController', function ($scope, $http) { 16 | // console.log("Hello world from PosController/poscontroller.js"); 17 | 18 | $scope.drinks = []; 19 | $scope.foods = []; 20 | $scope.other = []; 21 | 22 | $scope.categories = [ 23 | { 24 | 'category': 'Foods' 25 | }, 26 | { 27 | 'category': 'Drinks' 28 | }, 29 | { 30 | 'category': 'Other' 31 | } 32 | ] 33 | $scope.selectedCategory = ''; 34 | $scope.order = []; 35 | $scope.new = {}; 36 | $scope.totOrders = 0; 37 | 38 | $scope.addToOrder = function (item, qty) { 39 | var flag = 0; 40 | if ($scope.order.length > 0) { 41 | for (var i = 0; i < $scope.order.length; i++) { 42 | if (item.id === $scope.order[i].id) { 43 | item.qty += qty; 44 | flag = 1; 45 | break; 46 | } 47 | } 48 | if (flag === 0) { 49 | item.qty = 1; 50 | } 51 | if (item.qty < 2) { 52 | $scope.order.push(item); 53 | } 54 | } else { 55 | item.qty = qty; 56 | $scope.order.push(item); 57 | } 58 | }; 59 | 60 | $scope.removeOneEntity = function (item) { 61 | for (var i = 0; i < $scope.order.length; i++) { 62 | if (item.id === $scope.order[i].id) { 63 | item.qty -= 1; 64 | if (item.qty === 0) { 65 | $scope.order.splice(i, 1); 66 | } 67 | } 68 | } 69 | }; 70 | 71 | $scope.removeItem = function (item) { 72 | for (var i = 0; i < $scope.order.length; i++) { 73 | if (item.id === $scope.order[i].id) { 74 | $scope.order.splice(i, 1); 75 | } 76 | } 77 | }; 78 | 79 | $scope.getTotal = function () { 80 | var tot = 0; 81 | for (var i = 0; i < $scope.order.length; i++) { 82 | tot += ($scope.order[i].price * $scope.order[i].qty) 83 | } 84 | return tot; 85 | }; 86 | 87 | $scope.clearOrder = function () { 88 | $scope.order = []; 89 | }; 90 | 91 | $scope.getDate = function () { 92 | var today = new Date(); 93 | var mm = today.getMonth() + 1; 94 | var dd = today.getDate(); 95 | var yyyy = today.getFullYear(); 96 | 97 | var date = dd + "/" + mm + "/" + yyyy 98 | 99 | return date 100 | }; 101 | 102 | $scope.checkout = function (index) { 103 | alert($scope.getDate() + " - Order Number: " + ($scope.totOrders + 1) + "\n\nOrder amount: $" + $scope.getTotal().toFixed(2) + "\n\nPayment received. Thanks."); 104 | $scope.order = []; 105 | $scope.totOrders += 1; 106 | }; 107 | 108 | var refresh = function () { 109 | $http.get('/productlist').success(function (response) { 110 | $scope.productlist = response; 111 | $scope.product = ""; 112 | // console.log("RESPONSE: " + response); 113 | 114 | angular.forEach(response, function (item, key) { 115 | // console.log("pushing --> " + item.name); 116 | if (item.category === "Foods") { 117 | $scope.foods.push({ 118 | id: item._id, 119 | name: item.name, 120 | price: item.price 121 | }); 122 | } else if (item.category === "Drinks") { 123 | $scope.drinks.push({ 124 | id: item._id, 125 | name: item.name, 126 | price: item.price 127 | }); 128 | } else { 129 | $scope.other.push({ 130 | id: item._id, 131 | name: item.name, 132 | price: item.price 133 | }); 134 | } 135 | }); 136 | }); 137 | }; 138 | }); 139 | 140 | 141 | function AppCtrl($scope, $http) { 142 | // console.log("Hello world from AppCtrl/poscontroller.js"); 143 | 144 | var refresh = function () { 145 | $http.get('/productlist').success(function (response) { 146 | $scope.foods.length = 0; // clear all the buttons from the Menu Panel 147 | $scope.drinks.length = 0; // clear all the buttons from the Menu Panel 148 | $scope.other.length = 0; // clear all the buttons from the Menu Panel 149 | 150 | $scope.productlist = response; 151 | $scope.product = ""; 152 | // console.log("RESPONSE: " + response); 153 | 154 | angular.forEach(response, function (item, key) { 155 | // console.log("adding menu item --> " + item.name); 156 | 157 | if (item.category === "Foods") { 158 | $scope.foods.push({ 159 | id: item._id, 160 | name: item.name, 161 | price: item.price 162 | }); 163 | } else if (item.category === "Drinks") { 164 | $scope.drinks.push({ 165 | id: item._id, 166 | name: item.name, 167 | price: item.price 168 | }); 169 | } else { 170 | $scope.other.push({ 171 | id: item._id, 172 | name: item.name, 173 | price: item.price 174 | }); 175 | } 176 | }); 177 | }); 178 | }; 179 | 180 | refresh(); 181 | 182 | $scope.addProduct = function () { 183 | var nameStr = $scope.product.name; 184 | var priceStr = $scope.product.price; 185 | var priceRegex = /^((\d{0,3}(,\d{3})+)|\d+)(\.\d{2})?$/; // valid currency values only 186 | 187 | if (nameStr.length > 36) { 188 | alert("Item name can be a maximum of 36 characters long."); 189 | } else if (!priceRegex.test(priceStr)) { 190 | alert("Please enter a valid price."); 191 | } else { 192 | // alert("Adding item: " + nameStr); 193 | $scope.product.category = $scope.selectedCategory; 194 | $scope.product.user = $scope.uname; 195 | console.log($scope.product); 196 | $http.post('/productlist', $scope.product).success(function (response) { 197 | // console.log("addProduct: " + $scope.product); 198 | refresh(); // refresh the Menu Panel 199 | }); 200 | } 201 | }; 202 | 203 | $scope.remove = function (id) { 204 | console.log(id); 205 | $http.delete('/productlist/' + id).success(function (response) { 206 | // console.log("remove: " + response); 207 | refresh(); // refresh the Menu Panel 208 | }); 209 | }; 210 | } 211 | 212 | function TimeCtrl($scope, $timeout) { 213 | $scope.clock = "loading clock..."; // initialize the time variable 214 | $scope.tickInterval = 1000 //ms 215 | 216 | var tick = function () { 217 | $scope.clock = Date.now() // get the current time 218 | $timeout(tick, $scope.tickInterval); // reset the timer 219 | } 220 | 221 | // start the timer 222 | $timeout(tick, $scope.tickInterval); 223 | } 224 | -------------------------------------------------------------------------------- /routes/index.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 David Kim 2 | // This program is licensed under the "MIT License". 3 | // Please see the file COPYING in the source 4 | // distribution of this software for license terms. 5 | 6 | var express = require('express'); 7 | var app = express(); 8 | var router = express.Router(); 9 | 10 | // get homepage 11 | router.get('/', ensureAuthenticated, function (req, res) { 12 | res.render('index', { 13 | username: req.user.username 14 | }); 15 | }); 16 | 17 | function ensureAuthenticated(req, res, next) { 18 | if (req.isAuthenticated()) { 19 | return next(); 20 | } else { 21 | res.redirect('/users/login'); 22 | } 23 | } 24 | 25 | module.exports = router; 26 | -------------------------------------------------------------------------------- /routes/users.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 David Kim 2 | // This program is licensed under the "MIT License". 3 | // Please see the file COPYING in the source 4 | // distribution of this software for license terms. 5 | 6 | var express = require('express'); 7 | var router = express.Router(); 8 | var passport = require('passport'); 9 | var LocalStrategy = require('passport-local').Strategy; 10 | 11 | var User = require('../models/user'); 12 | 13 | // register 14 | router.get('/register', function (req, res) { 15 | res.render('register'); 16 | }); 17 | 18 | // login 19 | router.get('/login', function (req, res) { 20 | res.render('login'); 21 | }); 22 | 23 | // register user 24 | router.post('/register', function (req, res) { 25 | var name = req.body.name; 26 | var email = req.body.email; 27 | var username = req.body.username; 28 | var password = req.body.password; 29 | var password2 = req.body.password2; 30 | 31 | // validation 32 | req.checkBody('name', 'Name is required').notEmpty(); 33 | req.checkBody('email', 'Email is required').notEmpty(); 34 | req.checkBody('email', 'Email is not valid').isEmail(); 35 | req.checkBody('username', 'Username is required').notEmpty(); 36 | req.checkBody('password', 'Password is required').notEmpty(); 37 | req.checkBody('password2', 'Passwords do not match').equals(req.body.password); 38 | 39 | var errors = req.validationErrors(); 40 | 41 | if (errors) { 42 | res.render('register', { 43 | errors: errors 44 | }); 45 | } else { 46 | var newUser = new User({ 47 | name: name, 48 | email: email, 49 | username: username, 50 | password: password 51 | }); 52 | 53 | User.createUser(newUser, function (err, user) { 54 | if (err) throw err; 55 | console.log(user); 56 | }); 57 | 58 | req.flash('success_msg', 'You are registered and can now login'); 59 | 60 | res.redirect('/users/login'); 61 | } 62 | }); 63 | 64 | passport.use(new LocalStrategy( 65 | function (username, password, done) { 66 | User.getUserByUsername(username, function (err, user) { 67 | if (err) throw err; 68 | if (!user) { 69 | return done(null, false, { 70 | message: 'Unknown User' 71 | }); 72 | } 73 | 74 | User.comparePassword(password, user.password, function (err, isMatch) { 75 | if (err) throw err; 76 | if (isMatch) { 77 | return done(null, user); 78 | } 79 | else { 80 | return done(null, false, { 81 | message: 'Invalid password' 82 | }); 83 | } 84 | }); 85 | }); 86 | })); 87 | 88 | passport.serializeUser(function (user, done) { 89 | done(null, user.id); 90 | }); 91 | 92 | passport.deserializeUser(function (id, done) { 93 | User.getUserById(id, function (err, user) { 94 | done(err, user); 95 | }); 96 | }); 97 | 98 | router.post('/login', 99 | passport.authenticate('local', { 100 | successRedirect: '/', 101 | failureRedirect: '/users/login', 102 | failureFlash: true 103 | }), 104 | function (req, res) { 105 | res.redirect('/'); 106 | }); 107 | 108 | router.get('/logout', function (req, res) { 109 | req.logout(); 110 | 111 | req.flash('success_msg', 'You are logged out'); 112 | 113 | res.redirect('/users/login'); 114 | }); 115 | 116 | module.exports = router; 117 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 David Kim 2 | // This program is licensed under the "MIT License". 3 | // Please see the file COPYING in the source 4 | // distribution of this software for license terms. 5 | 6 | var express = require('express'), 7 | app = express(), 8 | path = require('path'), 9 | cookieParser = require('cookie-parser'), 10 | bodyParser = require('body-parser'), 11 | exphbs = require('express-handlebars'), 12 | expressValidator = require('express-validator'), 13 | flash = require('connect-flash'), 14 | session = require('express-session'), 15 | passport = require('passport'), 16 | LocalStrategy = require('passport-local').Strategy, 17 | routes = require('./routes/index'), 18 | users = require('./routes/users'), 19 | mongo = require('mongodb'), 20 | mongoose = require('mongoose'), 21 | db = mongoose.connection; 22 | 23 | 24 | // init express 25 | var app = express(); 26 | 27 | // connect to a local database 28 | //mongoose.connect('mongodb://localhost/loginapp'); 29 | 30 | // or, connect to MongoDB's Atlas (a cloud-hosted MongoDB service) 31 | var username = "djk3" 32 | var password = "Da72vid87!" 33 | var uri = "mongodb://" + username + ":" + password + "@openposcluster-shard-00-00-zb2uf.mongodb.net:27017,openposcluster-shard-00-01-zb2uf.mongodb.net:27017,openposcluster-shard-00-02-zb2uf.mongodb.net:27017/OpenPOS?ssl=true&replicaSet=OpenPOSCluster-shard-0&authSource=admin"; 34 | mongoose.connect(uri, { 35 | useMongoClient: true 36 | }); 37 | var db1 = mongoose.connection; 38 | db1.on('error', console.error.bind(console, 'connection error:')); 39 | db1.once('open', function () { 40 | console.log("Connected to MongoDB Atlas"); 41 | }); 42 | 43 | // define a mongoose schema 44 | var productSchema = mongoose.Schema({ 45 | name: String, 46 | price: String, 47 | category: String, 48 | user: String 49 | }); 50 | 51 | // define a mongoose model 52 | var Products = mongoose.model('products', productSchema, 'products'); 53 | 54 | // setup view engine 55 | app.set('views', path.join(__dirname, 'views')); 56 | app.engine('handlebars', exphbs({ 57 | defaultLayout: 'layout' 58 | })); 59 | app.set('view engine', 'handlebars'); 60 | 61 | // setup bodyParser middleware 62 | app.use(bodyParser.json()); 63 | app.use(bodyParser.urlencoded({ 64 | // extended: false 65 | extended: true 66 | })); 67 | app.use(cookieParser()); 68 | 69 | // setup an express session 70 | app.use(session({ 71 | secret: 'secret', 72 | saveUninitialized: true, 73 | resave: true 74 | })); 75 | 76 | // init passport 77 | app.use(passport.initialize()); 78 | app.use(passport.session()); 79 | 80 | // setup express validator 81 | app.use(expressValidator({ 82 | errorFormatter: function (param, msg, value) { 83 | var namespace = param.split('.'), 84 | root = namespace.shift(), 85 | formParam = root; 86 | 87 | while (namespace.length) { 88 | formParam += '[' + namespace.shift() + ']'; 89 | } 90 | return { 91 | param: formParam, 92 | msg: msg, 93 | value: value 94 | }; 95 | } 96 | })); 97 | 98 | // connect flash 99 | app.use(flash()); 100 | 101 | // global vars 102 | app.use(function (req, res, next) { 103 | res.locals.success_msg = req.flash('success_msg'); 104 | res.locals.error_msg = req.flash('error_msg'); 105 | res.locals.error = req.flash('error'); 106 | res.locals.user = req.user || null; 107 | next(); 108 | }); 109 | 110 | // -------------------- END - Express routing for product CRUD operations -------------------- 111 | 112 | // username variable 113 | var uname = ""; 114 | 115 | function isEmpty(str) { 116 | return (!str || 0 === str.length); 117 | } 118 | 119 | // find products from db 120 | app.get('/productlist', function (req, res) { 121 | 122 | // make sure uname is set to the login username 123 | if (typeof req["user"].username === 'undefined') { 124 | uname = req["user"].user; 125 | } 126 | 127 | if (isEmpty(uname)) { 128 | uname = req["user"].username; 129 | } else if (!isEmpty(req['user'].username) && (uname !== req['user'].username)) { 130 | uname = req['user'].username; 131 | } 132 | 133 | // find all products (documents) belonging to the user 134 | console.log(uname, "just logged into OpenPOS"); 135 | Products.find({ 136 | user: uname 137 | }, function (err, docs) { 138 | console.log("Finding all docs for", uname + ":\n", docs); 139 | res.json(docs); 140 | }); 141 | }); 142 | 143 | // add product from db 144 | app.post('/productlist', function (req, res) { 145 | console.log("Item requested to be added to the db: ", req.body); 146 | 147 | // create a new product using the data sent from the client 148 | var p = new Products(req.body); 149 | 150 | // save the new product to the db 151 | p.save(function (err, doc) { 152 | if (err) { 153 | return console.error(err) 154 | } else { 155 | console.log("Saving p to db: ", doc); 156 | res.json(doc); 157 | } 158 | }); 159 | }); 160 | 161 | // delete product from db 162 | app.delete('/productlist/:id', function (req, res) { 163 | 164 | // get the product id 165 | var id = req.params.id; 166 | console.log("Removing item - id: " + id); 167 | 168 | // remove the product from the db 169 | Products.remove({ 170 | _id: id 171 | }, function (err, doc) { 172 | res.json(doc); 173 | }); 174 | }); 175 | 176 | // -------------------- START - Express routing for product CRUD operations -------------------- 177 | 178 | // setup static routes 179 | app.use(express.static(__dirname + '/public')); 180 | 181 | // setup routes 182 | app.use('/', routes); 183 | app.use('/users', users); 184 | 185 | // set the port 186 | app.set('port', (process.env.PORT || 3000)); 187 | 188 | // start the server 189 | app.listen(app.get('port'), function () { 190 | console.log('Server running on port ' + app.get('port')); 191 | }); 192 | -------------------------------------------------------------------------------- /views/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kimdj/OpenPOS/f1f4d12452e5cefa7b3a96c2e3edb114edd31cf2/views/.DS_Store -------------------------------------------------------------------------------- /views/index.handlebars: -------------------------------------------------------------------------------- 1 |
2 |
3 | 11 |
12 |
13 | 14 | 15 | 16 |
17 | 18 |
19 |
20 |
21 |
22 |
23 | 29 |
30 |
31 |
32 | 33 |
34 |
35 | 36 |
37 |
38 | 39 |
40 |
41 |

Add A New Item

42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 |
NamePriceCategoryAction
[[product.name]][[product.price]][[product.category]]
67 |
68 |
69 |
70 | 73 |
74 |
75 | 76 |
77 |
78 |
79 |
80 |
Order Summary
81 |
Total Orders: [[totOrders]]
82 |
83 |
84 |
85 |
86 | Ready To Take Orders! 87 |
88 |
    89 |
  • 90 |
    91 |
    92 | 93 |
    94 |
    95 | [[item.name]] 96 |
    97 |
    98 |
    [[item.price * item.qty | currency:"$":0]]
    99 |
    100 |
    101 |
    [[item.price | currency:"$":0]]
    102 |
    103 |
    104 | 107 | 110 |
    111 |
    112 | 115 |
    116 |
    117 |
  • 118 |
119 |
120 | 123 | 129 |
130 |
131 |
132 |
133 | -------------------------------------------------------------------------------- /views/layouts/layout.handlebars: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | OpenPOS Demo 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 |
40 |
41 | 56 |
57 | 58 | 59 |
60 |
61 | {{#if success_msg}} 62 |
{{success_msg}}
63 | {{/if}} {{#if error_msg}} 64 |
{{error_msg}}
65 | {{/if}} {{#if error}} 66 |
{{error}}
67 | {{/if}} 68 |
69 |
70 |
71 | 72 | 73 | 74 | {{{body}}} 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 96 | -------------------------------------------------------------------------------- /views/login.handlebars: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 |
6 | 7 | 8 |
9 |
10 | 11 | 12 |
13 | 14 |
15 |
16 |
17 | -------------------------------------------------------------------------------- /views/register.handlebars: -------------------------------------------------------------------------------- 1 | 7 | 8 |
9 | 10 | {{#if errors}} {{#each errors}} 11 |
{{msg}}
12 | {{/each}} {{/if}} 13 |
14 |
15 | 16 | 17 |
18 |
19 | 20 | 21 |
22 |
23 | 24 | 25 |
26 |
27 | 28 | 29 |
30 |
31 | 32 | 33 |
34 | 35 |
36 |
37 | --------------------------------------------------------------------------------