├── .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 |
20 |
27 |
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 |
20 |
27 |
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 |
69 |
--------------------------------------------------------------------------------
/tutorial/step-07/favicons/safari-pinned-tab.svg:
--------------------------------------------------------------------------------
1 |
2 |
4 |
69 |
--------------------------------------------------------------------------------
/tutorial/step-08/favicons/safari-pinned-tab.svg:
--------------------------------------------------------------------------------
1 |
2 |
4 |
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 |
20 |
27 |
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 |
--------------------------------------------------------------------------------