"use strict"; // modified page.js by visionmedia // - changed regexes to components // - removed unused crap // - refactored to classes // - simplified method chains // - added ability to call .save() in .exit() without side effects // - page refresh recovers state from history // - rename .save() to .replaceState() // - offer .url const clickEvent = document.ontouchstart ? "touchstart" : "click"; const uri = require("./util/uri.js"); let location = window.history.location || window.location; function _getOrigin() { return ( location.protocol + "//" + location.hostname + (location.port ? ":" + location.port : "") ); } function _isSameOrigin(href) { return href && href.indexOf(_getOrigin()) === 0; } function _getBaseHref() { const bases = document.getElementsByTagName("base"); return bases.length > 0 ? bases[0].href.replace(_getOrigin(), "").replace(/\/+$/, "") : ""; } class Context { constructor(path, state) { const base = _getBaseHref(); path = path.indexOf("/") !== 0 ? "/" + path : path; path = path.indexOf(base) !== 0 ? base + path : path; this.canonicalPath = path; this.path = !path.indexOf(base) ? path.slice(base.length) : path; this.title = document.title; this.state = state || {}; this.state.path = path; this.parameters = {}; } pushState() { history.pushState(this.state, this.title, this.canonicalPath); } replaceState() { history.replaceState(this.state, this.title, this.canonicalPath); } } class Route { constructor(path) { this.method = "GET"; this.path = path; this.parameterNames = []; if (this.path === null) { this.regex = /.*/; } else { let parts = []; for (let component of this.path) { if (component[0] === ":") { parts.push("([^/]+)"); this.parameterNames.push(component.substr(1)); } else { // assert [a-z]+ parts.push(component); } } let regexString = "^/" + parts.join("/"); regexString += "(?:/*|/((?:(?:[a-z]+=[^/]+);)*(?:[a-z]+=[^/]+)))$"; this.parameterNames.push("variable"); this.regex = new RegExp(regexString); } } middleware(fn) { return (ctx, next) => { if (this.match(ctx.path, ctx.parameters)) { return fn(ctx, next); } next(); }; } match(path, parameters) { const qsIndex = path.indexOf("?"); const pathname = ~qsIndex ? path.slice(0, qsIndex) : path; const match = this.regex.exec(pathname); if (!match) { return false; } try { for (let i = 1; i < match.length; i++) { const name = this.parameterNames[i - 1]; const value = match[i]; if (value === undefined) { continue; } if (name === "variable") { for (let word of (value || "").split(/;/)) { const [key, subvalue] = word.split(/=/, 2); parameters[key] = uri.unescapeParam(subvalue); } } else { parameters[name] = uri.unescapeParam(value); } } } catch (e) { return false; } return true; } } class Router { constructor() { this._callbacks = []; this._exits = []; } enter(path) { const route = new Route(path); for (let i = 1; i < arguments.length; ++i) { this._callbacks.push(route.middleware(arguments[i])); } } exit(path, fn) { const route = new Route(path); for (let i = 1; i < arguments.length; ++i) { this._exits.push(route.middleware(arguments[i])); } } start() { if (this._running) { return; } this._running = true; this._onPopState = _onPopState(this); this._onClick = _onClick(this); window.addEventListener("popstate", this._onPopState, false); document.addEventListener(clickEvent, this._onClick, false); const url = location.pathname + location.search + location.hash; return this.replace(url, history.state, true); } stop() { if (!this._running) { return; } this._running = false; document.removeEventListener(clickEvent, this._onClick, false); window.removeEventListener("popstate", this._onPopState, false); } showNoDispatch(path, state) { const ctx = new Context(path, state); ctx.pushState(); this.ctx = ctx; return ctx; } show(path, state, push) { const ctx = new Context(path, state); const oldPath = this.ctx ? this.ctx.path : ctx.path; this.dispatch(ctx, () => { if (ctx.path !== oldPath && push !== false) { ctx.pushState(); } }); return ctx; } replace(path, state, dispatch) { var ctx = new Context(path, state); if (dispatch) { this.dispatch(ctx, () => { ctx.replaceState(); }); } else { ctx.replaceState(); } return ctx; } dispatch(ctx, middle) { const swap = (_ctx, next) => { this.ctx = ctx; middle(); next(); }; const callChain = (this.ctx ? this._exits : []).concat( [swap], this._callbacks, [this._unhandled, (ctx, next) => {}] ); let i = 0; let fn = () => { callChain[i++](this.ctx, fn); }; fn(); } _unhandled(ctx, next) { let current = location.pathname + location.search; if (current === ctx.canonicalPath) { return; } this.stop(); location.href = ctx.canonicalPath; } get url() { return location.pathname + location.search + location.hash; } } const _onPopState = (router) => { let loaded = false; if (document.readyState === "complete") { loaded = true; } else { window.addEventListener("load", () => { setTimeout(() => { loaded = true; }, 0); }); } return (e) => { if (!loaded) { return; } if (e.state) { const path = e.state.path; router.replace(path, e.state, true); } else { router.show(location.pathname + location.hash, undefined, false); } }; }; const _onClick = (router) => { return (e) => { if (1 !== _which(e)) { return; } if (e.metaKey || e.ctrlKey || e.shiftKey) { return; } if (e.defaultPrevented) { return; } let el = e.path ? e.path[0] : e.target; while (el && el.nodeName !== "A") { el = el.parentNode; } if (!el || el.nodeName !== "A") { return; } if ( el.hasAttribute("download") || el.getAttribute("rel") === "external" ) { return; } const link = el.getAttribute("href"); if (el.pathname === location.pathname && (el.hash || "#" === link)) { return; } if (link && link.indexOf("mailto:") > -1) { return; } if (el.target) { return; } if (!_isSameOrigin(el.href)) { return; } const base = _getBaseHref(); const orig = el.pathname + el.search + (el.hash || ""); const path = !orig.indexOf(base) ? orig.slice(base.length) : orig; if (base && orig === path) { return; } e.preventDefault(); router.show(orig); }; }; function _which(e) { e = e || window.event; return e.which === null ? e.button : e.which; } Router.prototype.Context = Context; Router.prototype.Route = Route; module.exports = new Router();