├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── deploy.sh ├── expense.js ├── icons ├── android-icon-144x144.png ├── android-icon-1x.png ├── android-icon-2x.png ├── android-icon-4x.png ├── favicon-16x16.png └── favicon-32x32.png ├── index.html ├── init.js ├── manifest.json ├── register-serviceworker.js ├── style.css ├── sw.js ├── transfer.js ├── utils.js └── vendor └── mdl ├── LICENSE ├── bower.json ├── material.css ├── material.js ├── material.min.css ├── material.min.css.map ├── material.min.js ├── material.min.js.map └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # taken from https://github.com/steveklabnik/automatically_update_github_pages_with_travis_example 2 | language: node_js 3 | install: true 4 | script: true 5 | after_success: 6 | - test $TRAVIS_PULL_REQUEST == "false" && test $TRAVIS_BRANCH == "master" && bash deploy.sh 7 | env: 8 | global: 9 | - secure: "rgaMiu+KbosqEIjD2RU1B/rCmIEsfS6LAoCvNbWzj6yB/6hw4gOZo+zkxvhdyI9tZN1+xZxxtkIJKcfObb+HlV6/2byAR6OEw+TDfatWei/re4oQoE1G1AX3YpgE+2SDPz4uV1/CFWwcfzU3+cQIXKz3kAQelzyyxJALzYuyLYkEAWrODfmSH+Nv7MvNF/j1L6RgSPm4f7gOG18SUYnV/Q16u1pkya2QDL1cvyewXCRlv9hpD3YWpmC+Is7QTuvgRafzRK8yoibRm3lbXGoMD4wFdAikaGNKUK3SKHg6hr14pBUmy26y42QDxcaVbyLSXYnEULld48qkXkvLPQqeKUxNHMEsO4FWzCOOy78/Au1h0c0etrWHYsqOVwTBgYMoD2rmDN6UKyZ5QZrs3aZa7jfKrdIytlsB5m8w3N6xUcqKWqz2cSvepTrtouk14gDa0l0z9QzUI0nIIFxMOA3p4v6iscVPm9cOBfoAObCeX1e4ZoV0/xDU4NWf1H0q32BSvUQXOEYVmk9CKvGQUImGTcPF0lj8RZ4yCz98Rsgu64DbJaJo5W8f8gmbZ8l2Sy+8IHcndzjZdXg3KNZVHFpUTPLVX188HW7YzzNgygK+QyTegDTf/n4NHxTJoSUcJy38t0u+hIt2su6JT/1hi15oPRSYozTs8SYmc9nhuAbVFNg=" 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Mitul Shah 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. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 💸 expense-manager [![Build Status](https://travis-ci.org/mitul45/expense-manager.svg?branch=master)](https://travis-ci.org/mitul45/expense-manager) 2 | 3 | _"If you can't measure it, you can't improve it."_ - [Peter Drucker](https://en.wikipedia.org/wiki/Peter_Drucker) 4 | 5 | Take control back. Introducing Expense Manager, an app to track your daily spendings. It is made of two main components: 6 | 1. [Main application](https://mitul45.github.io/expense-manager/): Used to add expenses to the sheet 7 | 2. [Expense Sheet](https://docs.google.com/spreadsheets/d/1NfF1A0UC6qLuOE7eiTsAzNVAskNcYeuPHAkzSURH0Pc/edit#gid=0): This is where you can do all kinds of analysis/summarization of your expenses. 8 | 9 | Why? Because Google Sheets is really good with numbers, but entering data from the mobile app is [not very convenient](http://i.imgur.com/NfaGKEI.gifv). The idea is to make adding expense [as simple as it can be](http://i.imgur.com/tg6UzFe.gifv). You should add them at the same moment you make a transaction. Make it like a habit. 10 | 11 | Detailed analysis of the sheet can be deferred till you get an access to a computer. You can plot fancy charts at end of the month, set the budget for next week, etc. And I feel all of that need not necessarily be done on small screen. 12 | 13 | ## Features 14 | - Built for the web - works cross-platform (iOS, Android, Mac, Windows, Linux). 15 | - Uses [Google Sheet](https://docs.google.com/spreadsheets/d/1NfF1A0UC6qLuOE7eiTsAzNVAskNcYeuPHAkzSURH0Pc/edit?usp=sharing) as a database to store expenses. **Why?** 16 | 1. Privacy. It's your personal data. It should belong to you. 17 | 1. Sheets is [way better](https://www.google.co.in/search?q=cool+things+you+can+do+with+excel&oq=cool+things+your+can+do+with+ex&aqs=chrome.1.69i57j0l5.10138j0j4&sourceid=chrome&ie=UTF-8#q=cool+things+you+can+do+with+google+sheets) at handling numbers than me. You can do all kinds of analysis using graphs, formulas, etc. 18 | 1. I didn't want to write backend :nerd_face: 19 | - [`Progressive Web App`](https://developers.google.com/web/progressive-web-apps/) - Quick to load, can be installed as a standalone app on phone. 20 | - Easier sharing. Sharing expenses with someone (wife, family)? [Share](https://support.google.com/docs/answer/2494822?co=GENIE.Platform%3DDesktop&hl=en) the expense sheet and all of your combined data belongs to the single sheet. 21 | - Backup. Didn't I tell you it uses Google Sheets to store expenses? Your data is always backed up on :partly_sunny: 22 | - [Mobile friendly](http://i.imgur.com/vqz7zDA.png) layout. 23 | - [NEW] Supports internal amount transfer enteries (things like withdrawing cash, investing to an retirement account, etc) 24 | 25 | ## How to get started 26 | 1. Copy this [sheet](https://docs.google.com/spreadsheets/d/1NfF1A0UC6qLuOE7eiTsAzNVAskNcYeuPHAkzSURH0Pc/edit?usp=sharing) to your Google Drive. After sign in, choose `File -> Make a Copy...`. 27 | ![Make a Copy](http://i.imgur.com/qpLUsmY.png) 28 | 1. Don't rename it. It should be named `Expense Sheet`. 29 | ![Expense Sheet](http://i.imgur.com/ncOBzsa.png) 30 | 1. Update categories, account names, initial values in [Data sheet](https://docs.google.com/spreadsheets/d/1NfF1A0UC6qLuOE7eiTsAzNVAskNcYeuPHAkzSURH0Pc/edit#gid=1956004401). Clear our sample expenses in the first sheet. 31 | 1. That's it! You can start adding expenses now. 32 | 33 | ### Permissions 34 | - Read access to Google Drive to find `Expense Sheet`. 35 | - Read and write access to Google Sheets to add expenses. 36 | 37 | ## Check it out 38 | https://mitul45.github.io/expense-manager/ :rocket: 39 | -------------------------------------------------------------------------------- /deploy.sh: -------------------------------------------------------------------------------- 1 | # taken from https://github.com/steveklabnik/automatically_update_github_pages_with_travis_example 2 | 3 | #!/bin/bash 4 | 5 | set -o errexit -o nounset 6 | 7 | if [ "$TRAVIS_BRANCH" != "master" ] 8 | then 9 | echo "This commit was made against the $TRAVIS_BRANCH and not the master! No deploy!" 10 | exit 0 11 | fi 12 | 13 | rev=$(git rev-parse --short HEAD) 14 | 15 | git init 16 | git config user.name "Mitul Shah" 17 | git config user.email "45.mitul@gmail.com" 18 | 19 | git remote add upstream "https://$GH_TOKEN@github.com/mitul45/expense-manager.git" 20 | git fetch upstream 21 | git reset upstream/gh-pages 22 | 23 | touch . 24 | 25 | git add -A . 26 | git commit -m "rebuild pages at ${rev}" 27 | git push -q upstream HEAD:gh-pages 28 | -------------------------------------------------------------------------------- /expense.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | const utils = window.expenseManager.utils; 3 | 4 | // Cached DOM bindings 5 | const byID = document.getElementById.bind(document); 6 | const expenseForm = byID("expense-form"); 7 | const descriptionEl = byID("expense-description"); 8 | const dateEl = byID("expense-date"); 9 | const accountEl = byID("expense-account"); 10 | const categoryEl = byID("expense-category"); 11 | const amountEl = byID("expense-amount"); 12 | const isIncomeEl = byID("is-income"); 13 | const addExpenseBtn = byID("add-expesne"); 14 | const snackbarContainer = byID("toast-container"); 15 | 16 | /** 17 | * Append expense to the expense sheet 18 | */ 19 | function addExpense(event) { 20 | if (!expenseForm.checkValidity()) return false; 21 | 22 | event.preventDefault(); 23 | utils.showLoader(); 24 | 25 | const expenseDate = dateEl.value; 26 | const descriptionVal = descriptionEl.value; 27 | const accountVal = accountEl.value; 28 | const categoryVal = categoryEl.value; 29 | const amountVal = amountEl.value; 30 | const isIncome = isIncomeEl.checked; 31 | 32 | const dateObj = { 33 | yyyy: expenseDate.substr(0, 4), 34 | mm: expenseDate.substr(5, 2), 35 | dd: expenseDate.substr(-2) 36 | }; 37 | gapi.client.sheets.spreadsheets.values 38 | .append( 39 | utils.appendRequestObj([ 40 | [ 41 | `=DATE(${dateObj.yyyy}, ${dateObj.mm}, ${dateObj.dd})`, 42 | descriptionVal, 43 | accountVal, 44 | categoryVal, 45 | isIncome ? 0 : amountVal, // income amount 46 | isIncome ? amountVal : 0, // expense amount 47 | false // is internal transfer? 48 | ] 49 | ]) 50 | ) 51 | .then( 52 | response => { 53 | // reset fileds 54 | descriptionEl.value = ""; 55 | amountEl.value = ""; 56 | snackbarContainer.MaterialSnackbar.showSnackbar({ 57 | message: "Expense added!" 58 | }); 59 | utils.hideLoader(); 60 | }, 61 | response => { 62 | utils.hideLoader(); 63 | let message = "Sorry, something went wrong"; 64 | if (response.status === 403) { 65 | message = "Please copy the sheet in your drive"; 66 | } 67 | console.log(response); 68 | snackbarContainer.MaterialSnackbar.showSnackbar({ 69 | message, 70 | actionHandler: () => { 71 | window.open( 72 | "https://github.com/mitul45/expense-manager/blob/master/README.md#how-to-get-started", 73 | "_blank" 74 | ); 75 | }, 76 | actionText: "Details", 77 | timeout: 5 * 60 * 1000 78 | }); 79 | } 80 | ); 81 | } 82 | 83 | function init(sheetID, accounts, categories) { 84 | // set date picker's defalt value as today 85 | dateEl.value = new Date().toISOString().substr(0, 10); 86 | 87 | // initialize accounts and categories dropdown 88 | accountEl.innerHTML = accounts.sort().map(utils.wrapInOption).join(); 89 | categoryEl.innerHTML = categories.sort().map(utils.wrapInOption).join(); 90 | 91 | // set lister for `Save` button 92 | addExpenseBtn.onclick = addExpense.bind(null); 93 | } 94 | 95 | window.expenseManager.expenseForm = { 96 | init 97 | }; 98 | })(); 99 | -------------------------------------------------------------------------------- /icons/android-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitul45/expense-manager/5487ab546a9e0589c86948cd3324bda3d8c942e4/icons/android-icon-144x144.png -------------------------------------------------------------------------------- /icons/android-icon-1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitul45/expense-manager/5487ab546a9e0589c86948cd3324bda3d8c942e4/icons/android-icon-1x.png -------------------------------------------------------------------------------- /icons/android-icon-2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitul45/expense-manager/5487ab546a9e0589c86948cd3324bda3d8c942e4/icons/android-icon-2x.png -------------------------------------------------------------------------------- /icons/android-icon-4x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitul45/expense-manager/5487ab546a9e0589c86948cd3324bda3d8c942e4/icons/android-icon-4x.png -------------------------------------------------------------------------------- /icons/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitul45/expense-manager/5487ab546a9e0589c86948cd3324bda3d8c942e4/icons/favicon-16x16.png -------------------------------------------------------------------------------- /icons/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitul45/expense-manager/5487ab546a9e0589c86948cd3324bda3d8c942e4/icons/favicon-32x32.png -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Expense Manager 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 |
20 |
21 | 22 | 23 | Expense Manager 24 | 25 |
26 | 35 |
36 |
37 |
38 |
39 |
40 | 41 |
42 | 43 |
44 |
45 | Expense 46 | Transfer 47 |
48 | 49 |
50 |
51 |
52 | 53 | 54 |
55 |
56 | 57 |
58 | 62 | 66 |
67 | 68 | 69 |
70 | 74 |
75 |
76 | 79 |
80 |
81 |
82 |
83 |

info What is this?

84 | Record a transaction which is not expense/income. e.g. paying for your credit card bills (saving account -> credit card), withdrawing cash (checkings account -> cash), or making an investment. 85 | 86 |
87 | 88 | 89 |
90 |
91 | 92 | 93 |
94 | 99 | 104 |
105 | 106 | 107 |
108 |
109 |
110 | 114 |
115 |
116 |
117 | 118 | 121 |
122 |
123 |
124 | 125 |
126 |
127 |
128 |
129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 140 | 141 | 142 | 153 | 154 | 155 | 156 | -------------------------------------------------------------------------------- /init.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | const utils = window.expenseManager.utils; 3 | 4 | // Cached DOM bindings 5 | const byID = document.getElementById.bind(document); 6 | const authorizeButton = byID("authorize-button"); 7 | const signoutButton = byID("signout-button"); 8 | const forms = byID("forms"); 9 | const formLoader = byID("form-loader"); 10 | const snackbarContainer = byID("toast-container"); 11 | 12 | utils.hideLoader = utils.hideLoader.bind(null, forms, formLoader); 13 | utils.showLoader = utils.showLoader.bind(null, forms, formLoader); 14 | 15 | /** 16 | * On load, called to load the auth2 library and API client library. 17 | */ 18 | function handleClientLoad() { 19 | gapi.load("client:auth2", initClient); 20 | } 21 | 22 | /** 23 | * Sign in the user upon button click. 24 | */ 25 | function handleAuthClick(event) { 26 | gapi.auth2.getAuthInstance().signIn(); 27 | } 28 | 29 | /** 30 | * Sign out the user upon button click. 31 | */ 32 | function handleSignoutClick(event) { 33 | gapi.auth2.getAuthInstance().signOut(); 34 | } 35 | 36 | /** 37 | * Initializes the API client library and sets up sign-in state 38 | * listeners. 39 | */ 40 | function initClient() { 41 | const CLIENT_ID = 42 | "840179112792-bhg3k1h0dcnp9ltelj21o6vibphjcufe.apps.googleusercontent.com"; 43 | const DISCOVERY_DOCS = [ 44 | "https://sheets.googleapis.com/$discovery/rest?version=v4", 45 | "https://www.googleapis.com/discovery/v1/apis/drive/v3/rest" 46 | ]; 47 | 48 | // Write access for spreadsheet to add expenses, readonly access for drive to find sheet ID 49 | const SCOPES = 50 | "https://www.googleapis.com/auth/spreadsheets https://www.googleapis.com/auth/drive.metadata.readonly"; 51 | 52 | gapi.client 53 | .init({ 54 | discoveryDocs: DISCOVERY_DOCS, 55 | clientId: CLIENT_ID, 56 | scope: SCOPES 57 | }) 58 | .then(() => { 59 | // Listen for sign-in state changes. 60 | gapi.auth2.getAuthInstance().isSignedIn.listen(updateSigninStatus); 61 | 62 | // Handle the initial sign-in state. 63 | updateSigninStatus(gapi.auth2.getAuthInstance().isSignedIn.get()); 64 | authorizeButton.onclick = handleAuthClick.bind(null); 65 | signoutButton.onclick = handleSignoutClick.bind(null); 66 | }); 67 | } 68 | 69 | /** 70 | * Called when the signed in status changes, to update the UI 71 | * appropriately. After a sign-in, find expense sheet id. 72 | */ 73 | function updateSigninStatus(isSignedIn) { 74 | if (isSignedIn) { 75 | onSignin(); 76 | } else { 77 | utils.showEl(authorizeButton); 78 | utils.hideEl(signoutButton); 79 | utils.hideEl(forms); 80 | utils.hideEl(formLoader); 81 | } 82 | } 83 | 84 | /** 85 | * On successful signin - Update authorization buttons, make a call to get sheetID 86 | */ 87 | function onSignin() { 88 | utils.hideEl(authorizeButton); 89 | utils.showEl(signoutButton); 90 | 91 | getSheetID("Expense Sheet") 92 | .then(getCategoriesAndAccount, sheetNotFound) 93 | .then(initApp); 94 | 95 | function sheetNotFound() { 96 | snackbarContainer.MaterialSnackbar.showSnackbar({ 97 | message: "Cannot find the sheet!", 98 | actionHandler: () => { 99 | window.open( 100 | "https://github.com/mitul45/expense-manager/blob/master/README.md#how-to-get-started", 101 | "_blank" 102 | ); 103 | }, 104 | actionText: "Details", 105 | timeout: 5 * 60 * 1000 106 | }); 107 | } 108 | } 109 | 110 | /** 111 | * Get sheet ID for a given sheet name 112 | * 113 | * @param {String} sheetName Sheet name to search in user's drive 114 | * @returns {Promise} a promise resolves successfully with sheetID if it's available in user's drive 115 | */ 116 | function getSheetID(sheetName) { 117 | return new Promise((resolve, reject) => { 118 | gapi.client.drive.files 119 | .list({ 120 | q: `name='${sheetName}' and mimeType='application/vnd.google-apps.spreadsheet'`, 121 | orderBy: "starred" 122 | }) 123 | .then(response => { 124 | if (response.result.files.length === 0) reject(); 125 | else resolve(response.result.files[0].id); 126 | }); 127 | }); 128 | } 129 | 130 | /** 131 | * Fetch all accounts, and categories info from spreadsheet 132 | * 133 | * @param {String} sheetID Expense sheetID 134 | */ 135 | function getCategoriesAndAccount(sheetID) { 136 | return new Promise((resolve, reject) => { 137 | const ACCOUNT_RANGE = "Data!A2:A50"; 138 | const CATEGORY_RANGE = "Data!E2:E50"; 139 | 140 | gapi.client.sheets.spreadsheets.values 141 | .batchGet( 142 | utils.batchGetRequestObj(sheetID, [ACCOUNT_RANGE, CATEGORY_RANGE]) 143 | ) 144 | .then(response => { 145 | const accounts = response.result.valueRanges[0].values[0]; 146 | const categories = response.result.valueRanges[1].values[0]; 147 | resolve({ sheetID, accounts, categories }); 148 | }); 149 | }); 150 | } 151 | 152 | function initApp(data) { 153 | utils.hideLoader(); 154 | 155 | window.expenseManager.expenseForm.init( 156 | data.sheetID, 157 | data.accounts, 158 | data.categories 159 | ); 160 | window.expenseManager.transferForm.init(data.sheetID, data.accounts); 161 | 162 | utils.appendRequestObj = utils.appendRequestObj.bind(null, data.sheetID); 163 | } 164 | 165 | window.handleClientLoad = handleClientLoad.bind(null); 166 | })(); 167 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Expense Manager", 3 | "name": "Expense Manager", 4 | "description": "Track your spendings using expense manager", 5 | "icons": [ 6 | { 7 | "src": "icons/android-icon-1x.png", 8 | "type": "image/png", 9 | "sizes": "48x48" 10 | }, 11 | { 12 | "src": "icons/android-icon-2x.png", 13 | "type": "image/png", 14 | "sizes": "96x96" 15 | }, 16 | { 17 | "src": "icons/android-icon-144x144.png", 18 | "type": "image/png", 19 | "sizes": "144x144" 20 | }, 21 | { 22 | "src": "icons/android-icon-4x.png", 23 | "type": "image/png", 24 | "sizes": "192x192" 25 | } 26 | ], 27 | "start_url": "./index.html?utm_source=homescreen", 28 | "display": "standalone" 29 | } 30 | -------------------------------------------------------------------------------- /register-serviceworker.js: -------------------------------------------------------------------------------- 1 | // register for service worker 2 | if ("serviceWorker" in navigator) { 3 | window.addEventListener("load", () => { 4 | navigator.serviceWorker.register("sw.js").then( 5 | registration => { 6 | // Registration was successful 7 | console.log( 8 | "ServiceWorker registration successful with scope: ", 9 | registration.scope 10 | ); 11 | }, 12 | err => { 13 | // registration failed :( 14 | console.log("ServiceWorker registration failed: ", err); 15 | } 16 | ); 17 | }); 18 | } 19 | -------------------------------------------------------------------------------- /style.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-size: 16px; 3 | } 4 | 5 | .flex-container { 6 | display: flex; 7 | flex-direction: column; 8 | align-items: center; 9 | justify-content: space-between; 10 | /* 100vh - header height */ 11 | min-height: calc(100vh - 56px); 12 | width: 100vw; 13 | } 14 | 15 | #expense-form, #transfer-form { 16 | display: flex; 17 | flex-direction: column; 18 | align-items: center; 19 | margin-top: 15px; 20 | } 21 | 22 | #authorize-button { 23 | display: none; 24 | margin: auto; 25 | font-size: 1.5em; 26 | } 27 | 28 | .footer { 29 | padding: 10px; 30 | background: #424242; 31 | color: #9e9e9e; 32 | font-weight: 300 !important; 33 | margin-top: 15px; 34 | font-size: 12px; 35 | } 36 | 37 | .footer a { 38 | color: white; 39 | font-size: 12px; 40 | } 41 | 42 | #forms, #signout-button { 43 | display: none; 44 | } 45 | 46 | #form-loader { 47 | margin: auto; 48 | } 49 | 50 | .dropdown { 51 | display: flex; 52 | flex-direction: row; 53 | justify-content: space-between; 54 | width: 300px; 55 | margin-bottom: 10px; 56 | font-size: 14px; 57 | } 58 | 59 | .dropdown select { 60 | margin-left: 10px; 61 | flex-grow: 1; 62 | max-width: 50%; 63 | } 64 | 65 | /* Material desing overrides */ 66 | .mdl-switch { 67 | width: auto; 68 | } 69 | 70 | .mdl-layout__header-row { 71 | padding-left: 20px; 72 | } 73 | 74 | .material-icons { 75 | font-size: 14px; 76 | } 77 | -------------------------------------------------------------------------------- /sw.js: -------------------------------------------------------------------------------- 1 | var CACHE_NAME = "expense-manager-cache"; 2 | var urlsToCache = [ 3 | "style.css", 4 | "icons/favicon-32x32.png", 5 | "icons/favicon-16x16.png", 6 | "init.js", 7 | "transfer.js", 8 | "utils.js", 9 | "expense.js", 10 | "vendor/mdl/material.min.js", 11 | "vendor/mdl/material.min.css" 12 | ]; 13 | 14 | // cache after the first install 15 | self.addEventListener("install", function(event) { 16 | // Perform install steps 17 | event.waitUntil( 18 | caches.open(CACHE_NAME).then(function(cache) { 19 | console.log("Opened cache"); 20 | return cache.addAll(urlsToCache); 21 | }) 22 | ); 23 | }); 24 | 25 | // listen for fetch events 26 | self.addEventListener("fetch", function(event) { 27 | const requestURL = new URL(event.request.url); 28 | event.respondWith( 29 | caches.open(CACHE_NAME).then(function(cache) { 30 | return caches.match(event.request).then(function(response) { 31 | var fetchPromise = fetch(event.request).then(function(networkResponse) { 32 | // cache same host files only 33 | if ( 34 | requestURL.hostname === "mitul45.github.io" || 35 | requestURL.hostname === "localhost" 36 | ) 37 | cache.put(event.request, networkResponse.clone()); 38 | return networkResponse; 39 | }); 40 | return response || fetchPromise; 41 | }); 42 | }) 43 | ); 44 | }); 45 | -------------------------------------------------------------------------------- /transfer.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | const utils = window.expenseManager.utils; 3 | 4 | // Cached DOM bindings 5 | const byID = document.getElementById.bind(document); 6 | const transferFrom = byID("transfer-form"); 7 | const descriptionEl = byID("transfer-description"); 8 | const dateEl = byID("transfer-date"); 9 | const fromAccountEl = byID("transfer-from-account"); 10 | const toAccountEl = byID("transfer-to-account"); 11 | const amountEl = byID("transfer-amount"); 12 | const saveBtn = byID("save"); 13 | const snackbarContainer = byID("toast-container"); 14 | 15 | /** 16 | * Append transfer log to the expense sheet 17 | */ 18 | function save(event) { 19 | if (!transferFrom.checkValidity()) return false; 20 | 21 | event.preventDefault(); 22 | utils.showLoader(); 23 | 24 | const expenseDate = dateEl.value; 25 | const descriptionVal = descriptionEl.value; 26 | const fromAccountVal = fromAccountEl.value; 27 | const toAccountVal = toAccountEl.value; 28 | const amountVal = amountEl.value; 29 | 30 | const dateObj = { 31 | yyyy: expenseDate.substr(0, 4), 32 | mm: expenseDate.substr(5, 2), 33 | dd: expenseDate.substr(-2) 34 | }; 35 | gapi.client.sheets.spreadsheets.values 36 | .append( 37 | utils.appendRequestObj([ 38 | [ 39 | `=DATE(${dateObj.yyyy}, ${dateObj.mm}, ${dateObj.dd})`, 40 | descriptionVal, 41 | fromAccountVal, 42 | "Transfers", // category 43 | amountVal, // expense 44 | 0, // income 45 | true // is internal transfer? 46 | ], 47 | [ 48 | `=DATE(${dateObj.yyyy}, ${dateObj.mm}, ${dateObj.dd})`, 49 | descriptionVal, 50 | toAccountVal, 51 | "Transfers", // category 52 | 0, // expense 53 | amountVal, // income 54 | true // is internal transfer? 55 | ] 56 | ]) 57 | ) 58 | .then( 59 | response => { 60 | // reset fileds 61 | descriptionEl.value = ""; 62 | amountEl.value = ""; 63 | snackbarContainer.MaterialSnackbar.showSnackbar({ 64 | message: "Expense added!" 65 | }); 66 | utils.hideLoader(); 67 | }, 68 | response => { 69 | utils.hideLoader(); 70 | let message = "Sorry, something went wrong"; 71 | if (response.status === 403) { 72 | message = "Please copy the sheet in your drive"; 73 | } 74 | console.log(response); 75 | snackbarContainer.MaterialSnackbar.showSnackbar({ 76 | message, 77 | actionHandler: () => { 78 | window.open( 79 | "https://github.com/mitul45/expense-manager/blob/master/README.md#how-to-get-started", 80 | "_blank" 81 | ); 82 | }, 83 | actionText: "Details", 84 | timeout: 5 * 60 * 1000 85 | }); 86 | } 87 | ); 88 | } 89 | 90 | function init(sheetID, accounts) { 91 | // set date picker's defalt value as today 92 | dateEl.value = new Date().toISOString().substr(0, 10); 93 | accounts = accounts.sort(); 94 | 95 | // initialize accounts and categories dropdown 96 | fromAccountEl.innerHTML = accounts.map(utils.wrapInOption).join(); 97 | toAccountEl.innerHTML = accounts.map(utils.wrapInOption).join(); 98 | 99 | // In MDL - `required` input fields are invalid on page load by default (which looks bad). 100 | // Fix: https://github.com/google/material-design-lite/issues/1502#issuecomment-257405822 101 | document 102 | .querySelectorAll("*[data-required]") 103 | .forEach(e => (e.required = true)); 104 | 105 | // set lister for `Save` button 106 | saveBtn.onclick = save.bind(null); 107 | } 108 | 109 | window.expenseManager.transferForm = { 110 | init 111 | }; 112 | })(); 113 | -------------------------------------------------------------------------------- /utils.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | /** 3 | * @param {DOMElement} el 4 | */ 5 | function hideEl(el) { 6 | el.style.display = "none"; 7 | } 8 | 9 | /** 10 | * @param {DOMElement} el 11 | * @param {String} displayStyle - (optional) flex, inline 12 | */ 13 | function showEl(el, displayStyle) { 14 | el.style.display = displayStyle ? displayStyle : "block"; 15 | } 16 | 17 | /** 18 | * show loader, hide forms 19 | */ 20 | function showLoader(forms, loader) { 21 | hideEl(forms); 22 | showEl(loader); 23 | } 24 | 25 | /** 26 | * hide loader, show forms 27 | */ 28 | function hideLoader(forms, loader) { 29 | hideEl(loader); 30 | showEl(forms); 31 | } 32 | 33 | /** 34 | * Generate append request object - for given sheet and values to append 35 | * Docs: https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets.values/append 36 | * 37 | * @param {String} spreadsheetId Expense sheet ID 38 | * @param {Array} values values to be appended 39 | * @returns {Object} request object for append 40 | */ 41 | function appendRequestObj(spreadsheetId, values) { 42 | return { 43 | // The ID of the spreadsheet to update. 44 | spreadsheetId, 45 | 46 | // The A1 notation of a range to search for a logical table of data. 47 | // Values will be appended after the last row of the table. 48 | range: "Expenses!A1", 49 | 50 | includeValuesInResponse: true, 51 | 52 | responseDateTimeRenderOption: "FORMATTED_STRING", 53 | 54 | responseValueRenderOption: "FORMATTED_VALUE", 55 | 56 | // How the input data should be interpreted. 57 | valueInputOption: "USER_ENTERED", 58 | 59 | // How the input data should be inserted. 60 | insertDataOption: "INSERT_ROWS", 61 | 62 | resource: { 63 | values 64 | } 65 | }; 66 | } 67 | 68 | /** 69 | * Generate batchGet request object - for given sheet, and range. 70 | * Docs: https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets.values/batchGet 71 | * 72 | * @param {String} sheetID Expense sheet ID 73 | * @param {Array} ranges List of ranges in A1 notation 74 | * @returns {Object} request object for batchGet 75 | */ 76 | function batchGetRequestObj(spreadsheetId, ranges) { 77 | return { 78 | spreadsheetId, 79 | ranges, 80 | dateTimeRenderOption: "FORMATTED_STRING", 81 | majorDimension: "COLUMNS", 82 | valueRenderOption: "FORMATTED_VALUE" 83 | }; 84 | } 85 | 86 | function wrapInOption(option) { 87 | return ``; 88 | } 89 | 90 | window.expenseManager = window.expenseManager || {}; 91 | window.expenseManager.utils = window.expenseManager.utils || { 92 | showEl, 93 | hideEl, 94 | hideLoader, 95 | showLoader, 96 | wrapInOption, 97 | batchGetRequestObj, 98 | appendRequestObj 99 | }; 100 | })(); 101 | -------------------------------------------------------------------------------- /vendor/mdl/LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright 2015 Google Inc 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | 204 | All code in any directories or sub-directories that end with *.html or 205 | *.css is licensed under the Creative Commons Attribution International 206 | 4.0 License, which full text can be found here: 207 | https://creativecommons.org/licenses/by/4.0/legalcode. 208 | 209 | As an exception to this license, all html or css that is generated by 210 | the software at the direction of the user is copyright the user. The 211 | user has full ownership and control over such content, including 212 | whether and how they wish to license it. 213 | -------------------------------------------------------------------------------- /vendor/mdl/bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "material-design-lite", 3 | "version": "1.3.0", 4 | "homepage": "https://github.com/google/material-design-lite", 5 | "authors": [ 6 | "Material Design Lite team" 7 | ], 8 | "description": "Material Design Components in CSS, JS and HTML", 9 | "main": [ 10 | "material.min.css", 11 | "material.min.js" 12 | ], 13 | "keywords": [ 14 | "material", 15 | "design", 16 | "styleguide", 17 | "style", 18 | "guide" 19 | ], 20 | "license": "Apache-2", 21 | "ignore": [ 22 | "**/.*", 23 | "node_modules", 24 | "bower_components", 25 | "./lib/.bower_components", 26 | "test", 27 | "tests" 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /vendor/mdl/material.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * material-design-lite - Material Design Components in CSS, JS and HTML 3 | * @version v1.3.0 4 | * @license Apache-2.0 5 | * @copyright 2015 Google, Inc. 6 | * @link https://github.com/google/material-design-lite 7 | */ 8 | !function(){"use strict";function e(e,t){if(e){if(t.element_.classList.contains(t.CssClasses_.MDL_JS_RIPPLE_EFFECT)){var s=document.createElement("span");s.classList.add(t.CssClasses_.MDL_RIPPLE_CONTAINER),s.classList.add(t.CssClasses_.MDL_JS_RIPPLE_EFFECT);var i=document.createElement("span");i.classList.add(t.CssClasses_.MDL_RIPPLE),s.appendChild(i),e.appendChild(s)}e.addEventListener("click",function(s){if("#"===e.getAttribute("href").charAt(0)){s.preventDefault();var i=e.href.split("#")[1],n=t.element_.querySelector("#"+i);t.resetTabState_(),t.resetPanelState_(),e.classList.add(t.CssClasses_.ACTIVE_CLASS),n.classList.add(t.CssClasses_.ACTIVE_CLASS)}})}}function t(e,t,s,i){function n(){var n=e.href.split("#")[1],a=i.content_.querySelector("#"+n);i.resetTabState_(t),i.resetPanelState_(s),e.classList.add(i.CssClasses_.IS_ACTIVE),a.classList.add(i.CssClasses_.IS_ACTIVE)}if(i.tabBar_.classList.contains(i.CssClasses_.JS_RIPPLE_EFFECT)){var a=document.createElement("span");a.classList.add(i.CssClasses_.RIPPLE_CONTAINER),a.classList.add(i.CssClasses_.JS_RIPPLE_EFFECT);var l=document.createElement("span");l.classList.add(i.CssClasses_.RIPPLE),a.appendChild(l),e.appendChild(a)}i.tabBar_.classList.contains(i.CssClasses_.TAB_MANUAL_SWITCH)||e.addEventListener("click",function(t){"#"===e.getAttribute("href").charAt(0)&&(t.preventDefault(),n())}),e.show=n}var s={upgradeDom:function(e,t){},upgradeElement:function(e,t){},upgradeElements:function(e){},upgradeAllRegistered:function(){},registerUpgradedCallback:function(e,t){},register:function(e){},downgradeElements:function(e){}};s=function(){function e(e,t){for(var s=0;s0&&l(t.children))}function o(t){var s="undefined"==typeof t.widget&&"undefined"==typeof t.widget,i=!0;s||(i=t.widget||t.widget);var n={classConstructor:t.constructor||t.constructor,className:t.classAsString||t.classAsString,cssClass:t.cssClass||t.cssClass,widget:i,callbacks:[]};if(c.forEach(function(e){if(e.cssClass===n.cssClass)throw new Error("The provided cssClass has already been registered: "+e.cssClass);if(e.className===n.className)throw new Error("The provided className has already been registered")}),t.constructor.prototype.hasOwnProperty(C))throw new Error("MDL component classes must not have "+C+" defined as a property.");var a=e(t.classAsString,n);a||c.push(n)}function r(t,s){var i=e(t);i&&i.callbacks.push(s)}function _(){for(var e=0;e0&&this.container_.classList.contains(this.CssClasses_.IS_VISIBLE)&&(e.keyCode===this.Keycodes_.UP_ARROW?(e.preventDefault(),t[t.length-1].focus()):e.keyCode===this.Keycodes_.DOWN_ARROW&&(e.preventDefault(),t[0].focus()))}},d.prototype.handleItemKeyboardEvent_=function(e){if(this.element_&&this.container_){var t=this.element_.querySelectorAll("."+this.CssClasses_.ITEM+":not([disabled])");if(t&&t.length>0&&this.container_.classList.contains(this.CssClasses_.IS_VISIBLE)){var s=Array.prototype.slice.call(t).indexOf(e.target);if(e.keyCode===this.Keycodes_.UP_ARROW)e.preventDefault(),s>0?t[s-1].focus():t[t.length-1].focus();else if(e.keyCode===this.Keycodes_.DOWN_ARROW)e.preventDefault(),t.length>s+1?t[s+1].focus():t[0].focus();else if(e.keyCode===this.Keycodes_.SPACE||e.keyCode===this.Keycodes_.ENTER){e.preventDefault();var i=new MouseEvent("mousedown");e.target.dispatchEvent(i),i=new MouseEvent("mouseup"),e.target.dispatchEvent(i),e.target.click()}else e.keyCode===this.Keycodes_.ESCAPE&&(e.preventDefault(),this.hide())}}},d.prototype.handleItemClick_=function(e){e.target.hasAttribute("disabled")?e.stopPropagation():(this.closing_=!0,window.setTimeout(function(e){this.hide(),this.closing_=!1}.bind(this),this.Constant_.CLOSE_TIMEOUT))},d.prototype.applyClip_=function(e,t){this.element_.classList.contains(this.CssClasses_.UNALIGNED)?this.element_.style.clip="":this.element_.classList.contains(this.CssClasses_.BOTTOM_RIGHT)?this.element_.style.clip="rect(0 "+t+"px 0 "+t+"px)":this.element_.classList.contains(this.CssClasses_.TOP_LEFT)?this.element_.style.clip="rect("+e+"px 0 "+e+"px 0)":this.element_.classList.contains(this.CssClasses_.TOP_RIGHT)?this.element_.style.clip="rect("+e+"px "+t+"px "+e+"px "+t+"px)":this.element_.style.clip=""},d.prototype.removeAnimationEndListener_=function(e){e.target.classList.remove(d.prototype.CssClasses_.IS_ANIMATING)},d.prototype.addAnimationEndListener_=function(){this.element_.addEventListener("transitionend",this.removeAnimationEndListener_),this.element_.addEventListener("webkitTransitionEnd",this.removeAnimationEndListener_)},d.prototype.show=function(e){if(this.element_&&this.container_&&this.outline_){var t=this.element_.getBoundingClientRect().height,s=this.element_.getBoundingClientRect().width;this.container_.style.width=s+"px",this.container_.style.height=t+"px",this.outline_.style.width=s+"px",this.outline_.style.height=t+"px";for(var i=this.Constant_.TRANSITION_DURATION_SECONDS*this.Constant_.TRANSITION_DURATION_FRACTION,n=this.element_.querySelectorAll("."+this.CssClasses_.ITEM),a=0;a0&&this.showSnackbar(this.queuedNotifications_.shift())},C.prototype.cleanup_=function(){this.element_.classList.remove(this.cssClasses_.ACTIVE),setTimeout(function(){this.element_.setAttribute("aria-hidden","true"),this.textElement_.textContent="",Boolean(this.actionElement_.getAttribute("aria-hidden"))||(this.setActionHidden_(!0),this.actionElement_.textContent="",this.actionElement_.removeEventListener("click",this.actionHandler_)),this.actionHandler_=void 0,this.message_=void 0,this.actionText_=void 0,this.active=!1,this.checkQueue_()}.bind(this),this.Constant_.ANIMATION_LENGTH)},C.prototype.setActionHidden_=function(e){e?this.actionElement_.setAttribute("aria-hidden","true"):this.actionElement_.removeAttribute("aria-hidden")},s.register({constructor:C,classAsString:"MaterialSnackbar",cssClass:"mdl-js-snackbar",widget:!0});var u=function(e){this.element_=e,this.init()};window.MaterialSpinner=u,u.prototype.Constant_={MDL_SPINNER_LAYER_COUNT:4},u.prototype.CssClasses_={MDL_SPINNER_LAYER:"mdl-spinner__layer",MDL_SPINNER_CIRCLE_CLIPPER:"mdl-spinner__circle-clipper",MDL_SPINNER_CIRCLE:"mdl-spinner__circle",MDL_SPINNER_GAP_PATCH:"mdl-spinner__gap-patch",MDL_SPINNER_LEFT:"mdl-spinner__left",MDL_SPINNER_RIGHT:"mdl-spinner__right"},u.prototype.createLayer=function(e){var t=document.createElement("div");t.classList.add(this.CssClasses_.MDL_SPINNER_LAYER),t.classList.add(this.CssClasses_.MDL_SPINNER_LAYER+"-"+e);var s=document.createElement("div");s.classList.add(this.CssClasses_.MDL_SPINNER_CIRCLE_CLIPPER),s.classList.add(this.CssClasses_.MDL_SPINNER_LEFT);var i=document.createElement("div");i.classList.add(this.CssClasses_.MDL_SPINNER_GAP_PATCH);var n=document.createElement("div");n.classList.add(this.CssClasses_.MDL_SPINNER_CIRCLE_CLIPPER),n.classList.add(this.CssClasses_.MDL_SPINNER_RIGHT);for(var a=[s,i,n],l=0;l=this.maxRows&&e.preventDefault()},L.prototype.onFocus_=function(e){this.element_.classList.add(this.CssClasses_.IS_FOCUSED)},L.prototype.onBlur_=function(e){this.element_.classList.remove(this.CssClasses_.IS_FOCUSED)},L.prototype.onReset_=function(e){this.updateClasses_()},L.prototype.updateClasses_=function(){this.checkDisabled(),this.checkValidity(),this.checkDirty(),this.checkFocus()},L.prototype.checkDisabled=function(){this.input_.disabled?this.element_.classList.add(this.CssClasses_.IS_DISABLED):this.element_.classList.remove(this.CssClasses_.IS_DISABLED)},L.prototype.checkDisabled=L.prototype.checkDisabled,L.prototype.checkFocus=function(){Boolean(this.element_.querySelector(":focus"))?this.element_.classList.add(this.CssClasses_.IS_FOCUSED):this.element_.classList.remove(this.CssClasses_.IS_FOCUSED)},L.prototype.checkFocus=L.prototype.checkFocus,L.prototype.checkValidity=function(){this.input_.validity&&(this.input_.validity.valid?this.element_.classList.remove(this.CssClasses_.IS_INVALID):this.element_.classList.add(this.CssClasses_.IS_INVALID))},L.prototype.checkValidity=L.prototype.checkValidity,L.prototype.checkDirty=function(){this.input_.value&&this.input_.value.length>0?this.element_.classList.add(this.CssClasses_.IS_DIRTY):this.element_.classList.remove(this.CssClasses_.IS_DIRTY)},L.prototype.checkDirty=L.prototype.checkDirty,L.prototype.disable=function(){this.input_.disabled=!0,this.updateClasses_()},L.prototype.disable=L.prototype.disable,L.prototype.enable=function(){this.input_.disabled=!1,this.updateClasses_()},L.prototype.enable=L.prototype.enable,L.prototype.change=function(e){this.input_.value=e||"",this.updateClasses_()},L.prototype.change=L.prototype.change,L.prototype.init=function(){if(this.element_&&(this.label_=this.element_.querySelector("."+this.CssClasses_.LABEL),this.input_=this.element_.querySelector("."+this.CssClasses_.INPUT),this.input_)){this.input_.hasAttribute(this.Constant_.MAX_ROWS_ATTRIBUTE)&&(this.maxRows=parseInt(this.input_.getAttribute(this.Constant_.MAX_ROWS_ATTRIBUTE),10),isNaN(this.maxRows)&&(this.maxRows=this.Constant_.NO_MAX_ROWS)),this.input_.hasAttribute("placeholder")&&this.element_.classList.add(this.CssClasses_.HAS_PLACEHOLDER),this.boundUpdateClassesHandler=this.updateClasses_.bind(this),this.boundFocusHandler=this.onFocus_.bind(this),this.boundBlurHandler=this.onBlur_.bind(this),this.boundResetHandler=this.onReset_.bind(this),this.input_.addEventListener("input",this.boundUpdateClassesHandler),this.input_.addEventListener("focus",this.boundFocusHandler),this.input_.addEventListener("blur",this.boundBlurHandler),this.input_.addEventListener("reset",this.boundResetHandler),this.maxRows!==this.Constant_.NO_MAX_ROWS&&(this.boundKeyDownHandler=this.onKeyDown_.bind(this),this.input_.addEventListener("keydown",this.boundKeyDownHandler));var e=this.element_.classList.contains(this.CssClasses_.IS_INVALID);this.updateClasses_(),this.element_.classList.add(this.CssClasses_.IS_UPGRADED),e&&this.element_.classList.add(this.CssClasses_.IS_INVALID),this.input_.hasAttribute("autofocus")&&(this.element_.focus(),this.checkFocus())}},s.register({constructor:L,classAsString:"MaterialTextfield",cssClass:"mdl-js-textfield",widget:!0});var I=function(e){this.element_=e,this.init()};window.MaterialTooltip=I,I.prototype.Constant_={},I.prototype.CssClasses_={IS_ACTIVE:"is-active",BOTTOM:"mdl-tooltip--bottom",LEFT:"mdl-tooltip--left",RIGHT:"mdl-tooltip--right",TOP:"mdl-tooltip--top"},I.prototype.handleMouseEnter_=function(e){var t=e.target.getBoundingClientRect(),s=t.left+t.width/2,i=t.top+t.height/2,n=-1*(this.element_.offsetWidth/2),a=-1*(this.element_.offsetHeight/2);this.element_.classList.contains(this.CssClasses_.LEFT)||this.element_.classList.contains(this.CssClasses_.RIGHT)?(s=t.width/2,i+a<0?(this.element_.style.top="0",this.element_.style.marginTop="0"):(this.element_.style.top=i+"px",this.element_.style.marginTop=a+"px")):s+n<0?(this.element_.style.left="0",this.element_.style.marginLeft="0"):(this.element_.style.left=s+"px",this.element_.style.marginLeft=n+"px"),this.element_.classList.contains(this.CssClasses_.TOP)?this.element_.style.top=t.top-this.element_.offsetHeight-10+"px":this.element_.classList.contains(this.CssClasses_.RIGHT)?this.element_.style.left=t.left+t.width+10+"px":this.element_.classList.contains(this.CssClasses_.LEFT)?this.element_.style.left=t.left-this.element_.offsetWidth-10+"px":this.element_.style.top=t.top+t.height+10+"px",this.element_.classList.add(this.CssClasses_.IS_ACTIVE)},I.prototype.hideTooltip_=function(){this.element_.classList.remove(this.CssClasses_.IS_ACTIVE)},I.prototype.init=function(){if(this.element_){var e=this.element_.getAttribute("for")||this.element_.getAttribute("data-mdl-for");e&&(this.forElement_=document.getElementById(e)),this.forElement_&&(this.forElement_.hasAttribute("tabindex")||this.forElement_.setAttribute("tabindex","0"),this.boundMouseEnterHandler=this.handleMouseEnter_.bind(this),this.boundMouseLeaveAndScrollHandler=this.hideTooltip_.bind(this),this.forElement_.addEventListener("mouseenter",this.boundMouseEnterHandler,!1),this.forElement_.addEventListener("touchend",this.boundMouseEnterHandler,!1),this.forElement_.addEventListener("mouseleave",this.boundMouseLeaveAndScrollHandler,!1),window.addEventListener("scroll",this.boundMouseLeaveAndScrollHandler,!0),window.addEventListener("touchstart",this.boundMouseLeaveAndScrollHandler))}},s.register({constructor:I,classAsString:"MaterialTooltip",cssClass:"mdl-tooltip"});var f=function(e){this.element_=e,this.init()};window.MaterialLayout=f,f.prototype.Constant_={MAX_WIDTH:"(max-width: 1024px)",TAB_SCROLL_PIXELS:100,RESIZE_TIMEOUT:100,MENU_ICON:"",CHEVRON_LEFT:"chevron_left",CHEVRON_RIGHT:"chevron_right"},f.prototype.Keycodes_={ENTER:13,ESCAPE:27,SPACE:32},f.prototype.Mode_={STANDARD:0,SEAMED:1,WATERFALL:2,SCROLL:3},f.prototype.CssClasses_={CONTAINER:"mdl-layout__container",HEADER:"mdl-layout__header",DRAWER:"mdl-layout__drawer",CONTENT:"mdl-layout__content",DRAWER_BTN:"mdl-layout__drawer-button",ICON:"material-icons",JS_RIPPLE_EFFECT:"mdl-js-ripple-effect",RIPPLE_CONTAINER:"mdl-layout__tab-ripple-container",RIPPLE:"mdl-ripple",RIPPLE_IGNORE_EVENTS:"mdl-js-ripple-effect--ignore-events",HEADER_SEAMED:"mdl-layout__header--seamed",HEADER_WATERFALL:"mdl-layout__header--waterfall",HEADER_SCROLL:"mdl-layout__header--scroll",FIXED_HEADER:"mdl-layout--fixed-header",OBFUSCATOR:"mdl-layout__obfuscator",TAB_BAR:"mdl-layout__tab-bar",TAB_CONTAINER:"mdl-layout__tab-bar-container",TAB:"mdl-layout__tab",TAB_BAR_BUTTON:"mdl-layout__tab-bar-button",TAB_BAR_LEFT_BUTTON:"mdl-layout__tab-bar-left-button",TAB_BAR_RIGHT_BUTTON:"mdl-layout__tab-bar-right-button",TAB_MANUAL_SWITCH:"mdl-layout__tab-manual-switch",PANEL:"mdl-layout__tab-panel",HAS_DRAWER:"has-drawer",HAS_TABS:"has-tabs",HAS_SCROLLING_HEADER:"has-scrolling-header",CASTING_SHADOW:"is-casting-shadow",IS_COMPACT:"is-compact",IS_SMALL_SCREEN:"is-small-screen",IS_DRAWER_OPEN:"is-visible",IS_ACTIVE:"is-active",IS_UPGRADED:"is-upgraded",IS_ANIMATING:"is-animating",ON_LARGE_SCREEN:"mdl-layout--large-screen-only",ON_SMALL_SCREEN:"mdl-layout--small-screen-only"},f.prototype.contentScrollHandler_=function(){if(!this.header_.classList.contains(this.CssClasses_.IS_ANIMATING)){var e=!this.element_.classList.contains(this.CssClasses_.IS_SMALL_SCREEN)||this.element_.classList.contains(this.CssClasses_.FIXED_HEADER);this.content_.scrollTop>0&&!this.header_.classList.contains(this.CssClasses_.IS_COMPACT)?(this.header_.classList.add(this.CssClasses_.CASTING_SHADOW),this.header_.classList.add(this.CssClasses_.IS_COMPACT),e&&this.header_.classList.add(this.CssClasses_.IS_ANIMATING)):this.content_.scrollTop<=0&&this.header_.classList.contains(this.CssClasses_.IS_COMPACT)&&(this.header_.classList.remove(this.CssClasses_.CASTING_SHADOW),this.header_.classList.remove(this.CssClasses_.IS_COMPACT),e&&this.header_.classList.add(this.CssClasses_.IS_ANIMATING))}},f.prototype.keyboardEventHandler_=function(e){e.keyCode===this.Keycodes_.ESCAPE&&this.drawer_.classList.contains(this.CssClasses_.IS_DRAWER_OPEN)&&this.toggleDrawer()},f.prototype.screenSizeHandler_=function(){this.screenSizeMediaQuery_.matches?this.element_.classList.add(this.CssClasses_.IS_SMALL_SCREEN):(this.element_.classList.remove(this.CssClasses_.IS_SMALL_SCREEN),this.drawer_&&(this.drawer_.classList.remove(this.CssClasses_.IS_DRAWER_OPEN),this.obfuscator_.classList.remove(this.CssClasses_.IS_DRAWER_OPEN)))},f.prototype.drawerToggleHandler_=function(e){if(e&&"keydown"===e.type){if(e.keyCode!==this.Keycodes_.SPACE&&e.keyCode!==this.Keycodes_.ENTER)return;e.preventDefault()}this.toggleDrawer()},f.prototype.headerTransitionEndHandler_=function(){this.header_.classList.remove(this.CssClasses_.IS_ANIMATING)},f.prototype.headerClickHandler_=function(){this.header_.classList.contains(this.CssClasses_.IS_COMPACT)&&(this.header_.classList.remove(this.CssClasses_.IS_COMPACT),this.header_.classList.add(this.CssClasses_.IS_ANIMATING))},f.prototype.resetTabState_=function(e){for(var t=0;t0?c.classList.add(this.CssClasses_.IS_ACTIVE):c.classList.remove(this.CssClasses_.IS_ACTIVE),this.tabBar_.scrollLeft0)return;this.setFrameCount(1);var i,n,a=e.currentTarget.getBoundingClientRect();if(0===e.clientX&&0===e.clientY)i=Math.round(a.width/2),n=Math.round(a.height/2);else{var l=void 0!==e.clientX?e.clientX:e.touches[0].clientX,o=void 0!==e.clientY?e.clientY:e.touches[0].clientY;i=Math.round(l-a.left),n=Math.round(o-a.top)}this.setRippleXY(i,n),this.setRippleStyles(!0),window.requestAnimationFrame(this.animFrameHandler.bind(this))}},S.prototype.upHandler_=function(e){e&&2!==e.detail&&window.setTimeout(function(){this.rippleElement_.classList.remove(this.CssClasses_.IS_VISIBLE)}.bind(this),0)},S.prototype.init=function(){if(this.element_){var e=this.element_.classList.contains(this.CssClasses_.RIPPLE_CENTER);this.element_.classList.contains(this.CssClasses_.RIPPLE_EFFECT_IGNORE_EVENTS)||(this.rippleElement_=this.element_.querySelector("."+this.CssClasses_.RIPPLE),this.frameCount_=0,this.rippleSize_=0,this.x_=0,this.y_=0,this.ignoringMouseDown_=!1,this.boundDownHandler=this.downHandler_.bind(this),this.element_.addEventListener("mousedown",this.boundDownHandler),this.element_.addEventListener("touchstart",this.boundDownHandler),this.boundUpHandler=this.upHandler_.bind(this),this.element_.addEventListener("mouseup",this.boundUpHandler),this.element_.addEventListener("mouseleave",this.boundUpHandler),this.element_.addEventListener("touchend",this.boundUpHandler),this.element_.addEventListener("blur",this.boundUpHandler),this.getFrameCount=function(){return this.frameCount_},this.setFrameCount=function(e){this.frameCount_=e},this.getRippleElement=function(){return this.rippleElement_},this.setRippleXY=function(e,t){this.x_=e,this.y_=t},this.setRippleStyles=function(t){if(null!==this.rippleElement_){var s,i,n,a="translate("+this.x_+"px, "+this.y_+"px)";t?(i=this.Constant_.INITIAL_SCALE,n=this.Constant_.INITIAL_SIZE):(i=this.Constant_.FINAL_SCALE,n=this.rippleSize_+"px",e&&(a="translate("+this.boundWidth/2+"px, "+this.boundHeight/2+"px)")),s="translate(-50%, -50%) "+a+i,this.rippleElement_.style.webkitTransform=s,this.rippleElement_.style.msTransform=s,this.rippleElement_.style.transform=s,t?this.rippleElement_.classList.remove(this.CssClasses_.IS_ANIMATING):this.rippleElement_.classList.add(this.CssClasses_.IS_ANIMATING)}},this.animFrameHandler=function(){this.frameCount_-- >0?window.requestAnimationFrame(this.animFrameHandler.bind(this)):this.setRippleStyles(!1)})}},s.register({constructor:S,classAsString:"MaterialRipple",cssClass:"mdl-js-ripple-effect",widget:!1})}(); 10 | //# sourceMappingURL=material.min.js.map 11 | -------------------------------------------------------------------------------- /vendor/mdl/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "material-design-lite", 3 | "version": "1.3.0", 4 | "description": "Material Design Components in CSS, JS and HTML", 5 | "private": true, 6 | "license": "Apache-2.0", 7 | "author": "Google", 8 | "repository": "google/material-design-lite", 9 | "main": "dist/material.min.js", 10 | "devDependencies": { 11 | "acorn": "^4.0.3", 12 | "babel-core": "^6.20.0", 13 | "babel-preset-es2015": "^6.18.0", 14 | "browser-sync": "^2.2.3", 15 | "chai": "^3.3.0", 16 | "chai-jquery": "^2.0.0", 17 | "del": "^2.0.2", 18 | "drool": "^0.4.0", 19 | "escodegen": "^1.6.1", 20 | "google-closure-compiler": "", 21 | "gulp": "^3.9.0", 22 | "gulp-autoprefixer": "^3.0.2", 23 | "gulp-cache": "^0.4.5", 24 | "gulp-closure-compiler": "^0.4.0", 25 | "gulp-concat": "^2.4.1", 26 | "gulp-connect": "^5.0.0", 27 | "gulp-css-inline-images": "^0.1.1", 28 | "gulp-csso": "1.0.0", 29 | "gulp-file": "^0.3.0", 30 | "gulp-flatten": "^0.3.1", 31 | "gulp-front-matter": "^1.2.2", 32 | "gulp-header": "^1.2.2", 33 | "gulp-if": "^2.0.0", 34 | "gulp-iife": "^0.3.0", 35 | "gulp-imagemin": "^3.1.0", 36 | "gulp-jscs": "^4.0.0", 37 | "gulp-jshint": "^2.0.4", 38 | "gulp-load-plugins": "^1.3.0", 39 | "gulp-marked": "^1.0.0", 40 | "gulp-mocha-phantomjs": "^0.12.0", 41 | "gulp-open": "^2.0.0", 42 | "gulp-rename": "^1.2.0", 43 | "gulp-replace": "^0.5.3", 44 | "gulp-sass": "3.0.0", 45 | "gulp-shell": "^0.5.2", 46 | "gulp-size": "^2.0.0", 47 | "gulp-sourcemaps": "^2.0.1", 48 | "gulp-subtree": "^0.1.0", 49 | "gulp-tap": "^0.1.3", 50 | "gulp-uglify": "^2.0.0", 51 | "gulp-util": "^3.0.4", 52 | "gulp-zip": "^3.0.2", 53 | "humanize": "0.0.9", 54 | "jquery": "^3.1.1", 55 | "jshint": "^2.9.4", 56 | "jshint-stylish": "^2.2.1", 57 | "merge-stream": "^1.0.0", 58 | "mocha": "^3.0.2", 59 | "prismjs": "0.0.1", 60 | "run-sequence": "^1.0.2", 61 | "swig": "^1.4.2", 62 | "through2": "^2.0.0", 63 | "vinyl-paths": "^2.0.0" 64 | }, 65 | "engines": { 66 | "node": ">=0.12.0" 67 | }, 68 | "scripts": { 69 | "test": "gulp && git status | grep 'working directory clean' >/dev/null || (echo 'Please commit all changes generated by building'; exit 1)" 70 | }, 71 | "babel": { 72 | "only": "gulpfile.babel.js", 73 | "presets": [ 74 | "es2015" 75 | ] 76 | } 77 | } 78 | --------------------------------------------------------------------------------