├── .babelrc
├── .eleventy.js
├── .github
├── ISSUE_TEMPLATE
│ └── bug_report.md
└── workflows
│ └── build.yml
├── .gitignore
├── .nojekyll
├── .npmignore
├── LICENSE.md
├── README.md
├── dist
├── taxi.esm.js
├── taxi.esm.js.map
├── taxi.js
├── taxi.js.map
├── taxi.modern.js
├── taxi.modern.js.map
├── taxi.umd.js
└── taxi.umd.js.map
├── docs
├── _data
│ ├── global.js
│ └── nav.json
├── _includes
│ ├── layouts
│ │ └── base.njk
│ └── nav.njk
├── _public
│ ├── .nojekyll
│ ├── CNAME
│ ├── android-chrome-192x192.png
│ ├── android-chrome-384x384.png
│ ├── apple-touch-icon.png
│ ├── browserconfig.xml
│ ├── favicon-16x16.png
│ ├── favicon-32x32.png
│ ├── favicon.ico
│ ├── fonts
│ │ ├── Inter-Bold.woff
│ │ ├── Inter-Bold.woff2
│ │ ├── Inter-Regular.woff
│ │ ├── Inter-Regular.woff2
│ │ ├── NeueMontreal-Regular.woff
│ │ └── NeueMontreal-Regular.woff2
│ ├── mstile-150x150.png
│ ├── site.webmanifest
│ └── social.jpg
├── api-events.md
├── assets
│ ├── js
│ │ ├── index.js
│ │ ├── renderers
│ │ │ └── DefaultRenderer.js
│ │ └── transitions
│ │ │ └── DefaultTransition.js
│ └── sass
│ │ ├── index.scss
│ │ └── prism.scss
├── how-to-use.md
├── index.njk
├── mix-manifest.json
├── navigation-lifecycle.md
├── reloading-css.md
├── reloading-js.md
├── renderers.md
├── routing.md
├── tailwind.config.js
└── transitions.md
├── package-lock.json
├── package.json
├── src
├── Core.d.ts
├── Core.js
├── Renderer.d.ts
├── Renderer.js
├── RouteStore.d.ts
├── RouteStore.js
├── Transition.d.ts
├── Transition.js
├── helpers.d.ts
├── helpers.js
├── taxi.d.ts
└── taxi.js
├── tsconfig.json
└── webpack.mix.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "plugins": [
3 | ["prismjs", {
4 | "languages": ["javascript", "html"],
5 | "plugins": ["toolbar", "copy-to-clipboard"],
6 | "css": false
7 | }]
8 | ]
9 | }
--------------------------------------------------------------------------------
/.eleventy.js:
--------------------------------------------------------------------------------
1 | const htmlmin = require("html-minifier")
2 | const markdownIt = require("markdown-it")
3 | const anchors = require('markdown-it-anchor')
4 | const date = Date.now()
5 |
6 | function getFromManifest(name) {
7 | try {
8 | const assets = require("./_site/mix-manifest.json")
9 |
10 | if (name[0] !== '/') {
11 | name = '/' + name
12 | }
13 |
14 | return assets[name] || name + '?id=' + date
15 | } catch (error) {
16 | return name + '?id=' + date
17 | }
18 | }
19 |
20 | module.exports = function (eleventyConfig) {
21 | eleventyConfig.addPassthroughCopy({ "docs/_public": '/' })
22 |
23 | /**
24 | * Add the asset shortcode to fetch an asset name from mix-manifest.json
25 | *
26 | * @example {% asset 'css/style.css' %}
27 | */
28 | eleventyConfig.addShortcode("asset", function (name) {
29 | return getFromManifest(name)
30 | });
31 |
32 | eleventyConfig.addTransform("htmlmin", function(content, outputPath) {
33 | if (outputPath && outputPath.endsWith(".html")) {
34 | return htmlmin.minify(content, {
35 | useShortDoctype: true,
36 | removeComments: true,
37 | minifyJS: true,
38 | collapseWhitespace: true
39 | });
40 | }
41 |
42 | return content;
43 | });
44 |
45 | eleventyConfig.addFilter('stringify', (value) => {
46 | return JSON.stringify(value)
47 | })
48 |
49 |
50 | let markdownLibrary = markdownIt({
51 | html: true
52 | })
53 | .use(anchors, {
54 | tabIndex: false
55 | });
56 |
57 | eleventyConfig.setLibrary("md", markdownLibrary);
58 |
59 | return {
60 | dir: { input: 'docs', data: '_data' },
61 | passthroughFileCopy: true,
62 | templateFormats: ['njk', 'md', 'html'],
63 | htmlTemplateEngine: 'njk'
64 | }
65 | };
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: ''
5 | labels: bug
6 | assignees: jakewhiteley
7 |
8 | ---
9 |
10 | **Describe the bug**
11 | A clear and concise description of what the bug is.
12 |
13 | **To Reproduce**
14 | Steps to reproduce the behavior:
15 |
16 | **Info**
17 | If applicable, add screenshots to help explain your problem.
18 |
19 | - Device: [e.g. Windows desktop, Macbook, iphone]
20 | - OS: [e.g. iOS]
21 | - Browser [e.g. chrome, safari]
22 | - Version [e.g. 22]
23 |
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: Build Eleventy
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 |
8 | jobs:
9 | build:
10 | runs-on: ubuntu-20.04
11 |
12 | strategy:
13 | matrix:
14 | node-version: [16.x]
15 |
16 | steps:
17 | - uses: actions/checkout@v2
18 |
19 | - name: Use Node.js ${{ matrix.node-version }}
20 | uses: actions/setup-node@v1
21 | with:
22 | node-version: ${{ matrix.node-version }}
23 |
24 | - name: Install dependencies & build
25 | run: |
26 | npm ci
27 | npm run docs
28 |
29 | - name: Deploy
30 | uses: peaceiris/actions-gh-pages@v3
31 | with:
32 | publish_dir: ./_site
33 | github_token: ${{ secrets.GITHUB_TOKEN }}
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea/
2 | node_modules/
3 | _site
4 |
--------------------------------------------------------------------------------
/.nojekyll:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/craftedbygc/taxi/6785cd56f1f843401a12b867ce0b01425af4c0bb/.nojekyll
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | .idea/
2 | node_modules/
3 | *.mix.js
4 | tsconfig.json
5 | .gitignore
6 | .eleventy.js
7 | docs
8 | _site
9 | tests
10 | .nojekyll
11 | .github
12 | .babelrc
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | BSD 3-Clause License
2 |
3 | Copyright (c) 2025, Jake Whiteley / Unseen Studio Ltd
4 |
5 | Redistribution and use in source and binary forms, with or without
6 | modification, are permitted provided that the following conditions are met:
7 |
8 | 1. Redistributions of source code must retain the above copyright notice, this
9 | list of conditions and the following disclaimer.
10 |
11 | 2. Redistributions in binary form must reproduce the above copyright notice,
12 | this list of conditions and the following disclaimer in the documentation
13 | and/or other materials provided with the distribution.
14 |
15 | 3. Neither the name of the copyright holder nor the names of its
16 | contributors may be used to endorse or promote products derived from
17 | this software without specific prior written permission.
18 |
19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
23 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
27 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Taxi.js is the spiritual successor to Highway.js.
6 |
7 | Full Documentation
8 |
9 |
10 | npm i @unseenco/taxi
or yarn add @unseenco/taxi
11 |
12 |
13 |
14 |
15 | ----
16 |
17 | Taxi is a js library for adding AJAX navigation and beautiful transitions to your website.
18 |
19 | It was designed as a drop-in replacement for [Highway.js](https://github.com/Dogstudio/highway) which is sadly no longer maintained.
20 |
21 | ### Enhancements over Highway:
22 |
23 | * URL-based routing
24 | * Better cache management
25 | * Ability to preload URLs
26 | * Blocks navigation during an active transition (can be opted out)
27 | * Auto runs javascript on the new page
28 | * Previous page's content is automatically removed (you can opt out of this if you like)
29 | * Click events on links can be intercepted via `stopPropagation` without hacks
30 |
31 |
32 | ### Differences to Highway
33 | * Different public API
34 | * New methods and functionality
35 | * `data-taxi`, `data-taxi-view`, `data-taxi-ignore` are to be used instead of `data-router-wrapper`, `data-router-view`, `data-router-disabled` respectively.
36 | * `attach` and `detach` are no longer methods - link clicks are listened to via delegation so these are no longer needed.
37 | * `redirect` is now `navigateTo` as "redirect" felt weird as a method name!
38 | * Renderers now have an `initialLoad` method
39 | * The params passed to renderers, transitions, and events are now a little different
40 | * Old content is automatically removed during a transition - so no need to manually call `from.remove()` in your transitions.
41 |
42 | ----
43 |
44 | Full Documentation
45 |
--------------------------------------------------------------------------------
/dist/taxi.esm.js:
--------------------------------------------------------------------------------
1 | import t from"@unseenco/e";function e(){return e=Object.assign||function(t){for(var e=1;et.length)&&(e=t.length);for(var r=0,n=new Array(e);r=t.length?{done:!0}:{done:!1,value:t[i++]}}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var i=new DOMParser;function o(t){var e=new URL(t,window.location.origin),r=e.hash.length?t.replace(e.hash,""):null;return{hasHash:e.hash.length>0,pathname:e.pathname.replace(/\/+$/,""),host:e.host,search:e.search,raw:t,href:r||e.href}}function a(t,e){t.parentNode.replaceChild(s(t,e),t)}function c(t,e){("HEAD"===t.parentNode.tagName?document.head:document.body).appendChild(s(t,e))}function s(t,e){for(var r=document.createElement(e),n=0;ne.length)&&(t=e.length);for(var r=0,n=new Array(t);r=e.length?{done:!0}:{done:!1,value:e[i++]}}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var o=new DOMParser;function a(e){var t=new URL(e,window.location.origin),r=t.hash.length?e.replace(t.hash,""):null;return{hasHash:t.hash.length>0,pathname:t.pathname.replace(/\/+$/,""),host:t.host,search:t.search,raw:e,href:r||t.href}}function c(e,t){e.parentNode.replaceChild(h(e,t),e)}function s(e,t){("HEAD"===e.parentNode.tagName?document.head:document.body).appendChild(h(e,t))}function h(e,t){for(var r=document.createElement(t),n=0;n 0,\n\t\tpathname: details.pathname.replace(/\\/+$/, ''),\n\t\thost: details.host,\n\t\tsearch: details.search,\n\t\traw: url,\n\t\thref: normalized || details.href\n\t}\n}\n\n/**\n * Reloads a provided script/stylesheet by replacing with itself.\n *\n * @param {HTMLElement|HTMLScriptElement|HTMLStyleElement} node\n * @param {string} elementType - 'SCRIPT' or 'STYLE'\n */\nexport function reloadElement(node, elementType) {\n\tnode.parentNode.replaceChild(duplicateElement(node, elementType), node)\n}\n\n/**\n * Loads a provided script/stylesheet by appending a clone to the current document.\n *\n * @param {HTMLElement|HTMLStyleElement} node\n * @param {string} elementType - 'SCRIPT' or 'STYLE'\n */\nexport function appendElement(node, elementType) {\n\tconst target = node.parentNode.tagName === 'HEAD' ? document.head : document.body\n\ttarget.appendChild(duplicateElement(node, elementType))\n}\n\n/**\n * Creates a clone of a given HTMLElement or HTMLStyleElement\n *\n * @param {HTMLElement|HTMLStyleElement} node\n * @param {string} elementType - 'SCRIPT' or 'STYLE'\n * @return {HTMLElement|HTMLStyleElement}\n */\nexport function duplicateElement(node, elementType) {\n\tconst replacement = document.createElement(elementType)\n\n\tfor (let k = 0; k < node.attributes.length; k++) {\n\t\tconst attr = node.attributes[k]\n\t\treplacement.setAttribute(attr.nodeName, attr.nodeValue)\n\t}\n\n\t// Inline Script or Style\n\tif (node.innerHTML) {\n\t\treplacement.innerHTML = node.innerHTML\n\t}\n\n\treturn replacement\n}\n","export default class Transition {\n\t/**\n\t * @param {{wrapper: HTMLElement}} props\n\t */\n\tconstructor({ wrapper }) {\n\t\tthis.wrapper = wrapper\n\t}\n\n\t/**\n\t * @param {{ from: HTMLElement|Element, trigger: string|HTMLElement|false }} props\n\t * @return {Promise}\n\t */\n\tleave(props) {\n\t\treturn new Promise((resolve) => {\n\t\t\tthis.onLeave({ ...props, done: resolve })\n\t\t})\n\t}\n\n\t/**\n\t * @param {{ to: HTMLElement|Element, trigger: string|HTMLElement|false }} props\n\t * @return {Promise}\n\t */\n\tenter(props) {\n\t\treturn new Promise((resolve) => {\n\t\t\tthis.onEnter({ ...props, done: resolve })\n\t\t})\n\t}\n\n\t/**\n\t * Handle the transition leaving the previous page.\n\t * @param {{from: HTMLElement|Element, trigger: string|HTMLElement|false, done: function}} props\n\t */\n\tonLeave({ from, trigger, done }) {\n\t\tdone()\n\t}\n\n\t/**\n\t * Handle the transition entering the next page.\n\t * @param {{to: HTMLElement|Element, trigger: string|HTMLElement|false, done: function}} props\n\t */\n\tonEnter({ to, trigger, done }) {\n\t\tdone()\n\t}\n}\n","import Transition from \"./Transition\"\n\nexport default class Renderer {\n\t/**\n\t * @param {{content: HTMLElement|Element, page: Document|Node, title: string, wrapper: Element}} props\n\t */\n\tconstructor({ content, page, title, wrapper }) {\n\t\tthis._contentString = content.outerHTML\n\t\tthis._DOM = null\n\t\tthis.page = page\n\t\tthis.title = title\n\t\tthis.wrapper = wrapper\n\t\tthis.content = this.wrapper.lastElementChild\n\t}\n\n\tonEnter() {\n\n\t}\n\n\tonEnterCompleted() {\n\n\t}\n\n\tonLeave() {\n\n\t}\n\n\tonLeaveCompleted() {\n\n\t}\n\n\tinitialLoad() {\n\t\tthis.onEnter()\n\t\tthis.onEnterCompleted()\n\t}\n\n\tupdate() {\n\t\tdocument.title = this.title\n\t\tthis.wrapper.appendChild(this._DOM.firstElementChild)\n\t\tthis.content = this.wrapper.lastElementChild\n\t\tthis._DOM = null\n\t}\n\n\tcreateDom() {\n\t\tif (!this._DOM) {\n\t\t\tthis._DOM = document.createElement('div')\n\t\t\tthis._DOM.innerHTML = this._contentString\n\t\t}\n\t}\n\n\tremove() {\n\t\tthis.wrapper.firstElementChild.remove()\n\t}\n\n\t/**\n\t * Called when transitioning into the current page.\n\t * @param {Transition} transition\n\t * @param {string|HTMLElement|false} trigger\n\t * @return {Promise}\n\t */\n\tenter(transition, trigger) {\n\t\treturn new Promise((resolve) => {\n\t\t\tthis.onEnter()\n\n\t\t\ttransition.enter({ trigger, to: this.content })\n\t\t\t\t.then(() => {\n\t\t\t\t\tthis.onEnterCompleted()\n\t\t\t\t\tresolve()\n\t\t\t\t})\n\t\t})\n\t}\n\n\t/**\n\t * Called when transitioning away from the current page.\n\t * @param {Transition} transition\n\t * @param {string|HTMLElement|false} trigger\n\t * @param {boolean} removeOldContent\n\t * @return {Promise}\n\t */\n\tleave(transition, trigger, removeOldContent) {\n\t\treturn new Promise((resolve) => {\n\t\t\tthis.onLeave()\n\n\t\t\ttransition.leave({ trigger, from: this.content })\n\t\t\t\t.then(() => {\n\t\t\t\t\tif (removeOldContent) {\n\t\t\t\t\t\tthis.remove()\n\t\t\t\t\t}\n\n\t\t\t\t\tthis.onLeaveCompleted()\n\t\t\t\t\tresolve()\n\t\t\t\t})\n\t\t})\n\t}\n}\n","export default class RouteStore {\n\t/**\n\t * @type {Map>}\n\t */\n\tdata = new Map()\n\n\t/**\n\t * @type {Map}\n\t */\n\tregexCache = new Map()\n\n\t/**\n\t *\n\t * @param {string} fromPattern\n\t * @param {string} toPattern\n\t * @param {string} transition\n\t */\n\tadd(fromPattern, toPattern, transition) {\n\t\tif (!this.data.has(fromPattern)) {\n\t\t\tthis.data.set(fromPattern, new Map())\n\t\t\tthis.regexCache.set(fromPattern, new RegExp(`^${fromPattern}$`))\n\t\t}\n\n\t\tthis.data.get(fromPattern).set(toPattern, transition)\n\t\tthis.regexCache.set(toPattern, new RegExp(`^${toPattern}$`))\n\t}\n\n\t/**\n\t *\n\t * @param {{ raw: string, href: string, hasHash: boolean, pathname: string }} currentUrl\n\t * @param {{ raw: string, href: string, hasHash: boolean, pathname: string }} nextUrl\n\t * @return {string|null}\n\t */\n\tfindMatch(currentUrl, nextUrl) {\n\t\t// Loop through all from patterns\n\t\tfor (const [fromPattern, potentialMatches] of this.data) {\n\t\t\t// If we have a match\n\t\t\tif (currentUrl.pathname.match(this.regexCache.get(fromPattern))) {\n\t\t\t\t// loop through all associated to patterns\n\t\t\t\tfor (const [toPattern, transition] of potentialMatches) {\n\t\t\t\t\t// If we find a match, return it\n\t\t\t\t\tif (nextUrl.pathname.match(this.regexCache.get(toPattern))) {\n\t\t\t\t\t\treturn transition\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\n\t\treturn null\n\t}\n}\n","import E from '@unseenco/e'\nimport { appendElement, parseDom, processUrl, reloadElement } from './helpers'\nimport Transition from './Transition'\nimport Renderer from './Renderer'\nimport RouteStore from './RouteStore'\n\nconst IN_PROGRESS = 'A transition is currently in progress'\n\n/**\n * @typedef CacheEntry\n * @type {object}\n * @property {typeof Renderer|Renderer} renderer\n * @property {Document|Node} page\n * @property {array} scripts\n * @property {HTMLLinkElement[]} styles\n * @property {string} finalUrl\n * @property {boolean} skipCache\n * @property {string} title\n * @property {HTMLElement|Element} content\n */\n\nexport default class Core {\n\tisTransitioning = false\n\n\t/**\n\t * @type {CacheEntry|null}\n\t */\n\tcurrentCacheEntry = null\n\n\t/**\n\t * @type {Map}\n\t */\n\tcache = new Map()\n\n\t/**\n\t * @private\n\t * @type {Map}\n\t */\n\tactivePromises = new Map()\n\n\t/**\n\t * @param {{\n\t * \t\tlinks?: string,\n\t * \t\tremoveOldContent?: boolean,\n\t * \t\tallowInterruption?: boolean,\n\t * \t\tbypassCache?: boolean,\n\t * \t\tenablePrefetch?: boolean,\n\t * \t\trenderers?: Object.,\n\t * \t\ttransitions?: Object.,\n\t * \t\treloadJsFilter?: boolean|function(HTMLElement): boolean,\n\t * \t\treloadCssFilter?: boolean|function(HTMLLinkElement): boolean\n\t * }} parameters\n\t */\n\tconstructor(parameters = {}) {\n\t\tconst {\n\t\t\tlinks = 'a[href]:not([target]):not([href^=\\\\#]):not([data-taxi-ignore])',\n\t\t\tremoveOldContent = true,\n\t\t\tallowInterruption = false,\n\t\t\tbypassCache = false,\n\t\t\tenablePrefetch = true,\n\t\t\trenderers = {\n\t\t\t\tdefault: Renderer\n\t\t\t},\n\t\t\ttransitions = {\n\t\t\t\tdefault: Transition\n\t\t\t},\n\t\t\treloadJsFilter = (element) => element.dataset.taxiReload !== undefined,\n\t\t\treloadCssFilter = (element) => true //element.dataset.taxiReload !== undefined\n\t\t} = parameters\n\n\t\tthis.renderers = renderers\n\t\tthis.transitions = transitions\n\t\tthis.defaultRenderer = this.renderers.default || Renderer\n\t\tthis.defaultTransition = this.transitions.default || Transition\n\t\tthis.wrapper = document.querySelector('[data-taxi]')\n\t\tthis.reloadJsFilter = reloadJsFilter\n\t\tthis.reloadCssFilter = reloadCssFilter\n\t\tthis.removeOldContent = removeOldContent\n\t\tthis.allowInterruption = allowInterruption\n\t\tthis.bypassCache = bypassCache\n\t\tthis.enablePrefetch = enablePrefetch\n\t\tthis.cache = new Map()\n\t\tthis.isPopping = false\n\n\t\t// Add delegated link events\n\t\tthis.attachEvents(links)\n\n\t\tthis.currentLocation = processUrl(window.location.href)\n\n\t\t// as this is the initial page load, prime this page into the cache\n\t\tthis.cache.set(this.currentLocation.href, this.createCacheEntry(document.cloneNode(true), window.location.href))\n\n\t\t// fire the current Renderer enter methods\n\t\tthis.currentCacheEntry = this.cache.get(this.currentLocation.href)\n\t\tthis.currentCacheEntry.renderer.initialLoad()\n\t}\n\n\t/**\n\t * @param {string} renderer\n\t */\n\tsetDefaultRenderer(renderer) {\n\t\tthis.defaultRenderer = this.renderers[renderer]\n\t}\n\n\t/**\n\t * @param {string} transition\n\t */\n\tsetDefaultTransition(transition) {\n\t\tthis.defaultTransition = this.transitions[transition]\n\t}\n\n\t/**\n\t * Registers a route into the RouteStore\n\t *\n\t * @param {string} fromPattern\n\t * @param {string} toPattern\n\t * @param {string} transition\n\t */\n\taddRoute(fromPattern, toPattern, transition) {\n\t\tif (!this.router) {\n\t\t\tthis.router = new RouteStore()\n\t\t}\n\n\t\tthis.router.add(fromPattern, toPattern, transition)\n\t}\n\n\t/**\n\t * Prime the cache for a given URL\n\t *\n\t * @param {string} url\n\t * @param {boolean} [preloadAssets]\n\t * @return {Promise}\n\t */\n\tpreload(url, preloadAssets = false) {\n\t\t// convert relative URLs to absolute\n\t\turl = processUrl(url).href\n\n\t\tif (!this.cache.has(url)) {\n\t\t\treturn this.fetch(url, false)\n\t\t\t\t.then(async (response) => {\n\t\t\t\t\tthis.cache.set(url, this.createCacheEntry(response.html, response.url))\n\n\t\t\t\t\tif (preloadAssets) {\n\t\t\t\t\t\tthis.cache.get(url).renderer.createDom()\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t\t.catch(err => console.warn(err))\n\t\t}\n\n\t\treturn Promise.resolve()\n\t}\n\n\t/**\n\t * Updates the HTML cache for a given URL.\n\t * If no URL is passed, then cache for the current page is updated.\n\t * Useful when adding/removing content via AJAX such as a search page or infinite loader.\n\t *\n\t * @param {string} [url]\n\t */\n\tupdateCache(url) {\n\t\tconst key = processUrl(url || window.location.href).href\n\n\t\tif (this.cache.has(key)) {\n\t\t\tthis.cache.delete(key)\n\t\t}\n\n\t\tthis.cache.set(key, this.createCacheEntry(document.cloneNode(true), key))\n\t}\n\n\t/**\n\t * Clears the cache for a given URL.\n\t * If no URL is passed, then cache for the current page is cleared.\n\t *\n\t * @param {string} [url]\n\t */\n\tclearCache(url) {\n\t\tconst key = processUrl(url || window.location.href).href\n\n\t\tif (this.cache.has(key)) {\n\t\t\tthis.cache.delete(key)\n\t\t}\n\t}\n\n\t/**\n\t * @param {string} url\n\t * @param {string|false} [transition]\n\t * @param {string|false|HTMLElement} [trigger]\n\t * @return {Promise}\n\t */\n\tnavigateTo(url, transition = false, trigger = false) {\n\t\treturn new Promise((resolve, reject) => {\n\t\t\t// Don't allow multiple navigations to occur at once\n\t\t\tif (!this.allowInterruption && this.isTransitioning) {\n\t\t\t\treject(new Error(IN_PROGRESS))\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tthis.isTransitioning = true\n\t\t\tthis.isPopping = true\n\t\t\tthis.targetLocation = processUrl(url)\n\t\t\tthis.popTarget = window.location.href\n\n\t\t\tconst TransitionClass = new (this.chooseTransition(transition))({ wrapper: this.wrapper })\n\n\t\t\tlet navigationPromise\n\n\t\t\tif (this.bypassCache || !this.cache.has(this.targetLocation.href) || this.cache.get(this.targetLocation.href).skipCache) {\n\t\t\t\tconst fetched = this.fetch(this.targetLocation.href)\n\t\t\t\t\t.then((response) => {\n\t\t\t\t\t\tthis.cache.set(this.targetLocation.href, this.createCacheEntry(response.html, response.url))\n\t\t\t\t\t\tthis.cache.get(this.targetLocation.href).renderer.createDom()\n\t\t\t\t\t})\n\t\t\t\t\t.catch(err => {\n\t\t\t\t\t\t// we encountered a 4** or 5** error, redirect to the requested URL\n\t\t\t\t\t\twindow.location.href = url\n\t\t\t\t\t})\n\n\t\t\t\tnavigationPromise = this.beforeFetch(this.targetLocation, TransitionClass, trigger)\n\t\t\t\t\t.then(async () => {\n\t\t\t\t\t\treturn fetched.then(async () => {\n\t\t\t\t\t\t\treturn await this.afterFetch(this.targetLocation, TransitionClass, this.cache.get(this.targetLocation.href), trigger)\n\t\t\t\t\t\t})\n\t\t\t\t\t})\n\t\t\t} else {\n\t\t\t\tthis.cache.get(this.targetLocation.href).renderer.createDom()\n\n\t\t\t\tnavigationPromise = this.beforeFetch(this.targetLocation, TransitionClass, trigger)\n\t\t\t\t\t.then(async () => {\n\t\t\t\t\t\treturn await this.afterFetch(this.targetLocation, TransitionClass, this.cache.get(this.targetLocation.href), trigger)\n\t\t\t\t\t})\n\t\t\t}\n\n\t\t\tnavigationPromise.then(() => {\n\t\t\t\tresolve()\n\t\t\t})\n\t\t})\n\t}\n\n\t/**\n\t * Add an event listener.\n\t * @param {string} event\n\t * @param {any} callback\n\t */\n\ton(event, callback) {\n\t\tE.on(event, callback)\n\t}\n\n\t/**\n\t * Remove an event listener.\n\t * @param {string} event\n\t * @param {any} [callback]\n\t */\n\toff(event, callback) {\n\t\tE.off(event, callback)\n\t}\n\n\t/**\n\t * @private\n\t * @param {{ raw: string, href: string, hasHash: boolean, pathname: string }} url\n\t * @param {Transition} TransitionClass\n\t * @param {string|HTMLElement|false} trigger\n\t * @return {Promise}\n\t */\n\tbeforeFetch(url, TransitionClass, trigger) {\n\t\tE.emit('NAVIGATE_OUT', {\n\t\t\tfrom: this.currentCacheEntry,\n\t\t\ttrigger\n\t\t})\n\n\t\treturn new Promise((resolve) => {\n\t\t\tthis.currentCacheEntry.renderer.leave(TransitionClass, trigger, this.removeOldContent)\n\t\t\t\t.then(() => {\n\t\t\t\t\tif (trigger !== 'popstate') {\n\t\t\t\t\t\twindow.history.pushState({}, '', url.raw)\n\t\t\t\t\t}\n\n\t\t\t\t\tresolve()\n\t\t\t\t})\n\t\t})\n\t}\n\n\t/**\n\t * @private\n\t * @param {{ raw: string, href: string, host: string, hasHash: boolean, pathname: string }} url\n\t * @param {Transition} TransitionClass\n\t * @param {CacheEntry} entry\n\t * @param {string|HTMLElement|false} trigger\n\t * @return {Promise}\n\t */\n\tafterFetch(url, TransitionClass, entry, trigger) {\n\t\tthis.currentLocation = url\n\t\tthis.popTarget = this.currentLocation.href\n\n\t\treturn new Promise((resolve) => {\n\t\t\tentry.renderer.update()\n\n\t\t\tE.emit('NAVIGATE_IN', {\n\t\t\t\tfrom: this.currentCacheEntry,\n\t\t\t\tto: entry,\n\t\t\t\ttrigger\n\t\t\t})\n\n\t\t\tif (this.reloadJsFilter) {\n\t\t\t\tthis.loadScripts(entry.scripts)\n\t\t\t}\n\n\t\t\tif (this.reloadCssFilter) {\n\t\t\t\tthis.loadStyles(entry.styles)\n\t\t\t}\n\n\t\t\t// If the fetched url had a redirect chain, then replace the history to reflect the final resolved URL\n\t\t\tif (trigger !== 'popstate' && url.href !== entry.finalUrl) {\n\t\t\t\twindow.history.replaceState({}, '', entry.finalUrl)\n\t\t\t}\n\n\t\t\tentry.renderer.enter(TransitionClass, trigger)\n\t\t\t\t.then(() => {\n\t\t\t\t\tE.emit('NAVIGATE_END', {\n\t\t\t\t\t\tfrom: this.currentCacheEntry,\n\t\t\t\t\t\tto: entry,\n\t\t\t\t\t\ttrigger\n\t\t\t\t\t})\n\n\t\t\t\t\tthis.currentCacheEntry = entry\n\t\t\t\t\tthis.isTransitioning = false\n\t\t\t\t\tthis.isPopping = false\n\t\t\t\t\tresolve()\n\t\t\t\t})\n\t\t})\n\t}\n\n\t/**\n\t * Load up scripts from the target page if needed\n\t *\n\t * @param {HTMLElement[]} cachedScripts\n\t */\n\tloadScripts(cachedScripts) {\n\t\tconst newScripts = [...cachedScripts]\n\t\tconst currentScripts = Array.from(document.querySelectorAll('script')).filter(this.reloadJsFilter)\n\n\t\t// loop through all new scripts\n\t\tfor (let i = 0; i < currentScripts.length; i++) {\n\t\t\tfor (let n = 0; n < newScripts.length; n++) {\n\t\t\t\tif (currentScripts[i].outerHTML === newScripts[n].outerHTML) {\n\t\t\t\t\treloadElement(currentScripts[i], 'SCRIPT')\n\t\t\t\t\tnewScripts.splice(n, 1)\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tfor (const script of newScripts) {\n\t\t\tappendElement(script, 'SCRIPT')\n\t\t}\n\t}\n\n\t/**\n\t * Load up styles from the target page if needed\n\t *\n\t * @param {Array} cachedStyles\n\t */\n\tloadStyles(cachedStyles) {\n\t\tconst currentStyles = Array.from(document.querySelectorAll('link[rel=\"stylesheet\"]')).filter(this.reloadCssFilter)\n\t\tconst currentInlineStyles = Array.from(document.querySelectorAll('style')).filter(this.reloadCssFilter)\n\n\t\tconst newInlineStyles = cachedStyles.filter(el => {\n\t\t\t// no el.href, assume it's an inline style\n\t\t\tif (!el.href) {\n\t\t\t\treturn true\n\t\t\t} else if (!currentStyles.find((link) => link.href === el.href)) {\n\t\t\t\tdocument.body.append(el)\n\t\t\t\treturn false\n\t\t\t}\n\t\t})\n\n\t\t// loop through all new inline styles\n\t\tfor (let i = 0; i < currentInlineStyles.length; i++) {\n\t\t\tfor (let n = 0; n < newInlineStyles.length; n++) {\n\t\t\t\tif (currentInlineStyles[i].outerHTML === newInlineStyles[n].outerHTML) {\n\t\t\t\t\treloadElement(currentInlineStyles[i], 'STYLE')\n\t\t\t\t\tnewInlineStyles.splice(n, 1)\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tfor (const style of newInlineStyles) {\n\t\t\tappendElement(style, 'STYLE')\n\t\t}\n\t}\n\n\t/**\n\t * @private\n\t * @param {string} links\n\t */\n\tattachEvents(links) {\n\t\tE.delegate('click', links, this.onClick)\n\t\tE.on('popstate', window, this.onPopstate)\n\n\t\tif (this.enablePrefetch) {\n\t\t\tE.delegate('mouseenter focus', links, this.onPrefetch)\n\t\t}\n\t}\n\n\t/**\n\t * @private\n\t * @param {MouseEvent} e\n\t */\n\tonClick = (e) => {\n\t\tif (!(e.metaKey || e.ctrlKey)) {\n\t\t\tconst target = processUrl(e.currentTarget.href)\n\t\t\tthis.currentLocation = processUrl(window.location.href)\n\n\t\t\tif (this.currentLocation.host !== target.host) {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\t// the target is a new URL, or is removing the hash from the current URL\n\t\t\tif (this.currentLocation.href !== target.href || (this.currentLocation.hasHash && !target.hasHash)) {\n\t\t\t\te.preventDefault()\n\t\t\t\t// noinspection JSIgnoredPromiseFromCall\n\t\t\t\tthis.navigateTo(target.raw, e.currentTarget.dataset.transition || false, e.currentTarget).catch(err => console.warn(err))\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\t// a click to the current URL was detected\n\t\t\tif (!this.currentLocation.hasHash && !target.hasHash) {\n\t\t\t\te.preventDefault()\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * @private\n\t * @return {void|boolean}\n\t */\n\tonPopstate = () => {\n\t\tconst target = processUrl(window.location.href)\n\n\t\t// don't trigger for on-page anchors\n\t\tif (\n\t\t\ttarget.pathname === this.currentLocation.pathname\n\t\t\t&& target.search === this.currentLocation.search\n\t\t\t&& !this.isPopping\n\t\t) {\n\t\t\treturn false\n\t\t}\n\n\t\tif (!this.allowInterruption && (this.isTransitioning || this.isPopping)) {\n\t\t\t// overwrite history state with current page if currently navigating\n\t\t\twindow.history.pushState({}, '', this.popTarget)\n\t\t\tconsole.warn(IN_PROGRESS)\n\t\t\treturn false\n\t\t}\n\n\t\tif (!this.isPopping) {\n\t\t\tthis.popTarget = window.location.href\n\t\t}\n\n\t\tthis.isPopping = true\n\n\t\t// noinspection JSIgnoredPromiseFromCall\n\t\tthis.navigateTo(window.location.href, false, 'popstate')\n\t}\n\n\t/**\n\t * @private\n\t * @param {MouseEvent} e\n\t */\n\tonPrefetch = (e) => {\n\t\tconst target = processUrl(e.currentTarget.href)\n\n\t\tif (this.currentLocation.host !== target.host) {\n\t\t\treturn\n\t\t}\n\n\t\tthis.preload(e.currentTarget.href, false)\n\t}\n\n\t/**\n\t * @private\n\t * @param {string} url\n\t * @param {boolean} [runFallback]\n\t * @return {Promise<{html: Document, url: string}>}\n\t */\n\tfetch(url, runFallback = true) {\n\t\t// If Taxi is currently performing a fetch for the given URL, return that instead of starting a new request\n\t\tif (this.activePromises.has(url)) {\n\t\t\treturn this.activePromises.get(url)\n\t\t}\n\n\t\tconst request = new Promise((resolve, reject) => {\n\t\t\tlet resolvedUrl\n\n\t\t\tfetch(url, {\n\t\t\t\tmode: 'same-origin',\n\t\t\t\tmethod: 'GET',\n\t\t\t\theaders: { 'X-Requested-With': 'Taxi' },\n\t\t\t\tcredentials: 'same-origin'\n\t\t\t})\n\t\t\t\t.then((response) => {\n\t\t\t\t\tif (!response.ok) {\n\t\t\t\t\t\treject('Taxi encountered a non 2xx HTTP status code')\n\n\t\t\t\t\t\tif (runFallback) {\n\t\t\t\t\t\t\twindow.location.href = url\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tresolvedUrl = response.url\n\n\t\t\t\t\treturn response.text()\n\t\t\t\t})\n\t\t\t\t.then((htmlString) => {\n\t\t\t\t\tresolve({ html: parseDom(htmlString), url: resolvedUrl })\n\t\t\t\t})\n\t\t\t\t.catch((err) => {\n\t\t\t\t\treject(err)\n\n\t\t\t\t\tif (runFallback) {\n\t\t\t\t\t\twindow.location.href = url\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t\t.finally(() => {\n\t\t\t\t\tthis.activePromises.delete(url)\n\t\t\t\t})\n\t\t})\n\n\t\tthis.activePromises.set(url, request)\n\n\t\treturn request\n\t}\n\n\t/**\n\t * @private\n\t * @param {string|false} transition\n\t * @return {Transition|function}\n\t */\n\tchooseTransition(transition) {\n\t\tif (transition) {\n\t\t\treturn this.transitions[transition]\n\t\t}\n\n\t\tconst routeTransition = this.router?.findMatch(this.currentLocation, this.targetLocation)\n\n\t\tif (routeTransition) {\n\t\t\treturn this.transitions[routeTransition]\n\t\t}\n\n\t\treturn this.defaultTransition\n\t}\n\n\t/**\n\t * @private\n\t * @param {Document|Node} page\n\t * @param {string} url\n\t * @return {CacheEntry}\n\t */\n\tcreateCacheEntry(page, url) {\n\t\tconst content = page.querySelector('[data-taxi-view]')\n\t\tconst Renderer = content.dataset.taxiView.length ? this.renderers[content.dataset.taxiView] : this.defaultRenderer\n\n\t\tif (!Renderer) {\n\t\t\tconsole.warn(`The Renderer \"${content.dataset.taxiView}\" was set in the data-taxi-view of the requested page, but not registered in Taxi.`)\n\t\t}\n\n\t\treturn {\n\t\t\tpage,\n\t\t\tcontent,\n\t\t\tfinalUrl: url,\n\t\t\tskipCache: content.hasAttribute('data-taxi-nocache'),\n\t\t\tscripts: this.reloadJsFilter ? Array.from(page.querySelectorAll('script')).filter(this.reloadJsFilter) : [],\n\t\t\tstyles: this.reloadCssFilter ? Array.from(page.querySelectorAll('link[rel=\"stylesheet\"], style')).filter(this.reloadCssFilter) : [],\n\t\t\ttitle: page.title,\n\t\t\trenderer: new Renderer({\n\t\t\t\twrapper: this.wrapper,\n\t\t\t\ttitle: page.title,\n\t\t\t\tcontent,\n\t\t\t\tpage\n\t\t\t})\n\t\t}\n\t}\n}\n"],"names":["parser","DOMParser","processUrl","url","details","URL","window","location","origin","normalized","hash","length","replace","hasHash","pathname","host","search","raw","href","reloadElement","node","elementType","parentNode","replaceChild","duplicateElement","appendElement","tagName","document","head","body","appendChild","replacement","createElement","k","attributes","attr","setAttribute","nodeName","nodeValue","innerHTML","Transition","this","wrapper","leave","props","Promise","resolve","_this","onLeave","done","enter","_this2","onEnter","Renderer","page","title","_contentString","content","outerHTML","_DOM","lastElementChild","onEnterCompleted","onLeaveCompleted","initialLoad","update","firstElementChild","createDom","remove","transition","trigger","to","then","removeOldContent","from","RouteStore","data","Map","regexCache","add","fromPattern","toPattern","has","set","RegExp","get","findMatch","currentUrl","nextUrl","potentialMatches","match","IN_PROGRESS","Core","parameters","isTransitioning","currentCacheEntry","cache","activePromises","onClick","e","metaKey","ctrlKey","target","currentTarget","currentLocation","preventDefault","navigateTo","dataset","err","console","warn","onPopstate","isPopping","allowInterruption","popTarget","history","pushState","onPrefetch","preload","links","bypassCache","enablePrefetch","renderers","transitions","default","reloadJsFilter","element","undefined","taxiReload","reloadCssFilter","defaultRenderer","defaultTransition","querySelector","attachEvents","createCacheEntry","cloneNode","renderer","setDefaultRenderer","setDefaultTransition","addRoute","router","preloadAssets","fetch","response","html","updateCache","key","clearCache","reject","_this3","targetLocation","navigationPromise","TransitionClass","chooseTransition","skipCache","fetched","beforeFetch","afterFetch","Error","on","event","callback","E","off","emit","_this4","entry","_this5","loadScripts","scripts","loadStyles","styles","finalUrl","replaceState","cachedScripts","newScripts","currentScripts","Array","querySelectorAll","filter","i","n","splice","cachedStyles","currentStyles","currentInlineStyles","newInlineStyles","el","find","link","append","delegate","runFallback","request","resolvedUrl","mode","method","headers","credentials","ok","text","htmlString","parseFromString","_this6","routeTransition","_this$router","taxiView","hasAttribute"],"mappings":"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"}
--------------------------------------------------------------------------------
/dist/taxi.modern.js:
--------------------------------------------------------------------------------
1 | import t from"@unseenco/e";const e=new DOMParser;function r(t){const e=new URL(t,window.location.origin),r=e.hash.length?t.replace(e.hash,""):null;return{hasHash:e.hash.length>0,pathname:e.pathname.replace(/\/+$/,""),host:e.host,search:e.search,raw:t,href:r||e.href}}function i(t,e){t.parentNode.replaceChild(s(t,e),t)}function n(t,e){("HEAD"===t.parentNode.tagName?document.head:document.body).appendChild(s(t,e))}function s(t,e){const r=document.createElement(e);for(let e=0;e{this.onLeave(a({},t,{done:e}))})}enter(t){return new Promise(e=>{this.onEnter(a({},t,{done:e}))})}onLeave({done:t}){t()}onEnter({done:t}){t()}}class h{constructor({content:t,page:e,title:r,wrapper:i}){this._contentString=t.outerHTML,this._DOM=null,this.page=e,this.title=r,this.wrapper=i,this.content=this.wrapper.lastElementChild}onEnter(){}onEnterCompleted(){}onLeave(){}onLeaveCompleted(){}initialLoad(){this.onEnter(),this.onEnterCompleted()}update(){document.title=this.title,this.wrapper.appendChild(this._DOM.firstElementChild),this.content=this.wrapper.lastElementChild,this._DOM=null}createDom(){this._DOM||(this._DOM=document.createElement("div"),this._DOM.innerHTML=this._contentString)}remove(){this.wrapper.firstElementChild.remove()}enter(t,e){return new Promise(r=>{this.onEnter(),t.enter({trigger:e,to:this.content}).then(()=>{this.onEnterCompleted(),r()})})}leave(t,e,r){return new Promise(i=>{this.onLeave(),t.leave({trigger:e,from:this.content}).then(()=>{r&&this.remove(),this.onLeaveCompleted(),i()})})}}class c{constructor(){this.data=new Map,this.regexCache=new Map}add(t,e,r){this.data.has(t)||(this.data.set(t,new Map),this.regexCache.set(t,new RegExp(`^${t}$`))),this.data.get(t).set(e,r),this.regexCache.set(e,new RegExp(`^${e}$`))}findMatch(t,e){for(const[r,i]of this.data)if(t.pathname.match(this.regexCache.get(r))){for(const[t,r]of i)if(e.pathname.match(this.regexCache.get(t)))return r;break}return null}}const l="A transition is currently in progress";class d{constructor(t={}){this.isTransitioning=!1,this.currentCacheEntry=null,this.cache=new Map,this.activePromises=new Map,this.onClick=t=>{if(!t.metaKey&&!t.ctrlKey){const e=r(t.currentTarget.href);if(this.currentLocation=r(window.location.href),this.currentLocation.host!==e.host)return;if(this.currentLocation.href!==e.href||this.currentLocation.hasHash&&!e.hasHash)return t.preventDefault(),void this.navigateTo(e.raw,t.currentTarget.dataset.transition||!1,t.currentTarget).catch(t=>console.warn(t));this.currentLocation.hasHash||e.hasHash||t.preventDefault()}},this.onPopstate=()=>{const t=r(window.location.href);return!(t.pathname===this.currentLocation.pathname&&t.search===this.currentLocation.search&&!this.isPopping)&&(this.allowInterruption||!this.isTransitioning&&!this.isPopping?(this.isPopping||(this.popTarget=window.location.href),this.isPopping=!0,void this.navigateTo(window.location.href,!1,"popstate")):(window.history.pushState({},"",this.popTarget),console.warn(l),!1))},this.onPrefetch=t=>{const e=r(t.currentTarget.href);this.currentLocation.host===e.host&&this.preload(t.currentTarget.href,!1)};const{links:e="a[href]:not([target]):not([href^=\\#]):not([data-taxi-ignore])",removeOldContent:i=!0,allowInterruption:n=!1,bypassCache:s=!1,enablePrefetch:a=!0,renderers:c={default:h},transitions:d={default:o},reloadJsFilter:u=(t=>void 0!==t.dataset.taxiReload),reloadCssFilter:f=(t=>!0)}=t;this.renderers=c,this.transitions=d,this.defaultRenderer=this.renderers.default||h,this.defaultTransition=this.transitions.default||o,this.wrapper=document.querySelector("[data-taxi]"),this.reloadJsFilter=u,this.reloadCssFilter=f,this.removeOldContent=i,this.allowInterruption=n,this.bypassCache=s,this.enablePrefetch=a,this.cache=new Map,this.isPopping=!1,this.attachEvents(e),this.currentLocation=r(window.location.href),this.cache.set(this.currentLocation.href,this.createCacheEntry(document.cloneNode(!0),window.location.href)),this.currentCacheEntry=this.cache.get(this.currentLocation.href),this.currentCacheEntry.renderer.initialLoad()}setDefaultRenderer(t){this.defaultRenderer=this.renderers[t]}setDefaultTransition(t){this.defaultTransition=this.transitions[t]}addRoute(t,e,r){this.router||(this.router=new c),this.router.add(t,e,r)}preload(t,e=!1){var i=this;return t=r(t).href,this.cache.has(t)?Promise.resolve():this.fetch(t,!1).then(async function(r){i.cache.set(t,i.createCacheEntry(r.html,r.url)),e&&i.cache.get(t).renderer.createDom()}).catch(t=>console.warn(t))}updateCache(t){const e=r(t||window.location.href).href;this.cache.has(e)&&this.cache.delete(e),this.cache.set(e,this.createCacheEntry(document.cloneNode(!0),e))}clearCache(t){const e=r(t||window.location.href).href;this.cache.has(e)&&this.cache.delete(e)}navigateTo(t,e=!1,i=!1){var n=this;return new Promise((s,a)=>{if(!this.allowInterruption&&this.isTransitioning)return void a(new Error(l));this.isTransitioning=!0,this.isPopping=!0,this.targetLocation=r(t),this.popTarget=window.location.href;const o=new(this.chooseTransition(e))({wrapper:this.wrapper});let h;if(this.bypassCache||!this.cache.has(this.targetLocation.href)||this.cache.get(this.targetLocation.href).skipCache){const e=this.fetch(this.targetLocation.href).then(t=>{this.cache.set(this.targetLocation.href,this.createCacheEntry(t.html,t.url)),this.cache.get(this.targetLocation.href).renderer.createDom()}).catch(e=>{window.location.href=t});h=this.beforeFetch(this.targetLocation,o,i).then(async function(){return e.then(async function(){return await n.afterFetch(n.targetLocation,o,n.cache.get(n.targetLocation.href),i)})})}else this.cache.get(this.targetLocation.href).renderer.createDom(),h=this.beforeFetch(this.targetLocation,o,i).then(async function(){return await n.afterFetch(n.targetLocation,o,n.cache.get(n.targetLocation.href),i)});h.then(()=>{s()})})}on(e,r){t.on(e,r)}off(e,r){t.off(e,r)}beforeFetch(e,r,i){return t.emit("NAVIGATE_OUT",{from:this.currentCacheEntry,trigger:i}),new Promise(t=>{this.currentCacheEntry.renderer.leave(r,i,this.removeOldContent).then(()=>{"popstate"!==i&&window.history.pushState({},"",e.raw),t()})})}afterFetch(e,r,i,n){return this.currentLocation=e,this.popTarget=this.currentLocation.href,new Promise(s=>{i.renderer.update(),t.emit("NAVIGATE_IN",{from:this.currentCacheEntry,to:i,trigger:n}),this.reloadJsFilter&&this.loadScripts(i.scripts),this.reloadCssFilter&&this.loadStyles(i.styles),"popstate"!==n&&e.href!==i.finalUrl&&window.history.replaceState({},"",i.finalUrl),i.renderer.enter(r,n).then(()=>{t.emit("NAVIGATE_END",{from:this.currentCacheEntry,to:i,trigger:n}),this.currentCacheEntry=i,this.isTransitioning=!1,this.isPopping=!1,s()})})}loadScripts(t){const e=[...t],r=Array.from(document.querySelectorAll("script")).filter(this.reloadJsFilter);for(let t=0;t!t.href||(e.find(e=>e.href===t.href)?void 0:(document.body.append(t),!1)));for(let t=0;t{let s;fetch(t,{mode:"same-origin",method:"GET",headers:{"X-Requested-With":"Taxi"},credentials:"same-origin"}).then(e=>(e.ok||(n("Taxi encountered a non 2xx HTTP status code"),r&&(window.location.href=t)),s=e.url,e.text())).then(t=>{var r;i({html:(r=t,"string"==typeof r?e.parseFromString(r,"text/html"):r),url:s})}).catch(e=>{n(e),r&&(window.location.href=t)}).finally(()=>{this.activePromises.delete(t)})});return this.activePromises.set(t,i),i}chooseTransition(t){var e;if(t)return this.transitions[t];const r=null==(e=this.router)?void 0:e.findMatch(this.currentLocation,this.targetLocation);return r?this.transitions[r]:this.defaultTransition}createCacheEntry(t,e){const r=t.querySelector("[data-taxi-view]"),i=r.dataset.taxiView.length?this.renderers[r.dataset.taxiView]:this.defaultRenderer;return i||console.warn(`The Renderer "${r.dataset.taxiView}" was set in the data-taxi-view of the requested page, but not registered in Taxi.`),{page:t,content:r,finalUrl:e,skipCache:r.hasAttribute("data-taxi-nocache"),scripts:this.reloadJsFilter?Array.from(t.querySelectorAll("script")).filter(this.reloadJsFilter):[],styles:this.reloadCssFilter?Array.from(t.querySelectorAll('link[rel="stylesheet"], style')).filter(this.reloadCssFilter):[],title:t.title,renderer:new i({wrapper:this.wrapper,title:t.title,content:r,page:t})}}}export{d as Core,h as Renderer,o as Transition};
2 | //# sourceMappingURL=taxi.modern.js.map
3 |
--------------------------------------------------------------------------------
/dist/taxi.modern.js.map:
--------------------------------------------------------------------------------
1 | {"version":3,"file":"taxi.modern.js","sources":["../src/helpers.js","../src/Transition.js","../src/Renderer.js","../src/RouteStore.js","../src/Core.js"],"sourcesContent":["const parser = new DOMParser()\n\n/**\n * Parse a HTML string into a proper Document.\n *\n * @param {string|Document} html\n * @return {Document|*}\n */\nexport function parseDom(html) {\n\treturn typeof html === 'string' ? parser.parseFromString(html, 'text/html') : html\n}\n\n/**\n * Extract details from a given URL string. Assumed to be on the current TLD.\n *\n * @param {string} url\n * @return {{raw: string, href: string, host: string, search: string, hasHash: boolean, pathname: string}}\n */\nexport function processUrl(url) {\n\tconst details = new URL(url, window.location.origin)\n\tconst normalized = details.hash.length ? url.replace(details.hash, '') : null\n\n\treturn {\n\t\thasHash: details.hash.length > 0,\n\t\tpathname: details.pathname.replace(/\\/+$/, ''),\n\t\thost: details.host,\n\t\tsearch: details.search,\n\t\traw: url,\n\t\thref: normalized || details.href\n\t}\n}\n\n/**\n * Reloads a provided script/stylesheet by replacing with itself.\n *\n * @param {HTMLElement|HTMLScriptElement|HTMLStyleElement} node\n * @param {string} elementType - 'SCRIPT' or 'STYLE'\n */\nexport function reloadElement(node, elementType) {\n\tnode.parentNode.replaceChild(duplicateElement(node, elementType), node)\n}\n\n/**\n * Loads a provided script/stylesheet by appending a clone to the current document.\n *\n * @param {HTMLElement|HTMLStyleElement} node\n * @param {string} elementType - 'SCRIPT' or 'STYLE'\n */\nexport function appendElement(node, elementType) {\n\tconst target = node.parentNode.tagName === 'HEAD' ? document.head : document.body\n\ttarget.appendChild(duplicateElement(node, elementType))\n}\n\n/**\n * Creates a clone of a given HTMLElement or HTMLStyleElement\n *\n * @param {HTMLElement|HTMLStyleElement} node\n * @param {string} elementType - 'SCRIPT' or 'STYLE'\n * @return {HTMLElement|HTMLStyleElement}\n */\nexport function duplicateElement(node, elementType) {\n\tconst replacement = document.createElement(elementType)\n\n\tfor (let k = 0; k < node.attributes.length; k++) {\n\t\tconst attr = node.attributes[k]\n\t\treplacement.setAttribute(attr.nodeName, attr.nodeValue)\n\t}\n\n\t// Inline Script or Style\n\tif (node.innerHTML) {\n\t\treplacement.innerHTML = node.innerHTML\n\t}\n\n\treturn replacement\n}\n","export default class Transition {\n\t/**\n\t * @param {{wrapper: HTMLElement}} props\n\t */\n\tconstructor({ wrapper }) {\n\t\tthis.wrapper = wrapper\n\t}\n\n\t/**\n\t * @param {{ from: HTMLElement|Element, trigger: string|HTMLElement|false }} props\n\t * @return {Promise}\n\t */\n\tleave(props) {\n\t\treturn new Promise((resolve) => {\n\t\t\tthis.onLeave({ ...props, done: resolve })\n\t\t})\n\t}\n\n\t/**\n\t * @param {{ to: HTMLElement|Element, trigger: string|HTMLElement|false }} props\n\t * @return {Promise}\n\t */\n\tenter(props) {\n\t\treturn new Promise((resolve) => {\n\t\t\tthis.onEnter({ ...props, done: resolve })\n\t\t})\n\t}\n\n\t/**\n\t * Handle the transition leaving the previous page.\n\t * @param {{from: HTMLElement|Element, trigger: string|HTMLElement|false, done: function}} props\n\t */\n\tonLeave({ from, trigger, done }) {\n\t\tdone()\n\t}\n\n\t/**\n\t * Handle the transition entering the next page.\n\t * @param {{to: HTMLElement|Element, trigger: string|HTMLElement|false, done: function}} props\n\t */\n\tonEnter({ to, trigger, done }) {\n\t\tdone()\n\t}\n}\n","import Transition from \"./Transition\"\n\nexport default class Renderer {\n\t/**\n\t * @param {{content: HTMLElement|Element, page: Document|Node, title: string, wrapper: Element}} props\n\t */\n\tconstructor({ content, page, title, wrapper }) {\n\t\tthis._contentString = content.outerHTML\n\t\tthis._DOM = null\n\t\tthis.page = page\n\t\tthis.title = title\n\t\tthis.wrapper = wrapper\n\t\tthis.content = this.wrapper.lastElementChild\n\t}\n\n\tonEnter() {\n\n\t}\n\n\tonEnterCompleted() {\n\n\t}\n\n\tonLeave() {\n\n\t}\n\n\tonLeaveCompleted() {\n\n\t}\n\n\tinitialLoad() {\n\t\tthis.onEnter()\n\t\tthis.onEnterCompleted()\n\t}\n\n\tupdate() {\n\t\tdocument.title = this.title\n\t\tthis.wrapper.appendChild(this._DOM.firstElementChild)\n\t\tthis.content = this.wrapper.lastElementChild\n\t\tthis._DOM = null\n\t}\n\n\tcreateDom() {\n\t\tif (!this._DOM) {\n\t\t\tthis._DOM = document.createElement('div')\n\t\t\tthis._DOM.innerHTML = this._contentString\n\t\t}\n\t}\n\n\tremove() {\n\t\tthis.wrapper.firstElementChild.remove()\n\t}\n\n\t/**\n\t * Called when transitioning into the current page.\n\t * @param {Transition} transition\n\t * @param {string|HTMLElement|false} trigger\n\t * @return {Promise}\n\t */\n\tenter(transition, trigger) {\n\t\treturn new Promise((resolve) => {\n\t\t\tthis.onEnter()\n\n\t\t\ttransition.enter({ trigger, to: this.content })\n\t\t\t\t.then(() => {\n\t\t\t\t\tthis.onEnterCompleted()\n\t\t\t\t\tresolve()\n\t\t\t\t})\n\t\t})\n\t}\n\n\t/**\n\t * Called when transitioning away from the current page.\n\t * @param {Transition} transition\n\t * @param {string|HTMLElement|false} trigger\n\t * @param {boolean} removeOldContent\n\t * @return {Promise}\n\t */\n\tleave(transition, trigger, removeOldContent) {\n\t\treturn new Promise((resolve) => {\n\t\t\tthis.onLeave()\n\n\t\t\ttransition.leave({ trigger, from: this.content })\n\t\t\t\t.then(() => {\n\t\t\t\t\tif (removeOldContent) {\n\t\t\t\t\t\tthis.remove()\n\t\t\t\t\t}\n\n\t\t\t\t\tthis.onLeaveCompleted()\n\t\t\t\t\tresolve()\n\t\t\t\t})\n\t\t})\n\t}\n}\n","export default class RouteStore {\n\t/**\n\t * @type {Map>}\n\t */\n\tdata = new Map()\n\n\t/**\n\t * @type {Map}\n\t */\n\tregexCache = new Map()\n\n\t/**\n\t *\n\t * @param {string} fromPattern\n\t * @param {string} toPattern\n\t * @param {string} transition\n\t */\n\tadd(fromPattern, toPattern, transition) {\n\t\tif (!this.data.has(fromPattern)) {\n\t\t\tthis.data.set(fromPattern, new Map())\n\t\t\tthis.regexCache.set(fromPattern, new RegExp(`^${fromPattern}$`))\n\t\t}\n\n\t\tthis.data.get(fromPattern).set(toPattern, transition)\n\t\tthis.regexCache.set(toPattern, new RegExp(`^${toPattern}$`))\n\t}\n\n\t/**\n\t *\n\t * @param {{ raw: string, href: string, hasHash: boolean, pathname: string }} currentUrl\n\t * @param {{ raw: string, href: string, hasHash: boolean, pathname: string }} nextUrl\n\t * @return {string|null}\n\t */\n\tfindMatch(currentUrl, nextUrl) {\n\t\t// Loop through all from patterns\n\t\tfor (const [fromPattern, potentialMatches] of this.data) {\n\t\t\t// If we have a match\n\t\t\tif (currentUrl.pathname.match(this.regexCache.get(fromPattern))) {\n\t\t\t\t// loop through all associated to patterns\n\t\t\t\tfor (const [toPattern, transition] of potentialMatches) {\n\t\t\t\t\t// If we find a match, return it\n\t\t\t\t\tif (nextUrl.pathname.match(this.regexCache.get(toPattern))) {\n\t\t\t\t\t\treturn transition\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\n\t\treturn null\n\t}\n}\n","import E from '@unseenco/e'\nimport { appendElement, parseDom, processUrl, reloadElement } from './helpers'\nimport Transition from './Transition'\nimport Renderer from './Renderer'\nimport RouteStore from './RouteStore'\n\nconst IN_PROGRESS = 'A transition is currently in progress'\n\n/**\n * @typedef CacheEntry\n * @type {object}\n * @property {typeof Renderer|Renderer} renderer\n * @property {Document|Node} page\n * @property {array} scripts\n * @property {HTMLLinkElement[]} styles\n * @property {string} finalUrl\n * @property {boolean} skipCache\n * @property {string} title\n * @property {HTMLElement|Element} content\n */\n\nexport default class Core {\n\tisTransitioning = false\n\n\t/**\n\t * @type {CacheEntry|null}\n\t */\n\tcurrentCacheEntry = null\n\n\t/**\n\t * @type {Map}\n\t */\n\tcache = new Map()\n\n\t/**\n\t * @private\n\t * @type {Map}\n\t */\n\tactivePromises = new Map()\n\n\t/**\n\t * @param {{\n\t * \t\tlinks?: string,\n\t * \t\tremoveOldContent?: boolean,\n\t * \t\tallowInterruption?: boolean,\n\t * \t\tbypassCache?: boolean,\n\t * \t\tenablePrefetch?: boolean,\n\t * \t\trenderers?: Object.,\n\t * \t\ttransitions?: Object.,\n\t * \t\treloadJsFilter?: boolean|function(HTMLElement): boolean,\n\t * \t\treloadCssFilter?: boolean|function(HTMLLinkElement): boolean\n\t * }} parameters\n\t */\n\tconstructor(parameters = {}) {\n\t\tconst {\n\t\t\tlinks = 'a[href]:not([target]):not([href^=\\\\#]):not([data-taxi-ignore])',\n\t\t\tremoveOldContent = true,\n\t\t\tallowInterruption = false,\n\t\t\tbypassCache = false,\n\t\t\tenablePrefetch = true,\n\t\t\trenderers = {\n\t\t\t\tdefault: Renderer\n\t\t\t},\n\t\t\ttransitions = {\n\t\t\t\tdefault: Transition\n\t\t\t},\n\t\t\treloadJsFilter = (element) => element.dataset.taxiReload !== undefined,\n\t\t\treloadCssFilter = (element) => true //element.dataset.taxiReload !== undefined\n\t\t} = parameters\n\n\t\tthis.renderers = renderers\n\t\tthis.transitions = transitions\n\t\tthis.defaultRenderer = this.renderers.default || Renderer\n\t\tthis.defaultTransition = this.transitions.default || Transition\n\t\tthis.wrapper = document.querySelector('[data-taxi]')\n\t\tthis.reloadJsFilter = reloadJsFilter\n\t\tthis.reloadCssFilter = reloadCssFilter\n\t\tthis.removeOldContent = removeOldContent\n\t\tthis.allowInterruption = allowInterruption\n\t\tthis.bypassCache = bypassCache\n\t\tthis.enablePrefetch = enablePrefetch\n\t\tthis.cache = new Map()\n\t\tthis.isPopping = false\n\n\t\t// Add delegated link events\n\t\tthis.attachEvents(links)\n\n\t\tthis.currentLocation = processUrl(window.location.href)\n\n\t\t// as this is the initial page load, prime this page into the cache\n\t\tthis.cache.set(this.currentLocation.href, this.createCacheEntry(document.cloneNode(true), window.location.href))\n\n\t\t// fire the current Renderer enter methods\n\t\tthis.currentCacheEntry = this.cache.get(this.currentLocation.href)\n\t\tthis.currentCacheEntry.renderer.initialLoad()\n\t}\n\n\t/**\n\t * @param {string} renderer\n\t */\n\tsetDefaultRenderer(renderer) {\n\t\tthis.defaultRenderer = this.renderers[renderer]\n\t}\n\n\t/**\n\t * @param {string} transition\n\t */\n\tsetDefaultTransition(transition) {\n\t\tthis.defaultTransition = this.transitions[transition]\n\t}\n\n\t/**\n\t * Registers a route into the RouteStore\n\t *\n\t * @param {string} fromPattern\n\t * @param {string} toPattern\n\t * @param {string} transition\n\t */\n\taddRoute(fromPattern, toPattern, transition) {\n\t\tif (!this.router) {\n\t\t\tthis.router = new RouteStore()\n\t\t}\n\n\t\tthis.router.add(fromPattern, toPattern, transition)\n\t}\n\n\t/**\n\t * Prime the cache for a given URL\n\t *\n\t * @param {string} url\n\t * @param {boolean} [preloadAssets]\n\t * @return {Promise}\n\t */\n\tpreload(url, preloadAssets = false) {\n\t\t// convert relative URLs to absolute\n\t\turl = processUrl(url).href\n\n\t\tif (!this.cache.has(url)) {\n\t\t\treturn this.fetch(url, false)\n\t\t\t\t.then(async (response) => {\n\t\t\t\t\tthis.cache.set(url, this.createCacheEntry(response.html, response.url))\n\n\t\t\t\t\tif (preloadAssets) {\n\t\t\t\t\t\tthis.cache.get(url).renderer.createDom()\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t\t.catch(err => console.warn(err))\n\t\t}\n\n\t\treturn Promise.resolve()\n\t}\n\n\t/**\n\t * Updates the HTML cache for a given URL.\n\t * If no URL is passed, then cache for the current page is updated.\n\t * Useful when adding/removing content via AJAX such as a search page or infinite loader.\n\t *\n\t * @param {string} [url]\n\t */\n\tupdateCache(url) {\n\t\tconst key = processUrl(url || window.location.href).href\n\n\t\tif (this.cache.has(key)) {\n\t\t\tthis.cache.delete(key)\n\t\t}\n\n\t\tthis.cache.set(key, this.createCacheEntry(document.cloneNode(true), key))\n\t}\n\n\t/**\n\t * Clears the cache for a given URL.\n\t * If no URL is passed, then cache for the current page is cleared.\n\t *\n\t * @param {string} [url]\n\t */\n\tclearCache(url) {\n\t\tconst key = processUrl(url || window.location.href).href\n\n\t\tif (this.cache.has(key)) {\n\t\t\tthis.cache.delete(key)\n\t\t}\n\t}\n\n\t/**\n\t * @param {string} url\n\t * @param {string|false} [transition]\n\t * @param {string|false|HTMLElement} [trigger]\n\t * @return {Promise}\n\t */\n\tnavigateTo(url, transition = false, trigger = false) {\n\t\treturn new Promise((resolve, reject) => {\n\t\t\t// Don't allow multiple navigations to occur at once\n\t\t\tif (!this.allowInterruption && this.isTransitioning) {\n\t\t\t\treject(new Error(IN_PROGRESS))\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tthis.isTransitioning = true\n\t\t\tthis.isPopping = true\n\t\t\tthis.targetLocation = processUrl(url)\n\t\t\tthis.popTarget = window.location.href\n\n\t\t\tconst TransitionClass = new (this.chooseTransition(transition))({ wrapper: this.wrapper })\n\n\t\t\tlet navigationPromise\n\n\t\t\tif (this.bypassCache || !this.cache.has(this.targetLocation.href) || this.cache.get(this.targetLocation.href).skipCache) {\n\t\t\t\tconst fetched = this.fetch(this.targetLocation.href)\n\t\t\t\t\t.then((response) => {\n\t\t\t\t\t\tthis.cache.set(this.targetLocation.href, this.createCacheEntry(response.html, response.url))\n\t\t\t\t\t\tthis.cache.get(this.targetLocation.href).renderer.createDom()\n\t\t\t\t\t})\n\t\t\t\t\t.catch(err => {\n\t\t\t\t\t\t// we encountered a 4** or 5** error, redirect to the requested URL\n\t\t\t\t\t\twindow.location.href = url\n\t\t\t\t\t})\n\n\t\t\t\tnavigationPromise = this.beforeFetch(this.targetLocation, TransitionClass, trigger)\n\t\t\t\t\t.then(async () => {\n\t\t\t\t\t\treturn fetched.then(async () => {\n\t\t\t\t\t\t\treturn await this.afterFetch(this.targetLocation, TransitionClass, this.cache.get(this.targetLocation.href), trigger)\n\t\t\t\t\t\t})\n\t\t\t\t\t})\n\t\t\t} else {\n\t\t\t\tthis.cache.get(this.targetLocation.href).renderer.createDom()\n\n\t\t\t\tnavigationPromise = this.beforeFetch(this.targetLocation, TransitionClass, trigger)\n\t\t\t\t\t.then(async () => {\n\t\t\t\t\t\treturn await this.afterFetch(this.targetLocation, TransitionClass, this.cache.get(this.targetLocation.href), trigger)\n\t\t\t\t\t})\n\t\t\t}\n\n\t\t\tnavigationPromise.then(() => {\n\t\t\t\tresolve()\n\t\t\t})\n\t\t})\n\t}\n\n\t/**\n\t * Add an event listener.\n\t * @param {string} event\n\t * @param {any} callback\n\t */\n\ton(event, callback) {\n\t\tE.on(event, callback)\n\t}\n\n\t/**\n\t * Remove an event listener.\n\t * @param {string} event\n\t * @param {any} [callback]\n\t */\n\toff(event, callback) {\n\t\tE.off(event, callback)\n\t}\n\n\t/**\n\t * @private\n\t * @param {{ raw: string, href: string, hasHash: boolean, pathname: string }} url\n\t * @param {Transition} TransitionClass\n\t * @param {string|HTMLElement|false} trigger\n\t * @return {Promise}\n\t */\n\tbeforeFetch(url, TransitionClass, trigger) {\n\t\tE.emit('NAVIGATE_OUT', {\n\t\t\tfrom: this.currentCacheEntry,\n\t\t\ttrigger\n\t\t})\n\n\t\treturn new Promise((resolve) => {\n\t\t\tthis.currentCacheEntry.renderer.leave(TransitionClass, trigger, this.removeOldContent)\n\t\t\t\t.then(() => {\n\t\t\t\t\tif (trigger !== 'popstate') {\n\t\t\t\t\t\twindow.history.pushState({}, '', url.raw)\n\t\t\t\t\t}\n\n\t\t\t\t\tresolve()\n\t\t\t\t})\n\t\t})\n\t}\n\n\t/**\n\t * @private\n\t * @param {{ raw: string, href: string, host: string, hasHash: boolean, pathname: string }} url\n\t * @param {Transition} TransitionClass\n\t * @param {CacheEntry} entry\n\t * @param {string|HTMLElement|false} trigger\n\t * @return {Promise}\n\t */\n\tafterFetch(url, TransitionClass, entry, trigger) {\n\t\tthis.currentLocation = url\n\t\tthis.popTarget = this.currentLocation.href\n\n\t\treturn new Promise((resolve) => {\n\t\t\tentry.renderer.update()\n\n\t\t\tE.emit('NAVIGATE_IN', {\n\t\t\t\tfrom: this.currentCacheEntry,\n\t\t\t\tto: entry,\n\t\t\t\ttrigger\n\t\t\t})\n\n\t\t\tif (this.reloadJsFilter) {\n\t\t\t\tthis.loadScripts(entry.scripts)\n\t\t\t}\n\n\t\t\tif (this.reloadCssFilter) {\n\t\t\t\tthis.loadStyles(entry.styles)\n\t\t\t}\n\n\t\t\t// If the fetched url had a redirect chain, then replace the history to reflect the final resolved URL\n\t\t\tif (trigger !== 'popstate' && url.href !== entry.finalUrl) {\n\t\t\t\twindow.history.replaceState({}, '', entry.finalUrl)\n\t\t\t}\n\n\t\t\tentry.renderer.enter(TransitionClass, trigger)\n\t\t\t\t.then(() => {\n\t\t\t\t\tE.emit('NAVIGATE_END', {\n\t\t\t\t\t\tfrom: this.currentCacheEntry,\n\t\t\t\t\t\tto: entry,\n\t\t\t\t\t\ttrigger\n\t\t\t\t\t})\n\n\t\t\t\t\tthis.currentCacheEntry = entry\n\t\t\t\t\tthis.isTransitioning = false\n\t\t\t\t\tthis.isPopping = false\n\t\t\t\t\tresolve()\n\t\t\t\t})\n\t\t})\n\t}\n\n\t/**\n\t * Load up scripts from the target page if needed\n\t *\n\t * @param {HTMLElement[]} cachedScripts\n\t */\n\tloadScripts(cachedScripts) {\n\t\tconst newScripts = [...cachedScripts]\n\t\tconst currentScripts = Array.from(document.querySelectorAll('script')).filter(this.reloadJsFilter)\n\n\t\t// loop through all new scripts\n\t\tfor (let i = 0; i < currentScripts.length; i++) {\n\t\t\tfor (let n = 0; n < newScripts.length; n++) {\n\t\t\t\tif (currentScripts[i].outerHTML === newScripts[n].outerHTML) {\n\t\t\t\t\treloadElement(currentScripts[i], 'SCRIPT')\n\t\t\t\t\tnewScripts.splice(n, 1)\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tfor (const script of newScripts) {\n\t\t\tappendElement(script, 'SCRIPT')\n\t\t}\n\t}\n\n\t/**\n\t * Load up styles from the target page if needed\n\t *\n\t * @param {Array} cachedStyles\n\t */\n\tloadStyles(cachedStyles) {\n\t\tconst currentStyles = Array.from(document.querySelectorAll('link[rel=\"stylesheet\"]')).filter(this.reloadCssFilter)\n\t\tconst currentInlineStyles = Array.from(document.querySelectorAll('style')).filter(this.reloadCssFilter)\n\n\t\tconst newInlineStyles = cachedStyles.filter(el => {\n\t\t\t// no el.href, assume it's an inline style\n\t\t\tif (!el.href) {\n\t\t\t\treturn true\n\t\t\t} else if (!currentStyles.find((link) => link.href === el.href)) {\n\t\t\t\tdocument.body.append(el)\n\t\t\t\treturn false\n\t\t\t}\n\t\t})\n\n\t\t// loop through all new inline styles\n\t\tfor (let i = 0; i < currentInlineStyles.length; i++) {\n\t\t\tfor (let n = 0; n < newInlineStyles.length; n++) {\n\t\t\t\tif (currentInlineStyles[i].outerHTML === newInlineStyles[n].outerHTML) {\n\t\t\t\t\treloadElement(currentInlineStyles[i], 'STYLE')\n\t\t\t\t\tnewInlineStyles.splice(n, 1)\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tfor (const style of newInlineStyles) {\n\t\t\tappendElement(style, 'STYLE')\n\t\t}\n\t}\n\n\t/**\n\t * @private\n\t * @param {string} links\n\t */\n\tattachEvents(links) {\n\t\tE.delegate('click', links, this.onClick)\n\t\tE.on('popstate', window, this.onPopstate)\n\n\t\tif (this.enablePrefetch) {\n\t\t\tE.delegate('mouseenter focus', links, this.onPrefetch)\n\t\t}\n\t}\n\n\t/**\n\t * @private\n\t * @param {MouseEvent} e\n\t */\n\tonClick = (e) => {\n\t\tif (!(e.metaKey || e.ctrlKey)) {\n\t\t\tconst target = processUrl(e.currentTarget.href)\n\t\t\tthis.currentLocation = processUrl(window.location.href)\n\n\t\t\tif (this.currentLocation.host !== target.host) {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\t// the target is a new URL, or is removing the hash from the current URL\n\t\t\tif (this.currentLocation.href !== target.href || (this.currentLocation.hasHash && !target.hasHash)) {\n\t\t\t\te.preventDefault()\n\t\t\t\t// noinspection JSIgnoredPromiseFromCall\n\t\t\t\tthis.navigateTo(target.raw, e.currentTarget.dataset.transition || false, e.currentTarget).catch(err => console.warn(err))\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\t// a click to the current URL was detected\n\t\t\tif (!this.currentLocation.hasHash && !target.hasHash) {\n\t\t\t\te.preventDefault()\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * @private\n\t * @return {void|boolean}\n\t */\n\tonPopstate = () => {\n\t\tconst target = processUrl(window.location.href)\n\n\t\t// don't trigger for on-page anchors\n\t\tif (\n\t\t\ttarget.pathname === this.currentLocation.pathname\n\t\t\t&& target.search === this.currentLocation.search\n\t\t\t&& !this.isPopping\n\t\t) {\n\t\t\treturn false\n\t\t}\n\n\t\tif (!this.allowInterruption && (this.isTransitioning || this.isPopping)) {\n\t\t\t// overwrite history state with current page if currently navigating\n\t\t\twindow.history.pushState({}, '', this.popTarget)\n\t\t\tconsole.warn(IN_PROGRESS)\n\t\t\treturn false\n\t\t}\n\n\t\tif (!this.isPopping) {\n\t\t\tthis.popTarget = window.location.href\n\t\t}\n\n\t\tthis.isPopping = true\n\n\t\t// noinspection JSIgnoredPromiseFromCall\n\t\tthis.navigateTo(window.location.href, false, 'popstate')\n\t}\n\n\t/**\n\t * @private\n\t * @param {MouseEvent} e\n\t */\n\tonPrefetch = (e) => {\n\t\tconst target = processUrl(e.currentTarget.href)\n\n\t\tif (this.currentLocation.host !== target.host) {\n\t\t\treturn\n\t\t}\n\n\t\tthis.preload(e.currentTarget.href, false)\n\t}\n\n\t/**\n\t * @private\n\t * @param {string} url\n\t * @param {boolean} [runFallback]\n\t * @return {Promise<{html: Document, url: string}>}\n\t */\n\tfetch(url, runFallback = true) {\n\t\t// If Taxi is currently performing a fetch for the given URL, return that instead of starting a new request\n\t\tif (this.activePromises.has(url)) {\n\t\t\treturn this.activePromises.get(url)\n\t\t}\n\n\t\tconst request = new Promise((resolve, reject) => {\n\t\t\tlet resolvedUrl\n\n\t\t\tfetch(url, {\n\t\t\t\tmode: 'same-origin',\n\t\t\t\tmethod: 'GET',\n\t\t\t\theaders: { 'X-Requested-With': 'Taxi' },\n\t\t\t\tcredentials: 'same-origin'\n\t\t\t})\n\t\t\t\t.then((response) => {\n\t\t\t\t\tif (!response.ok) {\n\t\t\t\t\t\treject('Taxi encountered a non 2xx HTTP status code')\n\n\t\t\t\t\t\tif (runFallback) {\n\t\t\t\t\t\t\twindow.location.href = url\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tresolvedUrl = response.url\n\n\t\t\t\t\treturn response.text()\n\t\t\t\t})\n\t\t\t\t.then((htmlString) => {\n\t\t\t\t\tresolve({ html: parseDom(htmlString), url: resolvedUrl })\n\t\t\t\t})\n\t\t\t\t.catch((err) => {\n\t\t\t\t\treject(err)\n\n\t\t\t\t\tif (runFallback) {\n\t\t\t\t\t\twindow.location.href = url\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t\t.finally(() => {\n\t\t\t\t\tthis.activePromises.delete(url)\n\t\t\t\t})\n\t\t})\n\n\t\tthis.activePromises.set(url, request)\n\n\t\treturn request\n\t}\n\n\t/**\n\t * @private\n\t * @param {string|false} transition\n\t * @return {Transition|function}\n\t */\n\tchooseTransition(transition) {\n\t\tif (transition) {\n\t\t\treturn this.transitions[transition]\n\t\t}\n\n\t\tconst routeTransition = this.router?.findMatch(this.currentLocation, this.targetLocation)\n\n\t\tif (routeTransition) {\n\t\t\treturn this.transitions[routeTransition]\n\t\t}\n\n\t\treturn this.defaultTransition\n\t}\n\n\t/**\n\t * @private\n\t * @param {Document|Node} page\n\t * @param {string} url\n\t * @return {CacheEntry}\n\t */\n\tcreateCacheEntry(page, url) {\n\t\tconst content = page.querySelector('[data-taxi-view]')\n\t\tconst Renderer = content.dataset.taxiView.length ? this.renderers[content.dataset.taxiView] : this.defaultRenderer\n\n\t\tif (!Renderer) {\n\t\t\tconsole.warn(`The Renderer \"${content.dataset.taxiView}\" was set in the data-taxi-view of the requested page, but not registered in Taxi.`)\n\t\t}\n\n\t\treturn {\n\t\t\tpage,\n\t\t\tcontent,\n\t\t\tfinalUrl: url,\n\t\t\tskipCache: content.hasAttribute('data-taxi-nocache'),\n\t\t\tscripts: this.reloadJsFilter ? Array.from(page.querySelectorAll('script')).filter(this.reloadJsFilter) : [],\n\t\t\tstyles: this.reloadCssFilter ? Array.from(page.querySelectorAll('link[rel=\"stylesheet\"], style')).filter(this.reloadCssFilter) : [],\n\t\t\ttitle: page.title,\n\t\t\trenderer: new Renderer({\n\t\t\t\twrapper: this.wrapper,\n\t\t\t\ttitle: page.title,\n\t\t\t\tcontent,\n\t\t\t\tpage\n\t\t\t})\n\t\t}\n\t}\n}\n"],"names":["parser","DOMParser","processUrl","url","details","URL","window","location","origin","normalized","hash","length","replace","hasHash","pathname","host","search","raw","href","reloadElement","node","elementType","parentNode","replaceChild","duplicateElement","appendElement","tagName","document","head","body","appendChild","replacement","createElement","k","attributes","attr","setAttribute","nodeName","nodeValue","innerHTML","Transition","constructor","wrapper","this","leave","props","Promise","resolve","onLeave","done","enter","onEnter","Renderer","content","page","title","_contentString","outerHTML","_DOM","lastElementChild","onEnterCompleted","onLeaveCompleted","initialLoad","update","firstElementChild","createDom","remove","transition","trigger","to","then","removeOldContent","from","RouteStore","data","Map","regexCache","add","fromPattern","toPattern","has","set","RegExp","get","findMatch","currentUrl","nextUrl","potentialMatches","match","IN_PROGRESS","Core","parameters","isTransitioning","currentCacheEntry","cache","activePromises","onClick","e","metaKey","ctrlKey","target","currentTarget","currentLocation","preventDefault","navigateTo","dataset","catch","err","console","warn","onPopstate","isPopping","allowInterruption","popTarget","history","pushState","onPrefetch","preload","links","bypassCache","enablePrefetch","renderers","default","transitions","reloadJsFilter","element","undefined","taxiReload","reloadCssFilter","defaultRenderer","defaultTransition","querySelector","attachEvents","createCacheEntry","cloneNode","renderer","setDefaultRenderer","setDefaultTransition","addRoute","router","preloadAssets","fetch","async","response","_this","html","updateCache","key","delete","clearCache","reject","Error","targetLocation","TransitionClass","chooseTransition","navigationPromise","skipCache","fetched","beforeFetch","_this2","afterFetch","on","event","callback","E","off","emit","entry","loadScripts","scripts","loadStyles","styles","finalUrl","replaceState","cachedScripts","newScripts","currentScripts","Array","querySelectorAll","filter","i","n","splice","script","cachedStyles","currentStyles","currentInlineStyles","newInlineStyles","el","find","link","append","style","delegate","runFallback","request","resolvedUrl","mode","method","headers","credentials","ok","text","htmlString","parseFromString","finally","routeTransition","_this$router","taxiView","hasAttribute"],"mappings":"2BAAA,MAAMA,EAAS,IAAIC,mBAkBHC,EAAWC,GAC1B,MAAMC,EAAU,IAAIC,IAAIF,EAAKG,OAAOC,SAASC,QACvCC,EAAaL,EAAQM,KAAKC,OAASR,EAAIS,QAAQR,EAAQM,KAAM,IAAM,KAEzE,MAAO,CACNG,QAAST,EAAQM,KAAKC,OAAS,EAC/BG,SAAUV,EAAQU,SAASF,QAAQ,OAAQ,IAC3CG,KAAMX,EAAQW,KACdC,OAAQZ,EAAQY,OAChBC,IAAKd,EACLe,KAAMT,GAAcL,EAAQc,KAE7B,UAQeC,EAAcC,EAAMC,GACnCD,EAAKE,WAAWC,aAAaC,EAAiBJ,EAAMC,GAAcD,EAClE,UAQeK,EAAcL,EAAMC,IACQ,SAA5BD,EAAKE,WAAWI,QAAqBC,SAASC,KAAOD,SAASE,MACtEC,YAAYN,EAAiBJ,EAAMC,GAC1C,UASeG,EAAiBJ,EAAMC,GACtC,MAAMU,EAAcJ,SAASK,cAAcX,GAE3C,IAAK,IAAIY,EAAI,EAAGA,EAAIb,EAAKc,WAAWvB,OAAQsB,IAAK,CAChD,MAAME,EAAOf,EAAKc,WAAWD,GAC7BF,EAAYK,aAAaD,EAAKE,SAAUF,EAAKG,UAC7C,CAOD,OAJIlB,EAAKmB,YACRR,EAAYQ,UAAYnB,EAAKmB,WAGvBR,CACP,uNC1EoBS,EAIpBC,aAAYC,QAAEA,IACbC,KAAKD,QAAUA,CACf,CAMDE,MAAMC,GACL,WAAWC,QAASC,IACnBJ,KAAKK,aAAaH,GAAOI,KAAMF,MAEhC,CAMDG,MAAML,GACL,WAAWC,QAASC,IACnBJ,KAAKQ,aAAaN,GAAOI,KAAMF,MAEhC,CAMDC,SAAQC,KAAiBA,IACxBA,GACA,CAMDE,SAAQF,KAAeA,IACtBA,GACA,QCxCmBG,EAIpBX,aAAYY,QAAEA,EAAFC,KAAWA,EAAXC,MAAiBA,EAAjBb,QAAwBA,IACnCC,KAAKa,eAAiBH,EAAQI,UAC9Bd,KAAKe,KAAO,KACZf,KAAKW,KAAOA,EACZX,KAAKY,MAAQA,EACbZ,KAAKD,QAAUA,EACfC,KAAKU,QAAUV,KAAKD,QAAQiB,gBAC5B,CAEDR,WAIAS,oBAIAZ,WAIAa,oBAIAC,cACCnB,KAAKQ,UACLR,KAAKiB,kBACL,CAEDG,SACCpC,SAAS4B,MAAQZ,KAAKY,MACtBZ,KAAKD,QAAQZ,YAAYa,KAAKe,KAAKM,mBACnCrB,KAAKU,QAAUV,KAAKD,QAAQiB,iBAC5BhB,KAAKe,KAAO,IACZ,CAEDO,YACMtB,KAAKe,OACTf,KAAKe,KAAO/B,SAASK,cAAc,OACnCW,KAAKe,KAAKnB,UAAYI,KAAKa,eAE5B,CAEDU,SACCvB,KAAKD,QAAQsB,kBAAkBE,QAC/B,CAQDhB,MAAMiB,EAAYC,GACjB,WAAWtB,QAASC,IACnBJ,KAAKQ,UAELgB,EAAWjB,MAAM,CAAEkB,UAASC,GAAI1B,KAAKU,UACnCiB,KAAK,KACL3B,KAAKiB,mBACLb,OAGH,CASDH,MAAMuB,EAAYC,EAASG,GAC1B,WAAWzB,QAASC,IACnBJ,KAAKK,UAELmB,EAAWvB,MAAM,CAAEwB,UAASI,KAAM7B,KAAKU,UACrCiB,KAAK,KACDC,GACH5B,KAAKuB,SAGNvB,KAAKkB,mBACLd,OAGH,QC7FmB0B,qBAIpBC,KAAO,IAAIC,SAKXC,WAAa,IAAID,GATc,CAiB/BE,IAAIC,EAAaC,EAAWZ,GACtBxB,KAAK+B,KAAKM,IAAIF,KAClBnC,KAAK+B,KAAKO,IAAIH,EAAa,IAAIH,KAC/BhC,KAAKiC,WAAWK,IAAIH,EAAa,IAAII,OAAQ,IAAGJ,QAGjDnC,KAAK+B,KAAKS,IAAIL,GAAaG,IAAIF,EAAWZ,GAC1CxB,KAAKiC,WAAWK,IAAIF,EAAW,IAAIG,OAAQ,IAAGH,MAC9C,CAQDK,UAAUC,EAAYC,GAErB,IAAK,MAAOR,EAAaS,UAA0Bb,KAElD,GAAIW,EAAWvE,SAAS0E,MAAM7C,KAAKiC,WAAWO,IAAIL,IAAe,CAEhE,IAAK,MAAOC,EAAWZ,KAAeoB,EAErC,GAAID,EAAQxE,SAAS0E,MAAM7C,KAAKiC,WAAWO,IAAIJ,IAC9C,OAAOZ,EAIT,KACA,CAGF,WACA,EC7CF,MAAMsB,EAAc,8CAeCC,EAgCpBjD,YAAYkD,EAAa,SA/BzBC,iBAAkB,OAKlBC,kBAAoB,UAKpBC,MAAQ,IAAInB,SAMZoB,eAAiB,IAAIpB,SAkXrBqB,QAAWC,IACV,IAAMA,EAAEC,UAAWD,EAAEE,QAAU,CAC9B,MAAMC,EAASlG,EAAW+F,EAAEI,cAAcnF,MAG1C,GAFAyB,KAAK2D,gBAAkBpG,EAAWI,OAAOC,SAASW,MAE9CyB,KAAK2D,gBAAgBvF,OAASqF,EAAOrF,KACxC,OAID,GAAI4B,KAAK2D,gBAAgBpF,OAASkF,EAAOlF,MAASyB,KAAK2D,gBAAgBzF,UAAYuF,EAAOvF,QAIzF,OAHAoF,EAAEM,sBAEF5D,KAAK6D,WAAWJ,EAAOnF,IAAKgF,EAAEI,cAAcI,QAAQtC,aAAc,EAAO8B,EAAEI,eAAeK,MAAMC,GAAOC,QAAQC,KAAKF,IAKhHhE,KAAK2D,gBAAgBzF,SAAYuF,EAAOvF,SAC5CoF,EAAEM,gBAEH,QAOFO,WAAa,KACZ,MAAMV,EAASlG,EAAWI,OAAOC,SAASW,MAG1C,QACCkF,EAAOtF,WAAa6B,KAAK2D,gBAAgBxF,UACtCsF,EAAOpF,SAAW2B,KAAK2D,gBAAgBtF,SACtC2B,KAAKoE,aAKLpE,KAAKqE,oBAAsBrE,KAAKiD,kBAAmBjD,KAAKoE,WAOxDpE,KAAKoE,YACTpE,KAAKsE,UAAY3G,OAAOC,SAASW,MAGlCyB,KAAKoE,WAAY,OAGjBpE,KAAK6D,WAAWlG,OAAOC,SAASW,MAAM,EAAO,cAZ5CZ,OAAO4G,QAAQC,UAAU,GAAI,GAAIxE,KAAKsE,WACtCL,QAAQC,KAAKpB,cAkBf2B,WAAcnB,IACb,MAAMG,EAASlG,EAAW+F,EAAEI,cAAcnF,MAEtCyB,KAAK2D,gBAAgBvF,OAASqF,EAAOrF,MAIzC4B,KAAK0E,QAAQpB,EAAEI,cAAcnF,MAAM,IAtanC,MAAMoG,MACLA,EAAQ,iEADH/C,iBAELA,GAAmB,EAFdyC,kBAGLA,GAAoB,EAHfO,YAILA,GAAc,EAJTC,eAKLA,GAAiB,EALZC,UAMLA,EAAY,CACXC,QAAStE,GAPLuE,YASLA,EAAc,CACbD,QAASlF,GAVLoF,eAYLA,EAAkBC,SAA2CC,IAA/BD,EAAQpB,QAAQsB,YAZzCC,gBAaLA,EAAmBH,KAAY,IAC5BlC,EAEJhD,KAAK8E,UAAYA,EACjB9E,KAAKgF,YAAcA,EACnBhF,KAAKsF,gBAAkBtF,KAAK8E,UAAUC,SAAWtE,EACjDT,KAAKuF,kBAAoBvF,KAAKgF,YAAYD,SAAWlF,EACrDG,KAAKD,QAAUf,SAASwG,cAAc,eACtCxF,KAAKiF,eAAiBA,EACtBjF,KAAKqF,gBAAkBA,EACvBrF,KAAK4B,iBAAmBA,EACxB5B,KAAKqE,kBAAoBA,EACzBrE,KAAK4E,YAAcA,EACnB5E,KAAK6E,eAAiBA,EACtB7E,KAAKmD,MAAQ,IAAInB,IACjBhC,KAAKoE,WAAY,EAGjBpE,KAAKyF,aAAad,GAElB3E,KAAK2D,gBAAkBpG,EAAWI,OAAOC,SAASW,MAGlDyB,KAAKmD,MAAMb,IAAItC,KAAK2D,gBAAgBpF,KAAMyB,KAAK0F,iBAAiB1G,SAAS2G,WAAU,GAAOhI,OAAOC,SAASW,OAG1GyB,KAAKkD,kBAAoBlD,KAAKmD,MAAMX,IAAIxC,KAAK2D,gBAAgBpF,MAC7DyB,KAAKkD,kBAAkB0C,SAASzE,aAChC,CAKD0E,mBAAmBD,GAClB5F,KAAKsF,gBAAkBtF,KAAK8E,UAAUc,EACtC,CAKDE,qBAAqBtE,GACpBxB,KAAKuF,kBAAoBvF,KAAKgF,YAAYxD,EAC1C,CASDuE,SAAS5D,EAAaC,EAAWZ,GAC3BxB,KAAKgG,SACThG,KAAKgG,OAAS,IAAIlE,GAGnB9B,KAAKgG,OAAO9D,IAAIC,EAAaC,EAAWZ,EACxC,CASDkD,QAAQlH,EAAKyI,GAAgB,cAI5B,OAFAzI,EAAMD,EAAWC,GAAKe,KAEjByB,KAAKmD,MAAMd,IAAI7E,GAYb2C,QAAQC,eAXF8F,MAAM1I,GAAK,GACrBmE,KAAKwE,eAAOC,GACZC,EAAKlD,MAAMb,IAAI9E,EAAK6I,EAAKX,iBAAiBU,EAASE,KAAMF,EAAS5I,MAE9DyI,GACHI,EAAKlD,MAAMX,IAAIhF,GAAKoI,SAAStE,WAE9B,GACAyC,MAAMC,GAAOC,QAAQC,KAAKF,GAI7B,CASDuC,YAAY/I,GACX,MAAMgJ,EAAMjJ,EAAWC,GAAOG,OAAOC,SAASW,MAAMA,KAEhDyB,KAAKmD,MAAMd,IAAImE,IAClBxG,KAAKmD,MAAMsD,OAAOD,GAGnBxG,KAAKmD,MAAMb,IAAIkE,EAAKxG,KAAK0F,iBAAiB1G,SAAS2G,WAAU,GAAOa,GACpE,CAQDE,WAAWlJ,GACV,MAAMgJ,EAAMjJ,EAAWC,GAAOG,OAAOC,SAASW,MAAMA,KAEhDyB,KAAKmD,MAAMd,IAAImE,IAClBxG,KAAKmD,MAAMsD,OAAOD,EAEnB,CAQD3C,WAAWrG,EAAKgE,GAAa,EAAOC,GAAU,cAC7C,WAAWtB,QAAQ,CAACC,EAASuG,KAE5B,IAAK3G,KAAKqE,mBAAqBrE,KAAKiD,gBAEnC,YADA0D,EAAO,IAAIC,MAAM9D,IAIlB9C,KAAKiD,iBAAkB,EACvBjD,KAAKoE,WAAY,EACjBpE,KAAK6G,eAAiBtJ,EAAWC,GACjCwC,KAAKsE,UAAY3G,OAAOC,SAASW,KAEjC,MAAMuI,EAAkB,IAAK9G,KAAK+G,iBAAiBvF,GAA3B,CAAwC,CAAEzB,QAASC,KAAKD,UAEhF,IAAIiH,EAEJ,GAAIhH,KAAK4E,cAAgB5E,KAAKmD,MAAMd,IAAIrC,KAAK6G,eAAetI,OAASyB,KAAKmD,MAAMX,IAAIxC,KAAK6G,eAAetI,MAAM0I,UAAW,CACxH,MAAMC,EAAUlH,KAAKkG,MAAMlG,KAAK6G,eAAetI,MAC7CoD,KAAMyE,IACNpG,KAAKmD,MAAMb,IAAItC,KAAK6G,eAAetI,KAAMyB,KAAK0F,iBAAiBU,EAASE,KAAMF,EAAS5I,MACvFwC,KAAKmD,MAAMX,IAAIxC,KAAK6G,eAAetI,MAAMqH,SAAStE,cAElDyC,MAAMC,IAENrG,OAAOC,SAASW,KAAOf,IAGzBwJ,EAAoBhH,KAAKmH,YAAYnH,KAAK6G,eAAgBC,EAAiBrF,GACzEE,KAAKwE,iBACL,OAAOe,EAAQvF,KAAKwE,iBACnB,aAAaiB,EAAKC,WAAWD,EAAKP,eAAgBC,EAAiBM,EAAKjE,MAAMX,IAAI4E,EAAKP,eAAetI,MAAOkD,EAC7G,EACD,EACF,MACAzB,KAAKmD,MAAMX,IAAIxC,KAAK6G,eAAetI,MAAMqH,SAAStE,YAElD0F,EAAoBhH,KAAKmH,YAAYnH,KAAK6G,eAAgBC,EAAiBrF,GACzEE,KAAKwE,iBACL,aAAaiB,EAAKC,WAAWD,EAAKP,eAAgBC,EAAiBM,EAAKjE,MAAMX,IAAI4E,EAAKP,eAAetI,MAAOkD,EAC7G,GAGHuF,EAAkBrF,KAAK,KACtBvB,OAGF,CAODkH,GAAGC,EAAOC,GACTC,EAAEH,GAAGC,EAAOC,EACZ,CAODE,IAAIH,EAAOC,GACVC,EAAEC,IAAIH,EAAOC,EACb,CASDL,YAAY3J,EAAKsJ,EAAiBrF,GAMjC,OALAgG,EAAEE,KAAK,eAAgB,CACtB9F,KAAM7B,KAAKkD,kBACXzB,gBAGUtB,QAASC,IACnBJ,KAAKkD,kBAAkB0C,SAAS3F,MAAM6G,EAAiBrF,EAASzB,KAAK4B,kBACnED,KAAK,KACW,aAAZF,GACH9D,OAAO4G,QAAQC,UAAU,GAAI,GAAIhH,EAAIc,KAGtC8B,OAGH,CAUDiH,WAAW7J,EAAKsJ,EAAiBc,EAAOnG,GAIvC,OAHAzB,KAAK2D,gBAAkBnG,EACvBwC,KAAKsE,UAAYtE,KAAK2D,gBAAgBpF,SAE3B4B,QAASC,IACnBwH,EAAMhC,SAASxE,SAEfqG,EAAEE,KAAK,cAAe,CACrB9F,KAAM7B,KAAKkD,kBACXxB,GAAIkG,EACJnG,YAGGzB,KAAKiF,gBACRjF,KAAK6H,YAAYD,EAAME,SAGpB9H,KAAKqF,iBACRrF,KAAK+H,WAAWH,EAAMI,QAIP,aAAZvG,GAA0BjE,EAAIe,OAASqJ,EAAMK,UAChDtK,OAAO4G,QAAQ2D,aAAa,GAAI,GAAIN,EAAMK,UAG3CL,EAAMhC,SAASrF,MAAMuG,EAAiBrF,GACpCE,KAAK,KACL8F,EAAEE,KAAK,eAAgB,CACtB9F,KAAM7B,KAAKkD,kBACXxB,GAAIkG,EACJnG,YAGDzB,KAAKkD,kBAAoB0E,EACzB5H,KAAKiD,iBAAkB,EACvBjD,KAAKoE,WAAY,EACjBhE,OAGH,CAODyH,YAAYM,GACX,MAAMC,EAAa,IAAID,GACjBE,EAAiBC,MAAMzG,KAAK7C,SAASuJ,iBAAiB,WAAWC,OAAOxI,KAAKiF,gBAGnF,IAAK,IAAIwD,EAAI,EAAGA,EAAIJ,EAAerK,OAAQyK,IAC1C,IAAK,IAAIC,EAAI,EAAGA,EAAIN,EAAWpK,OAAQ0K,IACtC,GAAIL,EAAeI,GAAG3H,YAAcsH,EAAWM,GAAG5H,UAAW,CAC5DtC,EAAc6J,EAAeI,GAAI,UACjCL,EAAWO,OAAOD,EAAG,GACrB,KACA,CAIH,IAAK,MAAME,KAAUR,EACpBtJ,EAAc8J,EAAQ,SAEvB,CAODb,WAAWc,GACV,MAAMC,EAAgBR,MAAMzG,KAAK7C,SAASuJ,iBAAiB,2BAA2BC,OAAOxI,KAAKqF,iBAC5F0D,EAAsBT,MAAMzG,KAAK7C,SAASuJ,iBAAiB,UAAUC,OAAOxI,KAAKqF,iBAEjF2D,EAAkBH,EAAaL,OAAOS,IAEtCA,EAAG1K,OAEIuK,EAAcI,KAAMC,GAASA,EAAK5K,OAAS0K,EAAG1K,cACzDS,SAASE,KAAKkK,OAAOH,SAMvB,IAAK,IAAIR,EAAI,EAAGA,EAAIM,EAAoB/K,OAAQyK,IAC/C,IAAK,IAAIC,EAAI,EAAGA,EAAIM,EAAgBhL,OAAQ0K,IAC3C,GAAIK,EAAoBN,GAAG3H,YAAckI,EAAgBN,GAAG5H,UAAW,CACtEtC,EAAcuK,EAAoBN,GAAI,SACtCO,EAAgBL,OAAOD,EAAG,GAC1B,KACA,CAIH,IAAK,MAAMW,KAASL,EACnBlK,EAAcuK,EAAO,QAEtB,CAMD5D,aAAad,GACZ8C,EAAE6B,SAAS,QAAS3E,EAAO3E,KAAKqD,SAChCoE,EAAEH,GAAG,WAAY3J,OAAQqC,KAAKmE,YAE1BnE,KAAK6E,gBACR4C,EAAE6B,SAAS,mBAAoB3E,EAAO3E,KAAKyE,WAE5C,CAmFDyB,MAAM1I,EAAK+L,GAAc,GAExB,GAAIvJ,KAAKoD,eAAef,IAAI7E,GAC3B,YAAY4F,eAAeZ,IAAIhF,GAGhC,MAAMgM,EAAU,IAAIrJ,QAAQ,CAACC,EAASuG,KACrC,IAAI8C,EAEJvD,MAAM1I,EAAK,CACVkM,KAAM,cACNC,OAAQ,MACRC,QAAS,CAAE,mBAAoB,QAC/BC,YAAa,gBAEZlI,KAAMyE,IACDA,EAAS0D,KACbnD,EAAO,+CAEH4C,IACH5L,OAAOC,SAASW,KAAOf,IAIzBiM,EAAcrD,EAAS5I,IAEhB4I,EAAS2D,SAEhBpI,KAAMqI,QJzfc1D,EI0fpBlG,EAAQ,CAAEkG,MJ1fUA,EI0fK0D,EJzfN,iBAAT1D,EAAoBjJ,EAAO4M,gBAAgB3D,EAAM,aAAeA,GIyfpC9I,IAAKiM,MAE3C1F,MAAOC,IACP2C,EAAO3C,GAEHuF,IACH5L,OAAOC,SAASW,KAAOf,EACvB,GAED0M,QAAQ,KACRlK,KAAKoD,eAAeqD,OAAOjJ,OAM9B,OAFAwC,KAAKoD,eAAed,IAAI9E,EAAKgM,GAEtBA,CACP,CAODzC,iBAAiBvF,SAChB,GAAIA,EACH,YAAYwD,YAAYxD,GAGzB,MAAM2I,WAAkBnK,KAAKgG,eAALoE,EAAa3H,UAAUzC,KAAK2D,gBAAiB3D,KAAK6G,gBAE1E,OAAIsD,OACSnF,YAAYmF,QAGb5E,iBACZ,CAQDG,iBAAiB/E,EAAMnD,GACtB,MAAMkD,EAAUC,EAAK6E,cAAc,oBAC7B/E,EAAWC,EAAQoD,QAAQuG,SAASrM,OAASgC,KAAK8E,UAAUpE,EAAQoD,QAAQuG,UAAYrK,KAAKsF,gBAMnG,OAJK7E,GACJwD,QAAQC,KAAM,iBAAgBxD,EAAQoD,QAAQuG,8FAGxC,CACN1J,OACAD,UACAuH,SAAUzK,EACVyJ,UAAWvG,EAAQ4J,aAAa,qBAChCxC,QAAS9H,KAAKiF,eAAiBqD,MAAMzG,KAAKlB,EAAK4H,iBAAiB,WAAWC,OAAOxI,KAAKiF,gBAAkB,GACzG+C,OAAQhI,KAAKqF,gBAAkBiD,MAAMzG,KAAKlB,EAAK4H,iBAAiB,kCAAkCC,OAAOxI,KAAKqF,iBAAmB,GACjIzE,MAAOD,EAAKC,MACZgF,SAAU,IAAInF,EAAS,CACtBV,QAASC,KAAKD,QACda,MAAOD,EAAKC,MACZF,UACAC,SAGF"}
--------------------------------------------------------------------------------
/dist/taxi.umd.js:
--------------------------------------------------------------------------------
1 | !function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports,require("@unseenco/e")):"function"==typeof define&&define.amd?define(["exports","@unseenco/e"],t):t((e||self).taxi={},e.E)}(this,function(e,t){function r(e){return e&&"object"==typeof e&&"default"in e?e:{default:e}}var n=/*#__PURE__*/r(t);function i(){return i=Object.assign||function(e){for(var t=1;te.length)&&(t=e.length);for(var r=0,n=new Array(t);r=e.length?{done:!0}:{done:!1,value:e[n++]}}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var c=new DOMParser;function s(e){var t=new URL(e,window.location.origin),r=t.hash.length?e.replace(t.hash,""):null;return{hasHash:t.hash.length>0,pathname:t.pathname.replace(/\/+$/,""),host:t.host,search:t.search,raw:e,href:r||t.href}}function h(e,t){e.parentNode.replaceChild(l(e,t),e)}function u(e,t){("HEAD"===e.parentNode.tagName?document.head:document.body).appendChild(l(e,t))}function l(e,t){for(var r=document.createElement(t),n=0;n
2 |
3 |
4 |
5 |
6 | Taxi.js - {{ title }}
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 | {% include "nav.njk" %}
68 |
69 |
70 |
71 |
72 |
73 | {% block content %}{% endblock %}
74 | {{ content | safe }}
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
--------------------------------------------------------------------------------
/docs/_includes/nav.njk:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
8 |
9 |
29 |
30 |
50 |
51 |
52 |
53 |
54 |
55 | {% for nav in nav.pages %}
56 |
57 | {{ nav.name }}
58 |
59 | {% endfor %}
60 |
61 |
62 |
63 |
64 |
86 |
--------------------------------------------------------------------------------
/docs/_public/.nojekyll:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/craftedbygc/taxi/6785cd56f1f843401a12b867ce0b01425af4c0bb/docs/_public/.nojekyll
--------------------------------------------------------------------------------
/docs/_public/CNAME:
--------------------------------------------------------------------------------
1 | taxi.js.org
--------------------------------------------------------------------------------
/docs/_public/android-chrome-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/craftedbygc/taxi/6785cd56f1f843401a12b867ce0b01425af4c0bb/docs/_public/android-chrome-192x192.png
--------------------------------------------------------------------------------
/docs/_public/android-chrome-384x384.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/craftedbygc/taxi/6785cd56f1f843401a12b867ce0b01425af4c0bb/docs/_public/android-chrome-384x384.png
--------------------------------------------------------------------------------
/docs/_public/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/craftedbygc/taxi/6785cd56f1f843401a12b867ce0b01425af4c0bb/docs/_public/apple-touch-icon.png
--------------------------------------------------------------------------------
/docs/_public/browserconfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | #da532c
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/docs/_public/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/craftedbygc/taxi/6785cd56f1f843401a12b867ce0b01425af4c0bb/docs/_public/favicon-16x16.png
--------------------------------------------------------------------------------
/docs/_public/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/craftedbygc/taxi/6785cd56f1f843401a12b867ce0b01425af4c0bb/docs/_public/favicon-32x32.png
--------------------------------------------------------------------------------
/docs/_public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/craftedbygc/taxi/6785cd56f1f843401a12b867ce0b01425af4c0bb/docs/_public/favicon.ico
--------------------------------------------------------------------------------
/docs/_public/fonts/Inter-Bold.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/craftedbygc/taxi/6785cd56f1f843401a12b867ce0b01425af4c0bb/docs/_public/fonts/Inter-Bold.woff
--------------------------------------------------------------------------------
/docs/_public/fonts/Inter-Bold.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/craftedbygc/taxi/6785cd56f1f843401a12b867ce0b01425af4c0bb/docs/_public/fonts/Inter-Bold.woff2
--------------------------------------------------------------------------------
/docs/_public/fonts/Inter-Regular.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/craftedbygc/taxi/6785cd56f1f843401a12b867ce0b01425af4c0bb/docs/_public/fonts/Inter-Regular.woff
--------------------------------------------------------------------------------
/docs/_public/fonts/Inter-Regular.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/craftedbygc/taxi/6785cd56f1f843401a12b867ce0b01425af4c0bb/docs/_public/fonts/Inter-Regular.woff2
--------------------------------------------------------------------------------
/docs/_public/fonts/NeueMontreal-Regular.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/craftedbygc/taxi/6785cd56f1f843401a12b867ce0b01425af4c0bb/docs/_public/fonts/NeueMontreal-Regular.woff
--------------------------------------------------------------------------------
/docs/_public/fonts/NeueMontreal-Regular.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/craftedbygc/taxi/6785cd56f1f843401a12b867ce0b01425af4c0bb/docs/_public/fonts/NeueMontreal-Regular.woff2
--------------------------------------------------------------------------------
/docs/_public/mstile-150x150.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/craftedbygc/taxi/6785cd56f1f843401a12b867ce0b01425af4c0bb/docs/_public/mstile-150x150.png
--------------------------------------------------------------------------------
/docs/_public/site.webmanifest:
--------------------------------------------------------------------------------
1 | {
2 | "name": "",
3 | "short_name": "",
4 | "icons": [
5 | {
6 | "src": "/android-chrome-192x192.png",
7 | "sizes": "192x192",
8 | "type": "image/png"
9 | },
10 | {
11 | "src": "/android-chrome-384x384.png",
12 | "sizes": "384x384",
13 | "type": "image/png"
14 | }
15 | ],
16 | "theme_color": "#ffffff",
17 | "background_color": "#ffffff",
18 | "display": "standalone"
19 | }
20 |
--------------------------------------------------------------------------------
/docs/_public/social.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/craftedbygc/taxi/6785cd56f1f843401a12b867ce0b01425af4c0bb/docs/_public/social.jpg
--------------------------------------------------------------------------------
/docs/api-events.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: layouts/base.njk
3 | title: API and Events Reference
4 | ---
5 |
6 | # API
7 |
8 | ## addRoute()
9 | Registers a route into the RouteStore.
10 |
11 | ```js
12 | /**
13 | * addRoute(fromPattern: string, toPattern: string, transition: string): void
14 | */
15 | taxi.addRoute('/blog/.*', '/', 'blogToHome')
16 | ```
17 |
18 | ## navigateTo()
19 | Perform a manual navigation to the provided URL.
20 |
21 | If a `transition` name is not provided then Taxi will try and find a match in the RouteStore, otherwise the default transition will be used.
22 |
23 | ```js
24 | /**
25 | * navigateTo(url: string, transition?: string = false): Promise
26 | */
27 | taxi.navigateTo('/contact')
28 |
29 | taxi.navigateTo('/contact', 'explcitTransition').then(() => { ... })
30 | ```
31 |
32 | ## preload()
33 | Prefetch the provided URL and add it to the cache ahead of any user navigation.
34 |
35 | ```js
36 | /**
37 | * preload( url: string, preloadAssets?: boolean = false): Promise
38 | */
39 | taxi.preload('/path/to/preload')
40 | ```
41 |
42 | You can pass a second argument to indicate you want to preload the assets on the target URL as well (images, media, etc):
43 | ```js
44 | taxi.preload('/path/to/preload', true)
45 | ```
46 |
47 |
48 | As `preload` returns a promise, you can also run code based on whether the fetch was a success or not:
49 |
50 | ```js
51 | taxi.preload('/path/to/404')
52 | .then(() => console.log('success!'))
53 | .catch(err => console.warn(err))
54 | ```
55 |
56 | ## updateCache()
57 | Updates the cached HTML for the provided URL. If no URL is provided, update cache for the current URL.
58 |
59 | Useful when adding/removing content via AJAX such as a search page or infinite scroll.
60 |
61 | ```js
62 | /**
63 | * updateCache(url?: string): void
64 | */
65 | taxi.updateCache()
66 | ```
67 |
68 |
69 | ## clearCache()
70 | Remove the cached HTML for the provided URL. If no URL is provided, remove cache for the current URL.
71 |
72 | ```js
73 | /**
74 | * clearCache(url?: string): void
75 | */
76 | taxi.clearCache('/path/to/delete')
77 | ```
78 |
79 | ## setDefaultRenderer()
80 | If you don't like "default" as the name of your default renderer, you can change the default renderer to be anything you like here.
81 |
82 | ```js
83 | /**
84 | * setDefaultRenderer(renderer: string): void
85 | */
86 | taxi.setDefaultRenderer('myRenderer')
87 | ```
88 |
89 | ## setDefaultTransition()
90 | Same as `setDefaultRenderer`, but for the transitions instead.
91 | ```js
92 | /**
93 | * setDefaultTransition(transition: string): void
94 | */
95 | taxi.setDefaultTransition('myTransition')
96 | ```
97 |
98 |
99 |
100 | ## Events
101 | Events are handled by [@unseenco/e](https://www.npmjs.com/package/@unseenco/e).
102 |
103 | ### Adding Listeners
104 | ```js
105 | import { Core } from '@unseenco/taxi'
106 |
107 | const taxi = new Core({ ... })
108 |
109 | // This event is sent everytime a `data-taxi-view` is added to the DOM
110 | taxi.on('NAVIGATE_IN', ({ to, trigger }) => {
111 | // ...
112 | })
113 |
114 | // This event is sent before the `onLeave()` method of a transition is run to hide a `data-router-view`
115 | taxi.on('NAVIGATE_OUT', ({ from, trigger }) => {
116 | // ...
117 | })
118 |
119 | // This event is sent everytime the `done()` method is called in the `onEnter()` method of a transition
120 | taxi.on('NAVIGATE_END', ({ to, from, trigger }) => {
121 | // ...
122 | })
123 | ```
124 |
125 | ### Removing Listeners
126 | You can call `taxi.off(event_name)` to remove all listeners for an event, or pass the callback to remove just that listener instead:
127 |
128 | ```js
129 | function foo() {
130 | ...
131 | }
132 |
133 | taxi.on('NAVIGATE_OUT', foo)
134 |
135 | // Remove just the foo listener
136 | taxi.off('NAVIGATE_OUT', foo)
137 |
138 | // Remove all listeners
139 | taxi.off('NAVIGATE_IN')
140 | ```
--------------------------------------------------------------------------------
/docs/assets/js/index.js:
--------------------------------------------------------------------------------
1 | import gsap from 'gsap'
2 | import E from '@unseenco/e'
3 | import { Core } from '../../../src/taxi'
4 | import DefaultRenderer from './renderers/DefaultRenderer'
5 | import DefaultTransition from './transitions/DefaultTransition'
6 |
7 | E.on('DOMContentLoaded', window, function () {
8 | const taxi = new Core({
9 | renderers: {
10 | default: DefaultRenderer
11 | },
12 | transitions: {
13 | default: DefaultTransition
14 | }
15 | })
16 |
17 | const navItems = document.querySelectorAll('.js-nav li')
18 | const menuToggle = document.querySelector('.js-menu-toggle')
19 | const menu = document.getElementById('main-menu')
20 |
21 | taxi.on('NAVIGATE_OUT', () => {
22 | navItems.forEach(el => {
23 | el.classList.remove('active')
24 | })
25 |
26 | // close menu if open
27 | menuToggle.setAttribute('aria-expanded', 'false')
28 |
29 | gsap.to(menu, { x: () => -menu.clientWidth })
30 | .then(() => {
31 | menu.classList.add('invisible')
32 | })
33 | })
34 |
35 | taxi.on('NAVIGATE_IN', ({ to }) => {
36 | window.scrollTo(0, 0)
37 |
38 | const next = to.page.querySelector('.js-nav li.active a')
39 |
40 | if (next) {
41 | navItems.forEach(el => {
42 | if (el.firstElementChild.href === next.href) {
43 | el.classList.add('active')
44 | }
45 | })
46 | }
47 | })
48 |
49 | if (localStorage.theme === 'dark' || (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
50 | document.documentElement.classList.add('dark')
51 | } else {
52 | document.documentElement.classList.remove('dark')
53 | }
54 |
55 | E.on('click', '.js-theme-toggle', () => {
56 | if (document.documentElement.classList.contains('dark')) {
57 | document.documentElement.classList.remove('dark')
58 | localStorage.setItem('theme', 'light')
59 | } else {
60 | document.documentElement.classList.add('dark')
61 | localStorage.setItem('theme', 'dark')
62 | }
63 | })
64 |
65 | // Mobile menu
66 | E.on('click', menuToggle, () => {
67 | const current = menuToggle.getAttribute('aria-expanded')
68 |
69 | if (current === 'false') {
70 | menuToggle.setAttribute('aria-expanded', 'true')
71 | menu.classList.remove('invisible')
72 | gsap.to(menu, { x: 0 })
73 | } else {
74 | menuToggle.setAttribute('aria-expanded', 'false')
75 |
76 | gsap.to(menu, { x: () => -menu.clientWidth })
77 | .then(() => {
78 | menu.classList.add('invisible')
79 | })
80 | }
81 | })
82 |
83 | //star count
84 | const starcount = document.querySelector('.js-stars')
85 | const version = document.querySelector('.js-version')
86 |
87 | fetch('https://api.github.com/repos/craftedbygc/taxi')
88 | .then((response) => response.json())
89 | .then((data) => {
90 | if (!data.message) {
91 | starcount.innerHTML = convertStars(data.stargazers_count)
92 | } else {
93 | throw new Error('GitHub API hates me')
94 | }
95 | })
96 | .catch((error) => {
97 | console.error('Error:', error);
98 | starcount.parentElement.remove()
99 | })
100 |
101 | fetch('https://registry.npmjs.org/@unseenco/taxi/latest')
102 | .then((response) => response.json())
103 | .then((data) => {
104 | if (data.version) {
105 | version.innerHTML = `v${data.version}`
106 | }
107 | })
108 | .catch((error) => {
109 | console.error('Error:', error);
110 | })
111 | })
112 |
113 | function convertStars(amount) {
114 | return Math.abs(amount) > 999
115 | ? Math.sign(amount) * (Math.abs(amount) / 1000).toFixed(1) + "k"
116 | : Math.sign(amount) * Math.abs(amount);
117 | }
118 |
--------------------------------------------------------------------------------
/docs/assets/js/renderers/DefaultRenderer.js:
--------------------------------------------------------------------------------
1 | import { Renderer } from '../../../../src/taxi'
2 | import Prism from 'prismjs';
3 |
4 | export default class DefaultRenderer extends Renderer {
5 | onEnter() {
6 | Prism.manual = true;
7 | Prism.highlightAll()
8 | }
9 |
10 | onEnterCompleted() {
11 | }
12 |
13 | onLeave() {
14 | }
15 |
16 | onLeaveCompleted() {
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/docs/assets/js/transitions/DefaultTransition.js:
--------------------------------------------------------------------------------
1 | import { Transition } from '../../../../src/taxi'
2 | import gsap from 'gsap'
3 |
4 | export default class DefaultTransition extends Transition {
5 | onLeave({ from, trigger, done }) {
6 | const overlay = document.querySelector('.js-overlay')
7 |
8 | gsap.timeline()
9 | .to(this.wrapper, { opacity: 0 }, 0)
10 | .to(overlay, { width: '100%', ease: 'power3.in', duration: .8 }, 0)
11 | .then(() => {
12 | done()
13 | })
14 | }
15 |
16 | onEnter({ to, trigger, done }) {
17 | const overlay = document.querySelector('.js-overlay')
18 | const tail = document.querySelector('.js-overlay-tail')
19 |
20 | gsap.timeline()
21 | .set(tail, {width: '100%'})
22 | .to(overlay, { x: '100%', ease: 'power3.out', duration: .85 }, 0)
23 | .to(tail, { x: '100%', ease: 'power3.out', duration: .85 }, .15)
24 | .to(this.wrapper, { opacity: 1 }, .3)
25 | .then(() => {
26 | gsap.set(tail, { width: 0, x: 0 })
27 | gsap.set(overlay, { width: 0, x: 0 })
28 | done()
29 | })
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/docs/assets/sass/index.scss:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | @import 'prism';
6 |
7 | @layer base {
8 | code:not(pre code) {
9 | @apply bg-gray-100 px-1 border rounded-sm;
10 |
11 | .dark & {
12 | @apply bg-code border-0;
13 | }
14 | }
15 |
16 | pre:not([class]) {
17 | @apply bg-code border rounded block text-gray-200 text-base;
18 | }
19 |
20 | h1, h2, h3 {
21 | @apply font-neue relative;
22 | scroll-margin-top: 6rem;
23 | }
24 |
25 | .active a {
26 | @apply translate-x-[7px];
27 | }
28 |
29 | .code-toolbar {
30 | @apply relative;
31 |
32 | .toolbar {
33 | @apply absolute top-2 right-2 text-sm bg-slate-700 text-white opacity-0 transition-opacity py-0.5 px-1 rounded shadow;
34 | }
35 |
36 | &:hover {
37 | .toolbar {
38 | @apply opacity-100;
39 | }
40 | }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/docs/assets/sass/prism.scss:
--------------------------------------------------------------------------------
1 | code[class*="language-"],
2 | pre[class*="language-"] {
3 | text-align: left;
4 | white-space: pre;
5 | word-spacing: normal;
6 | word-break: normal;
7 | word-wrap: normal;
8 | color: #c3cee3;
9 | background: #263238;
10 | font-family: Roboto Mono, monospace;
11 | font-size: .875rem;
12 | line-height: 1.5;
13 |
14 | -moz-tab-size: 4;
15 | -o-tab-size: 4;
16 | tab-size: 4;
17 |
18 | -webkit-hyphens: none;
19 | -moz-hyphens: none;
20 | -ms-hyphens: none;
21 | hyphens: none;
22 |
23 | @screen 2xl {
24 | font-size: 1rem;
25 | }
26 | }
27 |
28 | code[class*="language-"]::-moz-selection,
29 | pre[class*="language-"]::-moz-selection,
30 | code[class*="language-"] ::-moz-selection,
31 | pre[class*="language-"] ::-moz-selection {
32 | background: #363636;
33 | }
34 |
35 | code[class*="language-"]::selection,
36 | pre[class*="language-"]::selection,
37 | code[class*="language-"] ::selection,
38 | pre[class*="language-"] ::selection {
39 | background: #363636;
40 | }
41 |
42 | :not(pre) > code[class*="language-"] {
43 | white-space: normal;
44 | border-radius: 0.2em;
45 | padding: 0.1em;
46 | }
47 |
48 | pre[class*="language-"] {
49 | overflow: auto;
50 | position: relative;
51 | margin: 0.5em 0;
52 | padding: 1.25em 1em;
53 | }
54 |
55 | .language-css > code,
56 | .language-sass > code,
57 | .language-scss > code {
58 | color: #fd9170;
59 | }
60 |
61 | [class*="language-"] .namespace {
62 | opacity: 0.7;
63 | }
64 |
65 | .token.atrule {
66 | color: #c792ea;
67 | }
68 |
69 | .token.attr-name {
70 | color: #ffcb6b;
71 | }
72 |
73 | .token.attr-value {
74 | color: #c3e88d;
75 | }
76 |
77 | .token.attribute {
78 | color: #c3e88d;
79 | }
80 |
81 | .token.boolean {
82 | color: #c792ea;
83 | }
84 |
85 | .token.builtin {
86 | color: #ffcb6b;
87 | }
88 |
89 | .token.cdata {
90 | color: #80cbc4;
91 | }
92 |
93 | .token.char {
94 | color: #80cbc4;
95 | }
96 |
97 | .token.class {
98 | color: #ffcb6b;
99 | }
100 |
101 | .token.class-name {
102 | color: #F99157;
103 | }
104 |
105 | .token.color {
106 | color: #F99157;
107 | }
108 |
109 | .token.comment {
110 | color: #546e7a;
111 | }
112 |
113 | .token.constant {
114 | color: #c792ea;
115 | }
116 |
117 | .token.deleted {
118 | color: #f07178;
119 | }
120 |
121 | .token.doctype {
122 | color: #546e7a;
123 | }
124 |
125 | .token.entity {
126 | color: #f07178;
127 | }
128 |
129 | .token.function {
130 | color: #c792ea;
131 | }
132 |
133 | .token.hexcode {
134 | color: #F99157;
135 | }
136 |
137 | .token.id {
138 | color: #c792ea;
139 | font-weight: bold;
140 | }
141 |
142 | .token.important {
143 | color: #c792ea;
144 | font-weight: bold;
145 | }
146 |
147 | .token.inserted {
148 | color: #80cbc4;
149 | }
150 |
151 | .token.keyword {
152 | color: #c792ea;
153 | font-style: italic;
154 | }
155 |
156 | .token.number {
157 | color: #fd9170;
158 | }
159 |
160 | .token.operator {
161 | color: #89ddff;
162 | }
163 |
164 | .token.prolog {
165 | color: #546e7a;
166 | }
167 |
168 | .token.property {
169 | color: #80cbc4;
170 | }
171 |
172 | .token.pseudo-class {
173 | color: #c3e88d;
174 | }
175 |
176 | .token.pseudo-element {
177 | color: #c3e88d;
178 | }
179 |
180 | .token.punctuation {
181 | color: #89ddff;
182 | }
183 |
184 | .token.regex {
185 | color: #F99157;
186 | }
187 |
188 | .token.selector {
189 | color: #f07178;
190 | }
191 |
192 | .token.string {
193 | color: #c3e88d;
194 | }
195 |
196 | .token.symbol {
197 | color: #c792ea;
198 | }
199 |
200 | .token.tag {
201 | color: #f07178;
202 | }
203 |
204 | .token.unit {
205 | color: #f07178;
206 | }
207 |
208 | .token.url {
209 | color: #fd9170;
210 | }
211 |
212 | .token.variable {
213 | color: #f07178;
214 | }
215 |
--------------------------------------------------------------------------------
/docs/how-to-use.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: layouts/base.njk
3 | title: How To Use
4 | ---
5 | # How to Use
6 | ## Get the package
7 |
8 | Simply include [the package](https://www.npmjs.com/package/@unseenco/taxi) through your favourite package manager:
9 |
10 | ### npm
11 | ```
12 | npm i @unseenco/taxi
13 | ```
14 |
15 | ### yarn
16 | ```
17 | yarn add @unseenco/taxi
18 | ```
19 |
20 | ### pnpm
21 | ```
22 | pnpm add @unseenco/taxi
23 | ```
24 |
25 | ## Setting up
26 | Next, you need to import `Taxi.Core` into your code and create a new instance:
27 |
28 | ```js
29 | import { Core } from '@unseenco/taxi'
30 |
31 | const taxi = new Core()
32 |
33 | // or if you prefer
34 |
35 | import * as Taxi from '@unseenco/taxi'
36 |
37 | const taxi = new Taxi.Core()
38 | ```
39 |
40 | Then amend your HTML so that `data-taxi` is added to the parent of the content you want to replace during a transition, and `data-taxi-view` is added to the element you are replacing:
41 |
42 |
43 | ```html
44 |
45 |
46 | ...
47 |
48 |
49 | ```
50 |
51 | **Please note:** The `data-taxi-view` element **has to be the only child** of `data-taxi`.
52 |
53 |
54 | Now when you navigate in your app, `data-taxi-view` will be replaced with the `data-taxi-view` from the target URL instead of the whole page loading 🥳
55 |
56 |
57 | ## Via CDN
58 | You can use Taxi via a CDN thanks to the kind folks at unpkg.com. Just be sure to note that the main export is `taxi` with a lowercase t:
59 |
60 | ```html
61 |
62 |
63 |
64 |
65 |
66 | ...
67 |
68 |
69 |
70 |
73 | ```
74 |
75 | ## Which links are handled by Taxi?
76 | Taxi will only transition links to a domain which is the same as the current URL (for obvious reasons).
77 |
78 | By default, Taxi will not transition links which:
79 |
80 | * have `data-taxi-ignore` present on the link element;
81 | * are anchor links for the current page;
82 | * have a `target` attribute present on the link element;
83 |
84 | Of course, you can always change this behaviour using the [links option](#links-string).
85 |
86 | ## Options
87 | When creating a new Taxi instance, you can pass an object of options into the constructor:
88 |
89 | ```js
90 | const taxi = new Core({ ... })
91 | ```
92 |
93 | Let's look at these in more detail.
94 |
95 | ### renderers `Object.`
96 | Please see [Renderers]({{ global.url }}/renderers/) for more information.
97 |
98 |
99 | ### transitions `Object.`
100 | Please see [Transitions]({{ global.url }}/transitions/) for more information.
101 |
102 | ### links `string`
103 | Links is a CSS selector which Taxi uses to decide if a clicked link should be transitioned or not.
104 |
105 | Here is the default value:
106 | ```js
107 | const taxi = new Core({
108 | links: 'a:not([target]):not([href^=\\#]):not([data-taxi-ignore])'
109 | })
110 | ```
111 |
112 | As you can see the default value ignored links with a `target` attribute, is an anchor link on the current page, or has `data-taxi-ignore` present.
113 |
114 | You can use this option to extend this behaviour and fine tune which links are considered valid.
115 |
116 |
117 | ### removeOldContent `boolean`
118 | Taxi will remove the previous page's content after the Transition's `onLeave` method has finished. Set this to `false` to disable this behaviour.
119 |
120 | ### allowInterruption `boolean`
121 | Taxi blocks further navigation while a transition is in progress. Set this to `true` to disable this behaviour.
122 |
123 |
124 | ### bypassCache `boolean`
125 | Default behaviour is to cache the contents of a URL after fetching it to make repeated visits faster. Set this to `true` to disable the cache completely.
126 |
127 | If you want default behaviour, but wish to force certain pages to always be fetched (and never loaded from cache), you can add the `data-taxi-nocache` attribute to the `data-taxi-view` element on that page.
128 |
129 |
130 | ### enablePrefetch `boolean`
131 | Default behaviour is to preload links on your website whenever the `mouseenter` or `focus` event is triggered.
132 |
133 | If you want to disable this or want to implement your own preloading strategy, set this to `false`.
134 |
135 | ### reloadJsFilter `bool|function(element: HTMLElement)`
136 | Please see [Reloading JS]({{ global.url }}/reloading-js/) for more information.
137 |
138 | ### reloadCssFilter `bool|function(element: HTMLLinkElement)`
139 | Please see [Reloading CSS]({{ global.url }}/reloading-css/) for more information.
140 |
141 |
142 |
143 |
What's next:
144 |
147 |
--------------------------------------------------------------------------------
/docs/index.njk:
--------------------------------------------------------------------------------
1 | ---
2 | title: Vanilla Javascript Page Transitions Manager
3 | ---
4 | {% extends 'layouts/base.njk' %}
5 |
6 | {% block content %}
7 | Taxi.js
8 |
9 | Taxi is a 🤏 small and 🐊 snappy js library for adding slick PJAX navigation and beautiful transitions to
10 | your website.
11 |
12 | It was designed as a drop-in replacement (with enhancements) for Highway.js , which is sadly no longer maintained.
13 |
14 | Improvements over Highway:
15 |
16 |
17 | URL-based routing
18 | Perfect for working with WebGL websites, you can now specify unique transitions for every possible navigation.
19 |
20 |
21 | Page cache management
22 | Perfect for archives and search result pages, you can now preserve changes made to the DOM by user interaction.
23 |
24 |
25 | Ability to preload URLs
26 | First-class support for adding pages to the cache ahead of schedule.
27 |
28 |
29 | Blocks navigation during an active transition
30 | Stop users from performing state-breaking navigation by default.
31 |
32 |
33 | Auto-run javascript on the new page
34 | If you are using a CMS or splitting your JS into page-specific modules, we have you covered.
35 |
36 |
37 | Previous page's content is automatically removed (you can opt out of this if you like)
38 | Clean code, clean mind.
39 |
40 |
41 | Click events on links can be intercepted via stopPropagation
without hacks
42 | This finally works as you expect.
43 |
44 |
45 | Handles 301 redirects gracefully
46 | The user is served the correct content, and the browser navigation works exactly as you would want.
47 |
48 |
49 |
50 | Differences to Highway
51 |
52 |
53 | Different public API
54 |
55 |
56 |
57 | Different data attributes
58 | data-taxi
,
data-taxi-view
, and
data-taxi-ignore
are to be used instead of
data-router-wrapper
,
data-router-view
, and
data-router-disabled
respectively. See
how to use for more.
59 |
60 |
61 | attach
and detach
are no longer methods
62 | Link clicks are listened to via delegation so these are no longer needed.
63 |
64 |
65 | redirect
is now navigateTo
66 | It always felt weird as a method name, and now it doesn't.
67 |
68 |
69 | Renderers now have an initialLoad
method
70 | Nice and simple.
71 |
72 |
73 | Old content is automatically removed during a transition
74 | You can opt of this behaviour if you like via the
allowInterruption option.
75 |
76 |
77 |
78 | Contributors
79 | Made with ❤️ by Jake Whiteley , and feedback from the Okay Dev community .
80 |
81 |
82 |
What's next:
83 |
86 |
87 | {% endblock %}
--------------------------------------------------------------------------------
/docs/mix-manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "/assets/js/index.js": "/assets/js/index.js?id=9f73e36163448599799c"
3 | }
4 |
--------------------------------------------------------------------------------
/docs/navigation-lifecycle.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: layouts/base.njk
3 | title: Navigation Lifecycle
4 | ---
5 |
6 | # Navigation Lifecycle
7 | Now that you understand [Renderers]({{ global.url }}/renderers) and [Transitions]({{ global.url }}/transitions/), how does it all fit together?
8 |
9 | Let's use a **real world example** to find out:
10 |
11 | 1. A user clicks a link in your app
12 | 2. Taxi will go and fetch the new page the user has requested, and attach it to the current Document so images etc start downloading
13 | 3. Taxi [checks to see which Transition]({{ global.url }}/transitions/#how-transitions-are-chosen) should be used
14 | 4. The current Renderer's `onLeave` method is called
15 | 5. Then the chosen Transition's `onLeave`. The old page content is removed here unless you set `removeOldContent: false` when initing Taxi.
16 | 6. Then the Renderer's `onLeaveCompleted`
17 | 7. Next, Taxi will add the new page's content to the container
18 | 8. Taxi will look at the new page content and call the `onEnter` method of the Renderer set via the new page's `data-taxi-view` attribute, or the default if not defined
19 | 9. Then call the Transition's `onEnter` method
20 | 10. Finally, when the transition is all finished, the new Renderer's `onEnterComplete` is called
21 |
22 |
23 |
24 |
What's next:
25 |
28 |
--------------------------------------------------------------------------------
/docs/reloading-css.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: layouts/base.njk
3 | title: Reloading CSS
4 | ---
5 |
6 | # Running CSS on New Pages
7 | Similarly to [Reloading JS]({{ global.url }}/reloading-js/) Taxi can also reload and run CSS present from the next page after navigation.
8 |
9 | If enabled, this feature will run just after the `NAVIGATE_IN` event, after the new content has been appended to the DOM, but before the `Renderer.onEnter` method is called.
10 |
11 | ## Choosing which stylesheets are reloaded
12 | By default, only stylesheets with the `data-taxi-reload` attribute are reloaded after a navigation.
13 |
14 | ```html
15 |
16 |
17 |
18 |
19 |
20 | ```
21 |
22 | If using the super cool [astro](https://astro.build/) or some other build tool which outputs per-page/component CSS, `reloadCssFilter` accepts a callback function to filter styles on the new page and decide which to load.
23 |
24 | Your callback is passed the `link` element, and must return a boolean indicating whether the stylesheet should be reloaded or not (you could check the src or id attributes for example).
25 |
26 | Here is the default callback for `reloadCssFilter`:
27 |
28 | ```js
29 | (element) => element.dataset.taxiReload !== undefined
30 | ```
31 |
32 | and here is a custom example which loads everything:
33 | ```js
34 | import { Core } from '@unseenco/taxi'
35 |
36 | const taxi = new Core({
37 | reloadCssFilter: (element) => true
38 | })
39 | ```
40 |
41 |
42 | ## Disabling this feature
43 | Just set `reloadCssFilter` to false when initing Taxi:
44 |
45 | ```js
46 | import { Core } from '@unseenco/taxi'
47 |
48 | const taxi = new Core({
49 | reloadCssFilter: false
50 | })
51 | ```
52 |
53 |
54 |
What's next:
55 |
58 |
--------------------------------------------------------------------------------
/docs/reloading-js.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: layouts/base.njk
3 | title: Reloading JS
4 | ---
5 |
6 | # Running JS on New Pages
7 | Taxi can reload and run js present on a fetched page during the navigation cycle. This is especially useful when working with traditional CMSs such as WordPress or Magento, or if you wanted to split your js if you have a particularly heavy page.
8 |
9 | It will also parse and execute inline js, allowing you to add data to the `window` object for example.
10 |
11 | If enabled, this feature will run just after the `NAVIGATE_IN` event, after the new content has been appended to the DOM, but before the `Renderer.onEnter` method is called.
12 |
13 | ## Choosing which scripts are reloaded
14 | By default, only scripts with the `data-taxi-reload` attribute are reloaded after a navigation.
15 |
16 | ```html
17 |
18 |
19 |
20 |
21 |
22 | ```
23 | In certain situations, you may not have control over the `` tags directly. Luckily `reloadJsFilter` accepts a callback function to filter scripts on the new page and decide which to load.
24 |
25 | Your callback is passed the `script` element, and must return a boolean indicating whether the script should be reloaded or not (you could check the src or id attributes for example).
26 |
27 | Here is the default callback for `reloadJsFilter`:
28 |
29 | ```js
30 | (element) => element.dataset.taxiReload !== undefined
31 | ```
32 |
33 | and here is a custom example:
34 | ```js
35 | import { Core } from '@unseenco/taxi'
36 |
37 | const taxi = new Core({
38 | reloadJsFilter: (element) => element.dataset.taxiReload !== undefined || element.src?.match('myscript.js')
39 | })
40 | ```
41 |
42 |
43 | ## Disabling this feature
44 | Just set `reloadJsFilter` to false when initing Taxi:
45 |
46 | ```js
47 | import { Core } from '@unseenco/taxi'
48 |
49 | const taxi = new Core({
50 | reloadJsFilter: false
51 | })
52 | ```
53 |
54 |
55 |
What's next:
56 |
59 |
--------------------------------------------------------------------------------
/docs/renderers.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: layouts/base.njk
3 | title: Renderers
4 | ---
5 |
6 | # Renderers
7 | A Renderer is run everytime a page is shown or hidden when using Taxi. They are an ideal place to init/destroy components on the page, or play intro animations.
8 |
9 | All Renderers should extend `@unseenco/taxi.Renderer` and look something like this:
10 |
11 | ```js
12 | import { Renderer } from '@unseenco/taxi';
13 |
14 | export default class CustomRenderer extends Renderer {
15 | onEnter() {
16 | // run after the new content has been added to the Taxi container
17 | }
18 |
19 | onEnterCompleted() {
20 | // run after the transition.onEnter has fully completed
21 | }
22 |
23 | onLeave() {
24 | // run before the transition.onLeave method is called
25 | }
26 |
27 | onLeaveCompleted() {
28 | // run after the transition.onleave has fully completed
29 | }
30 | }
31 | ```
32 |
33 | The following props are available within Renderer methods:
34 | * `this.page` : The entire document that is being rendered
35 | * `this.title` : The document.title of the page being rendered
36 | * `this.wrapper` : A reference to the `data-taxi` element
37 | * `this.content` : A reference to the `data-taxi-view` element which is being added to the DOM
38 |
39 | ## Registering a Renderer
40 | When initializing Taxi, you can pass through an object of renderers to register:
41 |
42 | ```js
43 | import MyRenderer from './renderers/MyRenderer'
44 | import SomeOtherRenderer from './renderers/SomeOtherRenderer'
45 |
46 | const taxi = new Core({
47 | renderers: {
48 | mine: MyRenderer,
49 | someOther: SomeOtherRenderer,
50 | }
51 | })
52 | ```
53 |
54 |
55 | ## Associating a Renderer with a page
56 | When Taxi fetches a page, it looks at the `data-taxi-view` attribute to choose the renderer for that page.
57 |
58 | If we wanted to run `SomeOtherRenderer` from the example above when visiting a page, we would just use the key we added it under as the value for `data-taxi-view`:
59 |
60 | ```html
61 |
62 | ...
63 |
64 | ```
65 |
66 | Now when the page is transitioned to, Taxi knows to find the renderer with a key of `someOther` and run it.
67 |
68 | ### Default Renderers
69 | But what happens if we don't specify a value to `data-taxi-view`?
70 | ```html
71 | ...
72 | ```
73 | Taxi will look for a renderer with a key of `default` and run that as a fallback:
74 |
75 | ```js
76 | const taxi = new Core({
77 | renderers: {
78 | default: MyRenderer,
79 | someOther: SomeOtherRenderer,
80 | }
81 | })
82 | ```
83 |
84 | If you don't like `default` as the key for this fallback renderer, you can set your own using the `SomeOtherRenderer` method:
85 |
86 | ```js
87 | const taxi = new Core({
88 | renderers: {
89 | mine: MyRenderer,
90 | someOther: SomeOtherRenderer,
91 | }
92 | })
93 |
94 | taxi.setDefaultRenderer('mine')
95 | ```
96 |
97 |
98 | ## Running code on the initial visit to your site
99 | Renderers are called whenever a navigation takes place, but the correct Renderer is also called when a user first visits your site.
100 |
101 | There may be things you want to setup at this time such as persistent components like navigation, or smoothscroll for example.
102 |
103 | To aid with this, Renderers also have an `initialLoad` method which is only run on a user's first visit.
104 |
105 | As no navigation has taken place, Taxi won't fire your Renderer's `onEnter` or `onEnterCompleted` methods, so we suggest running them here is a good idea:
106 |
107 | ```js
108 | import { Renderer } from '@unseenco/taxi';
109 |
110 | export default class CustomRenderer extends Renderer {
111 | initialLoad() {
112 | // run code that should only happen once for your site
113 |
114 | this.onEnter()
115 | this.onEnterCompleted()
116 | }
117 |
118 | // rest of your methods
119 | }
120 | ```
121 |
122 |
123 |
What's next:
124 |
127 |
128 |
--------------------------------------------------------------------------------
/docs/routing.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: layouts/base.njk
3 | title: Routing
4 | ---
5 |
6 | # Routing
7 | Routing in Taxi is used to choose which [Transition]({{ global.url }}/transitions/) to choose when a user performs a navigation.
8 |
9 | They are defined via the `addRoute` method, and consist of a regex to run against the current URL, a regex to run against the new URL after the navigation, and the transition to use if matched.
10 |
11 | Here are a few examples:
12 |
13 | ```js
14 | // Transition from a blog page to the homepage
15 | taxi.addRoute('/blog/.*', '', 'blogToHome')
16 |
17 | // Transition the homepage to any other page
18 | taxi.addRoute('', '.*', 'fromHome')
19 |
20 | // Transition from the about page, to the contact page
21 | taxi.addRoute('/about', '/contact', 'aboutToContact')
22 | ```
23 |
24 | **Please note:** Your regex is wrapped inside `^` and `$` automatically, so a regex of `/api` will match `/api` **but not** `/v2/api`. Keep this in mind when adding routing rules!
25 |
26 | **Also please note:** All trailing slashes are stripped from all URLs before they are matched. This means that `/` will not match your homepage, but `''` or `/?` will.
27 |
28 | They are also run as `RegExp` so there's no need to escape slashes. 👊
29 |
30 |
31 | Suck at regular expressions? Start here: [RegEx help for newbies](https://softchris.github.io/pages/javascript-regex.html)
32 |
33 |
34 | ## Route Ordering
35 | Routes are tested in **the same order they are declared**, and as soon as a match is found, that transition is chosen.
36 |
37 |
38 | Lost? Well consider the following:
39 |
40 | ```js
41 | // bad
42 | taxi.addRoute('/pages/.*', '', 'somethingElse')
43 | taxi.addRoute('/pages/specific', '', 'something')
44 |
45 | // good
46 | taxi.addRoute('/pages/specific', '', 'something')
47 | taxi.addRoute('/pages/.*', '', 'somethingElse')
48 | ```
49 |
50 | In the above example, if the user was navigating from `/pages/specific` to the homepage, only the second example would match and run the "something" transition.
51 |
52 | This is because the first example registers the catch-all **before** the specific rule, so the specific one is never reached.
53 |
54 |
55 |
56 |
What's next:
57 |
60 |
--------------------------------------------------------------------------------
/docs/tailwind.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | content: ['./docs/**/*.{html,js,njk,md}'],
3 | safelist: ['toolbar', 'code-toolbar'],
4 | darkMode: 'class',
5 | plugins: [
6 | require('@tailwindcss/typography')
7 | ],
8 | theme: {
9 | extend: {
10 | fontFamily: {
11 | sans: ["Inter", "sans-serif"],
12 | neue: ["Neue Montreal", "sans-serif"],
13 | },
14 | colors: {
15 | taxi: '#F4BA00',
16 | code: '#263238'
17 | },
18 | typography: {
19 | DEFAULT: {
20 | css: {
21 | "code::before": {content: ''},
22 | "code::after": {content: ''},
23 | "--tw-prose-pre-bg": '#263238',
24 | "--tw-prose-invert-pre-bg": '#263238',
25 | }
26 | }
27 | }
28 | }
29 | },
30 | }
--------------------------------------------------------------------------------
/docs/transitions.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: layouts/base.njk
3 | title: Transitions
4 | ---
5 |
6 | # Transitions
7 | Whenever a user navigates on your site, a Transition class is run to provide the fancy animation between the two pages.
8 |
9 | A transition consists of an `onLeave` method called when leaving the current page, and an `onEnter` method which is called after the new content has been added to the DOM.
10 |
11 | Each method is passed an object containing the `done` promise resolve function to call when your animation is finished, and the `trigger` that caused the navigation (either `'popstate'` for browser navigation, the `Element` if a link click, or `false` if the navigation was caused programmatically via `navigateTo`.
12 |
13 | The methods are also passed a reference to the active `data-taxi-view` element: When leaving the current `data-taxi-view` is passed, and when entering the new `data-taxi-view` is passed instead:
14 |
15 | ```js
16 | import { Transition } from '@unseenco/taxi'
17 |
18 | export default class MyTransition extends Transition {
19 | /**
20 | * Handle the transition leaving the previous page.
21 | * @param { { from: HTMLElement, trigger: string|HTMLElement|false, done: function } } props
22 | */
23 | onLeave({ from, trigger, done }) {
24 | // do something ...
25 | done()
26 | }
27 |
28 | /**
29 | * Handle the transition entering the next page.
30 | * @param { { to: HTMLElement, trigger: string|HTMLElement|false, done: function } } props
31 | */
32 | onEnter({ to, trigger, done }) {
33 | // do something else ...
34 | done()
35 | }
36 | }
37 | ```
38 |
39 | `this.wrapper` is also available, which is a reference to the main `data-taxi` container.
40 |
41 | ## Registering a transition
42 | As with [renderers]({{ global.url }}/renderers/), when initializing Taxi you should pass through an object of Transitions to register:
43 |
44 | ```js
45 | import MyTransition from './transitions/MyTransition'
46 | import SomeOtherTransition from './transitions/SomeOtherTransition'
47 |
48 | const taxi = new Core({
49 | transitions: {
50 | mine: MyTransition,
51 | someOther: SomeOtherTransition,
52 | }
53 | })
54 | ```
55 |
56 | ### Setting a default Transition
57 | Taxi will look for a transition with a key of `default`:
58 |
59 | ```js
60 | const taxi = new Core({
61 | transitions: {
62 | default: MyTransition
63 | }
64 | })
65 | ```
66 |
67 | But of course this can be changed by using the `setDefaultTransition` method:
68 |
69 | ```js
70 | const taxi = new Core({
71 | transitions: {
72 | mine: MyTransition
73 | }
74 | })
75 |
76 | taxi.setDefaultTransition('mine')
77 | ```
78 |
79 | Confused as to why you would want to set a default Transition? No worries, read on to find out (it's important!).
80 |
81 | ## How transitions are chosen
82 | Taxi has a distinct hierarchy when it comes to choosing which transition to run during a navigation.
83 |
84 | The checks Taxi takes to find a Transition are as follows:
85 |
86 | ### 1. Explicit Transition
87 | You can specify a `data-transition` attribute on a clicked link like so:
88 | ```html
89 | ...
90 | ```
91 | If a user clicks the above link, the `"someTransition"` transition will be used.
92 |
93 | These are for special cases really, as browser navigation (back/forward buttons) will never trigger this.
94 |
95 | ### 2. Route Transition
96 | The navigation had no explicit Transition associated with it, so Taxi will next check the defined routes to see if a contextual transition can be matched.
97 |
98 | Learn more about [routing]({{ global.url }}/routing/).
99 |
100 | ### 3. Default Transition
101 | If there was no explicit transition, and no matches from the router, finally the default transition will be used.
102 |
103 |
104 |
What's next:
105 |
108 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@unseenco/taxi",
3 | "description": "A modern page transition library which supports routing, preloading, and additional script reloading.",
4 | "version": "1.9.0",
5 | "author": {
6 | "name": "Jake Whiteley / Unseen Studio Ltd",
7 | "email": "jake@unseen.co",
8 | "url": "https://unseen.co"
9 | },
10 | "license": "BSD-3-Clause",
11 | "source": "src/taxi.js",
12 | "main": "src/taxi.js",
13 | "umd:main": "./dist/taxi.umd.js",
14 | "publishConfig": {
15 | "source": "src/taxi.js",
16 | "main": "dist/taxi.js"
17 | },
18 | "repository": {
19 | "type": "git",
20 | "url": "git+https://github.com/craftedbygc/taxi.git"
21 | },
22 | "bugs": {
23 | "url": "https://github.com/craftedbygc/taxi/issues"
24 | },
25 | "homepage": "https://github.com/craftedbygc/taxi#readme",
26 | "contributors": [
27 | {
28 | "name": "Jake Whiteley",
29 | "email": "jake@craftedbygc.com"
30 | }
31 | ],
32 | "dependencies": {
33 | "@unseenco/e": "^2.3.0"
34 | },
35 | "devDependencies": {
36 | "@11ty/eleventy": "^1.0.1",
37 | "@tailwindcss/typography": "^0.5.3",
38 | "@types/estree": "^1.0.0",
39 | "babel-plugin-prismjs": "^2.1.0",
40 | "browser-sync": "^2.27.7",
41 | "browser-sync-webpack-plugin": "^2.3.0",
42 | "cross-env": "^7.0.3",
43 | "gsap": "^3.10.2",
44 | "html-minifier": "^4.0.0",
45 | "laravel-mix": "^6.0.41",
46 | "markdown-it-anchor": "^8.6.4",
47 | "microbundle": "^0.14.2",
48 | "mix-tailwindcss": "^1.3.0",
49 | "npm-run-all": "^4.1.5",
50 | "prismjs": "^1.28.0",
51 | "resolve-url-loader": "^5.0.0",
52 | "sass": "^1.53.0",
53 | "sass-loader": "^12.1.0",
54 | "tailwindcss": "^3.1.5",
55 | "typescript": "^4.5.5"
56 | },
57 | "scripts": {
58 | "build": "microbundle",
59 | "ts": "tsc",
60 | "start": "npm-run-all -p dev:*",
61 | "dev:eleventy": "eleventy --serve --quiet",
62 | "dev:mix": "mix watch",
63 | "docs": "npm-run-all -p docs:*",
64 | "docs:eleventy": "cross-env ENVIRONMENT=prod eleventy",
65 | "docs:mix": "mix --production"
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/src/Core.d.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @typedef CacheEntry
3 | * @type {object}
4 | * @property {typeof Renderer|Renderer} renderer
5 | * @property {Document|Node} page
6 | * @property {array} scripts
7 | * @property {HTMLLinkElement[]} styles
8 | * @property {string} finalUrl
9 | * @property {boolean} skipCache
10 | * @property {string} title
11 | * @property {HTMLElement|Element} content
12 | */
13 | export default class Core {
14 | /**
15 | * @param {{
16 | * links?: string,
17 | * removeOldContent?: boolean,
18 | * allowInterruption?: boolean,
19 | * bypassCache?: boolean,
20 | * enablePrefetch?: boolean,
21 | * renderers?: Object.,
22 | * transitions?: Object.,
23 | * reloadJsFilter?: boolean|function(HTMLElement): boolean,
24 | * reloadCssFilter?: boolean|function(HTMLLinkElement): boolean
25 | * }} parameters
26 | */
27 | constructor(parameters?: {
28 | links?: string;
29 | removeOldContent?: boolean;
30 | allowInterruption?: boolean;
31 | bypassCache?: boolean;
32 | enablePrefetch?: boolean;
33 | renderers?: {
34 | [x: string]: typeof Renderer;
35 | };
36 | transitions?: {
37 | [x: string]: typeof Transition;
38 | };
39 | reloadJsFilter?: boolean | ((arg0: HTMLElement) => boolean);
40 | reloadCssFilter?: boolean | ((arg0: HTMLLinkElement) => boolean);
41 | });
42 | isTransitioning: boolean;
43 | /**
44 | * @type {CacheEntry|null}
45 | */
46 | currentCacheEntry: CacheEntry;
47 | /**
48 | * @type {Map}
49 | */
50 | cache: Map;
51 | /**
52 | * @private
53 | * @type {Map}
54 | */
55 | private activePromises;
56 | renderers: {
57 | [x: string]: typeof Renderer;
58 | };
59 | transitions: {
60 | [x: string]: typeof Transition;
61 | };
62 | defaultRenderer: typeof Renderer;
63 | defaultTransition: typeof Transition;
64 | wrapper: Element;
65 | reloadJsFilter: boolean | ((element: HTMLElement) => boolean);
66 | reloadCssFilter: boolean | ((arg0: HTMLLinkElement) => boolean) | ((element: HTMLLinkElement) => true);
67 | removeOldContent: boolean;
68 | allowInterruption: boolean;
69 | bypassCache: boolean;
70 | enablePrefetch: boolean;
71 | isPopping: boolean;
72 | currentLocation: {
73 | raw: string;
74 | href: string;
75 | host: string;
76 | search: string;
77 | hasHash: boolean;
78 | pathname: string;
79 | };
80 | /**
81 | * @param {string} renderer
82 | */
83 | setDefaultRenderer(renderer: string): void;
84 | /**
85 | * @param {string} transition
86 | */
87 | setDefaultTransition(transition: string): void;
88 | /**
89 | * Registers a route into the RouteStore
90 | *
91 | * @param {string} fromPattern
92 | * @param {string} toPattern
93 | * @param {string} transition
94 | */
95 | addRoute(fromPattern: string, toPattern: string, transition: string): void;
96 | router: RouteStore;
97 | /**
98 | * Prime the cache for a given URL
99 | *
100 | * @param {string} url
101 | * @param {boolean} [preloadAssets]
102 | * @return {Promise}
103 | */
104 | preload(url: string, preloadAssets?: boolean): Promise;
105 | /**
106 | * Updates the HTML cache for a given URL.
107 | * If no URL is passed, then cache for the current page is updated.
108 | * Useful when adding/removing content via AJAX such as a search page or infinite loader.
109 | *
110 | * @param {string} [url]
111 | */
112 | updateCache(url?: string): void;
113 | /**
114 | * Clears the cache for a given URL.
115 | * If no URL is passed, then cache for the current page is cleared.
116 | *
117 | * @param {string} [url]
118 | */
119 | clearCache(url?: string): void;
120 | /**
121 | * @param {string} url
122 | * @param {string|false} [transition]
123 | * @param {string|false|HTMLElement} [trigger]
124 | * @return {Promise}
125 | */
126 | navigateTo(url: string, transition?: string | false, trigger?: string | false | HTMLElement): Promise;
127 | targetLocation: {
128 | raw: string;
129 | href: string;
130 | host: string;
131 | search: string;
132 | hasHash: boolean;
133 | pathname: string;
134 | };
135 | popTarget: string;
136 | /**
137 | * Add an event listener.
138 | * @param {string} event
139 | * @param {any} callback
140 | */
141 | on(event: string, callback: any): void;
142 | /**
143 | * Remove an event listener.
144 | * @param {string} event
145 | * @param {any} [callback]
146 | */
147 | off(event: string, callback?: any): void;
148 | /**
149 | * @private
150 | * @param {{ raw: string, href: string, hasHash: boolean, pathname: string }} url
151 | * @param {Transition} TransitionClass
152 | * @param {string|HTMLElement|false} trigger
153 | * @return {Promise}
154 | */
155 | private beforeFetch;
156 | /**
157 | * @private
158 | * @param {{ raw: string, href: string, host: string, hasHash: boolean, pathname: string }} url
159 | * @param {Transition} TransitionClass
160 | * @param {CacheEntry} entry
161 | * @param {string|HTMLElement|false} trigger
162 | * @return {Promise}
163 | */
164 | private afterFetch;
165 | /**
166 | * Load up scripts from the target page if needed
167 | *
168 | * @param {HTMLElement[]} cachedScripts
169 | */
170 | loadScripts(cachedScripts: HTMLElement[]): void;
171 | /**
172 | * Load up styles from the target page if needed
173 | *
174 | * @param {Array} cachedStyles
175 | */
176 | loadStyles(cachedStyles: Array): void;
177 | /**
178 | * @private
179 | * @param {string} links
180 | */
181 | private attachEvents;
182 | /**
183 | * @private
184 | * @param {MouseEvent} e
185 | */
186 | private onClick;
187 | /**
188 | * @private
189 | * @return {void|boolean}
190 | */
191 | private onPopstate;
192 | /**
193 | * @private
194 | * @param {MouseEvent} e
195 | */
196 | private onPrefetch;
197 | /**
198 | * @private
199 | * @param {string} url
200 | * @param {boolean} [runFallback]
201 | * @return {Promise<{html: Document, url: string}>}
202 | */
203 | private fetch;
204 | /**
205 | * @private
206 | * @param {string|false} transition
207 | * @return {Transition|function}
208 | */
209 | private chooseTransition;
210 | /**
211 | * @private
212 | * @param {Document|Node} page
213 | * @param {string} url
214 | * @return {CacheEntry}
215 | */
216 | private createCacheEntry;
217 | }
218 | export type CacheEntry = {
219 | renderer: typeof Renderer | Renderer;
220 | page: Document | Node;
221 | scripts: any[];
222 | styles: HTMLLinkElement[];
223 | finalUrl: string;
224 | skipCache: boolean;
225 | title: string;
226 | content: HTMLElement | Element;
227 | };
228 | import Renderer from "./Renderer";
229 | import Transition from "./Transition";
230 | import RouteStore from "./RouteStore";
231 |
--------------------------------------------------------------------------------
/src/Core.js:
--------------------------------------------------------------------------------
1 | import E from '@unseenco/e'
2 | import { appendElement, parseDom, processUrl, reloadElement } from './helpers'
3 | import Transition from './Transition'
4 | import Renderer from './Renderer'
5 | import RouteStore from './RouteStore'
6 |
7 | const IN_PROGRESS = 'A transition is currently in progress'
8 |
9 | /**
10 | * @typedef CacheEntry
11 | * @type {object}
12 | * @property {typeof Renderer|Renderer} renderer
13 | * @property {Document|Node} page
14 | * @property {array} scripts
15 | * @property {HTMLLinkElement[]} styles
16 | * @property {string} finalUrl
17 | * @property {boolean} skipCache
18 | * @property {string} title
19 | * @property {HTMLElement|Element} content
20 | */
21 |
22 | export default class Core {
23 | isTransitioning = false
24 |
25 | /**
26 | * @type {CacheEntry|null}
27 | */
28 | currentCacheEntry = null
29 |
30 | /**
31 | * @type {Map}
32 | */
33 | cache = new Map()
34 |
35 | /**
36 | * @private
37 | * @type {Map}
38 | */
39 | activePromises = new Map()
40 |
41 | /**
42 | * @param {{
43 | * links?: string,
44 | * removeOldContent?: boolean,
45 | * allowInterruption?: boolean,
46 | * bypassCache?: boolean,
47 | * enablePrefetch?: boolean,
48 | * renderers?: Object.,
49 | * transitions?: Object.,
50 | * reloadJsFilter?: boolean|function(HTMLElement): boolean,
51 | * reloadCssFilter?: boolean|function(HTMLLinkElement): boolean
52 | * }} parameters
53 | */
54 | constructor(parameters = {}) {
55 | const {
56 | links = 'a[href]:not([target]):not([href^=\\#]):not([data-taxi-ignore])',
57 | removeOldContent = true,
58 | allowInterruption = false,
59 | bypassCache = false,
60 | enablePrefetch = true,
61 | renderers = {
62 | default: Renderer
63 | },
64 | transitions = {
65 | default: Transition
66 | },
67 | reloadJsFilter = (element) => element.dataset.taxiReload !== undefined,
68 | reloadCssFilter = (element) => true //element.dataset.taxiReload !== undefined
69 | } = parameters
70 |
71 | this.renderers = renderers
72 | this.transitions = transitions
73 | this.defaultRenderer = this.renderers.default || Renderer
74 | this.defaultTransition = this.transitions.default || Transition
75 | this.wrapper = document.querySelector('[data-taxi]')
76 | this.reloadJsFilter = reloadJsFilter
77 | this.reloadCssFilter = reloadCssFilter
78 | this.removeOldContent = removeOldContent
79 | this.allowInterruption = allowInterruption
80 | this.bypassCache = bypassCache
81 | this.enablePrefetch = enablePrefetch
82 | this.cache = new Map()
83 | this.isPopping = false
84 |
85 | // Add delegated link events
86 | this.attachEvents(links)
87 |
88 | this.currentLocation = processUrl(window.location.href)
89 |
90 | // as this is the initial page load, prime this page into the cache
91 | this.cache.set(this.currentLocation.href, this.createCacheEntry(document.cloneNode(true), window.location.href))
92 |
93 | // fire the current Renderer enter methods
94 | this.currentCacheEntry = this.cache.get(this.currentLocation.href)
95 | this.currentCacheEntry.renderer.initialLoad()
96 | }
97 |
98 | /**
99 | * @param {string} renderer
100 | */
101 | setDefaultRenderer(renderer) {
102 | this.defaultRenderer = this.renderers[renderer]
103 | }
104 |
105 | /**
106 | * @param {string} transition
107 | */
108 | setDefaultTransition(transition) {
109 | this.defaultTransition = this.transitions[transition]
110 | }
111 |
112 | /**
113 | * Registers a route into the RouteStore
114 | *
115 | * @param {string} fromPattern
116 | * @param {string} toPattern
117 | * @param {string} transition
118 | */
119 | addRoute(fromPattern, toPattern, transition) {
120 | if (!this.router) {
121 | this.router = new RouteStore()
122 | }
123 |
124 | this.router.add(fromPattern, toPattern, transition)
125 | }
126 |
127 | /**
128 | * Prime the cache for a given URL
129 | *
130 | * @param {string} url
131 | * @param {boolean} [preloadAssets]
132 | * @return {Promise}
133 | */
134 | preload(url, preloadAssets = false) {
135 | // convert relative URLs to absolute
136 | url = processUrl(url).href
137 |
138 | if (!this.cache.has(url)) {
139 | return this.fetch(url, false)
140 | .then(async (response) => {
141 | this.cache.set(url, this.createCacheEntry(response.html, response.url))
142 |
143 | if (preloadAssets) {
144 | this.cache.get(url).renderer.createDom()
145 | }
146 | })
147 | .catch(err => console.warn(err))
148 | }
149 |
150 | return Promise.resolve()
151 | }
152 |
153 | /**
154 | * Updates the HTML cache for a given URL.
155 | * If no URL is passed, then cache for the current page is updated.
156 | * Useful when adding/removing content via AJAX such as a search page or infinite loader.
157 | *
158 | * @param {string} [url]
159 | */
160 | updateCache(url) {
161 | const key = processUrl(url || window.location.href).href
162 |
163 | if (this.cache.has(key)) {
164 | this.cache.delete(key)
165 | }
166 |
167 | this.cache.set(key, this.createCacheEntry(document.cloneNode(true), key))
168 | }
169 |
170 | /**
171 | * Clears the cache for a given URL.
172 | * If no URL is passed, then cache for the current page is cleared.
173 | *
174 | * @param {string} [url]
175 | */
176 | clearCache(url) {
177 | const key = processUrl(url || window.location.href).href
178 |
179 | if (this.cache.has(key)) {
180 | this.cache.delete(key)
181 | }
182 | }
183 |
184 | /**
185 | * @param {string} url
186 | * @param {string|false} [transition]
187 | * @param {string|false|HTMLElement} [trigger]
188 | * @return {Promise}
189 | */
190 | navigateTo(url, transition = false, trigger = false) {
191 | return new Promise((resolve, reject) => {
192 | // Don't allow multiple navigations to occur at once
193 | if (!this.allowInterruption && this.isTransitioning) {
194 | reject(new Error(IN_PROGRESS))
195 | return
196 | }
197 |
198 | this.isTransitioning = true
199 | this.isPopping = true
200 | this.targetLocation = processUrl(url)
201 | this.popTarget = window.location.href
202 |
203 | const TransitionClass = new (this.chooseTransition(transition))({ wrapper: this.wrapper })
204 |
205 | let navigationPromise
206 |
207 | if (this.bypassCache || !this.cache.has(this.targetLocation.href) || this.cache.get(this.targetLocation.href).skipCache) {
208 | const fetched = this.fetch(this.targetLocation.href)
209 | .then((response) => {
210 | this.cache.set(this.targetLocation.href, this.createCacheEntry(response.html, response.url))
211 | this.cache.get(this.targetLocation.href).renderer.createDom()
212 | })
213 | .catch(err => {
214 | // we encountered a 4** or 5** error, redirect to the requested URL
215 | window.location.href = url
216 | })
217 |
218 | navigationPromise = this.beforeFetch(this.targetLocation, TransitionClass, trigger)
219 | .then(async () => {
220 | return fetched.then(async () => {
221 | return await this.afterFetch(this.targetLocation, TransitionClass, this.cache.get(this.targetLocation.href), trigger)
222 | })
223 | })
224 | } else {
225 | this.cache.get(this.targetLocation.href).renderer.createDom()
226 |
227 | navigationPromise = this.beforeFetch(this.targetLocation, TransitionClass, trigger)
228 | .then(async () => {
229 | return await this.afterFetch(this.targetLocation, TransitionClass, this.cache.get(this.targetLocation.href), trigger)
230 | })
231 | }
232 |
233 | navigationPromise.then(() => {
234 | resolve()
235 | })
236 | })
237 | }
238 |
239 | /**
240 | * Add an event listener.
241 | * @param {string} event
242 | * @param {any} callback
243 | */
244 | on(event, callback) {
245 | E.on(event, callback)
246 | }
247 |
248 | /**
249 | * Remove an event listener.
250 | * @param {string} event
251 | * @param {any} [callback]
252 | */
253 | off(event, callback) {
254 | E.off(event, callback)
255 | }
256 |
257 | /**
258 | * @private
259 | * @param {{ raw: string, href: string, hasHash: boolean, pathname: string }} url
260 | * @param {Transition} TransitionClass
261 | * @param {string|HTMLElement|false} trigger
262 | * @return {Promise}
263 | */
264 | beforeFetch(url, TransitionClass, trigger) {
265 | E.emit('NAVIGATE_OUT', {
266 | from: this.currentCacheEntry,
267 | trigger
268 | })
269 |
270 | return new Promise((resolve) => {
271 | this.currentCacheEntry.renderer.leave(TransitionClass, trigger, this.removeOldContent)
272 | .then(() => {
273 | if (trigger !== 'popstate') {
274 | window.history.pushState({}, '', url.raw)
275 | }
276 |
277 | resolve()
278 | })
279 | })
280 | }
281 |
282 | /**
283 | * @private
284 | * @param {{ raw: string, href: string, host: string, hasHash: boolean, pathname: string }} url
285 | * @param {Transition} TransitionClass
286 | * @param {CacheEntry} entry
287 | * @param {string|HTMLElement|false} trigger
288 | * @return {Promise}
289 | */
290 | afterFetch(url, TransitionClass, entry, trigger) {
291 | this.currentLocation = url
292 | this.popTarget = this.currentLocation.href
293 |
294 | return new Promise((resolve) => {
295 | entry.renderer.update()
296 |
297 | E.emit('NAVIGATE_IN', {
298 | from: this.currentCacheEntry,
299 | to: entry,
300 | trigger
301 | })
302 |
303 | if (this.reloadJsFilter) {
304 | this.loadScripts(entry.scripts)
305 | }
306 |
307 | if (this.reloadCssFilter) {
308 | this.loadStyles(entry.styles)
309 | }
310 |
311 | // If the fetched url had a redirect chain, then replace the history to reflect the final resolved URL
312 | if (trigger !== 'popstate' && url.href !== entry.finalUrl) {
313 | window.history.replaceState({}, '', entry.finalUrl)
314 | }
315 |
316 | entry.renderer.enter(TransitionClass, trigger)
317 | .then(() => {
318 | E.emit('NAVIGATE_END', {
319 | from: this.currentCacheEntry,
320 | to: entry,
321 | trigger
322 | })
323 |
324 | this.currentCacheEntry = entry
325 | this.isTransitioning = false
326 | this.isPopping = false
327 | resolve()
328 | })
329 | })
330 | }
331 |
332 | /**
333 | * Load up scripts from the target page if needed
334 | *
335 | * @param {HTMLElement[]} cachedScripts
336 | */
337 | loadScripts(cachedScripts) {
338 | const newScripts = [...cachedScripts]
339 | const currentScripts = Array.from(document.querySelectorAll('script')).filter(this.reloadJsFilter)
340 |
341 | // loop through all new scripts
342 | for (let i = 0; i < currentScripts.length; i++) {
343 | for (let n = 0; n < newScripts.length; n++) {
344 | if (currentScripts[i].outerHTML === newScripts[n].outerHTML) {
345 | reloadElement(currentScripts[i], 'SCRIPT')
346 | newScripts.splice(n, 1)
347 | break
348 | }
349 | }
350 | }
351 |
352 | for (const script of newScripts) {
353 | appendElement(script, 'SCRIPT')
354 | }
355 | }
356 |
357 | /**
358 | * Load up styles from the target page if needed
359 | *
360 | * @param {Array} cachedStyles
361 | */
362 | loadStyles(cachedStyles) {
363 | const currentStyles = Array.from(document.querySelectorAll('link[rel="stylesheet"]')).filter(this.reloadCssFilter)
364 | const currentInlineStyles = Array.from(document.querySelectorAll('style')).filter(this.reloadCssFilter)
365 |
366 | const newInlineStyles = cachedStyles.filter(el => {
367 | // no el.href, assume it's an inline style
368 | if (!el.href) {
369 | return true
370 | } else if (!currentStyles.find((link) => link.href === el.href)) {
371 | document.body.append(el)
372 | return false
373 | }
374 | })
375 |
376 | // loop through all new inline styles
377 | for (let i = 0; i < currentInlineStyles.length; i++) {
378 | for (let n = 0; n < newInlineStyles.length; n++) {
379 | if (currentInlineStyles[i].outerHTML === newInlineStyles[n].outerHTML) {
380 | reloadElement(currentInlineStyles[i], 'STYLE')
381 | newInlineStyles.splice(n, 1)
382 | break
383 | }
384 | }
385 | }
386 |
387 | for (const style of newInlineStyles) {
388 | appendElement(style, 'STYLE')
389 | }
390 | }
391 |
392 | /**
393 | * @private
394 | * @param {string} links
395 | */
396 | attachEvents(links) {
397 | E.delegate('click', links, this.onClick)
398 | E.on('popstate', window, this.onPopstate)
399 |
400 | if (this.enablePrefetch) {
401 | E.delegate('mouseenter focus', links, this.onPrefetch)
402 | }
403 | }
404 |
405 | /**
406 | * @private
407 | * @param {MouseEvent} e
408 | */
409 | onClick = (e) => {
410 | if (!(e.metaKey || e.ctrlKey)) {
411 | const target = processUrl(e.currentTarget.href)
412 | this.currentLocation = processUrl(window.location.href)
413 |
414 | if (this.currentLocation.host !== target.host) {
415 | return
416 | }
417 |
418 | // the target is a new URL, or is removing the hash from the current URL
419 | if (this.currentLocation.href !== target.href || (this.currentLocation.hasHash && !target.hasHash)) {
420 | e.preventDefault()
421 | // noinspection JSIgnoredPromiseFromCall
422 | this.navigateTo(target.raw, e.currentTarget.dataset.transition || false, e.currentTarget).catch(err => console.warn(err))
423 | return
424 | }
425 |
426 | // a click to the current URL was detected
427 | if (!this.currentLocation.hasHash && !target.hasHash) {
428 | e.preventDefault()
429 | }
430 | }
431 | }
432 |
433 | /**
434 | * @private
435 | * @return {void|boolean}
436 | */
437 | onPopstate = () => {
438 | const target = processUrl(window.location.href)
439 |
440 | // don't trigger for on-page anchors
441 | if (
442 | target.pathname === this.currentLocation.pathname
443 | && target.search === this.currentLocation.search
444 | && !this.isPopping
445 | ) {
446 | return false
447 | }
448 |
449 | if (!this.allowInterruption && (this.isTransitioning || this.isPopping)) {
450 | // overwrite history state with current page if currently navigating
451 | window.history.pushState({}, '', this.popTarget)
452 | console.warn(IN_PROGRESS)
453 | return false
454 | }
455 |
456 | if (!this.isPopping) {
457 | this.popTarget = window.location.href
458 | }
459 |
460 | this.isPopping = true
461 |
462 | // noinspection JSIgnoredPromiseFromCall
463 | this.navigateTo(window.location.href, false, 'popstate')
464 | }
465 |
466 | /**
467 | * @private
468 | * @param {MouseEvent} e
469 | */
470 | onPrefetch = (e) => {
471 | const target = processUrl(e.currentTarget.href)
472 |
473 | if (this.currentLocation.host !== target.host) {
474 | return
475 | }
476 |
477 | this.preload(e.currentTarget.href, false)
478 | }
479 |
480 | /**
481 | * @private
482 | * @param {string} url
483 | * @param {boolean} [runFallback]
484 | * @return {Promise<{html: Document, url: string}>}
485 | */
486 | fetch(url, runFallback = true) {
487 | // If Taxi is currently performing a fetch for the given URL, return that instead of starting a new request
488 | if (this.activePromises.has(url)) {
489 | return this.activePromises.get(url)
490 | }
491 |
492 | const request = new Promise((resolve, reject) => {
493 | let resolvedUrl
494 |
495 | fetch(url, {
496 | mode: 'same-origin',
497 | method: 'GET',
498 | headers: { 'X-Requested-With': 'Taxi' },
499 | credentials: 'same-origin'
500 | })
501 | .then((response) => {
502 | if (!response.ok) {
503 | reject('Taxi encountered a non 2xx HTTP status code')
504 |
505 | if (runFallback) {
506 | window.location.href = url
507 | }
508 | }
509 |
510 | resolvedUrl = response.url
511 |
512 | return response.text()
513 | })
514 | .then((htmlString) => {
515 | resolve({ html: parseDom(htmlString), url: resolvedUrl })
516 | })
517 | .catch((err) => {
518 | reject(err)
519 |
520 | if (runFallback) {
521 | window.location.href = url
522 | }
523 | })
524 | .finally(() => {
525 | this.activePromises.delete(url)
526 | })
527 | })
528 |
529 | this.activePromises.set(url, request)
530 |
531 | return request
532 | }
533 |
534 | /**
535 | * @private
536 | * @param {string|false} transition
537 | * @return {Transition|function}
538 | */
539 | chooseTransition(transition) {
540 | if (transition) {
541 | return this.transitions[transition]
542 | }
543 |
544 | const routeTransition = this.router?.findMatch(this.currentLocation, this.targetLocation)
545 |
546 | if (routeTransition) {
547 | return this.transitions[routeTransition]
548 | }
549 |
550 | return this.defaultTransition
551 | }
552 |
553 | /**
554 | * @private
555 | * @param {Document|Node} page
556 | * @param {string} url
557 | * @return {CacheEntry}
558 | */
559 | createCacheEntry(page, url) {
560 | const content = page.querySelector('[data-taxi-view]')
561 | const Renderer = content.dataset.taxiView.length ? this.renderers[content.dataset.taxiView] : this.defaultRenderer
562 |
563 | if (!Renderer) {
564 | console.warn(`The Renderer "${content.dataset.taxiView}" was set in the data-taxi-view of the requested page, but not registered in Taxi.`)
565 | }
566 |
567 | return {
568 | page,
569 | content,
570 | finalUrl: url,
571 | skipCache: content.hasAttribute('data-taxi-nocache'),
572 | scripts: this.reloadJsFilter ? Array.from(page.querySelectorAll('script')).filter(this.reloadJsFilter) : [],
573 | styles: this.reloadCssFilter ? Array.from(page.querySelectorAll('link[rel="stylesheet"], style')).filter(this.reloadCssFilter) : [],
574 | title: page.title,
575 | renderer: new Renderer({
576 | wrapper: this.wrapper,
577 | title: page.title,
578 | content,
579 | page
580 | })
581 | }
582 | }
583 | }
584 |
--------------------------------------------------------------------------------
/src/Renderer.d.ts:
--------------------------------------------------------------------------------
1 | export default class Renderer {
2 | /**
3 | * @param {{content: HTMLElement|Element, page: Document|Node, title: string, wrapper: Element}} props
4 | */
5 | constructor({ content, page, title, wrapper }: {
6 | content: HTMLElement | Element;
7 | page: Document | Node;
8 | title: string;
9 | wrapper: Element;
10 | });
11 | _contentString: string;
12 | _DOM: HTMLDivElement;
13 | page: Node | Document;
14 | title: string;
15 | wrapper: Element;
16 | content: Element;
17 | onEnter(): void;
18 | onEnterCompleted(): void;
19 | onLeave(): void;
20 | onLeaveCompleted(): void;
21 | initialLoad(): void;
22 | update(): void;
23 | createDom(): void;
24 | remove(): void;
25 | /**
26 | * Called when transitioning into the current page.
27 | * @param {Transition} transition
28 | * @param {string|HTMLElement|false} trigger
29 | * @return {Promise}
30 | */
31 | enter(transition: Transition, trigger: string | HTMLElement | false): Promise;
32 | /**
33 | * Called when transitioning away from the current page.
34 | * @param {Transition} transition
35 | * @param {string|HTMLElement|false} trigger
36 | * @param {boolean} removeOldContent
37 | * @return {Promise}
38 | */
39 | leave(transition: Transition, trigger: string | HTMLElement | false, removeOldContent: boolean): Promise;
40 | }
41 | import Transition from "./Transition";
42 |
--------------------------------------------------------------------------------
/src/Renderer.js:
--------------------------------------------------------------------------------
1 | import Transition from "./Transition"
2 |
3 | export default class Renderer {
4 | /**
5 | * @param {{content: HTMLElement|Element, page: Document|Node, title: string, wrapper: Element}} props
6 | */
7 | constructor({ content, page, title, wrapper }) {
8 | this._contentString = content.outerHTML
9 | this._DOM = null
10 | this.page = page
11 | this.title = title
12 | this.wrapper = wrapper
13 | this.content = this.wrapper.lastElementChild
14 | }
15 |
16 | onEnter() {
17 |
18 | }
19 |
20 | onEnterCompleted() {
21 |
22 | }
23 |
24 | onLeave() {
25 |
26 | }
27 |
28 | onLeaveCompleted() {
29 |
30 | }
31 |
32 | initialLoad() {
33 | this.onEnter()
34 | this.onEnterCompleted()
35 | }
36 |
37 | update() {
38 | document.title = this.title
39 | this.wrapper.appendChild(this._DOM.firstElementChild)
40 | this.content = this.wrapper.lastElementChild
41 | this._DOM = null
42 | }
43 |
44 | createDom() {
45 | if (!this._DOM) {
46 | this._DOM = document.createElement('div')
47 | this._DOM.innerHTML = this._contentString
48 | }
49 | }
50 |
51 | remove() {
52 | this.wrapper.firstElementChild.remove()
53 | }
54 |
55 | /**
56 | * Called when transitioning into the current page.
57 | * @param {Transition} transition
58 | * @param {string|HTMLElement|false} trigger
59 | * @return {Promise}
60 | */
61 | enter(transition, trigger) {
62 | return new Promise((resolve) => {
63 | this.onEnter()
64 |
65 | transition.enter({ trigger, to: this.content })
66 | .then(() => {
67 | this.onEnterCompleted()
68 | resolve()
69 | })
70 | })
71 | }
72 |
73 | /**
74 | * Called when transitioning away from the current page.
75 | * @param {Transition} transition
76 | * @param {string|HTMLElement|false} trigger
77 | * @param {boolean} removeOldContent
78 | * @return {Promise}
79 | */
80 | leave(transition, trigger, removeOldContent) {
81 | return new Promise((resolve) => {
82 | this.onLeave()
83 |
84 | transition.leave({ trigger, from: this.content })
85 | .then(() => {
86 | if (removeOldContent) {
87 | this.remove()
88 | }
89 |
90 | this.onLeaveCompleted()
91 | resolve()
92 | })
93 | })
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/src/RouteStore.d.ts:
--------------------------------------------------------------------------------
1 | export default class RouteStore {
2 | /**
3 | * @type {Map>}
4 | */
5 | data: Map>;
6 | /**
7 | * @type {Map}
8 | */
9 | regexCache: Map;
10 | /**
11 | *
12 | * @param {string} fromPattern
13 | * @param {string} toPattern
14 | * @param {string} transition
15 | */
16 | add(fromPattern: string, toPattern: string, transition: string): void;
17 | /**
18 | *
19 | * @param {{ raw: string, href: string, hasHash: boolean, pathname: string }} currentUrl
20 | * @param {{ raw: string, href: string, hasHash: boolean, pathname: string }} nextUrl
21 | * @return {string|null}
22 | */
23 | findMatch(currentUrl: {
24 | raw: string;
25 | href: string;
26 | hasHash: boolean;
27 | pathname: string;
28 | }, nextUrl: {
29 | raw: string;
30 | href: string;
31 | hasHash: boolean;
32 | pathname: string;
33 | }): string | null;
34 | }
35 |
--------------------------------------------------------------------------------
/src/RouteStore.js:
--------------------------------------------------------------------------------
1 | export default class RouteStore {
2 | /**
3 | * @type {Map>}
4 | */
5 | data = new Map()
6 |
7 | /**
8 | * @type {Map}
9 | */
10 | regexCache = new Map()
11 |
12 | /**
13 | *
14 | * @param {string} fromPattern
15 | * @param {string} toPattern
16 | * @param {string} transition
17 | */
18 | add(fromPattern, toPattern, transition) {
19 | if (!this.data.has(fromPattern)) {
20 | this.data.set(fromPattern, new Map())
21 | this.regexCache.set(fromPattern, new RegExp(`^${fromPattern}$`))
22 | }
23 |
24 | this.data.get(fromPattern).set(toPattern, transition)
25 | this.regexCache.set(toPattern, new RegExp(`^${toPattern}$`))
26 | }
27 |
28 | /**
29 | *
30 | * @param {{ raw: string, href: string, hasHash: boolean, pathname: string }} currentUrl
31 | * @param {{ raw: string, href: string, hasHash: boolean, pathname: string }} nextUrl
32 | * @return {string|null}
33 | */
34 | findMatch(currentUrl, nextUrl) {
35 | // Loop through all from patterns
36 | for (const [fromPattern, potentialMatches] of this.data) {
37 | // If we have a match
38 | if (currentUrl.pathname.match(this.regexCache.get(fromPattern))) {
39 | // loop through all associated to patterns
40 | for (const [toPattern, transition] of potentialMatches) {
41 | // If we find a match, return it
42 | if (nextUrl.pathname.match(this.regexCache.get(toPattern))) {
43 | return transition
44 | }
45 | }
46 |
47 | break
48 | }
49 | }
50 |
51 | return null
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/src/Transition.d.ts:
--------------------------------------------------------------------------------
1 | export default class Transition {
2 | /**
3 | * @param {{wrapper: HTMLElement}} props
4 | */
5 | constructor({ wrapper }: {
6 | wrapper: HTMLElement;
7 | });
8 | wrapper: HTMLElement;
9 | /**
10 | * @param {{ from: HTMLElement|Element, trigger: string|HTMLElement|false }} props
11 | * @return {Promise}
12 | */
13 | leave(props: {
14 | from: HTMLElement | Element;
15 | trigger: string | HTMLElement | false;
16 | }): Promise;
17 | /**
18 | * @param {{ to: HTMLElement|Element, trigger: string|HTMLElement|false }} props
19 | * @return {Promise}
20 | */
21 | enter(props: {
22 | to: HTMLElement | Element;
23 | trigger: string | HTMLElement | false;
24 | }): Promise;
25 | /**
26 | * Handle the transition leaving the previous page.
27 | * @param {{from: HTMLElement|Element, trigger: string|HTMLElement|false, done: function}} props
28 | */
29 | onLeave({ from, trigger, done }: {
30 | from: HTMLElement | Element;
31 | trigger: string | HTMLElement | false;
32 | done: Function;
33 | }): void;
34 | /**
35 | * Handle the transition entering the next page.
36 | * @param {{to: HTMLElement|Element, trigger: string|HTMLElement|false, done: function}} props
37 | */
38 | onEnter({ to, trigger, done }: {
39 | to: HTMLElement | Element;
40 | trigger: string | HTMLElement | false;
41 | done: Function;
42 | }): void;
43 | }
44 |
--------------------------------------------------------------------------------
/src/Transition.js:
--------------------------------------------------------------------------------
1 | export default class Transition {
2 | /**
3 | * @param {{wrapper: HTMLElement}} props
4 | */
5 | constructor({ wrapper }) {
6 | this.wrapper = wrapper
7 | }
8 |
9 | /**
10 | * @param {{ from: HTMLElement|Element, trigger: string|HTMLElement|false }} props
11 | * @return {Promise}
12 | */
13 | leave(props) {
14 | return new Promise((resolve) => {
15 | this.onLeave({ ...props, done: resolve })
16 | })
17 | }
18 |
19 | /**
20 | * @param {{ to: HTMLElement|Element, trigger: string|HTMLElement|false }} props
21 | * @return {Promise}
22 | */
23 | enter(props) {
24 | return new Promise((resolve) => {
25 | this.onEnter({ ...props, done: resolve })
26 | })
27 | }
28 |
29 | /**
30 | * Handle the transition leaving the previous page.
31 | * @param {{from: HTMLElement|Element, trigger: string|HTMLElement|false, done: function}} props
32 | */
33 | onLeave({ from, trigger, done }) {
34 | done()
35 | }
36 |
37 | /**
38 | * Handle the transition entering the next page.
39 | * @param {{to: HTMLElement|Element, trigger: string|HTMLElement|false, done: function}} props
40 | */
41 | onEnter({ to, trigger, done }) {
42 | done()
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/helpers.d.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Parse a HTML string into a proper Document.
3 | *
4 | * @param {string|Document} html
5 | * @return {Document|*}
6 | */
7 | export function parseDom(html: string | Document): Document | any;
8 | /**
9 | * Extract details from a given URL string. Assumed to be on the current TLD.
10 | *
11 | * @param {string} url
12 | * @return {{raw: string, href: string, host: string, search: string, hasHash: boolean, pathname: string}}
13 | */
14 | export function processUrl(url: string): {
15 | raw: string;
16 | href: string;
17 | host: string;
18 | search: string;
19 | hasHash: boolean;
20 | pathname: string;
21 | };
22 | /**
23 | * Reloads a provided script/stylesheet by replacing with itself.
24 | *
25 | * @param {HTMLElement|HTMLScriptElement|HTMLStyleElement} node
26 | * @param {string} elementType - 'SCRIPT' or 'STYLE'
27 | */
28 | export function reloadElement(node: HTMLElement | HTMLScriptElement | HTMLStyleElement, elementType: string): void;
29 | /**
30 | * Loads a provided script/stylesheet by appending a clone to the current document.
31 | *
32 | * @param {HTMLElement|HTMLStyleElement} node
33 | * @param {string} elementType - 'SCRIPT' or 'STYLE'
34 | */
35 | export function appendElement(node: HTMLElement | HTMLStyleElement, elementType: string): void;
36 | /**
37 | * Creates a clone of a given HTMLElement or HTMLStyleElement
38 | *
39 | * @param {HTMLElement|HTMLStyleElement} node
40 | * @param {string} elementType - 'SCRIPT' or 'STYLE'
41 | * @return {HTMLElement|HTMLStyleElement}
42 | */
43 | export function duplicateElement(node: HTMLElement | HTMLStyleElement, elementType: string): HTMLElement | HTMLStyleElement;
44 |
--------------------------------------------------------------------------------
/src/helpers.js:
--------------------------------------------------------------------------------
1 | const parser = new DOMParser()
2 |
3 | /**
4 | * Parse a HTML string into a proper Document.
5 | *
6 | * @param {string|Document} html
7 | * @return {Document|*}
8 | */
9 | export function parseDom(html) {
10 | return typeof html === 'string' ? parser.parseFromString(html, 'text/html') : html
11 | }
12 |
13 | /**
14 | * Extract details from a given URL string. Assumed to be on the current TLD.
15 | *
16 | * @param {string} url
17 | * @return {{raw: string, href: string, host: string, search: string, hasHash: boolean, pathname: string}}
18 | */
19 | export function processUrl(url) {
20 | const details = new URL(url, window.location.origin)
21 | const normalized = details.hash.length ? url.replace(details.hash, '') : null
22 |
23 | return {
24 | hasHash: details.hash.length > 0,
25 | pathname: details.pathname.replace(/\/+$/, ''),
26 | host: details.host,
27 | search: details.search,
28 | raw: url,
29 | href: normalized || details.href
30 | }
31 | }
32 |
33 | /**
34 | * Reloads a provided script/stylesheet by replacing with itself.
35 | *
36 | * @param {HTMLElement|HTMLScriptElement|HTMLStyleElement} node
37 | * @param {string} elementType - 'SCRIPT' or 'STYLE'
38 | */
39 | export function reloadElement(node, elementType) {
40 | node.parentNode.replaceChild(duplicateElement(node, elementType), node)
41 | }
42 |
43 | /**
44 | * Loads a provided script/stylesheet by appending a clone to the current document.
45 | *
46 | * @param {HTMLElement|HTMLStyleElement} node
47 | * @param {string} elementType - 'SCRIPT' or 'STYLE'
48 | */
49 | export function appendElement(node, elementType) {
50 | const target = node.parentNode.tagName === 'HEAD' ? document.head : document.body
51 | target.appendChild(duplicateElement(node, elementType))
52 | }
53 |
54 | /**
55 | * Creates a clone of a given HTMLElement or HTMLStyleElement
56 | *
57 | * @param {HTMLElement|HTMLStyleElement} node
58 | * @param {string} elementType - 'SCRIPT' or 'STYLE'
59 | * @return {HTMLElement|HTMLStyleElement}
60 | */
61 | export function duplicateElement(node, elementType) {
62 | const replacement = document.createElement(elementType)
63 |
64 | for (let k = 0; k < node.attributes.length; k++) {
65 | const attr = node.attributes[k]
66 | replacement.setAttribute(attr.nodeName, attr.nodeValue)
67 | }
68 |
69 | // Inline Script or Style
70 | if (node.innerHTML) {
71 | replacement.innerHTML = node.innerHTML
72 | }
73 |
74 | return replacement
75 | }
76 |
--------------------------------------------------------------------------------
/src/taxi.d.ts:
--------------------------------------------------------------------------------
1 | import Core from "./Core";
2 | import Renderer from "./Renderer";
3 | import Transition from "./Transition";
4 | export { Core, Renderer, Transition };
5 |
--------------------------------------------------------------------------------
/src/taxi.js:
--------------------------------------------------------------------------------
1 | import Core from "./Core"
2 | import Renderer from "./Renderer"
3 | import Transition from "./Transition"
4 |
5 | export {
6 | Core,
7 | Renderer,
8 | Transition
9 | }
10 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | // Change this to match your project
3 | "include": [
4 | "src/**/*.js"
5 | ],
6 | "compilerOptions": {
7 | "allowJs": true,
8 | "declaration": true,
9 | "emitDeclarationOnly": true,
10 | //"outDir": "dist"
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/webpack.mix.js:
--------------------------------------------------------------------------------
1 | let mix = require('laravel-mix')
2 | require('mix-tailwindcss')
3 |
4 | mix.js('docs/assets/js/index.js', 'assets/js')
5 | .setPublicPath('_site')
6 | .version()
7 | .sass('docs/assets/sass/index.scss', 'assets/css')
8 | .tailwind('./docs/tailwind.config.js')
9 |
10 | if (!mix.inProduction()) {
11 | mix.browserSync({
12 | server: "_site",
13 | files: ["_site/**/*.html", "_site/assets/css/*.css", "_site/assets/js/*.js"]
14 | })
15 | }
16 |
--------------------------------------------------------------------------------