├── .eslintignore ├── .dockerignore ├── .gitignore ├── server ├── views │ ├── index.handlebars │ ├── url-1.handlebars │ ├── url-2.handlebars │ ├── layouts │ │ ├── default.handlebars │ │ └── app-shell.handlebars │ └── partials │ │ ├── close-page.handlebars │ │ ├── async-css.handlebars │ │ └── open-page.handlebars ├── controllers │ ├── static-page-controller.js │ ├── api-controller.js │ └── server-controller.js └── models │ └── path-config.js ├── src ├── favicon.ico ├── images │ ├── icon-128x128.png │ ├── side-nav-bg@2x.jpg │ ├── apple-touch-icon.png │ ├── chrome-touch-icon-192x192.png │ ├── chrome-splashscreen-icon-384x384.png │ ├── ms-touch-icon-144x144-precomposed.png │ ├── ic_add_24px.svg │ ├── ic_menu_24px.svg │ └── ic_info_outline_24px.svg ├── scripts │ ├── core.es6.js │ ├── static-page.es6.js │ ├── controller │ │ ├── StaticPageController.js │ │ ├── ApplicationController.js │ │ ├── Controller.js │ │ └── PageController.js │ ├── libs │ │ ├── ToasterSingleton.js │ │ └── RouterSingleton.js │ └── view │ │ └── NavDrawerView.js ├── styles │ ├── core │ │ ├── _colors.scss │ │ ├── _card.scss │ │ ├── _toast.scss │ │ ├── _z-index.scss │ │ ├── _loader.scss │ │ ├── _main.scss │ │ ├── _dialog.scss │ │ ├── _header.scss │ │ ├── _side-nav.scss │ │ └── _core.scss │ └── core.scss ├── manifest.json ├── third_party │ └── serviceworker-cache-polyfill.es5.js └── old-sw.es6.js ├── gulp-tasks ├── nodemon.js ├── clean.js ├── bump.js ├── html.js ├── copy.js ├── images.js ├── watch.js ├── service-worker.js ├── styles.js └── scripts.js ├── app.js ├── CONTRIBUTING.md ├── package.json ├── .eslintrc ├── app.yaml ├── gulpfile.js ├── Dockerfile ├── README.md └── LICENSE /.eslintignore: -------------------------------------------------------------------------------- 1 | src/third_party 2 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .dockerignore 3 | Dockerfile 4 | .git 5 | .hg 6 | .svn -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | *.pyc 4 | dist 5 | tests 6 | npm-debug.log 7 | -------------------------------------------------------------------------------- /server/views/index.handlebars: -------------------------------------------------------------------------------- 1 |
2 |

Index

3 |
4 | -------------------------------------------------------------------------------- /server/views/url-1.handlebars: -------------------------------------------------------------------------------- 1 |
2 |

URL 1

3 |
4 | -------------------------------------------------------------------------------- /server/views/url-2.handlebars: -------------------------------------------------------------------------------- 1 |
2 |

URL 2

3 |
4 | -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/download/application-shell/master/src/favicon.ico -------------------------------------------------------------------------------- /src/images/icon-128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/download/application-shell/master/src/images/icon-128x128.png -------------------------------------------------------------------------------- /src/images/side-nav-bg@2x.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/download/application-shell/master/src/images/side-nav-bg@2x.jpg -------------------------------------------------------------------------------- /src/images/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/download/application-shell/master/src/images/apple-touch-icon.png -------------------------------------------------------------------------------- /src/images/chrome-touch-icon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/download/application-shell/master/src/images/chrome-touch-icon-192x192.png -------------------------------------------------------------------------------- /src/images/chrome-splashscreen-icon-384x384.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/download/application-shell/master/src/images/chrome-splashscreen-icon-384x384.png -------------------------------------------------------------------------------- /src/images/ms-touch-icon-144x144-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/download/application-shell/master/src/images/ms-touch-icon-144x144-precomposed.png -------------------------------------------------------------------------------- /server/views/layouts/default.handlebars: -------------------------------------------------------------------------------- 1 | {{> open-page}} 2 |
3 | {{{body}}} 4 |
5 | {{> close-page}} 6 | -------------------------------------------------------------------------------- /src/images/ic_add_24px.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/images/ic_menu_24px.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /gulp-tasks/nodemon.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'); 2 | var nodemon = require('gulp-nodemon'); 3 | var env = require('gulp-env'); 4 | 5 | gulp.task('nodemon', function() { 6 | env({ 7 | vars: { 8 | PORT: GLOBAL.config.port 9 | } 10 | }); 11 | 12 | return nodemon({ 13 | script: 'app.js' 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /src/images/ic_info_outline_24px.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /server/views/layouts/app-shell.handlebars: -------------------------------------------------------------------------------- 1 | {{> open-page}} 2 |
3 | 4 |
5 | 6 | 7 | 8 | 9 | 14 | 15 | {{> close-page}} 16 | -------------------------------------------------------------------------------- /src/scripts/core.es6.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2015 Google Inc. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of 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, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | import ApplicationController from './controller/ApplicationController'; 19 | 20 | new ApplicationController(); 21 | -------------------------------------------------------------------------------- /src/scripts/static-page.es6.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2015 Google Inc. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of 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, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | import StaticPageController from './controller/StaticPageController'; 19 | 20 | new StaticPageController(); 21 | -------------------------------------------------------------------------------- /server/views/partials/close-page.handlebars: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |

App shell

5 |
6 | 7 |
8 | Index 9 | URL 1 10 | URL 2 11 |
12 | 13 |
Version @VERSION@
14 |
15 |
16 | 17 | 18 | 19 | {{> async-css }} 20 | 21 | {{#each data.remoteScripts}} 22 | 23 | {{~/each}} 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/styles/core/_colors.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Copyright 2015 Google Inc. All rights reserved. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | $primary: #3F51B5; 19 | $primaryLight: #C5CAE9; 20 | $primaryDark: #303F9F; 21 | $secondary: #FF4081; 22 | $secondaryDark: #4527A0; 23 | $toast: #404040; 24 | $background: #FAFAFA; 25 | -------------------------------------------------------------------------------- /server/controllers/static-page-controller.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var pathConfigs = require('../models/path-config.js'); 4 | 5 | function StaticPageController() { 6 | 7 | } 8 | 9 | // This method looks at the request path and renders the appropriate handlebars 10 | // template 11 | StaticPageController.prototype.onRequest = function(req, res) { 12 | var pathConfig = pathConfigs.getConfig(req.path); 13 | if (!pathConfig) { 14 | res.status(404).send(); 15 | return; 16 | } 17 | 18 | 19 | switch (req.path) { 20 | case '/app-shell': 21 | // Render with app-shell layout and include no initial content 22 | pathConfig.layout = 'app-shell'; 23 | res.render('', pathConfig); 24 | return; 25 | default: 26 | // Use default layout 27 | res.render(pathConfig.data.view, pathConfig); 28 | return; 29 | } 30 | }; 31 | 32 | module.exports = StaticPageController; 33 | -------------------------------------------------------------------------------- /gulp-tasks/clean.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Copyright 2015 Google Inc. All rights reserved. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | var gulp = require('gulp'); 19 | var del = require('del'); 20 | 21 | gulp.task('clean', function(cb) { 22 | del([GLOBAL.config.dest], {dot: true}) 23 | .then(function() { 24 | cb(); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /src/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "App shell", 3 | "name": "App shell", 4 | "start_url": "./?utm_source=web_app_manifest", 5 | "icons": [{ 6 | "src": "images/icon-128x128.png", 7 | "sizes": "128x128", 8 | "type": "image/png" 9 | }, { 10 | "src": "images/apple-touch-icon.png", 11 | "sizes": "152x152", 12 | "type": "image/png" 13 | }, { 14 | "src": "images/ms-touch-icon-144x144-precomposed.png", 15 | "sizes": "144x144", 16 | "type": "image/png" 17 | }, { 18 | "src": "images/chrome-touch-icon-192x192.png", 19 | "sizes": "192x192", 20 | "type": "image/png" 21 | },{ 22 | "src": "images/chrome-splashscreen-icon-384x384.png", 23 | "sizes": "384x384", 24 | "type": "image/png" 25 | }], 26 | "display": "standalone", 27 | "orientation": "portrait", 28 | "background_color": "#3E4EB8", 29 | "theme_color": "#2E3AA1" 30 | } 31 | -------------------------------------------------------------------------------- /src/styles/core.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Copyright 2015 Google Inc. All rights reserved. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | @import 'core/_colors'; 19 | @import 'core/_card'; 20 | @import 'core/_core'; 21 | @import 'core/_dialog'; 22 | @import 'core/_side-nav'; 23 | @import 'core/_main'; 24 | @import 'core/_header'; 25 | @import 'core/_loader'; 26 | @import 'core/_z-index'; 27 | @import 'core/_toast'; 28 | -------------------------------------------------------------------------------- /src/styles/core/_card.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Copyright 2015 Google Inc. All rights reserved. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | .card { 19 | padding: 16px; 20 | position: relative; 21 | box-sizing: border-box; 22 | background: #fff; 23 | border-radius: 2px; 24 | margin: 16px; 25 | box-shadow: 0 2px 2px 0 rgba(0,0,0,.14), 26 | 0 3px 1px -2px rgba(0,0,0,.2), 27 | 0 1px 5px 0 rgba(0,0,0,.12); 28 | } 29 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * 5 | * The structure of this node server is the following: 6 | * 1.) server-controller starts an express server which defines the static 7 | * content, port number and sets up handle bars to use the views and layouts 8 | * 2.) URLs are then routed by defining a url and calling addEndpoint(). On 9 | * requests to a matching url the onRequest() method is called in the 10 | * passed in controller (i.e. StaticPageController) 11 | * 12 | */ 13 | 14 | var serverController = require('./server/controllers/server-controller'); 15 | var StaticPageController = require( 16 | './server/controllers/static-page-controller'); 17 | var APIController = require('./server/controllers/api-controller'); 18 | 19 | // APIController serves up the HTML without any HTML body or head 20 | serverController.addEndpoint('/api*', new APIController( 21 | serverController.getHandleBarsInstance() 22 | )); 23 | // The static page controller serves the basic form of the pages 24 | serverController.addEndpoint('/*', new StaticPageController()); 25 | serverController.startServer(process.env.PORT); 26 | -------------------------------------------------------------------------------- /gulp-tasks/bump.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Copyright 2015 Google Inc. All rights reserved. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | var gulp = require('gulp'); 19 | var bump = require('gulp-bump'); 20 | var fs = require('fs'); 21 | 22 | gulp.task('update-pkg', function() { 23 | return gulp.src('./package.json') 24 | .pipe(bump({type:'patch'})) 25 | .pipe(gulp.dest('./')); 26 | }); 27 | 28 | gulp.task('bump', ['update-pkg'], function(cb) { 29 | GLOBAL.config.version = 30 | JSON.parse(fs.readFileSync('./package.json', 'utf8')).version; 31 | cb(); 32 | }); 33 | -------------------------------------------------------------------------------- /server/views/partials/async-css.handlebars: -------------------------------------------------------------------------------- 1 | {{#if data.remoteStyles}} 2 | 26 | 27 | 28 | 33 | {{~/if}} 34 | -------------------------------------------------------------------------------- /src/scripts/controller/StaticPageController.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Copyright 2015 Google Inc. All rights reserved. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | import Controller from './Controller'; 19 | import NavDrawerView from './../view/NavDrawerView'; 20 | 21 | export default class StaticPageController extends Controller { 22 | 23 | constructor() { 24 | super(); 25 | 26 | var navDrawer = new NavDrawerView(); 27 | 28 | var sideNavToggleButton = document.querySelector('.js-toggle-menu'); 29 | sideNavToggleButton.addEventListener('click', () => { 30 | navDrawer.toggle(); 31 | }); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /gulp-tasks/html.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Copyright 2015 Google Inc. All rights reserved. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the 'License'); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an 'AS IS' BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | var gulp = require('gulp'); 19 | var gulpif = require('gulp-if'); 20 | var minifyHtml = require('gulp-minify-html'); 21 | var replace = require('gulp-replace'); 22 | 23 | gulp.task('html:watch', function() { 24 | gulp.watch(GLOBAL.config.src + '/**/*.html', ['html']); 25 | }); 26 | 27 | gulp.task('html', function() { 28 | return gulp.src([ 29 | GLOBAL.config.src + '/**/*.html', 30 | ]) 31 | .pipe(gulpif(GLOBAL.config.env == 'prod', minifyHtml())) 32 | .pipe(replace(/@VERSION@/g, GLOBAL.config.version)) 33 | .pipe(gulp.dest(config.dest)); 34 | }); 35 | -------------------------------------------------------------------------------- /src/styles/core/_toast.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Copyright 2015 Google Inc. All rights reserved. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | .toast-view { 19 | background-color: $toast; 20 | border-radius: 3px; 21 | box-shadow: 0 0 2px rgba(0,0,0,.12), 22 | 0 2px 4px rgba(0,0,0,.24); 23 | color: #fff; 24 | line-height: 20px; 25 | margin-top: 8px; 26 | padding: 16px; 27 | transition: opacity 200ms, 28 | transform 300ms cubic-bezier(0.165,0.840,0.440,1.000); 29 | white-space: nowrap; 30 | opacity: 0; 31 | transform: translateY(20px); 32 | will-change: transform; 33 | position: fixed; 34 | left: 16px; 35 | bottom: 16px; 36 | } 37 | 38 | .toast-view--visible { 39 | opacity: 1; 40 | transform: translateY(0); 41 | } 42 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to become a contributor and submit your own code 2 | 3 | ## Contributor License Agreements 4 | 5 | We'd love to accept your sample apps and patches! Before we can take them, we have to jump a couple of legal hurdles. 6 | 7 | Please fill out either the individual or corporate Contributor License Agreement (CLA). 8 | * If you are an individual writing original source code and you're sure you own the intellectual property, then you'll need to sign an [individual CLA](https://developers.google.com/open-source/cla/individual). 9 | * If you work for a company that wants to allow you to contribute your work, then you'll need to sign a [corporate CLA](https://developers.google.com/open-source/cla/corporate). 10 | 11 | Follow either of the two links above to access the appropriate CLA and instructions for how to sign and return it. Once we receive it, we'll be able to 12 | accept your pull requests. 13 | 14 | ## Contributing A Patch 15 | 16 | 1. Submit an issue describing your proposed change to the repo in question. 17 | 1. The repo owner will respond to your issue promptly. 18 | 1. If your proposed change is accepted, and you haven't already done so, sign a Contributor License Agreement (see details above). 19 | 1. Fork the desired repo, develop and test your code changes. 20 | 1. Ensure that your code adheres to the existing style in the sample to which you are contributing. 21 | 1. Submit a pull request. 22 | -------------------------------------------------------------------------------- /gulp-tasks/copy.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Copyright 2015 Google Inc. All rights reserved. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | var gulp = require('gulp'); 19 | var runSequence = require('run-sequence'); 20 | var del = require('del'); 21 | 22 | gulp.task('copy:watch', function() { 23 | gulp.watch(GLOBAL.config.src + '/*.*', ['copy:root']); 24 | }); 25 | 26 | gulp.task('copy:cleanRoot', function(cb) { 27 | del([GLOBAL.config.dest + '/*.{json,txt,ico}'], {dot: true}) 28 | .then(function() { 29 | cb(); 30 | }); 31 | }); 32 | 33 | gulp.task('copy:root', ['copy:cleanRoot'], function() { 34 | return gulp.src([ 35 | GLOBAL.config.src + '/*.{json,txt,ico}', 36 | ]) 37 | .pipe(gulp.dest(GLOBAL.config.dest)); 38 | }); 39 | 40 | gulp.task('copy', function(cb) { 41 | runSequence( 42 | 'copy:root', 43 | cb); 44 | }); 45 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "app-shell", 3 | "version": "0.2.0", 4 | "private": true, 5 | "license": "Apache Version 2.0", 6 | "engines": { 7 | "node": ">=4.1.0" 8 | }, 9 | "scripts": { 10 | "start": "node app.js", 11 | "monitor": "gulp dev", 12 | "postinstall": "gulp", 13 | "deploy": "gcloud preview app deploy app.yaml --promote" 14 | }, 15 | "main": "app.js", 16 | "dependencies": { 17 | "babel-preset-es2015": "^6.0.15", 18 | "babelify": "^7.2.0", 19 | "browserify": "^11.2.0", 20 | "del": "^2.0.2", 21 | "express": "^4.13.3", 22 | "express-handlebars": "^2.0.1", 23 | "gcloud": "^0.24.1", 24 | "glob": "^5.0.15", 25 | "gulp": "^3.9.0", 26 | "gulp-autoprefixer": "^3.1.0", 27 | "gulp-bump": "^1.0.0", 28 | "gulp-env": "^0.2.0", 29 | "gulp-eslint": "^1.0.0", 30 | "gulp-if": "^2.0.0", 31 | "gulp-imagemin": "^2.3.0", 32 | "gulp-license": "^1.0.0", 33 | "gulp-minify-css": "^1.2.1", 34 | "gulp-minify-html": "^1.0.4", 35 | "gulp-nodemon": "^2.0.4", 36 | "gulp-rename": "^1.2.2", 37 | "gulp-replace": "^0.5.4", 38 | "gulp-sass": "^2.0.4", 39 | "gulp-sourcemaps": "^1.6.0", 40 | "gulp-streamify": "^1.0.2", 41 | "gulp-uglify": "^1.4.2", 42 | "gulp-util": "^3.0.6", 43 | "require-dir": "^0.3.0", 44 | "run-sequence": "^1.1.4", 45 | "sw-precache": "^2.2.0", 46 | "vinyl-source-stream": "^1.1.0" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /server/views/partials/open-page.handlebars: -------------------------------------------------------------------------------- 1 | 18 | 19 | 20 | 21 | 22 | 23 | {{data.title}} 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 |
37 | 40 | 41 |

App Shell

42 |
43 | -------------------------------------------------------------------------------- /gulp-tasks/images.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Copyright 2015 Google Inc. All rights reserved. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the 'License'); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an 'AS IS' BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | var gulp = require('gulp'); 18 | var del = require('del'); 19 | var gulpif = require('gulp-if'); 20 | var imagemin = require('gulp-imagemin'); 21 | 22 | gulp.task('images:watch', function() { 23 | gulp.watch(GLOBAL.config.src + '/images/**/*.*', ['images']); 24 | }); 25 | 26 | gulp.task('images:clean', function(cb) { 27 | del([GLOBAL.config.dest + '/*.{png,jpg,jpeg,gif,svg}'], {dot: true}) 28 | .then(function() { 29 | cb(); 30 | }); 31 | }); 32 | 33 | gulp.task('images', ['images:clean'], function() { 34 | return gulp.src(GLOBAL.config.src + '/**/*.{png,jpg,jpeg,gif,svg}') 35 | .pipe(gulpif(GLOBAL.config.env == 'prod', imagemin({ 36 | progressive: true, 37 | interlaced: true, 38 | svgoPlugins: [{removeViewBox: false}], 39 | }))) 40 | .pipe(gulp.dest(GLOBAL.config.dest)); 41 | }); 42 | -------------------------------------------------------------------------------- /src/scripts/libs/ToasterSingleton.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2014 Google Inc. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of 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, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | export default class ToasterSingleton { 18 | static getToaster() { 19 | if (typeof window.ToasterInstance_ !== 'undefined') { 20 | return window.ToasterInstance_; 21 | } 22 | 23 | window.ToasterInstance_ = new Toaster(); 24 | 25 | return window.ToasterInstance_; 26 | } 27 | } 28 | 29 | class Toaster { 30 | 31 | constructor() { 32 | this.view = document.querySelector('.js-toast-view'); 33 | this.hideTimeout = 0; 34 | } 35 | 36 | toast(message) { 37 | this.view.textContent = message; 38 | this.view.classList.add('toast-view--visible'); 39 | 40 | clearTimeout(this.hideTimeout); 41 | this.hideTimeout = setTimeout(() => { 42 | this.hide(); 43 | }, 3000); 44 | } 45 | 46 | hide() { 47 | this.view.classList.remove('toast-view--visible'); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /server/controllers/api-controller.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var path = require('path'); 4 | 5 | var pathConfigs = require('../models/path-config.js'); 6 | 7 | function APIController(handlebarsInstance) { 8 | this.handlebarsInstance = handlebarsInstance; 9 | } 10 | 11 | // This method looks at the request path and renders the appropriate handlebars 12 | // template 13 | APIController.prototype.onRequest = function(req, res) { 14 | var urlSections = req.path.split('/'); 15 | urlSections = urlSections.filter(function(sectionString) { 16 | return sectionString.length > 0; 17 | }); 18 | 19 | var urlPath = null; 20 | if (urlSections.length === 1) { 21 | urlPath = '/'; 22 | } else { 23 | urlPath = '/' + urlSections[1]; 24 | } 25 | 26 | var pathConfig = pathConfigs.getConfig(urlPath); 27 | if (!pathConfig) { 28 | res.status(404).send(); 29 | return; 30 | } 31 | 32 | var viewPath = path.join( 33 | __dirname, 34 | '/../views', 35 | pathConfig.data.view + '.handlebars' 36 | ); 37 | 38 | this.handlebarsInstance.render(viewPath, pathConfig) 39 | .then(function(renderedTemplate) { 40 | res.json({ 41 | title: pathConfig.data.title, 42 | partialinlinestyles: pathConfig.data.inlineStyles, 43 | partialremotestyles: pathConfig.data.remoteStyles, 44 | partialscripts: pathConfig.data.remoteScripts, 45 | partialhtml: renderedTemplate 46 | }); 47 | }) 48 | .catch(function(err) { 49 | res.status(500).send(); 50 | }); 51 | }; 52 | 53 | module.exports = APIController; 54 | -------------------------------------------------------------------------------- /src/styles/core/_z-index.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Copyright 2015 Google Inc. All rights reserved. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | .header { 19 | z-index: 2; 20 | } 21 | 22 | .main { 23 | z-index: 0; 24 | } 25 | 26 | .new-appshelling-btn { 27 | z-index: 3; 28 | } 29 | 30 | .view-underpanel { 31 | z-index: 4; 32 | } 33 | 34 | .details-view { 35 | z-index: 6; 36 | } 37 | 38 | .edit-view__circular-reveal-container { 39 | z-index: 10; 40 | } 41 | 42 | .appshell-view { 43 | z-index: 5; 44 | } 45 | 46 | .details-view__box-reveal { 47 | z-index: 5; 48 | } 49 | 50 | .details-view__panel { 51 | z-index: 6; 52 | } 53 | 54 | .details-view__playback { 55 | z-index: 1; 56 | } 57 | 58 | .edit-view__panel { 59 | z-index: 7; 60 | } 61 | 62 | body:after { 63 | z-index: 100; 64 | } 65 | 66 | .side-nav { 67 | z-index: 200; 68 | } 69 | 70 | .dialog-view { 71 | z-index: 250; 72 | } 73 | 74 | .toast-view { 75 | z-index: 300; 76 | } 77 | 78 | .loader { 79 | z-index: 400; 80 | } 81 | -------------------------------------------------------------------------------- /src/styles/core/_loader.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Copyright 2015 Google Inc. All rights reserved. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | .loader { 19 | left: 50%; 20 | top: 50%; 21 | position: fixed; 22 | transform: translate(-50%, -50%); 23 | 24 | #spinner { 25 | box-sizing: border-box; 26 | stroke: #673AB7; 27 | stroke-width: 3px; 28 | transform-origin: 50%; 29 | 30 | animation: line 1.6s cubic-bezier(0.4, 0.0, 0.2, 1) infinite, 31 | rotate 1.6s linear infinite; 32 | } 33 | 34 | @keyframes rotate { 35 | 36 | from { 37 | transform: rotate(0) 38 | } 39 | 40 | to { 41 | transform: rotate(450deg); 42 | } 43 | } 44 | 45 | @keyframes line { 46 | 0% { 47 | stroke-dasharray: 2, 85.964; 48 | transform: rotate(0); 49 | } 50 | 51 | 50% { 52 | stroke-dasharray: 65.973, 21.9911; 53 | stroke-dashoffset: 0; 54 | } 55 | 56 | 100% { 57 | stroke-dasharray: 2, 85.964; 58 | stroke-dashoffset: -65.973; 59 | transform: rotate(90deg); 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/styles/core/_main.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Copyright 2015 Google Inc. All rights reserved. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | .main { 19 | padding-top: 72px; 20 | flex: 1; 21 | overflow-x: hidden; 22 | overflow-y: auto; 23 | -webkit-overflow-scrolling: touch; 24 | } 25 | 26 | .superfail .main { 27 | background: url(/images/superfail.svg) center center no-repeat; 28 | } 29 | 30 | .empty-set-cta { 31 | color: $primary; 32 | font-size: 20px; 33 | position: fixed; 34 | left: 0; 35 | top: 0; 36 | width: 100%; 37 | height: 100%; 38 | background: $background; 39 | opacity: 0; 40 | will-change: opacity; 41 | pointer-events: none; 42 | transition: opacity 0.333s cubic-bezier(0,0,0.21,1); 43 | 44 | display: flex; 45 | flex-direction: column; 46 | flex-wrap: nowrap; 47 | justify-content: center; 48 | align-content: center; 49 | align-items: center; 50 | } 51 | 52 | .empty-set-cta--visible { 53 | opacity: 1; 54 | } 55 | 56 | @media (min-width: 600px) { 57 | .main { 58 | padding-top: 160px; 59 | } 60 | } 61 | 62 | @media (min-width: 960px) { 63 | .main__inner { 64 | width: 960px; 65 | margin: 0 auto; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "ecmaFeatures": { "modules": true }, 3 | "rules": { 4 | "array-bracket-spacing": 2, 5 | "block-spacing": [2, "never"], 6 | "brace-style": [2, "1tbs", { "allowSingleLine": false }], 7 | "camelcase": [2, {"properties": "always"}], 8 | "curly": 2, 9 | "default-case": 2, 10 | "dot-notation": 2, 11 | "eqeqeq": 2, 12 | "indent": [ 13 | 2, 14 | 2 15 | ], 16 | "key-spacing": [2, {"beforeColon": false, "afterColon": true}], 17 | "max-len": [2, 80, 2], 18 | "new-cap": 2, 19 | "no-console": 0, 20 | "no-else-return": 2, 21 | "no-eval": 2, 22 | "no-multi-spaces": 2, 23 | "no-multiple-empty-lines": [2, {"max": 2}], 24 | "no-shadow": 2, 25 | "no-trailing-spaces": 2, 26 | "no-unused-expressions": 2, 27 | "object-curly-spacing": [2, "never"], 28 | "padded-blocks": [2, "never"], 29 | "quotes": [ 30 | 2, 31 | "single" 32 | ], 33 | "semi": [ 34 | 2, 35 | "always" 36 | ], 37 | "space-after-keywords": 2, 38 | "space-before-blocks": 2, 39 | "space-before-function-paren": [2, "never"], 40 | "spaced-comment": 2, 41 | "valid-typeof": 2, 42 | "no-unused-vars": [2, {"args": "none"}], 43 | "no-empty-class": 0, 44 | "no-extra-strict": 0, 45 | "no-reserved-keys": 0, 46 | "no-space-before-semi": 0, 47 | "no-wrap-func": 0 48 | }, 49 | "env": { 50 | "es6": true, 51 | "browser": true, 52 | "node": true 53 | }, 54 | "extends": "eslint:recommended", 55 | "globals": { 56 | "importScripts": true 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /gulp-tasks/watch.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Copyright 2015 Google Inc. All rights reserved. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | var gulp = require('gulp'); 19 | var runSequence = require('run-sequence'); 20 | 21 | /** 22 | * 23 | * This watch task looks for any gulp tasks ending in ':watch'. 24 | * This allows each file to define it's own watch task and this 25 | * will automatically pick it up and run it. 26 | * 27 | */ 28 | gulp.task('watch', function() { 29 | // Get all of the gulp task names 30 | var taskNames = Object.keys(gulp.tasks); 31 | 32 | // Store ':watch' tasks in this array 33 | var gulpWatchTasks = []; 34 | 35 | // Loop over all tasknames 36 | for (var i = 0; i < taskNames.length; i++) { 37 | var taskName = taskNames[i]; 38 | 39 | // Split tasks on the ':' character 40 | var taskParts = taskName.split(':'); 41 | 42 | // Check length is greater one to avoid selecting this task & 43 | // check if the last part is 'watch' 44 | if (taskParts.length > 1 && 45 | taskParts[taskParts.length - 1].toLowerCase() === 'watch') { 46 | // Add task to the watch tasks list 47 | gulpWatchTasks.push(taskName); 48 | } 49 | } 50 | 51 | // Run the gulp watch tasks 52 | runSequence(gulpWatchTasks); 53 | }); 54 | -------------------------------------------------------------------------------- /app.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2015, Google, Inc. 2 | # Licensed under the Apache License, Version 2.0 (the "License"); 3 | # you may not use this file except in compliance with the License. 4 | # You may obtain a copy of the License at 5 | # 6 | # http://www.apache.org/licenses/LICENSE-2.0 7 | # 8 | # Unless required by applicable law or agreed to in writing, software 9 | # distributed under the License is distributed on an "AS IS" BASIS, 10 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | # See the License for the specific language governing permissions and 12 | # limitations under the License. 13 | # 14 | # [START app_yaml] 15 | runtime: custom 16 | vm: true 17 | api_version: 1 18 | env_variables: 19 | PORT: 8080 20 | 21 | automatic_scaling: 22 | min_num_instances: 2 23 | max_num_instances: 20 24 | cool_down_period_sec: 60 25 | cpu_utilization: 26 | target_utilization: 0.5 27 | 28 | handlers: 29 | 30 | - url: / 31 | script: template.app 32 | secure: always 33 | 34 | - url: /images/(.*) 35 | static_files: dist/images/\1 36 | expiration: "365d" 37 | upload: dist/images/(.*) 38 | secure: always 39 | 40 | - url: /sw.js 41 | static_files: dist/sw.js 42 | expiration: "1s" 43 | upload: dist/sw.js 44 | secure: always 45 | 46 | - url: /favicon.ico 47 | static_files: dist/favicon.ico 48 | expiration: "365d" 49 | upload: dist/favicon.ico 50 | secure: always 51 | 52 | - url: /manifest.json 53 | static_files: dist/manifest.json 54 | expiration: "1s" 55 | upload: dist/manifest.json 56 | secure: always 57 | 58 | - url: /LICENSE 59 | static_files: dist/LICENSE 60 | expiration: "7d" 61 | mime_type: text/plain 62 | upload: dist/LICENSE 63 | secure: always 64 | 65 | - url: /(.*) 66 | script: template.app 67 | secure: always 68 | 69 | - url: /src/(.*) 70 | static_files: src/\1 71 | upload: src/(.*) 72 | secure: always 73 | # [END app_yaml] 74 | -------------------------------------------------------------------------------- /server/models/path-config.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var path = require('path'); 3 | 4 | var pathConfigs = { 5 | '/': { 6 | view: 'index', 7 | title: 'Index', 8 | inlineStyles: getFileContents(['/styles/core.css']), 9 | remoteStyles: ['https://fonts.googleapis.com/css?family=Roboto:' + 10 | '400,300,700,500,400italic'], 11 | remoteScripts: ['/scripts/static-page.js'] 12 | }, 13 | '/url-1': { 14 | view: 'url-1', 15 | title: 'URL 1', 16 | inlineStyles: getFileContents(['/styles/core.css']), 17 | remoteStyles: ['https://fonts.googleapis.com/css?family=Roboto:' + 18 | '400,300,700,500,400italic'], 19 | remoteScripts: ['/scripts/static-page.js'] 20 | }, 21 | '/url-2': { 22 | view: 'url-2', 23 | title: 'URL 2', 24 | inlineStyles: getFileContents(['/styles/core.css']), 25 | remoteStyles: ['https://fonts.googleapis.com/css?family=Roboto:' + 26 | '400,300,700,500,400italic'], 27 | remoteScripts: ['/scripts/static-page.js'] 28 | }, 29 | '/app-shell': { 30 | view: '', 31 | title: 'App Shell', 32 | inlineStyles: getFileContents(['/styles/core.css']), 33 | remoteStyles: ['https://fonts.googleapis.com/css?family=Roboto:' + 34 | '400,300,700,500,400italic'], 35 | remoteScripts: ['/scripts/core.js'] 36 | } 37 | }; 38 | 39 | function getFileContents(files) { 40 | // Concat inline styles for document 41 | var flattenedContents = ''; 42 | var pathPrefix = '/../../dist/'; 43 | files.forEach(function(file) { 44 | flattenedContents += fs.readFileSync(path.resolve(__dirname) + 45 | pathPrefix + file); 46 | }); 47 | 48 | return flattenedContents; 49 | } 50 | 51 | module.exports = { 52 | getConfig: function(urlPath) { 53 | var object = pathConfigs[urlPath]; 54 | 55 | // Check if the path is actually valid. 56 | if (!object) { 57 | return null; 58 | } 59 | 60 | return { 61 | 'data': object 62 | }; 63 | } 64 | }; 65 | -------------------------------------------------------------------------------- /gulp-tasks/service-worker.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Copyright 2015 Google Inc. All rights reserved. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the 'License'); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an 'AS IS' BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | var gulp = require('gulp'); 19 | var path = require('path'); 20 | var fs = require('fs'); 21 | var swPrecache = require('sw-precache'); 22 | 23 | // This is used as the cacheID, worth only reading the file once. 24 | var packageName = JSON.parse(fs.readFileSync('./package.json', 'utf8')).name; 25 | 26 | gulp.task('service-worker:watch', function(cb) { 27 | gulp.watch(GLOBAL.config.dest + '/**/*.*', ['service-worker']); 28 | gulp.watch(GLOBAL.config.src + '/../server/views/**/*.*', 29 | ['service-worker']); 30 | }); 31 | 32 | gulp.task('service-worker', function(cb) { 33 | swPrecache.write(path.join(GLOBAL.config.dest, 'sw.js'), { 34 | staticFileGlobs: [ 35 | GLOBAL.config.dest + '/**/*.{js,html,css,png,jpg,jpeg,gif,svg}', 36 | GLOBAL.config.dest + '/manifest.json' 37 | ], 38 | dynamicUrlToDependencies: { 39 | '/app-shell': ['server/views/layouts/app-shell.handlebars'], 40 | '/api/': [ 41 | 'server/views/index.handlebars', 42 | GLOBAL.config.dest + '/styles/core.css' 43 | ], 44 | '/api/url-1': [ 45 | 'server/views/url-1.handlebars' 46 | ], 47 | '/api/url-2': [ 48 | 'server/views/url-2.handlebars' 49 | ] 50 | }, 51 | stripPrefix: GLOBAL.config.dest, 52 | navigateFallback: '/app-shell', 53 | cacheId: packageName, 54 | handleFetch: (GLOBAL.config.env === 'prod') 55 | }) 56 | .then(cb) 57 | .catch(() => { 58 | cb(); 59 | }); 60 | }); 61 | -------------------------------------------------------------------------------- /server/controllers/server-controller.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var express = require('express'); 3 | var exphbs = require('express-handlebars'); 4 | 5 | function ServerController() { 6 | var expressApp = express(); 7 | var handleBarsInstance = exphbs.create({ 8 | defaultLayout: 'default', 9 | layoutsDir: path.join(__dirname, '/../views/layouts'), 10 | partialsDir: path.join(__dirname, '/../views/partials') 11 | }); 12 | 13 | // Set up the use of handle bars and set the path for views and layouts 14 | expressApp.set('views', path.join(__dirname, '/../views')); 15 | expressApp.engine('handlebars', handleBarsInstance.engine); 16 | expressApp.set('view engine', 'handlebars'); 17 | 18 | // Define static assets path - i.e. styles, scripts etc. 19 | expressApp.use('/', 20 | express.static(path.join(__dirname + '/../../dist/'))); 21 | 22 | var expressServer = null; 23 | 24 | this.getExpressApp = function() { 25 | return expressApp; 26 | }; 27 | 28 | this.setExpressServer = function(server) { 29 | expressServer = server; 30 | }; 31 | 32 | this.getExpressServer = function() { 33 | return expressServer; 34 | }; 35 | 36 | this.getHandleBarsInstance = function() { 37 | return handleBarsInstance; 38 | }; 39 | } 40 | 41 | ServerController.prototype.startServer = function(port) { 42 | // As a failsafe use port 0 if the input isn't defined 43 | // this will result in a random port being assigned 44 | // See : https://nodejs.org/api/http.html for details 45 | if ( 46 | typeof port === 'undefined' || 47 | port === null || 48 | isNaN(parseInt(port, 10)) 49 | ) { 50 | port = 0; 51 | } 52 | 53 | var server = this.getExpressApp().listen(port, () => { 54 | var serverPort = server.address().port; 55 | console.log('Server running on port ' + serverPort); 56 | }); 57 | this.setExpressServer(server); 58 | }; 59 | 60 | ServerController.prototype.addEndpoint = function(endpoint, controller) { 61 | // Add the endpoint and call the onRequest method when a request is made 62 | this.getExpressApp().get(endpoint, function(req, res) { 63 | controller.onRequest(req, res); 64 | }); 65 | }; 66 | 67 | module.exports = new ServerController(); 68 | -------------------------------------------------------------------------------- /src/scripts/controller/ApplicationController.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Copyright 2015 Google Inc. All rights reserved. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | import Controller from './Controller'; 19 | import RouterSingleton from '../libs/RouterSingleton'; 20 | import PageController from './PageController'; 21 | import NavDrawerView from './../view/NavDrawerView'; 22 | 23 | export default class ApplicationController extends Controller { 24 | 25 | constructor() { 26 | super(); 27 | 28 | var navDrawer = new NavDrawerView(); 29 | 30 | var sideNavToggleButton = document.querySelector('.js-toggle-menu'); 31 | sideNavToggleButton.addEventListener('click', () => { 32 | navDrawer.toggle(); 33 | }); 34 | 35 | // TODO: Find more elegant solution to this and handling anchors in the 36 | // web app for dynamically loaded content 37 | var anchorElements = navDrawer.sideNavContent.querySelectorAll('a'); 38 | for (var i = 0; i < anchorElements.length; i++) { 39 | if (!anchorElements[i].href) { 40 | continue; 41 | } 42 | 43 | anchorElements[i].addEventListener('click', (clickEvent) => { 44 | clickEvent.preventDefault(); 45 | 46 | navDrawer.close(); 47 | 48 | var router = RouterSingleton.getRouter(); 49 | router.goToPath(clickEvent.target.href); 50 | }); 51 | } 52 | 53 | var router = RouterSingleton.getRouter(); 54 | router.addRoute('/', new PageController()); 55 | router.addRoute('/url-1', new PageController()); 56 | router.addRoute('/url-2', new PageController()); 57 | router.setDefaultRoute(new PageController()); 58 | router.requestStateUpdate(); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/styles/core/_dialog.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Copyright 2015 Google Inc. All rights reserved. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | .dialog-view { 19 | background: rgba(0,0,0,0.57); 20 | position: fixed; 21 | left: 0; 22 | top: 0; 23 | width: 100%; 24 | height: 100%; 25 | opacity: 0; 26 | pointer-events: none; 27 | will-change: opacity; 28 | transition: opacity 0.333s cubic-bezier(0,0,0.21,1); 29 | } 30 | 31 | .dialog-view--visible { 32 | opacity: 1; 33 | pointer-events: auto; 34 | } 35 | 36 | .dialog-view__panel { 37 | background: #FFF; 38 | border-radius: 2px; 39 | box-shadow: 0 0 14px rgba(0,0,0,.24), 40 | 0 14px 28px rgba(0,0,0,.48); 41 | min-width: 280px; 42 | position: absolute; 43 | left: 50%; 44 | top: 50%; 45 | transform: translate(-50%, -50%) translateY(30px); 46 | transition: transform 0.333s cubic-bezier(0,0,0.21,1) 0.05s; 47 | } 48 | 49 | .dialog-view--visible .dialog-view__panel { 50 | transform: translate(-50%, -50%); 51 | } 52 | 53 | .dialog-view__panel-header { 54 | padding: 24px; 55 | } 56 | 57 | .dialog-view__panel-footer { 58 | padding: 8px; 59 | text-align: right; 60 | } 61 | 62 | .dialog-view__panel-button { 63 | height: 36px; 64 | line-height: 1; 65 | text-transform: uppercase; 66 | color: $secondary; 67 | font-size: 15px; 68 | font-weight: 500; 69 | background: none; 70 | border: none; 71 | padding: 0 8px; 72 | } 73 | 74 | .dialog-view__panel-title { 75 | line-height: 32px; 76 | font-size: 24px; 77 | color: #000; 78 | opacity: 0.87; 79 | font-weight: 500; 80 | margin: 0; 81 | } 82 | 83 | .dialog-view__panel-message { 84 | font-size: 16px; 85 | line-height: 24px; 86 | margin: 20px 0 0 0; 87 | opacity: 0.54; 88 | } 89 | -------------------------------------------------------------------------------- /gulp-tasks/styles.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Copyright 2015 Google Inc. All rights reserved. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the 'License'); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an 'AS IS' BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | var gulp = require('gulp'); 19 | var del = require('del'); 20 | var gulpif = require('gulp-if'); 21 | var runSequence = require('run-sequence'); 22 | var minifyCSS = require('gulp-minify-css'); 23 | var autoprefixer = require('gulp-autoprefixer'); 24 | var sass = require('gulp-sass'); 25 | var sourcemaps = require('gulp-sourcemaps'); 26 | var license = require('gulp-license'); 27 | 28 | var AUTOPREFIXER_BROWSERS = [ 29 | 'ie >= 10', 30 | 'ie_mob >= 10', 31 | 'ff >= 30', 32 | 'chrome >= 34', 33 | 'safari >= 7', 34 | 'opera >= 23', 35 | 'ios >= 7', 36 | 'android >= 4.4', 37 | 'bb >= 10' 38 | ]; 39 | 40 | gulp.task('styles:watch', function() { 41 | gulp.watch(GLOBAL.config.src + '/**/*.scss', ['styles']); 42 | }); 43 | 44 | // Delete any files currently in the scripts destination path 45 | gulp.task('styles:clean', function(cb) { 46 | del([GLOBAL.config.dest + '/**/*.css'], {dot: true}) 47 | .then(function() { 48 | cb(); 49 | }); 50 | }); 51 | 52 | gulp.task('styles:sass', function() { 53 | return gulp.src(GLOBAL.config.src + '/**/*.scss') 54 | // Only create sourcemaps for dev 55 | .pipe(gulpif(GLOBAL.config.env !== 'prod', sourcemaps.init())) 56 | .pipe(sass().on('error', sass.logError)) 57 | .pipe(autoprefixer(AUTOPREFIXER_BROWSERS)) 58 | .pipe(gulpif(GLOBAL.config.env === 'prod', minifyCSS())) 59 | .pipe(license(GLOBAL.config.license, GLOBAL.config.licenseOptions)) 60 | .pipe(gulpif(GLOBAL.config.env !== 'prod', sourcemaps.write())) 61 | .pipe(gulp.dest(GLOBAL.config.dest)); 62 | }); 63 | 64 | gulp.task('styles', function(cb) { 65 | runSequence( 66 | 'styles:clean', 67 | 'styles:sass', 68 | cb 69 | ); 70 | }); 71 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Copyright 2015 Google Inc. All rights reserved. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | /** 19 | * 20 | * This is the Gulp build process file. Gulp allows you to create "tasks" 21 | * that can be chained together and will mutate the input before it's 22 | * served to a browser. For example, Javascript and Sass are minified 23 | * in production environments. 24 | * 25 | * This gulpfile calls require('require-dir')('gulp-tasks') which will look 26 | * in the gulp-tasks folder for any gulp "tasks" it can find and load them 27 | * so that they can be used in this file. 28 | * 29 | * If you find a task (see var allTasks below which is an array of tasks), 30 | * you should find a file with the same name inside of gulp-tasks/. This 31 | * file will have the task inside of it. 32 | * 33 | */ 34 | 35 | var gulp = require('gulp'); 36 | var fs = require('fs'); 37 | var runSequence = require('run-sequence'); 38 | 39 | // Get tasks from gulp-tasks directory 40 | require('require-dir')('gulp-tasks'); 41 | 42 | var projectPackage = JSON.parse(fs.readFileSync('./package.json', 'utf8')); 43 | GLOBAL.config = { 44 | env: 'prod', 45 | port: 8080, 46 | src: 'src', 47 | dest: 'dist', 48 | version: projectPackage.version, 49 | license: 'Apache', 50 | licenseOptions: { 51 | organization: 'Google Inc. All rights reserved.' 52 | } 53 | }; 54 | 55 | var allTasks = ['styles', 'scripts', 'copy', 'html', 'images']; 56 | gulp.task('default', function(cb) { 57 | runSequence( 58 | 'clean', 59 | 'bump', 60 | allTasks, 61 | 'service-worker', 62 | cb); 63 | }); 64 | 65 | function startWatchTasks() { 66 | return runSequence('clean', allTasks, 'service-worker', 'watch', 'nodemon'); 67 | } 68 | 69 | gulp.task('dev', function() { 70 | GLOBAL.config.env = 'dev'; 71 | GLOBAL.config.port = 8081; 72 | return startWatchTasks(); 73 | }); 74 | 75 | gulp.task('prod', startWatchTasks); 76 | -------------------------------------------------------------------------------- /src/third_party/serviceworker-cache-polyfill.es5.js: -------------------------------------------------------------------------------- 1 | if (!Cache.prototype.add) { 2 | Cache.prototype.add = function add(request) { 3 | return this.addAll([request]); 4 | }; 5 | } 6 | 7 | if (!Cache.prototype.addAll) { 8 | Cache.prototype.addAll = function addAll(requests) { 9 | var cache = this; 10 | 11 | // Since DOMExceptions are not constructable: 12 | function NetworkError(message) { 13 | this.name = 'NetworkError'; 14 | this.code = 19; 15 | this.message = message; 16 | } 17 | NetworkError.prototype = Object.create(Error.prototype); 18 | 19 | return Promise.resolve().then(function() { 20 | if (arguments.length < 1) throw new TypeError(); 21 | 22 | // Simulate sequence<(Request or USVString)> binding: 23 | var sequence = []; 24 | 25 | requests = requests.map(function(request) { 26 | if (request instanceof Request) { 27 | return request; 28 | } 29 | else { 30 | return String(request); // may throw TypeError 31 | } 32 | }); 33 | 34 | return Promise.all( 35 | requests.map(function(request) { 36 | if (typeof request === 'string') { 37 | request = new Request(request); 38 | } 39 | 40 | var scheme = new URL(request.url).protocol; 41 | 42 | if (scheme !== 'http:' && scheme !== 'https:') { 43 | throw new NetworkError("Invalid scheme"); 44 | } 45 | 46 | return fetch(request.clone()); 47 | }) 48 | ); 49 | }).then(function(responses) { 50 | // TODO: check that requests don't overwrite one another 51 | // (don't think this is possible to polyfill due to opaque responses) 52 | return Promise.all( 53 | responses.map(function(response, i) { 54 | return cache.put(requests[i], response); 55 | }) 56 | ); 57 | }).then(function() { 58 | return undefined; 59 | }); 60 | }; 61 | } 62 | 63 | if (!CacheStorage.prototype.match) { 64 | // This is probably vulnerable to race conditions (removing caches etc) 65 | CacheStorage.prototype.match = function match(request, opts) { 66 | var caches = this; 67 | 68 | return this.keys().then(function(cacheNames) { 69 | var match; 70 | 71 | return cacheNames.reduce(function(chain, cacheName) { 72 | return chain.then(function() { 73 | return match || caches.open(cacheName).then(function(cache) { 74 | return cache.match(request, opts); 75 | }).then(function(response) { 76 | match = response; 77 | return match; 78 | }); 79 | }); 80 | }, Promise.resolve()); 81 | }); 82 | }; 83 | } 84 | -------------------------------------------------------------------------------- /src/styles/core/_header.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Copyright 2015 Google Inc. All rights reserved. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | .header { 19 | width: 100%; 20 | height: 56px; 21 | color: #FFF; 22 | background: $primary; 23 | position: fixed; 24 | font-size: 20px; 25 | box-shadow: 0 4px 5px 0 rgba(0, 0, 0, 0.14), 26 | 0 2px 9px 1px rgba(0, 0, 0, 0.12), 27 | 0 4px 2px -2px rgba(0, 0, 0, 0.2); 28 | padding: 16px 16px 0 16px; 29 | will-change: transform; 30 | 31 | display: flex; 32 | flex-direction: row; 33 | flex-wrap: nowrap; 34 | justify-content: flex-start; 35 | align-items: stretch; 36 | align-content: center; 37 | transition: transform 0.233s cubic-bezier(0,0,0.21,1) 0.1s; 38 | } 39 | 40 | .header--collapsed { 41 | transition: transform 0.233s cubic-bezier(0,0,0.21,1) 0.13s; 42 | transform: translateY(-56px); 43 | } 44 | 45 | .header__menu { 46 | background: url(/images/ic_menu_24px.svg) center center no-repeat; 47 | width: 24px; 48 | height: 24px; 49 | margin-right: 16px; 50 | text-indent: -30000px; 51 | overflow: hidden; 52 | opacity: 0.54; 53 | transition: opacity 0.333s cubic-bezier(0,0,0.21,1); 54 | border: none; 55 | outline: none; 56 | } 57 | 58 | .header__menu:focus, 59 | .header__menu:hover { 60 | opacity: 1; 61 | border: 1px solid white; 62 | } 63 | 64 | .header__title { 65 | font-weight: 400; 66 | font-size: 20px; 67 | margin: 0; 68 | flex: 1; 69 | } 70 | 71 | @media(min-width: 600px) { 72 | .header { 73 | padding: 16px 32px 0 24px; 74 | height: 144px; 75 | flex-direction: column; 76 | align-content: space-between; 77 | align-items: flex-start; 78 | } 79 | 80 | .header__menu { 81 | margin: 4px 0 20px 0; 82 | } 83 | 84 | .header__title { 85 | font-size: 34px; 86 | height: 80px; 87 | line-height: 80px; 88 | } 89 | 90 | .header--collapsed { 91 | transition: none; 92 | transform: none; 93 | } 94 | } 95 | 96 | @media(min-width: 960px) { 97 | .header__title { 98 | width: 888px; 99 | margin: 0 auto; 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Dockerfile extending the generic Node image with application files for a 2 | # single application. 3 | FROM beta.gcr.io/google_appengine/nodejs 4 | # Check to see if the the version included in the base runtime satisfies \ 5 | # >=4.1.0, if not then do an npm install of the latest available \ 6 | # version that satisfies it. \ 7 | RUN npm install https://storage.googleapis.com/gae_node_packages/semver.tar.gz && \ 8 | (node -e 'var semver = require("semver"); \ 9 | if (!semver.satisfies(process.version, ">=4.1.0")) \ 10 | process.exit(1);' || \ 11 | (version=$(curl -L https://storage.googleapis.com/gae_node_packages/node_versions | \ 12 | node -e ' \ 13 | var semver = require("semver"); \ 14 | var http = require("http"); \ 15 | var spec = process.argv[1]; \ 16 | var latest = ""; \ 17 | var versions = ""; \ 18 | var selected_version; \ 19 | \ 20 | function verifyBinary(version) { \ 21 | var options = { \ 22 | "host": "storage.googleapis.com", \ 23 | "method": "HEAD", \ 24 | "path": "/gae_node_packages/node-" + version + \ 25 | "-linux-x64.tar.gz" \ 26 | }; \ 27 | var req = http.request(options, function (res) { \ 28 | if (res.statusCode == 404) { \ 29 | console.error("Binaries for Node satisfying version " + \ 30 | version + " are not available."); \ 31 | process.exit(1); \ 32 | } \ 33 | }); \ 34 | req.end(); \ 35 | } \ 36 | function satisfies(version) { \ 37 | if (semver.satisfies(version, spec)) { \ 38 | process.stdout.write(version); \ 39 | verifyBinary(version); \ 40 | return true; \ 41 | } \ 42 | } \ 43 | process.stdin.on("data", function(data) { \ 44 | versions += data; \ 45 | }); \ 46 | process.stdin.on("end", function() { \ 47 | versions = \ 48 | versions.split("\n").sort().reverse(); \ 49 | if (!versions.some(satisfies)) { \ 50 | console.error("No version of Node found satisfying: " + \ 51 | spec); \ 52 | process.exit(1); \ 53 | } \ 54 | });' \ 55 | ">=4.1.0") && \ 56 | rm -rf /nodejs/* && \ 57 | (curl https://storage.googleapis.com/gae_node_packages/node-$version-linux-x64.tar.gz | \ 58 | tar xzf - -C /nodejs --strip-components=1 \ 59 | ) \ 60 | ) \ 61 | ) 62 | COPY . /app/ 63 | # You have to specify "--unsafe-perm" with npm install 64 | # when running as root. Failing to do this can cause 65 | # install to appear to succeed even if a preinstall 66 | # script fails, and may have other adverse consequences 67 | # as well. 68 | RUN npm --unsafe-perm install 69 | CMD npm start 70 | -------------------------------------------------------------------------------- /src/old-sw.es6.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2015 Google Inc. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of 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, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | importScripts('third_party/serviceworker-cache-polyfill.js'); 18 | 19 | var CACHE_NAME = 'appshell'; 20 | var CACHE_VERSION = '@VERSION@'; 21 | 22 | self.oninstall = function(event) { 23 | var urls = [ 24 | '/app-shell', 25 | '/images/chrome-touch-icon-192x192.png', 26 | 27 | '/images/side-nav-bg@2x.jpg', 28 | 29 | '/images/ic_menu_24px.svg', 30 | '/images/ic_add_24px.svg', 31 | '/images/ic_info_outline_24px.svg', 32 | 33 | '/scripts/core.js', 34 | '/styles/core.css', 35 | 36 | '/favicon.ico', 37 | '/manifest.json', 38 | 39 | '/api/', 40 | '/api/url-1', 41 | '/api/url-2', 42 | '/api/index' 43 | ]; 44 | 45 | urls = urls.map(function(url) { 46 | return new Request(url, {credentials: 'include'}); 47 | }); 48 | 49 | event.waitUntil( 50 | caches 51 | .open(CACHE_NAME + '-v' + CACHE_VERSION) 52 | .then(function(cache) { 53 | return cache.addAll(urls); 54 | }) 55 | ); 56 | }; 57 | 58 | self.onactivate = function(event) { 59 | var currentCacheName = CACHE_NAME + '-v' + CACHE_VERSION; 60 | caches.keys().then(function(cacheNames) { 61 | return Promise.all( 62 | cacheNames.map(function(cacheName) { 63 | // TODO: This should never get called 64 | // can we drop this check? 65 | if (cacheName.indexOf(CACHE_NAME) === -1) { 66 | return; 67 | } 68 | 69 | if (cacheName !== currentCacheName) { 70 | return caches.delete(cacheName); 71 | } 72 | }) 73 | ); 74 | }); 75 | }; 76 | 77 | self.onfetch = function(event) { 78 | var request = event.request; 79 | event.respondWith( 80 | // Check the cache for a hit of the asset as is. 81 | caches.match(request).then((response) => { 82 | // If we have a response return it. 83 | if (response) { 84 | console.log(' sw: [cached] ' + request.url); 85 | return response; 86 | } 87 | 88 | // For other requests on our domain, return the app shell 89 | var url = new URL(request.url); 90 | if (url.host === this.location.host) { 91 | if ( 92 | url.pathname.indexOf('.') === -1 && 93 | url.pathname.indexOf('/partials') !== 0 94 | ) { 95 | console.log(' sw: [app-shell redirect] ' + request.url); 96 | return caches.match('/app-shell'); 97 | } 98 | } 99 | 100 | // If here, then it should be a request for external url 101 | // analytics or web fonts for example. 102 | console.log(' sw: [fetch] ' + request.url); 103 | return fetch(request); 104 | }) 105 | ); 106 | }; 107 | -------------------------------------------------------------------------------- /src/scripts/libs/RouterSingleton.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2014 Google Inc. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of 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" p 12 | BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | export default class RouterSingleton { 19 | 20 | static getRouter() { 21 | if (typeof window.RouterInstance_ !== 'undefined') { 22 | return window.RouterInstance_; 23 | } 24 | 25 | window.RouterInstance_ = new Router(); 26 | 27 | return window.RouterInstance_; 28 | } 29 | 30 | } 31 | 32 | class Router { 33 | constructor() { 34 | this.routes = {}; 35 | this.currentPath = null; 36 | this.defaultActivity = null; 37 | 38 | window.addEventListener('popstate', (e) => { 39 | this.onPopState(e); 40 | }); 41 | } 42 | 43 | addRoute(path, activity) { 44 | if (this.routes[path]) { 45 | throw 'A handler already exists for this path: ' + path; 46 | } 47 | 48 | this.routes[path] = activity; 49 | } 50 | 51 | setDefaultRoute(activity) { 52 | if (this.defaultActivity) { 53 | throw 'A default handler already exists'; 54 | } 55 | 56 | this.defaultActivity = activity; 57 | } 58 | 59 | removeRoute(path) { 60 | if (!this.routes[path]) { 61 | return; 62 | } 63 | 64 | delete this.routes[path]; 65 | } 66 | 67 | requestStateUpdate() { 68 | requestAnimationFrame(() => { 69 | this.manageState(); 70 | }); 71 | } 72 | 73 | manageState() { 74 | var newPath = document.location.pathname; 75 | var newActivity = this.routes[newPath]; 76 | var currentActivity = this.routes[this.currentPath]; 77 | 78 | if (!newActivity && this.defaultActivity) { 79 | newActivity = this.defaultActivity; 80 | } 81 | 82 | if (this.currentPath === newPath) { 83 | if (typeof newActivity.onUpdate === 'function') { 84 | newActivity.onUpdate(); 85 | return true; 86 | } 87 | 88 | return false; 89 | } 90 | 91 | // Remove the old action and update the reference. 92 | if (currentActivity) { 93 | // Allow the incoming view to delay the outgoing one 94 | // so that we don't get too much overlapping animation. 95 | currentActivity.onFinish(); 96 | } 97 | 98 | if (newActivity) { 99 | newActivity.onStart(newPath); 100 | this.currentPath = newPath; 101 | } else { 102 | this.currentPath = null; 103 | } 104 | 105 | return true; 106 | } 107 | 108 | goToPath(path, title = null) { 109 | console.log('goToPath() path = ' + path); 110 | // Only process real changes. 111 | if (path === window.location.pathname) { 112 | return; 113 | } 114 | 115 | history.pushState(undefined, title, path); 116 | requestAnimationFrame(() => { 117 | this.manageState(); 118 | }); 119 | } 120 | 121 | onPopState(e) { 122 | e.preventDefault(); 123 | this.requestStateUpdate(); 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/scripts/controller/Controller.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Copyright 2015 Google Inc. All rights reserved. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | import ToasterSingleton from '../libs/ToasterSingleton'; 19 | 20 | export default class Controller { 21 | 22 | constructor(registerServiceWorker = true) { 23 | if (registerServiceWorker) { 24 | this.registerServiceWorker(); 25 | } 26 | } 27 | 28 | registerServiceWorker() { 29 | if (!('serviceWorker' in navigator)) { 30 | // Service worker is not supported on this platform 31 | return; 32 | } 33 | 34 | navigator.serviceWorker.register('/sw.js', { 35 | scope: '/' 36 | }).then((registration) => { 37 | console.log('Service worker is registered.'); 38 | 39 | var isUpdate = false; 40 | 41 | // If this fires we should check if there's a new Service Worker 42 | // waiting to be activated. If so, ask the user to force refresh. 43 | if (registration.active) { 44 | isUpdate = true; 45 | } 46 | 47 | registration.onupdatefound = function(updateEvent) { 48 | console.log('A new Service Worker version has been found...'); 49 | 50 | // If an update is found the spec says that there is a new Service 51 | // Worker installing, so we should wait for that to complete then 52 | // show a notification to the user. 53 | registration.installing.onstatechange = function(event) { 54 | if (this.state === 'installed') { 55 | if (isUpdate) { 56 | ToasterSingleton.getToaster().toast( 57 | 'App updated. Restart for the new version.'); 58 | } else { 59 | ToasterSingleton.getToaster().toast( 60 | 'App ready for offline use.'); 61 | } 62 | } 63 | }; 64 | }; 65 | }) 66 | .catch((err) => { 67 | console.log('Unable to register service worker.', err); 68 | }); 69 | } 70 | 71 | loadScript(url) { 72 | return new Promise((resolve, reject) => { 73 | var script = document.createElement('script'); 74 | script.async = true; 75 | script.src = url; 76 | 77 | script.onload = resolve; 78 | script.onerror = reject; 79 | 80 | document.head.appendChild(script); 81 | }); 82 | } 83 | 84 | loadCSS(url) { 85 | return new Promise((resolve, reject) => { 86 | var xhr = new XMLHttpRequest(); 87 | xhr.open('GET', url); 88 | xhr.responseType = 'text'; 89 | xhr.onload = function(e) { 90 | if (this.status === 200) { 91 | var style = document.createElement('style'); 92 | style.textContent = xhr.response; 93 | document.head.appendChild(style); 94 | resolve(); 95 | } else { 96 | reject(); 97 | } 98 | }; 99 | xhr.send(); 100 | }); 101 | } 102 | 103 | } 104 | -------------------------------------------------------------------------------- /src/scripts/view/NavDrawerView.js: -------------------------------------------------------------------------------- 1 | export default class NavDrawerView { 2 | 3 | constructor() { 4 | this.rootElement = document.querySelector('.js-side-nav'); 5 | this.sideNavContent = this.rootElement 6 | .querySelector('.js-side-nav-content'); 7 | this.sideNavBody = this.rootElement.querySelector('.side-nav__body'); 8 | 9 | this.rootElement.addEventListener('click', () => { 10 | this.close(); 11 | }); 12 | 13 | this.sideNavContent.addEventListener('click', (e) => { 14 | e.stopPropagation(); 15 | }); 16 | 17 | this.hasUnprefixedTransform = 'transform' in document.documentElement.style; 18 | if (this.hasUnprefixedTransform) { 19 | // Touch slop is a variable that is defined to suggest anything larger 20 | // than this value was an intended gesture by the user. 21 | // 8 is a value from Android platform. 22 | // 3 was added as a factor made up from what felt right. 23 | var TOUCH_SLOP = 8 * window.devicePixelRatio * 3; 24 | 25 | var touchStartX; 26 | var sideNavTransform; 27 | 28 | var onSideNavTouchStart = (e) => { 29 | e.preventDefault(); 30 | touchStartX = e.touches[0].pageX; 31 | }; 32 | 33 | var onSideNavTouchMove = (e) => { 34 | e.preventDefault(); 35 | 36 | var newTouchX = e.touches[0].pageX; 37 | sideNavTransform = Math.min(0, newTouchX - touchStartX); 38 | 39 | this.sideNavContent.style.transform = 40 | 'translateX(' + sideNavTransform + 'px)'; 41 | }; 42 | 43 | var onSideNavTouchEnd = (e) => { 44 | if (sideNavTransform < -TOUCH_SLOP) { 45 | this.close(); 46 | return; 47 | } 48 | 49 | this.open(); 50 | }; 51 | 52 | this.sideNavContent.addEventListener('touchstart', onSideNavTouchStart); 53 | this.sideNavContent.addEventListener('touchmove', onSideNavTouchMove); 54 | this.sideNavContent.addEventListener('touchend', onSideNavTouchEnd); 55 | } 56 | } 57 | 58 | isOpen() { 59 | return this.rootElement.classList.contains('side-nav--visible'); 60 | } 61 | 62 | toggle() { 63 | if (this.isOpen()) { 64 | this.close(); 65 | } else { 66 | this.open(); 67 | } 68 | } 69 | 70 | close() { 71 | this.rootElement.classList.remove('side-nav--visible'); 72 | this.sideNavContent.classList.add('side-nav__content--animatable'); 73 | 74 | if (this.hasUnprefixedTransform) { 75 | this.sideNavContent.style.transform = 'translateX(-102%)'; 76 | } else { 77 | this.sideNavContent.classList.remove('side-nav--visible'); 78 | } 79 | } 80 | 81 | open() { 82 | this.rootElement.classList.add('side-nav--visible'); 83 | 84 | if (this.hasUnprefixedTransform) { 85 | var onSideNavTransitionEnd = (e) => { 86 | this.sideNavBody.focus(); 87 | 88 | this.sideNavContent.classList.remove('side-nav__content--animatable'); 89 | this.sideNavContent.removeEventListener('transitionend', 90 | onSideNavTransitionEnd); 91 | }; 92 | 93 | this.sideNavContent.classList.add('side-nav__content--animatable'); 94 | this.sideNavContent.addEventListener('transitionend', 95 | onSideNavTransitionEnd); 96 | 97 | requestAnimationFrame( () => { 98 | this.sideNavContent.style.transform = 'translateX(0px)'; 99 | }); 100 | } else { 101 | this.sideNavContent.classList.add('side-nav--visible'); 102 | } 103 | } 104 | 105 | } 106 | -------------------------------------------------------------------------------- /src/styles/core/_side-nav.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Copyright 2015 Google Inc. All rights reserved. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | .side-nav { 19 | width: 100%; 20 | height: 100%; 21 | position: fixed; 22 | pointer-events: none; 23 | top: 0; 24 | left: 0; 25 | overflow: hidden; 26 | } 27 | 28 | .side-nav:before { 29 | content: ''; 30 | width: 100%; 31 | height: 100%; 32 | background: #000; 33 | opacity: 0; 34 | display: block; 35 | position: absolute; 36 | will-change: opacity; 37 | transition: opacity 0.233s cubic-bezier(0,0,0.21,1); 38 | } 39 | 40 | .side-nav--visible { 41 | pointer-events: auto; 42 | } 43 | 44 | .side-nav--visible:before { 45 | opacity: 0.7; 46 | } 47 | 48 | .side-nav__content { 49 | background: #FAFAFA; 50 | width: 80%; 51 | max-width: 304px; 52 | height: 100%; 53 | overflow: hidden; 54 | position: relative; 55 | 56 | box-shadow: 0 0 4px rgba(0, 0, 0, .14), 57 | 0 4px 8px rgba(0, 0, 0, .28); 58 | 59 | will-change: transform; 60 | transform: translateX(-102%); 61 | 62 | &.side-nav--visible { 63 | transform: translateX(0px); 64 | } 65 | } 66 | 67 | 68 | .side-nav__content--animatable { 69 | transition: transform 0.233s cubic-bezier(0,0,0.21,1); 70 | } 71 | 72 | .side-nav__header { 73 | background: $primary url(/images/side-nav-bg@2x.jpg); 74 | background-size: cover; 75 | width: 100%; 76 | height: 171px; 77 | position: relative; 78 | } 79 | 80 | .side-nav__title { 81 | font-size: 16px; 82 | line-height: 1; 83 | color: #FFF; 84 | position: absolute; 85 | bottom: 8px; 86 | left: 16px; 87 | height: 16px; 88 | font-weight: 500; 89 | } 90 | 91 | .side-nav__body { 92 | padding-top: 8px; 93 | } 94 | 95 | .side-nav__version { 96 | position: absolute; 97 | bottom: 16px; 98 | left: 16px; 99 | font-size: 14px; 100 | opacity: 0.54; 101 | } 102 | 103 | .side-nav__delete-memos, 104 | .side-nav__delete-all, 105 | .side-nav__blog-post, 106 | .side-nav__file-bug-report { 107 | font-family: 'Roboto'; 108 | font-size: 14px; 109 | outline: none; 110 | height: 48px; 111 | padding-left: 72px; 112 | width: 100%; 113 | text-align: left; 114 | display: block; 115 | border: none; 116 | background: url(/images/ic_delete_24px.svg) 16px 12px no-repeat; 117 | color: rgba(0,0,0,0.87); 118 | cursor: pointer; 119 | } 120 | 121 | .side-nav__delete-all { 122 | background-image: url(/images/ic_restore_24px.svg); 123 | } 124 | 125 | .side-nav__blog-post { 126 | background-image: url(/images/ic_info_outline_24px.svg); 127 | line-height: 48px; 128 | text-decoration: none; 129 | } 130 | 131 | .side-nav__blog-post:focus { 132 | background-color: #eee; 133 | outline: 0; 134 | } 135 | 136 | .side-nav__file-bug-report { 137 | background-image: url(/images/ic_feedback_24px.svg); 138 | line-height: 48px; 139 | text-decoration: none; 140 | } 141 | 142 | .side-nav__blog-post.active { 143 | font-weight: bold; 144 | background-color: rgba(0,0,0,.05); 145 | } 146 | -------------------------------------------------------------------------------- /src/scripts/controller/PageController.js: -------------------------------------------------------------------------------- 1 | export default class PageController { 2 | constructor() { 3 | this.loader = document.querySelector('.js-global-loader'); 4 | this.mainContainer = document.querySelector('.js-global-main'); 5 | 6 | this.DEFAULT_TITLE = 'App Shell'; 7 | } 8 | 9 | onUpdate() { 10 | console.log('onUpdate: ', this.path); 11 | } 12 | 13 | onStart(path) { 14 | console.log('onStart: ', path); 15 | 16 | // Show loading dialog while we get content 17 | this.loader.classList.remove('is-hidden'); 18 | 19 | this.updateNavDrawer(path); 20 | 21 | fetch('/api' + path) 22 | .then((response) => { 23 | if (response.status === 404) { 24 | this.show404(); 25 | return null; 26 | } 27 | 28 | return response.json(); 29 | }) 30 | .then((responseObject) => { 31 | // Hide loading dialog 32 | this.loader.classList.add('is-hidden'); 33 | 34 | if (responseObject === null) { 35 | throw new Error('Unexpected response from Server.'); 36 | } 37 | 38 | if (responseObject.title) { 39 | document.title = responseObject.title; 40 | } else { 41 | document.title = this.DEFAULT_TITLE; 42 | } 43 | 44 | // Add style element to the document head 45 | var styleElement = document.createElement('style'); 46 | styleElement.textContent = responseObject.partialinlinestyles; 47 | document.head.appendChild(styleElement); 48 | 49 | // Add content from partial to page 50 | this.mainContainer.innerHTML = responseObject.partialhtml; 51 | 52 | // TODO: Handle remote scripts 53 | 54 | // TODO: Handle remote styles 55 | }) 56 | .catch((error) => { 57 | this.showError('There was a problem loading this page'); 58 | }); 59 | } 60 | 61 | onFinish() { 62 | console.log('onFinish'); 63 | // Remove any existing styles 64 | var insertedStyles = 65 | document.querySelector('.js-partial-styles'); 66 | if (insertedStyles) { 67 | document.head.removeChild(insertedStyles); 68 | } 69 | 70 | // Remove the current content 71 | while (this.mainContainer.firstChild) { 72 | this.mainContainer.removeChild(this.mainContainer.firstChild); 73 | } 74 | } 75 | 76 | show404() { 77 | var headingElement = document.createElement('h1'); 78 | headingElement.textContent = '404.'; 79 | this.mainContainer.appendChild(headingElement); 80 | 81 | var paragraphElement = document.createElement('p'); 82 | paragraphElement.textContent = 'Oops - looks like this ' + 83 | 'page isn\'t valid.'; 84 | this.mainContainer.appendChild(paragraphElement); 85 | } 86 | 87 | showError(msg) { 88 | var headingElement = document.createElement('h1'); 89 | headingElement.textContent = 'Ooopps.'; 90 | this.mainContainer.appendChild(headingElement); 91 | 92 | var paragraphElement = document.createElement('p'); 93 | paragraphElement.textContent = 'There was a problem displaying this page ' + 94 | ', sorry about that.'; 95 | this.mainContainer.appendChild(paragraphElement); 96 | } 97 | 98 | updateNavDrawer(path) { 99 | var nodeList = document.querySelectorAll('.side-nav__body a'); 100 | [].forEach.call(nodeList, function(el) { 101 | // Reset active states 102 | el.classList.remove('active'); 103 | // We could compare against path, but easier to compare 104 | // against the current document href 105 | if (el.href === document.location.href) { 106 | el.classList.add('active'); 107 | } 108 | }); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/styles/core/_core.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Copyright 2015 Google Inc. All rights reserved. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | * { 19 | box-sizing: border-box; 20 | } 21 | 22 | html, body { 23 | padding: 0; 24 | margin: 0; 25 | height: 100%; 26 | width: 100%; 27 | font-family: 'Helvetica', 'Verdana', sans-serif; 28 | font-weight: 400; 29 | // Not implemented yet but is a nice solution for async loading fonts 30 | font-display: optional; 31 | color: #444; 32 | -webkit-font-smoothing: antialiased; 33 | -moz-osx-font-smoothing: grayscale; 34 | } 35 | 36 | html { 37 | overflow: hidden; 38 | } 39 | 40 | body { 41 | display: flex; 42 | flex-direction: column; 43 | flex-wrap: nowrap; 44 | justify-content: flex-start; 45 | align-items: stretch; 46 | align-content: stretch; 47 | background: #ececec; 48 | } 49 | 50 | body:after { 51 | content: ''; 52 | position: fixed; 53 | top: 0; 54 | left: 0; 55 | width: 100%; 56 | height: 100%; 57 | pointer-events: none; 58 | background: $background; 59 | opacity: 0; 60 | will-change: opacity; 61 | transition: opacity 0.333s cubic-bezier(0,0,0.21,1) 0.4s; 62 | } 63 | 64 | h1, h2, h3, h4, h5, h6 { 65 | font-family: 'Roboto', 'Helvetica', 'Verdana', sans-serif; 66 | } 67 | 68 | .app-deeplink:after { 69 | opacity: 1; 70 | pointer-events: auto; 71 | } 72 | 73 | a { 74 | color: $secondary; 75 | } 76 | 77 | .is-hidden { 78 | display: none; 79 | } 80 | 81 | button::-moz-focus-inner { 82 | border: 0; 83 | } 84 | 85 | @media(min-width: 600px) { 86 | .view-underpanel { 87 | top: 0; 88 | right: 0; 89 | position: fixed; 90 | width: 400px; 91 | height: 100%; 92 | overflow: hidden; 93 | pointer-events: none; 94 | } 95 | 96 | .view-underpanel__block { 97 | position: absolute; 98 | top: 0; 99 | right: 0; 100 | width: 360px; 101 | height: 100%; 102 | background: $background; 103 | box-shadow: 0 0 14px rgba(0,0,0,.24), 104 | 0 14px 28px rgba(0,0,0,.48); 105 | transform: translateX(105%); 106 | transition: transform 0.233s cubic-bezier(0,0,0.21,1) 0.04s, 107 | opacity 0.213s cubic-bezier(0,0,0.21,1) 0.04s; 108 | will-change: transform; 109 | opacity: 0; 110 | } 111 | 112 | .view-underpanel__block:after { 113 | content: ''; 114 | height: 144px; 115 | width: 100%; 116 | display: block; 117 | background: $primaryDark; 118 | position: absolute; 119 | top: 0; 120 | left: 0; 121 | } 122 | 123 | .view-underpanel--visible .view-underpanel__block, 124 | .view-underpanel--locked .view-underpanel__block { 125 | opacity: 1; 126 | transform: translateX(0); 127 | } 128 | } 129 | 130 | @media(min-width: 960px) { 131 | .view-underpanel { 132 | margin-top: 56px; 133 | left: 46%; 134 | right: auto; 135 | position: fixed; 136 | width: 520px; 137 | height: 100%; 138 | overflow: hidden; 139 | pointer-events: none; 140 | } 141 | 142 | .view-underpanel__block { 143 | opacity: 0.0001; 144 | width: 504px; 145 | left: 8px; 146 | transform: translateY(50px); 147 | transition: transform 0.233s cubic-bezier(0,0,0.21,1) 0.04s, 148 | opacity 0.213s cubic-bezier(0,0,0.21,1) 0.04s; 149 | 150 | box-shadow: 0 0 6px rgba(0,0,0,.16), 151 | 0 6px 12px rgba(0,0,0,.32); 152 | } 153 | 154 | .view-underpanel__block:after { 155 | height: 288px; 156 | } 157 | 158 | .view-underpanel--visible .view-underpanel__block, 159 | .view-underpanel--locked .view-underpanel__block { 160 | opacity: 1; 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /gulp-tasks/scripts.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Copyright 2015 Google Inc. All rights reserved. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the 'License'); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an 'AS IS' BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | var gulp = require('gulp'); 19 | var del = require('del'); 20 | var runSequence = require('run-sequence'); 21 | var eslint = require('gulp-eslint'); 22 | var path = require('path'); 23 | var glob = require('glob'); 24 | var browserify = require('browserify'); 25 | var gutil = require('gulp-util'); 26 | var source = require('vinyl-source-stream'); 27 | var uglify = require('gulp-uglify'); 28 | var gulpif = require('gulp-if'); 29 | var streamify = require('gulp-streamify'); 30 | var replace = require('gulp-replace'); 31 | var license = require('gulp-license'); 32 | var sourcemaps = require('gulp-sourcemaps'); 33 | var rename = require('gulp-rename'); 34 | 35 | gulp.task('scripts:watch', function() { 36 | gulp.watch(GLOBAL.config.src + '/**/*.js', ['scripts']); 37 | gulp.watch(['./.eslintrc', './.eslintignore'], ['scripts']); 38 | }); 39 | 40 | // Takes a set of objects defining inputs of javascript files 41 | // to run through browserify and babelify 42 | function compileES6Bundles(browserifyBundles, cb) { 43 | // Check if there is anything to do 44 | if (browserifyBundles.length === 0) { 45 | return cb(); 46 | } 47 | 48 | var finishedCount = 0; 49 | browserifyBundles.forEach(function(bundle) { 50 | var browserifyBundle = browserify({ 51 | entries: [bundle.srcPath] 52 | }) 53 | .transform('babelify', {presets: ['es2015']}); 54 | 55 | try { 56 | return browserifyBundle.bundle() 57 | .on('log', gutil.log.bind(gutil, 'Browserify Log')) 58 | .on('error', function(err) { 59 | gutil.log('Browserify Error', err); 60 | this.emit('end'); 61 | }) 62 | .pipe(source(bundle.outputFilename)) 63 | .pipe(replace(/@VERSION@/g, GLOBAL.config.version)) 64 | 65 | // If this is a production build - minify JS 66 | .pipe(gulpif(GLOBAL.config.env === 'prod', streamify(uglify()))) 67 | .pipe(license(GLOBAL.config.license, GLOBAL.config.licenseOptions)) 68 | .pipe(gulp.dest(bundle.dest)) 69 | .on('end', function() { 70 | finishedCount++; 71 | 72 | if (finishedCount === browserifyBundles.length) { 73 | cb(); 74 | } 75 | }); 76 | } catch (exception) { 77 | console.log(exception); 78 | } 79 | }); 80 | } 81 | 82 | // This takes a source path and finds all files ending 83 | // with .es6.js and creates the bundles to run through browserify 84 | // and babelify 85 | function generateES6Bundles(srcPath, cb) { 86 | if (!srcPath) { 87 | return cb(new Error('Invalid source path given to generateES6Bundles')); 88 | } 89 | 90 | var es6Filepaths = glob.sync(srcPath + '/**/*.es6.js'); 91 | 92 | var browserifyBundles = []; 93 | es6Filepaths.forEach(function(filepath) { 94 | var filename = path.basename(filepath); 95 | var directoryOfFile = path.dirname(filepath); 96 | var relativeDirectory = path.relative( 97 | srcPath, 98 | directoryOfFile); 99 | 100 | // Replace .es6.js with .js for the final output 101 | var outputFilename = 102 | filename.substring(0, filename.length - '.es6.js'.length) + '.js'; 103 | 104 | browserifyBundles.push({ 105 | srcPath: './' + filepath, 106 | outputFilename: outputFilename, 107 | dest: path.join(GLOBAL.config.dest, relativeDirectory) 108 | }); 109 | }); 110 | 111 | compileES6Bundles(browserifyBundles, cb); 112 | } 113 | 114 | gulp.task('scripts:eslint', function() { 115 | return gulp.src([GLOBAL.config.src + '/**/*.js']) 116 | 117 | // eslint() attaches the lint output to the eslint property, 118 | // of the file object so it can be used by other modules. 119 | .pipe(eslint()) 120 | 121 | // eslint.format() outputs the lint results to the console. 122 | // Alternatively use eslint.formatEach() (see Docs). 123 | .pipe(eslint.format()) 124 | 125 | // To have the process exit with an error code (1) on 126 | // lint error, return the stream and pipe to failOnError last. 127 | .pipe(gulpif(GLOBAL.config.env === 'prod', eslint.failOnError())); 128 | }); 129 | 130 | gulp.task('scripts:es6', function(cb) { 131 | generateES6Bundles(GLOBAL.config.src, cb); 132 | }); 133 | 134 | gulp.task('scripts:es5', function() { 135 | return gulp.src([GLOBAL.config.src + '/**/*.es5.js']) 136 | .pipe(gulpif(GLOBAL.config.env !== 'prod', sourcemaps.init())) 137 | 138 | // Remove the .es5 from the end of the file name using gulp-rename 139 | .pipe(rename(function(filePath) { 140 | var fileExtensionLength = '.es5'.length; 141 | filePath.basename = filePath.basename.substr( 142 | 0, filePath.basename.length - fileExtensionLength); 143 | })) 144 | 145 | .pipe(replace(/@VERSION@/g, GLOBAL.config.version)) 146 | .pipe(gulpif(GLOBAL.config.env === 'prod', uglify())) 147 | .pipe(license(GLOBAL.config.license, GLOBAL.config.licenseOptions)) 148 | .pipe(gulpif(GLOBAL.config.env !== 'prod', sourcemaps.write())) 149 | .pipe(gulp.dest(GLOBAL.config.dest)); 150 | }); 151 | 152 | // Delete any files currently in the scripts destination path 153 | gulp.task('scripts:clean', function(cb) { 154 | del([GLOBAL.config.dest + '/**/*.js'], {dot: true}) 155 | .then(function() { 156 | cb(); 157 | }); 158 | }); 159 | 160 | gulp.task('scripts', function(cb) { 161 | runSequence( 162 | [ 163 | 'scripts:clean', 164 | 'scripts:eslint' 165 | ], 166 | [ 167 | 'scripts:es6', 168 | 'scripts:es5' 169 | ], 170 | cb 171 | ); 172 | }); 173 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Application Shell Architecture 2 | 3 | A modern web application architecture leveraging [Service Worker](http://www.html5rocks.com/en/tutorials/service-worker/introduction/) to offline cache your application 'shell' and populate the content using JS. This means you can get pixels on the screen without the network, even if the content eventually comes from the network - a great performance win. In browsers without SW, we gracefully degrade to server-side rendering (e.g iOS). [Demo](https://app-shell.appspot.com/). 4 | 5 | Full details of the architecture can be found in [Instant Loading Web Apps With An Application Shell Architecture](https://medium.com/@addyosmani/instant-loading-web-apps-with-an-application-shell-architecture-7c0c2f10c73#.a4d09g3j4) and [Instant Loading with Service Workers](https://www.youtube.com/watch?v=jCKZDTtUA2A&feature=youtu.be). 6 | 7 | ## Goals 8 | 9 | * Time to first paint is extremely fast 10 | * Content is rendered. App shell can be a placeholder. 11 | * User can scroll, but doesn’t necessarily need to be able to navigate or deeply interact. 12 | * First pageload < 1000ms 13 | * App shell is progressively enhanced in. 14 | * User can now navigate within the app. 15 | * Second pageload 16 | * Shell is loaded from SW cache 17 | * Content loads off the network 18 | 19 | ## Installation 20 | 21 | Install dependencies using npm: 22 | 23 | ```sh 24 | $ npm install -g gulp nodemon && npm install 25 | ``` 26 | 27 | ## Usage 28 | 29 | ### Production Build 30 | 31 | ```sh 32 | $ gulp 33 | ``` 34 | 35 | ### Development Build with Watch 36 | 37 | ```sh 38 | $ gulp dev 39 | ``` 40 | 41 | ### Serve/watch 42 | 43 | Once you've got a production build or development build of gulp done, start the 44 | server with: 45 | 46 | ```sh 47 | $ nodemon server/app.js 48 | ``` 49 | 50 | Alternatively, you can just run `npm run monitor`. The application shell should now be available on port `8080`. 51 | 52 | ### Deployment 53 | 54 | We've deployed the project to Node.js on [Google Cloud](https://cloud.google.com/nodejs/). To do the same, follow the steps in their Node.js deployment [getting started](https://cloud.google.com/nodejs/getting-started/hello-world) guide and after running `npm install` run `gcloud preview app deploy app.yaml --promote`. If everything works correctly, you should have the project deployed to your custom AppSpot endpoint. 55 | 56 | ## Notes 57 | 58 | ## Tips for your application shell 59 | 60 | In a [Progressive Web App](https://infrequently.org/2015/06/progressive-apps-escaping-tabs-without-losing-our-soul/), everything necessary to load the the simplest "shell" of your UI consists of HTML, CSS and JavaScript. Keep this shell as lean as possible. Some of it will come from your application’s index file (inline DOM, styles) and the rest may be loaded from external scripts and stylesheets. Together, they are all you need to display a simple, static app. It’s important to keep the shell of your webapp lean to ensure that some inline static structural content can be displayed as soon as the webapp is opened, regardless of the network being available or not. 61 | 62 | A static webapp that always displays the same content may not be what your users expect - it may well be quite dynamic. This means the app may need to fetch data specific to the user’s current needs so this data can come from the network / a server-side API but we logically separate this work for our app from the application shell. When it comes to offline support, structuring your app so that there's a clear distinction between the page shell and the dynamic or state-specific resources will come in very handy. 63 | 64 | ## Gotchas 65 | 66 | There are no hard and fast rules with this architecture, but there are a few gotchas you should be aware of. 67 | 68 | * Requests for application content may be delayed by various processes such loading of the app shell, loading of JavaScript or fetch requests. Jake Archibald hacked around this by initiating the data request in his Wikipedia offline web app as he [served the shell](https://github.com/jakearchibald/offline-wikipedia/blob/master/public/js/sw/index.js#L59). 69 | 70 | * In the application shell architecture, downloading and adding content can interfere with progressive rendering. This can be an issue for larger JavaScript bundles or longer pieces of content on slow connections. It might even cause performance issues when reading content from the disk. Where possible *include* meaningful page content with the initial download rather than making a separate request for it. In the Wikipedia application, Jake was loading third party content and had to work around this, which is why he used the [Streams API](https://github.com/jakearchibald/offline-wikipedia/blob/master/public/js/page/views/article.js#L86). We strongly recommend reducing the number of requests made for your page content if at all possible. 71 | 72 | ## FAQs 73 | 74 | * Why is there a noscript tag for CSS files? 75 | 76 | There is a great deal of content surrounding how to optimize for the 77 | [critical render path](https://developers.google.com/web/fundamentals/performance/critical-rendering-path/?hl=en). 78 | This boils down to, prioritize CSS for the visible viewport on first load 79 | by inlining those styles and then asynchronously load in additional styles. 80 | At the moment the web doesn't have any way to asynchronously load extra CSS 81 | files. The only way you can do it is to use JavaScript to add the CSS files 82 | after the page has loaded / started to render. 83 | 84 | The reason we have a `