UNPKG

@kospa/router

Version:

Router component for kospa framework

472 lines (471 loc) 17.9 kB
var __extends = (this && this.__extends) || (function () { var extendStatics = function (d, b) { extendStatics = Object.setPrototypeOf || ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; }; return extendStatics(d, b); }; return function (d, b) { extendStatics(d, b); function __() { this.constructor = d; } d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); }; })(); var __spreadArrays = (this && this.__spreadArrays) || function () { for (var s = 0, i = 0, il = arguments.length; i < il; i++) s += arguments[i].length; for (var r = Array(s), k = 0, i = 0; i < il; i++) for (var a = arguments[i], j = 0, jl = a.length; j < jl; j++, k++) r[k] = a[j]; return r; }; (function (factory) { if (typeof module === "object" && typeof module.exports === "object") { var v = factory(require, exports); if (v !== undefined) module.exports = v; } else if (typeof define === "function" && define.amd) { define(["require", "exports", "knockout", "@kospa/base/composer", "@kospa/base/system", "@kospa/base/activator"], factory); } })(function (require, exports) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.Router = exports.BaseRouter = void 0; var ko = require("knockout"); var composer = require("@kospa/base/composer"); var system = require("@kospa/base/system"); var activator = require("@kospa/base/activator"); //#region BaseRouter Class var hasHistory = !!history.pushState, push = Array.prototype.push, slice = Array.prototype.slice; var BaseRouter = /** @class */ (function () { function BaseRouter(options) { this.routes = { none: { matcher: null, handlers: [baseNotFound] } }; this.mode = hasHistory ? "history" : "hash"; this.root = ""; if (options) { if (options.mode) { this.mode = options.mode === "history" && hasHistory ? "history" : "hash"; } var root = normalizeRoute(options.root); if (root) { this.root = root + "/"; this.rootRegExp = new RegExp("^" + this.root + "?"); } if (options.onError) { this._onError = options.onError; } } } BaseRouter.prototype.use = function () { var config = createRouteConfig(arguments), // eslint-disable-line routeId = config.matcher.toString(), route = this.routes[routeId]; if (route) { push.apply(route.handlers, config.handlers); } else { this.routes[routeId] = config; } return this; }; BaseRouter.prototype.unuse = function () { var config = createRouteConfig(arguments), // eslint-disable-line routeId = config.matcher.toString(), route = this.routes[routeId]; if (route) { var handler = void 0, index = void 0; while (handler = config.handlers.pop()) { // eslint-disable-line index = route.handlers.indexOf(handler); if (index !== -1) { route.handlers.splice(index, 1); } } if (route.handlers.length === 0) { delete this.routes[routeId]; } } return this; }; /** * Register handlers for given path. * * @param path Path or regex * @param handlers Handlers for the route */ BaseRouter.prototype.add = function (path) { var handlers = []; for (var _i = 1; _i < arguments.length; _i++) { handlers[_i - 1] = arguments[_i]; } var routeRegExp = typeof path === "string" ? createRouteRegExp(path) : path; return this.use.apply(this, __spreadArrays([routeRegExp], handlers)); }; /** * Remove handlers from given path. * * @param path Path or regex * @param handlers Handlers to unregister */ BaseRouter.prototype.remove = function (path) { var handlers = []; for (var _i = 1; _i < arguments.length; _i++) { handlers[_i - 1] = arguments[_i]; } var routeRegExp = typeof path === "string" ? createRouteRegExp(path) : path; return this.unuse.apply(this, __spreadArrays([routeRegExp], handlers)); }; /** * Register handlers that are executed when no route is found. * * @param handlers Middlewares and route config to use when no route is found */ BaseRouter.prototype.none = function () { var handlers = []; for (var _i = 0; _i < arguments.length; _i++) { handlers[_i] = arguments[_i]; } var route = this.routes.none; if (route.handlers[0] === baseNotFound) { route.handlers = handlers; } else { push.apply(route.handlers, handlers); } return this; }; /** Start listening for URL changes. */ BaseRouter.prototype.start = function () { var _this = this; var current = null; var delay = function () { _this.timeout = setTimeout(handle, 50); // eslint-disable-line }; var handle = function () { if (BaseRouter.skip) { BaseRouter.skip = false; current = _this._current; return delay(); } var f = _this.getFragment(); if (current !== f) { current = f; _this.handle(f).then(delay, delay); } else { delay(); } }; this.stop(); delay(); return this; }; /** Stop listening for route changes */ BaseRouter.prototype.stop = function () { clearInterval(this.timeout); return this; }; /** Stop and clear all configs. */ BaseRouter.prototype.clear = function () { this.stop(); this.routes = { none: { matcher: null, handlers: [baseNotFound] } }; this.mode = "hash"; this.root = ""; return this; }; /** * Navigate to the given path. * * @param path Path to navigate to (/ if empty) */ BaseRouter.prototype.navigate = function (path) { path = this.root + normalizeRoute(path); BaseRouter.skip = false; if (hasHistory) { history.pushState(null, null, this.mode === "history" ? path : "#" + path); } else { location.hash = "#" + path; } return this; }; /** * Replace the current path with the given one. * * @param path Path to navigate to (/ if empty) * @param skipHandling true to avoid handling the new path */ BaseRouter.prototype.replace = function (path, skipHandling) { path = this.root + normalizeRoute(path); BaseRouter.skip = skipHandling || false; if (hasHistory) { history.replaceState(null, null, this.mode === "history" ? path : "#" + path); } else { location.hash = "#" + path; } return this; }; /** * Manually handle the given route. * * @param fragment Fragment to handle (current path if not specifed) */ BaseRouter.prototype.handle = function (fragment) { var _this = this; if (fragment === void 0) { fragment = this.getFragment(); } if (fragment === null) { return Promise.resolve(this); } var handlers = []; Object.keys(this.routes).forEach(function (routeId) { var route = _this.routes[routeId]; if (!route.matcher) return; var match = fragment.match(route.matcher); if (match) { handlers.push(executeHandlers.bind(_this, _this.routes[routeId].handlers, slice.call(match, 1))); } }); if (handlers.length === 0) { handlers = this.routes.none.handlers; } return executeHandlers(handlers) .catch(this.onError.bind(this)) .then(function () { _this._current = fragment; return _this; }); }; /** Get current fragment path. */ BaseRouter.prototype.getFragment = function () { var fragment = this.mode === "history" ? location.pathname : location.hash; fragment = normalizeRoute(fragment); if (this.root === "") { return fragment; } if (this.rootRegExp && this.rootRegExp.test(fragment)) { return fragment.replace(this.rootRegExp, ""); } return null; }; /** Allows to override the error handling. */ BaseRouter.prototype.onError = function (err) { console.log("err", err); var onErr = this._onError || baseOnError; return onErr.call(this, err); }; BaseRouter.skip = false; return BaseRouter; }()); exports.BaseRouter = BaseRouter; //#region Private Methods function createRouteConfig(args) { var arg = args[0]; var i = 0, routeRegExp; if (typeof arg === "function") { routeRegExp = /(.*)/; } else { routeRegExp = typeof arg === "string" ? createRouteRegExp(arg + "(*)") : arg; i = 1; } return { matcher: routeRegExp, handlers: slice.call(args, i) }; } function normalizeRoute(path) { return String(path || "") .replace(/^#/, "") .replace(/\/$/, "") .replace(/^\//, ""); } function createRouteRegExp(route) { route = normalizeRoute(route) .replace(/\*/g, function () { return ".*"; }) .replace(/\?/g, function () { return "\\?"; }) .replace(/\(([^)]+)\)/g, function (_, t1) { t1 = t1.replace(/:[a-zA-Z0-9]+/g, function () { return "([^\\/\\(\\)\\?]+?)"; }); return "(?:" + t1 + ")?"; }) .replace(/:[a-zA-Z0-9]+/g, function () { return "([^\\/\\(\\)\\?]+?)"; }); return new RegExp("^" + route + "$"); } function executeHandlers(_handlers, args) { if (args === void 0) { args = []; } var handlers = _handlers.slice(); return next(); function next() { var handler = handlers.shift(); if (!handler) { return Promise.resolve(); } return Promise.resolve() .then(function () { return handler.apply(void 0, args); }) .then(function (res) { if (res === false) { return false; } return next(); }); } } function baseNotFound() { throw new Error("Not Found"); } function baseOnError(err) { system.error("router>", err); if (this._current) { this.replace(this._current, true); } throw err; } var Router = /** @class */ (function (_super) { __extends(Router, _super); function Router(options) { var _this = _super.call(this, options) || this; _this.routeHandlers = {}; _this.currentRoute = ko.observable(); _this.currentViewModel = activator.createActivateObservable(); _this.isNavigating = ko.observable(false); _this.navigation = ko.pureComputed(function () { return Object.keys(_this.routeHandlers) .map(function (key) { return _this.routeHandlers[key]; }) .filter(function (config) { return ko.unwrap(config.visible); }); }); return _this; } /** * Configure a route for a ViewModel. * * @param config Configuration to register */ Router.prototype.route = function (config) { var handlerId = config.path.toString(), handler = createRouteHandler(this, config); config.handler = handler; if (!config.href && typeof config.path === "string") { config.href = (this.mode === "hash" ? "#" : "") + this.root + config.path; } this.routeHandlers[handlerId] = config; this.add(config.path, handler); return this; }; /** * Unregister a route for a ViewModel * * @param config Configuration to remove */ Router.prototype.deroute = function (config) { var handlerId = config.path.toString(), innerConfig = this.routeHandlers[handlerId]; if (innerConfig && config.handler) { this.remove(innerConfig.path, config.handler); delete this.routeHandlers[handlerId]; } return this; }; /** * Register a ViewModel when no handlers match the current route. * * @param config Configuration for the not found handler */ Router.prototype.notFound = function (config) { this.none(createRouteHandler(this, config)); return this; }; /** * Register a child router for the given prefix. * * @param path Path prefix for the child router * @param childRouter Child router */ Router.prototype.child = function (path, childRouter) { if (!childRouter) { childRouter = new Router({ mode: this.mode, root: path, onError: this.onError.bind(this) }); } this.use(new RegExp("^" + path + "(?:/(.*))?$"), childRouter.handle.bind(childRouter)); return childRouter; }; /** Stop the router and clear all configs. */ Router.prototype.clear = function () { _super.prototype.clear.call(this); this.routeHandlers = {}; this.currentRoute(null); this.currentViewModel(null); return this; }; /** * Manually handle the given fragment. * * @param fragment URL fragment to handle (current fragment if empty) */ Router.prototype.handle = function (fragment) { var _this = this; if (fragment === null) { return Promise.resolve(this); } this.isNavigating(true); return _super.prototype.handle.call(this, fragment).then(function () { _this.isNavigating(false); return _this; }, function (err) { _this.isNavigating(false); throw err; }); }; return Router; }(BaseRouter)); exports.Router = Router; var rootRouter = new Router(); exports.default = rootRouter; function createRouteHandler(self, route) { return function () { var args = []; for (var _i = 0; _i < arguments.length; _i++) { args[_i] = arguments[_i]; } var oldRoute = self.currentRoute(); self.currentRoute(route); self.currentViewModel.args = args; self.currentViewModel(route.viewmodel); return self.currentViewModel.then(function (vm) { var title = vm && vm.title ? ko.unwrap(vm.title) : ko.unwrap(route.title); if (title) { document.title = title; } }, function (err) { self.currentRoute(oldRoute); throw err; }); }; } //#endregion //#region Router Handlers ko.bindingHandlers["router"] = { init: function (element, valueAccessor) { var val = valueAccessor(), router; if (val instanceof Router) { router = val; } else { val = typeof val === "object" ? val.router : val; router = val || rootRouter; } ko.computed({ disposeWhenNodeIsRemoved: element, read: function () { var config = router.currentRoute.peek(), vm = router.currentViewModel(); if (!config || !vm) { return; } composer.compose(element, system.extend({}, config, { viewmodel: vm })) .catch(system.error); } }); return { controlsDescendantBindings: true }; } }; ko.virtualElements.allowedBindings["router"] = true; ko.components.register("kospa-router", { template: "<!--ko router: $data--><!--/ko-->" }); }); //#endregion