├── .gitignore ├── .cfignore ├── favicons ├── favicon.ico ├── favicon-16x16.png ├── favicon-32x32.png ├── mstile-150x150.png ├── apple-touch-icon.png ├── android-chrome-192x192.png ├── android-chrome-512x512.png └── safari-pinned-tab.svg ├── tutorial ├── resources │ ├── step-01.png │ ├── step-02.png │ ├── step-03.png │ ├── step-04.png │ ├── step-05.png │ ├── step-08.png │ ├── step-05-pwa.png │ ├── step-06-pwa.png │ └── step-07-pwa.png ├── step-07 │ ├── favicons │ │ ├── favicon.ico │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ ├── mstile-150x150.png │ │ ├── apple-touch-icon.png │ │ ├── android-chrome-192x192.png │ │ ├── android-chrome-512x512.png │ │ └── safari-pinned-tab.svg │ ├── browserconfig.xml │ ├── manifest.json │ ├── worker.js │ ├── shoppinglist.css │ ├── shoppinglist.model.js │ ├── shoppinglist.js │ └── index.html ├── step-08 │ ├── favicons │ │ ├── favicon.ico │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ ├── mstile-150x150.png │ │ ├── apple-touch-icon.png │ │ ├── android-chrome-192x192.png │ │ ├── android-chrome-512x512.png │ │ └── safari-pinned-tab.svg │ ├── browserconfig.xml │ ├── manifest.json │ ├── worker.js │ ├── shoppinglist.css │ ├── shoppinglist.model.js │ └── index.html ├── step-01 │ ├── shoppinglist.js │ ├── shoppinglist.model.js │ ├── shoppinglist.css │ └── index.html ├── step-02 │ ├── shoppinglist.css │ ├── shoppinglist.model.js │ ├── index.html │ └── shoppinglist.js ├── step-06 │ ├── worker.js │ ├── shoppinglist.css │ ├── shoppinglist.model.js │ ├── index.html │ └── shoppinglist.js ├── step-03 │ ├── shoppinglist.css │ ├── shoppinglist.model.js │ ├── index.html │ └── shoppinglist.js ├── step-04 │ ├── shoppinglist.css │ ├── shoppinglist.model.js │ ├── shoppinglist.js │ └── index.html └── step-05 │ ├── shoppinglist.css │ ├── shoppinglist.model.js │ ├── index.html │ └── shoppinglist.js ├── doc └── source │ └── images │ ├── create_db.png │ ├── enable_cors.png │ ├── replicator.png │ ├── architecture.png │ ├── shopping_lists1.png │ ├── shopping_lists2.png │ └── Screen Shot 2018-01-22 at 10.04.01 AM.png ├── test └── test.js ├── .travis.yml ├── repository.yaml ├── browserconfig.xml ├── manifest.yml ├── karma.conf.js ├── manifest.json ├── package.json ├── app.js ├── CONTRIBUTING.md ├── MAINTAINERS.md ├── worker.js ├── css └── shoppinglist.css └── js └── shoppinglist.model.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | -------------------------------------------------------------------------------- /.cfignore: -------------------------------------------------------------------------------- 1 | .gitignore 2 | .DS_Store 3 | node_modules/ -------------------------------------------------------------------------------- /favicons/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ibm-watson-data-lab/shopping-list-vanillajs-pouchdb/HEAD/favicons/favicon.ico -------------------------------------------------------------------------------- /favicons/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ibm-watson-data-lab/shopping-list-vanillajs-pouchdb/HEAD/favicons/favicon-16x16.png -------------------------------------------------------------------------------- /favicons/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ibm-watson-data-lab/shopping-list-vanillajs-pouchdb/HEAD/favicons/favicon-32x32.png -------------------------------------------------------------------------------- /favicons/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ibm-watson-data-lab/shopping-list-vanillajs-pouchdb/HEAD/favicons/mstile-150x150.png -------------------------------------------------------------------------------- /favicons/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ibm-watson-data-lab/shopping-list-vanillajs-pouchdb/HEAD/favicons/apple-touch-icon.png -------------------------------------------------------------------------------- /tutorial/resources/step-01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ibm-watson-data-lab/shopping-list-vanillajs-pouchdb/HEAD/tutorial/resources/step-01.png -------------------------------------------------------------------------------- /tutorial/resources/step-02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ibm-watson-data-lab/shopping-list-vanillajs-pouchdb/HEAD/tutorial/resources/step-02.png -------------------------------------------------------------------------------- /tutorial/resources/step-03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ibm-watson-data-lab/shopping-list-vanillajs-pouchdb/HEAD/tutorial/resources/step-03.png -------------------------------------------------------------------------------- /tutorial/resources/step-04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ibm-watson-data-lab/shopping-list-vanillajs-pouchdb/HEAD/tutorial/resources/step-04.png -------------------------------------------------------------------------------- /tutorial/resources/step-05.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ibm-watson-data-lab/shopping-list-vanillajs-pouchdb/HEAD/tutorial/resources/step-05.png -------------------------------------------------------------------------------- /tutorial/resources/step-08.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ibm-watson-data-lab/shopping-list-vanillajs-pouchdb/HEAD/tutorial/resources/step-08.png -------------------------------------------------------------------------------- /doc/source/images/create_db.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ibm-watson-data-lab/shopping-list-vanillajs-pouchdb/HEAD/doc/source/images/create_db.png -------------------------------------------------------------------------------- /doc/source/images/enable_cors.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ibm-watson-data-lab/shopping-list-vanillajs-pouchdb/HEAD/doc/source/images/enable_cors.png -------------------------------------------------------------------------------- /doc/source/images/replicator.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ibm-watson-data-lab/shopping-list-vanillajs-pouchdb/HEAD/doc/source/images/replicator.png -------------------------------------------------------------------------------- /doc/source/images/architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ibm-watson-data-lab/shopping-list-vanillajs-pouchdb/HEAD/doc/source/images/architecture.png -------------------------------------------------------------------------------- /favicons/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ibm-watson-data-lab/shopping-list-vanillajs-pouchdb/HEAD/favicons/android-chrome-192x192.png -------------------------------------------------------------------------------- /favicons/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ibm-watson-data-lab/shopping-list-vanillajs-pouchdb/HEAD/favicons/android-chrome-512x512.png -------------------------------------------------------------------------------- /tutorial/resources/step-05-pwa.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ibm-watson-data-lab/shopping-list-vanillajs-pouchdb/HEAD/tutorial/resources/step-05-pwa.png -------------------------------------------------------------------------------- /tutorial/resources/step-06-pwa.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ibm-watson-data-lab/shopping-list-vanillajs-pouchdb/HEAD/tutorial/resources/step-06-pwa.png -------------------------------------------------------------------------------- /tutorial/resources/step-07-pwa.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ibm-watson-data-lab/shopping-list-vanillajs-pouchdb/HEAD/tutorial/resources/step-07-pwa.png -------------------------------------------------------------------------------- /doc/source/images/shopping_lists1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ibm-watson-data-lab/shopping-list-vanillajs-pouchdb/HEAD/doc/source/images/shopping_lists1.png -------------------------------------------------------------------------------- /doc/source/images/shopping_lists2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ibm-watson-data-lab/shopping-list-vanillajs-pouchdb/HEAD/doc/source/images/shopping_lists2.png -------------------------------------------------------------------------------- /tutorial/step-07/favicons/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ibm-watson-data-lab/shopping-list-vanillajs-pouchdb/HEAD/tutorial/step-07/favicons/favicon.ico -------------------------------------------------------------------------------- /tutorial/step-08/favicons/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ibm-watson-data-lab/shopping-list-vanillajs-pouchdb/HEAD/tutorial/step-08/favicons/favicon.ico -------------------------------------------------------------------------------- /tutorial/step-07/favicons/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ibm-watson-data-lab/shopping-list-vanillajs-pouchdb/HEAD/tutorial/step-07/favicons/favicon-16x16.png -------------------------------------------------------------------------------- /tutorial/step-07/favicons/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ibm-watson-data-lab/shopping-list-vanillajs-pouchdb/HEAD/tutorial/step-07/favicons/favicon-32x32.png -------------------------------------------------------------------------------- /tutorial/step-08/favicons/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ibm-watson-data-lab/shopping-list-vanillajs-pouchdb/HEAD/tutorial/step-08/favicons/favicon-16x16.png -------------------------------------------------------------------------------- /tutorial/step-08/favicons/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ibm-watson-data-lab/shopping-list-vanillajs-pouchdb/HEAD/tutorial/step-08/favicons/favicon-32x32.png -------------------------------------------------------------------------------- /tutorial/step-07/favicons/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ibm-watson-data-lab/shopping-list-vanillajs-pouchdb/HEAD/tutorial/step-07/favicons/mstile-150x150.png -------------------------------------------------------------------------------- /tutorial/step-08/favicons/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ibm-watson-data-lab/shopping-list-vanillajs-pouchdb/HEAD/tutorial/step-08/favicons/mstile-150x150.png -------------------------------------------------------------------------------- /tutorial/step-07/favicons/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ibm-watson-data-lab/shopping-list-vanillajs-pouchdb/HEAD/tutorial/step-07/favicons/apple-touch-icon.png -------------------------------------------------------------------------------- /tutorial/step-08/favicons/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ibm-watson-data-lab/shopping-list-vanillajs-pouchdb/HEAD/tutorial/step-08/favicons/apple-touch-icon.png -------------------------------------------------------------------------------- /tutorial/step-07/favicons/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ibm-watson-data-lab/shopping-list-vanillajs-pouchdb/HEAD/tutorial/step-07/favicons/android-chrome-192x192.png -------------------------------------------------------------------------------- /tutorial/step-07/favicons/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ibm-watson-data-lab/shopping-list-vanillajs-pouchdb/HEAD/tutorial/step-07/favicons/android-chrome-512x512.png -------------------------------------------------------------------------------- /tutorial/step-08/favicons/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ibm-watson-data-lab/shopping-list-vanillajs-pouchdb/HEAD/tutorial/step-08/favicons/android-chrome-192x192.png -------------------------------------------------------------------------------- /tutorial/step-08/favicons/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ibm-watson-data-lab/shopping-list-vanillajs-pouchdb/HEAD/tutorial/step-08/favicons/android-chrome-512x512.png -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | /* global describe, it, assert */ 2 | 3 | // TODO: add real tests 4 | describe('test', function () { 5 | it('should return 1', function () { 6 | assert.equal(1, 1) 7 | }) 8 | }) 9 | -------------------------------------------------------------------------------- /doc/source/images/Screen Shot 2018-01-22 at 10.04.01 AM.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ibm-watson-data-lab/shopping-list-vanillajs-pouchdb/HEAD/doc/source/images/Screen Shot 2018-01-22 at 10.04.01 AM.png -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '8' 4 | - '7' 5 | - '6' 6 | dist: trusty 7 | sudo: required 8 | addons: 9 | chrome: stable 10 | cache: 11 | directories: 12 | - node_modules 13 | script: npm test -------------------------------------------------------------------------------- /repository.yaml: -------------------------------------------------------------------------------- 1 | id: https://github.com/ibm-watson-data-lab/shopping-list-vanillajs-pouchdb 2 | event_id: web 3 | event_organizer: wdp-devadv 4 | 5 | runtimes: 6 | - Cloud Foundry 7 | 8 | services: 9 | - Cloudant NoSQL DB 10 | 11 | language: nodejs -------------------------------------------------------------------------------- /browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #283b4f 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /tutorial/step-07/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #283b4f 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /tutorial/step-08/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #283b4f 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /manifest.yml: -------------------------------------------------------------------------------- 1 | --- 2 | declared-services: 3 | sljsp-cloudantNoSQLDB: 4 | label: cloudantNoSQLDB 5 | plan: Lite 6 | applications: 7 | - name: shopping-list-vanillajs-pouchdb 8 | buildpack: sdk-for-nodejs 9 | memory: 256M 10 | instances: 1 11 | disk_quota: 1024M 12 | host: shopping-list-vanillajs-pouchdb-${random-word} 13 | services: 14 | - sljsp-cloudantNoSQLDB 15 | 16 | -------------------------------------------------------------------------------- /tutorial/step-01/shoppinglist.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict' 3 | 4 | var model = null 5 | 6 | var shopper = function (themodel) { 7 | if (themodel) { 8 | themodel(function (err, response) { 9 | if (err) { 10 | console.error(err) 11 | } else { 12 | model = response 13 | console.log('shopper ready!') 14 | } 15 | }) 16 | } 17 | return this 18 | } 19 | 20 | window.shopper = shopper 21 | }()) 22 | -------------------------------------------------------------------------------- /tutorial/step-01/shoppinglist.model.js: -------------------------------------------------------------------------------- 1 | /* global PouchDB */ 2 | 3 | (function () { 4 | 'use strict' 5 | 6 | // PouchDB 7 | var db = null 8 | 9 | var model = function (callback) { 10 | db = new PouchDB('shopping') 11 | if (typeof callback === 'function') { 12 | console.log('model ready!') 13 | callback(null, model) 14 | } 15 | } 16 | 17 | window.addEventListener('DOMContentLoaded', function () { 18 | window.shopper(model) 19 | }) 20 | }()) 21 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Configuration for the Karma Test Runner 3 | * 4 | * @param {Object} config 5 | */ 6 | module.exports = function (config) { 7 | config.set({ 8 | frameworks: ['mocha', 'chai'], 9 | files: ['test/**/*.js'], 10 | reporters: ['progress'], 11 | port: 9876, // karma web server port 12 | colors: true, 13 | logLevel: config.LOG_INFO, 14 | browsers: ['ChromeHeadless'], 15 | autoWatch: false, 16 | // singleRun: false, // Karma captures browsers, runs the tests and exits 17 | concurrency: Infinity 18 | }) 19 | } 20 | -------------------------------------------------------------------------------- /tutorial/step-07/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Shopper", 3 | "name": "Shopping List", 4 | "start_url": "/", 5 | "background_color": "#283b4f", 6 | "theme_color": "#283b4f", 7 | "display": "standalone", 8 | "orientation": "portrait", 9 | "icons": [ 10 | { 11 | "src": "favicons/android-chrome-192x192.png", 12 | "sizes": "192x192", 13 | "type": "image/png" 14 | }, 15 | { 16 | "src": "favicons/android-chrome-512x512.png", 17 | "sizes": "512x512", 18 | "type": "image/png" 19 | } 20 | ] 21 | } -------------------------------------------------------------------------------- /tutorial/step-08/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Shopper", 3 | "name": "Shopping List", 4 | "start_url": "/", 5 | "background_color": "#283b4f", 6 | "theme_color": "#283b4f", 7 | "display": "standalone", 8 | "orientation": "portrait", 9 | "icons": [ 10 | { 11 | "src": "favicons/android-chrome-192x192.png", 12 | "sizes": "192x192", 13 | "type": "image/png" 14 | }, 15 | { 16 | "src": "favicons/android-chrome-512x512.png", 17 | "sizes": "512x512", 18 | "type": "image/png" 19 | } 20 | ] 21 | } -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Shopper", 3 | "name": "Shopping List", 4 | "start_url": "/shopping-list-vanillajs-pouchdb", 5 | "background_color": "#283b4f", 6 | "theme_color": "#283b4f", 7 | "display": "standalone", 8 | "orientation": "portrait", 9 | "icons": [ 10 | { 11 | "src": "favicons/android-chrome-192x192.png", 12 | "sizes": "192x192", 13 | "type": "image/png" 14 | }, 15 | { 16 | "src": "favicons/android-chrome-512x512.png", 17 | "sizes": "512x512", 18 | "type": "image/png" 19 | } 20 | ] 21 | } -------------------------------------------------------------------------------- /tutorial/step-01/shoppinglist.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: #E1E2E1; 3 | box-shadow: none !important; 4 | overflow-x: hidden; 5 | } 6 | 7 | body * { 8 | box-shadow: none !important; 9 | } 10 | 11 | .primary-color { 12 | background-color: #52647A !important; 13 | } 14 | 15 | .primary-color * { 16 | color: #FFFFFF !important; 17 | } 18 | 19 | nav .nav-wrapper { 20 | padding-left: 10px; 21 | } 22 | 23 | main { 24 | display: flex; 25 | margin: 0 auto 75px auto; 26 | width: 200vw; 27 | } 28 | 29 | #shopping-lists, 30 | #shopping-list-items { 31 | display: inline-block; 32 | flex: 1; 33 | margin: 0; 34 | padding: 0; 35 | transition-property: transform; 36 | transition-duration: 0.5s; 37 | } 38 | 39 | #shopping-list-items { 40 | background-color: #FFFFFF; 41 | border: 0 none; 42 | vertical-align: top; 43 | } 44 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "shopping-list-vanillajs-pouchdb", 3 | "version": "1.0.0", 4 | "description": "Offline First shopping list app written with plain JavaScript and PouchDB", 5 | "main": "app.js", 6 | "scripts": { 7 | "test": "standard */*.js *.js && karma start --single-run --browsers ChromeHeadless karma.conf.js", 8 | "start": "node app.js" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/ibm-watson-data-lab/shopping-list-vanillajs-pouchdb.git" 13 | }, 14 | "keywords": [ 15 | "OffineFirst", 16 | "JavaScript", 17 | "PouchDB", 18 | "CouchDB" 19 | ], 20 | "author": "vabarbosa", 21 | "license": "Apache-2.0", 22 | "bugs": { 23 | "url": "https://github.com/ibm-watson-data-lab/shopping-list-vanillajs-pouchdb/issues" 24 | }, 25 | "homepage": "https://github.com/ibm-watson-data-lab/shopping-list-vanillajs-pouchdb#readme", 26 | "devDependencies": { 27 | "chai": "^4.1.2", 28 | "karma": "^2.0.0", 29 | "karma-chai": "^0.1.0", 30 | "karma-chrome-launcher": "^2.2.0", 31 | "karma-mocha": "^1.3.0", 32 | "mocha": "^5.0.0", 33 | "standard": "^10.0.3" 34 | }, 35 | "dependencies": { 36 | "express": "^4.16.2", 37 | "metrics-tracker-client": "^0.2.3" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017 IBM Corp. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the 'License'); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an 'AS IS' BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | 'use strict' 18 | 19 | const express = require('express') 20 | const path = require('path') 21 | const app = express() 22 | const port = process.env.PORT || process.env.VCAP_APP_PORT || 8081 23 | 24 | app.use('/js', express.static(path.join(__dirname, 'js'))) 25 | app.use('/css', express.static(path.join(__dirname, 'css'))) 26 | app.use('/favicons', express.static(path.join(__dirname, 'favicons'))) 27 | app.use('/', express.static(path.join(__dirname))) 28 | 29 | app.get('/shopping-list-vanillajs-pouchdb/index.html', (req, res) => { 30 | res.sendFile(path.join(__dirname, '/index.html')) 31 | }) 32 | 33 | app.get('/shopping-list-vanillajs-pouchdb', (req, res) => { 34 | res.sendFile(path.join(__dirname, '/index.html')) 35 | }) 36 | 37 | app.get('/', (req, res) => { 38 | res.redirect('/shopping-list-vanillajs-pouchdb') 39 | }) 40 | 41 | app.listen(port, () => { 42 | console.log(`Server starting on ${port}`) 43 | }) 44 | 45 | require('metrics-tracker-client').track() 46 | -------------------------------------------------------------------------------- /tutorial/step-01/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | Shopping List | vanilla JavaScript | PouchDB 16 | 17 | 18 | 19 | 28 | 29 | 30 |
31 |
32 | 33 |
34 | 35 | 38 |
39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /tutorial/step-02/shoppinglist.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: #E1E2E1; 3 | box-shadow: none !important; 4 | overflow-x: hidden; 5 | } 6 | 7 | body * { 8 | box-shadow: none !important; 9 | } 10 | 11 | .primary-color { 12 | background-color: #52647A !important; 13 | } 14 | 15 | .primary-color * { 16 | color: #FFFFFF !important; 17 | } 18 | 19 | nav .nav-wrapper { 20 | padding-left: 10px; 21 | } 22 | 23 | main { 24 | display: flex; 25 | margin: 0 auto 75px auto; 26 | width: 200vw; 27 | } 28 | 29 | #shopping-lists, 30 | #shopping-list-items { 31 | display: inline-block; 32 | flex: 1; 33 | margin: 0; 34 | padding: 0; 35 | transition-property: transform; 36 | transition-duration: 0.5s; 37 | } 38 | 39 | #shopping-list-items { 40 | background-color: #FFFFFF; 41 | border: 0 none; 42 | vertical-align: top; 43 | } 44 | 45 | main #add-button { 46 | position: fixed; 47 | bottom: 25px; 48 | right: 25px; 49 | } 50 | 51 | main .card { 52 | margin: 30px; 53 | } 54 | 55 | .secondary-color { 56 | background-color: #FF6CA1 !important; 57 | } 58 | 59 | .secondary-color * { 60 | color: #000000 !important; 61 | } 62 | 63 | h5 { 64 | font-weight: 300; 65 | } 66 | 67 | .btn-flat { 68 | color: #7f91A9; 69 | } 70 | 71 | .btn-flat:hover { 72 | background-color: rgba(0,0,0,0.1); 73 | } 74 | 75 | input:focus:not([disabled]):not([readonly]) { 76 | border-bottom-color: #52647A !important; 77 | } 78 | 79 | body.shopping-list-add .list-bottom-sheet { 80 | z-index: 1007; 81 | display: block; 82 | bottom: 0px; 83 | opacity: 1; 84 | transition-property: bottom; 85 | transition-duration: .75s; 86 | } 87 | 88 | body.shopping-list-add .modal-overlay { 89 | display: block; 90 | opacity: 0.5; 91 | transition-property: all; 92 | transition-duration: 0.25s; 93 | z-index: 1006; 94 | } 95 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Contributing In General 2 | 3 | Our project welcomes external contributions! If you have an itch, please 4 | feel free to scratch it. 5 | 6 | To contribute code or documentation, you can submit a pull request. A 7 | good way to familiarize yourself with the codebase and contribution process is 8 | to look for and tackle low-hanging fruit in the issue tracker. Before embarking on 9 | a more ambitious contribution, please raise an issue for discussion. 10 | 11 | **We appreciate your effort, and want to avoid a situation where a contribution 12 | requires extensive rework (by you or by us), sits in the queue for a long time, 13 | or cannot be accepted at all!** 14 | 15 | ### Proposing new features 16 | 17 | If you would like to implement a new feature, please raise an issue before sending a pull 18 | request so the feature can be discussed. This is to avoid you spending your 19 | valuable time working on a feature that the project developers are not willing 20 | or able to accept into the code base. 21 | 22 | ### Fixing bugs 23 | 24 | If you would like to fix a bug, please raise an issue before sending a pull 25 | request so it can be discussed. If the fix is trivial or non controversial then 26 | this is not usually necessary. 27 | 28 | ### Merge approval 29 | 30 | Two project maintainers will need to review and approve your pull request before it 31 | is merged. They may request changes or ask questions so keep an eye on your pull 32 | request while review is in progress. Maintainers will expect that: 33 | 34 | - tests pass, and new features have accompanying tests (see the project `README` for more information about running tests) 35 | - documentation has been updated where appropriate 36 | - any coding standards have been followed (see the project `README` for more information about coding standards) 37 | 38 | Some or all of these checks may be automated so look out for immediate feedback from the 39 | CI system on your pull request. 40 | 41 | For more details, see the [MAINTAINERS](MAINTAINERS.md) page. 42 | 43 | ## Developer Setup 44 | 45 | Follow the "Run locally" steps in the `README` and you will have a local git repo 46 | and test environment, or use "Deploy to IBM Cloud" and you can use the IBM Cloud DevOps environment. 47 | 48 | -------------------------------------------------------------------------------- /tutorial/step-06/worker.js: -------------------------------------------------------------------------------- 1 | /* global self, caches, fetch */ 2 | 'use strict' 3 | 4 | var CACHE_NAME = 'v1' 5 | 6 | var urlstocache = [ 7 | '/', 8 | 'index.html', 9 | 'https://fonts.googleapis.com/icon?family=Material+Icons', 10 | 'https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700', 11 | 'https://cdnjs.cloudflare.com/ajax/libs/materialize/0.100.2/css/materialize.min.css', 12 | 'https://cdn.jsdelivr.net/npm/pouchdb@6.3.4/dist/pouchdb.min.js', 13 | 'https://cdn.jsdelivr.net/npm/pouchdb@6.3.4/dist/pouchdb.find.min.js', 14 | 'shoppinglist.css', 15 | 'shoppinglist.js', 16 | 'shoppinglist.model.js' 17 | ] 18 | 19 | var fromnetwork = function (request, cache) { 20 | return fetch(request).then(function (response) { 21 | if (request.url.indexOf('https://fonts.gstatic.com') === 0) { 22 | // cache fonts 23 | if (response.status < 400) { 24 | cache.put(request, response.clone()) 25 | } 26 | } 27 | return response 28 | }) 29 | } 30 | 31 | // install/cache page assets 32 | self.addEventListener('install', function (event) { 33 | event.waitUntil( 34 | caches.open(CACHE_NAME) 35 | .then(function (cache) { 36 | console.log('cache opened') 37 | return cache.addAll(urlstocache) 38 | }) 39 | ) 40 | }) 41 | 42 | // intercept page requests 43 | self.addEventListener('fetch', function (event) { 44 | console.log('fetch', event.request.url) 45 | event.respondWith( 46 | caches 47 | .open(CACHE_NAME) 48 | .then(function (cache) { 49 | // try from network first 50 | return fromnetwork(event.request, cache) 51 | .catch(function () { 52 | // network failed retrieve from cache 53 | return cache.match(event.request) 54 | }) 55 | }) 56 | ) 57 | }) 58 | 59 | // service worker activated, remove outdated cache 60 | self.addEventListener('activate', function (event) { 61 | console.log('worker activated') 62 | event.waitUntil( 63 | caches.keys().then(function (keys) { 64 | return Promise.all( 65 | keys.filter(function (key) { 66 | // filter old versioned keys 67 | return key !== CACHE_NAME 68 | }).map(function (key) { 69 | return caches.delete(key) 70 | }) 71 | ) 72 | }) 73 | ) 74 | }) 75 | -------------------------------------------------------------------------------- /tutorial/step-02/shoppinglist.model.js: -------------------------------------------------------------------------------- 1 | /* global PouchDB */ 2 | 3 | (function () { 4 | 'use strict' 5 | 6 | // PouchDB 7 | var db = null 8 | 9 | // Shopping List Schema 10 | // https://github.com/ibm-watson-data-lab/shopping-list#shopping-list-example 11 | var initListDoc = function (doc) { 12 | return { 13 | '_id': 'list:' + new Date().toISOString(), 14 | 'type': 'list', 15 | 'version': 1, 16 | 'title': doc.title, 17 | 'checked': !!doc.checked, 18 | 'place': { 19 | 'title': doc.place ? doc.place.title : '', 20 | 'license': doc.place ? doc.place.license : '', 21 | 'lat': doc.place ? doc.place.lat : null, 22 | 'lon': doc.place ? doc.place.lon : null, 23 | 'address': doc.place ? doc.place.address : {} 24 | }, 25 | 'createdAt': new Date().toISOString(), 26 | 'updatedAt': '' 27 | } 28 | } 29 | 30 | var model = function (callback) { 31 | db = new PouchDB('shopping') 32 | 33 | db.info(function (err, info) { 34 | if (err) { 35 | console.error(err) 36 | } else { 37 | console.log('model.info', info) 38 | } 39 | }) 40 | 41 | db.createIndex({ 42 | index: { fields: ['type'] } 43 | }, function (err, response) { 44 | if (typeof callback === 'function') { 45 | console.log('model ready!') 46 | callback(err, model) 47 | } 48 | }) 49 | } 50 | 51 | model.lists = function (callback) { 52 | db.find({ 53 | selector: { 54 | type: 'list' 55 | } 56 | }, function (err, response) { 57 | if (typeof callback === 'function') { 58 | var docs = response ? response.docs || response : response 59 | callback(err, docs) 60 | } 61 | }) 62 | } 63 | 64 | model.save = function (d, callback) { 65 | var doc = null 66 | 67 | if (d.type === 'list') { 68 | doc = initListDoc(d) 69 | } 70 | 71 | if (doc) { 72 | db.put(doc, function (err, response) { 73 | if (typeof callback === 'function') { 74 | callback(err, response) 75 | } 76 | }) 77 | } else { 78 | if (typeof callback === 'function') { 79 | callback(new Error('Missing or unsupport doc type'), null) 80 | } 81 | } 82 | } 83 | 84 | window.addEventListener('DOMContentLoaded', function () { 85 | window.shopper(model) 86 | }) 87 | }()) 88 | -------------------------------------------------------------------------------- /tutorial/step-03/shoppinglist.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: #E1E2E1; 3 | box-shadow: none !important; 4 | overflow-x: hidden; 5 | } 6 | 7 | body * { 8 | box-shadow: none !important; 9 | } 10 | 11 | .primary-color { 12 | background-color: #52647A !important; 13 | } 14 | 15 | .primary-color * { 16 | color: #FFFFFF !important; 17 | } 18 | 19 | nav .nav-wrapper { 20 | padding-left: 10px; 21 | } 22 | 23 | main { 24 | display: flex; 25 | margin: 0 auto 75px auto; 26 | width: 200vw; 27 | } 28 | 29 | #shopping-lists, 30 | #shopping-list-items { 31 | display: inline-block; 32 | flex: 1; 33 | margin: 0; 34 | padding: 0; 35 | transition-property: transform; 36 | transition-duration: 0.5s; 37 | } 38 | 39 | #shopping-list-items { 40 | background-color: #FFFFFF; 41 | border: 0 none; 42 | vertical-align: top; 43 | } 44 | 45 | main #add-button { 46 | position: fixed; 47 | bottom: 25px; 48 | right: 25px; 49 | } 50 | 51 | main .card { 52 | margin: 30px; 53 | } 54 | 55 | .secondary-color { 56 | background-color: #FF6CA1 !important; 57 | } 58 | 59 | .secondary-color * { 60 | color: #000000 !important; 61 | } 62 | 63 | h5 { 64 | font-weight: 300; 65 | } 66 | 67 | .btn-flat { 68 | color: #7f91A9; 69 | } 70 | 71 | .btn-flat:hover { 72 | background-color: rgba(0,0,0,0.1); 73 | } 74 | 75 | input:focus:not([disabled]):not([readonly]) { 76 | border-bottom-color: #52647A !important; 77 | } 78 | 79 | body.shopping-list-add .list-bottom-sheet { 80 | z-index: 1007; 81 | display: block; 82 | bottom: 0px; 83 | opacity: 1; 84 | transition-property: bottom; 85 | transition-duration: .75s; 86 | } 87 | 88 | body.shopping-list-add .modal-overlay { 89 | display: block; 90 | opacity: 0.5; 91 | transition-property: all; 92 | transition-duration: 0.25s; 93 | z-index: 1006; 94 | } 95 | 96 | .collapsible { 97 | overflow-y: hidden; 98 | margin: 0; 99 | max-height: calc(100vh - 65px); 100 | transition-property: all; 101 | transition-duration: .55s; 102 | } 103 | 104 | .collapsible.closed { 105 | max-height: 0; 106 | transition-property: all; 107 | transition-duration: .15s; 108 | } 109 | 110 | .card.collapsible { 111 | transition-duration: 2s; 112 | } 113 | 114 | .list-edit .card-action { 115 | border: 0 none; 116 | padding: 0; 117 | text-align: right; 118 | } 119 | 120 | .list-edit form { 121 | margin: 15px; 122 | } 123 | -------------------------------------------------------------------------------- /tutorial/step-07/worker.js: -------------------------------------------------------------------------------- 1 | /* global self, caches, fetch */ 2 | 'use strict' 3 | 4 | var CACHE_NAME = 'v1' 5 | 6 | var urlstocache = [ 7 | '/', 8 | 'index.html', 9 | 'favicons/android-chrome-192x192.png', 10 | 'favicons/android-chrome-512x512.png', 11 | 'favicons/apple-touch-icon.png', 12 | 'favicons/favicon-16x16.png', 13 | 'favicons/favicon-32x32.png', 14 | 'favicons/favicon.ico', 15 | 'favicons/mstile-150x150.png', 16 | 'favicons/safari-pinned-tab.svg', 17 | 'https://fonts.googleapis.com/icon?family=Material+Icons', 18 | 'https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700', 19 | 'https://cdnjs.cloudflare.com/ajax/libs/materialize/0.100.2/css/materialize.min.css', 20 | 'https://cdn.jsdelivr.net/npm/pouchdb@6.3.4/dist/pouchdb.min.js', 21 | 'https://cdn.jsdelivr.net/npm/pouchdb@6.3.4/dist/pouchdb.find.min.js', 22 | 'shoppinglist.css', 23 | 'shoppinglist.js', 24 | 'shoppinglist.model.js' 25 | ] 26 | 27 | var fromnetwork = function (request, cache) { 28 | return fetch(request).then(function (response) { 29 | if (request.url.indexOf('https://fonts.gstatic.com') === 0) { 30 | // cache fonts 31 | if (response.status < 400) { 32 | cache.put(request, response.clone()) 33 | } 34 | } 35 | return response 36 | }) 37 | } 38 | 39 | // install/cache page assets 40 | self.addEventListener('install', function (event) { 41 | event.waitUntil( 42 | caches.open(CACHE_NAME) 43 | .then(function (cache) { 44 | console.log('cache opened') 45 | return cache.addAll(urlstocache) 46 | }) 47 | ) 48 | }) 49 | 50 | // intercept page requests 51 | self.addEventListener('fetch', function (event) { 52 | console.log('fetch', event.request.url) 53 | event.respondWith( 54 | caches 55 | .open(CACHE_NAME) 56 | .then(function (cache) { 57 | // try from network first 58 | return fromnetwork(event.request, cache) 59 | .catch(function () { 60 | // network failed retrieve from cache 61 | return cache.match(event.request) 62 | }) 63 | }) 64 | ) 65 | }) 66 | 67 | // service worker activated, remove outdated cache 68 | self.addEventListener('activate', function (event) { 69 | console.log('worker activated') 70 | event.waitUntil( 71 | caches.keys().then(function (keys) { 72 | return Promise.all( 73 | keys.filter(function (key) { 74 | // filter old versioned keys 75 | return key !== CACHE_NAME 76 | }).map(function (key) { 77 | return caches.delete(key) 78 | }) 79 | ) 80 | }) 81 | ) 82 | }) 83 | -------------------------------------------------------------------------------- /tutorial/step-08/worker.js: -------------------------------------------------------------------------------- 1 | /* global self, caches, fetch */ 2 | 'use strict' 3 | 4 | var CACHE_NAME = 'v1' 5 | 6 | var urlstocache = [ 7 | '/', 8 | 'index.html', 9 | 'favicons/android-chrome-192x192.png', 10 | 'favicons/android-chrome-512x512.png', 11 | 'favicons/apple-touch-icon.png', 12 | 'favicons/favicon-16x16.png', 13 | 'favicons/favicon-32x32.png', 14 | 'favicons/favicon.ico', 15 | 'favicons/mstile-150x150.png', 16 | 'favicons/safari-pinned-tab.svg', 17 | 'https://fonts.googleapis.com/icon?family=Material+Icons', 18 | 'https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700', 19 | 'https://cdnjs.cloudflare.com/ajax/libs/materialize/0.100.2/css/materialize.min.css', 20 | 'https://cdn.jsdelivr.net/npm/pouchdb@6.3.4/dist/pouchdb.min.js', 21 | 'https://cdn.jsdelivr.net/npm/pouchdb@6.3.4/dist/pouchdb.find.min.js', 22 | 'shoppinglist.css', 23 | 'shoppinglist.js', 24 | 'shoppinglist.model.js' 25 | ] 26 | 27 | var fromnetwork = function (request, cache) { 28 | return fetch(request).then(function (response) { 29 | if (request.url.indexOf('https://fonts.gstatic.com') === 0) { 30 | // cache fonts 31 | if (response.status < 400) { 32 | cache.put(request, response.clone()) 33 | } 34 | } 35 | return response 36 | }) 37 | } 38 | 39 | // install/cache page assets 40 | self.addEventListener('install', function (event) { 41 | event.waitUntil( 42 | caches.open(CACHE_NAME) 43 | .then(function (cache) { 44 | console.log('cache opened') 45 | return cache.addAll(urlstocache) 46 | }) 47 | ) 48 | }) 49 | 50 | // intercept page requests 51 | self.addEventListener('fetch', function (event) { 52 | console.log('fetch', event.request.url) 53 | event.respondWith( 54 | caches 55 | .open(CACHE_NAME) 56 | .then(function (cache) { 57 | // try from network first 58 | return fromnetwork(event.request, cache) 59 | .catch(function () { 60 | // network failed retrieve from cache 61 | return cache.match(event.request) 62 | }) 63 | }) 64 | ) 65 | }) 66 | 67 | // service worker activated, remove outdated cache 68 | self.addEventListener('activate', function (event) { 69 | console.log('worker activated') 70 | event.waitUntil( 71 | caches.keys().then(function (keys) { 72 | return Promise.all( 73 | keys.filter(function (key) { 74 | // filter old versioned keys 75 | return key !== CACHE_NAME 76 | }).map(function (key) { 77 | return caches.delete(key) 78 | }) 79 | ) 80 | }) 81 | ) 82 | }) 83 | -------------------------------------------------------------------------------- /MAINTAINERS.md: -------------------------------------------------------------------------------- 1 | # Maintainers Guide 2 | 3 | This guide is intended for maintainers - anybody with commit access to one or 4 | more Code Pattern repositories. 5 | 6 | ## Methodology 7 | 8 | This repository does not have a traditional release management cycle, but 9 | should instead be maintained as as a useful, working, and polished reference at 10 | all times. While all work can therefore be focused on the master branch, the 11 | quality of this branch should never be compromised. 12 | 13 | The remainder of this document details how to merge pull requests to the 14 | repositories. 15 | 16 | ## Merge approval 17 | 18 | The project maintainers use LGTM (Looks Good To Me) in comments on the pull 19 | request to indicate acceptance prior to merging. A change requires LGTMs from 20 | two project maintainers. If the code is written by a maintainer, the change 21 | only requires one additional LGTM. 22 | 23 | ## Reviewing Pull Requests 24 | 25 | We recommend reviewing pull requests directly within GitHub. This allows a 26 | public commentary on changes, providing transparency for all users. When 27 | providing feedback be civil, courteous, and kind. Disagreement is fine, so long 28 | as the discourse is carried out politely. If we see a record of uncivil or 29 | abusive comments, we will revoke your commit privileges and invite you to leave 30 | the project. 31 | 32 | During your review, consider the following points: 33 | 34 | ### Does the change have positive impact? 35 | 36 | Some proposed changes may not represent a positive impact to the project. Ask 37 | whether or not the change will make understanding the code easier, or if it 38 | could simply be a personal preference on the part of the author (see 39 | [bikeshedding](https://en.wiktionary.org/wiki/bikeshedding)). 40 | 41 | Pull requests that do not have a clear positive impact should be closed without 42 | merging. 43 | 44 | ### Do the changes make sense? 45 | 46 | If you do not understand what the changes are or what they accomplish, ask the 47 | author for clarification. Ask the author to add comments and/or clarify test 48 | case names to make the intentions clear. 49 | 50 | At times, such clarification will reveal that the author may not be using the 51 | code correctly, or is unaware of features that accommodate their needs. If you 52 | feel this is the case, work up a code sample that would address the pull 53 | request for them, and feel free to close the pull request once they confirm. 54 | 55 | ### Does the change introduce a new feature? 56 | 57 | For any given pull request, ask yourself "is this a new feature?" If so, does 58 | the pull request (or associated issue) contain narrative indicating the need 59 | for the feature? If not, ask them to provide that information. 60 | 61 | Are new unit tests in place that test all new behaviors introduced? If not, do 62 | not merge the feature until they are! Is documentation in place for the new 63 | feature? (See the documentation guidelines). If not do not merge the feature 64 | until it is! Is the feature necessary for general use cases? Try and keep the 65 | scope of any given component narrow. If a proposed feature does not fit that 66 | scope, recommend to the user that they maintain the feature on their own, and 67 | close the request. You may also recommend that they see if the feature gains 68 | traction among other users, and suggest they re-submit when they can show such 69 | support. 70 | -------------------------------------------------------------------------------- /worker.js: -------------------------------------------------------------------------------- 1 | /* global self, caches, fetch */ 2 | 'use strict' 3 | 4 | // cache name 5 | // when changed invalidates previous caches 6 | var CACHE_NAME = 'shopping-list-vanillajs-pouchdb-0.0.5' 7 | 8 | // assets to be cached 9 | var urlstocache = [ 10 | '/shopping-list-vanillajs-pouchdb', 11 | '/shopping-list-vanillajs-pouchdb/', 12 | '/shopping-list-vanillajs-pouchdb/index.html', 13 | 'favicons/android-chrome-192x192.png', 14 | 'favicons/android-chrome-512x512.png', 15 | 'favicons/apple-touch-icon.png', 16 | 'favicons/favicon-16x16.png', 17 | 'favicons/favicon-32x32.png', 18 | 'favicons/favicon.ico', 19 | 'favicons/mstile-150x150.png', 20 | 'favicons/safari-pinned-tab.svg', 21 | 'https://fonts.googleapis.com/icon?family=Material+Icons', 22 | 'https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700', 23 | 'https://cdnjs.cloudflare.com/ajax/libs/materialize/0.100.2/css/materialize.min.css', 24 | 'https://cdnjs.cloudflare.com/ajax/libs/cuid/1.3.8/browser-cuid.min.js', 25 | 'https://cdn.jsdelivr.net/npm/pouchdb@6.3.4/dist/pouchdb.min.js', 26 | 'https://cdn.jsdelivr.net/npm/pouchdb@6.3.4/dist/pouchdb.find.min.js', 27 | 'css/shoppinglist.css', 28 | 'js/shoppinglist.js', 29 | 'js/shoppinglist.model.js' 30 | ] 31 | 32 | /** 33 | * Makes a network call for the request (and caches any fonts.gstatic.com requests) 34 | * 35 | * @param {Object} request 36 | * @param {Object} cache 37 | * Store fonts.gstatic.com responses 38 | * @return {Object} 39 | * The response from the network request 40 | */ 41 | var fromnetwork = function (request, cache) { 42 | return fetch(request).then(function (response) { 43 | if (request.url.indexOf('https://fonts.gstatic.com') === 0) { 44 | // cache fonts 45 | if (response.status < 400) { 46 | cache.put(request, response.clone()) 47 | } 48 | } 49 | return response 50 | }) 51 | } 52 | 53 | // service worker installed 54 | // cache page assets 55 | self.addEventListener('install', function (event) { 56 | event.waitUntil( 57 | caches.open(CACHE_NAME) 58 | .then(function (cache) { 59 | console.log('cache opened') 60 | return cache.addAll(urlstocache) 61 | }) 62 | ) 63 | }) 64 | 65 | // intercept page requests 66 | // looks for response in the cache before attempting network call 67 | self.addEventListener('fetch', function (event) { 68 | console.log('fetch', event.request.url) 69 | event.respondWith( 70 | caches 71 | .open(CACHE_NAME) 72 | .then(function (cache) { 73 | // try from network first 74 | return fromnetwork(event.request, cache) 75 | .catch(function () { 76 | // network failed retrieve from cache 77 | return cache.match(event.request) 78 | }) 79 | }) 80 | ) 81 | }) 82 | 83 | // service worker activated 84 | // remove outdated cache 85 | self.addEventListener('activate', function (event) { 86 | console.log('worker activated') 87 | event.waitUntil( 88 | caches.keys().then(function (keys) { 89 | return Promise.all( 90 | keys.filter(function (key) { 91 | // filter old versioned keys 92 | return key !== CACHE_NAME 93 | }).map(function (key) { 94 | return caches.delete(key) 95 | }) 96 | ) 97 | }) 98 | ) 99 | }) 100 | -------------------------------------------------------------------------------- /tutorial/step-02/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | Shopping List | vanilla JavaScript | PouchDB 16 | 17 | 18 | 19 | 28 | 29 | 30 |
31 |
32 | 33 |
34 | 35 | 38 | 39 | 40 | 43 |
44 | 45 | 46 | 63 | 64 | 65 | 66 | 67 | 68 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | -------------------------------------------------------------------------------- /tutorial/step-02/shoppinglist.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict' 3 | 4 | var model = null 5 | 6 | // make doc id friendlier for using as DOM node id 7 | var sanitize = function (id) { 8 | return id.replace(/[:.]/gi, '-') 9 | } 10 | 11 | // add docs to DOM node list 12 | var addToList = function (docs) { 13 | for (var i = 0; i < docs.length; i++) { 14 | var doc = docs[i] 15 | 16 | var isList = doc.type === 'list' || doc._id.indexOf('list:') === 0 17 | var shoppinglists = null 18 | 19 | if (isList) { 20 | shoppinglists = document.getElementById('shopping-lists') 21 | } else { 22 | continue 23 | } 24 | 25 | doc._sanitizedid = sanitize(doc._id) 26 | 27 | var template = document.getElementById('shopping-list-template').innerHTML 28 | template = template.replace(/\{\{(.+?)\}\}/g, function ($0, $1) { 29 | var fields = ($1).split('.') 30 | var value = doc 31 | while (fields.length) { 32 | if (value.hasOwnProperty(fields[0])) { 33 | value = value[fields.shift()] 34 | } else { 35 | value = null 36 | break 37 | } 38 | } 39 | return value || '' 40 | }) 41 | 42 | var listdiv = document.createElement('div') 43 | listdiv.id = doc._sanitizedid 44 | listdiv.className = 'card collapsible' 45 | listdiv.innerHTML = template 46 | 47 | var existingdiv = document.getElementById(doc._sanitizedid) 48 | if (existingdiv) { 49 | shoppinglists.replaceChild(listdiv, existingdiv) 50 | } else { 51 | shoppinglists.insertBefore(listdiv, shoppinglists.firstChild) 52 | } 53 | } 54 | } 55 | 56 | var shopper = function (themodel) { 57 | if (themodel) { 58 | themodel(function (err, response) { 59 | if (err) { 60 | console.error(err) 61 | } else { 62 | model = response 63 | model.lists(function (err, docs) { 64 | if (err) { 65 | console.error(err) 66 | } else { 67 | addToList(docs, true) 68 | } 69 | console.log('shopper ready!') 70 | }) 71 | } 72 | }) 73 | } 74 | return this 75 | } 76 | 77 | shopper.openadd = function () { 78 | var form = document.getElementById('shopping-list-add') 79 | form.reset() 80 | document.body.className += ' ' + form.id 81 | } 82 | 83 | shopper.closeadd = function () { 84 | document.body.className = document.body.className.replace('shopping-list-add', '').trim() 85 | } 86 | 87 | shopper.add = function (event) { 88 | var form = event.target 89 | var elements = form.elements 90 | var doc = {} 91 | 92 | if (!elements['title'].value) { 93 | console.error('title required') 94 | } else { 95 | for (var i = 0; i < elements.length; i++) { 96 | if (elements[i].tagName.toLowerCase() !== 'button') { 97 | doc[elements[i].name] = elements[i].value 98 | } 99 | } 100 | 101 | model.save(doc, function (err, updated) { 102 | if (err) { 103 | console.error(err) 104 | } else { 105 | doc._id = doc._id || updated._id || updated.id 106 | addToList([doc]) 107 | shopper.closeadd() 108 | } 109 | }) 110 | } 111 | } 112 | 113 | window.shopper = shopper 114 | }()) 115 | -------------------------------------------------------------------------------- /tutorial/step-03/shoppinglist.model.js: -------------------------------------------------------------------------------- 1 | /* global PouchDB */ 2 | 3 | (function () { 4 | 'use strict' 5 | 6 | // PouchDB 7 | var db = null 8 | 9 | // Shopping List Schema 10 | // https://github.com/ibm-watson-data-lab/shopping-list#shopping-list-example 11 | var initListDoc = function (doc) { 12 | return { 13 | '_id': 'list:' + new Date().toISOString(), 14 | 'type': 'list', 15 | 'version': 1, 16 | 'title': doc.title, 17 | 'checked': !!doc.checked, 18 | 'place': { 19 | 'title': doc.place ? doc.place.title : '', 20 | 'license': doc.place ? doc.place.license : '', 21 | 'lat': doc.place ? doc.place.lat : null, 22 | 'lon': doc.place ? doc.place.lon : null, 23 | 'address': doc.place ? doc.place.address : {} 24 | }, 25 | 'createdAt': new Date().toISOString(), 26 | 'updatedAt': '' 27 | } 28 | } 29 | 30 | var model = function (callback) { 31 | db = new PouchDB('shopping') 32 | 33 | db.info(function (err, info) { 34 | if (err) { 35 | console.error(err) 36 | } else { 37 | console.log('model.info', info) 38 | } 39 | }) 40 | 41 | db.createIndex({ 42 | index: { fields: ['type'] } 43 | }, function (err, response) { 44 | if (typeof callback === 'function') { 45 | console.log('model ready!') 46 | callback(err, model) 47 | } 48 | }) 49 | } 50 | 51 | model.lists = function (callback) { 52 | db.find({ 53 | selector: { 54 | type: 'list' 55 | } 56 | }, function (err, response) { 57 | if (typeof callback === 'function') { 58 | var docs = response ? response.docs || response : response 59 | callback(err, docs) 60 | } 61 | }) 62 | } 63 | 64 | model.save = function (d, callback) { 65 | var doc = null 66 | if (d._id) { 67 | doc = d 68 | doc['updatedAt'] = new Date().toISOString() 69 | } else if (d.type === 'list') { 70 | doc = initListDoc(d) 71 | } 72 | 73 | if (doc) { 74 | db.put(doc, function (err, response) { 75 | if (typeof callback === 'function') { 76 | callback(err, response) 77 | } 78 | }) 79 | } else { 80 | if (typeof callback === 'function') { 81 | callback(new Error('Missing or unsupport doc type'), null) 82 | } 83 | } 84 | } 85 | 86 | model.get = function (id, callback) { 87 | db.get(id, function (err, doc) { 88 | if (typeof callback === 'function') { 89 | callback(err, doc) 90 | } 91 | }) 92 | } 93 | 94 | model.remove = function (id, callback) { 95 | function deleteRev (rev) { 96 | db.remove(id, rev, function (err, response) { 97 | if (typeof callback === 'function') { 98 | callback(err, response) 99 | } 100 | }) 101 | } 102 | 103 | if (id) { 104 | db.get(id, function (err, doc) { 105 | if (err) { 106 | if (typeof callback === 'function') { 107 | callback(err, null) 108 | } 109 | } else { 110 | deleteRev(doc._rev) 111 | } 112 | }) 113 | } else { 114 | if (typeof callback === 'function') { 115 | callback(new Error('Missing doc id'), null) 116 | } 117 | } 118 | } 119 | 120 | window.addEventListener('DOMContentLoaded', function () { 121 | window.shopper(model) 122 | }) 123 | }()) 124 | -------------------------------------------------------------------------------- /tutorial/step-04/shoppinglist.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: #E1E2E1; 3 | box-shadow: none !important; 4 | overflow-x: hidden; 5 | } 6 | 7 | body * { 8 | box-shadow: none !important; 9 | } 10 | 11 | .primary-color { 12 | background-color: #52647A !important; 13 | } 14 | 15 | .primary-color * { 16 | color: #FFFFFF !important; 17 | } 18 | 19 | nav .nav-wrapper { 20 | padding-left: 10px; 21 | } 22 | 23 | main { 24 | display: flex; 25 | margin: 0 auto 75px auto; 26 | width: 200vw; 27 | } 28 | 29 | #shopping-lists, 30 | #shopping-list-items { 31 | display: inline-block; 32 | flex: 1; 33 | margin: 0; 34 | padding: 0; 35 | transition-property: transform; 36 | transition-duration: 0.5s; 37 | } 38 | 39 | #shopping-list-items { 40 | background-color: #FFFFFF; 41 | border: 0 none; 42 | vertical-align: top; 43 | } 44 | 45 | main #add-button { 46 | position: fixed; 47 | bottom: 25px; 48 | right: 25px; 49 | } 50 | 51 | main .card { 52 | margin: 30px; 53 | } 54 | 55 | .secondary-color { 56 | background-color: #FF6CA1 !important; 57 | } 58 | 59 | .secondary-color * { 60 | color: #000000 !important; 61 | } 62 | 63 | h5 { 64 | font-weight: 300; 65 | } 66 | 67 | .btn-flat { 68 | color: #7f91A9; 69 | } 70 | 71 | .btn-flat:hover { 72 | background-color: rgba(0,0,0,0.1); 73 | } 74 | 75 | input:focus:not([disabled]):not([readonly]) { 76 | border-bottom-color: #52647A !important; 77 | } 78 | 79 | body.shopping-list-add .list-bottom-sheet, 80 | body.shopping-list-item-add .item-bottom-sheet { 81 | z-index: 1007; 82 | display: block; 83 | bottom: 0px; 84 | opacity: 1; 85 | transition-property: bottom; 86 | transition-duration: .75s; 87 | } 88 | 89 | body.shopping-list-add .modal-overlay, 90 | body.shopping-list-item-add .modal-overlay { 91 | display: block; 92 | opacity: 0.5; 93 | transition-property: all; 94 | transition-duration: 0.25s; 95 | z-index: 1006; 96 | } 97 | 98 | .collapsible { 99 | overflow-y: hidden; 100 | margin: 0; 101 | max-height: calc(100vh - 65px); 102 | transition-property: all; 103 | transition-duration: .55s; 104 | } 105 | 106 | .collapsible.closed { 107 | max-height: 0; 108 | transition-property: all; 109 | transition-duration: .15s; 110 | } 111 | 112 | .card.collapsible { 113 | transition-duration: 2s; 114 | } 115 | 116 | .list-edit .card-action, 117 | .item-edit .card-action { 118 | border: 0 none; 119 | padding: 0; 120 | text-align: right; 121 | } 122 | 123 | .item-edit form, 124 | .list-edit form { 125 | margin: 15px; 126 | } 127 | 128 | #shopping-list-items .card, 129 | #shopping-list-items .card .collapsible { 130 | border: 0 none; 131 | box-shadow: initial; 132 | } 133 | 134 | #shopping-list-items .collection-item { 135 | border-bottom: 1px solid #DDDDDD; 136 | padding: 30px; 137 | margin: 0 35px; 138 | } 139 | 140 | #shopping-list-items .card label { 141 | font-size: 20px; 142 | font-weight: 300; 143 | line-height: initial; 144 | } 145 | 146 | .collection-item input:checked ~ label { 147 | text-decoration: line-through; 148 | } 149 | 150 | .collection-item input:checked ~ button { 151 | visibility: hidden; 152 | } 153 | 154 | .collection-item input:not(:checked) ~ label { 155 | color: #212121; 156 | } 157 | 158 | body[data-list-id] { 159 | background-color: #FFFFFF !important; 160 | } 161 | 162 | body[data-list-id] #shopping-lists, 163 | body[data-list-id] #shopping-list-items { 164 | transform: translate(-100vw); 165 | } 166 | 167 | .goback { 168 | visibility: hidden; 169 | } 170 | 171 | body[data-list-id] .goback { 172 | visibility: initial; 173 | } 174 | -------------------------------------------------------------------------------- /tutorial/step-05/shoppinglist.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: #E1E2E1; 3 | box-shadow: none !important; 4 | overflow-x: hidden; 5 | } 6 | 7 | body * { 8 | box-shadow: none !important; 9 | } 10 | 11 | .primary-color { 12 | background-color: #52647A !important; 13 | } 14 | 15 | .primary-color * { 16 | color: #FFFFFF !important; 17 | } 18 | 19 | nav .nav-wrapper { 20 | padding-left: 10px; 21 | } 22 | 23 | main { 24 | display: flex; 25 | margin: 0 auto 75px auto; 26 | width: 200vw; 27 | } 28 | 29 | #shopping-lists, 30 | #shopping-list-items { 31 | display: inline-block; 32 | flex: 1; 33 | margin: 0; 34 | padding: 0; 35 | transition-property: transform; 36 | transition-duration: 0.5s; 37 | } 38 | 39 | #shopping-list-items { 40 | background-color: #FFFFFF; 41 | border: 0 none; 42 | vertical-align: top; 43 | } 44 | 45 | main #add-button { 46 | position: fixed; 47 | bottom: 25px; 48 | right: 25px; 49 | } 50 | 51 | main .card { 52 | margin: 30px; 53 | } 54 | 55 | .secondary-color { 56 | background-color: #FF6CA1 !important; 57 | } 58 | 59 | .secondary-color * { 60 | color: #000000 !important; 61 | } 62 | 63 | h5 { 64 | font-weight: 300; 65 | } 66 | 67 | .btn-flat { 68 | color: #7f91A9; 69 | } 70 | 71 | .btn-flat:hover { 72 | background-color: rgba(0,0,0,0.1); 73 | } 74 | 75 | input:focus:not([disabled]):not([readonly]) { 76 | border-bottom-color: #52647A !important; 77 | } 78 | 79 | body.shopping-list-add .list-bottom-sheet, 80 | body.shopping-list-item-add .item-bottom-sheet { 81 | z-index: 1007; 82 | display: block; 83 | bottom: 0px; 84 | opacity: 1; 85 | transition-property: bottom; 86 | transition-duration: .75s; 87 | } 88 | 89 | body.shopping-list-add .modal-overlay, 90 | body.shopping-list-item-add .modal-overlay { 91 | display: block; 92 | opacity: 0.5; 93 | transition-property: all; 94 | transition-duration: 0.25s; 95 | z-index: 1006; 96 | } 97 | 98 | .collapsible { 99 | overflow-y: hidden; 100 | margin: 0; 101 | max-height: calc(100vh - 65px); 102 | transition-property: all; 103 | transition-duration: .55s; 104 | } 105 | 106 | .collapsible.closed { 107 | max-height: 0; 108 | transition-property: all; 109 | transition-duration: .15s; 110 | } 111 | 112 | .card.collapsible { 113 | transition-duration: 2s; 114 | } 115 | 116 | .list-edit .card-action, 117 | .item-edit .card-action { 118 | border: 0 none; 119 | padding: 0; 120 | text-align: right; 121 | } 122 | 123 | .item-edit form, 124 | .list-edit form { 125 | margin: 15px; 126 | } 127 | 128 | #shopping-list-items .card, 129 | #shopping-list-items .card .collapsible { 130 | border: 0 none; 131 | box-shadow: initial; 132 | } 133 | 134 | #shopping-list-items .collection-item { 135 | border-bottom: 1px solid #DDDDDD; 136 | padding: 30px; 137 | margin: 0 35px; 138 | } 139 | 140 | #shopping-list-items .card label { 141 | font-size: 20px; 142 | font-weight: 300; 143 | line-height: initial; 144 | } 145 | 146 | .collection-item input:checked ~ label { 147 | text-decoration: line-through; 148 | } 149 | 150 | .collection-item input:checked ~ button { 151 | visibility: hidden; 152 | } 153 | 154 | .collection-item input:not(:checked) ~ label { 155 | color: #212121; 156 | } 157 | 158 | body[data-list-id] { 159 | background-color: #FFFFFF !important; 160 | } 161 | 162 | body[data-list-id] #shopping-lists, 163 | body[data-list-id] #shopping-list-items { 164 | transform: translate(-100vw); 165 | } 166 | 167 | .goback { 168 | visibility: hidden; 169 | } 170 | 171 | body[data-list-id] .goback { 172 | visibility: initial; 173 | } 174 | 175 | [type="checkbox"]:disabled:not(:checked) + label::before { 176 | display: none; 177 | } 178 | -------------------------------------------------------------------------------- /tutorial/step-06/shoppinglist.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: #E1E2E1; 3 | box-shadow: none !important; 4 | overflow-x: hidden; 5 | } 6 | 7 | body * { 8 | box-shadow: none !important; 9 | } 10 | 11 | .primary-color { 12 | background-color: #52647A !important; 13 | } 14 | 15 | .primary-color * { 16 | color: #FFFFFF !important; 17 | } 18 | 19 | nav .nav-wrapper { 20 | padding-left: 10px; 21 | } 22 | 23 | main { 24 | display: flex; 25 | margin: 0 auto 75px auto; 26 | width: 200vw; 27 | } 28 | 29 | #shopping-lists, 30 | #shopping-list-items { 31 | display: inline-block; 32 | flex: 1; 33 | margin: 0; 34 | padding: 0; 35 | transition-property: transform; 36 | transition-duration: 0.5s; 37 | } 38 | 39 | #shopping-list-items { 40 | background-color: #FFFFFF; 41 | border: 0 none; 42 | vertical-align: top; 43 | } 44 | 45 | main #add-button { 46 | position: fixed; 47 | bottom: 25px; 48 | right: 25px; 49 | } 50 | 51 | main .card { 52 | margin: 30px; 53 | } 54 | 55 | .secondary-color { 56 | background-color: #FF6CA1 !important; 57 | } 58 | 59 | .secondary-color * { 60 | color: #000000 !important; 61 | } 62 | 63 | h5 { 64 | font-weight: 300; 65 | } 66 | 67 | .btn-flat { 68 | color: #7f91A9; 69 | } 70 | 71 | .btn-flat:hover { 72 | background-color: rgba(0,0,0,0.1); 73 | } 74 | 75 | input:focus:not([disabled]):not([readonly]) { 76 | border-bottom-color: #52647A !important; 77 | } 78 | 79 | body.shopping-list-add .list-bottom-sheet, 80 | body.shopping-list-item-add .item-bottom-sheet { 81 | z-index: 1007; 82 | display: block; 83 | bottom: 0px; 84 | opacity: 1; 85 | transition-property: bottom; 86 | transition-duration: .75s; 87 | } 88 | 89 | body.shopping-list-add .modal-overlay, 90 | body.shopping-list-item-add .modal-overlay { 91 | display: block; 92 | opacity: 0.5; 93 | transition-property: all; 94 | transition-duration: 0.25s; 95 | z-index: 1006; 96 | } 97 | 98 | .collapsible { 99 | overflow-y: hidden; 100 | margin: 0; 101 | max-height: calc(100vh - 65px); 102 | transition-property: all; 103 | transition-duration: .55s; 104 | } 105 | 106 | .collapsible.closed { 107 | max-height: 0; 108 | transition-property: all; 109 | transition-duration: .15s; 110 | } 111 | 112 | .card.collapsible { 113 | transition-duration: 2s; 114 | } 115 | 116 | .list-edit .card-action, 117 | .item-edit .card-action { 118 | border: 0 none; 119 | padding: 0; 120 | text-align: right; 121 | } 122 | 123 | .item-edit form, 124 | .list-edit form { 125 | margin: 15px; 126 | } 127 | 128 | #shopping-list-items .card, 129 | #shopping-list-items .card .collapsible { 130 | border: 0 none; 131 | box-shadow: initial; 132 | } 133 | 134 | #shopping-list-items .collection-item { 135 | border-bottom: 1px solid #DDDDDD; 136 | padding: 30px; 137 | margin: 0 35px; 138 | } 139 | 140 | #shopping-list-items .card label { 141 | font-size: 20px; 142 | font-weight: 300; 143 | line-height: initial; 144 | } 145 | 146 | .collection-item input:checked ~ label { 147 | text-decoration: line-through; 148 | } 149 | 150 | .collection-item input:checked ~ button { 151 | visibility: hidden; 152 | } 153 | 154 | .collection-item input:not(:checked) ~ label { 155 | color: #212121; 156 | } 157 | 158 | body[data-list-id] { 159 | background-color: #FFFFFF !important; 160 | } 161 | 162 | body[data-list-id] #shopping-lists, 163 | body[data-list-id] #shopping-list-items { 164 | transform: translate(-100vw); 165 | } 166 | 167 | .goback { 168 | visibility: hidden; 169 | } 170 | 171 | body[data-list-id] .goback { 172 | visibility: initial; 173 | } 174 | 175 | [type="checkbox"]:disabled:not(:checked) + label::before { 176 | display: none; 177 | } 178 | -------------------------------------------------------------------------------- /tutorial/step-07/shoppinglist.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: #E1E2E1; 3 | box-shadow: none !important; 4 | overflow-x: hidden; 5 | } 6 | 7 | body * { 8 | box-shadow: none !important; 9 | } 10 | 11 | .primary-color { 12 | background-color: #52647A !important; 13 | } 14 | 15 | .primary-color * { 16 | color: #FFFFFF !important; 17 | } 18 | 19 | nav .nav-wrapper { 20 | padding-left: 10px; 21 | } 22 | 23 | main { 24 | display: flex; 25 | margin: 0 auto 75px auto; 26 | width: 200vw; 27 | } 28 | 29 | #shopping-lists, 30 | #shopping-list-items { 31 | display: inline-block; 32 | flex: 1; 33 | margin: 0; 34 | padding: 0; 35 | transition-property: transform; 36 | transition-duration: 0.5s; 37 | } 38 | 39 | #shopping-list-items { 40 | background-color: #FFFFFF; 41 | border: 0 none; 42 | vertical-align: top; 43 | } 44 | 45 | main #add-button { 46 | position: fixed; 47 | bottom: 25px; 48 | right: 25px; 49 | } 50 | 51 | main .card { 52 | margin: 30px; 53 | } 54 | 55 | .secondary-color { 56 | background-color: #FF6CA1 !important; 57 | } 58 | 59 | .secondary-color * { 60 | color: #000000 !important; 61 | } 62 | 63 | h5 { 64 | font-weight: 300; 65 | } 66 | 67 | .btn-flat { 68 | color: #7f91A9; 69 | } 70 | 71 | .btn-flat:hover { 72 | background-color: rgba(0,0,0,0.1); 73 | } 74 | 75 | input:focus:not([disabled]):not([readonly]) { 76 | border-bottom-color: #52647A !important; 77 | } 78 | 79 | body.shopping-list-add .list-bottom-sheet, 80 | body.shopping-list-item-add .item-bottom-sheet { 81 | z-index: 1007; 82 | display: block; 83 | bottom: 0px; 84 | opacity: 1; 85 | transition-property: bottom; 86 | transition-duration: .75s; 87 | } 88 | 89 | body.shopping-list-add .modal-overlay, 90 | body.shopping-list-item-add .modal-overlay { 91 | display: block; 92 | opacity: 0.5; 93 | transition-property: all; 94 | transition-duration: 0.25s; 95 | z-index: 1006; 96 | } 97 | 98 | .collapsible { 99 | overflow-y: hidden; 100 | margin: 0; 101 | max-height: calc(100vh - 65px); 102 | transition-property: all; 103 | transition-duration: .55s; 104 | } 105 | 106 | .collapsible.closed { 107 | max-height: 0; 108 | transition-property: all; 109 | transition-duration: .15s; 110 | } 111 | 112 | .card.collapsible { 113 | transition-duration: 2s; 114 | } 115 | 116 | .list-edit .card-action, 117 | .item-edit .card-action { 118 | border: 0 none; 119 | padding: 0; 120 | text-align: right; 121 | } 122 | 123 | .item-edit form, 124 | .list-edit form { 125 | margin: 15px; 126 | } 127 | 128 | #shopping-list-items .card, 129 | #shopping-list-items .card .collapsible { 130 | border: 0 none; 131 | box-shadow: initial; 132 | } 133 | 134 | #shopping-list-items .collection-item { 135 | border-bottom: 1px solid #DDDDDD; 136 | padding: 30px; 137 | margin: 0 35px; 138 | } 139 | 140 | #shopping-list-items .card label { 141 | font-size: 20px; 142 | font-weight: 300; 143 | line-height: initial; 144 | } 145 | 146 | .collection-item input:checked ~ label { 147 | text-decoration: line-through; 148 | } 149 | 150 | .collection-item input:checked ~ button { 151 | visibility: hidden; 152 | } 153 | 154 | .collection-item input:not(:checked) ~ label { 155 | color: #212121; 156 | } 157 | 158 | body[data-list-id] { 159 | background-color: #FFFFFF !important; 160 | } 161 | 162 | body[data-list-id] #shopping-lists, 163 | body[data-list-id] #shopping-list-items { 164 | transform: translate(-100vw); 165 | } 166 | 167 | .goback { 168 | visibility: hidden; 169 | } 170 | 171 | body[data-list-id] .goback { 172 | visibility: initial; 173 | } 174 | 175 | [type="checkbox"]:disabled:not(:checked) + label::before { 176 | display: none; 177 | } 178 | -------------------------------------------------------------------------------- /favicons/safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | Created by potrace 1.11, written by Peter Selinger 2001-2013 9 | 10 | 12 | 14 | 26 | 32 | 36 | 41 | 45 | 50 | 53 | 59 | 65 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /tutorial/step-07/favicons/safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | Created by potrace 1.11, written by Peter Selinger 2001-2013 9 | 10 | 12 | 14 | 26 | 32 | 36 | 41 | 45 | 50 | 53 | 59 | 65 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /tutorial/step-08/favicons/safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | Created by potrace 1.11, written by Peter Selinger 2001-2013 9 | 10 | 12 | 14 | 26 | 32 | 36 | 41 | 45 | 50 | 53 | 59 | 65 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /tutorial/step-03/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | Shopping List | vanilla JavaScript | PouchDB 16 | 17 | 18 | 19 | 28 | 29 | 30 |
31 |
32 | 33 |
34 | 35 | 38 | 39 | 40 | 43 |
44 | 45 | 46 | 63 | 64 | 65 | 66 | 67 | 68 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | -------------------------------------------------------------------------------- /tutorial/step-03/shoppinglist.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict' 3 | 4 | var model = null 5 | 6 | // make doc id friendlier for using as DOM node id 7 | var sanitize = function (id) { 8 | return id.replace(/[:.]/gi, '-') 9 | } 10 | 11 | // add docs to DOM node list 12 | var addToList = function (docs) { 13 | for (var i = 0; i < docs.length; i++) { 14 | var doc = docs[i] 15 | 16 | var isList = doc.type === 'list' || doc._id.indexOf('list:') === 0 17 | var shoppinglists = null 18 | 19 | if (isList) { 20 | shoppinglists = document.getElementById('shopping-lists') 21 | } else { 22 | continue 23 | } 24 | 25 | doc._sanitizedid = sanitize(doc._id) 26 | 27 | var template = document.getElementById('shopping-list-template').innerHTML 28 | template = template.replace(/\{\{(.+?)\}\}/g, function ($0, $1) { 29 | var fields = ($1).split('.') 30 | var value = doc 31 | while (fields.length) { 32 | if (value.hasOwnProperty(fields[0])) { 33 | value = value[fields.shift()] 34 | } else { 35 | value = null 36 | break 37 | } 38 | } 39 | return value || '' 40 | }) 41 | 42 | var listdiv = document.createElement('div') 43 | listdiv.id = doc._sanitizedid 44 | listdiv.className = 'card collapsible' 45 | listdiv.innerHTML = template 46 | 47 | var existingdiv = document.getElementById(doc._sanitizedid) 48 | if (existingdiv) { 49 | shoppinglists.replaceChild(listdiv, existingdiv) 50 | } else { 51 | shoppinglists.insertBefore(listdiv, shoppinglists.firstChild) 52 | } 53 | } 54 | } 55 | 56 | // remove from DOM node list 57 | var removeFromList = function (id) { 58 | var list = document.getElementById(sanitize(id)) 59 | shopper.toggle(list) 60 | list.parentElement.removeChild(list) 61 | } 62 | 63 | var shopper = function (themodel) { 64 | if (themodel) { 65 | themodel(function (err, response) { 66 | if (err) { 67 | console.error(err) 68 | } else { 69 | model = response 70 | model.lists(function (err, docs) { 71 | if (err) { 72 | console.error(err) 73 | } else { 74 | addToList(docs, true) 75 | } 76 | console.log('shopper ready!') 77 | }) 78 | } 79 | }) 80 | } 81 | return this 82 | } 83 | 84 | shopper.openadd = function () { 85 | var form = document.getElementById('shopping-list-add') 86 | form.reset() 87 | document.body.className += ' ' + form.id 88 | } 89 | 90 | shopper.closeadd = function () { 91 | document.body.className = document.body.className.replace('shopping-list-add', '').trim() 92 | } 93 | 94 | shopper.add = function (event) { 95 | var form = event.target 96 | var elements = form.elements 97 | var doc = {} 98 | 99 | if (!elements['title'].value) { 100 | console.error('title required') 101 | } else { 102 | for (var i = 0; i < elements.length; i++) { 103 | if (elements[i].tagName.toLowerCase() !== 'button') { 104 | doc[elements[i].name] = elements[i].value 105 | } 106 | } 107 | 108 | model.save(doc, function (err, updated) { 109 | if (err) { 110 | console.error(err) 111 | } else { 112 | doc._id = doc._id || updated._id || updated.id 113 | addToList([doc]) 114 | shopper.closeadd() 115 | } 116 | }) 117 | } 118 | } 119 | 120 | shopper.remove = function (id) { 121 | model.remove(id, function (err, response) { 122 | if (err) { 123 | console.log(err) 124 | } else { 125 | removeFromList(id) 126 | } 127 | }) 128 | } 129 | 130 | shopper.update = function (id) { 131 | var elements = document.getElementById('form-' + sanitize(id)).elements 132 | if (!elements['title'].value) { 133 | console.error('title required') 134 | } else { 135 | model.get(id, function (err, doc) { 136 | if (err) { 137 | console.log(err) 138 | } else { 139 | doc.title = elements['title'].value 140 | model.save(doc, function (err, updated) { 141 | if (err) { 142 | console.error(err) 143 | } else { 144 | addToList([doc]) 145 | } 146 | }) 147 | } 148 | }) 149 | } 150 | } 151 | 152 | shopper.toggle = function (node, event) { 153 | if (event) { 154 | event.stopPropagation() 155 | } 156 | if (typeof node === 'string') { 157 | var nodes = document.querySelectorAll('#' + node + ' .collapsible') 158 | for (var i = 0; i < nodes.length; i++) { 159 | if (nodes[i].classList) { 160 | nodes[i].classList.toggle('closed') 161 | } 162 | } 163 | } else { 164 | node.classList.toggle('closed') 165 | } 166 | } 167 | 168 | window.shopper = shopper 169 | }()) 170 | -------------------------------------------------------------------------------- /tutorial/step-04/shoppinglist.model.js: -------------------------------------------------------------------------------- 1 | /* global PouchDB */ 2 | 3 | (function () { 4 | 'use strict' 5 | 6 | // PouchDB 7 | var db = null 8 | 9 | // Shopping List Schema 10 | // https://github.com/ibm-watson-data-lab/shopping-list#shopping-list-example 11 | var initListDoc = function (doc) { 12 | return { 13 | '_id': 'list:' + new Date().toISOString(), 14 | 'type': 'list', 15 | 'version': 1, 16 | 'title': doc.title, 17 | 'checked': !!doc.checked, 18 | 'place': { 19 | 'title': doc.place ? doc.place.title : '', 20 | 'license': doc.place ? doc.place.license : '', 21 | 'lat': doc.place ? doc.place.lat : null, 22 | 'lon': doc.place ? doc.place.lon : null, 23 | 'address': doc.place ? doc.place.address : {} 24 | }, 25 | 'createdAt': new Date().toISOString(), 26 | 'updatedAt': '' 27 | } 28 | } 29 | 30 | // Shopping List Item Schema 31 | // https://github.com/ibm-watson-data-lab/shopping-list#shopping-list-item-example 32 | var initItemDoc = function (doc, listid) { 33 | return { 34 | '_id': 'item:' + new Date().toISOString(), 35 | 'type': 'item', 36 | 'version': 1, 37 | 'list': doc.list || listid, 38 | 'title': doc.title, 39 | 'checked': !!doc.checked, 40 | 'createdAt': new Date().toISOString(), 41 | 'updatedAt': '' 42 | } 43 | } 44 | 45 | var model = function (callback) { 46 | db = new PouchDB('shopping') 47 | 48 | db.info(function (err, info) { 49 | if (err) { 50 | console.error(err) 51 | } else { 52 | console.log('model.info', info) 53 | } 54 | }) 55 | 56 | db.createIndex({ 57 | index: { fields: ['type'] } 58 | }, function (err, response) { 59 | if (typeof callback === 'function') { 60 | console.log('model ready!') 61 | callback(err, model) 62 | } 63 | }) 64 | } 65 | 66 | model.lists = function (callback) { 67 | db.find({ 68 | selector: { 69 | type: 'list' 70 | } 71 | }, function (err, response) { 72 | if (typeof callback === 'function') { 73 | var docs = response ? response.docs || response : response 74 | callback(err, docs) 75 | } 76 | }) 77 | } 78 | 79 | model.save = function (d, callback) { 80 | var doc = null 81 | if (d._id) { 82 | doc = d 83 | doc['updatedAt'] = new Date().toISOString() 84 | } else if (d.type === 'list') { 85 | doc = initListDoc(d) 86 | } else if (d.type === 'item') { 87 | doc = initItemDoc(d) 88 | } 89 | 90 | if (doc) { 91 | db.put(doc, function (err, response) { 92 | if (typeof callback === 'function') { 93 | callback(err, response) 94 | } 95 | }) 96 | } else { 97 | if (typeof callback === 'function') { 98 | callback(new Error('Missing or unsupport doc type'), null) 99 | } 100 | } 101 | } 102 | 103 | model.get = function (id, callback) { 104 | db.get(id, function (err, doc) { 105 | if (typeof callback === 'function') { 106 | callback(err, doc) 107 | } 108 | }) 109 | } 110 | 111 | model.remove = function (id, callback) { 112 | function deleteRev (rev) { 113 | db.remove(id, rev, function (err, response) { 114 | if (typeof callback === 'function') { 115 | callback(err, response) 116 | } 117 | }) 118 | } 119 | 120 | if (id) { 121 | db.get(id, function (err, doc) { 122 | if (err) { 123 | if (typeof callback === 'function') { 124 | callback(err, null) 125 | } 126 | } else if (doc.type === 'list') { 127 | // remove all children 128 | model.items(doc._id, function (err, response) { 129 | if (err) { 130 | console.error(err) 131 | deleteRev(doc._rev) 132 | } else { 133 | var items = response ? response.docs || response : response 134 | if (items && items.length) { 135 | var markfordeletion = items.map(function (item) { 136 | item._deleted = true 137 | return item 138 | }) 139 | db.bulkDocs(markfordeletion, function (err, response) { 140 | if (err) { 141 | console.error(err) 142 | } 143 | deleteRev(doc._rev) 144 | }) 145 | } else { 146 | deleteRev(doc._rev) 147 | } 148 | } 149 | }) 150 | } else { 151 | deleteRev(doc._rev) 152 | } 153 | }) 154 | } else { 155 | if (typeof callback === 'function') { 156 | callback(new Error('Missing doc id'), null) 157 | } 158 | } 159 | } 160 | 161 | model.items = function (listid, callback) { 162 | db.find({ 163 | selector: { 164 | type: 'item', 165 | list: listid 166 | } 167 | }, function (err, response) { 168 | if (typeof callback === 'function') { 169 | var docs = response ? response.docs || response : response 170 | callback(err, docs) 171 | } 172 | }) 173 | } 174 | 175 | window.addEventListener('DOMContentLoaded', function () { 176 | window.shopper(model) 177 | }) 178 | }()) 179 | -------------------------------------------------------------------------------- /tutorial/step-05/shoppinglist.model.js: -------------------------------------------------------------------------------- 1 | /* global PouchDB */ 2 | 3 | (function () { 4 | 'use strict' 5 | 6 | // PouchDB 7 | var db = null 8 | 9 | // Shopping List Schema 10 | // https://github.com/ibm-watson-data-lab/shopping-list#shopping-list-example 11 | var initListDoc = function (doc) { 12 | return { 13 | '_id': 'list:' + new Date().toISOString(), 14 | 'type': 'list', 15 | 'version': 1, 16 | 'title': doc.title, 17 | 'checked': !!doc.checked, 18 | 'place': { 19 | 'title': doc.place ? doc.place.title : '', 20 | 'license': doc.place ? doc.place.license : '', 21 | 'lat': doc.place ? doc.place.lat : null, 22 | 'lon': doc.place ? doc.place.lon : null, 23 | 'address': doc.place ? doc.place.address : {} 24 | }, 25 | 'createdAt': new Date().toISOString(), 26 | 'updatedAt': '' 27 | } 28 | } 29 | 30 | // Shopping List Item Schema 31 | // https://github.com/ibm-watson-data-lab/shopping-list#shopping-list-item-example 32 | var initItemDoc = function (doc, listid) { 33 | return { 34 | '_id': 'item:' + new Date().toISOString(), 35 | 'type': 'item', 36 | 'version': 1, 37 | 'list': doc.list || listid, 38 | 'title': doc.title, 39 | 'checked': !!doc.checked, 40 | 'createdAt': new Date().toISOString(), 41 | 'updatedAt': '' 42 | } 43 | } 44 | 45 | var model = function (callback) { 46 | db = new PouchDB('shopping') 47 | 48 | db.info(function (err, info) { 49 | if (err) { 50 | console.error(err) 51 | } else { 52 | console.log('model.info', info) 53 | } 54 | }) 55 | 56 | db.createIndex({ 57 | index: { fields: ['type'] } 58 | }, function (err, response) { 59 | if (typeof callback === 'function') { 60 | console.log('model ready!') 61 | callback(err, model) 62 | } 63 | }) 64 | } 65 | 66 | model.lists = function (callback) { 67 | db.find({ 68 | selector: { 69 | type: 'list' 70 | } 71 | }, function (err, response) { 72 | if (typeof callback === 'function') { 73 | var docs = response ? response.docs || response : response 74 | callback(err, docs) 75 | } 76 | }) 77 | } 78 | 79 | model.save = function (d, callback) { 80 | var doc = null 81 | if (d._id) { 82 | doc = d 83 | doc['updatedAt'] = new Date().toISOString() 84 | } else if (d.type === 'list') { 85 | doc = initListDoc(d) 86 | } else if (d.type === 'item') { 87 | doc = initItemDoc(d) 88 | } 89 | 90 | if (doc) { 91 | db.put(doc, function (err, response) { 92 | if (typeof callback === 'function') { 93 | callback(err, response) 94 | } 95 | }) 96 | } else { 97 | if (typeof callback === 'function') { 98 | callback(new Error('Missing or unsupport doc type'), null) 99 | } 100 | } 101 | } 102 | 103 | model.get = function (id, callback) { 104 | db.get(id, function (err, doc) { 105 | if (typeof callback === 'function') { 106 | callback(err, doc) 107 | } 108 | }) 109 | } 110 | 111 | model.remove = function (id, callback) { 112 | function deleteRev (rev) { 113 | db.remove(id, rev, function (err, response) { 114 | if (typeof callback === 'function') { 115 | callback(err, response) 116 | } 117 | }) 118 | } 119 | 120 | if (id) { 121 | db.get(id, function (err, doc) { 122 | if (err) { 123 | if (typeof callback === 'function') { 124 | callback(err, null) 125 | } 126 | } else if (doc.type === 'list') { 127 | // remove all children 128 | model.items(doc._id, function (err, response) { 129 | if (err) { 130 | console.error(err) 131 | deleteRev(doc._rev) 132 | } else { 133 | var items = response ? response.docs || response : response 134 | if (items && items.length) { 135 | var markfordeletion = items.map(function (item) { 136 | item._deleted = true 137 | return item 138 | }) 139 | db.bulkDocs(markfordeletion, function (err, response) { 140 | if (err) { 141 | console.error(err) 142 | } 143 | deleteRev(doc._rev) 144 | }) 145 | } else { 146 | deleteRev(doc._rev) 147 | } 148 | } 149 | }) 150 | } else { 151 | deleteRev(doc._rev) 152 | } 153 | }) 154 | } else { 155 | if (typeof callback === 'function') { 156 | callback(new Error('Missing doc id'), null) 157 | } 158 | } 159 | } 160 | 161 | model.items = function (listid, callback) { 162 | db.find({ 163 | selector: { 164 | type: 'item', 165 | list: listid 166 | } 167 | }, function (err, response) { 168 | if (typeof callback === 'function') { 169 | var docs = response ? response.docs || response : response 170 | callback(err, docs) 171 | } 172 | }) 173 | } 174 | 175 | window.addEventListener('DOMContentLoaded', function () { 176 | window.shopper(model) 177 | }) 178 | }()) 179 | -------------------------------------------------------------------------------- /tutorial/step-06/shoppinglist.model.js: -------------------------------------------------------------------------------- 1 | /* global PouchDB */ 2 | 3 | (function () { 4 | 'use strict' 5 | 6 | // PouchDB 7 | var db = null 8 | 9 | // Shopping List Schema 10 | // https://github.com/ibm-watson-data-lab/shopping-list#shopping-list-example 11 | var initListDoc = function (doc) { 12 | return { 13 | '_id': 'list:' + new Date().toISOString(), 14 | 'type': 'list', 15 | 'version': 1, 16 | 'title': doc.title, 17 | 'checked': !!doc.checked, 18 | 'place': { 19 | 'title': doc.place ? doc.place.title : '', 20 | 'license': doc.place ? doc.place.license : '', 21 | 'lat': doc.place ? doc.place.lat : null, 22 | 'lon': doc.place ? doc.place.lon : null, 23 | 'address': doc.place ? doc.place.address : {} 24 | }, 25 | 'createdAt': new Date().toISOString(), 26 | 'updatedAt': '' 27 | } 28 | } 29 | 30 | // Shopping List Item Schema 31 | // https://github.com/ibm-watson-data-lab/shopping-list#shopping-list-item-example 32 | var initItemDoc = function (doc, listid) { 33 | return { 34 | '_id': 'item:' + new Date().toISOString(), 35 | 'type': 'item', 36 | 'version': 1, 37 | 'list': doc.list || listid, 38 | 'title': doc.title, 39 | 'checked': !!doc.checked, 40 | 'createdAt': new Date().toISOString(), 41 | 'updatedAt': '' 42 | } 43 | } 44 | 45 | var model = function (callback) { 46 | db = new PouchDB('shopping') 47 | 48 | db.info(function (err, info) { 49 | if (err) { 50 | console.error(err) 51 | } else { 52 | console.log('model.info', info) 53 | } 54 | }) 55 | 56 | db.createIndex({ 57 | index: { fields: ['type'] } 58 | }, function (err, response) { 59 | if (typeof callback === 'function') { 60 | console.log('model ready!') 61 | callback(err, model) 62 | } 63 | }) 64 | } 65 | 66 | model.lists = function (callback) { 67 | db.find({ 68 | selector: { 69 | type: 'list' 70 | } 71 | }, function (err, response) { 72 | if (typeof callback === 'function') { 73 | var docs = response ? response.docs || response : response 74 | callback(err, docs) 75 | } 76 | }) 77 | } 78 | 79 | model.save = function (d, callback) { 80 | var doc = null 81 | if (d._id) { 82 | doc = d 83 | doc['updatedAt'] = new Date().toISOString() 84 | } else if (d.type === 'list') { 85 | doc = initListDoc(d) 86 | } else if (d.type === 'item') { 87 | doc = initItemDoc(d) 88 | } 89 | 90 | if (doc) { 91 | db.put(doc, function (err, response) { 92 | if (typeof callback === 'function') { 93 | callback(err, response) 94 | } 95 | }) 96 | } else { 97 | if (typeof callback === 'function') { 98 | callback(new Error('Missing or unsupport doc type'), null) 99 | } 100 | } 101 | } 102 | 103 | model.get = function (id, callback) { 104 | db.get(id, function (err, doc) { 105 | if (typeof callback === 'function') { 106 | callback(err, doc) 107 | } 108 | }) 109 | } 110 | 111 | model.remove = function (id, callback) { 112 | function deleteRev (rev) { 113 | db.remove(id, rev, function (err, response) { 114 | if (typeof callback === 'function') { 115 | callback(err, response) 116 | } 117 | }) 118 | } 119 | 120 | if (id) { 121 | db.get(id, function (err, doc) { 122 | if (err) { 123 | if (typeof callback === 'function') { 124 | callback(err, null) 125 | } 126 | } else if (doc.type === 'list') { 127 | // remove all children 128 | model.items(doc._id, function (err, response) { 129 | if (err) { 130 | console.error(err) 131 | deleteRev(doc._rev) 132 | } else { 133 | var items = response ? response.docs || response : response 134 | if (items && items.length) { 135 | var markfordeletion = items.map(function (item) { 136 | item._deleted = true 137 | return item 138 | }) 139 | db.bulkDocs(markfordeletion, function (err, response) { 140 | if (err) { 141 | console.error(err) 142 | } 143 | deleteRev(doc._rev) 144 | }) 145 | } else { 146 | deleteRev(doc._rev) 147 | } 148 | } 149 | }) 150 | } else { 151 | deleteRev(doc._rev) 152 | } 153 | }) 154 | } else { 155 | if (typeof callback === 'function') { 156 | callback(new Error('Missing doc id'), null) 157 | } 158 | } 159 | } 160 | 161 | model.items = function (listid, callback) { 162 | db.find({ 163 | selector: { 164 | type: 'item', 165 | list: listid 166 | } 167 | }, function (err, response) { 168 | if (typeof callback === 'function') { 169 | var docs = response ? response.docs || response : response 170 | callback(err, docs) 171 | } 172 | }) 173 | } 174 | 175 | window.addEventListener('DOMContentLoaded', function () { 176 | window.shopper(model) 177 | }) 178 | }()) 179 | -------------------------------------------------------------------------------- /tutorial/step-07/shoppinglist.model.js: -------------------------------------------------------------------------------- 1 | /* global PouchDB */ 2 | 3 | (function () { 4 | 'use strict' 5 | 6 | // PouchDB 7 | var db = null 8 | 9 | // Shopping List Schema 10 | // https://github.com/ibm-watson-data-lab/shopping-list#shopping-list-example 11 | var initListDoc = function (doc) { 12 | return { 13 | '_id': 'list:' + new Date().toISOString(), 14 | 'type': 'list', 15 | 'version': 1, 16 | 'title': doc.title, 17 | 'checked': !!doc.checked, 18 | 'place': { 19 | 'title': doc.place ? doc.place.title : '', 20 | 'license': doc.place ? doc.place.license : '', 21 | 'lat': doc.place ? doc.place.lat : null, 22 | 'lon': doc.place ? doc.place.lon : null, 23 | 'address': doc.place ? doc.place.address : {} 24 | }, 25 | 'createdAt': new Date().toISOString(), 26 | 'updatedAt': '' 27 | } 28 | } 29 | 30 | // Shopping List Item Schema 31 | // https://github.com/ibm-watson-data-lab/shopping-list#shopping-list-item-example 32 | var initItemDoc = function (doc, listid) { 33 | return { 34 | '_id': 'item:' + new Date().toISOString(), 35 | 'type': 'item', 36 | 'version': 1, 37 | 'list': doc.list || listid, 38 | 'title': doc.title, 39 | 'checked': !!doc.checked, 40 | 'createdAt': new Date().toISOString(), 41 | 'updatedAt': '' 42 | } 43 | } 44 | 45 | var model = function (callback) { 46 | db = new PouchDB('shopping') 47 | 48 | db.info(function (err, info) { 49 | if (err) { 50 | console.error(err) 51 | } else { 52 | console.log('model.info', info) 53 | } 54 | }) 55 | 56 | db.createIndex({ 57 | index: { fields: ['type'] } 58 | }, function (err, response) { 59 | if (typeof callback === 'function') { 60 | console.log('model ready!') 61 | callback(err, model) 62 | } 63 | }) 64 | } 65 | 66 | model.lists = function (callback) { 67 | db.find({ 68 | selector: { 69 | type: 'list' 70 | } 71 | }, function (err, response) { 72 | if (typeof callback === 'function') { 73 | var docs = response ? response.docs || response : response 74 | callback(err, docs) 75 | } 76 | }) 77 | } 78 | 79 | model.save = function (d, callback) { 80 | var doc = null 81 | if (d._id) { 82 | doc = d 83 | doc['updatedAt'] = new Date().toISOString() 84 | } else if (d.type === 'list') { 85 | doc = initListDoc(d) 86 | } else if (d.type === 'item') { 87 | doc = initItemDoc(d) 88 | } 89 | 90 | if (doc) { 91 | db.put(doc, function (err, response) { 92 | if (typeof callback === 'function') { 93 | callback(err, response) 94 | } 95 | }) 96 | } else { 97 | if (typeof callback === 'function') { 98 | callback(new Error('Missing or unsupport doc type'), null) 99 | } 100 | } 101 | } 102 | 103 | model.get = function (id, callback) { 104 | db.get(id, function (err, doc) { 105 | if (typeof callback === 'function') { 106 | callback(err, doc) 107 | } 108 | }) 109 | } 110 | 111 | model.remove = function (id, callback) { 112 | function deleteRev (rev) { 113 | db.remove(id, rev, function (err, response) { 114 | if (typeof callback === 'function') { 115 | callback(err, response) 116 | } 117 | }) 118 | } 119 | 120 | if (id) { 121 | db.get(id, function (err, doc) { 122 | if (err) { 123 | if (typeof callback === 'function') { 124 | callback(err, null) 125 | } 126 | } else if (doc.type === 'list') { 127 | // remove all children 128 | model.items(doc._id, function (err, response) { 129 | if (err) { 130 | console.error(err) 131 | deleteRev(doc._rev) 132 | } else { 133 | var items = response ? response.docs || response : response 134 | if (items && items.length) { 135 | var markfordeletion = items.map(function (item) { 136 | item._deleted = true 137 | return item 138 | }) 139 | db.bulkDocs(markfordeletion, function (err, response) { 140 | if (err) { 141 | console.error(err) 142 | } 143 | deleteRev(doc._rev) 144 | }) 145 | } else { 146 | deleteRev(doc._rev) 147 | } 148 | } 149 | }) 150 | } else { 151 | deleteRev(doc._rev) 152 | } 153 | }) 154 | } else { 155 | if (typeof callback === 'function') { 156 | callback(new Error('Missing doc id'), null) 157 | } 158 | } 159 | } 160 | 161 | model.items = function (listid, callback) { 162 | db.find({ 163 | selector: { 164 | type: 'item', 165 | list: listid 166 | } 167 | }, function (err, response) { 168 | if (typeof callback === 'function') { 169 | var docs = response ? response.docs || response : response 170 | callback(err, docs) 171 | } 172 | }) 173 | } 174 | 175 | window.addEventListener('DOMContentLoaded', function () { 176 | window.shopper(model) 177 | }) 178 | }()) 179 | -------------------------------------------------------------------------------- /tutorial/step-08/shoppinglist.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: #E1E2E1; 3 | box-shadow: none !important; 4 | overflow-x: hidden; 5 | } 6 | 7 | body * { 8 | box-shadow: none !important; 9 | } 10 | 11 | .primary-color { 12 | background-color: #52647A !important; 13 | } 14 | 15 | .primary-color * { 16 | color: #FFFFFF !important; 17 | } 18 | 19 | nav .nav-wrapper { 20 | padding-left: 10px; 21 | } 22 | 23 | main { 24 | display: flex; 25 | margin: 0 auto 75px auto; 26 | width: 200vw; 27 | } 28 | 29 | #shopping-lists, 30 | #shopping-list-items { 31 | display: inline-block; 32 | flex: 1; 33 | margin: 0; 34 | padding: 0; 35 | transition-property: transform; 36 | transition-duration: 0.5s; 37 | } 38 | 39 | #shopping-list-items { 40 | background-color: #FFFFFF; 41 | border: 0 none; 42 | vertical-align: top; 43 | } 44 | 45 | main #add-button { 46 | position: fixed; 47 | bottom: 25px; 48 | right: 25px; 49 | } 50 | 51 | main .card { 52 | margin: 30px; 53 | } 54 | 55 | .secondary-color { 56 | background-color: #FF6CA1 !important; 57 | } 58 | 59 | .secondary-color * { 60 | color: #000000 !important; 61 | } 62 | 63 | h5 { 64 | font-weight: 300; 65 | } 66 | 67 | .btn-flat { 68 | color: #7f91A9; 69 | } 70 | 71 | .btn-flat:hover { 72 | background-color: rgba(0,0,0,0.1); 73 | } 74 | 75 | input:focus:not([disabled]):not([readonly]) { 76 | border-bottom-color: #52647A !important; 77 | } 78 | 79 | body.shopping-list-add .list-bottom-sheet, 80 | body.shopping-list-item-add .item-bottom-sheet { 81 | z-index: 1007; 82 | display: block; 83 | bottom: 0px; 84 | opacity: 1; 85 | transition-property: bottom; 86 | transition-duration: .75s; 87 | } 88 | 89 | body.shopping-list-add .modal-overlay, 90 | body.shopping-list-item-add .modal-overlay, 91 | body.shopping-list-settings .modal-overlay { 92 | display: block; 93 | opacity: 0.5; 94 | transition-property: all; 95 | transition-duration: 0.25s; 96 | z-index: 1006; 97 | } 98 | 99 | .collapsible { 100 | overflow-y: hidden; 101 | margin: 0; 102 | max-height: calc(100vh - 65px); 103 | transition-property: all; 104 | transition-duration: .55s; 105 | } 106 | 107 | .collapsible.closed { 108 | max-height: 0; 109 | transition-property: all; 110 | transition-duration: .15s; 111 | } 112 | 113 | .card.collapsible { 114 | transition-duration: 2s; 115 | } 116 | 117 | .list-edit .card-action, 118 | .item-edit .card-action { 119 | border: 0 none; 120 | padding: 0; 121 | text-align: right; 122 | } 123 | 124 | .item-edit form, 125 | .list-edit form { 126 | margin: 15px; 127 | } 128 | 129 | #shopping-list-items .card, 130 | #shopping-list-items .card .collapsible { 131 | border: 0 none; 132 | box-shadow: initial; 133 | } 134 | 135 | #shopping-list-items .collection-item { 136 | border-bottom: 1px solid #DDDDDD; 137 | padding: 30px; 138 | margin: 0 35px; 139 | } 140 | 141 | #shopping-list-items .card label { 142 | font-size: 20px; 143 | font-weight: 300; 144 | line-height: initial; 145 | } 146 | 147 | .collection-item input:checked ~ label { 148 | text-decoration: line-through; 149 | } 150 | 151 | .collection-item input:checked ~ button { 152 | visibility: hidden; 153 | } 154 | 155 | .collection-item input:not(:checked) ~ label { 156 | color: #212121; 157 | } 158 | 159 | body[data-list-id] { 160 | background-color: #FFFFFF !important; 161 | } 162 | 163 | body[data-list-id] #shopping-lists, 164 | body[data-list-id] #shopping-list-items { 165 | transform: translate(-100vw); 166 | } 167 | 168 | .goback { 169 | visibility: hidden; 170 | } 171 | 172 | body[data-list-id] .goback { 173 | visibility: initial; 174 | } 175 | 176 | [type="checkbox"]:disabled:not(:checked) + label::before { 177 | display: none; 178 | } 179 | 180 | .modal.top-sheet { 181 | bottom: auto; 182 | top: -100%; 183 | margin: 0; 184 | width: 100%; 185 | border-radius: 0; 186 | will-change: bottom, opacity; 187 | } 188 | 189 | body.shopping-list-settings .settings-top-sheet { 190 | z-index: 1007; 191 | display: block; 192 | top: 0px; 193 | opacity: 1; 194 | transition-property: top; 195 | transition-duration: .75s; 196 | } 197 | 198 | .settings { 199 | margin-right: 25px; 200 | } 201 | 202 | #shopping-list-settings .input-field { 203 | margin-top: 2.2rem; 204 | } 205 | 206 | #shopping-list-settings label { 207 | font-size: 1.2rem; 208 | top: -20px; 209 | } 210 | 211 | #shopping-list-settings .chip { 212 | display: none; 213 | font-weight: 300; 214 | } 215 | 216 | .shopping-list-error-sync #shopping-list-settings .chip { 217 | display: inline-block; 218 | background-color: #C83873; 219 | color: #FFFFFF; 220 | } 221 | 222 | .shopping-list-error-sync #shopping-list-settings .chip::after { 223 | content: 'Sync Error'; 224 | } 225 | 226 | .shopping-list-sync #shopping-list-settings .chip { 227 | display: inline-block; 228 | background-color: #273A4E; 229 | color: #FFFFFF; 230 | } 231 | 232 | .shopping-list-sync #shopping-list-settings .chip::after { 233 | content: 'Syncing'; 234 | } 235 | 236 | .shopping-list-sync nav a.settings > i { 237 | display: inline-block; 238 | animation-name: spinanimation; 239 | animation-duration: 0.7s; 240 | animation-timing-function: linear; 241 | animation-iteration-count: infinite; 242 | } 243 | 244 | @-webkit-keyframes spinanimation { 245 | from { -webkit-transform: rotate(0deg) } 246 | to { -webkit-transform: rotate(360deg) } 247 | } 248 | @-moz-keyframes spinanimation { 249 | from { -moz-transform: rotate(0deg) } 250 | to { -moz-transform: rotate(360deg) } 251 | } 252 | @-ms-keyframes spinanimation { 253 | from { -ms-transform: rotate(0deg) } 254 | to { -ms-transform:rotate(360deg) } 255 | } 256 | @keyframes spinanimation { 257 | from { transform: rotate(0deg) } 258 | to { transform: rotate(360deg) } 259 | } 260 | -------------------------------------------------------------------------------- /tutorial/step-04/shoppinglist.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict' 3 | 4 | var model = null 5 | 6 | // make doc id friendlier for using as DOM node id 7 | var sanitize = function (id) { 8 | return id.replace(/[:.]/gi, '-') 9 | } 10 | 11 | // add docs to DOM node list 12 | var addToList = function (docs, clear) { 13 | if (clear) { 14 | if (document.body.getAttribute('data-list-id')) { 15 | document.getElementById('shopping-list-items').innerHTML = '' 16 | } else { 17 | document.getElementById('shopping-lists').innerHTML = '' 18 | } 19 | } 20 | for (var i = 0; i < docs.length; i++) { 21 | var doc = docs[i] 22 | 23 | var isItem = doc.type === 'item' || doc._id.indexOf('item:') === 0 24 | var isList = doc.type === 'list' || doc._id.indexOf('list:') === 0 25 | var shoppinglists = null 26 | 27 | if (isList) { 28 | shoppinglists = document.getElementById('shopping-lists') 29 | } else if (isItem) { 30 | shoppinglists = document.getElementById('shopping-list-items') 31 | } else { 32 | continue 33 | } 34 | 35 | doc._sanitizedid = sanitize(doc._id) 36 | doc._checked = doc.checked ? 'checked="checked"' : '' 37 | 38 | var template = document.getElementById(isItem ? 'shopping-list-item-template' : 'shopping-list-template').innerHTML 39 | template = template.replace(/\{\{(.+?)\}\}/g, function ($0, $1) { 40 | var fields = ($1).split('.') 41 | var value = doc 42 | while (fields.length) { 43 | if (value.hasOwnProperty(fields[0])) { 44 | value = value[fields.shift()] 45 | } else { 46 | value = null 47 | break 48 | } 49 | } 50 | return value || '' 51 | }) 52 | 53 | var listdiv = document.createElement(isItem ? 'li' : 'div') 54 | listdiv.id = doc._sanitizedid 55 | listdiv.className = 'card ' + (isItem ? 'collection-item' : 'collapsible') 56 | listdiv.innerHTML = template 57 | 58 | var existingdiv = document.getElementById(doc._sanitizedid) 59 | if (existingdiv) { 60 | shoppinglists.replaceChild(listdiv, existingdiv) 61 | } else { 62 | shoppinglists.insertBefore(listdiv, shoppinglists.firstChild) 63 | } 64 | } 65 | } 66 | 67 | // remove from DOM node list 68 | var removeFromList = function (id) { 69 | var list = document.getElementById(sanitize(id)) 70 | shopper.toggle(list) 71 | list.parentElement.removeChild(list) 72 | } 73 | 74 | var shopper = function (themodel) { 75 | if (themodel) { 76 | themodel(function (err, response) { 77 | if (err) { 78 | console.error(err) 79 | } else { 80 | model = response 81 | model.lists(function (err, docs) { 82 | if (err) { 83 | console.error(err) 84 | } else { 85 | addToList(docs, true) 86 | } 87 | console.log('shopper ready!') 88 | }) 89 | } 90 | }) 91 | } 92 | return this 93 | } 94 | 95 | shopper.openadd = function () { 96 | var form = null 97 | if (document.body.getAttribute('data-list-id')) { 98 | form = document.getElementById('shopping-list-item-add') 99 | } else { 100 | form = document.getElementById('shopping-list-add') 101 | } 102 | form.reset() 103 | document.body.className += ' ' + form.id 104 | } 105 | 106 | shopper.closeadd = function () { 107 | document.body.className = document.body.className 108 | .replace('shopping-list-add', '') 109 | .replace('shopping-list-item-add', '') 110 | .trim() 111 | } 112 | 113 | shopper.add = function (event) { 114 | var form = event.target 115 | var elements = form.elements 116 | var doc = {} 117 | var listid = document.body.getAttribute('data-list-id') 118 | 119 | if (!elements['title'].value) { 120 | console.error('title required') 121 | } else if (listid && form.id.indexOf('list-item') === -1) { 122 | console.error('incorrect form') 123 | } else if (!listid && form.id.indexOf('list-item') > -1) { 124 | console.error('list id required') 125 | } else { 126 | for (var i = 0; i < elements.length; i++) { 127 | if (elements[i].tagName.toLowerCase() !== 'button') { 128 | doc[elements[i].name] = elements[i].value 129 | } 130 | } 131 | 132 | if (listid) { 133 | doc['list'] = listid 134 | } 135 | 136 | model.save(doc, function (err, updated) { 137 | if (err) { 138 | console.error(err) 139 | } else { 140 | doc._id = doc._id || updated._id || updated.id 141 | addToList([doc]) 142 | shopper.closeadd() 143 | } 144 | }) 145 | } 146 | } 147 | 148 | shopper.remove = function (id) { 149 | model.remove(id, function (err, response) { 150 | if (err) { 151 | console.log(err) 152 | } else { 153 | removeFromList(id) 154 | } 155 | }) 156 | } 157 | 158 | shopper.update = function (id) { 159 | var elements = document.getElementById('form-' + sanitize(id)).elements 160 | if (!elements['title'].value) { 161 | console.error('title required') 162 | } else { 163 | model.get(id, function (err, doc) { 164 | if (err) { 165 | console.log(err) 166 | } else { 167 | doc.title = elements['title'].value 168 | if (document.body.getAttribute('data-list-id')) { 169 | var checked = document.getElementById('checked-item-' + sanitize(id)) 170 | doc.checked = checked ? !!checked.checked : false 171 | } 172 | model.save(doc, function (err, updated) { 173 | if (err) { 174 | console.error(err) 175 | } else { 176 | addToList([doc]) 177 | } 178 | }) 179 | } 180 | }) 181 | } 182 | } 183 | 184 | shopper.toggle = function (node, event) { 185 | if (event) { 186 | event.stopPropagation() 187 | } 188 | if (typeof node === 'string') { 189 | var nodes = document.querySelectorAll('#' + node + ' .collapsible') 190 | for (var i = 0; i < nodes.length; i++) { 191 | if (nodes[i].classList) { 192 | nodes[i].classList.toggle('closed') 193 | } 194 | } 195 | } else { 196 | node.classList.toggle('closed') 197 | } 198 | } 199 | 200 | shopper.goto = function (listid, title, event) { 201 | if (event) { 202 | event.stopPropagation() 203 | } 204 | if (listid) { 205 | model.items(listid, function (err, docs) { 206 | if (err) { 207 | console.error(err) 208 | } else { 209 | document.getElementById('header-title').innerText = title 210 | document.body.setAttribute('data-list-id', listid) 211 | document.body.scrollTop = 0 212 | document.documentElement.scrollTop = 0 213 | docs.sort(function (a, b) { 214 | return a.title < b.title 215 | }) 216 | addToList(docs, true) 217 | } 218 | }) 219 | } else { 220 | document.body.removeAttribute('data-list-id') 221 | document.getElementById('header-title').innerText = 'Shopping List' 222 | } 223 | } 224 | 225 | window.shopper = shopper 226 | }()) 227 | -------------------------------------------------------------------------------- /tutorial/step-04/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | Shopping List | vanilla JavaScript | PouchDB 16 | 17 | 18 | 19 | 29 | 30 | 31 |
32 |
33 | 34 |
35 | 36 | 39 | 40 | 41 | 44 |
45 | 46 | 47 | 64 | 65 | 66 | 83 | 84 | 85 | 86 | 87 | 88 | 116 | 117 | 118 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | -------------------------------------------------------------------------------- /tutorial/step-08/shoppinglist.model.js: -------------------------------------------------------------------------------- 1 | /* global PouchDB */ 2 | 3 | (function () { 4 | 'use strict' 5 | 6 | // PouchDB 7 | var db = null 8 | var dbsync = null 9 | 10 | // Shopping List Schema 11 | // https://github.com/ibm-watson-data-lab/shopping-list#shopping-list-example 12 | var initListDoc = function (doc) { 13 | return { 14 | '_id': 'list:' + new Date().toISOString(), 15 | 'type': 'list', 16 | 'version': 1, 17 | 'title': doc.title, 18 | 'checked': !!doc.checked, 19 | 'place': { 20 | 'title': doc.place ? doc.place.title : '', 21 | 'license': doc.place ? doc.place.license : '', 22 | 'lat': doc.place ? doc.place.lat : null, 23 | 'lon': doc.place ? doc.place.lon : null, 24 | 'address': doc.place ? doc.place.address : {} 25 | }, 26 | 'createdAt': new Date().toISOString(), 27 | 'updatedAt': '' 28 | } 29 | } 30 | 31 | // Shopping List Item Schema 32 | // https://github.com/ibm-watson-data-lab/shopping-list#shopping-list-item-example 33 | var initItemDoc = function (doc, listid) { 34 | return { 35 | '_id': 'item:' + new Date().toISOString(), 36 | 'type': 'item', 37 | 'version': 1, 38 | 'list': doc.list || listid, 39 | 'title': doc.title, 40 | 'checked': !!doc.checked, 41 | 'createdAt': new Date().toISOString(), 42 | 'updatedAt': '' 43 | } 44 | } 45 | 46 | var model = function (callback) { 47 | db = new PouchDB('shopping') 48 | 49 | db.info(function (err, info) { 50 | if (err) { 51 | console.error(err) 52 | } else { 53 | console.log('model.info', info) 54 | } 55 | }) 56 | 57 | db.createIndex({ 58 | index: { fields: ['type'] } 59 | }, function (err, response) { 60 | if (typeof callback === 'function') { 61 | console.log('model ready!') 62 | callback(err, model) 63 | } 64 | }) 65 | } 66 | 67 | model.lists = function (callback) { 68 | db.find({ 69 | selector: { 70 | type: 'list' 71 | } 72 | }, function (err, response) { 73 | if (typeof callback === 'function') { 74 | var docs = response ? response.docs || response : response 75 | callback(err, docs) 76 | } 77 | }) 78 | } 79 | 80 | model.save = function (d, callback) { 81 | var doc = null 82 | if (d._id) { 83 | doc = d 84 | doc['updatedAt'] = new Date().toISOString() 85 | } else if (d.type === 'list') { 86 | doc = initListDoc(d) 87 | } else if (d.type === 'item') { 88 | doc = initItemDoc(d) 89 | } 90 | 91 | if (doc) { 92 | db.put(doc, function (err, response) { 93 | if (typeof callback === 'function') { 94 | callback(err, response) 95 | } 96 | }) 97 | } else { 98 | if (typeof callback === 'function') { 99 | callback(new Error('Missing or unsupport doc type'), null) 100 | } 101 | } 102 | } 103 | 104 | model.get = function (id, callback) { 105 | db.get(id, function (err, doc) { 106 | if (typeof callback === 'function') { 107 | callback(err, doc) 108 | } 109 | }) 110 | } 111 | 112 | model.remove = function (id, callback) { 113 | function deleteRev (rev) { 114 | db.remove(id, rev, function (err, response) { 115 | if (typeof callback === 'function') { 116 | callback(err, response) 117 | } 118 | }) 119 | } 120 | 121 | if (id) { 122 | db.get(id, function (err, doc) { 123 | if (err) { 124 | if (typeof callback === 'function') { 125 | callback(err, null) 126 | } 127 | } else if (doc.type === 'list') { 128 | // remove all children 129 | model.items(doc._id, function (err, response) { 130 | if (err) { 131 | console.error(err) 132 | deleteRev(doc._rev) 133 | } else { 134 | var items = response ? response.docs || response : response 135 | if (items && items.length) { 136 | var markfordeletion = items.map(function (item) { 137 | item._deleted = true 138 | return item 139 | }) 140 | db.bulkDocs(markfordeletion, function (err, response) { 141 | if (err) { 142 | console.error(err) 143 | } 144 | deleteRev(doc._rev) 145 | }) 146 | } else { 147 | deleteRev(doc._rev) 148 | } 149 | } 150 | }) 151 | } else { 152 | deleteRev(doc._rev) 153 | } 154 | }) 155 | } else { 156 | if (typeof callback === 'function') { 157 | callback(new Error('Missing doc id'), null) 158 | } 159 | } 160 | } 161 | 162 | model.items = function (listid, callback) { 163 | db.find({ 164 | selector: { 165 | type: 'item', 166 | list: listid 167 | } 168 | }, function (err, response) { 169 | if (typeof callback === 'function') { 170 | var docs = response ? response.docs || response : response 171 | callback(err, docs) 172 | } 173 | }) 174 | } 175 | 176 | model.settings = function (settings, callback) { 177 | var id = '_local/user' 178 | var cb = callback || settings 179 | if (callback && settings && typeof settings === 'object') { 180 | db.get(id, function (err, doc) { 181 | settings._id = id 182 | if (err) { 183 | console.error(err) 184 | } else { 185 | settings._rev = doc._rev 186 | } 187 | db.put(settings, function (err, response) { 188 | if (typeof cb === 'function') { 189 | cb(err, response) 190 | } 191 | }) 192 | }) 193 | } else { 194 | db.get(id, function (err, doc) { 195 | if (typeof cb === 'function') { 196 | cb(err, doc) 197 | } 198 | }) 199 | } 200 | } 201 | 202 | model.sync = function (remoteDB, oncomplete, onchange) { 203 | if (dbsync) { 204 | dbsync.cancel() 205 | } 206 | 207 | if (remoteDB) { 208 | // do one-off sync from the server until completion 209 | db.sync(remoteDB) 210 | .on('complete', function (info) { 211 | if (typeof oncomplete === 'function') { 212 | oncomplete(null, info) 213 | } 214 | 215 | // then two-way, continuous, retriable sync 216 | dbsync = db.sync(remoteDB, { live: true, retry: true }) 217 | .on('change', function (info) { 218 | // incoming changes only 219 | if (info.direction === 'pull' && info.change && info.change.docs) { 220 | if (typeof onchange === 'function') { 221 | onchange(null, info.change.docs) 222 | } 223 | } 224 | }) 225 | .on('error', function (err) { 226 | if (typeof onchange === 'function') { 227 | onchange(err, null) 228 | } 229 | }) 230 | }) 231 | .on('error', function (err) { 232 | if (typeof oncomplete === 'function') { 233 | oncomplete(err, null) 234 | } 235 | }) 236 | } else if (typeof oncomplete === 'function') { 237 | oncomplete() 238 | } 239 | } 240 | 241 | window.addEventListener('DOMContentLoaded', function () { 242 | window.shopper(model) 243 | }) 244 | }()) 245 | -------------------------------------------------------------------------------- /tutorial/step-05/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | Shopping List | vanilla JavaScript | PouchDB 16 | 17 | 18 | 19 | 29 | 30 | 31 |
32 |
33 | 34 |
35 | 36 | 39 | 40 | 41 | 44 |
45 | 46 | 47 | 64 | 65 | 66 | 83 | 84 | 85 | 86 | 87 | 88 | 120 | 121 | 122 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | -------------------------------------------------------------------------------- /css/shoppinglist.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: #E1E2E1; 3 | box-shadow: none !important; 4 | overflow-x: hidden; 5 | } 6 | 7 | body * { 8 | box-shadow: none !important; 9 | } 10 | 11 | body[data-list-id] { 12 | background-color: #FFFFFF !important; 13 | } 14 | 15 | h5 { 16 | font-weight: 300; 17 | } 18 | 19 | .btn-flat { 20 | color: #7f91A9; 21 | } 22 | .btn-flat:hover { 23 | background-color: rgba(0,0,0,0.1); 24 | } 25 | 26 | [type="checkbox"]:checked + label::before { 27 | border-right-color: #52647A; 28 | border-bottom-color: #52647A; 29 | } 30 | 31 | .primary-color { 32 | background-color: #52647A !important; 33 | } 34 | .primary-color * { 35 | color: #FFFFFF !important; 36 | } 37 | .primary-color.lighter { 38 | background-color: #7f91A9 !important; 39 | } 40 | .primary-color.darker { 41 | background-color: #273A4E !important; 42 | } 43 | 44 | .secondary-color { 45 | background-color: #FF6CA1 !important; 46 | } 47 | .secondary-color * { 48 | color: #000000 !important; 49 | } 50 | .secondary-color.ligher { 51 | background-color: #FF9FD2 !important; 52 | } 53 | .secondary-color.darker, 54 | button.secondary-color:focus, 55 | button.secondary-color:hover { 56 | background-color: #C83873 !important; 57 | } 58 | 59 | .tertiary-color { 60 | background-color: #E1E2E1 !important; 61 | } 62 | .tertiary-color-lighter { 63 | background-color: #F5F5F6 !important; 64 | } 65 | 66 | .primary-text { 67 | color: #52647A; 68 | } 69 | .primary-text.lighter { 70 | color: #7f91A9; 71 | } 72 | .primary-text.darker { 73 | color: #273A4E; 74 | } 75 | 76 | .secondary-text { 77 | color: #FF6CA1; 78 | } 79 | .secondary-text.lighter { 80 | color: #FF9FD2 !important; 81 | } 82 | .secondary-text.darker { 83 | color: #C83873; 84 | } 85 | 86 | .tertiary-text { 87 | color: #E1E2E1; 88 | } 89 | .tertiary-text-lighter { 90 | color: #F5F5F6; 91 | } 92 | 93 | body.shopping-list-offline .primary-color, 94 | body.shopping-list-offline .secondary-color { 95 | background-color: #273A4E !important; 96 | } 97 | body.shopping-list-offline .primary-color *, 98 | body.shopping-list-offline .secondary-color * { 99 | color: #E1E2E1 !important; 100 | } 101 | 102 | .item-edit form, 103 | .list-edit form { 104 | margin: 15px; 105 | } 106 | 107 | .collapsible { 108 | border: 0 none !important; 109 | } 110 | 111 | input:focus:not([disabled]):not([readonly]) { 112 | border-bottom-color: #52647A !important; 113 | } 114 | 115 | .goback { 116 | visibility: hidden; 117 | } 118 | 119 | body[data-list-id] .goback { 120 | visibility: initial; 121 | } 122 | 123 | nav .nav-wrapper { 124 | padding-left: 10px; 125 | } 126 | 127 | main { 128 | display: flex; 129 | margin: 0 auto 75px auto; 130 | width: 200vw; 131 | } 132 | 133 | main #add-button { 134 | position: fixed; 135 | bottom: 25px; 136 | right: 25px; 137 | } 138 | 139 | main .card { 140 | margin: 30px; 141 | } 142 | 143 | #shopping-lists, 144 | #shopping-list-items { 145 | display: inline-block; 146 | flex: 1; 147 | margin: 0; 148 | padding: 0; 149 | transition-property: transform; 150 | transition-duration: 0.5s; 151 | } 152 | 153 | #shopping-list-items { 154 | background-color: #FFFFFF; 155 | border: 0 none; 156 | vertical-align: top; 157 | } 158 | 159 | .list-edit .card-action, 160 | .item-edit .card-action { 161 | border: 0 none; 162 | padding: 0; 163 | text-align: right; 164 | } 165 | 166 | #shopping-lists .card-content { 167 | cursor: pointer; 168 | } 169 | 170 | #shopping-list-items .card, 171 | #shopping-list-items .card .collapsible { 172 | border: 0 none; 173 | box-shadow: initial; 174 | } 175 | 176 | #shopping-list-items .collection-item { 177 | border-bottom: 1px solid #DDDDDD; 178 | padding: 30px; 179 | margin: 0 35px; 180 | } 181 | 182 | #shopping-list-items .card label { 183 | font-size: 20px; 184 | font-weight: 300; 185 | line-height: initial; 186 | } 187 | 188 | .collection-item input:checked ~ label { 189 | text-decoration: line-through; 190 | } 191 | 192 | .collection-item input:checked ~ button { 193 | visibility: hidden; 194 | } 195 | 196 | .collection-item input:not(:checked) ~ label { 197 | color: #212121; 198 | } 199 | 200 | .collapsible { 201 | overflow-y: hidden; 202 | margin: 0; 203 | max-height: calc(100vh - 65px); 204 | transition-property: all; 205 | transition-duration: .55s; 206 | } 207 | 208 | .collapsible.closed { 209 | max-height: 0; 210 | transition-property: all; 211 | transition-duration: .15s; 212 | } 213 | 214 | .card.collapsible { 215 | transition-duration: 2s; 216 | } 217 | 218 | body.shopping-list-add .list-bottom-sheet, 219 | body.shopping-list-item-add .item-bottom-sheet { 220 | z-index: 1007; 221 | display: block; 222 | bottom: 0px; 223 | opacity: 1; 224 | transition-property: bottom; 225 | transition-duration: .75s; 226 | } 227 | 228 | body.shopping-list-add .modal-overlay, 229 | body.shopping-list-item-add .modal-overlay, 230 | body.shopping-list-settings .modal-overlay, 231 | body.shopping-list-about .modal-overlay { 232 | display: block; 233 | opacity: 0.5; 234 | transition-property: all; 235 | transition-duration: 0.25s; 236 | z-index: 1006; 237 | } 238 | 239 | body[data-list-id] #shopping-lists, 240 | body[data-list-id] #shopping-list-items { 241 | transform: translate(-100vw); 242 | } 243 | 244 | [type="checkbox"]:disabled:not(:checked) + label::before { 245 | display: none; 246 | } 247 | 248 | .more-btn { 249 | padding: 2px 15px; 250 | } 251 | 252 | .modal.top-sheet { 253 | bottom: auto; 254 | top: -100%; 255 | margin: 0; 256 | width: 100%; 257 | border-radius: 0; 258 | will-change: bottom, opacity; 259 | } 260 | 261 | body.shopping-list-settings .settings-top-sheet, 262 | body.shopping-list-about .about-top-sheet { 263 | z-index: 1007; 264 | display: block; 265 | top: 0px; 266 | opacity: 1; 267 | transition-property: top; 268 | transition-duration: .75s; 269 | } 270 | 271 | .settings { 272 | margin-right: 25px; 273 | } 274 | 275 | #shopping-list-settings .input-field { 276 | margin-top: 2.2rem; 277 | } 278 | 279 | #shopping-list-settings label { 280 | font-size: 1.2rem; 281 | top: -20px; 282 | } 283 | 284 | #shopping-list-settings .chip { 285 | display: none; 286 | font-weight: 300; 287 | } 288 | 289 | body.shopping-list-error-sync nav a.settings.settings-btn, 290 | body:not(.shopping-list-error-sync) nav a.settings.error-btn { 291 | display: none; 292 | } 293 | 294 | body:not(.shopping-list-error-sync) nav a.settings.settings-btn, 295 | body.shopping-list-error-sync nav a.settings.error-btn { 296 | display: initial; 297 | } 298 | 299 | .shopping-list-error-sync #shopping-list-settings .chip { 300 | display: inline-block; 301 | background-color: #C83873; 302 | color: #FFFFFF; 303 | } 304 | 305 | .shopping-list-error-sync #shopping-list-settings .chip::after { 306 | content: 'Sync Error'; 307 | } 308 | 309 | .shopping-list-sync #shopping-list-settings .chip { 310 | display: inline-block; 311 | background-color: #273A4E; 312 | color: #FFFFFF; 313 | } 314 | 315 | .shopping-list-sync #shopping-list-settings .chip::after { 316 | content: 'Syncing'; 317 | } 318 | 319 | .shopping-list-sync nav a.settings > i { 320 | display: inline-block; 321 | animation-name: spinanimation; 322 | animation-duration: 0.7s; 323 | animation-timing-function: linear; 324 | animation-iteration-count: infinite; 325 | } 326 | 327 | @-webkit-keyframes spinanimation { 328 | from { -webkit-transform: rotate(0deg) } 329 | to { -webkit-transform: rotate(360deg) } 330 | } 331 | @-moz-keyframes spinanimation { 332 | from { -moz-transform: rotate(0deg) } 333 | to { -moz-transform: rotate(360deg) } 334 | } 335 | @-ms-keyframes spinanimation { 336 | from { -ms-transform: rotate(0deg) } 337 | to { -ms-transform:rotate(360deg) } 338 | } 339 | @keyframes spinanimation { 340 | from { transform: rotate(0deg) } 341 | to { transform: rotate(360deg) } 342 | } 343 | -------------------------------------------------------------------------------- /tutorial/step-06/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | Shopping List | vanilla JavaScript | PouchDB 16 | 17 | 18 | 19 | 29 | 30 | 31 |
32 |
33 | 34 |
35 | 36 | 39 | 40 | 41 | 44 |
45 | 46 | 47 | 64 | 65 | 66 | 83 | 84 | 85 | 86 | 87 | 88 | 120 | 121 | 122 | 148 | 149 | 150 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | -------------------------------------------------------------------------------- /tutorial/step-05/shoppinglist.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict' 3 | 4 | var model = null 5 | 6 | // make doc id friendlier for using as DOM node id 7 | var sanitize = function (id) { 8 | return id.replace(/[:.]/gi, '-') 9 | } 10 | 11 | // add docs to DOM node list 12 | var addToList = function (docs, clear) { 13 | if (clear) { 14 | if (document.body.getAttribute('data-list-id')) { 15 | document.getElementById('shopping-list-items').innerHTML = '' 16 | } else { 17 | document.getElementById('shopping-lists').innerHTML = '' 18 | } 19 | } 20 | for (var i = 0; i < docs.length; i++) { 21 | var doc = docs[i] 22 | 23 | var isItem = doc.type === 'item' || doc._id.indexOf('item:') === 0 24 | var isList = doc.type === 'list' || doc._id.indexOf('list:') === 0 25 | var shoppinglists = null 26 | 27 | if (isList) { 28 | shoppinglists = document.getElementById('shopping-lists') 29 | } else if (isItem) { 30 | shoppinglists = document.getElementById('shopping-list-items') 31 | } else { 32 | continue 33 | } 34 | 35 | doc._sanitizedid = sanitize(doc._id) 36 | doc._checked = doc.checked ? 'checked="checked"' : '' 37 | 38 | var template = document.getElementById(isItem ? 'shopping-list-item-template' : 'shopping-list-template').innerHTML 39 | template = template.replace(/\{\{(.+?)\}\}/g, function ($0, $1) { 40 | var fields = ($1).split('.') 41 | var value = doc 42 | while (fields.length) { 43 | if (value.hasOwnProperty(fields[0])) { 44 | value = value[fields.shift()] 45 | } else { 46 | value = null 47 | break 48 | } 49 | } 50 | return value || '' 51 | }) 52 | 53 | var listdiv = document.createElement(isItem ? 'li' : 'div') 54 | listdiv.id = doc._sanitizedid 55 | listdiv.className = 'card ' + (isItem ? 'collection-item' : 'collapsible') 56 | listdiv.innerHTML = template 57 | 58 | var existingdiv = document.getElementById(doc._sanitizedid) 59 | if (existingdiv) { 60 | shoppinglists.replaceChild(listdiv, existingdiv) 61 | } else { 62 | shoppinglists.insertBefore(listdiv, shoppinglists.firstChild) 63 | } 64 | 65 | if (isItem) { 66 | updateItemCount(doc.list) 67 | } else { 68 | updateItemCount(doc._id) 69 | } 70 | } 71 | } 72 | 73 | // remove from DOM node list 74 | var removeFromList = function (id) { 75 | var list = document.getElementById(sanitize(id)) 76 | shopper.toggle(list) 77 | list.parentElement.removeChild(list) 78 | 79 | var listid = document.body.getAttribute('data-list-id') 80 | if (listid) { 81 | updateItemCount(listid) 82 | } 83 | } 84 | 85 | // figure out the checked items count for a list 86 | var updateItemCount = function (listid) { 87 | model.get(listid, function (err, doc) { 88 | if (err) { 89 | console.log(err) 90 | } else { 91 | model.items(listid, function (err, items) { 92 | if (err) { 93 | console.log(err) 94 | } else { 95 | var checked = 0 96 | for (var i = 0; i < items.length; i++) { 97 | checked += items[i].checked ? 1 : 0 98 | } 99 | var node = document.getElementById('checked-list-' + sanitize(listid)) 100 | if (node) { 101 | node.nextElementSibling.innerText = items.length ? (checked + ' of ' + items.length + ' items checked') : '0 items' 102 | node.checked = checked && checked === items.length 103 | if ((doc.checked && checked !== items.length) || 104 | (!doc.checked && checked === items.length)) { 105 | doc.checked = checked === items.length 106 | model.save(doc) 107 | } 108 | } 109 | } 110 | }) 111 | } 112 | }) 113 | } 114 | 115 | var shopper = function (themodel) { 116 | if (themodel) { 117 | themodel(function (err, response) { 118 | if (err) { 119 | console.error(err) 120 | } else { 121 | model = response 122 | model.lists(function (err, docs) { 123 | if (err) { 124 | console.error(err) 125 | } else { 126 | addToList(docs, true) 127 | } 128 | console.log('shopper ready!') 129 | }) 130 | } 131 | }) 132 | } 133 | return this 134 | } 135 | 136 | shopper.openadd = function () { 137 | var form = null 138 | if (document.body.getAttribute('data-list-id')) { 139 | form = document.getElementById('shopping-list-item-add') 140 | } else { 141 | form = document.getElementById('shopping-list-add') 142 | } 143 | form.reset() 144 | document.body.className += ' ' + form.id 145 | } 146 | 147 | shopper.closeadd = function () { 148 | document.body.className = document.body.className 149 | .replace('shopping-list-add', '') 150 | .replace('shopping-list-item-add', '') 151 | .trim() 152 | } 153 | 154 | shopper.add = function (event) { 155 | var form = event.target 156 | var elements = form.elements 157 | var doc = {} 158 | var listid = document.body.getAttribute('data-list-id') 159 | 160 | if (!elements['title'].value) { 161 | console.error('title required') 162 | } else if (listid && form.id.indexOf('list-item') === -1) { 163 | console.error('incorrect form') 164 | } else if (!listid && form.id.indexOf('list-item') > -1) { 165 | console.error('list id required') 166 | } else { 167 | for (var i = 0; i < elements.length; i++) { 168 | if (elements[i].tagName.toLowerCase() !== 'button') { 169 | doc[elements[i].name] = elements[i].value 170 | } 171 | } 172 | 173 | if (listid) { 174 | doc['list'] = listid 175 | } 176 | 177 | model.save(doc, function (err, updated) { 178 | if (err) { 179 | console.error(err) 180 | } else { 181 | doc._id = doc._id || updated._id || updated.id 182 | addToList([doc]) 183 | shopper.closeadd() 184 | } 185 | }) 186 | } 187 | } 188 | 189 | shopper.remove = function (id) { 190 | model.remove(id, function (err, response) { 191 | if (err) { 192 | console.log(err) 193 | } else { 194 | removeFromList(id) 195 | } 196 | }) 197 | } 198 | 199 | shopper.update = function (id) { 200 | var elements = document.getElementById('form-' + sanitize(id)).elements 201 | if (!elements['title'].value) { 202 | console.error('title required') 203 | } else { 204 | model.get(id, function (err, doc) { 205 | if (err) { 206 | console.log(err) 207 | } else { 208 | doc.title = elements['title'].value 209 | if (document.body.getAttribute('data-list-id')) { 210 | var checked = document.getElementById('checked-item-' + sanitize(id)) 211 | doc.checked = checked ? !!checked.checked : false 212 | } 213 | model.save(doc, function (err, updated) { 214 | if (err) { 215 | console.error(err) 216 | } else { 217 | addToList([doc]) 218 | } 219 | }) 220 | } 221 | }) 222 | } 223 | } 224 | 225 | shopper.toggle = function (node, event) { 226 | if (event) { 227 | event.stopPropagation() 228 | } 229 | if (typeof node === 'string') { 230 | var nodes = document.querySelectorAll('#' + node + ' .collapsible') 231 | for (var i = 0; i < nodes.length; i++) { 232 | if (nodes[i].classList) { 233 | nodes[i].classList.toggle('closed') 234 | } 235 | } 236 | } else { 237 | node.classList.toggle('closed') 238 | } 239 | } 240 | 241 | shopper.goto = function (listid, title, event) { 242 | if (event) { 243 | event.stopPropagation() 244 | } 245 | if (listid) { 246 | model.items(listid, function (err, docs) { 247 | if (err) { 248 | console.error(err) 249 | } else { 250 | document.getElementById('header-title').innerText = title 251 | document.body.setAttribute('data-list-id', listid) 252 | document.body.scrollTop = 0 253 | document.documentElement.scrollTop = 0 254 | docs.sort(function (a, b) { 255 | return a.title < b.title 256 | }) 257 | addToList(docs, true) 258 | } 259 | }) 260 | } else { 261 | document.body.removeAttribute('data-list-id') 262 | document.getElementById('header-title').innerText = 'Shopping List' 263 | } 264 | } 265 | 266 | window.shopper = shopper 267 | }()) 268 | -------------------------------------------------------------------------------- /tutorial/step-06/shoppinglist.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict' 3 | 4 | var model = null 5 | 6 | // make doc id friendlier for using as DOM node id 7 | var sanitize = function (id) { 8 | return id.replace(/[:.]/gi, '-') 9 | } 10 | 11 | // add docs to DOM node list 12 | var addToList = function (docs, clear) { 13 | if (clear) { 14 | if (document.body.getAttribute('data-list-id')) { 15 | document.getElementById('shopping-list-items').innerHTML = '' 16 | } else { 17 | document.getElementById('shopping-lists').innerHTML = '' 18 | } 19 | } 20 | for (var i = 0; i < docs.length; i++) { 21 | var doc = docs[i] 22 | 23 | var isItem = doc.type === 'item' || doc._id.indexOf('item:') === 0 24 | var isList = doc.type === 'list' || doc._id.indexOf('list:') === 0 25 | var shoppinglists = null 26 | 27 | if (isList) { 28 | shoppinglists = document.getElementById('shopping-lists') 29 | } else if (isItem) { 30 | shoppinglists = document.getElementById('shopping-list-items') 31 | } else { 32 | continue 33 | } 34 | 35 | doc._sanitizedid = sanitize(doc._id) 36 | doc._checked = doc.checked ? 'checked="checked"' : '' 37 | 38 | var template = document.getElementById(isItem ? 'shopping-list-item-template' : 'shopping-list-template').innerHTML 39 | template = template.replace(/\{\{(.+?)\}\}/g, function ($0, $1) { 40 | var fields = ($1).split('.') 41 | var value = doc 42 | while (fields.length) { 43 | if (value.hasOwnProperty(fields[0])) { 44 | value = value[fields.shift()] 45 | } else { 46 | value = null 47 | break 48 | } 49 | } 50 | return value || '' 51 | }) 52 | 53 | var listdiv = document.createElement(isItem ? 'li' : 'div') 54 | listdiv.id = doc._sanitizedid 55 | listdiv.className = 'card ' + (isItem ? 'collection-item' : 'collapsible') 56 | listdiv.innerHTML = template 57 | 58 | var existingdiv = document.getElementById(doc._sanitizedid) 59 | if (existingdiv) { 60 | shoppinglists.replaceChild(listdiv, existingdiv) 61 | } else { 62 | shoppinglists.insertBefore(listdiv, shoppinglists.firstChild) 63 | } 64 | 65 | if (isItem) { 66 | updateItemCount(doc.list) 67 | } else { 68 | updateItemCount(doc._id) 69 | } 70 | } 71 | } 72 | 73 | // remove from DOM node list 74 | var removeFromList = function (id) { 75 | var list = document.getElementById(sanitize(id)) 76 | shopper.toggle(list) 77 | list.parentElement.removeChild(list) 78 | 79 | var listid = document.body.getAttribute('data-list-id') 80 | if (listid) { 81 | updateItemCount(listid) 82 | } 83 | } 84 | 85 | // figure out the checked items count for a list 86 | var updateItemCount = function (listid) { 87 | model.get(listid, function (err, doc) { 88 | if (err) { 89 | console.log(err) 90 | } else { 91 | model.items(listid, function (err, items) { 92 | if (err) { 93 | console.log(err) 94 | } else { 95 | var checked = 0 96 | for (var i = 0; i < items.length; i++) { 97 | checked += items[i].checked ? 1 : 0 98 | } 99 | var node = document.getElementById('checked-list-' + sanitize(listid)) 100 | if (node) { 101 | node.nextElementSibling.innerText = items.length ? (checked + ' of ' + items.length + ' items checked') : '0 items' 102 | node.checked = checked && checked === items.length 103 | if ((doc.checked && checked !== items.length) || 104 | (!doc.checked && checked === items.length)) { 105 | doc.checked = checked === items.length 106 | model.save(doc) 107 | } 108 | } 109 | } 110 | }) 111 | } 112 | }) 113 | } 114 | 115 | var shopper = function (themodel) { 116 | if (themodel) { 117 | themodel(function (err, response) { 118 | if (err) { 119 | console.error(err) 120 | } else { 121 | model = response 122 | model.lists(function (err, docs) { 123 | if (err) { 124 | console.error(err) 125 | } else { 126 | addToList(docs, true) 127 | } 128 | console.log('shopper ready!') 129 | }) 130 | } 131 | }) 132 | } 133 | return this 134 | } 135 | 136 | shopper.openadd = function () { 137 | var form = null 138 | if (document.body.getAttribute('data-list-id')) { 139 | form = document.getElementById('shopping-list-item-add') 140 | } else { 141 | form = document.getElementById('shopping-list-add') 142 | } 143 | form.reset() 144 | document.body.className += ' ' + form.id 145 | } 146 | 147 | shopper.closeadd = function () { 148 | document.body.className = document.body.className 149 | .replace('shopping-list-add', '') 150 | .replace('shopping-list-item-add', '') 151 | .trim() 152 | } 153 | 154 | shopper.add = function (event) { 155 | var form = event.target 156 | var elements = form.elements 157 | var doc = {} 158 | var listid = document.body.getAttribute('data-list-id') 159 | 160 | if (!elements['title'].value) { 161 | console.error('title required') 162 | } else if (listid && form.id.indexOf('list-item') === -1) { 163 | console.error('incorrect form') 164 | } else if (!listid && form.id.indexOf('list-item') > -1) { 165 | console.error('list id required') 166 | } else { 167 | for (var i = 0; i < elements.length; i++) { 168 | if (elements[i].tagName.toLowerCase() !== 'button') { 169 | doc[elements[i].name] = elements[i].value 170 | } 171 | } 172 | 173 | if (listid) { 174 | doc['list'] = listid 175 | } 176 | 177 | model.save(doc, function (err, updated) { 178 | if (err) { 179 | console.error(err) 180 | } else { 181 | doc._id = doc._id || updated._id || updated.id 182 | addToList([doc]) 183 | shopper.closeadd() 184 | } 185 | }) 186 | } 187 | } 188 | 189 | shopper.remove = function (id) { 190 | model.remove(id, function (err, response) { 191 | if (err) { 192 | console.log(err) 193 | } else { 194 | removeFromList(id) 195 | } 196 | }) 197 | } 198 | 199 | shopper.update = function (id) { 200 | var elements = document.getElementById('form-' + sanitize(id)).elements 201 | if (!elements['title'].value) { 202 | console.error('title required') 203 | } else { 204 | model.get(id, function (err, doc) { 205 | if (err) { 206 | console.log(err) 207 | } else { 208 | doc.title = elements['title'].value 209 | if (document.body.getAttribute('data-list-id')) { 210 | var checked = document.getElementById('checked-item-' + sanitize(id)) 211 | doc.checked = checked ? !!checked.checked : false 212 | } 213 | model.save(doc, function (err, updated) { 214 | if (err) { 215 | console.error(err) 216 | } else { 217 | addToList([doc]) 218 | } 219 | }) 220 | } 221 | }) 222 | } 223 | } 224 | 225 | shopper.toggle = function (node, event) { 226 | if (event) { 227 | event.stopPropagation() 228 | } 229 | if (typeof node === 'string') { 230 | var nodes = document.querySelectorAll('#' + node + ' .collapsible') 231 | for (var i = 0; i < nodes.length; i++) { 232 | if (nodes[i].classList) { 233 | nodes[i].classList.toggle('closed') 234 | } 235 | } 236 | } else { 237 | node.classList.toggle('closed') 238 | } 239 | } 240 | 241 | shopper.goto = function (listid, title, event) { 242 | if (event) { 243 | event.stopPropagation() 244 | } 245 | if (listid) { 246 | model.items(listid, function (err, docs) { 247 | if (err) { 248 | console.error(err) 249 | } else { 250 | document.getElementById('header-title').innerText = title 251 | document.body.setAttribute('data-list-id', listid) 252 | document.body.scrollTop = 0 253 | document.documentElement.scrollTop = 0 254 | docs.sort(function (a, b) { 255 | return a.title < b.title 256 | }) 257 | addToList(docs, true) 258 | } 259 | }) 260 | } else { 261 | document.body.removeAttribute('data-list-id') 262 | document.getElementById('header-title').innerText = 'Shopping List' 263 | } 264 | } 265 | 266 | window.shopper = shopper 267 | }()) 268 | -------------------------------------------------------------------------------- /tutorial/step-07/shoppinglist.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict' 3 | 4 | var model = null 5 | 6 | // make doc id friendlier for using as DOM node id 7 | var sanitize = function (id) { 8 | return id.replace(/[:.]/gi, '-') 9 | } 10 | 11 | // add docs to DOM node list 12 | var addToList = function (docs, clear) { 13 | if (clear) { 14 | if (document.body.getAttribute('data-list-id')) { 15 | document.getElementById('shopping-list-items').innerHTML = '' 16 | } else { 17 | document.getElementById('shopping-lists').innerHTML = '' 18 | } 19 | } 20 | for (var i = 0; i < docs.length; i++) { 21 | var doc = docs[i] 22 | 23 | var isItem = doc.type === 'item' || doc._id.indexOf('item:') === 0 24 | var isList = doc.type === 'list' || doc._id.indexOf('list:') === 0 25 | var shoppinglists = null 26 | 27 | if (isList) { 28 | shoppinglists = document.getElementById('shopping-lists') 29 | } else if (isItem) { 30 | shoppinglists = document.getElementById('shopping-list-items') 31 | } else { 32 | continue 33 | } 34 | 35 | doc._sanitizedid = sanitize(doc._id) 36 | doc._checked = doc.checked ? 'checked="checked"' : '' 37 | 38 | var template = document.getElementById(isItem ? 'shopping-list-item-template' : 'shopping-list-template').innerHTML 39 | template = template.replace(/\{\{(.+?)\}\}/g, function ($0, $1) { 40 | var fields = ($1).split('.') 41 | var value = doc 42 | while (fields.length) { 43 | if (value.hasOwnProperty(fields[0])) { 44 | value = value[fields.shift()] 45 | } else { 46 | value = null 47 | break 48 | } 49 | } 50 | return value || '' 51 | }) 52 | 53 | var listdiv = document.createElement(isItem ? 'li' : 'div') 54 | listdiv.id = doc._sanitizedid 55 | listdiv.className = 'card ' + (isItem ? 'collection-item' : 'collapsible') 56 | listdiv.innerHTML = template 57 | 58 | var existingdiv = document.getElementById(doc._sanitizedid) 59 | if (existingdiv) { 60 | shoppinglists.replaceChild(listdiv, existingdiv) 61 | } else { 62 | shoppinglists.insertBefore(listdiv, shoppinglists.firstChild) 63 | } 64 | 65 | if (isItem) { 66 | updateItemCount(doc.list) 67 | } else { 68 | updateItemCount(doc._id) 69 | } 70 | } 71 | } 72 | 73 | // remove from DOM node list 74 | var removeFromList = function (id) { 75 | var list = document.getElementById(sanitize(id)) 76 | shopper.toggle(list) 77 | list.parentElement.removeChild(list) 78 | 79 | var listid = document.body.getAttribute('data-list-id') 80 | if (listid) { 81 | updateItemCount(listid) 82 | } 83 | } 84 | 85 | // figure out the checked items count for a list 86 | var updateItemCount = function (listid) { 87 | model.get(listid, function (err, doc) { 88 | if (err) { 89 | console.log(err) 90 | } else { 91 | model.items(listid, function (err, items) { 92 | if (err) { 93 | console.log(err) 94 | } else { 95 | var checked = 0 96 | for (var i = 0; i < items.length; i++) { 97 | checked += items[i].checked ? 1 : 0 98 | } 99 | var node = document.getElementById('checked-list-' + sanitize(listid)) 100 | if (node) { 101 | node.nextElementSibling.innerText = items.length ? (checked + ' of ' + items.length + ' items checked') : '0 items' 102 | node.checked = checked && checked === items.length 103 | if ((doc.checked && checked !== items.length) || 104 | (!doc.checked && checked === items.length)) { 105 | doc.checked = checked === items.length 106 | model.save(doc) 107 | } 108 | } 109 | } 110 | }) 111 | } 112 | }) 113 | } 114 | 115 | var shopper = function (themodel) { 116 | if (themodel) { 117 | themodel(function (err, response) { 118 | if (err) { 119 | console.error(err) 120 | } else { 121 | model = response 122 | model.lists(function (err, docs) { 123 | if (err) { 124 | console.error(err) 125 | } else { 126 | addToList(docs, true) 127 | } 128 | console.log('shopper ready!') 129 | }) 130 | } 131 | }) 132 | } 133 | return this 134 | } 135 | 136 | shopper.openadd = function () { 137 | var form = null 138 | if (document.body.getAttribute('data-list-id')) { 139 | form = document.getElementById('shopping-list-item-add') 140 | } else { 141 | form = document.getElementById('shopping-list-add') 142 | } 143 | form.reset() 144 | document.body.className += ' ' + form.id 145 | } 146 | 147 | shopper.closeadd = function () { 148 | document.body.className = document.body.className 149 | .replace('shopping-list-add', '') 150 | .replace('shopping-list-item-add', '') 151 | .trim() 152 | } 153 | 154 | shopper.add = function (event) { 155 | var form = event.target 156 | var elements = form.elements 157 | var doc = {} 158 | var listid = document.body.getAttribute('data-list-id') 159 | 160 | if (!elements['title'].value) { 161 | console.error('title required') 162 | } else if (listid && form.id.indexOf('list-item') === -1) { 163 | console.error('incorrect form') 164 | } else if (!listid && form.id.indexOf('list-item') > -1) { 165 | console.error('list id required') 166 | } else { 167 | for (var i = 0; i < elements.length; i++) { 168 | if (elements[i].tagName.toLowerCase() !== 'button') { 169 | doc[elements[i].name] = elements[i].value 170 | } 171 | } 172 | 173 | if (listid) { 174 | doc['list'] = listid 175 | } 176 | 177 | model.save(doc, function (err, updated) { 178 | if (err) { 179 | console.error(err) 180 | } else { 181 | doc._id = doc._id || updated._id || updated.id 182 | addToList([doc]) 183 | shopper.closeadd() 184 | } 185 | }) 186 | } 187 | } 188 | 189 | shopper.remove = function (id) { 190 | model.remove(id, function (err, response) { 191 | if (err) { 192 | console.log(err) 193 | } else { 194 | removeFromList(id) 195 | } 196 | }) 197 | } 198 | 199 | shopper.update = function (id) { 200 | var elements = document.getElementById('form-' + sanitize(id)).elements 201 | if (!elements['title'].value) { 202 | console.error('title required') 203 | } else { 204 | model.get(id, function (err, doc) { 205 | if (err) { 206 | console.log(err) 207 | } else { 208 | doc.title = elements['title'].value 209 | if (document.body.getAttribute('data-list-id')) { 210 | var checked = document.getElementById('checked-item-' + sanitize(id)) 211 | doc.checked = checked ? !!checked.checked : false 212 | } 213 | model.save(doc, function (err, updated) { 214 | if (err) { 215 | console.error(err) 216 | } else { 217 | addToList([doc]) 218 | } 219 | }) 220 | } 221 | }) 222 | } 223 | } 224 | 225 | shopper.toggle = function (node, event) { 226 | if (event) { 227 | event.stopPropagation() 228 | } 229 | if (typeof node === 'string') { 230 | var nodes = document.querySelectorAll('#' + node + ' .collapsible') 231 | for (var i = 0; i < nodes.length; i++) { 232 | if (nodes[i].classList) { 233 | nodes[i].classList.toggle('closed') 234 | } 235 | } 236 | } else { 237 | node.classList.toggle('closed') 238 | } 239 | } 240 | 241 | shopper.goto = function (listid, title, event) { 242 | if (event) { 243 | event.stopPropagation() 244 | } 245 | if (listid) { 246 | model.items(listid, function (err, docs) { 247 | if (err) { 248 | console.error(err) 249 | } else { 250 | document.getElementById('header-title').innerText = title 251 | document.body.setAttribute('data-list-id', listid) 252 | document.body.scrollTop = 0 253 | document.documentElement.scrollTop = 0 254 | docs.sort(function (a, b) { 255 | return a.title < b.title 256 | }) 257 | addToList(docs, true) 258 | } 259 | }) 260 | } else { 261 | document.body.removeAttribute('data-list-id') 262 | document.getElementById('header-title').innerText = 'Shopping List' 263 | } 264 | } 265 | 266 | window.shopper = shopper 267 | }()) 268 | -------------------------------------------------------------------------------- /tutorial/step-07/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | Shopping List | vanilla JavaScript | PouchDB 32 | 33 | 34 | 35 | 45 | 46 | 47 |
48 |
49 | 50 |
51 | 52 | 55 | 56 | 57 | 60 |
61 | 62 | 63 | 80 | 81 | 82 | 99 | 100 | 101 | 102 | 103 | 104 | 136 | 137 | 138 | 164 | 165 | 166 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | -------------------------------------------------------------------------------- /tutorial/step-08/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | Shopping List | vanilla JavaScript | PouchDB 32 | 33 | 34 | 35 | 47 | 48 | 49 |
50 |
51 | 52 |
53 | 54 | 57 | 58 | 59 | 62 |
63 | 64 | 65 | 82 | 83 | 84 | 101 | 102 | 103 | 121 | 122 | 123 | 124 | 125 | 126 | 158 | 159 | 160 | 186 | 187 | 188 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | -------------------------------------------------------------------------------- /js/shoppinglist.model.js: -------------------------------------------------------------------------------- 1 | /* global cuid, PouchDB */ 2 | 3 | (function () { 4 | 'use strict' 5 | 6 | // PouchDB 7 | var db = null 8 | var dbsync = null 9 | 10 | /** 11 | * Create a shopping list object corresponding to the Shopping List Schema 12 | * https://github.com/ibm-watson-data-lab/shopping-list#shopping-list-example 13 | * 14 | * @param {Object} doc 15 | * Properties of the shopping list 16 | * @return {Object} 17 | * The document to store in the DB 18 | */ 19 | var initListDoc = function (doc) { 20 | return { 21 | '_id': 'list:' + cuid(), 22 | 'type': 'list', 23 | 'version': 1, 24 | 'title': doc.title, 25 | 'checked': !!doc.checked, 26 | 'place': { 27 | 'title': doc.place ? doc.place.title : '', 28 | 'license': doc.place ? doc.place.license : '', 29 | 'lat': doc.place ? doc.place.lat : null, 30 | 'lon': doc.place ? doc.place.lon : null, 31 | 'address': doc.place ? doc.place.address : {} 32 | }, 33 | 'createdAt': new Date().toISOString(), 34 | 'updatedAt': '' 35 | } 36 | } 37 | 38 | /** 39 | * Create a shopping list item object corresponding to the Shopping List Item Schema 40 | * https://github.com/ibm-watson-data-lab/shopping-list#shopping-list-item-example 41 | * 42 | * @param {Object} doc 43 | * Properties of the shopping list item 44 | * @param {String} listid 45 | * The ID of the parent shopping list 46 | * @return {Object} 47 | * The document to store in the DB 48 | */ 49 | var initItemDoc = function (doc, listid) { 50 | return { 51 | '_id': 'item:' + cuid(), 52 | 'type': 'item', 53 | 'version': 1, 54 | 'list': doc.list || listid, 55 | 'title': doc.title, 56 | 'checked': !!doc.checked, 57 | 'createdAt': new Date().toISOString(), 58 | 'updatedAt': '' 59 | } 60 | } 61 | 62 | /** 63 | * Log and handle responses from DB requests 64 | * 65 | * @param {Object} error 66 | * Any error in handling the request 67 | * @param {Object} response 68 | * The response from the request 69 | * @param {Function} callback 70 | * Callback function to be called 71 | * @param {String} caller 72 | * Name or unique identifier of the initial calling function (for logging purproses) 73 | */ 74 | var handleResponse = function (error, response, callback, caller) { 75 | if (console) { 76 | console[error ? 'error' : 'log']((caller || ''), (error || response)) 77 | } 78 | if (typeof callback === 'function') { 79 | callback((error ? error.message || error : error), response) 80 | } 81 | } 82 | 83 | /** 84 | * Initiates the shopping list model object (creates DB and index) 85 | * 86 | * @param {Function} callback 87 | * The function to be called once model is initiated or fails initiation 88 | */ 89 | var model = function (callback) { 90 | db = new PouchDB('shopping') 91 | 92 | db.info(function (err, info) { 93 | if (err) { 94 | console.error(err) 95 | } else { 96 | console.log('model.info', info) 97 | } 98 | }) 99 | 100 | db.createIndex({ 101 | index: { fields: ['type'] } 102 | }, function (err, response) { 103 | handleResponse(err, model, callback, 'model.db.createIndex') 104 | }) 105 | } 106 | 107 | /** 108 | * Finds all shopping lists in the DB 109 | * 110 | * @param {Function} callback 111 | * The function to be called with the reponse 112 | */ 113 | model.lists = function (callback) { 114 | db.find({ 115 | selector: { 116 | type: 'list' 117 | } 118 | }, function (err, response) { 119 | var docs = response ? response.docs || response : response 120 | handleResponse(err, docs, callback, 'model.lists') 121 | }) 122 | } 123 | 124 | /** 125 | * Store the shopping list doc or shopping list item doc into the DB 126 | * 127 | * @param {Object} d 128 | * The shopping list or shopping list item to be stored 129 | * @param {Function} callback 130 | * The function to be called with the reponse 131 | */ 132 | model.save = function (d, callback) { 133 | var doc = null 134 | if (d._id) { 135 | doc = d 136 | doc['updatedAt'] = new Date().toISOString() 137 | } else if (d.type === 'list') { 138 | doc = initListDoc(d) 139 | } else if (d.type === 'item') { 140 | doc = initItemDoc(d) 141 | } 142 | 143 | if (doc) { 144 | db.put(doc, function (err, response) { 145 | handleResponse(err, response, callback, 'model.save') 146 | }) 147 | } else { 148 | handleResponse(new Error('Missing or unsupport doc type'), null, callback, 'model.save') 149 | } 150 | } 151 | 152 | /** 153 | * Retrieve a shopping list or shopping list item from the DB 154 | * 155 | * @param {String} id 156 | * The ID of shopping list or shopping list item to be retrieved 157 | * @param {Function} callback 158 | * The function to be called with the reponse 159 | */ 160 | model.get = function (id, callback) { 161 | db.get(id, function (err, doc) { 162 | handleResponse(err, doc, callback, 'model.get') 163 | }) 164 | } 165 | 166 | /** 167 | * Delete a shopping list or shopping list item from the DB 168 | * 169 | * @param {String} id 170 | * The ID of shopping list or shopping list item to be deleted 171 | * @param {Function} callback 172 | * The function to be called with the reponse 173 | */ 174 | model.remove = function (id, callback) { 175 | // delet doc using with the given id and revision 176 | function deleteRev (rev) { 177 | db.remove(id, rev, function (err, response) { 178 | handleResponse(err, response, callback, 'model.remove') 179 | }) 180 | } 181 | 182 | if (id) { 183 | // get the doc to be deleted 184 | db.get(id, function (err, doc) { 185 | if (err) { 186 | handleResponse(err, doc, callback, 'model.remove.get') 187 | } else if (doc.type === 'list') { 188 | // 1. get all shopping list items belonging to the shopping list 189 | model.items(doc._id, function (err, response) { 190 | if (err) { 191 | console.error('model.remove.items', err) 192 | deleteRev(doc._rev) 193 | } else { 194 | var items = response ? response.docs || response : response 195 | if (items && items.length) { 196 | var markfordeletion = items.map(function (item) { 197 | item._deleted = true 198 | return item 199 | }) 200 | // 2. delete all shopping list items 201 | db.bulkDocs(markfordeletion, function (err, response) { 202 | if (err) { 203 | console.error('model.remove.bulkDocs', err) 204 | } 205 | // 3. delete shopping list 206 | deleteRev(doc._rev) 207 | }) 208 | } else { 209 | // delete shopping list 210 | deleteRev(doc._rev) 211 | } 212 | } 213 | }) 214 | } else { 215 | // delete shopping lsit 216 | deleteRev(doc._rev) 217 | } 218 | }) 219 | } else { 220 | handleResponse(new Error('Missing id'), null, callback, 'model.remove') 221 | } 222 | } 223 | 224 | /** 225 | * Find all shopping list items in the DB for a given shopping list 226 | * 227 | * @param {String} listid 228 | * The ID of shopping list to find the items for 229 | * @param {Function} callback 230 | * The function to be called with the reponse 231 | */ 232 | model.items = function (listid, callback) { 233 | db.find({ 234 | selector: { 235 | type: 'item', 236 | list: listid 237 | } 238 | }, function (err, response) { 239 | var docs = response ? response.docs || response : response 240 | handleResponse(err, docs, callback, 'model.items') 241 | }) 242 | } 243 | 244 | /** 245 | * Get or set the settings for the shopping list app 246 | * 247 | * @param {Object} settings 248 | * The settings to be stored in the DB (or null if retrieving settings) 249 | * @param {Function} callback 250 | * The function to be called with the reponse 251 | */ 252 | model.settings = function (settings, callback) { 253 | var id = '_local/user' 254 | var cb = callback || settings 255 | if (callback && settings && typeof settings === 'object') { 256 | // 1. get existing settings from DB 257 | db.get(id, function (err, doc) { 258 | settings._id = id 259 | if (err) { 260 | console.error(err) 261 | } else { 262 | // 2. update revision 263 | settings._rev = doc._rev 264 | } 265 | // 3. store new settings in DB 266 | db.put(settings, function (err, response) { 267 | handleResponse(err, response, cb, 'model.settings.put') 268 | }) 269 | }) 270 | } else { 271 | // get existing settings 272 | db.get(id, function (err, doc) { 273 | handleResponse(err, doc, cb, 'model.settings.get') 274 | }) 275 | } 276 | } 277 | 278 | /** 279 | * Synchronize local DB with remote DB 280 | * 281 | * @param {String} remoteDB 282 | * The fully qualified URL for the remote DB 283 | * @param {Function} oncomplete 284 | * The function to be called when sync 'complete' event is received 285 | * @param {Function} onchange 286 | * The function to be called when sync 'change' event is received 287 | */ 288 | model.sync = function (remoteDB, oncomplete, onchange) { 289 | if (dbsync) { 290 | dbsync.cancel() 291 | } 292 | 293 | if (remoteDB) { 294 | // do one-off sync from the server until completion 295 | db.sync(remoteDB) 296 | .on('complete', function (info) { 297 | handleResponse(null, info, oncomplete, 'model.sync.complete') 298 | 299 | // then two-way, continuous, retriable sync 300 | dbsync = db.sync(remoteDB, { live: true, retry: true }) 301 | .on('change', function (info) { 302 | // incoming changes only 303 | if (info.direction === 'pull' && info.change && info.change.docs) { 304 | handleResponse(null, info.change.docs, onchange, 'model.sync.change') 305 | } 306 | }) 307 | .on('error', function (err) { 308 | handleResponse(err, null, onchange, 'model.sync.change.error') 309 | }) 310 | }) 311 | .on('error', function (err) { 312 | handleResponse(err, null, oncomplete, 'model.sync.error') 313 | }) 314 | } else if (typeof oncomplete === 'function') { 315 | oncomplete() 316 | } 317 | } 318 | 319 | window.addEventListener('DOMContentLoaded', function () { 320 | window.shopper(model) 321 | }) 322 | }()) 323 | --------------------------------------------------------------------------------