├── .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 [](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 | 
28 | 1. Don't rename it. It should be named `Expense Sheet`.
29 | 
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 |
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 |
--------------------------------------------------------------------------------