UNPKG

@app-elements/router

Version:
561 lines (465 loc) 13.9 kB
import React, { createContext, useContext, useRef, useEffect, useState } from 'react'; function _extends() { _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); } function _objectWithoutPropertiesLoose(source, excluded) { if (source == null) return {}; var target = {}; var sourceKeys = Object.keys(source); var key, i; for (i = 0; i < sourceKeys.length; i++) { key = sourceKeys[i]; if (excluded.indexOf(key) >= 0) continue; target[key] = source[key]; } return target; } var has = Object.prototype.hasOwnProperty; /** * Decode a URI encoded string. * * @param {String} input The URI encoded string. * @returns {String} The decoded string. * @api private */ var decode = function decode(input) { return decodeURIComponent(input.replace(/\+/g, ' ')); }; /** * Simple query string parser. * * @param {String} query The query string that needs to be parsed. * @returns {Object} * @api public */ function parse(query) { var parser = /([^=?&]+)=?([^&]*)/g; var result = {}; var part; while ((part = parser.exec(query)) != null) { var key = decode(part[1]); // // Prevent overriding of existing properties. This ensures that build-in // methods like `toString` or __proto__ are not overriden by malicious // querystrings. // if (key in result) continue; result[key] = decode(part[2]); } return result; } /** * Transform an object into a query string. * * @param {Object} obj Object that should be transformed. * @param {String} prefix Optional prefix. * @returns {String} * @api public */ function stringify(obj, prefix) { if (prefix === void 0) { prefix = ''; } var pairs = []; // // Optionally prefix with a '?' if needed // if (typeof prefix !== 'string') prefix = '?'; for (var key in obj) { if (has.call(obj, key)) { pairs.push(encodeURIComponent(key) + '=' + encodeURIComponent(obj[key])); } } return pairs.length ? prefix + pairs.join('&') : ''; } var hasProp = Object.prototype.hasOwnProperty; var segmentize = function segmentize(url) { return url.replace(/(^\/+|\/+$)/g, '').split('/'); }; function updateQuery(queries) { var existingParams = parse(window.location.search); return window.location.pathname + ("?" + stringify(_extends({}, existingParams, queries))); } // route matching logic, taken from preact-router var exec = function exec(url, route) { var reg = /(?:\?([^#]*))?(#.*)?$/; var c = url.match(reg); var matches = {}; var ret; if (c && c[1]) { var p = c[1].split('&'); for (var i = 0; i < p.length; i++) { var r = p[i].split('='); matches[decodeURIComponent(r[0])] = decodeURIComponent(r.slice(1).join('=')); } } url = segmentize(url.replace(reg, '')); route = segmentize(route || ''); var max = Math.max(url.length, route.length); for (var _i = 0; _i < max; _i++) { if (route[_i] && route[_i].charAt(0) === ':') { var param = route[_i].replace(/(^:|[+*?]+$)/g, ''); var flags = (route[_i].match(/[+*?]+$/) || {})[0] || ''; var plus = ~flags.indexOf('+'); var star = ~flags.indexOf('*'); var val = url[_i] || ''; if (!val && !star && (flags.indexOf('?') < 0 || plus)) { ret = false; break; } matches[param] = decodeURIComponent(val); if (plus || star) { matches[param] = url.slice(_i).map(decodeURIComponent).join('/'); break; } } else if (route[_i] !== url[_i]) { ret = false; break; } } if (ret === false) return false; return matches; }; function getRouteComponent(routes, currentPath) { for (var route in routes) { if (hasProp.call(routes[route], 'routes')) { var shouldRender = Object.values(routes[route].routes).some(function (_ref) { var path = _ref.path; return path && exec(currentPath, path); }); if (shouldRender) { var App = routes[route].component; return [App, null]; } } else { var routeArgs = exec(currentPath, routes[route].path); if (routeArgs) { var newRoute = { name: route, path: routes[route].path, args: _extends({}, routes[route].args || {}, routeArgs) }; var Component = routes[route].component; return [Component, newRoute]; } } } } var getAllRoutes = function getAllRoutes(routes) { return Object.keys(routes || {}).reduce(function (acc, r) { var _extends2; return hasProp.call(routes[r], 'routes') ? _extends({}, acc, getAllRoutes(routes[r].routes)) : _extends({}, acc, (_extends2 = {}, _extends2[r] = routes[r], _extends2)); }, {}); }; var getHref = function getHref(_ref2) { var rule = _ref2.rule, args = _ref2.args, queries = _ref2.queries, hash = _ref2.hash; var replaced = Object.keys(args).reduce(function (acc, k) { return acc.replace(":" + k, args[k]); }, rule.path); var hasQueries = Object.keys(queries).length > 0; var hashStr = hash != null ? "#" + hash : ''; return "" + replaced + (!hasQueries ? '' : '?' + stringify(queries)) + hashStr; }; var Context = createContext('Router'); var allRoutes; var skipScroll; function useRouterState() { var _useState = useState(null), path = _useState[0], setPath = _useState[1]; var _useState2 = useState(null), route = _useState2[0], setRoute = _useState2[1]; var routeTo = function routeTo(newPath) { if (newPath !== path) { window.history.pushState(null, null, newPath); setPath(newPath); } }; var setPathMaybe = function setPathMaybe(newPath) { if (path == null || path !== newPath) { setPath(newPath); } }; var setRouteMaybe = function setRouteMaybe(newRoute) { if (route == null || route.name !== newRoute.name) { setRoute(newRoute); } }; return { path: path, setPath: setPathMaybe, routeTo: routeTo, route: route, setRoute: setRouteMaybe }; } function useRouter() { var value = useContext(Context); if (value == null) { throw new Error('Component must be wrapped with <RouteProvider>'); } return value; } function useScrollToTop() { var backRef = useRef(false); var nodeRef = useRef(); var pathRef = useRef(); var _useRouter = useRouter(), path = _useRouter.path; useEffect(function () { var onPop = function onPop() { return backRef.current = true; }; window.addEventListener('popstate', onPop); return function () { return window.removeEventListener('popstate', onPop); }; }, []); var isBack = backRef && backRef.current; if (!isBack && path !== pathRef.current) { if (path !== skipScroll) { if (nodeRef && nodeRef.current) { nodeRef.current.scrollIntoView(); } else { window.scrollTo(0, 0); } } else { skipScroll = null; } pathRef.current = path; } else if (isBack) { backRef.current = false; } return nodeRef; } function Link(_ref) { var to = _ref.to, name = _ref.name, className = _ref.className, activeClass = _ref.activeClass, _ref$args = _ref.args, args = _ref$args === void 0 ? {} : _ref$args, _ref$queries = _ref.queries, queries = _ref$queries === void 0 ? {} : _ref$queries, hash = _ref.hash, noScroll = _ref.noScroll, children = _ref.children, props = _objectWithoutPropertiesLoose(_ref, ["to", "name", "className", "activeClass", "args", "queries", "hash", "noScroll", "children"]); if (allRoutes == null) { throw new Error('<Link /> must be child of <RouteProvider />'); } to = to || name; var rule = allRoutes[to]; if (!rule) { console.error('No route found for name: ' + to); return null; } var href = getHref({ rule: rule, args: args, queries: queries, hash: hash }); return /*#__PURE__*/React.createElement(Context.Consumer, null, function (_ref2) { var path = _ref2.path, routeTo = _ref2.routeTo; var isActive = path === href; var onClick = function onClick(ev) { ev.preventDefault(); if (noScroll) { skipScroll = href; } routeTo(href); }; return /*#__PURE__*/React.createElement("a", _extends({ href: href, onClick: onClick, className: ((className || '') + " " + (isActive ? activeClass : '')).trim() }, props), children); }); } function RouteTo(_ref3) { var to = _ref3.to, name = _ref3.name, url = _ref3.url, _ref3$args = _ref3.args, args = _ref3$args === void 0 ? {} : _ref3$args, _ref3$queries = _ref3.queries, queries = _ref3$queries === void 0 ? {} : _ref3$queries, hash = _ref3.hash; var href; to = to || name; if (url != null) { href = url; } else { if (allRoutes == null) { throw new Error('<RouteTo /> must be child of <RouteProvider />'); } var rule = allRoutes[to]; if (!rule) { throw new Error('No route found for name: ' + to); } href = getHref({ rule: rule, args: args, queries: queries, hash: hash }); } return /*#__PURE__*/React.createElement(Context.Consumer, null, function (context) { if (href) { context.routeTo(href); } return null; }); } function SyncRouterState(_ref4) { var children = _ref4.children; var routeNameRef = useRef(null); if (!children || typeof children !== 'function') { throw new Error('<SyncRouterState /> requires a function as a child.'); } return /*#__PURE__*/React.createElement(Context.Consumer, null, function (context) { if (!context || context.route == null) return null; if (routeNameRef.current == null || routeNameRef.current !== context.route.name) { children({ route: context.route, path: context.path }); routeNameRef.current = context.route.name; } }); } function RouteProvider(_ref5) { var routes = _ref5.routes, initialPath = _ref5.initialPath, children = _ref5.children; var value = useRouterState(); if (allRoutes == null) { allRoutes = getAllRoutes(routes); } useEffect(function () { if (value.path == null) { value.setPath(initialPath || window.location.pathname + window.location.search); } }, [value.path, value.setPath]); useEffect(function () { var onPop = function onPop(ev) { var url = window.location.pathname; if (value.path !== url) { window.history.replaceState(null, null, url); value.setPath(url); } }; window.addEventListener('popstate', onPop); return function () { return window.removeEventListener('popstate', onPop); }; }, [value.path, value.setPath]); return /*#__PURE__*/React.createElement(Context.Provider, { value: value }, children); } function Router(props) { if (!props.routes) { throw new Error('<Router /> must be given a routes object.'); } var localRoutes = props.routes; var context = useRouter(); if (context == null) { throw new Error('<Router /> must be wrapped with <RouteProvider />.'); } var path = context.path, setRoute = context.setRoute; if (path == null) { return null; } var pair = getRouteComponent(localRoutes, path); if (pair == null) { setRoute({ name: 'Not Found', args: {}, notFound: true, path: path }); return null; } var Component = pair[0], newRoute = pair[1]; if (newRoute) { setRoute(newRoute); } var childProps = newRoute != null ? newRoute.args : {}; return typeof Component === 'function' ? /*#__PURE__*/React.createElement(Component, childProps) : Component; } function StackRouter(_ref6) { var localRoutes = _ref6.routes, _ref6$limit = _ref6.limit, limit = _ref6$limit === void 0 ? 2 : _ref6$limit, children = _ref6.children; var stackRef = useRef([]); if (!localRoutes) { throw new Error('<StackRouter /> must be given a routes object.'); } var context = useRouter(); if (context == null) { throw new Error('<StackRouter /> must be wrapped with <RouteProvider />.'); } useEffect(function () { var onPop = function onPop() { stackRef.current = stackRef.current.slice(0, -1); }; window.addEventListener('popstate', onPop); return function () { return window.removeEventListener('popstate', onPop); }; }, [stackRef]); var path = context.path, setRoute = context.setRoute; if (path == null) { return null; } var pair = getRouteComponent(localRoutes, path); if (pair == null) { setRoute({ name: 'Not Found', args: {}, notFound: true, path: path }); return null; } var Component = pair[0], newRoute = pair[1]; if (newRoute) { setRoute(newRoute); var last = stackRef.current[stackRef.current.length - 1]; if (last == null || last.path !== path) { if (last != null) { stackRef.current[stackRef.current.length - 1].isBack = true; } stackRef.current = [].concat(stackRef.current, Object.assign({}, newRoute, { Component: Component, path: path })); } } var stack = stackRef.current.length > limit ? stackRef.current.slice(-limit) : [].concat(stackRef.current); return children({ stack: stack }); } export { Link, RouteProvider, RouteTo, Router, StackRouter, SyncRouterState, exec, getHref, updateQuery, useRouter, useScrollToTop }; //# sourceMappingURL=router.m.js.map