Skip to content

Commit

Permalink
fanout html (#1911)
Browse files Browse the repository at this point in the history
* basic fanout and dev middleware

* polyfills for ie11

* consume hl=xx, persist pathname

* simple replaceState routing

* support click to change page
  • Loading branch information
samthor committed Nov 25, 2016
1 parent 81b279c commit eeb1524
Show file tree
Hide file tree
Showing 9 changed files with 178 additions and 85 deletions.
1 change: 0 additions & 1 deletion bower.json
Expand Up @@ -38,7 +38,6 @@
"paper-icon-button": "PolymerElements/paper-icon-button#^1.0.5",
"paper-toolbar": "PolymerElements/paper-toolbar#^1.1.1",
"app-storage": "PolymerElements/app-storage#^0.9.0",
"flatiron-director": "PolymerLabs/flatiron-director#^1.0",
"jquery": "2.1.1",
"google-apis": "GoogleWebComponents/google-apis#^1.0",
"google-map": "GoogleWebComponents/google-map#^1.0",
Expand Down
2 changes: 1 addition & 1 deletion elements/santa-app-behavior.html
Expand Up @@ -46,7 +46,7 @@
* Returns the URL for the given route.
*/
urlFor: function(route) {
return pageUrl + '#' + route;
return new URL(route ? route + '.html' : './', window.location).toString();
},

};
Expand Down
85 changes: 22 additions & 63 deletions elements/santa-tracker-router.html
Expand Up @@ -13,13 +13,9 @@
specific language governing permissions and limitations under the License.
-->
<link rel="import" href="../components/polymer/polymer.html">
<script src="../components/flatiron-director/director/director.min.js"></script>
<script>
; // nb: prevents the above code from being invoked on the next statement
</script>

<!--
Router for Santa Tracker. Modified from github.com/PolymerLabs/flatiron-director.
Router for Santa Tracker.
-->
<dom-module id="santa-tracker-router">
<script>
Expand Down Expand Up @@ -54,17 +50,13 @@
}

/**
* Serializes an object of key/value URL params into a string.
* Gets the current route based on the pathname.
*
* @param {Object} urlParams The key/value URL params.
* @return {string} serialized URL params.
* @return {string|undefined} current route, or undefined for unknown
*/
function serializeParams(urlParams) {
var url = [];
for (var key in urlParams) {
url.push(key + '=' + urlParams[key]);
}
return url.join('&');
function currentRoute() {
var m = /\/(\w+)\.html$/.exec(window.location.pathname);
return m ? m[1] : undefined;
}

var privateRouter;
Expand All @@ -83,7 +75,8 @@
route: {
type: String,
observer: '_routeChanged',
notify: true
value: function() { return currentRoute(); },
notify: true,
},

/**
Expand All @@ -98,66 +91,32 @@
notify: true
},

/**
* Used to record the route seen by the private router. Prevents calls
* to window.history.replaceState caused by a route change. This is set
* before route is changed, and consumed when `_routeChanged` sees a new
* value matching this.
*/
_internalRoute: {
type: String
},

},

/**
* The Flatiron Route object.
*/
get router() {
if (!privateRouter) {
privateRouter = new Router();
privateRouter.init();
}
return privateRouter;
},

ready: function() {
var handler = function() {
var route = this.router.getRoute() ? this.router.getRoute().join(this.router.delimiter) : '';
var pieces = routeToParts(route);

this._internalRoute = pieces.route;
try {
this.route = pieces.route;
this._setSceneParams(Object.freeze(pieces.params));
} finally {
this._internalRoute = null;
}
}.bind(this);

// Catch all URLs.
this.router.on(/(.*)/, handler);
handler();
window.addEventListener('popstate', function() {
this.route = currentRoute();
}.bind(this));

// TODO: routeToParts?
},

detached: function() {
throw new Error('santa-tracker-router should never be detatched')
throw new Error('santa-tracker-router should never be detached')
},

go: function(route) {
window.location.hash = '#' + route;
replace: function(route) {
var url = new URL(route + '.html', window.location);
history.replaceState(null, '', url);
this.route = route;
},

_routeChanged: function(newValue) {
if (this._internalRoute == newValue) {
this._internalRoute = null;
_routeChanged: function(newValue, oldValue) {
if (oldValue === undefined || newValue === currentRoute()) {
return;
}

var url = window.location.href;
var hash = window.location.hash;
window.history.replaceState(null, '', url.substr(0, url.length - hash.length) + '#' + newValue);
this._setSceneParams({});
var url = new URL(newValue + '.html', window.location);
history.pushState(null, '', url);
},

});
Expand Down
45 changes: 37 additions & 8 deletions elements/santacontroller/santa-app.html
Expand Up @@ -58,7 +58,6 @@

<!-- Scenes -->
<lazy-pages id="lazypages"
selected="{{route}}"
selected-item="{{selectedScene}}"
selectable=":not([disabled])"
attr-for-selected="route"
Expand Down Expand Up @@ -581,6 +580,7 @@
},

listeners: {
'click': 'onClick',
'scene-progress': 'onSceneProgress',
'scene-ready': 'onSceneReady',
'analytics-track-event': 'trackEvent',
Expand Down Expand Up @@ -622,14 +622,46 @@
this._prepareHouses();

if (!this.route) {
this.route = this.defaultRoute;
var route = this.defaultRoute;
if (window.location.hash) {
route = window.location.hash.substr(1);
}
this.$.router.replace(route);
}
},

detached: function() {
throw new Error('santa-app was detached');
},

onClick: function(ev) {
var link = ev.target.closest('a[href]');
if (!link) { return; }

// Ignore if some weird meta keys are hit, this is probably 'open in new tab'.
if (ev.ctrlKey || ev.metaKey || ev.which > 1) { return; }

// Check domain/origin/etc.
var targetUrl = new URL(link.href);
if (targetUrl.origin !== window.location.origin) { return; }

// Check the URLs have the same dirname (e.g. "/foo/bar/" vs "/foo/bar/zing.html").
var lastSlash = window.location.pathname.lastIndexOf('/');
if (targetUrl.pathname.lastIndexOf('/') !== lastSlash) { return; }

// Pull out either the flat "/", or "/foo.html" at this path.
var m = /\/(?:(\w+)\.html|)$/.exec(targetUrl.pathname.substr(lastSlash));
if (!m) { return; }

// Good job! You found a route. But throw it out if it's invalid for some reason.
var route = m[1] || this.defaultRoute;
if (!this.$.lazypages.isValidRoute(route)) { return; }

// Set the route. Hooray! \o/
this.route = route;
ev.preventDefault();
},

/**
* Handles a loading progress hint, hiding or displaying the preloader.
*/
Expand Down Expand Up @@ -779,6 +811,7 @@
if (this.validateRoute()) {
// Route is valid. Send to analytics and load the scene.
this.analyticsService.trackPageView('/' + newValue);
this.$.lazypages.selected = newValue;
return;
}

Expand Down Expand Up @@ -1013,18 +1046,14 @@
}
this.todayHouse = todayHouse;
change && this._notifyHouses();

if (this.route !== undefined && !this.validateRoute()) {
this.route = this.defaultRoute;
}
},

_onKeysPressed: function(e, detail) {
switch (e.detail.key) {
case 'esc':
var wasOpen = this.$.chrome.closeDrawer();
if (!wasOpen) {
this.$.router.go(this.defaultRoute);
this.route = this.defaultRoute;
}
break;
case 'enter':
Expand All @@ -1035,7 +1064,7 @@

_duringTrackerChanged: function(newValue, oldValue) {
if (typeof oldValue === 'boolean' && typeof newValue === 'boolean') {
this.$.router.go(this.defaultRoute);
this.route = this.defaultRoute;
}
},

Expand Down
41 changes: 41 additions & 0 deletions gulp_scripts/fanout/index.js
@@ -0,0 +1,41 @@
/*
* Copyright 2016 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/

/* jshint node: true */

const path = require('path');
const through = require('through2');
const gutil = require('gulp-util');

module.exports = function fanout(sceneNames) {
return through.obj(function(file, enc, cb) {
if (file.isStream()) { throw new gutil.PluginError('fanout', 'No stream support'); }
if (file.isNull() || path.basename(file.path) !== 'index.html') {
// Only fanout if the file is index.html.
return cb(null, file);
}

const dir = path.dirname(file.path);
sceneNames.forEach(sceneName => {
const clone = file.clone();
clone.path = path.join(dir, 'scenes', `${sceneName}.html`);
this.push(clone);
});

this.push(file); // always push original file
cb();
});
};
1 change: 1 addition & 0 deletions gulp_scripts/index.js
Expand Up @@ -18,6 +18,7 @@ module.exports = {
changedFlag: require('./changed_flag'),
crisper: require('./crisper'),
devScene: require('./dev-scene'),
fanout: require('./fanout'),
fileManifest: require('./file_manifest'),
i18nManifest: require('./i18n_manifest'),
i18nReplace: require('./i18n_replace'),
Expand Down
14 changes: 14 additions & 0 deletions gulpfile.js
Expand Up @@ -116,6 +116,8 @@ const DIST_STATIC_DIR = argv.pretty ? PRETTY_DIR : (STATIC_DIR + '/' + argv.buil

// Broad scene config for Santa Tracker.
const SCENE_CLOSURE_CONFIG = require('./scenes');
// TODO(samthor): This list isn't exhaustive. It should be expanded.
const SCENE_FANOUT = ['village', 'tracker', 'boatload', 'matching', 'press', 'about'];

// List of scene names to compile.
const SCENE_NAMES = argv.scene ?
Expand Down Expand Up @@ -401,6 +403,7 @@ gulp.task('build-prod', function() {
strict: !!argv.strict,
path: '_messages',
}))
.pipe(scripts.fanout(SCENE_FANOUT))
.pipe(gulp.dest(DIST_PROD_DIR));

const jsStream = gulp.src(['sw.js'])
Expand Down Expand Up @@ -479,10 +482,21 @@ gulp.task('serve', ['default', 'watch'], function() {
livereloadFiles.push('**/*.min.js', '**/*.html');
}

const simplePath = new RegExp(/^\/(\w+)\.html(|\?.*)$/);
const fanoutHelper = function(req, res, next) {
// If we match a file which would be a fanout of index.html in prod, serve index.html instead.
const match = simplePath.exec(req.originalUrl);
if (match && SCENE_FANOUT.includes(match[1])) {
req.url = '/index.html';
}
return next();
};

const browserSync = require('browser-sync').create();
browserSync.init({
files: livereloadFiles,
injectChanges: argv.devmode, // Can not inject css into lazy Polymer scenes.
middleware: [fanoutHelper],
port: argv.port,
server: ['.', '.devmode'],
startPath: argv.scene && (argv.devmode ? `/scenes/${argv.scene}/` : `/#${argv.scene}`),
Expand Down
34 changes: 22 additions & 12 deletions index.html
Expand Up @@ -109,23 +109,33 @@
// Look in /intl/.../ and ?hl=... for user override lang. Send the browser to the correct /intl/
// version via History API. e.g., a user loading "/#foo?hl=de" will get "/intl/de/#foo".
var url = (function() {
// Look for ?hl=...
var match = window.location.search.match(/\bhl=([^&]*)\b/);
var hl = (match ? match[1] : null);

// Look for /intl/../ (look for _last_, in case it's in URL twice?!)
// Look for /intl/../ (look for _last_). This wins over ?hl=...
var match = window.location.pathname.match(/.*\/intl\/([^_/]+)(?:|_ALL)\//);
var urlLang = (match ? match[1] : hl);
window.requestLang = urlLang;
var lang = match && match[1];

// Otherwise, look for ?hl=..., and remove it from the URL regardless.
var search = window.location.search || '';
var matchLang = /(?:\?|&)hl=([^&]*)\b/;
if (!lang) {
match = matchLang.exec(search);
lang = match && match[1];
}
search = search.replace(matchLang, '').replace(/^&/, '?');

// Save requestLang on window. This is a bit ugly, but lets us persist this param.
window.requestLang = lang;

// Grab the final URL component. This is 'us', the fanned out HTML file.
match = window.location.pathname.match(/(\/(?:\w+\.html|))$/)
var path = match ? match[1] : '/';

// Generate canonical URL.
var url = window.location.origin + '/';
if (urlLang) {
var url = window.location.origin;
if (lang) {
var prod = window.location.hostname === 'santatracker.google.com';
url += 'intl/' + urlLang + (prod ? '' : '_ALL') + '/';
url += '/intl/' + lang + (prod ? '' : '_ALL');
}
// TODO(samthor): Consume ?hl=... here.
return url + (window.location.search || '') + window.location.hash;
return url + path + search + window.location.hash;
})();
history.replaceState(null, document.title, url);

Expand Down

0 comments on commit eeb1524

Please sign in to comment.