UNPKG

navigo

Version:

A simple vanilla JavaScript router with a fallback for older browsers

470 lines (409 loc) 14.5 kB
(function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : typeof define === 'function' && define.amd ? define(factory) : (global.Navigo = factory()); }(this, (function () { 'use strict'; var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; function isPushStateAvailable() { return !!(typeof window !== 'undefined' && window.history && window.history.pushState); } function Navigo(r, useHash, hash) { this.root = null; this._routes = []; this._useHash = useHash; this._hash = typeof hash === 'undefined' ? '#' : hash; this._paused = false; this._destroyed = false; this._lastRouteResolved = null; this._notFoundHandler = null; this._defaultHandler = null; this._usePushState = !useHash && isPushStateAvailable(); this._onLocationChange = this._onLocationChange.bind(this); this._genericHooks = null; this._historyAPIUpdateMethod = 'pushState'; if (r) { this.root = useHash ? r.replace(/\/$/, '/' + this._hash) : r.replace(/\/$/, ''); } else if (useHash) { this.root = this._cLoc().split(this._hash)[0].replace(/\/$/, '/' + this._hash); } this._listen(); this.updatePageLinks(); } function clean(s) { if (s instanceof RegExp) return s; return s.replace(/\/+$/, '').replace(/^\/+/, '^/'); } function regExpResultToParams(match, names) { if (names.length === 0) return null; if (!match) return null; return match.slice(1, match.length).reduce(function (params, value, index) { if (params === null) params = {}; params[names[index]] = decodeURIComponent(value); return params; }, null); } function replaceDynamicURLParts(route) { var paramNames = [], regexp; if (route instanceof RegExp) { regexp = route; } else { regexp = new RegExp(route.replace(Navigo.PARAMETER_REGEXP, function (full, dots, name) { paramNames.push(name); return Navigo.REPLACE_VARIABLE_REGEXP; }).replace(Navigo.WILDCARD_REGEXP, Navigo.REPLACE_WILDCARD) + Navigo.FOLLOWED_BY_SLASH_REGEXP, Navigo.MATCH_REGEXP_FLAGS); } return { regexp: regexp, paramNames: paramNames }; } function getUrlDepth(url) { return url.replace(/\/$/, '').split('/').length; } function compareUrlDepth(urlA, urlB) { return getUrlDepth(urlB) - getUrlDepth(urlA); } function findMatchedRoutes(url) { var routes = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : []; return routes.map(function (route) { var _replaceDynamicURLPar = replaceDynamicURLParts(clean(route.route)), regexp = _replaceDynamicURLPar.regexp, paramNames = _replaceDynamicURLPar.paramNames; var match = url.replace(/^\/+/, '/').match(regexp); var params = regExpResultToParams(match, paramNames); return match ? { match: match, route: route, params: params } : false; }).filter(function (m) { return m; }); } function match(url, routes) { return findMatchedRoutes(url, routes)[0] || false; } function root(url, routes) { var matched = routes.map(function (route) { return route.route === '' || route.route === '*' ? url : url.split(new RegExp(route.route + '($|\/)'))[0]; }); var fallbackURL = clean(url); if (matched.length > 1) { return matched.reduce(function (result, url) { if (result.length > url.length) result = url; return result; }, matched[0]); } else if (matched.length === 1) { return matched[0]; } return fallbackURL; } function isHashChangeAPIAvailable() { return typeof window !== 'undefined' && 'onhashchange' in window; } function extractGETParameters(url) { return url.split(/\?(.*)?$/).slice(1).join(''); } function getOnlyURL(url, useHash, hash) { var onlyURL = url, split; var cleanGETParam = function cleanGETParam(str) { return str.split(/\?(.*)?$/)[0]; }; if (typeof hash === 'undefined') { // To preserve BC hash = '#'; } if (isPushStateAvailable() && !useHash) { onlyURL = cleanGETParam(url).split(hash)[0]; } else { split = url.split(hash); onlyURL = split.length > 1 ? cleanGETParam(split[1]) : cleanGETParam(split[0]); } return onlyURL; } function manageHooks(handler, hooks, params) { if (hooks && (typeof hooks === 'undefined' ? 'undefined' : _typeof(hooks)) === 'object') { if (hooks.before) { hooks.before(function () { var shouldRoute = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : true; if (!shouldRoute) return; handler(); hooks.after && hooks.after(params); }, params); return; } else if (hooks.after) { handler(); hooks.after && hooks.after(params); return; } } handler(); } function isHashedRoot(url, useHash, hash) { if (isPushStateAvailable() && !useHash) { return false; } if (!url.match(hash)) { return false; } var split = url.split(hash); return split.length < 2 || split[1] === ''; } Navigo.prototype = { helpers: { match: match, root: root, clean: clean, getOnlyURL: getOnlyURL }, navigate: function navigate(path, absolute) { var to; path = path || ''; if (this._usePushState) { to = (!absolute ? this._getRoot() + '/' : '') + path.replace(/^\/+/, '/'); to = to.replace(/([^:])(\/{2,})/g, '$1/'); history[this._historyAPIUpdateMethod]({}, '', to); this.resolve(); } else if (typeof window !== 'undefined') { path = path.replace(new RegExp('^' + this._hash), ''); window.location.href = window.location.href.replace(/#$/, '').replace(new RegExp(this._hash + '.*$'), '') + this._hash + path; } return this; }, on: function on() { var _this = this; for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) { args[_key] = arguments[_key]; } if (typeof args[0] === 'function') { this._defaultHandler = { handler: args[0], hooks: args[1] }; } else if (args.length >= 2) { if (args[0] === '/') { var func = args[1]; if (_typeof(args[1]) === 'object') { func = args[1].uses; } this._defaultHandler = { handler: func, hooks: args[2] }; } else { this._add(args[0], args[1], args[2]); } } else if (_typeof(args[0]) === 'object') { var orderedRoutes = Object.keys(args[0]).sort(compareUrlDepth); orderedRoutes.forEach(function (route) { _this.on(route, args[0][route]); }); } return this; }, off: function off(handler) { if (this._defaultHandler !== null && handler === this._defaultHandler.handler) { this._defaultHandler = null; } else if (this._notFoundHandler !== null && handler === this._notFoundHandler.handler) { this._notFoundHandler = null; } this._routes = this._routes.reduce(function (result, r) { if (r.handler !== handler) result.push(r); return result; }, []); return this; }, notFound: function notFound(handler, hooks) { this._notFoundHandler = { handler: handler, hooks: hooks }; return this; }, resolve: function resolve(current) { var _this2 = this; var handler, m; var url = (current || this._cLoc()).replace(this._getRoot(), ''); if (this._useHash) { url = url.replace(new RegExp('^\/' + this._hash), '/'); } var GETParameters = extractGETParameters(current || this._cLoc()); var onlyURL = getOnlyURL(url, this._useHash, this._hash); if (this._paused) return false; if (this._lastRouteResolved && onlyURL === this._lastRouteResolved.url && GETParameters === this._lastRouteResolved.query) { if (this._lastRouteResolved.hooks && this._lastRouteResolved.hooks.already) { this._lastRouteResolved.hooks.already(this._lastRouteResolved.params); } return false; } m = match(onlyURL, this._routes); if (m) { this._callLeave(); this._lastRouteResolved = { url: onlyURL, query: GETParameters, hooks: m.route.hooks, params: m.params, name: m.route.name }; handler = m.route.handler; manageHooks(function () { manageHooks(function () { m.route.route instanceof RegExp ? handler.apply(undefined, m.match.slice(1, m.match.length)) : handler(m.params, GETParameters); }, m.route.hooks, m.params, _this2._genericHooks); }, this._genericHooks, m.params); return m; } else if (this._defaultHandler && (onlyURL === '' || onlyURL === '/' || onlyURL === this._hash || isHashedRoot(onlyURL, this._useHash, this._hash))) { manageHooks(function () { manageHooks(function () { _this2._callLeave(); _this2._lastRouteResolved = { url: onlyURL, query: GETParameters, hooks: _this2._defaultHandler.hooks }; _this2._defaultHandler.handler(GETParameters); }, _this2._defaultHandler.hooks); }, this._genericHooks); return true; } else if (this._notFoundHandler) { manageHooks(function () { manageHooks(function () { _this2._callLeave(); _this2._lastRouteResolved = { url: onlyURL, query: GETParameters, hooks: _this2._notFoundHandler.hooks }; _this2._notFoundHandler.handler(GETParameters); }, _this2._notFoundHandler.hooks); }, this._genericHooks); } return false; }, destroy: function destroy() { this._routes = []; this._destroyed = true; this._lastRouteResolved = null; this._genericHooks = null; clearTimeout(this._listeningInterval); if (typeof window !== 'undefined') { window.removeEventListener('popstate', this._onLocationChange); window.removeEventListener('hashchange', this._onLocationChange); } }, updatePageLinks: function updatePageLinks() { var self = this; if (typeof document === 'undefined') return; this._findLinks().forEach(function (link) { if (!link.hasListenerAttached) { link.addEventListener('click', function (e) { if ((e.ctrlKey || e.metaKey) && e.target.tagName.toLowerCase() == 'a') { return false; } var location = self.getLinkPath(link); if (!self._destroyed) { e.preventDefault(); self.navigate(location.replace(/\/+$/, '').replace(/^\/+/, '/')); } }); link.hasListenerAttached = true; } }); }, generate: function generate(name) { var data = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; var result = this._routes.reduce(function (result, route) { var key; if (route.name === name) { result = route.route; for (key in data) { result = result.toString().replace(':' + key, data[key]); } } return result; }, ''); return this._useHash ? this._hash + result : result; }, link: function link(path) { return this._getRoot() + path; }, pause: function pause() { var status = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : true; this._paused = status; if (status) { this._historyAPIUpdateMethod = 'replaceState'; } else { this._historyAPIUpdateMethod = 'pushState'; } }, resume: function resume() { this.pause(false); }, historyAPIUpdateMethod: function historyAPIUpdateMethod(value) { if (typeof value === 'undefined') return this._historyAPIUpdateMethod; this._historyAPIUpdateMethod = value; return value; }, disableIfAPINotAvailable: function disableIfAPINotAvailable() { if (!isPushStateAvailable()) { this.destroy(); } }, lastRouteResolved: function lastRouteResolved() { return this._lastRouteResolved; }, getLinkPath: function getLinkPath(link) { return link.getAttribute('href'); }, hooks: function hooks(_hooks) { this._genericHooks = _hooks; }, _add: function _add(route) { var handler = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null; var hooks = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : null; if (typeof route === 'string') { route = encodeURI(route); } this._routes.push((typeof handler === 'undefined' ? 'undefined' : _typeof(handler)) === 'object' ? { route: route, handler: handler.uses, name: handler.as, hooks: hooks || handler.hooks } : { route: route, handler: handler, hooks: hooks }); return this._add; }, _getRoot: function _getRoot() { if (this.root !== null) return this.root; this.root = root(this._cLoc().split('?')[0], this._routes); return this.root; }, _listen: function _listen() { var _this3 = this; if (this._usePushState) { window.addEventListener('popstate', this._onLocationChange); } else if (isHashChangeAPIAvailable()) { window.addEventListener('hashchange', this._onLocationChange); } else { var cached = this._cLoc(), current = void 0, _check = void 0; _check = function check() { current = _this3._cLoc(); if (cached !== current) { cached = current; _this3.resolve(); } _this3._listeningInterval = setTimeout(_check, 200); }; _check(); } }, _cLoc: function _cLoc() { if (typeof window !== 'undefined') { if (typeof window.__NAVIGO_WINDOW_LOCATION_MOCK__ !== 'undefined') { return window.__NAVIGO_WINDOW_LOCATION_MOCK__; } return clean(window.location.href); } return ''; }, _findLinks: function _findLinks() { return [].slice.call(document.querySelectorAll('[data-navigo]')); }, _onLocationChange: function _onLocationChange() { this.resolve(); }, _callLeave: function _callLeave() { var lastRouteResolved = this._lastRouteResolved; if (lastRouteResolved && lastRouteResolved.hooks && lastRouteResolved.hooks.leave) { lastRouteResolved.hooks.leave(lastRouteResolved.params); } } }; Navigo.PARAMETER_REGEXP = /([:*])(\w+)/g; Navigo.WILDCARD_REGEXP = /\*/g; Navigo.REPLACE_VARIABLE_REGEXP = '([^\/]+)'; Navigo.REPLACE_WILDCARD = '(?:.*)'; Navigo.FOLLOWED_BY_SLASH_REGEXP = '(?:\/$|$)'; Navigo.MATCH_REGEXP_FLAGS = ''; return Navigo; })));