├── .github ├── CODEOWNERS ├── dependabot.yml └── workflows │ └── node.js.yml ├── .gitignore ├── .npmrc ├── README.md ├── demo ├── index.html ├── package.json ├── src │ ├── components │ │ ├── base-route.js │ │ ├── dashboard-route.js │ │ ├── example-app.js │ │ ├── section-a-route.js │ │ ├── section-a1-route.js │ │ ├── section-b-route.js │ │ ├── section-b1-route.js │ │ ├── section-b2-route.js │ │ └── section-b2a-route.js │ └── js │ │ └── route-config.js ├── vite.config.js └── yarn.lock ├── lib ├── animated-routing-mixin.js ├── animated-routing.mixin.js ├── animated-routing.mixin.ts ├── page.js ├── route-data.js ├── route-tree-node.js ├── routing-interface.js ├── routing-mixin.js ├── routing.mixin.js └── routing.mixin.ts ├── package.json ├── router.js ├── test ├── fixtures │ └── custom-fixture.js ├── page-spec.js ├── route-tree-node-spec.js ├── router-spec.js └── utils │ ├── test-route-config.js │ └── testing-route-setup.js ├── tsconfig.buildts.json ├── tsconfig.json ├── types └── index.d.ts ├── vitest.config.ts └── yarn.lock /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @ChadKillingsworth @jrobinson01 @barronhagerman @jivewise @chrisgubbels 2 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "npm" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Tests 5 | 6 | on: 7 | push: 8 | branches: [ master ] 9 | pull_request: 10 | branches: [ master ] 11 | types: [opened, synchronize, ready_for_review] 12 | paths-ignore: 13 | - '**/*.md' 14 | jobs: 15 | test: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: actions/checkout@v4 19 | - name: run tests 20 | uses: actions/setup-node@v4 21 | with: 22 | node-version: '20.x' 23 | - run: yarn install 24 | - run: npx playwright install --with-deps chromium 25 | - run: CI=1 yarn test 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | yarn-error.log 3 | *.d.ts 4 | !types/index.d.ts 5 | .vitest -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | registry=https://registry.npmjs.org/ 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # @jack-henry/web-component-router 2 | 3 | Router for web-components based apps. The router creates a 4 | dom node tree and assigns attributes to the nodes based on path segments. 5 | When switching between routes, the router will re-use any element in 6 | common between the trees and simply update the attributes of existing 7 | elements. Elements should change/reset their state based solely off of 8 | attributes. 9 | 10 | By default, the router places child elements as the sole light-dom child 11 | of the parent (all other nodes are removed). Elements can override the 12 | `routeEnter` method functionality to customize this behavior. 13 | 14 | ## Demo Example 15 | 16 | In the /demo directory, run `yarn && yarn dev` to see the router in a basic application. 17 | 18 | ## Installation 19 | 20 | ``` 21 | npm install @jack-henry/web-component-router 22 | ``` 23 | 24 | ## Defining Routes 25 | 26 | The router uses [Page.js](https://visionmedia.github.io/page.js/) internally 27 | for route path definitions and callbacks. You must start by defining your 28 | routes and route tree. 29 | 30 | To create a tree you create `RouteTreeNodes` and add children. 31 | 32 | ```js 33 | import RouteTreeNode from '@jack-henry/web-component-router/lib/route-tree-node.js'; 34 | const routeNode = new RouteTreeNode(data); 35 | ``` 36 | 37 | Each node requires a `RouteData` object to describe it. 38 | 39 | ```js 40 | import RouteData from '@jack-henry/web-component-router/lib/route-data.js'; 41 | /** 42 | * @param {string} name of this route. Must be unique. 43 | * @param {string} tagName of the element. Case insensitive. 44 | * @param {string} path of this route (express style). 45 | * Empty strings indicate abstract routes - they are not 46 | * directly routable. Their callbacks are invoked and elements 47 | * created when a child route is activated. 48 | * @param {!Array=} namedParameters array in camelCase (optional). 49 | * These should match to named path segments. Each camel case name 50 | * is converted to a hyphenated name to be assigned to the element. 51 | * @param {boolean=} requiresAuthentication (optional - defaults true) 52 | * @param {function():Promise=} beforeEnter Optionally allows you to dynamically import the component for a given route. The route-mixin.js will call your beforeEnter on routeEnter if the component does not exist in the dom. 53 | */ 54 | const routeData = new RouteData( 55 | 'Name of this route', 56 | 'tag-name', 57 | '/path/:namedParameter', 58 | ['namedParameter'], // becomes attribute named-parameter="value" 59 | true, 60 | () => import('../tag-name.js')); 61 | ``` 62 | 63 | It is recommended to use enums and module imports to define the paths 64 | and ids so the strings are maintainable. 65 | 66 | **Example Routing Configuration** 67 | 68 | ```js 69 | /** 70 | * @fileoverview 71 | * 72 | * Route tree definition 73 | * 74 | * ___APP-ELEMENT___ 75 | * / \ 76 | * LOGIN-PAGE ___MAIN-LAYOUT_____ 77 | * / \ 78 | * MAIN-DASHBOARD DETAIL-VIEW 79 | */ 80 | 81 | import {RouteData, RouteTreeNode} from '@jack-henry/web-component-router'; 82 | 83 | const dashboard = new RouteTreeNode( 84 | new RouteData('MainDashboard', 'MAIN-DASHBOARD', '/')); 85 | 86 | const detailView = new RouteTreeNode( 87 | new RouteData('DetailView', 'DETAIL-VIEW', '/detail/:viewId', ['viewId'])); 88 | 89 | // This is an abstract route - you can't visit it directly. 90 | // However it is part of the dom tree 91 | const mainLayout = new RouteTreeNode( 92 | new RouteData('MainLayout', 'MAIN-LAYOUT', '')); 93 | 94 | mainLayout.addChild(dashboard); 95 | mainLayout.addChild(detailView); 96 | 97 | // This is an abstract route - you can't visit it directly. 98 | // However it is part of the dom tree 99 | // It also does not require authentication to view 100 | const app = new RouteTreeNode( 101 | new RouteData('App', 'APP-ELEMENT', '', [], false)); 102 | 103 | app.addChild(mainLayout); 104 | 105 | const loginPage = new RouteTreeNode( 106 | new RouteData('Login', 'LOGIN-PAGE', '/login', [], false)); 107 | 108 | app.addChild(loginPage); 109 | 110 | export default app; 111 | ``` 112 | 113 | ### Defining a route configuration in the Router's constructor 114 | 115 | Alternatively you can pass a `routeConfig` object when instantiating your router. This will use the `RouteTreeNode` and `RouteData` to create your applications routeTree. 116 | 117 | **Example RouteConfig object** 118 | ``` 119 | const routeConfig = { 120 | id: 'app', 121 | tagName: 'APP-MAIN', 122 | path: '', 123 | subRoutes: [{ 124 | id: 'app-user', 125 | tagName: 'APP-USER-PAGE', 126 | path: '/users/:userId([0-9]{1,6})', 127 | params: ['userId'], 128 | beforeEnter: () => import('../app-user-page.js') 129 | }, { 130 | id: 'app-user-account', 131 | tagName: 'APP-ACCOUNT-PAGE', 132 | path: '/users/:userId([0-9]{1,6})/accounts/:accountId([0-9]{1,6})', 133 | params: ['userId', 'accountId'], 134 | beforeEnter: () => import('../app-account-page.js') 135 | }, { 136 | id: 'app-about', 137 | tagName: 'APP-ABOUT', 138 | path: '/about', 139 | authenticated: false, 140 | beforeEnter: () => import('../app-about.js') 141 | }] 142 | }; 143 | 144 | const router = New Router(routeConfig); 145 | ``` 146 | 147 | When using this method the default is that a route requires authentication, as shown above in the 'about' route, set `authenticated` to false to create a route which does not require authentication. 148 | 149 | ## Redirecting 150 | 151 | To programmatically redirect to a page, use `router.go()`: 152 | 153 | ```javascript 154 | // Basic redirect; goes to the root page. 155 | router.go('/'); 156 | 157 | // Specifies a value for named parameter in the path. 158 | // NOTE: You must quote the properties so that Closure Compiler does not rename them! 159 | router.go('/detail/:viewId', {'viewId': id}); 160 | 161 | // Adds a query parameter to the URL. 162 | // NOTE: You must quote the properties so that Closure Compiler does not rename them! 163 | router.go('/login', {'redirect': destAfterLogin}); 164 | ``` 165 | 166 | **Note:** `router.go` usage can quickly become an anti pattern. Using proper HTML anchors with 167 | hrefs is preferable. `router.go` should only be used when programatic route changes are strictly 168 | required. 169 | 170 | ## Creating Routing Enabled Components 171 | 172 | Components used with the router are expected to define two methods 173 | which take the same arguments: 174 | 175 | ```js 176 | class MyElement extends HtmlElement { 177 | /** 178 | * Implementation for the callback on entering a route node. 179 | * routeEnter is called for EVERY route change. If the node 180 | * is shared between the old and new routes, the element 181 | * will be re-used but have attributes updated here. 182 | * 183 | * @param {!RouteTreeNode} currentNode 184 | * @param {!RouteTreeNode|undefined} nextNodeIfExists - the 185 | * child node of this route. 186 | * @param {string} routeId - unique name of the route 187 | * @param {!Context} context - page.js Context object 188 | * @return {!Promise} 189 | */ 190 | async routeEnter(currentNode, nextNodeIfExists, routeId, context) { 191 | // make sure to set this to indicate the route was recognized. 192 | context.handled = true; 193 | // do something with the node 194 | const currentElement = currentNode.getValue().element; 195 | } 196 | 197 | /** 198 | * Implementation for the callback on exiting a route node. 199 | * This method is ONLY called if this element is not being 200 | * used by the next route destination. 201 | * 202 | * @param {!RouteTreeNode} currentNode 203 | * @param {!RouteTreeNode|undefined} nextNode - parent node 204 | * @param {string} routeId - unique name of the route 205 | * @param {!Context} context - page.js Context object 206 | */ 207 | async routeExit(currentNode, nextNode, routeId, context) { 208 | const currentElement = currentNode.getValue().element; 209 | 210 | // remove the element from the dom 211 | if (currentElement.parentNode) { 212 | currentElement.parentNode.removeChild(/** @type {!Element} */ (currentElement)); 213 | } 214 | currentNode.getValue().element = undefined; 215 | } 216 | } 217 | ``` 218 | 219 | Most elements will either use (or inherit) the default implementations. 220 | Two mixins are provided to make this easy. When 221 | using the mixin, `routeEnter` and `routeExit` methods are only need defined 222 | when the default behavior needs modified. In most cases any overridden 223 | method should do minimal work and call `super.routeEnter` or `super.routeExit`. 224 | 225 | **Standard Routing Mixin** 226 | ```js 227 | import routeMixin from '@jack-henry/web-component-router/routing-mixin.js'; 228 | class MyElement extends routeMixin(HTMLElement) { } 229 | ``` 230 | 231 | **Animated Routing Mixin** 232 | 233 | The animated mixin applies a class to animated a node tree on entrance. 234 | Exit animations are currently not supported. 235 | 236 | ```js 237 | import animatedRouteMixin from '@jack-henry/web-component-router/animated-routing-mixin.js'; 238 | class MyElement extends animatedRouteMixin(HTMLElement, 'className') { } 239 | ``` 240 | 241 | ## Root App Element 242 | 243 | The routing configuration is typically defined inside the main app element 244 | which should be defined as the root node of the routing tree. 245 | 246 | The root element typically has a slightly different configuration. 247 | 248 | ```js 249 | import myAppRouteTree from './route-tree.js'; 250 | import router, {Context, routingMixin} from '@jack-henry/web-component-router'; 251 | 252 | class AppElement extends routingMixin(Polymer.Element) { 253 | static get is() { return 'app-element'; } 254 | 255 | connectedCallback() { 256 | super.connectedCallback(); 257 | 258 | router.routeTree = myAppRouteTree; 259 | // Define this instance as the root element 260 | router.routeTree.getValue().element = this; 261 | 262 | // Start routing 263 | router.start(); 264 | } 265 | 266 | async routeEnter(currentNode, nextNodeIfExists, routeId, context) { 267 | context.handled = true; 268 | const destinationNode = router.routeTree.getNodeByKey(routeId); 269 | if (isAuthenticated || !destinationNode.requiresAuthentication()) { 270 | // carry on. user is authenticated or doesn't need to be. 271 | return super.routeEnter(currentNode, nextNodeIfExists, routeId, context); 272 | } 273 | 274 | // Redirect to the login page 275 | router.go('/login'); 276 | 277 | // Don't continue routing - we have redirected to the 278 | // login page 279 | return false; 280 | } 281 | 282 | async routeExit(currentNode, nextNode, routeId, context) { 283 | // This method should never be called. The main app element 284 | // should never be on an exit path as it should always be in 285 | // common no matter what route is activated. 286 | } 287 | } 288 | ``` 289 | 290 | ## Saving Scroll Position 291 | 292 | When using the back button for navigation, the previous route scroll 293 | position should be preserved. To accomplish this, we use a global 294 | page.js exit callback. However, care must be taken as saving the scroll 295 | position should only occur on normal navigation. Back/Forward browser 296 | navigation should not save the scroll position as it causes a timing 297 | issue. 298 | 299 | ```js 300 | import myAppRouteTree from './route-tree.js'; 301 | import router, {routingMixin} from '@jack-henry/web-component-router'; 302 | 303 | class AppElement extends routingMixin(Polymer.Element) { 304 | static get is() { return 'app-element'; } 305 | 306 | connectedCallback() { 307 | super.connectedCallback(); 308 | 309 | router.routeTree = myAppRouteTree; 310 | // Define this instance as the root element 311 | router.routeTree.getValue().element = this; 312 | 313 | // Save the scroll position for every route exit 314 | router.addGlobalExitHandler(this.saveScrollPosition_.bind(this)); 315 | 316 | // Start routing 317 | router.start(); 318 | } 319 | 320 | /** 321 | * @param {!Context} context 322 | * @param {function(boolean=)} next 323 | * @private 324 | */ 325 | saveScrollPosition_(context, next) { 326 | if (!(router.nextStateWasPopped || 'scrollTop' in context.state)) { 327 | context.state['scrollTop'] = this.scrollTop; 328 | context.save(); 329 | } 330 | next(); 331 | } 332 | 333 | async routeEnter(currentNode, nextNodeIfExists, routeId, context) { 334 | // Restoring the scroll position needs to be async 335 | setTimeout(() => { 336 | this.scrollTop = context.state['scrollTop'] || 0; 337 | }, 0); 338 | return super.routeEnter(currentNode, nextNodeIfExists, routeId, context); 339 | } 340 | } 341 | ``` 342 | 343 | ## Router Reference 344 | 345 | ```js 346 | /** 347 | * Get or define the routing tree 348 | * @type {!RouteTreeNode} 349 | */ 350 | router.routeTree; 351 | 352 | /** 353 | * Get the current active route node 354 | * @type {string} 355 | */ 356 | router.currentNodeId; 357 | 358 | /** 359 | * Get the previous active route node 360 | * @type {string} 361 | */ 362 | router.prevNodeId; 363 | 364 | /** 365 | * Begin routing. Should only be called once. The routing tree 366 | * must first be defined. 367 | */ 368 | router.start(); 369 | 370 | /** 371 | * Navigate to the specified route path. 372 | * @param {string} path 373 | * @param {object=} params Values to use for named & query parameters 374 | */ 375 | router.go(path, params); 376 | 377 | /** 378 | * Register an exit callback to be invoked on every route change 379 | * @param {function(!Context, function(boolean=))} callback 380 | */ 381 | router.addGlobalExitHandler(callback); 382 | 383 | /** 384 | * Register a callback function which will be invoked when a route change 385 | * is initiated. 386 | * @param {!Function} callback 387 | */ 388 | router.addRouteChangeStartCallback(callback); 389 | 390 | /** 391 | * Unregister a callback function 392 | * @param {!Function} callback 393 | */ 394 | router.removeRouteChangeStartCallback(callback); 395 | 396 | /** 397 | * Register a callback function which will be invoked when a route change 398 | * is completed. 399 | * @param {!Function} callback 400 | */ 401 | router.addRouteChangeCompleteCallback(callback); 402 | 403 | /** 404 | * Unregister a callback function 405 | * @param {!Function} callback 406 | */ 407 | router.removeRouteChangeCompleteCallback(callback); 408 | 409 | /** 410 | * Anonymize the route path by replacing param values with their 411 | * param name. Used for analytics tracking 412 | * 413 | * @param {!Context} context route enter context 414 | * @return {!string} 415 | */ 416 | const urlPath = router.getRouteUrlWithoutParams(context); 417 | ``` 418 | -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | Routing Example App 9 | 22 | 23 | 24 | 25 | 26 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /demo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "web-router-demo", 3 | "version": "1.1.0", 4 | "private": true, 5 | "license": "UNLICENSED", 6 | "type": "module", 7 | "scripts": { 8 | "lint": "eslint \"src/**/*.js\"", 9 | "dev": "vite", 10 | "build": "vite build", 11 | "preview": "yarn build && vite preview" 12 | }, 13 | "devDependencies": { 14 | "eslint": "^8.48.0", 15 | "lit": "^3.1.1", 16 | "vite": "^5.4.10" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /demo/src/components/base-route.js: -------------------------------------------------------------------------------- 1 | import { LitElement, html, css } from 'lit'; 2 | import Router from '@jack-henry/web-component-router'; 3 | import {routingMixin} from '@jack-henry/web-component-router'; 4 | import {ROUTE_CONFIG, ROUTE_IDS, ROUTE_PATHS} from '../js/route-config.js'; 5 | import './dashboard-route.js'; 6 | import './section-a-route.js'; 7 | import './section-b-route.js'; 8 | 9 | class BaseRoute extends routingMixin(LitElement) { 10 | static styles = css` 11 | :host { 12 | display: block; 13 | background-color: #fff; 14 | padding: 16px; 15 | } 16 | 17 | header { 18 | font-size: 24px; 19 | font-weight: bold; 20 | margin-bottom: 16px; 21 | } 22 | 23 | code { 24 | background-color: #f0f0f0; 25 | color: #3451b2; 26 | padding: 2px 4px; 27 | border-radius: 4px; 28 | } 29 | pre { 30 | background-color: #f0f0f0; 31 | padding: 8px; 32 | border-radius: 4px; 33 | overflow-x: auto; 34 | width: 100%; 35 | box-sizing: border-box; 36 | } 37 | nav { 38 | display: flex; 39 | gap: 16px; 40 | margin-bottom: 16px; 41 | } 42 | a { 43 | text-decoration: none; 44 | color: #3451b2; 45 | font-weight: normal; 46 | } 47 | a[active] { 48 | font-weight: bold; 49 | text-decoration: underline; 50 | } 51 | `; 52 | 53 | static get properties() { 54 | return { 55 | router: { 56 | type: Object, 57 | }, 58 | activeRouteId: { 59 | type: String, 60 | }, 61 | }; 62 | } 63 | 64 | constructor() { 65 | super(); 66 | this.activeRouteId = undefined; 67 | this.router = new Router(ROUTE_CONFIG); 68 | this.router.routeTree.getValue().element = this; 69 | this.router.start(); 70 | } 71 | 72 | async routeEnter(currentNode, nextNodeIfExists, routeId, context) { 73 | await super.routeEnter(currentNode, nextNodeIfExists, routeId, context); 74 | // This method is called when entering a route 75 | console.log('Entering route:', routeId); 76 | console.log('Current node:', currentNode); 77 | console.log('Next node:', nextNodeIfExists); 78 | console.log('Context:', context); 79 | 80 | this.activeRouteId = routeId; 81 | console.log('Active route ID:', this.activeRouteId); 82 | 83 | } 84 | 85 | renderNavigation() { 86 | return html` 87 | 92 | `; 93 | } 94 | 95 | render() { 96 | return html` 97 |
The base route
98 |

This is the map of routes in this application.

99 |
100 | 
101 |         DEMO APP ROUTE TREE
102 | 
103 |         _BASE_ROUTE_
104 |       /       |      \
105 |    DASHBOARD  A      B
106 |              /     / \
107 |             A1   B1  B2
108 |                       |
109 |                       B2A
110 |       
111 |

The BASE_ROUTE component is the first route in the tree (with a path of ' ') and defines our router, all routes that follow are loaded into the slot below.

112 | 113 | ${this.renderNavigation()} 114 | 115 | 116 | 117 | `; 118 | } 119 | } 120 | 121 | // Define the custom element 122 | customElements.define('base-route', BaseRoute); 123 | -------------------------------------------------------------------------------- /demo/src/components/dashboard-route.js: -------------------------------------------------------------------------------- 1 | import { LitElement, html, css } from 'lit'; 2 | import {routingMixin} from '@jack-henry/web-component-router'; 3 | 4 | class DashboardRoute extends routingMixin(LitElement) { 5 | static styles = css` 6 | :host { 7 | display: block; 8 | background-color: #fff; 9 | padding: 16px; 10 | } 11 | `; 12 | 13 | render() { 14 | return html` 15 |

Dashboard Route

16 | `; 17 | } 18 | } 19 | 20 | // Define the custom element 21 | customElements.define('dashboard-route', DashboardRoute); 22 | -------------------------------------------------------------------------------- /demo/src/components/example-app.js: -------------------------------------------------------------------------------- 1 | import { LitElement, html, css } from 'lit'; 2 | import './base-route.js'; 3 | 4 | class ExampleApp extends LitElement { 5 | static styles = css` 6 | :host { 7 | display: block; 8 | background-color: #f0f0f0; 9 | padding: 16px; 10 | } 11 | 12 | header { 13 | font-size: 24px; 14 | font-weight: bold; 15 | margin-bottom: 16px; 16 | } 17 | `; 18 | 19 | render() { 20 | return html` 21 |
Example routing app
22 |

All routes will contain this text

23 | 24 | `; 25 | } 26 | } 27 | 28 | // Define the custom element 29 | customElements.define('example-app', ExampleApp); 30 | -------------------------------------------------------------------------------- /demo/src/components/section-a-route.js: -------------------------------------------------------------------------------- 1 | import { LitElement, html, css } from 'lit'; 2 | import {routingMixin} from '@jack-henry/web-component-router'; 3 | import './section-a1-route.js'; 4 | 5 | class SectionARoute extends routingMixin(LitElement) { 6 | static styles = css` 7 | :host { 8 | display: block; 9 | background-color: #fff; 10 | padding: 16px; 11 | } 12 | `; 13 | 14 | static get properties() { 15 | return { 16 | activeRouteId: { 17 | type: String, 18 | }, 19 | }; 20 | } 21 | 22 | constructor() { 23 | super(); 24 | this.activeRouteId = undefined; 25 | } 26 | 27 | async routeEnter(currentNode, nextNodeIfExists, routeId, context) { 28 | await super.routeEnter(currentNode, nextNodeIfExists, routeId, context); 29 | this.activeRouteId = routeId; 30 | } 31 | 32 | render() { 33 | return html` 34 |

Section A

35 | 36 |

This section has its own subRoute, which are loaded in slot below.

37 | 38 |

Active route ID: ${this.activeRouteId}

39 | 40 | 44 | 45 | 46 |
47 | 48 |

This placeholder will be replaced by the active subRoute.

49 |
50 |
51 | 52 | `; 53 | } 54 | } 55 | 56 | // Define the custom element 57 | customElements.define('section-a-route', SectionARoute); 58 | -------------------------------------------------------------------------------- /demo/src/components/section-a1-route.js: -------------------------------------------------------------------------------- 1 | import { LitElement, html, css } from 'lit'; 2 | import {routingMixin} from '@jack-henry/web-component-router'; 3 | 4 | class SectionA1Route extends routingMixin(LitElement) { 5 | static styles = css` 6 | :host { 7 | display: block; 8 | background-color: #fff; 9 | padding: 16px; 10 | } 11 | `; 12 | 13 | static get properties() { 14 | return { 15 | activeRouteId: { 16 | type: String, 17 | }, 18 | sectionAId: { 19 | type: String, 20 | attribute: 'section-a-id', 21 | }, 22 | }; 23 | } 24 | 25 | constructor() { 26 | super(); 27 | this.activeRouteId = undefined; 28 | this.sectionAId = undefined; 29 | } 30 | 31 | connectedCallback() { 32 | super.connectedCallback(); 33 | console.log('SectionA1Route connectedCallback'); 34 | } 35 | 36 | async routeEnter(currentNode, nextNodeIfExists, routeId, context) { 37 | await super.routeEnter(currentNode, nextNodeIfExists, routeId, context); 38 | this.activeRouteId = routeId; 39 | } 40 | 41 | render() { 42 | return html` 43 |

Section A1

44 | 45 |

This child route utilizes a parameter in the url path (sectionAId)

46 |

The component defines the property and the camel-case attribute

47 | 48 |
49 |   static get properties() {
50 |     return {
51 |       activeRouteId: {
52 |         type: String,
53 |       },
54 |       sectionAId: {
55 |         type: String,
56 |         attribute: 'section-a-id',
57 |       },
58 |     };
59 |   }
60 |       
61 | 62 |

Section A1 ID: ${this.sectionAId}

63 | `; 64 | } 65 | } 66 | 67 | // Define the custom element 68 | customElements.define('section-a1-route', SectionA1Route); 69 | -------------------------------------------------------------------------------- /demo/src/components/section-b-route.js: -------------------------------------------------------------------------------- 1 | import { LitElement, html, css } from 'lit'; 2 | import {routingMixin} from '@jack-henry/web-component-router'; 3 | import './section-b1-route.js'; 4 | import './section-b2-route.js'; 5 | 6 | class SectionBRoute extends routingMixin(LitElement) { 7 | static styles = css` 8 | :host { 9 | display: block; 10 | background-color: #fff; 11 | padding: 16px; 12 | } 13 | `; 14 | 15 | static get properties() { 16 | return { 17 | activeRouteId: { 18 | type: String, 19 | }, 20 | }; 21 | } 22 | 23 | constructor() { 24 | super(); 25 | this.activeRouteId = undefined; 26 | } 27 | 28 | async routeEnter(currentNode, nextNodeIfExists, routeId, context) { 29 | await super.routeEnter(currentNode, nextNodeIfExists, routeId, context); 30 | this.activeRouteId = routeId; 31 | } 32 | 33 | render() { 34 | return html` 35 |

Section B

36 | 37 |

This route has two child routes (B1 and B2). Each has a unique param which the router gets from the path and sets on the component.

38 |

This parent component is loaded for each sub route, but not reloaded when routing between children, so shared actions can be taken here for performance.

39 | 43 | 44 | 45 |
46 | 47 |

This placeholder will be replaced by the active subRoute.

48 |
49 |
50 | 51 | `; 52 | } 53 | } 54 | 55 | // Define the custom element 56 | customElements.define('section-b-route', SectionBRoute); 57 | -------------------------------------------------------------------------------- /demo/src/components/section-b1-route.js: -------------------------------------------------------------------------------- 1 | import { LitElement, html, css } from 'lit'; 2 | import {routingMixin} from '@jack-henry/web-component-router'; 3 | 4 | class SectionB1Route extends routingMixin(LitElement) { 5 | static styles = css` 6 | :host { 7 | display: block; 8 | background-color: #fff; 9 | padding: 16px; 10 | } 11 | `; 12 | 13 | static get properties() { 14 | return { 15 | activeRouteId: { 16 | type: String, 17 | }, 18 | sectionB1Id: { 19 | type: String, 20 | attribute: 'section-b1-id', 21 | }, 22 | }; 23 | } 24 | 25 | constructor() { 26 | super(); 27 | this.activeRouteId = undefined; 28 | this.sectionB1Id = undefined; 29 | } 30 | 31 | connectedCallback() { 32 | super.connectedCallback(); 33 | console.log('SectionA1Route connectedCallback'); 34 | } 35 | 36 | async routeEnter(currentNode, nextNodeIfExists, routeId, context) { 37 | await super.routeEnter(currentNode, nextNodeIfExists, routeId, context); 38 | this.activeRouteId = routeId; 39 | } 40 | 41 | render() { 42 | return html` 43 |

Section B1

44 | 45 |

This child route utilizes a parameter in the url path (sectionB1Id)

46 |

The component defines the property and the camel-case attribute

47 | 48 |
49 |   static get properties() {
50 |     return {
51 |       activeRouteId: {
52 |         type: String,
53 |       },
54 |       sectionAId: {
55 |         type: String,
56 |         attribute: 'section-b-1-id',
57 |       },
58 |     };
59 |   }
60 |       
61 | 62 |

Section b1 ID: ${this.sectionB1Id}

63 | `; 64 | } 65 | } 66 | 67 | // Define the custom element 68 | customElements.define('section-b1-route', SectionB1Route); 69 | -------------------------------------------------------------------------------- /demo/src/components/section-b2-route.js: -------------------------------------------------------------------------------- 1 | import { LitElement, html, css } from 'lit'; 2 | import {routingMixin} from '@jack-henry/web-component-router'; 3 | import './section-b2a-route.js'; 4 | 5 | class SectionB2Route extends routingMixin(LitElement) { 6 | static styles = css` 7 | :host { 8 | display: block; 9 | background-color: #fff; 10 | padding: 16px; 11 | } 12 | article { 13 | display: flex; 14 | gap: 16px; 15 | margin-top: 16px; 16 | } 17 | article > * { 18 | flex: 1; 19 | width: calc(50% - 8px); 20 | } 21 | article > div { 22 | border-left: 1px solid #ccc; 23 | padding-left: 16px; 24 | } 25 | `; 26 | 27 | static get properties() { 28 | return { 29 | activeRouteId: { 30 | type: String, 31 | }, 32 | sectionB2Id: { 33 | type: String, 34 | attribute: 'section-b2-id', 35 | }, 36 | }; 37 | } 38 | 39 | constructor() { 40 | super(); 41 | this.activeRouteId = undefined; 42 | this.sectionB2Id = undefined; 43 | } 44 | 45 | render() { 46 | return html` 47 |

Section B2

48 | 49 |

This subRoute route also has a subRoute

50 |

Both routes share the sectionB2Id property/param from the route.

51 |

And the subroute also has the sectionB2AId property set by the url.

52 |
53 |   static get properties() {
54 |     return {
55 |       activeRouteId: {
56 |         type: String,
57 |       },
58 |       sectionAId: {
59 |         type: String,
60 |         attribute: 'section-b-2-id',
61 |       },
62 |     };
63 |   }
64 |       
65 |
66 |

Section b2 ID: ${this.sectionB2Id}

67 |
68 | 69 |

Click the link to load the subRoute

70 | Section B2A 71 |
72 |
73 |
74 | `; 75 | } 76 | } 77 | 78 | // Define the custom element 79 | customElements.define('section-b2-route', SectionB2Route); 80 | -------------------------------------------------------------------------------- /demo/src/components/section-b2a-route.js: -------------------------------------------------------------------------------- 1 | import { LitElement, html, css } from 'lit'; 2 | import {routingMixin} from '@jack-henry/web-component-router'; 3 | 4 | class SectionB2ARoute extends routingMixin(LitElement) { 5 | static styles = css` 6 | :host { 7 | display: block; 8 | } 9 | `; 10 | 11 | static get properties() { 12 | return { 13 | activeRouteId: { 14 | type: String, 15 | }, 16 | sectionB2Id: { 17 | type: String, 18 | attribute: 'section-b2-id', 19 | }, 20 | sectionB2AId: { 21 | type: String, 22 | attribute: 'section-b2-a-id', 23 | }, 24 | }; 25 | } 26 | 27 | constructor() { 28 | super(); 29 | this.activeRouteId = undefined; 30 | this.sectionB2Id = undefined; 31 | this.sectionB2AId = undefined; 32 | } 33 | 34 | render() { 35 | return html` 36 |

Section b2a ID: ${this.sectionB2AId}

37 |

As you'll see here, sub routes don't need to replace the main content of the page, but are inserted in the default slot of the parent component.

38 | Back 39 | `; 40 | } 41 | } 42 | 43 | // Define the custom element 44 | customElements.define('section-b2a-route', SectionB2ARoute); 45 | -------------------------------------------------------------------------------- /demo/src/js/route-config.js: -------------------------------------------------------------------------------- 1 | export const ROUTE_IDS = { 2 | BASE: 'base', 3 | DASHBOARD: 'dashboard', 4 | SECTION_A: 'section-a', 5 | SECTION_A1: 'section-a1', 6 | SECTION_B: 'section-b', 7 | SECTION_B1: 'section-b1', 8 | SECTION_B2: 'section-b2', 9 | SECTION_B2A: 'section-b2a', 10 | } 11 | 12 | export const ROUTE_PATHS = { 13 | BASE: '', 14 | DASHBOARD: '/', 15 | SECTION_A: '/section-a', 16 | SECTION_A1: '/section-a/:sectionAId([a-z-]+)', 17 | SECTION_B: '/section-b', 18 | SECTION_B1: '/section-b/b1/:sectionB1Id([a-z-]+)', 19 | SECTION_B2: '/section-b/b2/:sectionB2Id([a-z-]+)', 20 | SECTION_B2A: '/section-b/b2/:sectionB2Id([a-z-]+)/b2a/:sectionB2AId([a-z-]+)', 21 | } 22 | 23 | export const ROUTE_CONFIG = { 24 | id: ROUTE_IDS.BASE, 25 | path: ROUTE_PATHS.BASE, 26 | tagName: 'base-route', 27 | subRoutes: [ 28 | { 29 | id: ROUTE_IDS.DASHBOARD, 30 | path: ROUTE_PATHS.DASHBOARD, 31 | tagName: 'dashboard-route', 32 | }, { 33 | id: ROUTE_IDS.SECTION_A, 34 | path: ROUTE_PATHS.SECTION_A, 35 | tagName: 'section-a-route', 36 | subRoutes: [ 37 | { 38 | id: ROUTE_IDS.SECTION_A1, 39 | path: ROUTE_PATHS.SECTION_A1, 40 | tagName: 'section-a1-route', 41 | params: ['sectionAId'], 42 | } 43 | ], 44 | }, { 45 | id: ROUTE_IDS.SECTION_B, 46 | path: ROUTE_PATHS.SECTION_B, 47 | tagName: 'section-b-route', 48 | subRoutes: [ 49 | { 50 | id: ROUTE_IDS.SECTION_B1, 51 | path: ROUTE_PATHS.SECTION_B1, 52 | tagName: 'section-b1-route', 53 | params: ['sectionB1Id'], 54 | }, 55 | { 56 | id: ROUTE_IDS.SECTION_B2, 57 | path: ROUTE_PATHS.SECTION_B2, 58 | tagName: 'section-b2-route', 59 | params: ['sectionB2Id'], 60 | subRoutes: [ 61 | { 62 | id: ROUTE_IDS.SECTION_B2A, 63 | path: ROUTE_PATHS.SECTION_B2A, 64 | tagName: 'section-b2a-route', 65 | params: ['sectionB2Id', 'sectionB2AId'], 66 | } 67 | ], 68 | } 69 | ], 70 | } 71 | ], 72 | }; 73 | -------------------------------------------------------------------------------- /demo/vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import { fileURLToPath, URL } from 'node:url'; 3 | 4 | export default defineConfig({ 5 | resolve: { 6 | alias: { 7 | '@jack-henry/web-component-router': fileURLToPath(new URL('../', import.meta.url)), 8 | }, 9 | }, 10 | }); 11 | -------------------------------------------------------------------------------- /demo/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | "@esbuild/aix-ppc64@0.21.5": 6 | version "0.21.5" 7 | resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz#c7184a326533fcdf1b8ee0733e21c713b975575f" 8 | integrity sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ== 9 | 10 | "@esbuild/android-arm64@0.21.5": 11 | version "0.21.5" 12 | resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz#09d9b4357780da9ea3a7dfb833a1f1ff439b4052" 13 | integrity sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A== 14 | 15 | "@esbuild/android-arm@0.21.5": 16 | version "0.21.5" 17 | resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.21.5.tgz#9b04384fb771926dfa6d7ad04324ecb2ab9b2e28" 18 | integrity sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg== 19 | 20 | "@esbuild/android-x64@0.21.5": 21 | version "0.21.5" 22 | resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.21.5.tgz#29918ec2db754cedcb6c1b04de8cd6547af6461e" 23 | integrity sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA== 24 | 25 | "@esbuild/darwin-arm64@0.21.5": 26 | version "0.21.5" 27 | resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz#e495b539660e51690f3928af50a76fb0a6ccff2a" 28 | integrity sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ== 29 | 30 | "@esbuild/darwin-x64@0.21.5": 31 | version "0.21.5" 32 | resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz#c13838fa57372839abdddc91d71542ceea2e1e22" 33 | integrity sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw== 34 | 35 | "@esbuild/freebsd-arm64@0.21.5": 36 | version "0.21.5" 37 | resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz#646b989aa20bf89fd071dd5dbfad69a3542e550e" 38 | integrity sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g== 39 | 40 | "@esbuild/freebsd-x64@0.21.5": 41 | version "0.21.5" 42 | resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz#aa615cfc80af954d3458906e38ca22c18cf5c261" 43 | integrity sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ== 44 | 45 | "@esbuild/linux-arm64@0.21.5": 46 | version "0.21.5" 47 | resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz#70ac6fa14f5cb7e1f7f887bcffb680ad09922b5b" 48 | integrity sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q== 49 | 50 | "@esbuild/linux-arm@0.21.5": 51 | version "0.21.5" 52 | resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz#fc6fd11a8aca56c1f6f3894f2bea0479f8f626b9" 53 | integrity sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA== 54 | 55 | "@esbuild/linux-ia32@0.21.5": 56 | version "0.21.5" 57 | resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz#3271f53b3f93e3d093d518d1649d6d68d346ede2" 58 | integrity sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg== 59 | 60 | "@esbuild/linux-loong64@0.21.5": 61 | version "0.21.5" 62 | resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz#ed62e04238c57026aea831c5a130b73c0f9f26df" 63 | integrity sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg== 64 | 65 | "@esbuild/linux-mips64el@0.21.5": 66 | version "0.21.5" 67 | resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz#e79b8eb48bf3b106fadec1ac8240fb97b4e64cbe" 68 | integrity sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg== 69 | 70 | "@esbuild/linux-ppc64@0.21.5": 71 | version "0.21.5" 72 | resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz#5f2203860a143b9919d383ef7573521fb154c3e4" 73 | integrity sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w== 74 | 75 | "@esbuild/linux-riscv64@0.21.5": 76 | version "0.21.5" 77 | resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz#07bcafd99322d5af62f618cb9e6a9b7f4bb825dc" 78 | integrity sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA== 79 | 80 | "@esbuild/linux-s390x@0.21.5": 81 | version "0.21.5" 82 | resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz#b7ccf686751d6a3e44b8627ababc8be3ef62d8de" 83 | integrity sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A== 84 | 85 | "@esbuild/linux-x64@0.21.5": 86 | version "0.21.5" 87 | resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz#6d8f0c768e070e64309af8004bb94e68ab2bb3b0" 88 | integrity sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ== 89 | 90 | "@esbuild/netbsd-x64@0.21.5": 91 | version "0.21.5" 92 | resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz#bbe430f60d378ecb88decb219c602667387a6047" 93 | integrity sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg== 94 | 95 | "@esbuild/openbsd-x64@0.21.5": 96 | version "0.21.5" 97 | resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz#99d1cf2937279560d2104821f5ccce220cb2af70" 98 | integrity sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow== 99 | 100 | "@esbuild/sunos-x64@0.21.5": 101 | version "0.21.5" 102 | resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz#08741512c10d529566baba837b4fe052c8f3487b" 103 | integrity sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg== 104 | 105 | "@esbuild/win32-arm64@0.21.5": 106 | version "0.21.5" 107 | resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz#675b7385398411240735016144ab2e99a60fc75d" 108 | integrity sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A== 109 | 110 | "@esbuild/win32-ia32@0.21.5": 111 | version "0.21.5" 112 | resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz#1bfc3ce98aa6ca9a0969e4d2af72144c59c1193b" 113 | integrity sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA== 114 | 115 | "@esbuild/win32-x64@0.21.5": 116 | version "0.21.5" 117 | resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz#acad351d582d157bb145535db2a6ff53dd514b5c" 118 | integrity sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw== 119 | 120 | "@eslint-community/eslint-utils@^4.2.0": 121 | version "4.7.0" 122 | resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz#607084630c6c033992a082de6e6fbc1a8b52175a" 123 | integrity sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw== 124 | dependencies: 125 | eslint-visitor-keys "^3.4.3" 126 | 127 | "@eslint-community/regexpp@^4.6.1": 128 | version "4.12.1" 129 | resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.12.1.tgz#cfc6cffe39df390a3841cde2abccf92eaa7ae0e0" 130 | integrity sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ== 131 | 132 | "@eslint/eslintrc@^2.1.4": 133 | version "2.1.4" 134 | resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-2.1.4.tgz#388a269f0f25c1b6adc317b5a2c55714894c70ad" 135 | integrity sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ== 136 | dependencies: 137 | ajv "^6.12.4" 138 | debug "^4.3.2" 139 | espree "^9.6.0" 140 | globals "^13.19.0" 141 | ignore "^5.2.0" 142 | import-fresh "^3.2.1" 143 | js-yaml "^4.1.0" 144 | minimatch "^3.1.2" 145 | strip-json-comments "^3.1.1" 146 | 147 | "@eslint/js@8.57.1": 148 | version "8.57.1" 149 | resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.57.1.tgz#de633db3ec2ef6a3c89e2f19038063e8a122e2c2" 150 | integrity sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q== 151 | 152 | "@humanwhocodes/config-array@^0.13.0": 153 | version "0.13.0" 154 | resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.13.0.tgz#fb907624df3256d04b9aa2df50d7aa97ec648748" 155 | integrity sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw== 156 | dependencies: 157 | "@humanwhocodes/object-schema" "^2.0.3" 158 | debug "^4.3.1" 159 | minimatch "^3.0.5" 160 | 161 | "@humanwhocodes/module-importer@^1.0.1": 162 | version "1.0.1" 163 | resolved "https://registry.yarnpkg.com/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz#af5b2691a22b44be847b0ca81641c5fb6ad0172c" 164 | integrity sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA== 165 | 166 | "@humanwhocodes/object-schema@^2.0.3": 167 | version "2.0.3" 168 | resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz#4a2868d75d6d6963e423bcf90b7fd1be343409d3" 169 | integrity sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA== 170 | 171 | "@lit-labs/ssr-dom-shim@^1.2.0": 172 | version "1.3.0" 173 | resolved "https://registry.yarnpkg.com/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.3.0.tgz#a28799c463177d1a0b0e5cefdc173da5ac859eb4" 174 | integrity sha512-nQIWonJ6eFAvUUrSlwyHDm/aE8PBDu5kRpL0vHMg6K8fK3Diq1xdPjTnsJSwxABhaZ+5eBi1btQB5ShUTKo4nQ== 175 | 176 | "@lit/reactive-element@^2.1.0": 177 | version "2.1.0" 178 | resolved "https://registry.yarnpkg.com/@lit/reactive-element/-/reactive-element-2.1.0.tgz#177148214488068ae209669040b7ce0f4dcc0d36" 179 | integrity sha512-L2qyoZSQClcBmq0qajBVbhYEcG6iK0XfLn66ifLe/RfC0/ihpc+pl0Wdn8bJ8o+hj38cG0fGXRgSS20MuXn7qA== 180 | dependencies: 181 | "@lit-labs/ssr-dom-shim" "^1.2.0" 182 | 183 | "@nodelib/fs.scandir@2.1.5": 184 | version "2.1.5" 185 | resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" 186 | integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g== 187 | dependencies: 188 | "@nodelib/fs.stat" "2.0.5" 189 | run-parallel "^1.1.9" 190 | 191 | "@nodelib/fs.stat@2.0.5": 192 | version "2.0.5" 193 | resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b" 194 | integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== 195 | 196 | "@nodelib/fs.walk@^1.2.8": 197 | version "1.2.8" 198 | resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a" 199 | integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg== 200 | dependencies: 201 | "@nodelib/fs.scandir" "2.1.5" 202 | fastq "^1.6.0" 203 | 204 | "@rollup/rollup-android-arm-eabi@4.41.0": 205 | version "4.41.0" 206 | resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.41.0.tgz#9145b38faf3fbfe3ec557130110e772f797335aa" 207 | integrity sha512-KxN+zCjOYHGwCl4UCtSfZ6jrq/qi88JDUtiEFk8LELEHq2Egfc/FgW+jItZiOLRuQfb/3xJSgFuNPC9jzggX+A== 208 | 209 | "@rollup/rollup-android-arm64@4.41.0": 210 | version "4.41.0" 211 | resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.41.0.tgz#d73d641c59e9d7827e5ce0af9dfbc168b95cce0f" 212 | integrity sha512-yDvqx3lWlcugozax3DItKJI5j05B0d4Kvnjx+5mwiUpWramVvmAByYigMplaoAQ3pvdprGCTCE03eduqE/8mPQ== 213 | 214 | "@rollup/rollup-darwin-arm64@4.41.0": 215 | version "4.41.0" 216 | resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.41.0.tgz#45d9d71d941117c98e7a5e77f60f0bc682d27e82" 217 | integrity sha512-2KOU574vD3gzcPSjxO0eyR5iWlnxxtmW1F5CkNOHmMlueKNCQkxR6+ekgWyVnz6zaZihpUNkGxjsYrkTJKhkaw== 218 | 219 | "@rollup/rollup-darwin-x64@4.41.0": 220 | version "4.41.0" 221 | resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.41.0.tgz#8d72fb5f81714cb43e90f263fb1674520cce3f2a" 222 | integrity sha512-gE5ACNSxHcEZyP2BA9TuTakfZvULEW4YAOtxl/A/YDbIir/wPKukde0BNPlnBiP88ecaN4BJI2TtAd+HKuZPQQ== 223 | 224 | "@rollup/rollup-freebsd-arm64@4.41.0": 225 | version "4.41.0" 226 | resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.41.0.tgz#a52b58852c9cec9255e382a2f335b08bc8c6111d" 227 | integrity sha512-GSxU6r5HnWij7FoSo7cZg3l5GPg4HFLkzsFFh0N/b16q5buW1NAWuCJ+HMtIdUEi6XF0qH+hN0TEd78laRp7Dg== 228 | 229 | "@rollup/rollup-freebsd-x64@4.41.0": 230 | version "4.41.0" 231 | resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.41.0.tgz#104511dc64612789ddda41d164ab07cdac84a6c1" 232 | integrity sha512-KGiGKGDg8qLRyOWmk6IeiHJzsN/OYxO6nSbT0Vj4MwjS2XQy/5emsmtoqLAabqrohbgLWJ5GV3s/ljdrIr8Qjg== 233 | 234 | "@rollup/rollup-linux-arm-gnueabihf@4.41.0": 235 | version "4.41.0" 236 | resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.41.0.tgz#643e3ad19c93903201fde89abd76baaee725e6c2" 237 | integrity sha512-46OzWeqEVQyX3N2/QdiU/CMXYDH/lSHpgfBkuhl3igpZiaB3ZIfSjKuOnybFVBQzjsLwkus2mjaESy8H41SzvA== 238 | 239 | "@rollup/rollup-linux-arm-musleabihf@4.41.0": 240 | version "4.41.0" 241 | resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.41.0.tgz#fdc6a595aec7b20c5bfdac81412028c56d734e63" 242 | integrity sha512-lfgW3KtQP4YauqdPpcUZHPcqQXmTmH4nYU0cplNeW583CMkAGjtImw4PKli09NFi2iQgChk4e9erkwlfYem6Lg== 243 | 244 | "@rollup/rollup-linux-arm64-gnu@4.41.0": 245 | version "4.41.0" 246 | resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.41.0.tgz#c28620bcd385496bdbbc24920b21f9fcca9ecbfa" 247 | integrity sha512-nn8mEyzMbdEJzT7cwxgObuwviMx6kPRxzYiOl6o/o+ChQq23gfdlZcUNnt89lPhhz3BYsZ72rp0rxNqBSfqlqw== 248 | 249 | "@rollup/rollup-linux-arm64-musl@4.41.0": 250 | version "4.41.0" 251 | resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.41.0.tgz#a6b71b1e8fa33bac9f65b6f879e8ed878035d120" 252 | integrity sha512-l+QK99je2zUKGd31Gh+45c4pGDAqZSuWQiuRFCdHYC2CSiO47qUWsCcenrI6p22hvHZrDje9QjwSMAFL3iwXwQ== 253 | 254 | "@rollup/rollup-linux-loongarch64-gnu@4.41.0": 255 | version "4.41.0" 256 | resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.41.0.tgz#b06374601ce865a1110324b2f06db574d3a1b0e1" 257 | integrity sha512-WbnJaxPv1gPIm6S8O/Wg+wfE/OzGSXlBMbOe4ie+zMyykMOeqmgD1BhPxZQuDqwUN+0T/xOFtL2RUWBspnZj3w== 258 | 259 | "@rollup/rollup-linux-powerpc64le-gnu@4.41.0": 260 | version "4.41.0" 261 | resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.41.0.tgz#8a2a1f6058c920889c2aff3753a20fead7a8cc26" 262 | integrity sha512-eRDWR5t67/b2g8Q/S8XPi0YdbKcCs4WQ8vklNnUYLaSWF+Cbv2axZsp4jni6/j7eKvMLYCYdcsv8dcU+a6QNFg== 263 | 264 | "@rollup/rollup-linux-riscv64-gnu@4.41.0": 265 | version "4.41.0" 266 | resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.41.0.tgz#8ef6f680d011b95a2f6546c6c31a37a33138035f" 267 | integrity sha512-TWrZb6GF5jsEKG7T1IHwlLMDRy2f3DPqYldmIhnA2DVqvvhY2Ai184vZGgahRrg8k9UBWoSlHv+suRfTN7Ua4A== 268 | 269 | "@rollup/rollup-linux-riscv64-musl@4.41.0": 270 | version "4.41.0" 271 | resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.41.0.tgz#9f4884c5955a7cd39b396f6e27aa59b3269988eb" 272 | integrity sha512-ieQljaZKuJpmWvd8gW87ZmSFwid6AxMDk5bhONJ57U8zT77zpZ/TPKkU9HpnnFrM4zsgr4kiGuzbIbZTGi7u9A== 273 | 274 | "@rollup/rollup-linux-s390x-gnu@4.41.0": 275 | version "4.41.0" 276 | resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.41.0.tgz#5619303cc51994e3df404a497f42c79dc5efd6eb" 277 | integrity sha512-/L3pW48SxrWAlVsKCN0dGLB2bi8Nv8pr5S5ocSM+S0XCn5RCVCXqi8GVtHFsOBBCSeR+u9brV2zno5+mg3S4Aw== 278 | 279 | "@rollup/rollup-linux-x64-gnu@4.41.0": 280 | version "4.41.0" 281 | resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.41.0.tgz#c3e42b66c04e25ad0f2a00beec42ede96ccc8983" 282 | integrity sha512-XMLeKjyH8NsEDCRptf6LO8lJk23o9wvB+dJwcXMaH6ZQbbkHu2dbGIUindbMtRN6ux1xKi16iXWu6q9mu7gDhQ== 283 | 284 | "@rollup/rollup-linux-x64-musl@4.41.0": 285 | version "4.41.0" 286 | resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.41.0.tgz#8d3452de42aa72fc5fc3e5ad1eb0b68030742a25" 287 | integrity sha512-m/P7LycHZTvSQeXhFmgmdqEiTqSV80zn6xHaQ1JSqwCtD1YGtwEK515Qmy9DcB2HK4dOUVypQxvhVSy06cJPEg== 288 | 289 | "@rollup/rollup-win32-arm64-msvc@4.41.0": 290 | version "4.41.0" 291 | resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.41.0.tgz#3b7bbd9f43f1c380061f306abce6f3f64de20306" 292 | integrity sha512-4yodtcOrFHpbomJGVEqZ8fzD4kfBeCbpsUy5Pqk4RluXOdsWdjLnjhiKy2w3qzcASWd04fp52Xz7JKarVJ5BTg== 293 | 294 | "@rollup/rollup-win32-ia32-msvc@4.41.0": 295 | version "4.41.0" 296 | resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.41.0.tgz#e27ef5c40bbec49fac3d4e4b1618fbe4597b40e5" 297 | integrity sha512-tmazCrAsKzdkXssEc65zIE1oC6xPHwfy9d5Ta25SRCDOZS+I6RypVVShWALNuU9bxIfGA0aqrmzlzoM5wO5SPQ== 298 | 299 | "@rollup/rollup-win32-x64-msvc@4.41.0": 300 | version "4.41.0" 301 | resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.41.0.tgz#b0b595ad4720259bbb81600750d26a655cac06be" 302 | integrity sha512-h1J+Yzjo/X+0EAvR2kIXJDuTuyT7drc+t2ALY0nIcGPbTatNOf0VWdhEA2Z4AAjv6X1NJV7SYo5oCTYRJhSlVA== 303 | 304 | "@types/estree@1.0.7": 305 | version "1.0.7" 306 | resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.7.tgz#4158d3105276773d5b7695cd4834b1722e4f37a8" 307 | integrity sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ== 308 | 309 | "@types/trusted-types@^2.0.2": 310 | version "2.0.7" 311 | resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-2.0.7.tgz#baccb07a970b91707df3a3e8ba6896c57ead2d11" 312 | integrity sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw== 313 | 314 | "@ungap/structured-clone@^1.2.0": 315 | version "1.3.0" 316 | resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.3.0.tgz#d06bbb384ebcf6c505fde1c3d0ed4ddffe0aaff8" 317 | integrity sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g== 318 | 319 | acorn-jsx@^5.3.2: 320 | version "5.3.2" 321 | resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" 322 | integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== 323 | 324 | acorn@^8.9.0: 325 | version "8.14.1" 326 | resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.14.1.tgz#721d5dc10f7d5b5609a891773d47731796935dfb" 327 | integrity sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg== 328 | 329 | ajv@^6.12.4: 330 | version "6.12.6" 331 | resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" 332 | integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== 333 | dependencies: 334 | fast-deep-equal "^3.1.1" 335 | fast-json-stable-stringify "^2.0.0" 336 | json-schema-traverse "^0.4.1" 337 | uri-js "^4.2.2" 338 | 339 | ansi-regex@^5.0.1: 340 | version "5.0.1" 341 | resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" 342 | integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== 343 | 344 | ansi-styles@^4.1.0: 345 | version "4.3.0" 346 | resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" 347 | integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== 348 | dependencies: 349 | color-convert "^2.0.1" 350 | 351 | argparse@^2.0.1: 352 | version "2.0.1" 353 | resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" 354 | integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== 355 | 356 | balanced-match@^1.0.0: 357 | version "1.0.2" 358 | resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" 359 | integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== 360 | 361 | brace-expansion@^1.1.7: 362 | version "1.1.11" 363 | resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" 364 | integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== 365 | dependencies: 366 | balanced-match "^1.0.0" 367 | concat-map "0.0.1" 368 | 369 | callsites@^3.0.0: 370 | version "3.1.0" 371 | resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" 372 | integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== 373 | 374 | chalk@^4.0.0: 375 | version "4.1.2" 376 | resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" 377 | integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== 378 | dependencies: 379 | ansi-styles "^4.1.0" 380 | supports-color "^7.1.0" 381 | 382 | color-convert@^2.0.1: 383 | version "2.0.1" 384 | resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" 385 | integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== 386 | dependencies: 387 | color-name "~1.1.4" 388 | 389 | color-name@~1.1.4: 390 | version "1.1.4" 391 | resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" 392 | integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== 393 | 394 | concat-map@0.0.1: 395 | version "0.0.1" 396 | resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" 397 | integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== 398 | 399 | cross-spawn@^7.0.2: 400 | version "7.0.6" 401 | resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f" 402 | integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA== 403 | dependencies: 404 | path-key "^3.1.0" 405 | shebang-command "^2.0.0" 406 | which "^2.0.1" 407 | 408 | debug@^4.3.1, debug@^4.3.2: 409 | version "4.4.1" 410 | resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.1.tgz#e5a8bc6cbc4c6cd3e64308b0693a3d4fa550189b" 411 | integrity sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ== 412 | dependencies: 413 | ms "^2.1.3" 414 | 415 | deep-is@^0.1.3: 416 | version "0.1.4" 417 | resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831" 418 | integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== 419 | 420 | doctrine@^3.0.0: 421 | version "3.0.0" 422 | resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961" 423 | integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w== 424 | dependencies: 425 | esutils "^2.0.2" 426 | 427 | esbuild@^0.21.3: 428 | version "0.21.5" 429 | resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.21.5.tgz#9ca301b120922959b766360d8ac830da0d02997d" 430 | integrity sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw== 431 | optionalDependencies: 432 | "@esbuild/aix-ppc64" "0.21.5" 433 | "@esbuild/android-arm" "0.21.5" 434 | "@esbuild/android-arm64" "0.21.5" 435 | "@esbuild/android-x64" "0.21.5" 436 | "@esbuild/darwin-arm64" "0.21.5" 437 | "@esbuild/darwin-x64" "0.21.5" 438 | "@esbuild/freebsd-arm64" "0.21.5" 439 | "@esbuild/freebsd-x64" "0.21.5" 440 | "@esbuild/linux-arm" "0.21.5" 441 | "@esbuild/linux-arm64" "0.21.5" 442 | "@esbuild/linux-ia32" "0.21.5" 443 | "@esbuild/linux-loong64" "0.21.5" 444 | "@esbuild/linux-mips64el" "0.21.5" 445 | "@esbuild/linux-ppc64" "0.21.5" 446 | "@esbuild/linux-riscv64" "0.21.5" 447 | "@esbuild/linux-s390x" "0.21.5" 448 | "@esbuild/linux-x64" "0.21.5" 449 | "@esbuild/netbsd-x64" "0.21.5" 450 | "@esbuild/openbsd-x64" "0.21.5" 451 | "@esbuild/sunos-x64" "0.21.5" 452 | "@esbuild/win32-arm64" "0.21.5" 453 | "@esbuild/win32-ia32" "0.21.5" 454 | "@esbuild/win32-x64" "0.21.5" 455 | 456 | escape-string-regexp@^4.0.0: 457 | version "4.0.0" 458 | resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" 459 | integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== 460 | 461 | eslint-scope@^7.2.2: 462 | version "7.2.2" 463 | resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-7.2.2.tgz#deb4f92563390f32006894af62a22dba1c46423f" 464 | integrity sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg== 465 | dependencies: 466 | esrecurse "^4.3.0" 467 | estraverse "^5.2.0" 468 | 469 | eslint-visitor-keys@^3.4.1, eslint-visitor-keys@^3.4.3: 470 | version "3.4.3" 471 | resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz#0cd72fe8550e3c2eae156a96a4dddcd1c8ac5800" 472 | integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag== 473 | 474 | eslint@^8.48.0: 475 | version "8.57.1" 476 | resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.57.1.tgz#7df109654aba7e3bbe5c8eae533c5e461d3c6ca9" 477 | integrity sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA== 478 | dependencies: 479 | "@eslint-community/eslint-utils" "^4.2.0" 480 | "@eslint-community/regexpp" "^4.6.1" 481 | "@eslint/eslintrc" "^2.1.4" 482 | "@eslint/js" "8.57.1" 483 | "@humanwhocodes/config-array" "^0.13.0" 484 | "@humanwhocodes/module-importer" "^1.0.1" 485 | "@nodelib/fs.walk" "^1.2.8" 486 | "@ungap/structured-clone" "^1.2.0" 487 | ajv "^6.12.4" 488 | chalk "^4.0.0" 489 | cross-spawn "^7.0.2" 490 | debug "^4.3.2" 491 | doctrine "^3.0.0" 492 | escape-string-regexp "^4.0.0" 493 | eslint-scope "^7.2.2" 494 | eslint-visitor-keys "^3.4.3" 495 | espree "^9.6.1" 496 | esquery "^1.4.2" 497 | esutils "^2.0.2" 498 | fast-deep-equal "^3.1.3" 499 | file-entry-cache "^6.0.1" 500 | find-up "^5.0.0" 501 | glob-parent "^6.0.2" 502 | globals "^13.19.0" 503 | graphemer "^1.4.0" 504 | ignore "^5.2.0" 505 | imurmurhash "^0.1.4" 506 | is-glob "^4.0.0" 507 | is-path-inside "^3.0.3" 508 | js-yaml "^4.1.0" 509 | json-stable-stringify-without-jsonify "^1.0.1" 510 | levn "^0.4.1" 511 | lodash.merge "^4.6.2" 512 | minimatch "^3.1.2" 513 | natural-compare "^1.4.0" 514 | optionator "^0.9.3" 515 | strip-ansi "^6.0.1" 516 | text-table "^0.2.0" 517 | 518 | espree@^9.6.0, espree@^9.6.1: 519 | version "9.6.1" 520 | resolved "https://registry.yarnpkg.com/espree/-/espree-9.6.1.tgz#a2a17b8e434690a5432f2f8018ce71d331a48c6f" 521 | integrity sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ== 522 | dependencies: 523 | acorn "^8.9.0" 524 | acorn-jsx "^5.3.2" 525 | eslint-visitor-keys "^3.4.1" 526 | 527 | esquery@^1.4.2: 528 | version "1.6.0" 529 | resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.6.0.tgz#91419234f804d852a82dceec3e16cdc22cf9dae7" 530 | integrity sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg== 531 | dependencies: 532 | estraverse "^5.1.0" 533 | 534 | esrecurse@^4.3.0: 535 | version "4.3.0" 536 | resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921" 537 | integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag== 538 | dependencies: 539 | estraverse "^5.2.0" 540 | 541 | estraverse@^5.1.0, estraverse@^5.2.0: 542 | version "5.3.0" 543 | resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123" 544 | integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== 545 | 546 | esutils@^2.0.2: 547 | version "2.0.3" 548 | resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" 549 | integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== 550 | 551 | fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: 552 | version "3.1.3" 553 | resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" 554 | integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== 555 | 556 | fast-json-stable-stringify@^2.0.0: 557 | version "2.1.0" 558 | resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" 559 | integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== 560 | 561 | fast-levenshtein@^2.0.6: 562 | version "2.0.6" 563 | resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" 564 | integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== 565 | 566 | fastq@^1.6.0: 567 | version "1.19.1" 568 | resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.19.1.tgz#d50eaba803c8846a883c16492821ebcd2cda55f5" 569 | integrity sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ== 570 | dependencies: 571 | reusify "^1.0.4" 572 | 573 | file-entry-cache@^6.0.1: 574 | version "6.0.1" 575 | resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027" 576 | integrity sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg== 577 | dependencies: 578 | flat-cache "^3.0.4" 579 | 580 | find-up@^5.0.0: 581 | version "5.0.0" 582 | resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc" 583 | integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== 584 | dependencies: 585 | locate-path "^6.0.0" 586 | path-exists "^4.0.0" 587 | 588 | flat-cache@^3.0.4: 589 | version "3.2.0" 590 | resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.2.0.tgz#2c0c2d5040c99b1632771a9d105725c0115363ee" 591 | integrity sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw== 592 | dependencies: 593 | flatted "^3.2.9" 594 | keyv "^4.5.3" 595 | rimraf "^3.0.2" 596 | 597 | flatted@^3.2.9: 598 | version "3.3.3" 599 | resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.3.3.tgz#67c8fad95454a7c7abebf74bb78ee74a44023358" 600 | integrity sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg== 601 | 602 | fs.realpath@^1.0.0: 603 | version "1.0.0" 604 | resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" 605 | integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== 606 | 607 | fsevents@~2.3.2, fsevents@~2.3.3: 608 | version "2.3.3" 609 | resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" 610 | integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== 611 | 612 | glob-parent@^6.0.2: 613 | version "6.0.2" 614 | resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-6.0.2.tgz#6d237d99083950c79290f24c7642a3de9a28f9e3" 615 | integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A== 616 | dependencies: 617 | is-glob "^4.0.3" 618 | 619 | glob@^7.1.3: 620 | version "7.2.3" 621 | resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" 622 | integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== 623 | dependencies: 624 | fs.realpath "^1.0.0" 625 | inflight "^1.0.4" 626 | inherits "2" 627 | minimatch "^3.1.1" 628 | once "^1.3.0" 629 | path-is-absolute "^1.0.0" 630 | 631 | globals@^13.19.0: 632 | version "13.24.0" 633 | resolved "https://registry.yarnpkg.com/globals/-/globals-13.24.0.tgz#8432a19d78ce0c1e833949c36adb345400bb1171" 634 | integrity sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ== 635 | dependencies: 636 | type-fest "^0.20.2" 637 | 638 | graphemer@^1.4.0: 639 | version "1.4.0" 640 | resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6" 641 | integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag== 642 | 643 | has-flag@^4.0.0: 644 | version "4.0.0" 645 | resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" 646 | integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== 647 | 648 | ignore@^5.2.0: 649 | version "5.3.2" 650 | resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.2.tgz#3cd40e729f3643fd87cb04e50bf0eb722bc596f5" 651 | integrity sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g== 652 | 653 | import-fresh@^3.2.1: 654 | version "3.3.1" 655 | resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.1.tgz#9cecb56503c0ada1f2741dbbd6546e4b13b57ccf" 656 | integrity sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ== 657 | dependencies: 658 | parent-module "^1.0.0" 659 | resolve-from "^4.0.0" 660 | 661 | imurmurhash@^0.1.4: 662 | version "0.1.4" 663 | resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" 664 | integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA== 665 | 666 | inflight@^1.0.4: 667 | version "1.0.6" 668 | resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" 669 | integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== 670 | dependencies: 671 | once "^1.3.0" 672 | wrappy "1" 673 | 674 | inherits@2: 675 | version "2.0.4" 676 | resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" 677 | integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== 678 | 679 | is-extglob@^2.1.1: 680 | version "2.1.1" 681 | resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" 682 | integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== 683 | 684 | is-glob@^4.0.0, is-glob@^4.0.3: 685 | version "4.0.3" 686 | resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" 687 | integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== 688 | dependencies: 689 | is-extglob "^2.1.1" 690 | 691 | is-path-inside@^3.0.3: 692 | version "3.0.3" 693 | resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283" 694 | integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ== 695 | 696 | isexe@^2.0.0: 697 | version "2.0.0" 698 | resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" 699 | integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== 700 | 701 | js-yaml@^4.1.0: 702 | version "4.1.0" 703 | resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" 704 | integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== 705 | dependencies: 706 | argparse "^2.0.1" 707 | 708 | json-buffer@3.0.1: 709 | version "3.0.1" 710 | resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13" 711 | integrity sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ== 712 | 713 | json-schema-traverse@^0.4.1: 714 | version "0.4.1" 715 | resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" 716 | integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== 717 | 718 | json-stable-stringify-without-jsonify@^1.0.1: 719 | version "1.0.1" 720 | resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" 721 | integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw== 722 | 723 | keyv@^4.5.3: 724 | version "4.5.4" 725 | resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93" 726 | integrity sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw== 727 | dependencies: 728 | json-buffer "3.0.1" 729 | 730 | levn@^0.4.1: 731 | version "0.4.1" 732 | resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade" 733 | integrity sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ== 734 | dependencies: 735 | prelude-ls "^1.2.1" 736 | type-check "~0.4.0" 737 | 738 | lit-element@^4.2.0: 739 | version "4.2.0" 740 | resolved "https://registry.yarnpkg.com/lit-element/-/lit-element-4.2.0.tgz#75dcf9e5fae3e3b5fd3f02a5d297c582d0bb0ba3" 741 | integrity sha512-MGrXJVAI5x+Bfth/pU9Kst1iWID6GHDLEzFEnyULB/sFiRLgkd8NPK/PeeXxktA3T6EIIaq8U3KcbTU5XFcP2Q== 742 | dependencies: 743 | "@lit-labs/ssr-dom-shim" "^1.2.0" 744 | "@lit/reactive-element" "^2.1.0" 745 | lit-html "^3.3.0" 746 | 747 | lit-html@^3.3.0: 748 | version "3.3.0" 749 | resolved "https://registry.yarnpkg.com/lit-html/-/lit-html-3.3.0.tgz#f66c734a6c69dbb12abf9a718fa5d3dfb46d0b7c" 750 | integrity sha512-RHoswrFAxY2d8Cf2mm4OZ1DgzCoBKUKSPvA1fhtSELxUERq2aQQ2h05pO9j81gS1o7RIRJ+CePLogfyahwmynw== 751 | dependencies: 752 | "@types/trusted-types" "^2.0.2" 753 | 754 | lit@^3.1.1: 755 | version "3.3.0" 756 | resolved "https://registry.yarnpkg.com/lit/-/lit-3.3.0.tgz#b3037ea94676fb89c3dde9951914efefd0441f17" 757 | integrity sha512-DGVsqsOIHBww2DqnuZzW7QsuCdahp50ojuDaBPC7jUDRpYoH0z7kHBBYZewRzer75FwtrkmkKk7iOAwSaWdBmw== 758 | dependencies: 759 | "@lit/reactive-element" "^2.1.0" 760 | lit-element "^4.2.0" 761 | lit-html "^3.3.0" 762 | 763 | locate-path@^6.0.0: 764 | version "6.0.0" 765 | resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286" 766 | integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw== 767 | dependencies: 768 | p-locate "^5.0.0" 769 | 770 | lodash.merge@^4.6.2: 771 | version "4.6.2" 772 | resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" 773 | integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== 774 | 775 | minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: 776 | version "3.1.2" 777 | resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" 778 | integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== 779 | dependencies: 780 | brace-expansion "^1.1.7" 781 | 782 | ms@^2.1.3: 783 | version "2.1.3" 784 | resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" 785 | integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== 786 | 787 | nanoid@^3.3.8: 788 | version "3.3.11" 789 | resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.11.tgz#4f4f112cefbe303202f2199838128936266d185b" 790 | integrity sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w== 791 | 792 | natural-compare@^1.4.0: 793 | version "1.4.0" 794 | resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" 795 | integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== 796 | 797 | once@^1.3.0: 798 | version "1.4.0" 799 | resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" 800 | integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== 801 | dependencies: 802 | wrappy "1" 803 | 804 | optionator@^0.9.3: 805 | version "0.9.4" 806 | resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.4.tgz#7ea1c1a5d91d764fb282139c88fe11e182a3a734" 807 | integrity sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g== 808 | dependencies: 809 | deep-is "^0.1.3" 810 | fast-levenshtein "^2.0.6" 811 | levn "^0.4.1" 812 | prelude-ls "^1.2.1" 813 | type-check "^0.4.0" 814 | word-wrap "^1.2.5" 815 | 816 | p-limit@^3.0.2: 817 | version "3.1.0" 818 | resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" 819 | integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== 820 | dependencies: 821 | yocto-queue "^0.1.0" 822 | 823 | p-locate@^5.0.0: 824 | version "5.0.0" 825 | resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834" 826 | integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw== 827 | dependencies: 828 | p-limit "^3.0.2" 829 | 830 | parent-module@^1.0.0: 831 | version "1.0.1" 832 | resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" 833 | integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g== 834 | dependencies: 835 | callsites "^3.0.0" 836 | 837 | path-exists@^4.0.0: 838 | version "4.0.0" 839 | resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" 840 | integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== 841 | 842 | path-is-absolute@^1.0.0: 843 | version "1.0.1" 844 | resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" 845 | integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== 846 | 847 | path-key@^3.1.0: 848 | version "3.1.1" 849 | resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" 850 | integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== 851 | 852 | picocolors@^1.1.1: 853 | version "1.1.1" 854 | resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b" 855 | integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA== 856 | 857 | postcss@^8.4.43: 858 | version "8.5.3" 859 | resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.3.tgz#1463b6f1c7fb16fe258736cba29a2de35237eafb" 860 | integrity sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A== 861 | dependencies: 862 | nanoid "^3.3.8" 863 | picocolors "^1.1.1" 864 | source-map-js "^1.2.1" 865 | 866 | prelude-ls@^1.2.1: 867 | version "1.2.1" 868 | resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" 869 | integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== 870 | 871 | punycode@^2.1.0: 872 | version "2.3.1" 873 | resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" 874 | integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== 875 | 876 | queue-microtask@^1.2.2: 877 | version "1.2.3" 878 | resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" 879 | integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== 880 | 881 | resolve-from@^4.0.0: 882 | version "4.0.0" 883 | resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" 884 | integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== 885 | 886 | reusify@^1.0.4: 887 | version "1.1.0" 888 | resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.1.0.tgz#0fe13b9522e1473f51b558ee796e08f11f9b489f" 889 | integrity sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw== 890 | 891 | rimraf@^3.0.2: 892 | version "3.0.2" 893 | resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" 894 | integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== 895 | dependencies: 896 | glob "^7.1.3" 897 | 898 | rollup@^4.20.0: 899 | version "4.41.0" 900 | resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.41.0.tgz#17476835d2967759e3ffebe5823ed15fc4b7d13e" 901 | integrity sha512-HqMFpUbWlf/tvcxBFNKnJyzc7Lk+XO3FGc3pbNBLqEbOz0gPLRgcrlS3UF4MfUrVlstOaP/q0kM6GVvi+LrLRg== 902 | dependencies: 903 | "@types/estree" "1.0.7" 904 | optionalDependencies: 905 | "@rollup/rollup-android-arm-eabi" "4.41.0" 906 | "@rollup/rollup-android-arm64" "4.41.0" 907 | "@rollup/rollup-darwin-arm64" "4.41.0" 908 | "@rollup/rollup-darwin-x64" "4.41.0" 909 | "@rollup/rollup-freebsd-arm64" "4.41.0" 910 | "@rollup/rollup-freebsd-x64" "4.41.0" 911 | "@rollup/rollup-linux-arm-gnueabihf" "4.41.0" 912 | "@rollup/rollup-linux-arm-musleabihf" "4.41.0" 913 | "@rollup/rollup-linux-arm64-gnu" "4.41.0" 914 | "@rollup/rollup-linux-arm64-musl" "4.41.0" 915 | "@rollup/rollup-linux-loongarch64-gnu" "4.41.0" 916 | "@rollup/rollup-linux-powerpc64le-gnu" "4.41.0" 917 | "@rollup/rollup-linux-riscv64-gnu" "4.41.0" 918 | "@rollup/rollup-linux-riscv64-musl" "4.41.0" 919 | "@rollup/rollup-linux-s390x-gnu" "4.41.0" 920 | "@rollup/rollup-linux-x64-gnu" "4.41.0" 921 | "@rollup/rollup-linux-x64-musl" "4.41.0" 922 | "@rollup/rollup-win32-arm64-msvc" "4.41.0" 923 | "@rollup/rollup-win32-ia32-msvc" "4.41.0" 924 | "@rollup/rollup-win32-x64-msvc" "4.41.0" 925 | fsevents "~2.3.2" 926 | 927 | run-parallel@^1.1.9: 928 | version "1.2.0" 929 | resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" 930 | integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA== 931 | dependencies: 932 | queue-microtask "^1.2.2" 933 | 934 | shebang-command@^2.0.0: 935 | version "2.0.0" 936 | resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" 937 | integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== 938 | dependencies: 939 | shebang-regex "^3.0.0" 940 | 941 | shebang-regex@^3.0.0: 942 | version "3.0.0" 943 | resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" 944 | integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== 945 | 946 | source-map-js@^1.2.1: 947 | version "1.2.1" 948 | resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46" 949 | integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA== 950 | 951 | strip-ansi@^6.0.1: 952 | version "6.0.1" 953 | resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" 954 | integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== 955 | dependencies: 956 | ansi-regex "^5.0.1" 957 | 958 | strip-json-comments@^3.1.1: 959 | version "3.1.1" 960 | resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" 961 | integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== 962 | 963 | supports-color@^7.1.0: 964 | version "7.2.0" 965 | resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" 966 | integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== 967 | dependencies: 968 | has-flag "^4.0.0" 969 | 970 | text-table@^0.2.0: 971 | version "0.2.0" 972 | resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" 973 | integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw== 974 | 975 | type-check@^0.4.0, type-check@~0.4.0: 976 | version "0.4.0" 977 | resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1" 978 | integrity sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew== 979 | dependencies: 980 | prelude-ls "^1.2.1" 981 | 982 | type-fest@^0.20.2: 983 | version "0.20.2" 984 | resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4" 985 | integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ== 986 | 987 | uri-js@^4.2.2: 988 | version "4.4.1" 989 | resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" 990 | integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== 991 | dependencies: 992 | punycode "^2.1.0" 993 | 994 | vite@^5.4.10: 995 | version "5.4.19" 996 | resolved "https://registry.yarnpkg.com/vite/-/vite-5.4.19.tgz#20efd060410044b3ed555049418a5e7d1998f959" 997 | integrity sha512-qO3aKv3HoQC8QKiNSTuUM1l9o/XX3+c+VTgLHbJWHZGeTPVAg2XwazI9UWzoxjIJCGCV2zU60uqMzjeLZuULqA== 998 | dependencies: 999 | esbuild "^0.21.3" 1000 | postcss "^8.4.43" 1001 | rollup "^4.20.0" 1002 | optionalDependencies: 1003 | fsevents "~2.3.3" 1004 | 1005 | which@^2.0.1: 1006 | version "2.0.2" 1007 | resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" 1008 | integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== 1009 | dependencies: 1010 | isexe "^2.0.0" 1011 | 1012 | word-wrap@^1.2.5: 1013 | version "1.2.5" 1014 | resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34" 1015 | integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA== 1016 | 1017 | wrappy@1: 1018 | version "1.0.2" 1019 | resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" 1020 | integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== 1021 | 1022 | yocto-queue@^0.1.0: 1023 | version "0.1.0" 1024 | resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" 1025 | integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== 1026 | -------------------------------------------------------------------------------- /lib/animated-routing-mixin.js: -------------------------------------------------------------------------------- 1 | import RouteTreeNode from './route-tree-node.js'; 2 | import {Context} from './page.js'; 3 | import basicRoutingMixin from './routing-mixin.js'; 4 | import BasicRoutingInterface from './routing-interface.js'; 5 | 6 | /** 7 | * @param {function(new:HTMLElement)} Superclass 8 | * @param {string} className 9 | * @mixinFunction 10 | * @polymer 11 | */ 12 | function animatedRoutingMixin(Superclass, className) { 13 | /** 14 | * @constructor 15 | * @extends {Superclass} 16 | * @implements {BasicRoutingInterface} 17 | */ 18 | // @ts-ignore 19 | const BasicRoutingElement = basicRoutingMixin(Superclass); 20 | 21 | /** 22 | * @mixinClass 23 | * @polymer 24 | * @extends {BasicRoutingElement} 25 | * @implements {BasicRoutingInterface} 26 | */ 27 | class AnimatedRouting extends BasicRoutingElement { 28 | connectedCallback() { 29 | // @ts-ignore 30 | super.connectedCallback(); 31 | document.documentElement.scrollTop = document.body.scrollTop = 0; 32 | } 33 | /** 34 | * Default implementation for the callback on entering a route node. 35 | * This will only be used if an element does not define it's own routeEnter method. 36 | * 37 | * @param {!RouteTreeNode} currentNode 38 | * @param {!RouteTreeNode|undefined} nextNodeIfExists 39 | * @param {string} routeId 40 | * @param {!Context} context 41 | * @return {!Promise} 42 | */ 43 | async routeEnter(currentNode, nextNodeIfExists, routeId, context) { 44 | const currentElement = currentNode.getValue().element; 45 | const animationEnd = (evt) => { 46 | const thisElem = /** @type {!Element} */ (/** @type {?} */ (this)); 47 | thisElem.classList.remove(className); 48 | thisElem.removeEventListener('animationend', animationEnd, false); 49 | }; 50 | currentElement.addEventListener('transitionend', animationEnd, false); 51 | currentElement.classList.add(className); 52 | return super.routeEnter(currentNode, nextNodeIfExists, routeId, context); 53 | } 54 | 55 | /** 56 | * Default implementation for the callback on exiting a route node. 57 | * This will only be used if an element does not define it's own routeExit method. 58 | * 59 | * @override 60 | * @param {!RouteTreeNode} currentNode 61 | * @param {!RouteTreeNode|undefined} nextNode 62 | * @param {string} routeId 63 | * @param {!Context} context 64 | * @return {!Promise} 65 | */ 66 | async routeExit(currentNode, nextNode, routeId, context) { 67 | const currentElement = currentNode.getValue().element; 68 | currentElement.addEventListener('animationend', AnimatedRouting.animationEnd, false); 69 | currentElement.classList.add(className); 70 | currentNode.getValue().element = undefined; 71 | } 72 | 73 | /** 74 | * @this {Element} 75 | * @param {Event} evt 76 | */ 77 | static animationEnd(evt) { 78 | this.removeEventListener('animationend', AnimatedRouting.animationEnd, false); 79 | if (this.parentNode) { 80 | this.parentNode.removeChild(this); 81 | } 82 | } 83 | } 84 | return AnimatedRouting; 85 | } 86 | 87 | export default animatedRoutingMixin; 88 | -------------------------------------------------------------------------------- /lib/animated-routing.mixin.js: -------------------------------------------------------------------------------- 1 | import animatedRouteMixin from './animated-routing-mixin.js'; 2 | function animatedRoutingMixin(Superclass, className) { 3 | return animatedRouteMixin(Superclass, className); 4 | } 5 | export default animatedRoutingMixin; 6 | -------------------------------------------------------------------------------- /lib/animated-routing.mixin.ts: -------------------------------------------------------------------------------- 1 | import {type Constructor, type RoutingMixinInterface} from './routing.mixin.js'; 2 | import animatedRouteMixin from './animated-routing-mixin.js'; 3 | 4 | function animatedRoutingMixin>(Superclass:T, className:string) { 5 | return animatedRouteMixin(Superclass, className) as unknown as Constructor & T; 6 | } 7 | 8 | export default animatedRoutingMixin; 9 | -------------------------------------------------------------------------------- /lib/page.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license Copyright (c) 2012 TJ Holowaychuk 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 5 | * documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the 6 | * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 7 | * permit persons to whom the Software is furnished to do so, subject to the following conditions: 8 | * 9 | * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the 10 | * Software. 11 | * 12 | * THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE 13 | * WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS 14 | * OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 15 | * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 16 | */ 17 | 18 | import {pathToRegexp} from 'path-to-regexp'; 19 | 20 | /** 21 | * Short-cuts for global-object checks 22 | */ 23 | const hasDocument = ('undefined' !== typeof document); 24 | const hasWindow = ('undefined' !== typeof window); 25 | const hasHistory = ('undefined' !== typeof history); 26 | const hasProcess = typeof process !== 'undefined'; 27 | 28 | /** 29 | * Detect click event 30 | */ 31 | const clickEvent = hasDocument && document.ontouchstart ? 'touchstart' : 'click'; 32 | 33 | /** 34 | * To work properly with the URL 35 | * history.location generated polyfill in https://github.com/devote/HTML5-History-API 36 | */ 37 | const isLocation = hasWindow && !!(/** @type {?} */ (window.history).location || window.location); 38 | 39 | /** 40 | * @typedef {{ 41 | * window: (Window|undefined), 42 | * decodeURLComponents: (boolean|undefined), 43 | * popstate: (boolean|undefined), 44 | * click: (boolean|undefined), 45 | * hashbang: (boolean|undefined), 46 | * dispatch: (boolean|undefined) 47 | * }} 48 | */ 49 | let PageOptions; 50 | 51 | /** @typedef {function(!Context, function(...?):?):?} */ 52 | let PageCallback; 53 | 54 | /** The page instance */ 55 | export class Page { 56 | constructor() { 57 | // public things 58 | /** @type {!Array} */ 59 | this.callbacks = []; 60 | /** @type {!Array} */ 61 | this.exits = []; 62 | this.current = ''; 63 | this.len = 0; 64 | /** @type {!Context} */ 65 | this.prevContext; 66 | 67 | // private things 68 | this._decodeURLComponents = true; 69 | this._base = ''; 70 | this._strict = false; 71 | this._running = false; 72 | this._hashbang = false; 73 | this._decodeURLComponents = true; 74 | this._popstate = true; 75 | this._click = true; 76 | /** @type {!Window|undefined} */ 77 | this._window = (hasWindow ? window : undefined); 78 | 79 | // bound functions 80 | this.clickHandler = this.clickHandler.bind(this); 81 | this._onpopstate = this._onpopstate.bind(this); 82 | } 83 | 84 | /** 85 | * Configure the instance of page. This can be called multiple times. 86 | * 87 | * @param {PageOptions=} options 88 | */ 89 | configure(options) { 90 | const opts = options || /** @type {!PageOptions} */ ({}); 91 | 92 | this._window = opts.window || (hasWindow ? window : undefined); 93 | this._decodeURLComponents = opts.decodeURLComponents !== false; 94 | this._popstate = opts.popstate !== false && hasWindow; 95 | this._click = opts.click !== false && hasDocument; 96 | this._hashbang = !!opts.hashbang; 97 | 98 | const _window = this._window; 99 | if (this._popstate) { 100 | _window.addEventListener('popstate', this._onpopstate, false); 101 | } else if (hasWindow) { 102 | _window.removeEventListener('popstate', this._onpopstate, false); 103 | } 104 | 105 | if (this._click) { 106 | _window.document.addEventListener(clickEvent, this.clickHandler, false); 107 | } else if (hasDocument) { 108 | _window.document.removeEventListener(clickEvent, this.clickHandler, false); 109 | } 110 | 111 | if (this._hashbang && hasWindow && !hasHistory) { 112 | _window.addEventListener('hashchange', this._onpopstate, false); 113 | } else if (hasWindow) { 114 | _window.removeEventListener('hashchange', this._onpopstate, false); 115 | } 116 | } 117 | 118 | /** 119 | * Get or set basepath to `path`. 120 | * 121 | * @param {string} path 122 | */ 123 | base(path) { 124 | if (0 === arguments.length) { 125 | return this._base; 126 | } 127 | this._base = path; 128 | } 129 | 130 | /** 131 | * Gets the `base`, which depends on whether we are using History or 132 | * hashbang routing. 133 | */ 134 | _getBase() { 135 | let base = this._base; 136 | if (!!base) { 137 | return base; 138 | } 139 | const loc = hasWindow && this._window && this._window.location; 140 | 141 | if (hasWindow && this._hashbang && loc && loc.protocol === 'file:') { 142 | base = loc.pathname; 143 | } 144 | 145 | return base; 146 | } 147 | 148 | /** 149 | * Get or set strict path matching to `enable` 150 | * 151 | * @param {boolean} enable 152 | */ 153 | strict(enable) { 154 | if (0 === arguments.length) { 155 | return this._strict; 156 | } 157 | this._strict = enable; 158 | } 159 | 160 | 161 | /** 162 | * Bind with the given `options`. 163 | * 164 | * Options: 165 | * 166 | * - `click` bind to click events [true] 167 | * - `popstate` bind to popstate [true] 168 | * - `dispatch` perform initial dispatch [true] 169 | * 170 | * @param {PageOptions=} options 171 | * @return {!Promise} 172 | */ 173 | async start(options) { 174 | const opts = options || /** @type {!PageOptions} */ ({}); 175 | this.configure(opts); 176 | 177 | if (false === opts.dispatch) { 178 | return; 179 | } 180 | this._running = true; 181 | 182 | let url; 183 | if (isLocation) { 184 | const window = this._window; 185 | const loc = window.location; 186 | 187 | if (this._hashbang && ~loc.hash.indexOf('#!')) { 188 | url = loc.hash.substr(2) + loc.search; 189 | } else if (this._hashbang) { 190 | url = loc.search + loc.hash; 191 | } else { 192 | url = loc.pathname + loc.search + loc.hash; 193 | } 194 | } 195 | 196 | await this.replace(url, null, true, opts.dispatch); 197 | } 198 | 199 | /** Unbind click and popstate event handlers. */ 200 | stop() { 201 | if (!this._running) { 202 | return; 203 | } 204 | this.current = ''; 205 | this.len = 0; 206 | this._running = false; 207 | 208 | const window = this._window; 209 | this._click && window.document.removeEventListener(clickEvent, this.clickHandler, false); 210 | hasWindow && window.removeEventListener('popstate', this._onpopstate, false); 211 | hasWindow && window.removeEventListener('hashchange', this._onpopstate, false); 212 | } 213 | 214 | /** 215 | * Show `path` with optional `state` object. 216 | * 217 | * @param {string} path 218 | * @param {Object=} state 219 | * @param {boolean=} dispatch 220 | * @param {boolean=} push 221 | * @return {!Promise} 222 | */ 223 | async show(path, state, dispatch = true, push = true) { 224 | const ctx = new Context(path, state, this, push); 225 | const prev = this.prevContext; 226 | this.prevContext = ctx; 227 | this.current = ctx.path; 228 | if (false !== dispatch) { 229 | await this.dispatch(ctx, prev); 230 | } else if (false !== ctx.handled && false !== push) { 231 | ctx.pushState(); 232 | } 233 | return ctx; 234 | } 235 | 236 | /** 237 | * Goes back in the history 238 | * Back should always let the current route push state and then go back. 239 | * 240 | * @param {string} path - fallback path to go back if no more history exists, if undefined defaults to page.base 241 | * @param {Object=} state 242 | */ 243 | back(path, state) { 244 | const page = this; 245 | if (this.len > 0) { 246 | const window = this._window; 247 | // this may need more testing to see if all browsers 248 | // wait for the next tick to go back in history 249 | hasHistory && window.history.back(); 250 | this.len--; 251 | } else if (path) { 252 | setTimeout(function () { 253 | page.show(path, state); 254 | }); 255 | } else { 256 | setTimeout(function () { 257 | page.show(page._getBase(), state); 258 | }); 259 | } 260 | } 261 | 262 | /** 263 | * Register route to redirect from one path to other 264 | * or just redirect to another route 265 | * 266 | * @param {string} from - if param 'to' is undefined redirects to 'from' 267 | * @param {string=} to 268 | */ 269 | redirect(from, to) { 270 | // Define route from a path to another 271 | if ('string' === typeof from && 'string' === typeof to) { 272 | this.register(from, (e) => { 273 | setTimeout(() => { 274 | this.replace(/** @type {!string} */ (to)); 275 | }, 0); 276 | }); 277 | } 278 | 279 | // Wait for the push state and replace it with another 280 | if ('string' === typeof from && 'undefined' === typeof to) { 281 | setTimeout(() => { 282 | this.replace(from); 283 | }, 0); 284 | } 285 | } 286 | 287 | /** 288 | * Replace `path` with optional `state` object. 289 | * 290 | * @param {string|undefined} path 291 | * @param {*=} state 292 | * @param {boolean=} init 293 | * @param {boolean=} dispatch 294 | * @return {!Promise} 295 | */ 296 | async replace(path, state, init, dispatch) { 297 | const ctx = new Context(path, state, this); 298 | const prev = this.prevContext; 299 | this.prevContext = ctx; 300 | this.current = ctx.path; 301 | ctx.init = init; 302 | ctx.save(); // save before dispatching, which may redirect 303 | if (false !== dispatch) { 304 | await this.dispatch(ctx, prev); 305 | } 306 | return ctx; 307 | } 308 | 309 | /** 310 | * Dispatch the given `ctx`. 311 | * 312 | * @param {!Context} ctx 313 | * @param {!Context} prev 314 | */ 315 | async dispatch(ctx, prev) { 316 | if (prev) { 317 | // Exit callbacks 318 | for (const fn of this.exits) { 319 | await new Promise((resolve) => fn(prev, resolve)); 320 | } 321 | } 322 | // Entry callbacks 323 | if (ctx.path !== this.current) { 324 | ctx.handled = false; 325 | } 326 | for (const fn of this.callbacks) { 327 | await new Promise((resolve) => fn(ctx, resolve)); 328 | } 329 | unhandled.call(this, ctx); 330 | } 331 | 332 | /** 333 | * Register an exit route on `path` with 334 | * callback `fn()`, which will be called 335 | * on the previous context when a new 336 | * page is visited. 337 | * 338 | * @param {!string|!PageCallback} path 339 | * @param {!PageCallback} fn 340 | * @param {...!PageCallback} fns 341 | */ 342 | exit(path, fn, ...fns) { 343 | if (typeof path === 'function') { 344 | return this.exit('*', path); 345 | } 346 | 347 | const callbacks = [fn].concat(fns); 348 | const route = new Route(path, null, this); 349 | for (let i = 0; i < callbacks.length; ++i) { 350 | this.exits.push(route.middleware(callbacks[i])); 351 | } 352 | } 353 | 354 | /** 355 | * Handle "click" events. 356 | * @param {!Event} evt 357 | */ 358 | clickHandler(evt) { 359 | const e = /** @type {!MouseEvent} */ (evt); 360 | if (1 !== this._which(e)) { 361 | return; 362 | } 363 | 364 | if (e.metaKey || e.ctrlKey || e.shiftKey) { 365 | return; 366 | } 367 | if (e.defaultPrevented) { 368 | return; 369 | } 370 | 371 | // ensure link 372 | // use shadow dom when available 373 | let el = /** @type {!HTMLElement} */ (e.target); 374 | if ((el.nodeName || '').toUpperCase() !== 'A') { 375 | const composedPath = e.composedPath(); 376 | for (let i = 0; i < composedPath.length; i++) { 377 | el = /** @type {!HTMLElement} */ (composedPath[i]); 378 | // el.nodeName for svg links are 'a' instead of 'A' 379 | if ((el.nodeName || '').toUpperCase() === 'A') { 380 | break; 381 | } 382 | } 383 | } 384 | 385 | if (!el || (el.nodeName || '').toUpperCase() !== 'A') { 386 | return; 387 | } 388 | let anchor = /** @type {!HTMLAnchorElement} */ (el); 389 | const svgAnchor = /** @type {!SVGAElement} */ (/** @type {?} */ (el)); 390 | 391 | // check if link is inside an svg 392 | // in this case, both href and target are always inside an object 393 | const svg = (typeof svgAnchor.href === 'object') && svgAnchor.href.constructor.name === 'SVGAnimatedString'; 394 | 395 | // Ignore if tag has 396 | // 1. "download" attribute 397 | // 2. rel="external" attribute 398 | if (anchor.hasAttribute('download') || anchor.getAttribute('rel') === 'external') { 399 | return; 400 | } 401 | 402 | // ensure non-hash for the same path 403 | const link = anchor.getAttribute('href'); 404 | if (!this._hashbang && this._samePath(anchor) && (anchor.hash || '#' === link)) { 405 | return; 406 | } 407 | 408 | // Check for mailto: in the href 409 | if (link && link.indexOf('mailto:') > -1) { 410 | return; 411 | } 412 | 413 | // check target 414 | // svg target is an object and its desired value is in .baseVal property 415 | if (svg ? svgAnchor.target.baseVal : svgAnchor.target) { 416 | return; 417 | } 418 | 419 | // x-origin 420 | // note: svg links that are not relative don't call click events (and skip page.js) 421 | // consequently, all svg links tested inside page.js are relative and in the same origin 422 | if (!svg && !this.sameOrigin(anchor.href)) { 423 | return; 424 | } 425 | 426 | // rebuild path 427 | // There aren't .pathname and .search properties in svg links, so we use href 428 | // Also, svg href is an object and its desired value is in .baseVal property 429 | let path = svg ? svgAnchor.href.baseVal : (anchor.pathname + anchor.search + (anchor.hash || '')); 430 | 431 | path = path[0] !== '/' ? `/${path}` : path; 432 | 433 | // strip leading "/[drive letter]:" on NW.js on Windows 434 | if (hasProcess && path.match(/^\/[a-zA-Z]:\//)) { 435 | path = path.replace(/^\/[a-zA-Z]:\//, '/'); 436 | } 437 | 438 | // same page 439 | const orig = path; 440 | const pageBase = this._getBase(); 441 | 442 | if (path.indexOf(pageBase) === 0) { 443 | path = path.substr(pageBase.length); 444 | } 445 | 446 | if (this._hashbang) { 447 | path = path.replace('#!', ''); 448 | } 449 | 450 | if (pageBase && orig === path && (!isLocation || this._window.location.protocol !== 'file:')) { 451 | return; 452 | } 453 | 454 | e.preventDefault(); 455 | this.show(orig); 456 | } 457 | 458 | /** 459 | * Event button. 460 | */ 461 | _which(e) { 462 | e = e || (hasWindow && this._window.event); 463 | return null == e.which ? e.button : e.which; 464 | } 465 | 466 | /** 467 | * Convert to a URL object 468 | */ 469 | _toURL(href) { 470 | const window = this._window; 471 | if (typeof URL === 'function' && isLocation) { 472 | return new URL(href, window.location.toString()); 473 | } else if (hasDocument) { 474 | const anc = window.document.createElement('a'); 475 | anc.href = href; 476 | return anc; 477 | } 478 | } 479 | 480 | /** 481 | * Check if `href` is the same origin. 482 | * @param {string} href 483 | */ 484 | sameOrigin(href) { 485 | if (!href || !isLocation) { 486 | return false; 487 | } 488 | 489 | const url = this._toURL(href); 490 | const window = this._window; 491 | 492 | const loc = window.location; 493 | 494 | 495 | // When the port is the default http port 80 for http, or 443 for 496 | // https, internet explorer 11 returns an empty string for loc.port, 497 | // so we need to compare loc.port with an empty string if url.port 498 | // is the default port 80 or 443. 499 | // Also the comparition with `port` is changed from `===` to `==` because 500 | // `port` can be a string sometimes. This only applies to ie11. 501 | return loc.protocol === url.protocol && 502 | loc.hostname === url.hostname && 503 | (loc.port === url.port || loc.port === '' && (url.port == '80' || url.port == '443')); 504 | } 505 | 506 | _samePath(url) { 507 | if (!isLocation) { 508 | return false; 509 | } 510 | const window = this._window; 511 | const loc = window.location; 512 | return url.pathname === loc.pathname && 513 | url.search === loc.search; 514 | } 515 | 516 | /** 517 | * Remove URL encoding from the given `str`. 518 | * Accommodates whitespace in both x-www-form-urlencoded 519 | * and regular percent-encoded form. 520 | * 521 | * @param {string} val - URL component to decode 522 | */ 523 | _decodeURLEncodedURIComponent(val) { 524 | if (typeof val !== 'string') { 525 | return val; 526 | } 527 | return this._decodeURLComponents ? decodeURIComponent(val.replace(/\+/g, ' ')) : val; 528 | } 529 | 530 | /** 531 | * Register `path` with callback `fn()` 532 | * 533 | * page.register('*', fn); 534 | * page.register('/user/:id', load, user); 535 | * 536 | * @param {string} path 537 | * @param {!PageCallback} fn 538 | * @param {...!PageCallback} fns 539 | */ 540 | register(path, fn, ...fns) { 541 | const route = new Route(/** @type {string} */ (path), null, this); 542 | const callbacks = [fn].concat(fns); 543 | for (let i = 0; i < callbacks.length; ++i) { 544 | this.callbacks.push(route.middleware(callbacks[i])); 545 | } 546 | } 547 | } 548 | 549 | /** Handle "popstate" events. */ 550 | Page.prototype._onpopstate = (function () { 551 | let loaded = false; 552 | if (!hasWindow) { 553 | return function () {}; 554 | } 555 | if (hasDocument && document.readyState === 'complete') { 556 | loaded = true; 557 | } else { 558 | const loadFn = function () { 559 | setTimeout(function () { 560 | loaded = true; 561 | }, 0); 562 | window.removeEventListener('load', loadFn); 563 | }; 564 | window.addEventListener('load', loadFn); 565 | } 566 | 567 | /** 568 | * @this {!Page} 569 | * @param {!Event} evt 570 | */ 571 | function onpopstate(evt) { 572 | const e = /** @type {!PopStateEvent} */ (evt); 573 | if (!loaded) { 574 | return; 575 | } 576 | const page = this; 577 | if (e.state) { 578 | const path = e.state.path; 579 | page.replace(path, e.state); 580 | } else if (isLocation) { 581 | const loc = page._window.location; 582 | page.show(loc.pathname + loc.search + loc.hash, undefined, undefined, false); 583 | } 584 | } 585 | return onpopstate; 586 | })(); 587 | 588 | /** 589 | * Unhandled `ctx`. When it's not the initial 590 | * popstate then redirect. If you wish to handle 591 | * 404s on your own use `page.register('*', callback)`. 592 | * 593 | * @param {Context} ctx 594 | * @this {!Page} 595 | */ 596 | function unhandled(ctx) { 597 | if (ctx.handled) { 598 | return; 599 | } 600 | let current; 601 | const page = this; 602 | const window = page._window; 603 | 604 | if (page._hashbang) { 605 | current = isLocation && this._getBase() + window.location.hash.replace('#!', ''); 606 | } else { 607 | current = isLocation && window.location.pathname + window.location.search; 608 | } 609 | 610 | if (current === ctx.canonicalPath) { 611 | return; 612 | } 613 | page.stop(); 614 | ctx.handled = false; 615 | const url = new URL(ctx.canonicalPath, window.location.origin); 616 | if (isLocation) { 617 | if (url.origin === window.location.origin) { 618 | window.location.href = url.toString(); 619 | } else { 620 | console.error('Cross domain route change prevented'); 621 | } 622 | } 623 | } 624 | 625 | /** 626 | * Escapes RegExp characters in the given string. 627 | * 628 | * @param {string} s 629 | */ 630 | function escapeRegExp(s) { 631 | return s.replace(/([.+*?=^!:${}()[\]|/\\])/g, '\\$1'); 632 | } 633 | 634 | export class Context { 635 | /** 636 | * Initialize a new "request" `Context` 637 | * with the given `path` and optional initial `state`. 638 | * 639 | * @param {string|undefined} path 640 | * @param {*=} state 641 | * @param {!Page=} pageInstance 642 | * @param {boolean=} push Should state be pushed when handled? 643 | */ 644 | constructor(path, state, pageInstance, push = false) { 645 | if (!pageInstance) { 646 | pageInstance = new Page(); 647 | pageInstance.configure(); 648 | } 649 | const _page = this.page = pageInstance; 650 | const window = _page._window; 651 | const hashbang = _page._hashbang; 652 | 653 | const pageBase = _page._getBase(); 654 | if ('/' === path[0] && 0 !== path.indexOf(pageBase)) { 655 | path = pageBase + (hashbang ? '#!' : '') + path; 656 | } 657 | const i = path.indexOf('?'); 658 | 659 | this.canonicalPath = path; 660 | const re = new RegExp(`^${escapeRegExp(pageBase)}`); 661 | this.path = path.replace(re, '') || '/'; 662 | if (hashbang) { 663 | this.path = this.path.replace('#!', '') || '/'; 664 | } 665 | 666 | this.title = (hasDocument ? window.document.title : undefined); 667 | this.state = state || {}; 668 | this.state.path = path; 669 | this.querystring = ~i ? _page._decodeURLEncodedURIComponent(path.slice(i + 1)) : ''; 670 | this.pathname = _page._decodeURLEncodedURIComponent(~i ? path.slice(0, i) : path); 671 | /** @type {!Object} */ 672 | this.params = {}; 673 | this.query = new URLSearchParams(this.querystring); 674 | 675 | /** 676 | * @private 677 | * @type {boolean} 678 | */ 679 | this.pushState_ = push; 680 | 681 | // fragment 682 | this.hash = ''; 683 | if (!hashbang) { 684 | if (!~this.path.indexOf('#')) { 685 | return; 686 | } 687 | const parts = this.path.split('#'); 688 | this.path = this.pathname = parts[0]; 689 | this.hash = _page._decodeURLEncodedURIComponent(parts[1]) || ''; 690 | this.querystring = this.querystring.split('#')[0]; 691 | } 692 | /** 693 | * @private 694 | * @type {boolean} 695 | */ 696 | this.handled_ = false; 697 | /** @type {boolean|undefined} */ 698 | this.init; 699 | /** @type {string|undefined} */ 700 | this.routePath; 701 | } 702 | 703 | /** @param {boolean} value */ 704 | set handled(value) { 705 | if (this.handled_ !== true && value === true && this.pushState_ !== false) { 706 | this.pushState(); 707 | } 708 | this.handled_ = value; 709 | } 710 | 711 | /** @return {boolean} */ 712 | get handled() { 713 | return this.handled_; 714 | } 715 | 716 | /** Push state. */ 717 | pushState() { 718 | const page = this.page; 719 | const window = page._window; 720 | const hashbang = page._hashbang; 721 | 722 | page.len++; 723 | if (hasHistory) { 724 | window.history.pushState(this.state, this.title || '', 725 | hashbang && this.path !== '/' ? '#!' + this.path : this.canonicalPath); 726 | } 727 | } 728 | 729 | replaceState() { 730 | return this.save(); 731 | } 732 | 733 | /** Save the context state. */ 734 | save() { 735 | const page = this.page; 736 | if (hasHistory) { 737 | page._window.history.replaceState(this.state, this.title || '', 738 | page._hashbang && this.path !== '/' ? '#!' + this.path : this.canonicalPath); 739 | } 740 | } 741 | } 742 | 743 | export class Route { 744 | /** 745 | * Initialize `Route` with the given HTTP `path`, 746 | * and an array of `callbacks` and `options`. 747 | * 748 | * Options: 749 | * 750 | * - `sensitive` enable case-sensitive routes 751 | * - `strict` enable strict matching for trailing slashes 752 | * 753 | * @param {string} path 754 | * @param {Object=} options 755 | * @param {!Page=} page 756 | */ 757 | constructor(path, options, page) { 758 | const _page = this.page = page || new Page(); 759 | const opts = options || {}; 760 | opts.strict = opts.strict || _page._strict; 761 | this.path = (path === '*') ? '(.*)' : path; 762 | this.method = 'GET'; 763 | this.regexp = pathToRegexp(this.path, this.keys = [], opts); 764 | } 765 | 766 | /** 767 | * Return route middleware with 768 | * the given callback `fn()`. 769 | * 770 | * @param {!PageCallback} fn 771 | * @return {!PageCallback} 772 | */ 773 | middleware(fn) { 774 | /** 775 | * @param {!Context} ctx 776 | * @param {function():?} next 777 | */ 778 | const callback = (ctx, next) => { 779 | if (this.match(ctx.path, ctx.params)) { 780 | ctx.routePath = this.path; 781 | return fn(ctx, next); 782 | } 783 | next(); 784 | }; 785 | return callback; 786 | } 787 | 788 | /** 789 | * Check if this route matches `path`, if so 790 | * populate `params`. 791 | * 792 | * @param {string} path 793 | * @param {Object} params 794 | * @return {boolean} 795 | */ 796 | match(path, params) { 797 | const keys = this.keys; 798 | const qsIndex = path.indexOf('?'); 799 | const pathname = ~qsIndex ? path.slice(0, qsIndex) : path; 800 | const m = this.regexp.exec(decodeURIComponent(pathname)); 801 | 802 | if (!m) { 803 | return false; 804 | } 805 | 806 | delete params[0]; 807 | 808 | for (let i = 1, len = m.length; i < len; ++i) { 809 | let key = keys[i - 1]; 810 | let val = this.page._decodeURLEncodedURIComponent(m[i]); 811 | if (val !== undefined || !(Object.hasOwnProperty.call(params, key.name))) { 812 | params[key.name] = val; 813 | } 814 | } 815 | 816 | return true; 817 | } 818 | } 819 | -------------------------------------------------------------------------------- /lib/route-data.js: -------------------------------------------------------------------------------- 1 | /** @fileoverview Basic data for a route */ 2 | 3 | class RouteData { 4 | /** 5 | * @param {string} id of this route 6 | * @param {string} tagName of the element 7 | * @param {string} path of this route 8 | * @param {!Array=} namedParameters list in camelCase. Will be 9 | * converted to a map of camelCase and hyphenated. 10 | * @param {boolean=} requiresAuthentication 11 | * @param {function():Promise=} beforeEnter 12 | */ 13 | constructor(id, tagName, path, namedParameters, requiresAuthentication, beforeEnter) { 14 | namedParameters = namedParameters || []; 15 | /** @type {!Object} */ 16 | const params = {}; 17 | const camelMatch = /[A-Z]/g; 18 | const camelMatchReplacer = (match) => `-${match.toLowerCase()}`; 19 | 20 | for (let i = 0; i < namedParameters.length; i++) { 21 | params[namedParameters[i]] = namedParameters[i].replace(camelMatch, camelMatchReplacer); 22 | } 23 | 24 | this.id = id; 25 | this.tagName = tagName; 26 | this.path = path; 27 | this.attributes = params; 28 | 29 | /** @type {!Element|undefined} */ 30 | this.element = undefined; 31 | this.requiresAuthentication = requiresAuthentication !== false; 32 | 33 | this.beforeEnter = beforeEnter || (() => Promise.resolve()); 34 | } 35 | } 36 | 37 | export default RouteData; 38 | -------------------------------------------------------------------------------- /lib/route-tree-node.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview A tree structure to hold routing states 3 | * 4 | * When changing from one route to another, callbacks 5 | * should be invoked in the proper order. In particular, 6 | * exitCallbacks from the old route should be called for 7 | * nodes which are not a member of the path of the new route. 8 | * It is expected the exitCallbacks will remove nodes from the 9 | * DOM and this should be avoided if the nodes would be immediately 10 | * re-added. 11 | * 12 | * Entry callbacks should always be called for the complete DOM. Entry 13 | * callbacks should only add DOM nodes if they are not already present. 14 | * However state parameters may have changed and so DOM attributes might 15 | * need updated. 16 | * 17 | * Implementation based on Closure-Library goog.struct.TreeNode 18 | * @see https://google.github.io/closure-library/api/goog.structs.TreeNode.html 19 | */ 20 | 21 | import {Context} from './page.js'; 22 | import RouteData from './route-data.js'; 23 | import BasicRoutingInterface from './routing-interface.js'; 24 | 25 | class RouteTreeNode { 26 | /** @param {!RouteData} data */ 27 | constructor(data) { 28 | /** 29 | * The key. 30 | * @private {string} 31 | */ 32 | this.key_ = data.id; 33 | 34 | /** 35 | * The value. 36 | * @private {!RouteData} 37 | */ 38 | this.value_ = data; 39 | 40 | /** 41 | * Reference to the parent node or null if it has no parent. 42 | * @private {?RouteTreeNode} 43 | */ 44 | this.parent_ = null; 45 | 46 | /** 47 | * Child nodes or null in case of leaf node. 48 | * @private {?Array} 49 | */ 50 | this.children_ = null; 51 | } 52 | 53 | /** 54 | * Gets the key. 55 | * @return {string} The key. 56 | */ 57 | getKey() { 58 | return this.key_; 59 | } 60 | 61 | /** 62 | * Gets the value. 63 | * @return {!RouteData} The value. 64 | */ 65 | getValue() { 66 | return this.value_; 67 | } 68 | 69 | /** @return {?RouteTreeNode} */ 70 | getParent() { 71 | return this.parent_; 72 | } 73 | 74 | /** @return {!Array} Immutable child nodes. */ 75 | getChildren() { 76 | return this.children_ || []; 77 | } 78 | 79 | /** 80 | * @return {!Array} All ancestor nodes in 81 | * bottom-up order. 82 | */ 83 | getAncestors() { 84 | const ancestors = []; 85 | let node = this.getParent(); 86 | while (node) { 87 | ancestors.push(node); 88 | node = node.getParent(); 89 | } 90 | return ancestors; 91 | } 92 | 93 | /** 94 | * @return {!RouteTreeNode} The root of the tree structure, 95 | * i.e. the farthest ancestor of the node or the node itself if it has no 96 | * parents. 97 | */ 98 | getRoot() { 99 | /** @type {!RouteTreeNode} */ 100 | let root = this; 101 | while (root.getParent()) { 102 | root = /** @type {!RouteTreeNode} */ (root.getParent()); 103 | } 104 | return root; 105 | } 106 | 107 | /** 108 | * Returns a node whose key matches the given one in the hierarchy rooted at 109 | * this node. The hierarchy is searched using an in-order traversal. 110 | * @param {string} key The key to search for. 111 | * @return {?RouteTreeNode} The node with the given key, or 112 | * null if no node with the given key exists in the hierarchy. 113 | */ 114 | getNodeByKey(key) { 115 | if (this.getKey() === key) { 116 | return this; 117 | } 118 | const children = this.getChildren(); 119 | for (let i = 0; i < children.length; i++) { 120 | const descendant = children[i].getNodeByKey(key); 121 | if (descendant) { 122 | return descendant; 123 | } 124 | } 125 | return null; 126 | } 127 | 128 | /** 129 | * Traverses the subtree with the possibility to skip branches. Starts with 130 | * this node, and visits the descendant nodes depth-first, in preorder. 131 | * @param {function(this:RouteTreeNode, !RouteTreeNode): 132 | * (boolean|undefined|void)} f Callback function. It takes the node as argument. 133 | * The children of this node will be visited if the callback returns true or 134 | * undefined, and will be skipped if the callback returns false. 135 | */ 136 | traverse(f) { 137 | if (f.call(this, this) !== false) { 138 | const children = this.getChildren(); 139 | for (let i = 0; i < children.length; i++) { 140 | children[i].traverse(f); 141 | } 142 | } 143 | } 144 | 145 | /** 146 | * Sets the parent node of this node. The callers must ensure that the parent 147 | * node and only that has this node among its children. 148 | * @param {RouteTreeNode} parent The parent to set. If 149 | * null, the node will be detached from the tree. 150 | * @protected 151 | */ 152 | setParent(parent) { 153 | this.parent_ = parent; 154 | } 155 | 156 | /** 157 | * Appends a child node to this node. 158 | * @param {!RouteTreeNode} child Orphan child node. 159 | */ 160 | addChild(child) { 161 | this.addChildAt(child, this.children_ ? this.children_.length : 0); 162 | } 163 | 164 | /** 165 | * Inserts a child node at the given index. 166 | * @param {!RouteTreeNode} child Orphan child node. 167 | * @param {number} index The position to insert at. 168 | */ 169 | addChildAt(child, index) { 170 | if (child.getParent()) { 171 | throw new Error('RouteTreeNode has an existing parent.'); 172 | } 173 | child.setParent(this); 174 | this.children_ = this.children_ || []; 175 | if (index < 0 || index > this.children_.length) { 176 | throw new Error('Index out of bounds.'); 177 | } 178 | this.children_.splice(index, 0, child); 179 | } 180 | 181 | /** 182 | * Removes the child node at the given index. 183 | * @param {number} index The position to remove from. 184 | * @return {RouteTreeNode} The removed node if any. 185 | */ 186 | removeChildAt(index) { 187 | const child = this.children_ && this.children_[index]; 188 | if (child) { 189 | child.setParent(null); 190 | this.children_.splice(index, 1); 191 | if (this.children_.length === 0) { 192 | this.children_ = null; 193 | } 194 | return child; 195 | } 196 | return null; 197 | } 198 | 199 | /** 200 | * Removes the given child node of this node. 201 | * @param {RouteTreeNode} child The node to remove. 202 | * @return {RouteTreeNode} The removed node if any. 203 | */ 204 | removeChild(child) { 205 | return child && 206 | this.removeChildAt(this.getChildren().indexOf(child)); 207 | } 208 | 209 | /** @return {boolean} */ 210 | requiresAuthentication() { 211 | if (this.getValue().requiresAuthentication) { 212 | return true; 213 | } 214 | 215 | if (this.getParent() !== null) { 216 | return this.getParent().requiresAuthentication(); 217 | } 218 | 219 | return false; 220 | } 221 | 222 | /** 223 | * Cause this route to become the active route. When a route is activated, 224 | * exitCallback functions from the routeData should be called up the tree in order 225 | * beginning with the previousNodeId until a common node is found with the 226 | * path from the root to this node. 227 | * 228 | * After exitCallback methods have all completed, entryCallbacks should 229 | * be called in order beginning with the root node down the tree through 230 | * this node. 231 | * 232 | * _Root_ 233 | * / \ 234 | * A D 235 | * / \ \ 236 | * B C E 237 | * 238 | * If the current route is "B" and the route for "E" is activated, 239 | * then the following callbacks should be invoked: 240 | * 241 | * B.exitCallback, A.exitCallback, Root.entryCallback, 242 | * D.entryCallback, E.entryCallback 243 | * 244 | * If the current route is "B" and the route for "C" is activated, 245 | * then the following callbacks should be invoked: 246 | * 247 | * B.exitCallback, Root.entryCallback, A.entryCallback, C.entryCallback 248 | * 249 | * @param {!string|undefined} previousRouteId 250 | * @param {!Context} context 251 | */ 252 | async activate(previousRouteId, context) { 253 | const routeId = this.getKey(); 254 | 255 | // Ancestors are a path from the parent up the tree to the root 256 | const entryNodes = [/** @type {!RouteTreeNode} */ (this)].concat(this.getAncestors()); 257 | entryNodes.reverse(); 258 | let exitNodes = []; 259 | 260 | // If a previousRouteId is provided, we need to calculate paths 261 | if (previousRouteId !== undefined) { 262 | const rootNode = this.getRoot(); 263 | const previousRouteNode = rootNode.getNodeByKey(previousRouteId); 264 | 265 | // Find common ancestors of the previous route node to this one. 266 | // Common ancestors should NOT have their exitCallbacks invoked. 267 | const previousRoutePath = [previousRouteNode].concat(previousRouteNode.getAncestors()); 268 | for (let i = 0; i < previousRoutePath.length; i++) { 269 | let foundCommonAncestor = false; 270 | for (let j = entryNodes.length - 1; j >= 0; j--) { 271 | // Once we find a common ancestor, trim the route and exit the loop. 272 | // Ancestors in common should not have their exitCallbacks invoked. 273 | if (previousRoutePath[i] === entryNodes[j]) { 274 | exitNodes = previousRoutePath.slice(0, i); 275 | foundCommonAncestor = true; 276 | break; 277 | } 278 | } 279 | if (foundCommonAncestor) { 280 | break; 281 | } 282 | } 283 | } 284 | 285 | // Exit nodes 286 | for (let exitIndex = 0; exitIndex < exitNodes.length; exitIndex++) { 287 | const currentExitNode = exitNodes[exitIndex]; 288 | const nextExitNode = exitNodes[exitIndex + 1]; 289 | const value = currentExitNode.getValue(); 290 | if (value) { 291 | const routingElem = /** @type {!BasicRoutingInterface} */ ( 292 | /** @type {?} */ (currentExitNode.getValue().element) 293 | ); 294 | if (!routingElem.routeExit) { 295 | throw new Error(`Element '${currentExitNode.getValue().tagName}' does not implement routeExit`); 296 | } 297 | await routingElem.routeExit(currentExitNode, nextExitNode, routeId, context); 298 | value.element = undefined; 299 | } 300 | } 301 | 302 | // entry nodes 303 | for (let entryIndex = 0; entryIndex < entryNodes.length; entryIndex++) { 304 | const currentEntryNode = entryNodes[entryIndex]; 305 | const nextEntryNode = entryNodes[entryIndex + 1]; 306 | 307 | if (currentEntryNode.getValue().element) { 308 | const routingElem = /** @type {!BasicRoutingInterface} */ ( 309 | /** @type {?} */ (currentEntryNode.getValue().element) 310 | ); 311 | if (!routingElem.routeEnter) { 312 | throw new Error(`Element '${currentEntryNode.getValue().tagName}' does not implement routeEnter`); 313 | } 314 | const shouldContinue = await routingElem.routeEnter(currentEntryNode, nextEntryNode, routeId, context); 315 | if (shouldContinue === false) { 316 | break; 317 | } 318 | } 319 | } 320 | } 321 | } 322 | 323 | export default RouteTreeNode; 324 | -------------------------------------------------------------------------------- /lib/routing-interface.js: -------------------------------------------------------------------------------- 1 | import RouteTreeNode from './route-tree-node.js'; 2 | import {Context} from './page.js'; 3 | 4 | /** @interface */ 5 | class BasicRoutingInterface { 6 | /** 7 | * Default implementation for the callback on entering a route node. 8 | * This will only be used if an element does not define it's own routeEnter method. 9 | * 10 | * @param {!RouteTreeNode} currentNode 11 | * @param {!RouteTreeNode|undefined} nextNodeIfExists 12 | * @param {string} routeId 13 | * @param {!Context} context 14 | * @return {!Promise} 15 | */ 16 | async routeEnter(currentNode, nextNodeIfExists, routeId, context) { } 17 | 18 | /** 19 | * Default implementation for the callback on exiting a route node. 20 | * This will only be used if an element does not define it's own routeExit method. 21 | * 22 | * @param {!RouteTreeNode} currentNode 23 | * @param {!RouteTreeNode|undefined} nextNode 24 | * @param {string} routeId 25 | * @param {!Context} context 26 | * @return {!Promise} 27 | */ 28 | async routeExit(currentNode, nextNode, routeId, context) { } 29 | }; 30 | 31 | export {BasicRoutingInterface as default}; 32 | -------------------------------------------------------------------------------- /lib/routing-mixin.js: -------------------------------------------------------------------------------- 1 | import RouteTreeNode from './route-tree-node.js'; 2 | import RouteData from './route-data.js'; 3 | import {Context} from './page.js'; 4 | import BasicRoutingInterface from './routing-interface.js'; 5 | 6 | /** 7 | * @param {function(new:HTMLElement)} Superclass 8 | * @polymer 9 | * @mixinFunction 10 | */ 11 | function routingMixin(Superclass) { 12 | /** 13 | * @polymer 14 | * @mixinClass 15 | * @implements {BasicRoutingInterface} 16 | */ 17 | class BasicRouting extends Superclass { 18 | /** 19 | * Default implementation for the callback on entering a route node. 20 | * This will only be used if an element does not define it's own routeEnter method. 21 | * 22 | * @param {!RouteTreeNode} currentNode 23 | * @param {!RouteTreeNode|undefined} nextNodeIfExists 24 | * @param {string} routeId 25 | * @param {!Context} context 26 | * @return {!Promise} 27 | */ 28 | async routeEnter(currentNode, nextNodeIfExists, routeId, context) { 29 | context.handled = true; 30 | const currentElement = /** @type {!Element} */ (currentNode.getValue().element); 31 | if (nextNodeIfExists) { 32 | const nextNode = /** @type {!RouteTreeNode} */ (nextNodeIfExists); 33 | 34 | const nextNodeData = /** @type {!RouteData} */(nextNode.getValue()); 35 | 36 | const thisElem = /** @type {!Element} */ (/** @type {?} */ (this)); 37 | /** @type {Element} */ 38 | let nextElement = nextNodeData.element || thisElem.querySelector(nextNodeData.tagName.toLowerCase()); 39 | // Reuse the element if it already exists in the dom. 40 | // Add a sanity check to make sure the element parent is what we expect 41 | if (!nextElement || nextElement.parentNode !== currentElement) { 42 | if (nextNodeData.tagName.indexOf('-') > 0) { 43 | let Elem = customElements && customElements.get(nextNodeData.tagName.toLowerCase()); 44 | if (!Elem) { 45 | await nextNodeData.beforeEnter(); 46 | // When code splitting, it's possible that the element created is not yet in the registry. 47 | // Wait until it is before creating it 48 | await customElements.whenDefined(nextNodeData.tagName.toLowerCase()); 49 | Elem = customElements.get(nextNodeData.tagName.toLowerCase()); 50 | } 51 | nextElement = new Elem(); 52 | } else { 53 | nextElement = document.createElement(nextNodeData.tagName); 54 | } 55 | } 56 | 57 | const setElementAttributes = (callCount, nextElement) => { 58 | try { 59 | // Set appropriate attributes on the element from the route params 60 | for (const key in nextNodeData.attributes) { 61 | if (key in context.params) { 62 | if (context.params[key] !== undefined) { 63 | nextElement.setAttribute(nextNodeData.attributes[key], context.params[key]); 64 | } else { 65 | nextElement.removeAttribute(nextNodeData.attributes[key]); 66 | } 67 | } 68 | } 69 | 70 | if (!nextElement.parentNode) { 71 | while (currentElement.firstChild) { 72 | currentElement.removeChild(currentElement.firstChild); 73 | } 74 | currentElement.appendChild(nextElement); 75 | } 76 | nextNode.getValue().element = /** @type {!Element} */ (nextElement); 77 | } catch (e) { 78 | // Internet Explorer can sometimes throw an exception when setting attributes immediately 79 | // after creating the element. Add a short delay and try again. 80 | if (/Trident/.test(navigator.userAgent) && callCount < 4) { 81 | return new Promise((resolve) => { 82 | setTimeout(resolve, 0); 83 | }).then(() => setElementAttributes(callCount + 1, nextElement)); 84 | } 85 | throw e; 86 | } 87 | }; 88 | await setElementAttributes(1, nextElement); 89 | } 90 | } 91 | 92 | /** 93 | * Default implementation for the callback on exiting a route node. 94 | * This will only be used if an element does not define it's own routeExit method. 95 | * 96 | * @param {!RouteTreeNode} currentNode 97 | * @param {!RouteTreeNode|undefined} nextNode 98 | * @param {string} routeId 99 | * @param {!Context} context 100 | * @return {!Promise} 101 | */ 102 | async routeExit(currentNode, nextNode, routeId, context) { 103 | const currentElement = currentNode.getValue().element; 104 | 105 | if (currentElement.parentNode) { 106 | currentElement.parentNode.removeChild(/** @type {!Element} */ (currentElement)); 107 | } 108 | currentNode.getValue().element = undefined; 109 | } 110 | } 111 | 112 | return BasicRouting; 113 | } 114 | 115 | //exporting BasicRoutingInterface for backward compatibility - don't break consumer imports 116 | export { routingMixin as default, BasicRoutingInterface }; 117 | -------------------------------------------------------------------------------- /lib/routing.mixin.js: -------------------------------------------------------------------------------- 1 | import BasicRoutingInterface from './routing-interface.js'; 2 | import routingMixin from './routing-mixin.js'; 3 | const RoutingMixin = (superclass) => { 4 | return routingMixin(superclass); 5 | }; 6 | //exporting BasicRoutingInterface for backward compatibility - don't break consumer imports 7 | export { RoutingMixin, BasicRoutingInterface }; 8 | -------------------------------------------------------------------------------- /lib/routing.mixin.ts: -------------------------------------------------------------------------------- 1 | import BasicRoutingInterface from './routing-interface.js'; 2 | import type { Context } from './page.js'; 3 | import type RouteTreeNode from './route-tree-node.js'; 4 | import routingMixin from './routing-mixin.js'; 5 | export type Constructor = new (...args: any[]) => T; 6 | export declare class RoutingMixinInterface { 7 | routeEnter(currentNode: RouteTreeNode, nextNodeIfExists: RouteTreeNode | undefined, routeId: string, context: Context): Promise; 8 | routeExit(currentNode: RouteTreeNode, nextNode: RouteTreeNode | undefined, routeId: string, context: Context): Promise; 9 | } 10 | const RoutingMixin = >(superclass: T) => { 11 | return routingMixin(superclass) as unknown as Constructor & T; 12 | }; 13 | 14 | //exporting BasicRoutingInterface for backward compatibility - don't break consumer imports 15 | export { RoutingMixin, BasicRoutingInterface }; 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@jack-henry/web-component-router", 3 | "version": "3.11.0", 4 | "description": "Web Components Router", 5 | "main": "router.js", 6 | "type": "module", 7 | "files": [ 8 | "lib/", 9 | "*.js", 10 | "*.mixin.ts", 11 | "*.d.ts", 12 | "package.json", 13 | "README.md" 14 | ], 15 | "scripts": { 16 | "build": "tsc && yarn build:ts", 17 | "build:ts": "tsc --project tsconfig.buildts.json", 18 | "clean": "rimraf ./lib/**/*.d.ts ./*.d.ts ./lib/*.mixin.js --glob", 19 | "prepublish": "yarn clean && yarn build", 20 | "pretest": "playwright install", 21 | "test": "vitest" 22 | }, 23 | "repository": { 24 | "type": "git", 25 | "url": "git+https://github.com/Banno/web-component-router.git" 26 | }, 27 | "author": "Chad Killingsworth ", 28 | "license": "Apache-2.0", 29 | "bugs": { 30 | "url": "https://github.com/Banno/web-component-router/issues" 31 | }, 32 | "homepage": "https://github.com/Banno/web-component-router", 33 | "dependencies": { 34 | "path-to-regexp": "^6.3.0" 35 | }, 36 | "resolutions": { 37 | "@types/node": "^18.11.18" 38 | }, 39 | "devDependencies": { 40 | "@polymer/polymer": "^3.4.1", 41 | "@types/node": "^22.10.0", 42 | "@vitest/browser": "^2.1.6", 43 | "playwright": "1.52.0", 44 | "rimraf": "^6.0.1", 45 | "typescript": "^5.2.2", 46 | "vite": "6.3.2", 47 | "vitest": "^2.1.6" 48 | }, 49 | "packageManager": "yarn@1.22.22" 50 | } 51 | -------------------------------------------------------------------------------- /router.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Router for banno web. 3 | * 4 | * Page.js is used to parse route paths and invoke callbacks 5 | * when routes are entered or exited. This class provides 6 | * support for storing the routing tree and registering the 7 | * route callbacks with Page.js. 8 | * 9 | * Routes should form a tree. Example: 10 | * 11 | * _Root_ 12 | * / \ 13 | * A D 14 | * / \ \ 15 | * B C E 16 | */ 17 | 18 | /** 19 | * @typedef {{ 20 | * id: string, 21 | * tagName: string, 22 | * path: string, 23 | * params: (Array|undefined), 24 | * authenticated: (boolean|undefined), 25 | * subRoutes: (Array|undefined), 26 | * beforeEnter: (function():Promise) 27 | * }} RouteConfig 28 | */ 29 | let RouteConfig; 30 | 31 | import {Context, Page} from './lib/page.js'; 32 | import RouteTreeNode from './lib/route-tree-node.js'; 33 | import routingMixin from './lib/routing-mixin.js'; 34 | import animatedRoutingMixin from './lib/animated-routing-mixin.js'; 35 | import BasicRoutingInterface from './lib/routing-interface.js'; 36 | import RouteData from './lib/route-data.js'; 37 | 38 | class Router { 39 | /** @param {RouteConfig=} routeConfig */ 40 | constructor(routeConfig) { 41 | /** @type {string|undefined} */ 42 | this.currentNodeId_; 43 | 44 | /** @type {string|undefined} */ 45 | this.prevNodeId_; 46 | 47 | /** @type {!RouteTreeNode|undefined} */ 48 | this.routeTree_ = routeConfig ? this.buildRouteTree(routeConfig) : undefined; 49 | 50 | this.nextStateWasPopped = false; 51 | 52 | // Uses the capture phase so that this executes before the page.js handler 53 | window.addEventListener('popstate', (evt) => { 54 | this.nextStateWasPopped = true; 55 | }, true); 56 | 57 | /** @type {!Set} */ 58 | this.routeChangeStartCallbacks_ = new Set(); 59 | /** @type {!Set} */ 60 | this.routeChangeCompleteCallbacks_ = new Set(); 61 | 62 | this.page = new Page(); 63 | } 64 | 65 | /** @return {!RouteTreeNode|undefined} */ 66 | get routeTree() { 67 | return this.routeTree_; 68 | } 69 | 70 | /** @param {!RouteTreeNode|undefined} root */ 71 | set routeTree(root) { 72 | this.routeTree_ = root; 73 | } 74 | 75 | /** @return {string|undefined} */ 76 | get currentNodeId() { 77 | return this.currentNodeId_; 78 | } 79 | 80 | /** @return {string|undefined} */ 81 | get prevNodeId() { 82 | return this.prevNodeId_; 83 | } 84 | 85 | /** @param {!RouteConfig} routeConfig */ 86 | buildRouteTree(routeConfig) { 87 | const authenticated = [true, false].includes(routeConfig.authenticated) ? routeConfig.authenticated : true; 88 | const node = new RouteTreeNode(new RouteData(routeConfig.id, routeConfig.tagName, routeConfig.path, routeConfig.params || [], authenticated, routeConfig.beforeEnter)); 89 | if (routeConfig.subRoutes) { 90 | routeConfig.subRoutes.forEach(route => { 91 | node.addChild(this.buildRouteTree(route)); 92 | }); 93 | } 94 | return node; 95 | } 96 | 97 | /** 98 | * Build the routing tree and begin routing 99 | * @return {!Promise} 100 | */ 101 | async start() { 102 | this.registerRoutes_(); 103 | 104 | document.addEventListener('tap', this.page.clickHandler.bind(this.page), false); 105 | document.addEventListener('click', this.page.clickHandler.bind(this.page), false); 106 | 107 | return this.page.start({ 108 | click: false, 109 | popstate: true, 110 | hashbang: false, 111 | decodeURLComponents: true, 112 | window: undefined, 113 | dispatch: undefined 114 | }); 115 | } 116 | 117 | /** 118 | * Navigate to the specified route 119 | * @param {string} path 120 | * @param {Object=} params Values to use for named & query parameters 121 | * @returns {!Promise} 122 | */ 123 | async go(path, params) { 124 | path = this.url(path, params); 125 | return this.page.show(path); 126 | } 127 | 128 | /** 129 | * Navigate to the specified route, but replace the current history event with the new one 130 | * @param {string} path 131 | * @param {Object=} params Values to use for named & query parameters 132 | * NOTE: You must quote the properties so that Closure Compiler does not rename them! 133 | * @return {!Promise} 134 | */ 135 | async redirect(path, params) { 136 | path = this.url(path, params); 137 | return this.page.replace(path); 138 | } 139 | 140 | /** 141 | * Return the path for the specified route 142 | * @param {string} path 143 | * @param {Object=} params Values to use for named & query parameters 144 | * NOTE: You must quote the properties so that Closure Compiler does not rename them! 145 | * @return {string} 146 | */ 147 | url(path, params) { 148 | const paramPattern = [ 149 | ':[a-zA-Z]+', // param name 150 | '(\\([^)]*\\))?', // optional parens with stuff inside 151 | '\\??', // optional question mark 152 | '(/|$)', // slash separator or end of string 153 | ]; 154 | 155 | // Replace params with their values. 156 | if (params) { 157 | path = Object.entries(params).reduce((currentPath, [key, val]) => { 158 | const pattern = paramPattern.slice(0); // clone the original pattern 159 | pattern[0] = `:${key}`; 160 | const pathMatcher = new RegExp(pattern.join('')); 161 | if (pathMatcher.test(currentPath)) { 162 | // Found the param in the path. Replace it with the given value. 163 | currentPath = currentPath.replace(pathMatcher, `${val}$2`); 164 | } else { 165 | // Append the param as a query parameter. 166 | const delimiter = currentPath.includes('?') ? '&' : '?'; 167 | currentPath += `${delimiter}${encodeURIComponent(key)}=${encodeURIComponent(val)}`; 168 | } 169 | return currentPath; 170 | }, path); 171 | } 172 | 173 | // Remove any optional params that don't have values. 174 | paramPattern[2] = '\\?'; // not optional for this test 175 | path = path.replace(new RegExp(paramPattern.join(''), 'g'), ''); 176 | 177 | return path; 178 | } 179 | 180 | /** 181 | * Register an exit callback to be invoked on every route change 182 | * @param {function(!Context, function(boolean=):?):?} callback 183 | */ 184 | addGlobalExitHandler(callback) { 185 | this.page.exit('*', callback); 186 | } 187 | 188 | /** 189 | * Register an exit callback for a particular route 190 | * @param {!string} route 191 | * @param {function(!Context, function(boolean=):?):?} callback 192 | */ 193 | addExitHandler(route, callback) { 194 | this.page.exit(route, callback); 195 | } 196 | 197 | /** 198 | * Register an entry callback for a particular route 199 | * @param {!string} route 200 | * @param {function(!Context, function(boolean=):?):?} callback 201 | */ 202 | addRouteHandler(route, callback) { 203 | this.page.register(route, callback); 204 | } 205 | 206 | /** @param {!function():?} callback */ 207 | addRouteChangeStartCallback(callback) { 208 | this.routeChangeStartCallbacks_.add(callback); 209 | } 210 | 211 | /** @param {!function():?} callback */ 212 | removeRouteChangeStartCallback(callback) { 213 | this.routeChangeStartCallbacks_.delete(callback); 214 | } 215 | 216 | /** @param {!function(!Error=):?} callback */ 217 | addRouteChangeCompleteCallback(callback) { 218 | this.routeChangeCompleteCallbacks_.add(callback); 219 | } 220 | 221 | /** @param {!function(!Error=):?} callback */ 222 | removeRouteChangeCompleteCallback(callback) { 223 | this.routeChangeCompleteCallbacks_.delete(callback); 224 | } 225 | 226 | /** 227 | * Walk the route tree and register route nodes with 228 | * the Page.js router. 229 | * 230 | * @private 231 | */ 232 | registerRoutes_() { 233 | this.routeTree_.traverse((node) => { 234 | if (node === null) { 235 | return; 236 | } 237 | 238 | const routeData = node.getValue(); 239 | 240 | // Routes with zero-length paths have no direct routes. 241 | // They only exist to wrap sub-routes. 242 | if (routeData.path.length === 0) { 243 | return; 244 | } 245 | 246 | this.page.register(routeData.path, this.routeChangeCallback_.bind(this, node)); 247 | }); 248 | } 249 | 250 | /** 251 | * @param {!RouteTreeNode} routeTreeNode 252 | * @param {!Context} context 253 | * @param {function():?} next 254 | * @private 255 | */ 256 | async routeChangeCallback_(routeTreeNode, context, next) { 257 | for (const cb of this.routeChangeStartCallbacks_) { 258 | cb(); 259 | } 260 | this.prevNodeId_ = this.currentNodeId_; 261 | this.currentNodeId_ = routeTreeNode.getKey(); 262 | /** @type {!Error|undefined} */ 263 | let routeError; 264 | try { 265 | await routeTreeNode.activate(this.prevNodeId, context); 266 | } catch (err) { 267 | routeError = err; 268 | } 269 | next(); 270 | this.nextStateWasPopped = false; 271 | for (const cb of this.routeChangeCompleteCallbacks_) { 272 | cb(routeError); 273 | } 274 | } 275 | 276 | /** 277 | * Replace route path param values with their param name 278 | * for analytics tracking 279 | * 280 | * @param {!Context} context route enter context 281 | * @return {!string} 282 | */ 283 | getRouteUrlWithoutParams(context) { 284 | const segments = context.path.split('/'); 285 | const params = {}; 286 | 287 | // flip the keys and values of the params 288 | for (const param in context.params) { 289 | if (context.params.hasOwnProperty(param)) { 290 | params[context.params[param]] = param; 291 | } 292 | } 293 | 294 | for (let i = 0; i < segments.length; i++) { 295 | if (segments[i] in params) { 296 | segments[i] = params[segments[i]]; 297 | } 298 | } 299 | 300 | return segments.join('/'); 301 | } 302 | } 303 | 304 | export default Router; 305 | export {animatedRoutingMixin, BasicRoutingInterface, Context, RouteData, RouteTreeNode, routingMixin}; 306 | -------------------------------------------------------------------------------- /test/fixtures/custom-fixture.js: -------------------------------------------------------------------------------- 1 | import {PolymerElement, html} from '@polymer/polymer/polymer-element.js'; 2 | import {BasicRoutingInterface, routingMixin} from '../../router.js'; 3 | /** 4 | * @constructor 5 | * @extends {PolymerElement} 6 | * @implements {BasicRoutingInterface} 7 | */ 8 | const RoutedElement = routingMixin(PolymerElement); 9 | 10 | class CustomFixtureElement extends RoutedElement { 11 | static get is() { 12 | return 'custom-fixture'; 13 | } 14 | 15 | static get template() { 16 | return html`
`; 17 | } 18 | } 19 | 20 | customElements.define(CustomFixtureElement.is, CustomFixtureElement); 21 | export default CustomFixtureElement; 22 | -------------------------------------------------------------------------------- /test/page-spec.js: -------------------------------------------------------------------------------- 1 | import { Context, Page } from '../lib/page.js'; 2 | import {vi, expect} from 'vitest'; 3 | 4 | function JSCompiler_renameProperty(propName, instance) { 5 | return propName; 6 | } 7 | 8 | describe('Page', () => { 9 | let page; 10 | 11 | beforeEach(() => { 12 | page = new Page(); 13 | vi.spyOn(History.prototype, 'pushState'); 14 | vi.spyOn(Context.prototype, 'pushState'); 15 | }); 16 | 17 | const dispatchPropertyName = JSCompiler_renameProperty('dispatch', Page.prototype); 18 | 19 | describe('show(path, state, dispatch = true, push = true)', () => { 20 | const path = '/'; 21 | const state = {}; 22 | const dispatch = true; 23 | const push = false; 24 | beforeEach(() => { 25 | vi.spyOn(page, dispatchPropertyName); 26 | }); 27 | describe('when dispatch === false', () => { 28 | const dispatch = false; 29 | it('does not call dispatch(ctx, prev)', async () => { 30 | await page.show(path, state, dispatch, push); 31 | expect(page.dispatch).not.toHaveBeenCalled(); 32 | }); 33 | describe('when push === true', () => { 34 | const push = true; 35 | it('calls context.pushState() synchronously', () => { 36 | page.show(path, state, dispatch, push); 37 | expect(Context.prototype.pushState).toHaveBeenCalled(); 38 | }); 39 | }); 40 | describe('when push === false', () => { 41 | const push = false; 42 | it('does not call context.pushState()', async () => { 43 | await page.show(path, state, dispatch, push); 44 | expect(Context.prototype.pushState).not.toHaveBeenCalled(); 45 | }); 46 | }); 47 | }); 48 | describe('when dispatch === true', () => { 49 | const dispatch = true; 50 | it('calls dispatch(ctx, prev, push)', () => { 51 | const prev = page.prevContext; 52 | page.show(path, state, dispatch); 53 | expect(page.dispatch).toHaveBeenCalledWith(expect.any(Context), prev); 54 | }); 55 | describe('when push === false', () => { 56 | const push = false; 57 | it('does not call context.pushState()', async () => { 58 | await page.show(path, state, dispatch, push); 59 | expect(Context.prototype.pushState).not.toHaveBeenCalled(); 60 | }); 61 | }); 62 | }); 63 | }); 64 | 65 | describe('dispatch(ctx, prev)', () => { 66 | let ctx; 67 | let prev; 68 | const state = {}; 69 | const prevState = {}; 70 | beforeEach(() => { 71 | ctx = new Context('/next', {...state}, page); 72 | prev = new Context('/prev', {...prevState}, page); 73 | // add entry callback to handle context to ensure that the `unhandled` callback doesn't do a full page reload 74 | page.callbacks.push((ctx, next) => { ctx.handled = true; next(); }); 75 | }); 76 | it('calls all exit callbacks in order', async () => { 77 | const exitCallback1 = async (ctx, next) => { 78 | ctx.state['exitCallback1Called'] = true; 79 | expect(ctx.state['exitCallback2Called']).toBe(undefined); 80 | next(); 81 | } 82 | const exitCallback2 = async (ctx, next) => { 83 | expect(prev.state['exitCallback1Called']).toBe(true); 84 | ctx.state['exitCallback2Called'] = true; 85 | next(); 86 | } 87 | page.exits = [exitCallback1, exitCallback2]; 88 | expect(prev.state['exitCallback1Called']).toBe(undefined); 89 | expect(prev.state['exitCallback2Called']).toBe(undefined); 90 | await page.dispatch(ctx, prev); 91 | expect(prev.state['exitCallback1Called']).toBe(true); 92 | expect(prev.state['exitCallback2Called']).toBe(true); 93 | }); 94 | it('calls all entry callbacks in order', async () => { 95 | const entryCallback1 = (ctx, next) => { 96 | ctx.handled = true; 97 | ctx.state['entryCallback1Called'] = true; 98 | expect(ctx.state['entryCallback2Called']).toBe(undefined); 99 | next(); 100 | } 101 | const entryCallback2 = (ctx, next) => { 102 | ctx.handled = true; 103 | expect(ctx.state['entryCallback1Called']).toBe(true); 104 | ctx.state['entryCallback2Called'] = true; 105 | next(); 106 | } 107 | page.callbacks = [entryCallback1, entryCallback2]; 108 | expect(ctx.state['entryCallback1Called']).toBe(undefined); 109 | expect(ctx.state['entryCallback2Called']).toBe(undefined); 110 | await page.dispatch(ctx, prev); 111 | expect(ctx.state['entryCallback1Called']).toBe(true); 112 | expect(ctx.state['entryCallback2Called']).toBe(true); 113 | }); 114 | 115 | describe('when at least one entry callback sets ctx.handled = true', () => { 116 | it('does not reset ctx.handled = false', async () => { 117 | const entryCallback1 = (ctx, next) => { 118 | ctx.handled = true; 119 | ctx.state['entryCallback1Called'] = true; 120 | expect(ctx.state['entryCallback2Called']).toBe(undefined); 121 | next(); 122 | } 123 | const entryCallback2 = (ctx, next) => { 124 | expect(ctx.state['entryCallback1Called']).toBe(true); 125 | expect(ctx.handled).toBe(true); 126 | ctx.state['entryCallback2Called'] = true; 127 | next(); 128 | } 129 | page.callbacks = [entryCallback1, entryCallback2]; 130 | expect(ctx.state['entryCallback1Called']).toBe(undefined); 131 | expect(ctx.state['entryCallback2Called']).toBe(undefined); 132 | await page.dispatch(ctx, prev); 133 | expect(ctx.state['entryCallback1Called']).toBe(true); 134 | expect(ctx.state['entryCallback2Called']).toBe(true); 135 | }); 136 | }); 137 | describe('when ctx.pushState === true and ctx.path === page.current', () => { 138 | const push = true; 139 | beforeEach(() => { 140 | ctx = new Context('/next', state, page, push); 141 | prev = new Context('/prev', prevState, page, push); 142 | page.current = ctx.path; 143 | }) 144 | it('calls context.pushState() asynchronously between exit and entry callbacks', async () => { 145 | const exitCallback = (ctx, next) => { 146 | expect(Context.prototype.pushState).not.toHaveBeenCalled(); // not yet 147 | ctx.state['exitCallbackCalled'] = true; 148 | next(); 149 | } 150 | const entryCallback = (ctx, next) => { 151 | expect(Context.prototype.pushState).not.toHaveBeenCalled(); // still not yet 152 | ctx.handled = true; 153 | // expect `pushState` to have been called when `handled` is changed from not true to `true` 154 | expect(Context.prototype.pushState).toHaveBeenCalled(); 155 | next(); 156 | } 157 | page.exits = [exitCallback]; 158 | page.callbacks = [entryCallback]; 159 | 160 | const dispatchPromise = page.dispatch(ctx, prev); 161 | expect(ctx.handled).toBe(undefined); // entry callback should not have been called yet 162 | await dispatchPromise; 163 | expect(prevState['exitCallbackCalled']).toBe(true); // exit handler should have been called 164 | expect(ctx.handled).toBe(true); // sanity check to ensure that the entry callback was called 165 | }); 166 | }); 167 | describe('when ctx.pushState === false', () => { 168 | const push = false; 169 | beforeEach(() => { 170 | ctx = new Context('/next', state, page, push); 171 | prev = new Context('/prev', prevState, page, push); 172 | page.current = ctx.path; 173 | }) 174 | it('does not call context.pushState()', async () => { 175 | const exitCallback = (ctx, next) => { 176 | expect(Context.prototype.pushState).not.toHaveBeenCalled(); // not yet 177 | ctx.state['exitCallbackCalled'] = true; 178 | next(); 179 | } 180 | const entryCallback = (ctx, next) => { 181 | ctx.handled = true; 182 | // expect `pushState` to have been called when `handled` is changed from not true to `true` 183 | expect(Context.prototype.pushState).not.toHaveBeenCalled(); 184 | next(); 185 | } 186 | page.exits = [exitCallback]; 187 | page.callbacks = [entryCallback]; 188 | 189 | const dispatchPromise = page.dispatch(ctx, prev); 190 | expect(ctx.handled).toBe(undefined); // entry callback should not have been called yet 191 | await dispatchPromise; 192 | expect(prevState['exitCallbackCalled']).toBe(true); // exit handler should have been called 193 | expect(ctx.handled).toBe(true); // sanity check to ensure that the entry callback was called 194 | }); 195 | }); 196 | }); 197 | }); 198 | -------------------------------------------------------------------------------- /test/route-tree-node-spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview 3 | * 4 | * @suppress {visibility} Tests are allowed to access private methods 5 | * 6 | * Route tree used for tests 7 | * 8 | * _Root_ 9 | * / \ 10 | * A D 11 | * / \ \ 12 | * B C E 13 | */ 14 | import testRouteTree from './utils/testing-route-setup.js'; 15 | import {Context} from '../router.js'; 16 | import {vi} from 'vitest'; 17 | 18 | describe('RouteTreeNode', () => { 19 | const ROOT = testRouteTree.tree.getNodeByKey(testRouteTree.Id.ROOT); 20 | const A = testRouteTree.tree.getNodeByKey(testRouteTree.Id.A); 21 | const B = testRouteTree.tree.getNodeByKey(testRouteTree.Id.B); 22 | const C = testRouteTree.tree.getNodeByKey(testRouteTree.Id.C); 23 | const D = testRouteTree.tree.getNodeByKey(testRouteTree.Id.D); 24 | const E = testRouteTree.tree.getNodeByKey(testRouteTree.Id.E); 25 | let routePath = []; 26 | 27 | /** @polymer */ 28 | class RoutedElement extends HTMLElement { // eslint-disable-line @banno/ux/custom-element-name 29 | static get is() { 30 | return 'routed-element'; 31 | } 32 | constructor(keyName = undefined) { 33 | super(); 34 | this.keyName = keyName; 35 | } 36 | async routeEnter(node, nextNode, routeId, context) { 37 | routePath.push(`${this.keyName}-enter`); 38 | } 39 | async routeExit(node, nextNode, routeId, context) { 40 | routePath.push(`${this.keyName}-exit`); 41 | } 42 | } 43 | customElements.define(RoutedElement.is, RoutedElement); 44 | 45 | it('node requires authentication', () => { 46 | expect(E.requiresAuthentication()).toBe(true); 47 | }); 48 | 49 | it('node does not require authentication', () => { 50 | expect(ROOT.requiresAuthentication()).toBe(false); 51 | }); 52 | 53 | it('node does not require authentication, but a parent does', () => { 54 | expect(B.requiresAuthentication()).toBe(true); 55 | }); 56 | 57 | describe('activate function', () => { 58 | beforeEach(() => { 59 | ROOT.getValue().element = new RoutedElement('ROOT'); 60 | A.getValue().element = new RoutedElement('A'); 61 | B.getValue().element = new RoutedElement('B'); 62 | C.getValue().element = new RoutedElement('C'); 63 | D.getValue().element = new RoutedElement('D'); 64 | E.getValue().element = new RoutedElement('E'); 65 | routePath = []; 66 | }); 67 | 68 | it('activating a route without a previous route id only invokes entry methods', async () => { 69 | await C.activate(undefined, new Context('/C')); 70 | expect(routePath.join('_')).toBe('ROOT-enter_A-enter_C-enter'); 71 | }); 72 | 73 | it('activating a route should call the correct methods', async () => { 74 | await E.activate(B.getKey(), new Context('/D/E')); 75 | expect(routePath.join('_')).toBe('B-exit_A-exit_ROOT-enter_D-enter_E-enter'); 76 | }); 77 | 78 | it('activating a route should call the correct methods 2', async () => { 79 | await C.activate(B.getKey(), new Context('/C')); 80 | expect(routePath.join('_')).toBe('B-exit_ROOT-enter_A-enter_C-enter'); 81 | }); 82 | 83 | it('returning "false" from the routeEnter method should prevent future methods from being invoked', async () => { 84 | vi.spyOn(A.getValue().element, 'routeEnter').mockImplementation(function() { 85 | routePath.push('A-enter'); 86 | return Promise.resolve(false); 87 | }); 88 | await C.activate(E.getKey(), new Context('/D/E')); 89 | expect(routePath.join('_')).toBe('E-exit_D-exit_ROOT-enter_A-enter'); 90 | }); 91 | }); 92 | }); 93 | -------------------------------------------------------------------------------- /test/router-spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview 3 | * 4 | * Route tree used for tests 5 | * 6 | * _Root_ 7 | * / \ 8 | * A D 9 | * / \ \ 10 | * B C E 11 | */ 12 | 13 | import testRouteTree from './utils/testing-route-setup.js'; 14 | import testRouteConfig from './utils/test-route-config.js'; 15 | import Router, {Context, RouteTreeNode} from '../router.js'; 16 | import RoutedElement from './fixtures/custom-fixture.js'; 17 | import {vi} from 'vitest'; 18 | 19 | function JSCompiler_renameProperty(propName, instance) { 20 | return propName; 21 | } 22 | 23 | describe('Router', () => { 24 | let router = new Router(); 25 | 26 | const A = testRouteTree.tree.getNodeByKey(testRouteTree.Id.A); 27 | const B = testRouteTree.tree.getNodeByKey(testRouteTree.Id.B); 28 | 29 | const originalRouteChangeCallback = router.routeChangeCallback_; 30 | /** @type {!Function} */ 31 | let newRouteChangeCallback; 32 | 33 | const startPropertyName = JSCompiler_renameProperty('start', router.page); 34 | 35 | beforeAll(() => { 36 | // reset router 37 | router.routeTree = testRouteTree.tree; 38 | 39 | router.routeChangeCallback_ = (function(...args) { 40 | originalRouteChangeCallback.apply(this, args); 41 | if (newRouteChangeCallback) { 42 | newRouteChangeCallback.apply(this, args); 43 | } 44 | }).bind(router); 45 | 46 | router.page.register('*', (context, next) => { 47 | context.handled = true; // prevents actually leaving the page 48 | next(); 49 | }); 50 | }); 51 | 52 | afterAll(() => { 53 | // reset router 54 | router.currentNodeId_ = undefined; 55 | }); 56 | 57 | beforeEach(() => { 58 | vi.spyOn(router.page, startPropertyName); 59 | }); 60 | 61 | it('.start should register routes and start routing', () => { 62 | const initialCallbackLength = router.page.callbacks.length; 63 | const builtinCallbackLength = 0; 64 | 65 | router.start(); 66 | // router.page should be called to register routes ROOT, B, C, D, E. 67 | // A should NOT be registered as it is abstract (has a zero length path). 68 | expect(router.page.callbacks.length).toBe(5 + initialCallbackLength + builtinCallbackLength); 69 | expect(router.page.start).toHaveBeenCalled(); 70 | }); 71 | 72 | it('should not have a previous route id initially', () => { 73 | expect(router.currentNodeId_).toBe(undefined); 74 | }); 75 | 76 | it('callbacks should call router.routeChangeCallback_ with the correct this binding and arguments', async () => { 77 | newRouteChangeCallback = (function(node, ...args) { 78 | expect(args.length).toBe(2); 79 | expect(this instanceof router.constructor).toBe(true); 80 | expect(node instanceof RouteTreeNode).toBe(true); 81 | 82 | }).bind(router); 83 | 84 | router.go('/B/somedata'); 85 | }); 86 | 87 | it('should store the previous route id', () => { 88 | expect(router.currentNodeId_).toBe(testRouteTree.Id.B); 89 | }); 90 | 91 | describe('Router constructor', () => { 92 | afterAll(() => { 93 | // reset routeTree 94 | router = new Router(); 95 | }); 96 | 97 | it('should leave the routeTree undefined if instantiated without a route configuration', () => { 98 | router = new Router(); 99 | expect(router.routeTree).toBe(undefined); 100 | }); 101 | 102 | it('should create the routeTree when instantiated with the route configuration', () => { 103 | router = new Router(testRouteConfig); 104 | expect(router.routeTree).not.toBe(undefined); 105 | }); 106 | }); 107 | 108 | describe('buildRouteTree', () => { 109 | const testSubRouteData = [{ 110 | id: 'app-user', 111 | tagName: 'APP-USER-PAGE', 112 | path: '/users/:userId([0-9]{1,6})', 113 | requiresAuthentication: true, 114 | params: ['userId'], 115 | beforeEnter: () => Promise.resolve(), 116 | }, { 117 | id: 'app-user-account', 118 | tagName: 'APP-ACCOUNT-PAGE', 119 | path: '/users/:userId([0-9]{1,6})/accounts/:accountId([0-9]{1,6})', 120 | requiresAuthentication: true, 121 | params: ['userId', 'accountId'], 122 | }, { 123 | id: 'app-about', 124 | tagName: 'APP-ABOUT', 125 | path: '/about', 126 | requiresAuthentication: false, 127 | }]; 128 | 129 | it('should create a routeTree with the correct properties', () => { 130 | const routeTree = router.buildRouteTree(testRouteConfig); 131 | const subRoutes = routeTree.getChildren(); 132 | expect(routeTree.requiresAuthentication()).toBe(true); 133 | expect(routeTree.getKey()).toBe('app'); 134 | expect(subRoutes.length).toBe(3); 135 | subRoutes.forEach((route, index) => { 136 | const data = route.getValue(); 137 | if (testSubRouteData[index].params) { 138 | expect(Object.keys(data.attributes)).toEqual(testSubRouteData[index].params); 139 | } 140 | expect(data.beforeEnter).not.toBe(undefined); 141 | ['id', 'tagName', 'path', 'requiresAuthentication'].forEach((prop) => { 142 | expect(data[prop]).toBe(testSubRouteData[index][prop]); 143 | }); 144 | 145 | }); 146 | }); 147 | 148 | it('should set authentication to true by default', () => { 149 | const routeTree = router.buildRouteTree(testRouteConfig); 150 | const subRoutes = routeTree.getChildren(); 151 | expect(subRoutes[0].getValue().requiresAuthentication).toBe(true); 152 | expect(subRoutes[2].getValue().requiresAuthentication).toBe(false); 153 | }); 154 | }); 155 | 156 | describe('url()', () => { 157 | it('should return the path if there are no other parameters', () => { 158 | expect(router.url('/A')).toBe('/A'); 159 | }); 160 | 161 | it('should replace path parameters with given values', () => { 162 | expect(router.url('/account/:accountId([-a-fA-F0-9]{36})/documents/:docId', { 163 | 'accountId': '1234', 164 | 'docId': '6789', 165 | })).toBe('/account/1234/documents/6789'); 166 | 167 | expect(router.url('/account/:accountId([-a-fA-F0-9]{36})/documents/:docId', { 168 | 'docId': '6789', 169 | 'accountId': '1234', 170 | })).toBe('/account/1234/documents/6789'); 171 | 172 | expect(router.url('/account/:accountId([-a-fA-F0-9]{36})?/documents/:docId?', { 173 | 'accountId': '2345', 174 | 'docId': '7890', 175 | })).toBe('/account/2345/documents/7890'); 176 | }); 177 | 178 | it('should allow a trailing slash', () => { 179 | expect(router.url('/account/:accountId/', { 180 | 'accountId': '1234' 181 | })).toBe('/account/1234/'); 182 | expect(router.url('/account/:accountId([-a-fA-F0-9]{36})/', { 183 | 'accountId': '1234' 184 | })).toBe('/account/1234/'); 185 | }); 186 | 187 | it('should append non-path parameters as query parameters', () => { 188 | const url = router.url('/B/:bData', { 189 | 'bData': 'bdata', 190 | 'foo': 1, 191 | 'bar[]': 'ABC & abc & 123', 192 | }); 193 | expect(url).toBe('/B/bdata?foo=1&bar%5B%5D=ABC%20%26%20abc%20%26%20123'); 194 | }); 195 | 196 | it('should exclude optional parameters', () => { 197 | expect(router.url('/account/:accountId([0-9]+)?')).toBe('/account/'); 198 | expect(router.url( 199 | '/pay/:mode(bill|person|edit)?/:billOrPaymentId([-a-fA-F0-9]{36})?', 200 | {'mode': 'bill'} 201 | )).toBe('/pay/bill/'); 202 | }); 203 | 204 | it('should combine multiple slashes', () => { 205 | const url = router.url('/account/:fromAccountId([0-9]+)?/:toAccountId([0-9]+)?/'); 206 | expect(url).toBe('/account/'); 207 | }); 208 | }); 209 | 210 | describe('go()', () => { 211 | beforeEach(() => { 212 | vi.spyOn(router.page, JSCompiler_renameProperty('show', router.page)). 213 | mockImplementation(async (path) => new Context(path)); 214 | }); 215 | 216 | it('should navigate to the given path', () => { 217 | router.go('/A'); 218 | expect(router.page.show).toHaveBeenCalledWith('/A'); 219 | }); 220 | 221 | it('should replace path parameters with given values', () => { 222 | router.go('/account/:accountId([-a-fA-F0-9]{36})/documents/:docId', { 223 | 'accountId': '1234', 224 | 'docId': '6789', 225 | }); 226 | expect(router.page.show).toHaveBeenCalledWith('/account/1234/documents/6789'); 227 | }); 228 | it('should resolve to a Context', async () => { 229 | const rp = await router.go('/A'); 230 | expect(rp instanceof Context); 231 | }); 232 | }); 233 | 234 | 235 | 236 | describe('query context', () => { 237 | afterEach(() => { 238 | // Remove the callback added in the test. 239 | router.page.callbacks.pop(); 240 | }); 241 | 242 | it('should be an empty object if there are no query parameters', (done) => { 243 | router.page.register('/test', (context, next) => { 244 | expect(context.query).toBeInstanceOf(URLSearchParams); 245 | expect(Array.from(context.query.keys())).toEqual([]); 246 | done(); 247 | }); 248 | router.go('/test'); 249 | }); 250 | 251 | it('should have properties that match the query parameters', (done) => { 252 | router.page.register('/test', (context, next) => { 253 | expect(context.query.get('foo')).toBe('bar'); 254 | expect(context.query.get('noValue')).toBe(''); 255 | done(); 256 | }); 257 | router.go('/test?foo=bar&noValue'); 258 | }); 259 | }); 260 | 261 | describe('routeEnter', () => { 262 | it('should create the next element when it does not exist', async () => { 263 | const context = new Context('/B/somedata'); 264 | context.params['bData'] = 'somedata'; 265 | A.getValue().element = document.createElement(testRouteTree.Id.A); 266 | B.getValue().element = undefined; 267 | await A.getValue().element.routeEnter(A, B, testRouteTree.Id.B, context); 268 | expect(B.getValue().element.tagName.toLowerCase()).toBe(testRouteTree.Id.B); 269 | }); 270 | 271 | it('registered attributes should be assigned as hyphenated properties', async () => { 272 | const context = new Context('/B/somedata'); 273 | context.params['bData'] = 'somedata'; 274 | A.getValue().element = document.createElement(testRouteTree.Id.A); 275 | B.getValue().element = undefined; 276 | await A.getValue().element.routeEnter(A, B, testRouteTree.Id.B, context); 277 | expect(B.getValue().element.getAttribute('b-data')).toBe('somedata'); 278 | }); 279 | 280 | it('undefined routing properties should clear associated attribute', async () => { 281 | const context = new Context('/B/somedata'); 282 | context.params['bData'] = undefined; 283 | A.getValue().element = document.createElement(testRouteTree.Id.A); 284 | B.getValue().element = undefined; 285 | await A.getValue().element.routeEnter(A, B, testRouteTree.Id.B, context); 286 | expect(B.getValue().element.getAttribute('b-data')).toBe(null); 287 | }); 288 | 289 | it('should reuse an element when it already exists on the next node', async () => { 290 | const context = new Context('/B/somedata'); 291 | context.params['bData'] = 'somedata'; 292 | const aElement = document.createElement(testRouteTree.Id.A); 293 | A.getValue().element = aElement; 294 | const bElement = document.createElement(testRouteTree.Id.B); 295 | B.getValue().element = bElement; 296 | aElement.appendChild(bElement); 297 | await A.getValue().element.routeEnter(A, B, testRouteTree.Id.B, context); 298 | expect(B.getValue().element).toBe(bElement); 299 | }); 300 | 301 | it('when reusing an element, it should still update the attributes', async () => { 302 | const context = new Context('/B/bar'); 303 | context.params['bData'] = 'bar'; 304 | const aElement = document.createElement(testRouteTree.Id.A); 305 | A.getValue().element = aElement; 306 | const bElement = document.createElement(testRouteTree.Id.B); 307 | bElement.setAttribute('b-data', 'foo'); 308 | B.getValue().element = bElement; 309 | aElement.appendChild(bElement); 310 | await RoutedElement.prototype.routeEnter(A, B, testRouteTree.Id.B, context); 311 | expect(B.getValue().element.getAttribute('b-data')).toBe('bar'); 312 | }); 313 | }); 314 | 315 | describe('routeExit', () => { 316 | it('should remove the element from the routing tree', async () => { 317 | const context = new Context('/D/E'); 318 | A.getValue().element = document.createElement(testRouteTree.Id.A); 319 | B.getValue().element = document.createElement(testRouteTree.Id.B); 320 | await B.getValue().element.routeExit(B, A, testRouteTree.Id.E, context); 321 | expect(B.getValue().element).toBe(undefined); 322 | }); 323 | 324 | it('should remove the element from the parent node', async () => { 325 | const context = new Context('/D/E'); 326 | A.getValue().element = document.createElement(testRouteTree.Id.A); 327 | const bElement = document.createElement(testRouteTree.Id.B); 328 | B.getValue().element = bElement; 329 | A.getValue().element.appendChild(bElement); 330 | await A.getValue().element.routeExit(B, A, testRouteTree.Id.E, context); 331 | expect(A.getValue().element.getElementsByTagName(testRouteTree.Id.B).length).toBe(0); 332 | }); 333 | }); 334 | }); 335 | -------------------------------------------------------------------------------- /test/utils/test-route-config.js: -------------------------------------------------------------------------------- 1 | const testRouteConfig = { 2 | id: 'app', 3 | tagName: 'APP-MAIN', 4 | path: '', 5 | subRoutes: [{ 6 | id: 'app-user', 7 | tagName: 'APP-USER-PAGE', 8 | path: '/users/:userId([0-9]{1,6})', 9 | params: ['userId'], 10 | beforeEnter: () => Promise.resolve(), 11 | }, { 12 | id: 'app-user-account', 13 | tagName: 'APP-ACCOUNT-PAGE', 14 | path: '/users/:userId([0-9]{1,6})/accounts/:accountId([0-9]{1,6})', 15 | params: ['userId', 'accountId'], 16 | }, { 17 | id: 'app-about', 18 | tagName: 'APP-ABOUT', 19 | path: '/about', 20 | authenticated: false, 21 | }] 22 | }; 23 | 24 | export default testRouteConfig; -------------------------------------------------------------------------------- /test/utils/testing-route-setup.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview 3 | * 4 | * Setup a routing tree for tests 5 | * 6 | * Route tree used for tests 7 | * 8 | * _Root_ 9 | * / \ 10 | * A D 11 | * / \ \ 12 | * B C E 13 | * 14 | * A is an abstract route (zero length path) 15 | * 16 | * B does not directly require authentication, but A does - so B should 17 | * effectively require authentication. 18 | */ 19 | 20 | import {RouteData, RouteTreeNode, routingMixin} from '../../router.js'; 21 | 22 | /** @enum {string} */ 23 | const RouteId = { 24 | ROOT: 'tests-root', 25 | A: 'tests-a', 26 | B: 'tests-b', 27 | C: 'tests-c', 28 | D: 'tests-d', 29 | E: 'tests-e' 30 | }; 31 | 32 | /** 33 | * @constructor 34 | * @extends {HTMLElement} 35 | * @implements {RoutingMixin.Type} 36 | */ 37 | const RoutedElement = routingMixin(HTMLElement); 38 | 39 | for (const routeId in RouteId) { 40 | if (RouteId.hasOwnProperty(routeId)) { 41 | class routeElem extends RoutedElement {} 42 | customElements.define(RouteId[routeId], routeElem); // eslint-disable-line @banno/ux/custom-elements-define 43 | } 44 | } 45 | 46 | const E = new RouteTreeNode(new RouteData(RouteId.E, RouteId.E, '/D/E')); 47 | const D = new RouteTreeNode(new RouteData(RouteId.D, RouteId.D, '/D')); 48 | D.addChild(E); 49 | const C = new RouteTreeNode(new RouteData(RouteId.C, RouteId.C, '/C')); 50 | const B = new RouteTreeNode(new RouteData(RouteId.B, RouteId.B, '/B/:bData', ['bData'], false)); 51 | const A = new RouteTreeNode(new RouteData(RouteId.A, RouteId.A, '')); 52 | A.addChild(B); 53 | A.addChild(C); 54 | const ROOT = new RouteTreeNode(new RouteData(RouteId.ROOT, RouteId.ROOT, '/', [], false)); 55 | ROOT.addChild(A); 56 | ROOT.addChild(D); 57 | 58 | export default { 59 | tree: ROOT, 60 | Id: RouteId 61 | }; 62 | -------------------------------------------------------------------------------- /tsconfig.buildts.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "noEmit": false, 6 | "emitDeclarationOnly": false, 7 | }, 8 | "include": ["lib/routing.mixin.ts", "lib/animated-routing.mixin.ts"], 9 | } 10 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esNext", 4 | "module": "NodeNext", 5 | "lib": ["es2015", "dom", "es2016.array.include", "ES2017.Object"], 6 | "allowJs": true, 7 | "checkJs": true, 8 | "noEmit": false, 9 | "declaration": true, 10 | "emitDeclarationOnly": true, 11 | "moduleResolution": "nodenext", 12 | "noImplicitThis": true, 13 | "noUnusedParameters": false, 14 | "typeRoots": ["types", "node_modules/@types"] 15 | }, 16 | "include": ["types/*.ts", "lib/**/*.js", "*.js"], 17 | "exclude": ["node_modules", "test"] 18 | } 19 | -------------------------------------------------------------------------------- /types/index.d.ts: -------------------------------------------------------------------------------- 1 | export {}; 2 | 3 | declare global { 4 | interface HTMLElement { 5 | connectedCallback(): void; 6 | disconnectedCallback(): void; 7 | adoptedCallback(): void; 8 | attributeChangedCallback(name: string, oldValue: any, newValue: any); 9 | } 10 | interface CustomElementConstructor { 11 | connectedCallback(): void; 12 | disconnectedCallback(): void; 13 | adoptedCallback(): void; 14 | attributeChangedCallback(name: string, oldValue: any, newValue: any); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | 3 | export default defineConfig((env) => ({ 4 | cacheDir: '.vitest/web-component-router', 5 | build: {}, 6 | optimzeDeps: { 7 | include: ['path-to-regexp'], 8 | }, 9 | test: { 10 | include: ["test/**/*-spec.js"], 11 | browser: { 12 | provider: 'playwright', 13 | enabled: true, 14 | headless: !!process.env.CI, 15 | name: 'chromium', 16 | }, 17 | globals: true, 18 | coverage: { 19 | enabled: false, 20 | all: true, 21 | provider: 'v8', 22 | reporter: ['lcov', 'json', 'json-summary', 'html'], 23 | include: [], 24 | exclude: [], 25 | lines: 80, 26 | branches: 80, 27 | functions: 80, 28 | statements: 80, 29 | }, 30 | }, 31 | })); 32 | --------------------------------------------------------------------------------