├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── docs ├── GUIDE.md ├── README.md ├── router-dom.md └── router.md ├── examples ├── dom │ ├── about │ │ └── index.html │ ├── contact │ │ └── index.html │ ├── index.html │ ├── index.js │ └── style.css └── simple │ └── index.html ├── index.js ├── lib ├── lemonade-router-dom.umd.js └── lemonade-router.umd.js ├── package-lock.json ├── package.json ├── rollup.config.js └── src ├── DefaultTransition.js ├── DefaultTransitionDOM.js ├── Router.js ├── RouterDOM.js └── helpers.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /examples 2 | /docs 3 | package-lock.json 4 | rollup.config.js 5 | node_modules 6 | .npmignore 7 | .gitignore -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Raphaël Améaume 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # lemonade-router 2 | 3 | `lemonade-router` is a minimal non-opiniated routing library to create websites and interactive experiences with custom transitions. It is heavily inspired by [Barba.js](https://github.com/barbajs/barba) and [React Router](https://github.com/ReactTraining/react-router) and uses [history](https://npmjs.com/package/history) under the hood. 4 | 5 | It is built using ES6 features. 6 | 7 | - [Documentation](https://github.com/raphaelameaume/lemonade-router/tree/master/docs/README.md) 8 | - [Examples](https://github.com/raphaelameaume/lemonade-router/tree/master/demo) 9 | 10 | ## Installation 11 | 12 | `npm install lemonade-router` 13 | 14 | ### Usage 15 | 16 | ```js 17 | import { Router } from "lemonade-router"; 18 | 19 | let router = Router(); 20 | 21 | // views 22 | router.view('/', () => Home()); 23 | router.view('/news', () => News()); 24 | 25 | // matches 26 | router.match('/news/:id', async ({ params }) => { 27 | let News = await import('./News.js'); 28 | 29 | router.view(`/news/${params.id}`, () => News(params.id)); 30 | }); 31 | 32 | router.match('*', async () => { 33 | let NotFound = await import('./NotFound.js'); 34 | 35 | router.view('*', () => NotFound()); 36 | }); 37 | 38 | // transitions 39 | router.transition('/', '/news', () => FromHomeToNews()); 40 | 41 | router.listen(); 42 | 43 | ``` 44 | 45 | ## Motivation 46 | This routing library attempts to solve different problems I had the past few years working on websites and interactives experiences: 47 | - Change URL without fetching an existing page, useful for WebGL experiences or when all the DOM is already here 48 | - Create custom transitions where I have total control over DOM changes 49 | - Define complex loading sequences 50 | - Allow multiple pages to work on different URLs (multilingual websites) 51 | - Split code to avoid loading big bundles 52 | 53 | ## Credits 54 | - [Barba.js](https://github.com/barbajs/barba) 55 | - [React Router](https://github.com/ReactTraining/react-router) 56 | - [Canvas-sketch](https://github.com/mattdesl/canvas-sketch) 57 | 58 | ## License 59 | 60 | MIT License, see [LICENSE](https://github.com/raphaelameaume/lemonade-router/tree/master/LICENSE) for details -------------------------------------------------------------------------------- /docs/GUIDE.md: -------------------------------------------------------------------------------- 1 | #### [lemonade-router](../README.md) → [Documentation](./README.md) → Guide 2 | 3 | --- 4 | 5 | - [Register a view](#register-a-view) 6 | - [Register a transition](#register-a-transition) 7 | - [Handle URL parameters](#handle-url-parameters) 8 | - [Code splitting](#code-splitting) 9 | 10 | ## Register a view 11 | ```js 12 | /* with functions */ 13 | function Home() { 14 | function enter() { 15 | console.log('Home :: enter'); 16 | } 17 | 18 | function leave() { 19 | console.log('Home :: enter'); 20 | } 21 | 22 | return { enter, leave }; 23 | } 24 | 25 | router.view('/', () => Home()); 26 | 27 | /* with classes */ 28 | class Home { 29 | constructor() {} 30 | enter() {} 31 | leave() {} 32 | } 33 | 34 | router.view('/' , () => return new Home()); 35 | ``` 36 | 37 | ## Register a transition 38 | ```js 39 | /* with functions*/ 40 | function CustomTransition() { 41 | return { 42 | play: (prevView, nextView) { 43 | // do sync or async stuff 44 | if (prevView) { // prevView is null on start 45 | prevView.leave(nextView) 46 | } 47 | 48 | nextView.enter(prevView); 49 | } 50 | } 51 | } 52 | 53 | router.transition('/about', '/news', () => CustomTransition(), false); 54 | 55 | /* with classes */ 56 | class CustomTransition() { 57 | constructor() {} 58 | play(prevView, nextView) {} 59 | } 60 | 61 | router.transition('/news', '/', () => new CustomTransition()); 62 | ``` 63 | 64 | ## Code Splitting 65 | ```js 66 | router.match('/about', async ({ url }) => { 67 | let About = await import('./About.js'); // dynamic import 68 | 69 | router.view(url, () => About()); 70 | }) 71 | ``` 72 | 73 | ## Handle URL parameters 74 | `router.match` supports async functions and return URL parameters as an object 75 | ```js 76 | router.match('/news/:slug', async ({ url, params }) => { 77 | let News = await import('./News.js'); 78 | 79 | router.view(url, () => News(params.slug)); 80 | }) 81 | ``` 82 | 83 | ## [RouterDOM] Use a custom fetch function 84 | By default, RouterDOM fetch the next page using the `fetch` API. In some cases you'll want to provide your own fetch function. To do so, you can overwrite the fetch function on RouterDOM. Be careful as Router.fetch expect a function returning a `Promise` in order to work. 85 | 86 | ```js 87 | import { RouterDOM } from "lemonade-router"; 88 | 89 | RouterDOM.fetch = (url) => { 90 | return new Promise((resolve, reject) => { 91 | let xhr = new XMLHttpRequest(); 92 | xhr.addEventListener("load", () => { 93 | resolve(xhr.responseText); 94 | }); 95 | xhr.open("GET", url); 96 | xhr.send(); 97 | }); 98 | }; 99 | ``` -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | #### [lemonade-router](../README.md) → [Documentation](./README.md) 2 | 3 | --- 4 | 5 | API docs 6 | - [`Router` documentation](./router.md) 7 | - [`RouterDOM` documentation](./router-dom.md) 8 | 9 | Guide 10 | - [Register a view](./GUIDE.md#register-a-view) 11 | - [Register a transition](./GUIDE.md#register-a-transition) 12 | - [Handle URL parameters](./GUIDE.md#handle-url-parameters) 13 | - [Code splitting](./GUIDE.md#code-splitting) 14 | - [Use a custom fetch function](./GUIDE.md#use-a-custom-fetch-function) -------------------------------------------------------------------------------- /docs/router-dom.md: -------------------------------------------------------------------------------- 1 | #### [lemonade-router](../README.md) → [Documentation](./README.md) → RouterDOM 2 | 3 | --- 4 | 5 | ## RouterDOM 6 | 7 | ### router = RouterDOM([options]) 8 | - `options.wrapperQuery`: A function used to query the main wrapper. Default to `() => querySelector('.lemonade-wrapper`)`. 9 | - `options.containerQuery`: A function used to query the containers. Default to `(wrapper) => wrapper.querySelector('.lemonade-container`)`. 10 | - `options.cacheEnabled`: A boolean defining if fetched pages should be cached. Default to `true`. 11 | Check [`Router` documentation](./router.md) for other available options. 12 | 13 | ### router.listen([options]) 14 | Check [`Router` documentation](./router.md) for available options. 15 | 16 | ### router.view(url, fn) 17 | See [`Router` documentation](./router.md#routerviewurl-fn)) 18 | 19 | ### router.transition(from, to, fn, backAndForth) 20 | See [`Router` documentation](./router.md#routertransitionfrom-to-fn-backandforth) 21 | 22 | ### router.match(pattern, fn) 23 | See [`Router` documentation](./router.md#routermatchpattern-fn) -------------------------------------------------------------------------------- /docs/router.md: -------------------------------------------------------------------------------- 1 | #### [lemonade-router](../README.md) → [Documentation](./README.md) → Router 2 | 3 | --- 4 | 5 | ## Router 6 | 7 | ### router = Router([options]) 8 | - `options.defaultTransition`: An object or a class implementing a `play(prevView, nextView, transitionParams)` method. Default to [DefaultTransition] 9 | - `options.scrollRestoration`: A string to change `history.scrollRestoration`. Check the [documentation](https://developers.google.com/web/updates/2015/09/history-api-scroll-restoration) to see available options. Default to `'auto'`. 10 | - `options.basename`: Set a prefix to URL before navigating to an other url. Useful for subdomain with relative links. 11 | - `options.transitionParams`: An object that will be passed to `transition.play` 12 | 13 | ### router.listen([options]) 14 | Search for current view from current URL and start listening to URL changes. 15 | - `options.clickEvents`: If set to `true`, will prevent links across the pages to trigger a page refresh 16 | - `options.clickIgnoreClass`: Disable previous behaviour for links with specified className 17 | 18 | ### router.view(url, fn) 19 | Register a view to the Router. If the current URL matches the `url` param, it will trigger `fn`. 20 | - `url`: A string or an array of string reflecting exact URLs 21 | - `fn`: Must return an instance of a view 22 | 23 | Guide: 24 | - [Register a view](./GUIDE.md#register-a-view) 25 | 26 | ### router.transition(from, to, fn, backAndForth) 27 | Register a transition to the router defined by source and destination URLs 28 | - `from`: A string or an array of URLs 29 | - `to`: A string or an array of URLs 30 | - `fn`: A function that returns a class or an object implementing a `play(prevView, nextView)` method (can be async) like in [DefaultTransition] 31 | - `backAndForth`: A boolean defining if the transition should be played in reverse too. Default to `true`. 32 | 33 | Guide: 34 | - [Register a transition](./GUIDE.md#register-a-transition) 35 | 36 | 37 | ### router.match(pattern, fn) 38 | Register a pattern and execute `fn` on match. Useful for URL with parameters like `/news/:slug` or code splitting. 39 | The router will resolve views after executing matching callbacks so you can add view in `fn`. 40 | - `options.pattern`: A string or an array of string of patterns. This uses [path-to-regexp](https://www.npmjs.com/package/path-to-regexp) under the hood. 41 | - `fn`: A sync or async function to be executed on match. 42 | 43 | Guides: 44 | - [Handle URL parameters](./GUIDE.md#handle-URL-parameters) 45 | - [Code splitting](./GUIDE.md#code-splitting) -------------------------------------------------------------------------------- /examples/dom/about/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | About 9 | 10 | 11 | 12 | 13 |
14 | Home 15 | About 16 | Contact 17 |
18 |
19 |
20 |

About

21 |
22 |
23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /examples/dom/contact/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Contact 9 | 10 | 11 | 12 | 13 |
14 | Home 15 | About 16 | Contact 17 |
18 |
19 |
20 |

Contact

21 |
22 |
23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /examples/dom/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Home 9 | 10 | 11 | 12 | 13 |
14 | Home 15 | About 16 | Contact 17 |
18 |
19 |
20 |

Home

21 |
22 |
23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /examples/dom/index.js: -------------------------------------------------------------------------------- 1 | function enterView(view) { 2 | view.style.display = 'flex'; 3 | } 4 | 5 | function leaveView(view) { 6 | view.style.display = 'none'; 7 | } 8 | 9 | function Home() { 10 | let view; 11 | 12 | function enter() { 13 | console.log('Home :: enter'); 14 | view = document.querySelector('.view--home'); 15 | enterView(view); 16 | } 17 | 18 | function leave() { 19 | console.log('Home :: leave'); 20 | leaveView(view); 21 | } 22 | 23 | return { enter, leave }; 24 | } 25 | 26 | function About() { 27 | let view; 28 | 29 | function enter() { 30 | console.log('About :: enter'); 31 | view = document.querySelector('.view--about'); 32 | enterView(view); 33 | } 34 | 35 | function leave() { 36 | console.log('About :: leave'); 37 | leaveView(view); 38 | } 39 | 40 | return { enter, leave }; 41 | } 42 | 43 | function Contact() { 44 | let view; 45 | 46 | function enter() { 47 | console.log('Contact :: enter'); 48 | view = document.querySelector('.view--contact'); 49 | enterView(view); 50 | } 51 | 52 | function leave() { 53 | console.log('Contact :: leave'); 54 | leaveView(view); 55 | } 56 | 57 | return { enter, leave }; 58 | } 59 | 60 | let router = Lemonade.RouterDOM({ 61 | basename: '/examples/dom', 62 | }); 63 | 64 | router.view('', Home); 65 | 66 | router.view('/contact/', Contact); 67 | 68 | router.match('/about', async () => { 69 | router.view('/about', About); 70 | }); 71 | 72 | router.listen({ 73 | clickEvents: true, 74 | }); -------------------------------------------------------------------------------- /examples/dom/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | font-family: Arial, sans-serif; 5 | } 6 | .header { 7 | background: #f0f0f0; 8 | border-radius: 2px; 9 | display: flex; 10 | justify-content: center; 11 | padding: 20px 0; 12 | } 13 | 14 | .link { 15 | margin: 0 20px; 16 | text-transform: uppercase; 17 | } 18 | 19 | .view { 20 | display: none; 21 | justify-content: center; 22 | } 23 | 24 | .view__title { 25 | font-size: 48px; 26 | margin-top: 100px; 27 | } -------------------------------------------------------------------------------- /examples/simple/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | lemonade-router 8 | 25 | 26 | 27 | 28 |
29 | Home 30 | About 31 | Contact 32 |
33 |
34 |

Home

35 |
36 |
37 |

About

38 |
39 |
40 |

Contact

41 |
42 | 43 | 44 | 115 | 116 | 117 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | export { Router } from "./src/Router.js"; 2 | export { RouterDOM } from "./src/RouterDOM.js"; -------------------------------------------------------------------------------- /lib/lemonade-router-dom.umd.js: -------------------------------------------------------------------------------- 1 | !function(e,n){"object"==typeof exports&&"undefined"!=typeof module?n(exports):"function"==typeof define&&define.amd?define(["exports"],n):n((e=e||self).Lemonade={})}(this,(function(e){"use strict";function n(){return(n=Object.assign||function(e){for(var n=1;n=0;h--){var d=a[h];"."===d?r(a,h):".."===d?(r(a,h),l++):l&&(r(a,h),l--)}if(!f)for(;l--;l)a.unshift("..");!f||""===a[0]||a[0]&&t(a[0])||a.unshift("");var p=a.join("/");return i&&"/"!==p.substr(-1)&&(p+="/"),p}(s.pathname,c.pathname)):s.pathname=c.pathname:s.pathname||(s.pathname="/"),s}var a=!("undefined"==typeof window||!window.document||!window.document.createElement);function c(e,n){n(window.confirm(e))}var s="popstate",f="hashchange";function u(){try{return window.history.state||{}}catch(e){return{}}}function l(e){void 0===e&&(e={}),a||function(e,n){if(!e)throw new Error("Invariant failed")}(!1);var t,r,i=window.history,l=(-1===(t=window.navigator.userAgent).indexOf("Android 2.")&&-1===t.indexOf("Android 4.0")||-1===t.indexOf("Mobile Safari")||-1!==t.indexOf("Chrome")||-1!==t.indexOf("Windows Phone"))&&window.history&&"pushState"in window.history,h=!(-1===window.navigator.userAgent.indexOf("Trident")),d=e,p=d.forceRefresh,v=void 0!==p&&p,w=d.getUserConfirmation,m=void 0===w?c:w,g=d.keyLength,y=void 0===g?6:g,x=e.basename?function(e){return"/"===e.charAt(e.length-1)?e.slice(0,-1):e}("/"===(r=e.basename).charAt(0)?r:"/"+r):"";function E(e){var n=e||{},t=n.key,r=n.state,i=window.location,a=i.pathname+i.search+i.hash;return x&&(a=function(e,n){return function(e,n){return 0===e.toLowerCase().indexOf(n.toLowerCase())&&-1!=="/?#".indexOf(e.charAt(n.length))}(e,n)?e.substr(n.length):e}(a,x)),o(a,r,t)}function A(){return Math.random().toString(36).substr(2,y)}var k,b,O=(k=null,b=[],{setPrompt:function(e){return k=e,function(){k===e&&(k=null)}},confirmTransitionTo:function(e,n,t,r){if(null!=k){var i="function"==typeof k?k(e,n):k;"string"==typeof i?"function"==typeof t?t(i,r):r(!0):r(!1!==i)}else r(!0)},appendListener:function(e){var n=!0;function t(){n&&e.apply(void 0,arguments)}return b.push(t),function(){n=!1,b=b.filter((function(e){return e!==t}))}},notifyListeners:function(){for(var e=arguments.length,n=new Array(e),t=0;t=48&&s<=57||s>=65&&s<=90||s>=97&&s<=122||95===s))break;a+=e[c++]}if(!a)throw new TypeError("Missing parameter name at "+t);n.push({type:"NAME",index:t,value:a}),t=c}else n.push({type:"CLOSE",index:t,value:e[t++]});else n.push({type:"OPEN",index:t,value:e[t++]});else n.push({type:"ESCAPED_CHAR",index:t++,value:e[t++]});else n.push({type:"MODIFIER",index:t,value:e[t++]})}return n.push({type:"END",index:t,value:""}),n}(e),r=n.prefixes,i=void 0===r?"./":r,o="[^"+d(n.delimiter||"/#?")+"]+?",a=[],c=0,s=0,f="",u=function(e){if(s-1:void 0===A;i||(v+="(?:"+h+"(?="+l+"))?"),k||(v+="(?="+h+"|"+l+")")}return new RegExp(v,p(t))}(h(e,t),n,t)}function w(e,n,t){return e instanceof RegExp?function(e,n){if(!n)return e;var t=e.source.match(/\((?!\?)/g);if(t)for(var r=0;r0){const[e,...o]=a,s=r.reduce(((e,n,t)=>(e[n.name]=o[t],e)),{}),f=[...n,i].filter((e=>!e.includes(":")));await t({urls:f,url:i,params:s}),c.has(i)&&(u=c.get(i));break}}if(u)break}if(u)if(d.nextLocation=s.createHref(n),a.length>0)for(let n=0;n{h(e,u),u=g(e.pathname,n)})),h(s.location,null),e&&document.addEventListener("click",(e=>{let n=e.target;for(;n&&!x(n);)n=n.parentNode;if(n&&function(e,n){const t=x(n),r=e.which>1||e.metaKey||e.ctrlKey||e.shiftKey||e.altKey,i=n.target&&"_blank"===n.target,o=window.location.protocol!==n.protocol||window.location.hostname!==n.hostname,a="string"===n.getAttribute("download"),c=t&&t.includes("mailto:");return!(r||i||o||a||c)}(e,n)&&!n.classList.contains(d.clickIgnoreClass)){e.preventDefault(),e.stopPropagation();const t=m(x(n));d.goTo(t)}}))},goTo:function(e){const{pathname:t,search:r,hash:o}=i(m(window.location.href)),a=m(e),{pathname:c,search:f,hash:u}=i(a);t===c&&r===f&&o===u||s.push(g(a,n))},history:s,getPath:e=>g(m(e),n)};return d}function A({wrapperQuery:e=(()=>document.querySelector(".lemonade-wrapper")),containerQuery:n=(e=>e.querySelector(".lemonade-container")),cacheEnabled:t=!0,defaultTransition:r={play:async function(e,n,{loadView:t,appendView:r,removeView:i}){e&&(await e.leave(n),await t(),r(),i()),await n.enter(e)}},transitionParams:i={},basename:o=""}={}){let a,c,s,f=E({defaultTransition:r,basename:o,transitionParams:{...i,loadView:async function(){let e,{nextLocation:r}=f,i=f.getPath(r);t&&l.cache.get(i)?e=u.get(i):(e=await A.fetch(r),t&&u.set(i,e));const o=document.createElement("html");o.innerHTML=e;const h=o.querySelector("title");h&&(document.title=h.textContent);return c=n(a),s=n(o),{prevContainer:c,nextContainer:s,temp:o}},appendView:function(){a.appendChild(s)},removeView:function(){a.removeChild(c)},wrapperQuery:e,containerQuery:n}}),u=new Map;const l={listen:function({clickEvents:r=!1,clickIgnoreClass:i="no-router"}={}){a=e(),c=n(a),f.listen({clickEvents:r,clickIgnoreClass:i});let o=f.getPath(window.location.href);t&&!u.has(o)&&u.set(o,document.documentElement.innerHTML)},match:f.match,view:f.view,transition:f.transition,goTo:f.goTo,getPath:f.getPath,cache:u};return l}A.fetch=async e=>{let n=await fetch(e);return await n.text()},e.RouterDOM=A,Object.defineProperty(e,"__esModule",{value:!0})})); 2 | -------------------------------------------------------------------------------- /lib/lemonade-router.umd.js: -------------------------------------------------------------------------------- 1 | !function(e,n){"object"==typeof exports&&"undefined"!=typeof module?n(exports):"function"==typeof define&&define.amd?define(["exports"],n):n((e=e||self).Lemonade={})}(this,(function(e){"use strict";function n(){return(n=Object.assign||function(e){for(var n=1;n=0;h--){var d=a[h];"."===d?r(a,h):".."===d?(r(a,h),l++):l&&(r(a,h),l--)}if(!c)for(;l--;l)a.unshift("..");!c||""===a[0]||a[0]&&t(a[0])||a.unshift("");var p=a.join("/");return i&&"/"!==p.substr(-1)&&(p+="/"),p}(f.pathname,s.pathname)):f.pathname=s.pathname:f.pathname||(f.pathname="/"),f}var a=!("undefined"==typeof window||!window.document||!window.document.createElement);function s(e,n){n(window.confirm(e))}var f="popstate",c="hashchange";function u(){try{return window.history.state||{}}catch(e){return{}}}function l(e){void 0===e&&(e={}),a||function(e,n){if(!e)throw new Error("Invariant failed")}(!1);var t,r,i=window.history,l=(-1===(t=window.navigator.userAgent).indexOf("Android 2.")&&-1===t.indexOf("Android 4.0")||-1===t.indexOf("Mobile Safari")||-1!==t.indexOf("Chrome")||-1!==t.indexOf("Windows Phone"))&&window.history&&"pushState"in window.history,h=!(-1===window.navigator.userAgent.indexOf("Trident")),d=e,p=d.forceRefresh,v=void 0!==p&&p,w=d.getUserConfirmation,g=void 0===w?s:w,y=d.keyLength,m=void 0===y?6:y,x=e.basename?function(e){return"/"===e.charAt(e.length-1)?e.slice(0,-1):e}("/"===(r=e.basename).charAt(0)?r:"/"+r):"";function A(e){var n=e||{},t=n.key,r=n.state,i=window.location,a=i.pathname+i.search+i.hash;return x&&(a=function(e,n){return function(e,n){return 0===e.toLowerCase().indexOf(n.toLowerCase())&&-1!=="/?#".indexOf(e.charAt(n.length))}(e,n)?e.substr(n.length):e}(a,x)),o(a,r,t)}function E(){return Math.random().toString(36).substr(2,m)}var O,b,k=(O=null,b=[],{setPrompt:function(e){return O=e,function(){O===e&&(O=null)}},confirmTransitionTo:function(e,n,t,r){if(null!=O){var i="function"==typeof O?O(e,n):O;"string"==typeof i?"function"==typeof t?t(i,r):r(!0):r(!1!==i)}else r(!0)},appendListener:function(e){var n=!0;function t(){n&&e.apply(void 0,arguments)}return b.push(t),function(){n=!1,b=b.filter((function(e){return e!==t}))}},notifyListeners:function(){for(var e=arguments.length,n=new Array(e),t=0;t=48&&f<=57||f>=65&&f<=90||f>=97&&f<=122||95===f))break;a+=e[s++]}if(!a)throw new TypeError("Missing parameter name at "+t);n.push({type:"NAME",index:t,value:a}),t=s}else n.push({type:"CLOSE",index:t,value:e[t++]});else n.push({type:"OPEN",index:t,value:e[t++]});else n.push({type:"ESCAPED_CHAR",index:t++,value:e[t++]});else n.push({type:"MODIFIER",index:t,value:e[t++]})}return n.push({type:"END",index:t,value:""}),n}(e),r=n.prefixes,i=void 0===r?"./":r,o="[^"+d(n.delimiter||"/#?")+"]+?",a=[],s=0,f=0,c="",u=function(e){if(f-1:void 0===E;i||(v+="(?:"+h+"(?="+l+"))?"),O||(v+="(?="+h+"|"+l+")")}return new RegExp(v,p(t))}(h(e,t),n,t)}function w(e,n,t){return e instanceof RegExp?function(e,n){if(!n)return e;var t=e.source.match(/\((?!\?)/g);if(t)for(var r=0;r0){const[e,...o]=a,f=r.reduce(((e,n,t)=>(e[n.name]=o[t],e)),{}),c=[...n,i].filter((e=>!e.includes(":")));await t({urls:c,url:i,params:f}),s.has(i)&&(u=s.get(i));break}}if(u)break}if(u)if(d.nextLocation=f.createHref(n),a.length>0)for(let n=0;n{h(e,u),u=y(e.pathname,n)})),h(f.location,null),e&&document.addEventListener("click",(e=>{let n=e.target;for(;n&&!x(n);)n=n.parentNode;if(n&&function(e,n){const t=x(n),r=e.which>1||e.metaKey||e.ctrlKey||e.shiftKey||e.altKey,i=n.target&&"_blank"===n.target,o=window.location.protocol!==n.protocol||window.location.hostname!==n.hostname,a="string"===n.getAttribute("download"),s=t&&t.includes("mailto:");return!(r||i||o||a||s)}(e,n)&&!n.classList.contains(d.clickIgnoreClass)){e.preventDefault(),e.stopPropagation();const t=g(x(n));d.goTo(t)}}))},goTo:function(e){const{pathname:t,search:r,hash:o}=i(g(window.location.href)),a=g(e),{pathname:s,search:c,hash:u}=i(a);t===s&&r===c&&o===u||f.push(y(a,n))},history:f,getPath:e=>y(g(e),n)};return d},Object.defineProperty(e,"__esModule",{value:!0})})); 2 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lemonade-router", 3 | "version": "0.2.1", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@babel/code-frame": { 8 | "version": "7.10.4", 9 | "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", 10 | "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", 11 | "dev": true, 12 | "requires": { 13 | "@babel/highlight": "^7.10.4" 14 | } 15 | }, 16 | "@babel/helper-validator-identifier": { 17 | "version": "7.10.4", 18 | "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", 19 | "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", 20 | "dev": true 21 | }, 22 | "@babel/highlight": { 23 | "version": "7.10.4", 24 | "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz", 25 | "integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==", 26 | "dev": true, 27 | "requires": { 28 | "@babel/helper-validator-identifier": "^7.10.4", 29 | "chalk": "^2.0.0", 30 | "js-tokens": "^4.0.0" 31 | } 32 | }, 33 | "@babel/runtime": { 34 | "version": "7.8.4", 35 | "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.8.4.tgz", 36 | "integrity": "sha512-neAp3zt80trRVBI1x0azq6c57aNBqYZH8KhMm3TaB7wEI5Q4A2SHfBHE8w9gOhI/lrqxtEbXZgQIrHP+wvSGwQ==", 37 | "requires": { 38 | "regenerator-runtime": "^0.13.2" 39 | } 40 | }, 41 | "@rollup/plugin-commonjs": { 42 | "version": "11.0.2", 43 | "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-11.0.2.tgz", 44 | "integrity": "sha512-MPYGZr0qdbV5zZj8/2AuomVpnRVXRU5XKXb3HVniwRoRCreGlf5kOE081isNWeiLIi6IYkwTX9zE0/c7V8g81g==", 45 | "dev": true, 46 | "requires": { 47 | "@rollup/pluginutils": "^3.0.0", 48 | "estree-walker": "^1.0.1", 49 | "is-reference": "^1.1.2", 50 | "magic-string": "^0.25.2", 51 | "resolve": "^1.11.0" 52 | }, 53 | "dependencies": { 54 | "estree-walker": { 55 | "version": "1.0.1", 56 | "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-1.0.1.tgz", 57 | "integrity": "sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==", 58 | "dev": true 59 | } 60 | } 61 | }, 62 | "@rollup/plugin-node-resolve": { 63 | "version": "7.1.1", 64 | "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-7.1.1.tgz", 65 | "integrity": "sha512-14ddhD7TnemeHE97a4rLOhobfYvUVcaYuqTnL8Ti7Jxi9V9Jr5LY7Gko4HZ5k4h4vqQM0gBQt6tsp9xXW94WPA==", 66 | "dev": true, 67 | "requires": { 68 | "@rollup/pluginutils": "^3.0.6", 69 | "@types/resolve": "0.0.8", 70 | "builtin-modules": "^3.1.0", 71 | "is-module": "^1.0.0", 72 | "resolve": "^1.14.2" 73 | } 74 | }, 75 | "@rollup/plugin-replace": { 76 | "version": "2.3.1", 77 | "resolved": "https://registry.npmjs.org/@rollup/plugin-replace/-/plugin-replace-2.3.1.tgz", 78 | "integrity": "sha512-qDcXj2VOa5+j0iudjb+LiwZHvBRRgWbHPhRmo1qde2KItTjuxDVQO21rp9/jOlzKR5YO0EsgRQoyox7fnL7y/A==", 79 | "dev": true, 80 | "requires": { 81 | "@rollup/pluginutils": "^3.0.4", 82 | "magic-string": "^0.25.5" 83 | } 84 | }, 85 | "@rollup/pluginutils": { 86 | "version": "3.0.8", 87 | "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-3.0.8.tgz", 88 | "integrity": "sha512-rYGeAc4sxcZ+kPG/Tw4/fwJODC3IXHYDH4qusdN/b6aLw5LPUbzpecYbEJh4sVQGPFJxd2dBU4kc1H3oy9/bnw==", 89 | "dev": true, 90 | "requires": { 91 | "estree-walker": "^1.0.1" 92 | }, 93 | "dependencies": { 94 | "estree-walker": { 95 | "version": "1.0.1", 96 | "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-1.0.1.tgz", 97 | "integrity": "sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==", 98 | "dev": true 99 | } 100 | } 101 | }, 102 | "@types/estree": { 103 | "version": "0.0.39", 104 | "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz", 105 | "integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==", 106 | "dev": true 107 | }, 108 | "@types/node": { 109 | "version": "13.7.0", 110 | "resolved": "https://registry.npmjs.org/@types/node/-/node-13.7.0.tgz", 111 | "integrity": "sha512-GnZbirvmqZUzMgkFn70c74OQpTTUcCzlhQliTzYjQMqg+hVKcDnxdL19Ne3UdYzdMA/+W3eb646FWn/ZaT1NfQ==", 112 | "dev": true 113 | }, 114 | "@types/resolve": { 115 | "version": "0.0.8", 116 | "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-0.0.8.tgz", 117 | "integrity": "sha512-auApPaJf3NPfe18hSoJkp8EbZzer2ISk7o8mCC3M9he/a04+gbMF97NkpD2S8riMGvm4BMRI59/SZQSaLTKpsQ==", 118 | "dev": true, 119 | "requires": { 120 | "@types/node": "*" 121 | } 122 | }, 123 | "acorn": { 124 | "version": "7.1.1", 125 | "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.1.1.tgz", 126 | "integrity": "sha512-add7dgA5ppRPxCFJoAGfMDi7PIBXq1RtGo7BhbLaxwrXPOmw8gq48Y9ozT01hUKy9byMjlR20EJhu5zlkErEkg==", 127 | "dev": true 128 | }, 129 | "ansi-styles": { 130 | "version": "3.2.1", 131 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", 132 | "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", 133 | "dev": true, 134 | "requires": { 135 | "color-convert": "^1.9.0" 136 | } 137 | }, 138 | "buffer-from": { 139 | "version": "1.1.1", 140 | "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", 141 | "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", 142 | "dev": true 143 | }, 144 | "builtin-modules": { 145 | "version": "3.1.0", 146 | "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.1.0.tgz", 147 | "integrity": "sha512-k0KL0aWZuBt2lrxrcASWDfwOLMnodeQjodT/1SxEQAXsHANgo6ZC/VEaSEHCXt7aSTZ4/4H5LKa+tBXmW7Vtvw==", 148 | "dev": true 149 | }, 150 | "chalk": { 151 | "version": "2.4.2", 152 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", 153 | "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", 154 | "dev": true, 155 | "requires": { 156 | "ansi-styles": "^3.2.1", 157 | "escape-string-regexp": "^1.0.5", 158 | "supports-color": "^5.3.0" 159 | } 160 | }, 161 | "color-convert": { 162 | "version": "1.9.3", 163 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", 164 | "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", 165 | "dev": true, 166 | "requires": { 167 | "color-name": "1.1.3" 168 | } 169 | }, 170 | "color-name": { 171 | "version": "1.1.3", 172 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", 173 | "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", 174 | "dev": true 175 | }, 176 | "commander": { 177 | "version": "2.20.3", 178 | "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", 179 | "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", 180 | "dev": true 181 | }, 182 | "escape-string-regexp": { 183 | "version": "1.0.5", 184 | "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", 185 | "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", 186 | "dev": true 187 | }, 188 | "has-flag": { 189 | "version": "3.0.0", 190 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", 191 | "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", 192 | "dev": true 193 | }, 194 | "history": { 195 | "version": "4.10.1", 196 | "resolved": "https://registry.npmjs.org/history/-/history-4.10.1.tgz", 197 | "integrity": "sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==", 198 | "requires": { 199 | "@babel/runtime": "^7.1.2", 200 | "loose-envify": "^1.2.0", 201 | "resolve-pathname": "^3.0.0", 202 | "tiny-invariant": "^1.0.2", 203 | "tiny-warning": "^1.0.0", 204 | "value-equal": "^1.0.1" 205 | } 206 | }, 207 | "is-module": { 208 | "version": "1.0.0", 209 | "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", 210 | "integrity": "sha1-Mlj7afeMFNW4FdZkM2tM/7ZEFZE=", 211 | "dev": true 212 | }, 213 | "is-reference": { 214 | "version": "1.1.4", 215 | "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.1.4.tgz", 216 | "integrity": "sha512-uJA/CDPO3Tao3GTrxYn6AwkM4nUPJiGGYu5+cB8qbC7WGFlrKZbiRo7SFKxUAEpFUfiHofWCXBUNhvYJMh+6zw==", 217 | "dev": true, 218 | "requires": { 219 | "@types/estree": "0.0.39" 220 | } 221 | }, 222 | "jest-worker": { 223 | "version": "26.5.0", 224 | "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-26.5.0.tgz", 225 | "integrity": "sha512-kTw66Dn4ZX7WpjZ7T/SUDgRhapFRKWmisVAF0Rv4Fu8SLFD7eLbqpLvbxVqYhSgaWa7I+bW7pHnbyfNsH6stug==", 226 | "dev": true, 227 | "requires": { 228 | "@types/node": "*", 229 | "merge-stream": "^2.0.0", 230 | "supports-color": "^7.0.0" 231 | }, 232 | "dependencies": { 233 | "has-flag": { 234 | "version": "4.0.0", 235 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", 236 | "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", 237 | "dev": true 238 | }, 239 | "supports-color": { 240 | "version": "7.2.0", 241 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", 242 | "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", 243 | "dev": true, 244 | "requires": { 245 | "has-flag": "^4.0.0" 246 | } 247 | } 248 | } 249 | }, 250 | "js-tokens": { 251 | "version": "4.0.0", 252 | "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", 253 | "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" 254 | }, 255 | "loose-envify": { 256 | "version": "1.4.0", 257 | "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", 258 | "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", 259 | "requires": { 260 | "js-tokens": "^3.0.0 || ^4.0.0" 261 | } 262 | }, 263 | "magic-string": { 264 | "version": "0.25.6", 265 | "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.6.tgz", 266 | "integrity": "sha512-3a5LOMSGoCTH5rbqobC2HuDNRtE2glHZ8J7pK+QZYppyWA36yuNpsX994rIY2nCuyP7CZYy7lQq/X2jygiZ89g==", 267 | "dev": true, 268 | "requires": { 269 | "sourcemap-codec": "^1.4.4" 270 | } 271 | }, 272 | "merge-stream": { 273 | "version": "2.0.0", 274 | "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", 275 | "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", 276 | "dev": true 277 | }, 278 | "path-parse": { 279 | "version": "1.0.7", 280 | "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", 281 | "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", 282 | "dev": true 283 | }, 284 | "path-to-regexp": { 285 | "version": "6.1.0", 286 | "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.1.0.tgz", 287 | "integrity": "sha512-h9DqehX3zZZDCEm+xbfU0ZmwCGFCAAraPJWMXJ4+v32NjZJilVg3k1TcKsRgIb8IQ/izZSaydDc1OhJCZvs2Dw==" 288 | }, 289 | "randombytes": { 290 | "version": "2.1.0", 291 | "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", 292 | "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", 293 | "dev": true, 294 | "requires": { 295 | "safe-buffer": "^5.1.0" 296 | } 297 | }, 298 | "regenerator-runtime": { 299 | "version": "0.13.3", 300 | "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.3.tgz", 301 | "integrity": "sha512-naKIZz2GQ8JWh///G7L3X6LaQUAMp2lvb1rvwwsURe/VXwD6VMfr+/1NuNw3ag8v2kY1aQ/go5SNn79O9JU7yw==" 302 | }, 303 | "resolve": { 304 | "version": "1.15.1", 305 | "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.15.1.tgz", 306 | "integrity": "sha512-84oo6ZTtoTUpjgNEr5SJyzQhzL72gaRodsSfyxC/AXRvwu0Yse9H8eF9IpGo7b8YetZhlI6v7ZQ6bKBFV/6S7w==", 307 | "dev": true, 308 | "requires": { 309 | "path-parse": "^1.0.6" 310 | } 311 | }, 312 | "resolve-pathname": { 313 | "version": "3.0.0", 314 | "resolved": "https://registry.npmjs.org/resolve-pathname/-/resolve-pathname-3.0.0.tgz", 315 | "integrity": "sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng==" 316 | }, 317 | "rollup": { 318 | "version": "1.31.0", 319 | "resolved": "https://registry.npmjs.org/rollup/-/rollup-1.31.0.tgz", 320 | "integrity": "sha512-9C6ovSyNeEwvuRuUUmsTpJcXac1AwSL1a3x+O5lpmQKZqi5mmrjauLeqIjvREC+yNRR8fPdzByojDng+af3nVw==", 321 | "dev": true, 322 | "requires": { 323 | "@types/estree": "*", 324 | "@types/node": "*", 325 | "acorn": "^7.1.0" 326 | } 327 | }, 328 | "rollup-plugin-terser": { 329 | "version": "7.0.2", 330 | "resolved": "https://registry.npmjs.org/rollup-plugin-terser/-/rollup-plugin-terser-7.0.2.tgz", 331 | "integrity": "sha512-w3iIaU4OxcF52UUXiZNsNeuXIMDvFrr+ZXK6bFZ0Q60qyVfq4uLptoS4bbq3paG3x216eQllFZX7zt6TIImguQ==", 332 | "dev": true, 333 | "requires": { 334 | "@babel/code-frame": "^7.10.4", 335 | "jest-worker": "^26.2.1", 336 | "serialize-javascript": "^4.0.0", 337 | "terser": "^5.0.0" 338 | } 339 | }, 340 | "safe-buffer": { 341 | "version": "5.2.1", 342 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", 343 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", 344 | "dev": true 345 | }, 346 | "serialize-javascript": { 347 | "version": "4.0.0", 348 | "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-4.0.0.tgz", 349 | "integrity": "sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==", 350 | "dev": true, 351 | "requires": { 352 | "randombytes": "^2.1.0" 353 | } 354 | }, 355 | "source-map": { 356 | "version": "0.7.3", 357 | "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", 358 | "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==", 359 | "dev": true 360 | }, 361 | "source-map-support": { 362 | "version": "0.5.19", 363 | "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz", 364 | "integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==", 365 | "dev": true, 366 | "requires": { 367 | "buffer-from": "^1.0.0", 368 | "source-map": "^0.6.0" 369 | }, 370 | "dependencies": { 371 | "source-map": { 372 | "version": "0.6.1", 373 | "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", 374 | "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", 375 | "dev": true 376 | } 377 | } 378 | }, 379 | "sourcemap-codec": { 380 | "version": "1.4.8", 381 | "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", 382 | "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==", 383 | "dev": true 384 | }, 385 | "supports-color": { 386 | "version": "5.5.0", 387 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", 388 | "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", 389 | "dev": true, 390 | "requires": { 391 | "has-flag": "^3.0.0" 392 | } 393 | }, 394 | "terser": { 395 | "version": "5.3.5", 396 | "resolved": "https://registry.npmjs.org/terser/-/terser-5.3.5.tgz", 397 | "integrity": "sha512-Qw3CZAMmmfU824AoGKalx+riwocSI5Cs0PoGp9RdSLfmxkmJgyBxqLBP/isDNtFyhHnitikvRMZzyVgeq+U+Tg==", 398 | "dev": true, 399 | "requires": { 400 | "commander": "^2.20.0", 401 | "source-map": "~0.7.2", 402 | "source-map-support": "~0.5.19" 403 | } 404 | }, 405 | "tiny-invariant": { 406 | "version": "1.1.0", 407 | "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.1.0.tgz", 408 | "integrity": "sha512-ytxQvrb1cPc9WBEI/HSeYYoGD0kWnGEOR8RY6KomWLBVhqz0RgTwVO9dLrGz7dC+nN9llyI7OKAgRq8Vq4ZBSw==" 409 | }, 410 | "tiny-warning": { 411 | "version": "1.0.3", 412 | "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz", 413 | "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==" 414 | }, 415 | "value-equal": { 416 | "version": "1.0.1", 417 | "resolved": "https://registry.npmjs.org/value-equal/-/value-equal-1.0.1.tgz", 418 | "integrity": "sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw==" 419 | } 420 | } 421 | } 422 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lemonade-router", 3 | "version": "0.2.1", 4 | "description": "Minimal routing library", 5 | "main": "index.js", 6 | "scripts": { 7 | "dev": "NODE_ENV='development' rollup -c -w", 8 | "build": "NODE_ENV='production' rollup -c" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/raphaelameaume/lemonade-router.git" 13 | }, 14 | "keywords": [ 15 | "routing", 16 | "client-side", 17 | "fetch" 18 | ], 19 | "author": "Raphaël Améaume", 20 | "license": "MIT", 21 | "bugs": { 22 | "url": "https://github.com/raphaelameaume/lemonade-router/issues" 23 | }, 24 | "homepage": "https://github.com/raphaelameaume/lemonade-router#readme", 25 | "dependencies": { 26 | "history": "4.10.1", 27 | "path-to-regexp": "6.1.0" 28 | }, 29 | "devDependencies": { 30 | "@rollup/plugin-commonjs": "^11.0.2", 31 | "@rollup/plugin-node-resolve": "^7.1.1", 32 | "@rollup/plugin-replace": "^2.3.1", 33 | "rollup": "^1.31.0", 34 | "rollup-plugin-terser": "^7.0.2" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import resolve from '@rollup/plugin-node-resolve'; 2 | import commonjs from '@rollup/plugin-commonjs'; 3 | import replace from '@rollup/plugin-replace'; 4 | import { terser } from 'rollup-plugin-terser'; 5 | 6 | const production = process.env.NODE_ENV === 'production'; 7 | 8 | const plugins = [ 9 | resolve(), 10 | commonjs(), 11 | replace({ 12 | 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV), 13 | }), 14 | production && terser() 15 | ]; 16 | 17 | export default [ 18 | // browser-friendly UMD build 19 | { 20 | input: 'src/Router.js', 21 | output: { 22 | name: 'Lemonade', 23 | file: 'lib/lemonade-router.umd.js', 24 | format: 'umd' 25 | }, 26 | plugins 27 | }, 28 | { 29 | input: 'src/RouterDOM.js', 30 | output: { 31 | name: 'Lemonade', 32 | file: 'lib/lemonade-router-dom.umd.js', 33 | format: 'umd' 34 | }, 35 | plugins 36 | } 37 | ]; -------------------------------------------------------------------------------- /src/DefaultTransition.js: -------------------------------------------------------------------------------- 1 | export function DefaultTransition() { 2 | function play(prevView, nextView) { 3 | if (prevView) { 4 | prevView.leave(nextView); 5 | } 6 | 7 | nextView.enter(prevView); 8 | } 9 | 10 | return { 11 | play, 12 | } 13 | } -------------------------------------------------------------------------------- /src/DefaultTransitionDOM.js: -------------------------------------------------------------------------------- 1 | export function DefaultTransitionDOM() { 2 | async function play(prevView, nextView, { loadView, appendView, removeView }) { 3 | if (prevView) { 4 | await prevView.leave(nextView); 5 | await loadView(); // must be called before appendView 6 | appendView(); 7 | removeView(); 8 | } 9 | 10 | await nextView.enter(prevView); 11 | } 12 | 13 | return { 14 | play, 15 | }; 16 | }; -------------------------------------------------------------------------------- /src/Router.js: -------------------------------------------------------------------------------- 1 | import { createBrowserHistory, parsePath } from "history"; 2 | import { pathToRegexp } from "path-to-regexp"; 3 | import { getPath, retrieveHref, preventClick, stripBasename, stripTrailingSlash } from "./helpers.js"; 4 | import { DefaultTransition } from "./DefaultTransition.js"; 5 | 6 | function Router({ 7 | defaultTransition = DefaultTransition(), 8 | basename = '', 9 | transitionParams = {}, 10 | } = {}) { 11 | const matches = []; 12 | const transitions = []; 13 | const views = new Map(); 14 | 15 | let history = createBrowserHistory({ basename }); 16 | let prevView = null; 17 | let prevPathname = null; 18 | 19 | /* 20 | * Register URL pattern 21 | * @param {string|array} urls 22 | * @param {function} fn 23 | */ 24 | function match(urls, fn) { 25 | if (!Array.isArray(urls)) { 26 | urls = [urls]; 27 | } 28 | 29 | matches.push({ urls, fn }); 30 | } 31 | 32 | /* 33 | * Register transition from one or multiple URLs to other(s) 34 | * @param {string|array} fromURLs - 35 | * @param {string|array} toURLs - 36 | * @param {function} fn - 37 | * @param {boolean} backAndForth - 38 | */ 39 | function transition(fromURLs, toURLs, fn, backAndForth = true) { 40 | fromURLs = !Array.isArray(fromURLs) ? [fromURLs] : fromURLs; 41 | toURLs = !Array.isArray(toURLs) ? [toURLs] : toURLs; 42 | 43 | transitions.push({ fromURLs, toURLs, fn, backAndForth }); 44 | } 45 | 46 | /* 47 | * Register view for one or multiple URLs 48 | * @param {string|array} urls - 49 | * @param {function} fn - 50 | */ 51 | function view(urls, fn) { 52 | const view = fn(); 53 | 54 | if (!Array.isArray(urls)) { 55 | urls = [urls]; 56 | } 57 | 58 | for (let i = 0; i < urls.length; i++) { 59 | let url = stripTrailingSlash(urls[i]); 60 | views.set(url, view); 61 | } 62 | } 63 | 64 | function goTo(href) { 65 | const { pathname, search, hash } = parsePath(getPath(window.location.href)); 66 | 67 | const nextPath = getPath(href); 68 | const { pathname: nextPathname, search: nextSearch, hash: nextHash } = parsePath(nextPath); 69 | 70 | if (pathname === nextPathname && search === nextSearch && hash === nextHash) return; 71 | 72 | history.push(stripBasename(nextPath, basename)); 73 | } 74 | 75 | async function apply(location, prevPathname) { 76 | try { 77 | const pathname = stripTrailingSlash(location.pathname); 78 | 79 | let nextView; 80 | 81 | if (views.has(pathname)) { 82 | nextView = views.get(pathname); 83 | } else { 84 | for (let i = 0; i < matches.length; i++) { 85 | const { urls, fn } = matches[i]; 86 | 87 | for (let j = 0; j < urls.length; j++) { 88 | const keys = []; 89 | const regex = urls[j] === '*' ? /^\/.*(?:\/)?$/i : pathToRegexp(urls[j], keys); 90 | const match = pathname !== "" ? regex.exec(pathname) : regex.exec('/'); 91 | 92 | if (match && match.length > 0) { 93 | const [url, ...values] = match; 94 | const params = keys.reduce((acc, key, index) => { 95 | acc[key.name] = values[index]; 96 | return acc; 97 | }, {}); 98 | 99 | // remove urls with params 100 | const all = [...urls, pathname].filter(u => !u.includes(':')); 101 | 102 | await fn({ urls: all, url: pathname, params }); 103 | 104 | if (views.has(pathname)) { 105 | nextView = views.get(pathname); 106 | } 107 | 108 | break; 109 | } 110 | } 111 | 112 | if (nextView) break; 113 | } 114 | } 115 | 116 | if (nextView) { 117 | router.nextLocation = history.createHref(location); 118 | 119 | if (transitions.length > 0) { 120 | for (let i = 0; i < transitions.length; i++) { 121 | const { fromURLs, toURLs, backAndForth, fn } = transitions[i]; 122 | 123 | const matchFrom = fromURLs.includes('*') || fromURLs.includes(prevPathname); 124 | const reverseMatchFrom = backAndForth && fromURLs.includes(pathname); 125 | const matchTo = toURLs.includes('*') || toURLs.includes(pathname); 126 | const reverseMatchTo = backAndForth && toURLs.includes(prevPathname); 127 | 128 | if ((matchFrom && matchTo) || (reverseMatchFrom && reverseMatchTo)) { 129 | const transition = await fn(); 130 | 131 | let currentPrevView = prevView; 132 | let currentNextView = nextView; 133 | prevView = nextView; 134 | 135 | await transition.play(currentPrevView, currentNextView, transitionParams); 136 | } else { 137 | let currentPrevView = prevView; 138 | let currentNextView = nextView; 139 | prevView = nextView; 140 | 141 | await defaultTransition.play(currentPrevView, currentNextView, transitionParams); 142 | } 143 | } 144 | } else { 145 | let currentPrevView = prevView; 146 | let currentNextView = nextView; 147 | prevView = nextView; 148 | 149 | await defaultTransition.play(currentPrevView, currentNextView, transitionParams); 150 | } 151 | } else { 152 | console.error('Router :: View not found', views); 153 | } 154 | } catch (error) { 155 | console.error(error); 156 | } 157 | } 158 | 159 | /* 160 | * Start listening to URL changes 161 | * @param {object} options - 162 | * @param {boolean} options.clickEvents - 163 | * @param {string} options.clickIgnoreClass - 164 | */ 165 | function listen({ clickEvents = false, clickIgnoreClass = 'no-router' } = {}) { 166 | router.clickIgnoreClass = clickIgnoreClass; 167 | 168 | prevPathname = stripBasename(parsePath(getPath(window.location.href)).pathname, basename); 169 | 170 | history.listen((location) => { 171 | apply(location, prevPathname); 172 | 173 | prevPathname = stripBasename(location.pathname, basename); 174 | }); 175 | 176 | apply(history.location, null); 177 | 178 | if (clickEvents) { 179 | document.addEventListener('click', (event) => { 180 | let target = event.target; 181 | 182 | while (target && !retrieveHref(target)) { 183 | target = target.parentNode; 184 | } 185 | 186 | if (target && preventClick(event, target) && !target.classList.contains(router.clickIgnoreClass)) { 187 | event.preventDefault(); 188 | event.stopPropagation(); 189 | 190 | const href = retrieveHref(target); 191 | const url = getPath(href); 192 | router.goTo(url); 193 | } 194 | }); 195 | } 196 | } 197 | 198 | const router = { 199 | nextLocation: null, 200 | clickIgnoreClass: 'no-router', 201 | match, 202 | transition, 203 | view, 204 | listen, 205 | goTo, 206 | history, 207 | getPath: (url) => stripBasename(getPath(url), basename), 208 | }; 209 | 210 | return router; 211 | } 212 | 213 | export { Router }; -------------------------------------------------------------------------------- /src/RouterDOM.js: -------------------------------------------------------------------------------- 1 | import { Router } from "./Router.js"; 2 | import { DefaultTransitionDOM } from "./DefaultTransitionDOM.js"; 3 | 4 | function RouterDOM({ 5 | wrapperQuery = () => document.querySelector('.lemonade-wrapper'), 6 | containerQuery = ($wrapper) => $wrapper.querySelector('.lemonade-container'), 7 | cacheEnabled = true, 8 | defaultTransition = DefaultTransitionDOM(), 9 | transitionParams = {}, 10 | basename = '' 11 | } = {}) { 12 | let router = Router({ 13 | defaultTransition, 14 | basename, 15 | transitionParams: { 16 | ...transitionParams, 17 | loadView, 18 | appendView, 19 | removeView, 20 | wrapperQuery, 21 | containerQuery, 22 | } 23 | }); 24 | let cache = new Map(); 25 | let $wrapper, $prevContainer, $nextContainer; 26 | 27 | function listen({ clickEvents = false, clickIgnoreClass = 'no-router' } = {}) { 28 | $wrapper = wrapperQuery(); 29 | $prevContainer = containerQuery($wrapper); 30 | 31 | router.listen({ clickEvents, clickIgnoreClass }); 32 | 33 | let path = router.getPath(window.location.href); 34 | if (cacheEnabled && !cache.has(path)) { 35 | cache.set(path, document.documentElement.innerHTML); 36 | } 37 | } 38 | 39 | async function loadView() { 40 | let html; 41 | let { nextLocation } = router; 42 | let nextPath = router.getPath(nextLocation); 43 | 44 | if (cacheEnabled && routerDOM.cache.get(nextPath)) { 45 | html = cache.get(nextPath); 46 | } else { 47 | html = await RouterDOM.fetch(nextLocation); 48 | 49 | if (cacheEnabled) { 50 | cache.set(nextPath, html); 51 | } 52 | } 53 | 54 | const temp = document.createElement('html'); 55 | temp.innerHTML = html; 56 | 57 | const title = temp.querySelector('title'); 58 | 59 | if (title) { 60 | document.title = title.textContent; 61 | } 62 | 63 | $prevContainer = containerQuery($wrapper); 64 | $nextContainer = containerQuery(temp); 65 | 66 | return { 67 | prevContainer: $prevContainer, 68 | nextContainer: $nextContainer, 69 | temp, 70 | }; 71 | } 72 | 73 | function appendView() { 74 | $wrapper.appendChild($nextContainer); 75 | } 76 | 77 | function removeView() { 78 | $wrapper.removeChild($prevContainer); 79 | } 80 | 81 | const routerDOM = { 82 | listen: listen, 83 | history: router.history, 84 | match: router.match, 85 | view: router.view, 86 | transition: router.transition, 87 | goTo: router.goTo, 88 | getPath: router.getPath, 89 | cache, 90 | }; 91 | 92 | return routerDOM; 93 | } 94 | 95 | RouterDOM.fetch = async (url) => { 96 | let response = await fetch(url); 97 | let html = await response.text(); 98 | 99 | return html; 100 | }; 101 | 102 | export { RouterDOM }; -------------------------------------------------------------------------------- /src/helpers.js: -------------------------------------------------------------------------------- 1 | export function getPath(url) { 2 | return parse(url).path; 3 | } 4 | 5 | export function parse(url) { 6 | let path = url.replace(window.location.origin, ''); 7 | 8 | return { path }; 9 | } 10 | 11 | /* 12 | * https://github.com/ReactTraining/history/blob/3f69f9e07b0a739419704cffc3b3563133281548/modules/PathUtils.js 13 | */ 14 | export function hasBasename(path, prefix) { 15 | return new RegExp('^' + prefix + '(\\/|\\?|#|$)', 'i').test(path); 16 | } 17 | 18 | export function stripBasename(path, prefix) { 19 | return hasBasename(path, prefix) ? path.substr(prefix.length) : path; 20 | } 21 | 22 | export function stripTrailingSlash(path) { 23 | return path.charAt(path.length - 1) === '/' ? path.slice(0, -1) : path; 24 | } 25 | 26 | /* 27 | * https://github.com/barbajs/barba/blob/1.x/src/Pjax/Pjax.js#L179 28 | */ 29 | export function retrieveHref(element) { 30 | if (element) { 31 | const xlink = element.getAttribute && element.getAttribute('xlink:href'); 32 | 33 | if (typeof xlink === 'string') { 34 | return xlink; 35 | } 36 | 37 | if (element.href) { 38 | return element.href; 39 | } 40 | } 41 | 42 | return false; 43 | } 44 | 45 | /* 46 | * https://github.com/barbajs/barba/blob/1.x/src/Pjax/Pjax.js#L239 47 | */ 48 | export function preventClick(event, element) { 49 | const href = retrieveHref(element); 50 | const withKey = event.which > 1 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey; 51 | const blankTarget = element.target && element.target === '_blank'; 52 | const differentDomain = window.location.protocol !== element.protocol || window.location.hostname !== element.hostname; 53 | const isDownload = element.getAttribute('download') === 'string'; 54 | const isMailto = href && href.includes('mailto:'); 55 | const shouldPrevent = !withKey && !blankTarget && !differentDomain && !isDownload && !isMailto; 56 | 57 | return shouldPrevent; 58 | } --------------------------------------------------------------------------------