UNPKG

marbles

Version:

Front-end framework for routing, http, and data handling

403 lines (339 loc) 10.1 kB
"use strict"; var _interopRequireWildcard = function (obj) { return obj && obj.__esModule ? obj : { "default": obj }; }; Object.defineProperty(exports, "__esModule", { value: true }); /* @flow weak */ var _Utils = require("./utils"); var _Utils2 = _interopRequireWildcard(_Utils); var _Dispatcher = require("./dispatcher"); var _Dispatcher2 = _interopRequireWildcard(_Dispatcher); var _QueryParams = require("./query_params"); var _QueryParams2 = _interopRequireWildcard(_QueryParams); /* * * * * * * * * * * * * * * * * * * * Inspired by Backbone.js History * * * * * * * * * * * * * * * * * * * */ var pathWithParams = function pathWithParams(path, params) { if (params.length === 0) { return path; } // clone params array params = [].concat(params); // we mutate the first param obj, so clone that params[0] = _Utils2["default"].extend({}, params[0]); // expand named params in path path = path.replace(/:([^\/]+)/g, function (m, key) { var paramObj = params[0]; if (paramObj.hasOwnProperty(key)) { var val = paramObj[key]; delete paramObj[key]; return encodeURIComponent(val); } else { return ":" + key; } }); // add remaining params to query string var queryString = _QueryParams2["default"].serializeParams(params); if (queryString.length > 1) { if (path.indexOf("?") !== -1) { path = path + "&" + queryString.substring(1); } else { path = path + queryString; } } return path; }; /** * @memberof Marbles * @class * @see Marbles.Router */ var History = _Utils2["default"].createClass({ displayName: "Marbles.History", willInitialize: function willInitialize() { this.started = false; this.handlers = []; this.options = {}; this.path = null; this.prevPath = null; this.handlePopState = this.handlePopState.bind(this); }, // register router register: function register(router) { router.history = this; router.routes.forEach((function (route) { this.route(route.route, route.name, route.handler, route.paramNames, route.opts, router); }).bind(this)); }, // register route handler // handlers are checked in the reverse order // they are defined, so if more than one // matches, only the one defined last will // be called route: (function (_route) { function route(_x, _x2, _x3, _x4, _x5, _x6) { return _route.apply(this, arguments); } route.toString = function () { return _route.toString(); }; return route; })(function (route, name, callback, paramNames, opts, router) { if (typeof callback !== "function") { throw new Error(this.constructor.displayName + ".prototype.route(): callback is not a function: " + JSON.stringify(callback)); } if (typeof route.test !== "function") { throw new Error(this.constructor.displayName + ".prototype.route(): expected route to be a RegExp: " + JSON.stringify(route)); } this.handlers.push({ route: route, name: name, paramNames: paramNames, callback: callback, opts: opts, router: router }); }), // navigate to given path via pushState // if available/enabled or by mutation // of window.location.href // // pass options.trigger = false to prevent // route handler from being called // // pass options.replaceState = true to // replace the current history item // // pass options.force = true to force // handler to be called even if path is // already loaded navigate: function navigate(path, options) { if (!this.started) { throw new Error("history has not been started"); } if (!options) { options = {}; } if (!options.hasOwnProperty("trigger")) { options.trigger = true; } if (!options.hasOwnProperty("replace")) { options.replace = false; } if (!options.hasOwnProperty("force")) { options.force = false; } if (path[0] === "/") { // trim / prefix path = path.substring(1); } if (options.params) { path = pathWithParams(path, options.params); } if (path === this.path && !options.force) { // we are already there and handler is not forced return; } path = this.pathWithRoot(path); if (!this.options.pushState) { // pushState is unavailable/disabled window.location.href = path; return; } // push or replace state var method = "pushState"; if (options.replace) { method = "replaceState"; } window.history[method]({}, document.title, path); if (options.trigger) { // cause route handler to be called this.loadURL({ replace: options.replace }); } }, pathWithRoot: function pathWithRoot(path) { // add path root if it's not already there var root = this.options.root; if (root && path.substr(0, root.length) !== root) { if (root.substring(root.length - 1) !== "/" && path.substr(0, 1) !== "/") { // add path seperator if not present in root or path path = "/" + path; } path = root + path; } return path; }, getURLFromPath: function getURLFromPath(path, params) { if (params && params.length !== 0) { path = pathWithParams(path, params); } return window.location.protocol + "//" + window.location.host + this.pathWithRoot(path); }, /** * @func * @param {Object} options * @desc Starts listenening to pushState events and calls route handlers when appropriate * @example * import History from "marbles/history"; * new History().start({ * root: "/", // if your app is mounted anywhere other than the domain root, enter the path prefix here * pushState: true, // set to `false` in the unlikely event you wish to disable pushState (falls back to manipulating window.location) * dispatcher: Marbles.Dispatcher // The Dispatcher all events are passed to * }); */ start: function start(options) { if (this.started) { throw new Error("history has already been started"); } if (!options) { options = {}; } if (!options.hasOwnProperty("trigger")) { options.trigger = true; } this.dispatcher = options.dispatcher || _Dispatcher2["default"]; this.context = options.context || {}; this.options = _Utils2["default"].extend({ root: "/", pushState: true }, options); this.path = this.getPath(); if (this.options.pushState) { // set pushState to false if it's not supported this.options.pushState = !!(window.history && window.history.pushState); } if (this.options.pushState) { // init back button binding window.addEventListener("popstate", this.handlePopState, false); } this.started = true; this.trigger("start"); if (options.trigger) { this.loadURL(); } }, // stop pushState handling stop: function stop() { if (this.options.pushState) { window.removeEventListener("popstate", this.handlePopState, false); } this.started = false; this.trigger("stop"); }, getPath: function getPath() { var path = window.location.pathname; if (window.location.search) { path += window.location.search; } var root = this.options.root.replace(/([^\/])\/$/, "$1"); if (path.indexOf(root) !== -1) { // trim root from path path = path.substr(root.length); } return path.replace(this.constructor.regex.routeStripper, ""); }, handlePopState: function handlePopState() { this.checkURL(); }, // check if path has changed checkURL: function checkURL() { var current = this.getPath(); if (current === this.path) { // path is the same, do nothing return; } this.loadURL(); }, getHandler: function getHandler(path) { path = path || this.getPath(); path = path.split("?")[0]; var handler = null; for (var i = 0, _len = this.handlers.length; i < _len; i++) { if (this.handlers[i].route.test(path)) { handler = this.handlers[i]; break; } } return handler; }, // Attempt to find handler for current path // returns matched handler or null loadURL: function loadURL(options) { options = options || {}; var prevPath = this.path; var prevParams = this.pathParams; if (!options.replace) { this.prevPath = prevPath; this.prevParams = prevParams; } var path = this.path = this.getPath(); var parts = path.match(this.constructor.regex.routeParts); path = parts[1]; var params = _QueryParams2["default"].deserializeParams(parts[2] || ""); this.pathParams = params; var prevHandler; if (this.path !== this.prevPath) { prevHandler = this.getHandler(prevPath); } var handler = this.getHandler(path); var __handlerAbort = false; var handlerAbort = function handlerAbort() { __handlerAbort = true; }; if (prevHandler) { var handlerUnloadEvent = { handler: prevHandler, nextHandler: handler, path: prevPath, nextPath: path, params: prevParams, nextParams: params, abort: handlerAbort, context: this.context }; if (prevHandler.router.beforeHandlerUnload) { prevHandler.router.beforeHandlerUnload.call(prevHandler.router, handlerUnloadEvent); } if (!__handlerAbort) { this.trigger("handler:before-unload", handlerUnloadEvent); } } if (handler && !__handlerAbort) { var router = handler.router; params = _QueryParams2["default"].combineParams(params, router.extractNamedParams.call(router, handler.route, path, handler.paramNames)); var event = { handler: handler, path: path, params: params, abort: handlerAbort, context: this.context }; if (handler.router.beforeHandler) { handler.router.beforeHandler.call(handler.router, event); } if (!__handlerAbort) { this.trigger("handler:before", event); } if (!__handlerAbort) { handler.callback.call(router, params, handler.opts, this.context); this.trigger("handler:after", { handler: handler, path: path, params: params }); } } return handler; }, trigger: function trigger(eventName, args) { return this.dispatcher.dispatch(_Utils2["default"].extend({ source: "Marbles.History", name: eventName }, args)); } }); History.regex = { routeStripper: /^[\/]/, routeParts: /^([^?]*)(?:\?(.*))?$/ // 1: path, 2: params }; exports.pathWithParams = pathWithParams; exports["default"] = History;